diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..c061c95fff --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,31 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: "monthly" + groups: + "github actions": + patterns: + - "*" + - package-ecosystem: pip + directory: "/" + schedule: + interval: "monthly" + open-pull-requests-limit: 10 + groups: + pytest: + patterns: + - "pytest*" + - "hypothesis" + pyinstaller: + patterns: + - "pyinstaller*" + mypy: + patterns: + - "types-*" + - "mypy" + openssl: + patterns: + - "pyopenssl" + - "cryptography" diff --git a/.github/node-version.txt b/.github/node-version.txt index 8351c19397..3c032078a4 100644 --- a/.github/node-version.txt +++ b/.github/node-version.txt @@ -1 +1 @@ -14 +18 diff --git a/.github/python-version.txt b/.github/python-version.txt index 2c0733315e..e4fba21835 100644 --- a/.github/python-version.txt +++ b/.github/python-version.txt @@ -1 +1 @@ -3.11 +3.12 diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml new file mode 100644 index 0000000000..c2ae2daec4 --- /dev/null +++ b/.github/workflows/autofix.yml @@ -0,0 +1,40 @@ +name: autofix.ci + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + autofix: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version-file: .github/python-version.txt + - run: pip install -e .[dev] + - run: ruff check --fix-only . + - run: ruff format . + + - run: web/gen/all + + - uses: actions/setup-node@v4 + with: + node-version-file: .github/node-version.txt + - run: npm ci + working-directory: web + - run: npm run eslint + working-directory: web + continue-on-error: true + - run: npm run prettier + working-directory: web + + - uses: mhils/add-pr-ref-in-changelog@main + + - uses: autofix-ci/action@dd55f44df8f7cdb7a6bf74c78677eb8acd40cd0a diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 93b4d32967..0b7a709d1f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,89 +1,68 @@ name: CI -on: [ push, pull_request, workflow_dispatch ] +on: + push: + branches: + - '**' + - '!dependabot/**' + - '!*-patch-*' + pull_request: + merge_group: + workflow_dispatch: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - lint-pr: - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - persist-credentials: false - - uses: actions/setup-python@v4 - with: - python-version-file: .github/python-version.txt - - uses: TrueBrain/actions-flake8@c2deca24d388aa5aedd6478332aa9df4600b5eac # v2.1 - # mirrored at https://github.com/mitmproxy/mitmproxy/settings/actions - lint-local: - if: github.event_name == 'push' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - persist-credentials: false - - uses: actions/setup-python@v4 - with: - python-version-file: .github/python-version.txt - - run: pip install tox - - run: tox -e flake8 + lint: + uses: mhils/workflows/.github/workflows/python-tox.yml@v11 + with: + cmd: tox -e lint || true + filename-matching: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - persist-credentials: false - - uses: actions/setup-python@v4 - with: - python-version-file: .github/python-version.txt - - run: pip install tox + uses: mhils/workflows/.github/workflows/python-tox.yml@v11 + with: + cmd: tox -e filename_matching || true + mypy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - persist-credentials: false - - uses: actions/setup-python@v4 - with: - python-version-file: .github/python-version.txt - - run: pip install tox - - run: tox -e mypy + uses: mhils/workflows/.github/workflows/python-tox.yml@v11 + with: + cmd: tox -e mypy || true + individual-coverage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - persist-credentials: false - fetch-depth: 0 - - uses: actions/setup-python@v4 - with: - python-version-file: .github/python-version.txt - - run: pip install tox - - run: tox -e individual_coverage + uses: mhils/workflows/.github/workflows/python-tox.yml@v11 + with: + cmd: tox -e individual_coverage || true + test: strategy: fail-fast: false matrix: include: - os: ubuntu-latest - py: "3.11" + py: "3.13" - os: windows-latest - py: "3.11" + py: "3.13" - os: macos-latest + py: "3.13" + - os: ubuntu-latest + py: "3.12" + - os: ubuntu-latest py: "3.11" - os: ubuntu-latest py: "3.10" runs-on: ${{ matrix.os }} steps: - run: printenv - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.py }} - run: pip install tox @@ -96,64 +75,122 @@ jobs: # run tests with loopback only. We need to sudo for unshare, which means we need an absolute path for tox. sudo unshare --net -- sh -c "ip link set lo up; $(which tox) -e py" if: matrix.os == 'ubuntu-latest' - - uses: codecov/codecov-action@a1ed4b322b4b38cb846afb5a0ebfa17086917d27 - # mirrored below and at https://github.com/mitmproxy/mitmproxy/settings/actions + - uses: codecov/codecov-action@v4 with: - file: ./coverage.xml - name: ${{ matrix.os }} + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml + + test-old-dependencies: + if: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version-file: .github/python-version.txt + - run: pip install tox-uv + - run: tox -e old-dependencies build: strategy: fail-fast: false matrix: include: - - image: macos-11 - platform: macos + - image: macos-14 + platform: macos-arm64 + - image: macos-13 + platform: macos-x86_64 - image: windows-2019 platform: windows - image: ubuntu-20.04 # Oldest available version so we get oldest glibc possible. platform: linux runs-on: ${{ matrix.image }} - env: - CI_BUILD_KEY: ${{ secrets.CI_BUILD_KEY }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version-file: .github/python-version.txt - - if: matrix.platform == 'windows' - uses: actions/cache@v3 - with: - path: release/installbuilder/setup - key: installbuilder - - run: pip install -e .[dev] - - run: python -u release/build.py standalone-binaries + - run: pip install .[dev] # pyinstaller 5.9 does not like pyproject.toml + editable installs. + + - if: startsWith(matrix.platform, 'macos') && github.repository == 'mitmproxy/mitmproxy' + && (startsWith(github.ref, 'refs/heads/') || startsWith(github.ref, 'refs/tags/')) + id: keychain + uses: apple-actions/import-codesign-certs@63fff01cd422d4b7b855d40ca1e9d34d2de9427d + with: + keychain: ${{ runner.temp }}/temp + p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }} + p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + - if: startsWith(matrix.platform, 'macos') && github.repository == 'mitmproxy/mitmproxy' + && (startsWith(github.ref, 'refs/heads/') || startsWith(github.ref, 'refs/tags/')) + run: | + python -u release/build.py macos-app \ + --keychain "${{ runner.temp }}/temp.keychain" \ + --team-id "S8XHQB96PW" \ + --apple-id "${{ secrets.APPLE_ID }}" \ + --password "${{ secrets.APPLE_APP_PASSWORD }}" + + # Linux - if: matrix.platform == 'linux' - run: python -u release/build.py --dirty wheel - - if: matrix.platform == 'windows' && github.repository == 'mitmproxy/mitmproxy' && github.event_name == 'push' - run: python -u release/build.py --dirty installbuilder-installer msix-installer - - uses: actions/upload-artifact@v3 + run: python -u release/build.py standalone-binaries wheel + + # Windows + - if: matrix.platform == 'windows' + run: python -u release/build.py standalone-binaries + + - uses: actions/upload-artifact@v4 with: - # artifacts must have different names, see https://github.com/actions/upload-artifact/issues/24 name: binaries.${{ matrix.platform }} - path: | - release/dist + path: release/dist + + build-wheel: + uses: mhils/workflows/.github/workflows/python-build.yml@v11 + with: + python-version-file: .github/python-version.txt + artifact: binaries.wheel + + build-windows-installer: + runs-on: windows-latest + if: github.repository == 'mitmproxy/mitmproxy' && ( + github.ref == 'refs/heads/main' || + github.ref == 'refs/heads/citest' || + startsWith(github.ref, 'refs/tags/') + ) + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version-file: .github/python-version.txt + + - run: pip install .[dev] # pyinstaller 5.9 does not like pyproject.toml + editable installs. + - run: python -u release/build.py installbuilder-installer msix-installer + env: + CI_BUILD_KEY: ${{ secrets.CI_BUILD_KEY }} + + - uses: actions/upload-artifact@v4 + with: + name: binaries.windows-installer + path: release/dist test-web-ui: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false - - run: git rev-parse --abbrev-ref HEAD - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version-file: .github/node-version.txt - name: Cache Node.js modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: # npm cache files are stored in `~/.npm` on Linux/macOS path: ~/.npm @@ -165,19 +202,34 @@ jobs: run: npm ci - working-directory: ./web run: npm test - - uses: codecov/codecov-action@a1ed4b322b4b38cb846afb5a0ebfa17086917d27 - # mirrored above and at https://github.com/mitmproxy/mitmproxy/settings/actions + - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./web/coverage/coverage-final.json + + test-docker: + runs-on: ubuntu-latest + needs: build-wheel + steps: + - uses: actions/checkout@v4 with: - file: ./web/coverage/coverage-final.json - name: web + persist-credentials: false + - uses: actions/download-artifact@v4 + with: + name: binaries.wheel + path: release/docker + - name: Build container + run: docker build --tag localtesting release/docker + - name: Test container + run: docker run --rm -v $PWD/release:/release localtesting mitmdump -s /release/selftest.py docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version-file: .github/python-version.txt - run: | @@ -186,7 +238,7 @@ jobs: sudo dpkg -i hugo*.deb - run: pip install -e .[dev] - run: ./docs/build.py - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: docs path: docs/public @@ -196,54 +248,110 @@ jobs: env: DOCS_ARCHIVE: true - if: startsWith(github.ref, 'refs/tags/') - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: docs-archive path: docs/public + check: + if: always() + needs: +# - lint +# - filename-matching +# - mypy +# - individual-coverage + - test + - test-docker +# - test-old-dependencies + - test-web-ui + - build + - build-wheel + - build-windows-installer + - docs + uses: mhils/workflows/.github/workflows/alls-green.yml@v11 + with: + jobs: ${{ toJSON(needs) }} + allowed-skips: build-windows-installer + # Separate from everything else because slow. - build-and-deploy-docker: + deploy-docker: if: github.repository == 'mitmproxy/mitmproxy' && ( - github.ref == 'refs/heads/main' - || github.ref == 'refs/heads/citest' - || startsWith(github.ref, 'refs/tags/') + github.ref == 'refs/heads/main' || + github.ref == 'refs/heads/citest' || + startsWith(github.ref, 'refs/tags/') ) + permissions: + id-token: write + attestations: write + packages: write environment: deploy-docker - needs: - - test - - test-web-ui - - build - - docs + needs: check runs-on: ubuntu-latest - env: - DOCKER_USERNAME: mitmbot - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false - - uses: actions/setup-python@v4 + - uses: actions/download-artifact@v4 with: - python-version-file: .github/python-version.txt - - uses: actions/download-artifact@v2 + name: binaries.wheel + path: release/docker + - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 + - uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v1.6.0 + + - name: Login to Docker Hub + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: - name: binaries.linux - path: release/dist - - uses: docker/setup-qemu-action@27d0a4f181a40b142cce983c5393082c365d1480 # v1.2.0 - - uses: docker/setup-buildx-action@b1f1f719c7cd5364be7c82e366366da322d01f7c # v1.6.0 - - run: python release/build-and-deploy-docker.py + username: mitmbot + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Login to GitHub Container Registry + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 + env: + DOCKER_METADATA_ANNOTATIONS_LEVELS: index + with: + images: | + mitmproxy/mitmproxy + ghcr.io/mitmproxy/mitmproxy + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=dev,enable=${{ github.ref == 'refs/heads/main' }} + type=raw,value=citest,enable=${{ github.ref == 'refs/heads/citest' }} + + - name: Build and push + id: push + uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 + with: + context: release/docker + platforms: linux/amd64,linux/arm64 + push: true + provenance: false + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + annotations: ${{ steps.meta.outputs.annotations }} + - uses: actions/attest-build-provenance@v1 + with: + subject-name: ghcr.io/${{ github.repository }} + subject-digest: ${{ steps.push.outputs.digest }} deploy: # This action has access to our AWS keys, so we are extra careful here. # In particular, we don't blindly `pip install` anything to minimize the risk of supply chain attacks. if: github.repository == 'mitmproxy/mitmproxy' && (startsWith(github.ref, 'refs/heads/') || startsWith(github.ref, 'refs/tags/')) environment: ${{ (github.ref == 'refs/heads/citest' || startsWith(github.ref, 'refs/tags/')) && 'deploy-release' || 'deploy-snapshot' }} - needs: - - test - - test-web-ui - - build - - docs + needs: check runs-on: ubuntu-latest + permissions: + id-token: write + attestations: write env: # PyPI and MSFT keys are only available for the deploy-release environment # The AWS access key for snapshots is scoped to branches/* as well. @@ -256,11 +364,14 @@ jobs: MSFT_TENANT_ID: ${{ secrets.MSFT_TENANT_ID }} MSFT_CLIENT_ID: ${{ secrets.MSFT_CLIENT_ID }} MSFT_CLIENT_SECRET: ${{ secrets.MSFT_CLIENT_SECRET }} + R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} + R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version-file: .github/python-version.txt - run: sudo apt-get update @@ -268,27 +379,27 @@ jobs: - if: startsWith(github.ref, 'refs/tags/') run: sudo apt-get install -y twine - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: docs path: docs/public - if: startsWith(github.ref, 'refs/tags/') - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: docs-archive path: docs/archive - - uses: actions/download-artifact@v3 - with: - name: binaries.windows - path: release/dist - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: - name: binaries.linux + pattern: binaries.* + merge-multiple: true path: release/dist - - uses: actions/download-artifact@v3 + - id: provenance + uses: actions/attest-build-provenance@v1 with: - name: binaries.macos - path: release/dist + subject-path: 'release/dist/*' + - run: | + REF=${{ github.ref_name }} + mv ${{ steps.provenance.outputs.bundle-path }} release/dist/mitmproxy-${REF#v}.sigstore - run: ls docs/public - run: ls release/dist diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e32fd6d1a5..161d80e7b4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,13 +22,13 @@ jobs: environment: deploy-release runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: token: ${{ secrets.GH_PUSH_TOKEN }} # this token works to push to the protected main branch. - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version-file: .github/node-version.txt - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version-file: .github/python-version.txt - run: ./release/release.py ${{ inputs.version }} ${{ inputs.skip-branch-status-check }} diff --git a/.gitignore b/.gitignore index 2a6f5eb373..a5dfafcc27 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ MANIFEST **/tmp /venv* +/.venv* *.py[cdo] *.swp *.swo diff --git a/CHANGELOG.md b/CHANGELOG.md index 5514361627..845a1c82ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,437 @@ # Release History + + ## Unreleased: mitmproxy next +- Stop sorting keys in JSON contentview + ([#7346](https://github.com/mitmproxy/mitmproxy/pull/7346), @injust) + +## 24 November 2024: mitmproxy 11.0.1 + +- Tighten HTTP detection heuristic to better support custom TCP-based protocols. + ([#7228](https://github.com/mitmproxy/mitmproxy/pull/7228), @fatanugraha) +- Implement stricter validation of HTTP headers to harden against request smuggling attacks. + ([#7345](https://github.com/mitmproxy/mitmproxy/issues/7345), @mhils) +- Increase HTTP/2 default flow control window size, fixing performance issues. + ([#7317](https://github.com/mitmproxy/mitmproxy/pull/7317), @sujaldev) +- Fix a bug where mitmproxy would incorrectly report that TLS 1.0 and 1.1 are not supported + with the current OpenSSL build. + ([#7241](https://github.com/mitmproxy/mitmproxy/pull/7241), @mhils) +- Docker: Update image to Python 3.13 on Debian Bookworm. + ([#7242](https://github.com/mitmproxy/mitmproxy/pull/7242), @mhils) +- Add a `tun` proxy mode that creates a virtual network device on Linux for transparent proxying. + ([#7278](https://github.com/mitmproxy/mitmproxy/pull/7278), @mhils) +- `browser.start` command now supports Firefox. + ([#7239](https://github.com/mitmproxy/mitmproxy/pull/7239), @sujaldev) +- Fix interaction of the `modify_headers` and `stream_large_bodies` options. + This may break users of `modify_headers` that rely on filters referencing the message body. + We expect this to be uncommon, but please make yourself heard if that's not the case. + ([#7286](https://github.com/mitmproxy/mitmproxy/pull/7286), @lukant) +- Fix a crash when handling corrupted compressed body in savehar addon and its tests. + ([#7320](https://github.com/mitmproxy/mitmproxy/pull/7320), @8192bytes) +- Remove dependency on `protobuf` library as it was no longer being used. + ([#7327](https://github.com/mitmproxy/mitmproxy/pull/7327), @matthew16550) + +## 02 October 2024: mitmproxy 11.0.0 + +- mitmproxy now supports transparent HTTP/3 proxying. + ([#7202](https://github.com/mitmproxy/mitmproxy/pull/7202), @errorxyz, @meitinger, @mhils) +- Add HTTP3 support in HTTPS reverse-proxy mode. + ([#7114](https://github.com/mitmproxy/mitmproxy/pull/7114), @errorxyz) +- mitmproxy now officially supports Python 3.13. + ([#6934](https://github.com/mitmproxy/mitmproxy/pull/6934), @mhils) +- Tighten HTTP detection heuristic to better support custom TCP-based protocols. + ([#7087](https://github.com/mitmproxy/mitmproxy/pull/7087)) +- Add `show_ignored_hosts` option to display ignored flows in the UI. + This option is implemented as a temporary workaround and will be removed in the future. + ([#6720](https://github.com/mitmproxy/mitmproxy/pull/6720), @NicolaiSoeborg) +- Fix slow tnetstring parsing in case of very large tnetstring. + ([#7121](https://github.com/mitmproxy/mitmproxy/pull/7121), @mik1904) +- Add `getaddrinfo`-based fallback for DNS resolution if we are unable to + determine the operating system's name servers. + ([#7122](https://github.com/mitmproxy/mitmproxy/pull/7122), @mhils) +- Improve the error message when users specify the `certs` option without a matching private key. + ([#7073](https://github.com/mitmproxy/mitmproxy/pull/7073), @mhils) +- Fix a bug where intermediate certificates would not be transmitted when using QUIC. + ([#7073](https://github.com/mitmproxy/mitmproxy/pull/7073), @mhils) +- Fix a bug where fragmented QUIC client hellos were not handled properly. + ([#7067](https://github.com/mitmproxy/mitmproxy/pull/7067), @errorxyz) +- Emit a warning when users configure a TLS version that is not supported by the + current OpenSSL build. + ([#7139](https://github.com/mitmproxy/mitmproxy/pull/7139), @mhils) +- Fix a bug where mitmproxy would crash when receiving `STOP_SENDING` QUIC frames. + ([#7119](https://github.com/mitmproxy/mitmproxy/pull/7119), @mhils) +- Fix error when unmarking all flows. + ([#7192](https://github.com/mitmproxy/mitmproxy/pull/7192), @bburky) +- Add addon to update the alt-svc header in reverse mode. + ([#7093](https://github.com/mitmproxy/mitmproxy/pull/7093), @errorxyz) +- Do not send unnecessary empty data frames when streaming HTTP/2. + ([#7196](https://github.com/mitmproxy/mitmproxy/pull/7196), @rubu) +- Fix a bug where mitmproxy would ignore Ctrl+C/SIGTERM on OpenBSD. + ([#7130](https://github.com/mitmproxy/mitmproxy/pull/7130), @catap) +- Fix of measurement unit in HAR import, duration is in milliseconds. + ([#7179](https://github.com/mitmproxy/mitmproxy/pull/7179), @dstd) +- `Connection.tls_version` now is `QUICv1` instead of `QUIC` for QUIC. + ([#7201](https://github.com/mitmproxy/mitmproxy/pull/7201), @mhils) +- Add support for full mTLS with client certs between client and mitmproxy. + ([#7175](https://github.com/mitmproxy/mitmproxy/pull/7175), @Kriechi) +- Update documentation adding a list of all possibile web_columns. + ([#7205](https://github.com/mitmproxy/mitmproxy/pull/7205), @lups2000, @Abhishek-Bohora) + +## 02 August 2024: mitmproxy 10.4.2 + +- Fix a crash on startup when mitmproxy is unable to determine the OS' DNS servers + ([#7066](https://github.com/mitmproxy/mitmproxy/pull/7066), @errorxyz) + +## 29 July 2024: mitmproxy 10.4.1 + +- Fix a bug where macOS local mode would not start up on macOS. + ([#7045](https://github.com/mitmproxy/mitmproxy/pull/7045), @mhils) +- Fix UDP error handling when we learn that the remote has disconnected. + ([#7045](https://github.com/mitmproxy/mitmproxy/pull/7045), @mhils) +- Container images are now published to both Docker Hub and GitHub Container Registry. + ([#7061](https://github.com/mitmproxy/mitmproxy/pull/7061), @mhils) + +## 25 July 2024: mitmproxy 10.4.0 + +* Add support for DNS over TCP. + ([#6935](https://github.com/mitmproxy/mitmproxy/pull/6935), @errorxyz) +* Add first MVP new Capture Tab in mitmweb + ([#6999](https://github.com/mitmproxy/mitmproxy/pull/6999), @lups2000) +* Add `HttpConnectedHook` and `HttpConnectErrorHook`. + ([#6930](https://github.com/mitmproxy/mitmproxy/pull/6930), @errorxyz) +* Fix non-linear growth in processing time for large HTTP bodies. + ([#6952](https://github.com/mitmproxy/mitmproxy/pull/6952), @jackfromeast) +* Fix a bug where connections would be incorrectly ignored with `allow_hosts`. + ([#7002](https://github.com/mitmproxy/mitmproxy/pull/7002), @JarLob, @mhils) +* Fix zstd decompression to read across frames. + ([#6921](https://github.com/mitmproxy/mitmproxy/pull/6921), @zendai) +* Handle certificates we cannot parse more gracefully. + ([#6994](https://github.com/mitmproxy/mitmproxy/pull/6994), @mhils) +* Parse compressed domain names in ResourceRecord data. + ([#6954](https://github.com/mitmproxy/mitmproxy/pull/6954), @errorxyz) +* Fix a bug where mitmweb's flow list would not stay at the bottom. + ([#7008](https://github.com/mitmproxy/mitmproxy/pull/7008), @mhils) +* Fix a bug where SSH connections would be incorrectly handled as HTTP. + ([#7041](https://github.com/mitmproxy/mitmproxy/pull/7041), @mhils) +* Skip UTF-8 byte-order marks (BOM) when loading HAR files. + ([#6897](https://github.com/mitmproxy/mitmproxy/pull/6897), @dstd) +* Allow `typing.Sequence[str]` to be an editable option. + ([#7001](https://github.com/mitmproxy/mitmproxy/pull/7001), @errorxyz) +* Add Host header to CONNECT requests. + ([#7021](https://github.com/mitmproxy/mitmproxy/pull/7021), @petsneakers) +* Support all query types in DNS mode. + ([#6975](https://github.com/mitmproxy/mitmproxy/pull/6975), @errorxyz) +* Fix a bug where mitmproxy would crash for pipelined HTTP flows. + ([#7031](https://github.com/mitmproxy/mitmproxy/pull/7031), @gdiepen, @mhils) +* Add an optional "index" column for mitmweb. + ([#7039](https://github.com/mitmproxy/mitmproxy/pull/7039), @mhils) + +## 12 June 2024: mitmproxy 10.3.1 + +* Release tags are now prefixed with `v` again. + ([#6810](https://github.com/mitmproxy/mitmproxy/pull/6810), @mhils) +* Fix a bug where mitmproxy would not exit when `-n` is passed. + ([#6819](https://github.com/mitmproxy/mitmproxy/pull/6819), @mhils) +* Set the `unbuffered` (stdout/stderr) flag for the `mitmdump` PyInstaller build. + ([#6821](https://github.com/mitmproxy/mitmproxy/pull/6821), @Prinzhorn) +* Fix a bug where client replay would not work with proxyauth. + ([#6866](https://github.com/mitmproxy/mitmproxy/pull/6866), @mhils) +* Fix slowdown when sending large amounts of data over HTTP/2. + ([#6875](https://github.com/mitmproxy/mitmproxy/pull/6875), @aib) +* Add an option to strip HTTPS records from DNS responses to block encrypted ClientHellos. + ([#6876](https://github.com/mitmproxy/mitmproxy/pull/6876), @errorxyz) +* Add an API to parse HTTPS records from DNS RDATA. + ([#6884](https://github.com/mitmproxy/mitmproxy/pull/6884), @errorxyz) +* Fix flow export in mitmweb for Safari + ([#6917](https://github.com/mitmproxy/mitmproxy/pull/6917), @mhils, @canyesilyurt) +* Releases now come with a Sigstore attestations file to demonstrate build provenance. + ([f05c050](https://github.com/mitmproxy/mitmproxy/commit/f05c050f615b9ab9963707944c893bc94e738525), @mhils) + +## 17 April 2024: mitmproxy 10.3.0 + +* Add support for editing non text files in a hex editor + ([#6768](https://github.com/mitmproxy/mitmproxy/pull/6768), @wnyyyy) +* Add `server_connect_error` hook that is triggered when connection establishment fails. + ([#6806](https://github.com/mitmproxy/mitmproxy/pull/6806), @haanhvu, @spacewasp, @mhils) +* Add section in mitmweb for rendering, adding and removing a comment + ([#6709](https://github.com/mitmproxy/mitmproxy/pull/6709), @lups2000) +* Fix multipart form content view being unusable. + ([#6653](https://github.com/mitmproxy/mitmproxy/pull/6653), @DaniElectra) +* Documentation Improvements on CA Certificate Generation + ([#5370](https://github.com/mitmproxy/mitmproxy/pull/5370), @zioalex) +* Make it possible to read flows from stdin with mitmweb. + ([#6732](https://github.com/mitmproxy/mitmproxy/pull/6732), @jaywor1) +* Update aioquic dependency to >= 1.0.0, < 2.0.0. + ([#6747](https://github.com/mitmproxy/mitmproxy/pull/6747), @jlaine) +* Fix a bug where async `client_connected` handlers would crash mitmproxy. + ([#6749](https://github.com/mitmproxy/mitmproxy/pull/6749), @mhils) +* Add button to close flow details panel + ([#6734](https://github.com/mitmproxy/mitmproxy/pull/6734), @lups2000) +* Ignore SIGPIPE signals when there is lots of traffic. + Socket errors are handled directly and do not require extra signals + that generate noise. + ([#6764](https://github.com/mitmproxy/mitmproxy/pull/6764), @changsin) +* Add primitive websocket interception and modification + ([#6766](https://github.com/mitmproxy/mitmproxy/pull/6766), @errorxyz) +* Add support for exporting websocket messages when using "raw" export. + ([#6767](https://github.com/mitmproxy/mitmproxy/pull/6767), @txrp0x9) +* The "save body" feature now also includes WebSocket messages. + ([#6767](https://github.com/mitmproxy/mitmproxy/pull/6767), @txrp0x9) +* Fix compatibility with older cryptography versions and silence a DeprecationWarning on Python <3.11. + ([#6790](https://github.com/mitmproxy/mitmproxy/pull/6790), @mhils) +* Fix a bug when proxying unicode domains. + ([#6796](https://github.com/mitmproxy/mitmproxy/pull/6796), @mhils) + + +## 07 March 2024: mitmproxy 10.2.4 + +* Fix a bug where errors during startup would not be displayed when running mitmproxy. + ([#6719](https://github.com/mitmproxy/mitmproxy/pull/6719), @mhils) +* Use newer cryptography APIs to avoid CryptographyDeprecationWarnings. + This bumps the minimum required version to cryptography 42.0. + ([#6718](https://github.com/mitmproxy/mitmproxy/pull/6718), @mhils) + + +## 06 March 2024: mitmproxy 10.2.3 + +* Fix a regression where `allow_hosts`/`ignore_hosts` would break with IPv6 connections. + ([#6614](https://github.com/mitmproxy/mitmproxy/pull/6614), @dqxpb) +* Fix bug where failed CONNECT request URLs are saved to HAR files incorrectly. + ([#6599](https://github.com/mitmproxy/mitmproxy/pull/6599), @basedBaba) +* Add an arm64 variant for the precompiled macOS app. + ([#6633](https://github.com/mitmproxy/mitmproxy/pull/6633), @mhils) +* Fix duplicate answers being returned in DNS queries. + ([#6648](https://github.com/mitmproxymitmproxy/pull/6648), @sujaldev) +* Fix bug where wireguard config is generated with incorrect endpoint when two or more NICs are active. + ([#6659](https://github.com/mitmproxy/mitmproxy/pull/6659), @basedBaba) +* Fix a regression when leaf cert creation would fail with intermediate CAs in `ca_file`. + ([#6666](https://github.com/mitmproxy/mitmproxy/pull/6666), @manselmi) +* Add `content_view_lines_cutoff` option to mitmdump + ([#6692](https://github.com/mitmproxy/mitmproxy/pull/6692), @errorxyz) +* Allow runtime modifications of HTTP flow filters for server replays + ([#6695](https://github.com/mitmproxy/mitmproxy/pull/6695), @errorxyz) +* Fix bug view options menu in case of overflow + ([#6697](https://github.com/mitmproxy/mitmproxy/pull/6697), @lups2000) +* Allow --allow-hosts and --ignore-hosts to work together + ([#6711](https://github.com/mitmproxy/mitmproxy/pull/6711), @dstd) + + +## 21 January 2024: mitmproxy 10.2.2 + +* Fix a regression where clientplayback would break due to eager task execution. + ([#6605](https://github.com/mitmproxy/mitmproxy/pull/6605), @mhils) +* Fix a regression where WebSocket connections would break due to eager task execution. + ([#6609](https://github.com/mitmproxy/mitmproxy/pull/6609), @mhils) +* Fix bug where insecure HTTP requests are saved incorrectly when exporting to HAR files. + ([#6578](https://github.com/mitmproxy/mitmproxy/pull/6578), @DaniElectra) +* `allow_hosts`/`ignore_hosts` option now matches against the full `host:port` string. + ([#6594](https://github.com/mitmproxy/mitmproxy/pull/6594), @LouisAsanaka) + + +## 06 January 2024: mitmproxy 10.2.1 + +* Fix a regression introduced in mitmproxy 10.2.0: WireGuard servers + now bind to all interfaces again. + ([#6587](https://github.com/mitmproxy/mitmproxy/pull/6587), @mhils) +* Remove stale reference to `ctx.log` in addon documentation. + ([#6552](https://github.com/mitmproxy/mitmproxy/pull/6552), @brojonat) +* Fix a bug where a traceback is shown during shutdown. + ([#6581](https://github.com/mitmproxy/mitmproxy/pull/6581), @mhils) + + +## 04 January 2024: mitmproxy 10.2.0 + +* *Local Redirect Mode* is now officially available on + [macOS](https://mitmproxy.org/posts/local-redirect/macos/) + and [Windows](https://mitmproxy.org/posts/local-redirect/windows/). + See the linked blog posts for details. (@emanuele-em, @mhils) +* UDP streams are now backed by a new implementation in `mitmproxy_rs`. + This represents a major API change as UDP traffic is now exposed as streams + instead of a callback for each packet. (@mhils) +* Fix a regression from mitmproxy 10.1.6 where `ignore_hosts` would terminate requests + instead of forwarding them. + ([#6559](https://github.com/mitmproxy/mitmproxy/pull/6559), @mhils) +* `ignore_hosts` now waits for the entire HTTP headers if it suspects the connection to be HTTP. + ([#6559](https://github.com/mitmproxy/mitmproxy/pull/6559), @mhils) + + +## 14 December 2023: mitmproxy 10.1.6 + +* Fix compatibility with Windows Schannel clients, which previously got + confused by CA and leaf certificate sharing the same Subject Key Identifier. + ([#6549](https://github.com/mitmproxy/mitmproxy/pull/6549), @driuba and @mhils) +* Change keybinding for exporting flow from "e" to "x" to avoid conflict with "edit" keybinding. + ([#6225](https://github.com/mitmproxy/mitmproxy/issues/6225), @Llama1412) +* Fix bug where response flows from HAR files had incorrect `content-length` headers + ([#6548](https://github.com/mitmproxy/mitmproxy/pull/6548), @zanieb) +* Improved handling for `allow_hosts`/`ignore_hosts` options in WireGuard mode (#5930). + ([#6513](https://github.com/mitmproxy/mitmproxy/pull/6513), @dsphper) +* Fix a bug where TCP connections were not closed properly. + ([#6543](https://github.com/mitmproxy/mitmproxy/pull/6543), @mhils) +* DNS resolution is now exempted from `ignore_hosts` in WireGuard Mode. + ([#6513](https://github.com/mitmproxy/mitmproxy/pull/6513), @dsphper) +* Fix case sensitivity of URL added to blocklist + ([#6493](https://github.com/mitmproxy/mitmproxy/pull/6493), @emanuele-em) +* Fix a bug where logging was stopped prematurely during shutdown. + ([#6541](https://github.com/mitmproxy/mitmproxy/pull/6541), @mhils) +* For plaintext traffic, `ignore_hosts` now also takes HTTP/1 host headers into account. + ([#6513](https://github.com/mitmproxy/mitmproxy/pull/6513), @dsphper) +* Fix empty cookie attributes being set to `Key=` instead of `Key` + ([#5084](https://github.com/mitmproxy/mitmproxy/pull/5084), @Speedlulu) +* Scripts with relative paths are now loaded relative to the config file and not where the command is ran + ([#4860](https://github.com/mitmproxy/mitmproxy/pull/4860), @Speedlulu) +* Fix `mitmweb` splitter becoming drag and drop. + ([#6492](https://github.com/mitmproxy/mitmproxy/pull/6492), @xBZZZZ) +* Enhance documentation and add alert log messages when stream_large_bodies and modify_body are set + ([#6514](https://github.com/mitmproxy/mitmproxy/pull/6514), @rosydawn6) + +### Breaking Changes +* Subject Alternative Names are now represented as `cryptography.x509.GeneralNames` instead of `list[str]` + across the codebase. This fixes a regression introduced in mitmproxy 10.1.1 related to punycode domain encoding. + ([#6537](https://github.com/mitmproxy/mitmproxy/pull/6537), @mhils) + + +## 14 November 2023: mitmproxy 10.1.5 + +* Remove stray `replay-extra` from CLI status bar. + ([37d62ce](https://github.com/mitmproxy/mitmproxy/commit/37d62ce73ebd57780cff5ecf8b2ee57ec7d8ab30), @mhils) + + +## 13 November 2023: mitmproxy 10.1.4 + +* Fix a hang/freeze in the macOS distributions when doing TLS negotiation. + ([#6480](https://github.com/mitmproxy/mitmproxy/pull/6480), @mhils) +* Update savehar addon to fix creating corrupt har files caused by empty response content + ([#6459](https://github.com/mitmproxy/mitmproxy/pull/6459), @lain3d) +* Update savehar addon to handle scenarios where "path" key in cookie + attrs dict is missing. + ([#6458](https://github.com/mitmproxy/mitmproxy/pull/6458), @pogzyb) +* Add `server_replay_extra` option to serverplayback to define behaviour + when replayable response is missing. + ([#6465](https://github.com/mitmproxy/mitmproxy/pull/6465), @dkarandikar) + + +## 04 November 2023: mitmproxy 10.1.3 + +* Fix a bug introduced in mitmproxy 10.1.2 where mitmweb would fail to establish + a WebSocket connection. Affected users may need to clear their browser cache + or hard-reload mitmweb (Ctrl+Shift+R). + ([#6454](https://github.com/mitmproxy/mitmproxy/pull/6454), @mhils) + + +## 03 November 2023: mitmproxy 10.1.2 + +* Add a raw hex stream contentview. + ([#6389](https://github.com/mitmproxy/mitmproxy/pull/6389), @mhils) +* Add a contentview for DNS-over-HTTPS. + ([#6389](https://github.com/mitmproxy/mitmproxy/pull/6389), @mhils) +* Replaced standalone mitmproxy binaries on macOS with an app bundle + that contains the mitmproxy/mitmweb/mitmdump CLI tools. + This change was necessary to support macOS code signing requirements. + Homebrew remains the recommended installation method. + ([#6447](https://github.com/mitmproxy/mitmproxy/pull/6447), @mhils) +* Fix certificate generation to work with strict mode OpenSSL 3.x clients + ([#6410](https://github.com/mitmproxy/mitmproxy/pull/6410), @mmaxim) +* Fix path() documentation that the return value might include the query string + ([#6412](https://github.com/mitmproxy/mitmproxy/pull/6412), @tddschn) +* mitmproxy now officially supports Python 3.12. + ([#6434](https://github.com/mitmproxy/mitmproxy/pull/6434), @mhils) +* Fix root-relative URLs so that mitmweb can run in subdirectories. + ([#6411](https://github.com/mitmproxy/mitmproxy/pull/6411), @davet2001) +* Add an optional parameter(ldap search filter key) to ProxyAuth-LDAP. + ([#6428](https://github.com/mitmproxy/mitmproxy/pull/6428), @outlaws-bai) +* Fix a regression when using the proxyauth addon with clients that (rightfully) reuse connections. + ([#6432](https://github.com/mitmproxy/mitmproxy/pull/6432), @mhils) + + +## 27 September 2023: mitmproxy 10.1.1 + +* Fix certificate generation for punycode domains. + ([#6382](https://github.com/mitmproxy/mitmproxy/pull/6382), @mhils) +* Fix a bug that would crash mitmweb when opening options. + ([#6386](https://github.com/mitmproxy/mitmproxy/pull/6386), @mhils) + + +## 24 September 2023: mitmproxy 10.1.0 + +* Add support for reading HAR files using the existing flow loading APIs, e.g. `mitmproxy -r example.har`. + ([#6335](https://github.com/mitmproxy/mitmproxy/pull/6335), @stanleygvi) +* Add support for writing HAR files using the `save.har` command and the `hardump` option for mitmdump. + ([#6368](https://github.com/mitmproxy/mitmproxy/pull/6368), @stanleygvi) +* Packaging changes: + - `mitmproxy-rs` does not depend on a protobuf compiler being available anymore, + we're now also providing a working source distribution for all platforms. + - On macOS, `mitmproxy-rs` now depends on `mitmproxy-macos`. We only provide binary wheels for this package because + it contains a code-signed system extension. Building from source requires a valid Apple Developer Id, see CI for + details. + - On Windows, `mitmproxy-rs` now depends on `mitmproxy-windows`. We only provide binary wheels for this package to + simplify our deployment process, see CI for how to build from source. + + ([#6303](https://github.com/mitmproxy/mitmproxy/issues/6303), @mhils) +* Increase maximum dump file size accepted by mitmweb + ([#6373](https://github.com/mitmproxy/mitmproxy/pull/6373), @t-wy) + + +## 04 August 2023: mitmproxy 10.0.0 + +* Add experimental support for HTTP/3 and QUIC. + ([#5435](https://github.com/mitmproxy/mitmproxy/issues/5435), @meitinger) +* ASGI/WSGI apps can now listen on all ports for a specific hostname. + This makes it simpler to accept both HTTP and HTTPS. + ([#5725](https://github.com/mitmproxy/mitmproxy/pull/5725), @mhils) +* Add `replay.server.add` command for adding flows to server replay buffer + ([#5851](https://github.com/mitmproxy/mitmproxy/pull/5851), @italankin) +* Remove string escaping in raw view. + ([#5470](https://github.com/mitmproxy/mitmproxy/issues/5470), @stephenspol) +* Updating `Request.port` now also updates the Host header if present. + This aligns with `Request.host`, which already does this. + ([#5908](https://github.com/mitmproxy/mitmproxy/pull/5908), @sujaldev) +* Fix editing of multipart HTTP requests from the CLI. + ([#5148](https://github.com/mitmproxy/mitmproxy/issues/5148), @mhils) +* Add documentation on using Magisk module for intercepting traffic in Android production builds. + ([#5924](https://github.com/mitmproxy/mitmproxy/pull/5924), @Jurrie) +* Fix a bug where the direction indicator in the message stream view would be in the wrong direction. + ([#5921](https://github.com/mitmproxy/mitmproxy/issues/5921), @konradh) +* Fix a bug where peername would be None in tls_passthrough script, which would make it not working. + ([#5904](https://github.com/mitmproxy/mitmproxy/pull/5904), @truebit) +* the `esc` key can now be used to exit the current view + ([#6087](https://github.com/mitmproxy/mitmproxy/pull/6087), @sujaldev) +* focus-follow shortcut will now work in flow view context too. + ([#6088](https://github.com/mitmproxy/mitmproxy/pull/6088), @sujaldev) +* Fix a bug where a server connection timeout would cause requests to be issued with a wrong SNI in reverse proxy mode. + ([#6148](https://github.com/mitmproxy/mitmproxy/pull/6148), @mhils) +* The `server_replay_nopop` option has been renamed to `server_replay_reuse` to avoid confusing double-negation. + ([#6084](https://github.com/mitmproxy/mitmproxy/issues/6084), @prady0t, @Semnodime) +* Add zstd to valid gRPC encoding schemes. + ([#6188](https://github.com/mitmproxy/mitmproxy/pull/6188), @tsaaristo) +* For reverse proxy directly accessed via IP address, the IP address is now included + as a subject in the generated certificate. + ([#6202](https://github.com/mitmproxy/mitmproxy/pull/6202), @mhils) +* Enable legacy SSL connect when connecting to server if the `ssl_insecure` flag is set. + ([#6281](https://github.com/mitmproxy/mitmproxy/pull/6281), @DurandA) +* Change wording in the [http-reply-from-proxy.py example](https://github.com/mitmproxy/mitmproxy/blob/main/examples/addons/http-reply-from-proxy.py). + ([#6117](https://github.com/mitmproxy/mitmproxy/pull/6117), @Semnodime) +* Added option to specify an elliptic curve for key exchange between mitmproxy <-> server + ([#6170](https://github.com/mitmproxy/mitmproxy/pull/6170), @Mike-Ki-ASD) +* Add "Prettier" code linting tool to mitmweb. + ([#5985](https://github.com/mitmproxy/mitmproxy/pull/5985), @alexgershberg) +* When logging exceptions, provide the entire exception object to log handlers + ([#6295](https://github.com/mitmproxy/mitmproxy/pull/6295), @mhils) +* mitmproxy now requires Python 3.10 or above. + ([#5954](https://github.com/mitmproxy/mitmproxy/pull/5954), @mhils) + +### Breaking Changes + +* The `onboarding_port` option has been removed. The onboarding app now responds + to all requests for the hostname specified in `onboarding_host`. +* `connection.Client` and `connection.Server` now accept keyword arguments only. + This is a breaking change for custom addons that use these classes directly. ## 07 April 2023: mitmproxy 9.0.1 @@ -29,7 +458,7 @@ ([#5414](https://github.com/mitmproxy/mitmproxy/pull/5414), @meitinger) * Add WireGuard mode to enable transparent proxying via WireGuard. ([#5562](https://github.com/mitmproxy/mitmproxy/pull/5562), @decathorpe, @mhils) -* Add DTLS support. +* Add DTLS support. ([#5397](https://github.com/mitmproxy/mitmproxy/pull/5397), @kckeiks). * Add a quick help bar to mitmproxy. ([#5381](https://github.com/mitmproxy/mitmproxy/pull/5381/), [#5652](https://github.com/mitmproxy/mitmproxy/pull/5652), @kckeiks, @mhils). @@ -43,6 +472,12 @@ See [the docs](https://docs.mitmproxy.org/dev/addons-api-changelog/) for details and upgrade instructions. ([#5590](https://github.com/mitmproxy/mitmproxy/pull/5590), @mhils) +### Breaking Changes + + * The `mode` option is now a list of server specs instead of a single spec. + The CLI interface is unaffected, but users may need to update their `config.yaml`. + ([#5393](https://github.com/mitmproxy/mitmproxy/pull/5393), @mhils) + ### Full Changelog * Mitmproxy binaries now ship with Python 3.11. @@ -53,7 +488,7 @@ ([#5623](https://github.com/mitmproxy/mitmproxy/issues/5623), @SapiensAnatis) * Add MQTT content view. ([#5588](https://github.com/mitmproxy/mitmproxy/pull/5588), @nikitastupin, @abbbe) -* Setting `connection_strategy` to `lazy` now also disables early +* Setting `connection_strategy` to `lazy` now also disables early upstream connections to fetch TLS certificate details. ([#5487](https://github.com/mitmproxy/mitmproxy/pull/5487), @mhils) * Fix order of event hooks on startup. @@ -123,7 +558,7 @@ ([#4469](https://github.com/mitmproxy/mitmproxy/issues/4469), @mhils) * Add flatpak support to the browser addon ([#5200](https://github.com/mitmproxy/mitmproxy/issues/5200), @pauloromeira) -* Add example addon to dump contents to files based on a filter expression +* Add example addon to dump contents to files based on a filter expression ([#5190](https://github.com/mitmproxy/mitmproxy/issues/5190), @redraw) * Fix a bug where the wrong SNI is sent to an upstream HTTPS proxy ([#5109](https://github.com/mitmproxy/mitmproxy/issues/5109), @mhils) @@ -133,14 +568,14 @@ ([#5217](https://github.com/mitmproxy/mitmproxy/issues/5217), @randomstuff) * Improve cut addon to better handle binary contents ([#3965](https://github.com/mitmproxy/mitmproxy/issues/3965), @mhils) -* Fix text truncation for full-width characters +* Fix text truncation for full-width characters ([#4278](https://github.com/mitmproxy/mitmproxy/issues/4278), @kjy00302) * Fix mitmweb export copy failed in non-secure domain. ([#5264](https://github.com/mitmproxy/mitmproxy/issues/5264), @Pactortester) * Add example script for manipulating cookies. ([#5278](https://github.com/mitmproxy/mitmproxy/issues/5278), @WillahScott) -* When opening an external viewer for message contents, mailcap files are not considered anymore. - This preempts the upcoming deprecation of Python's `mailcap` module. +* When opening an external viewer for message contents, mailcap files are not considered anymore. + This preempts the upcoming deprecation of Python's `mailcap` module. ([#5297](https://github.com/mitmproxy/mitmproxy/issues/5297), @KORraNpl) * Fix hostname encoding for IDNA domains in upstream mode. ([#5316](https://github.com/mitmproxy/mitmproxy/issues/5316), @nneonneo) @@ -201,7 +636,7 @@ * Add ability to specify custom ports with LDAP authentication (#5068, @demonoidvk) * Add support for rotating saved streams every hour or day (@EndUser509) * Console Improvements on Windows (@mhils) -* Fix processing of `--set` options (#5067, @marwinxxii) +* Fix processing of `--set` options (#5067, @marwinxxii) * Lowercase user-added header names and emit a log message to notify the user when using HTTP/2 (#4746, @mhils) * Exit early if there are errors on startup (#4544, @mhils) * Fixed encoding guessing: only search for meta tags in HTML bodies (##4566, @Prinzhorn) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 21e2f12e2f..c1dc150096 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,8 +18,7 @@ forward, please consider contributing in the following areas: ## Development Setup -To get started hacking on mitmproxy, please install a recent version of Python (we require at least Python 3.9). -Then, do the following: +To get started hacking on mitmproxy, please install the latest version of Python and do the following: ##### Linux / macOS @@ -83,7 +82,7 @@ For speedier testing, you can also run [pytest](http://pytest.org/) directly on ```shell cd test/mitmproxy/addons -pytest --cov mitmproxy.addons.anticache --cov-report term-missing --looponfail test_anticache.py +pytest --looponfail test_anticache.py ``` Please ensure that all patches are accompanied by matching changes in the test suite. The project tries to maintain 100% @@ -96,7 +95,7 @@ Keeping to a consistent code style throughout the project makes it easier to con We enforce the following check for all PRs: ```shell -tox -e flake8 +tox -e lint ``` If a linting error is detected, the automated pull request checks will fail and block merging. diff --git a/README.md b/README.md index 38ee2f43ea..8640efbbe2 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ The standard MITMProxy documentation follows below. # mitmproxy [![Continuous Integration Status](https://github.com/mitmproxy/mitmproxy/workflows/CI/badge.svg?branch=main)](https://github.com/mitmproxy/mitmproxy/actions?query=branch%3Amain) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/a38b0325dfb944839c0c8da354f70b1b)](https://app.codacy.com/gh/mitmproxy/mitmproxy/dashboard) +[![autofix.ci: enabled](https://shields.mitmproxy.org/badge/autofix.ci-yes-success?logo=data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjZmZmIiB2aWV3Qm94PSIwIDAgMTI4IDEyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCB0cmFuc2Zvcm09InNjYWxlKDAuMDYxLC0wLjA2MSkgdHJhbnNsYXRlKC0yNTAsLTE3NTApIiBkPSJNMTMyNSAtMzQwcS0xMTUgMCAtMTY0LjUgMzIuNXQtNDkuNSAxMTQuNXEwIDMyIDUgNzAuNXQxMC41IDcyLjV0NS41IDU0djIyMHEtMzQgLTkgLTY5LjUgLTE0dC03MS41IC01cS0xMzYgMCAtMjUxLjUgNjJ0LTE5MSAxNjl0LTkyLjUgMjQxcS05MCAxMjAgLTkwIDI2NnEwIDEwOCA0OC41IDIwMC41dDEzMiAxNTUuNXQxODguNSA4MXExNSA5OSAxMDAuNSAxODAuNXQyMTcgMTMwLjV0MjgyLjUgNDlxMTM2IDAgMjU2LjUgLTQ2IHQyMDkgLTEyNy41dDEyOC41IC0xODkuNXExNDkgLTgyIDIyNyAtMjEzLjV0NzggLTI5OS41cTAgLTEzNiAtNTggLTI0NnQtMTY1LjUgLTE4NC41dC0yNTYuNSAtMTAzLjVsLTI0MyAtMzAwdi01MnEwIC0yNyAzLjUgLTU2LjV0Ni41IC01Ny41dDMgLTUycTAgLTg1IC00MS41IC0xMTguNXQtMTU3LjUgLTMzLjV6TTEzMjUgLTI2MHE3NyAwIDk4IDE0LjV0MjEgNTcuNXEwIDI5IC0zIDY4dC02LjUgNzN0LTMuNSA0OHY2NGwyMDcgMjQ5IHEtMzEgMCAtNjAgNS41dC01NCAxMi41bC0xMDQgLTEyM3EtMSAzNCAtMiA2My41dC0xIDU0LjVxMCA2OSA5IDEyM2wzMSAyMDBsLTExNSAtMjhsLTQ2IC0yNzFsLTIwNSAyMjZxLTE5IC0xNSAtNDMgLTI4LjV0LTU1IC0yNi41bDIxOSAtMjQydi0yNzZxMCAtMjAgLTUuNSAtNjB0LTEwLjUgLTc5dC01IC01OHEwIC00MCAzMCAtNTMuNXQxMDQgLTEzLjV6TTEyNjIgNjE2cS0xMTkgMCAtMjI5LjUgMzQuNXQtMTkzLjUgOTYuNWw0OCA2NCBxNzMgLTU1IDE3MC41IC04NXQyMDQuNSAtMzBxMTM3IDAgMjQ5IDQ1LjV0MTc5IDEyMXQ2NyAxNjUuNWg4MHEwIC0xMTQgLTc3LjUgLTIwNy41dC0yMDggLTE0OXQtMjg5LjUgLTU1LjV6TTgwMyA1OTVxODAgMCAxNDkgMjkuNXQxMDggNzIuNWwyMjEgLTY3bDMwOSA4NnE0NyAtMzIgMTA0LjUgLTUwdDExNy41IC0xOHE5MSAwIDE2NSAzOHQxMTguNSAxMDMuNXQ0NC41IDE0Ni41cTAgNzYgLTM0LjUgMTQ5dC05NS41IDEzNHQtMTQzIDk5IHEtMzcgMTA3IC0xMTUuNSAxODMuNXQtMTg2IDExNy41dC0yMzAuNSA0MXEtMTAzIDAgLTE5Ny41IC0yNnQtMTY5IC03Mi41dC0xMTcuNSAtMTA4dC00MyAtMTMxLjVxMCAtMzQgMTQuNSAtNjIuNXQ0MC41IC01MC41bC01NSAtNTlxLTM0IDI5IC01NCA2NS41dC0yNSA4MS41cS04MSAtMTggLTE0NSAtNzB0LTEwMSAtMTI1LjV0LTM3IC0xNTguNXEwIC0xMDIgNDguNSAtMTgwLjV0MTI5LjUgLTEyM3QxNzkgLTQ0LjV6Ii8+PC9zdmc+)](https://autofix.ci) [![Coverage Status](https://shields.mitmproxy.org/codecov/c/github/mitmproxy/mitmproxy/main.svg?label=codecov)](https://codecov.io/gh/mitmproxy/mitmproxy) [![Latest Version](https://shields.mitmproxy.org/pypi/v/mitmproxy.svg)](https://pypi.python.org/pypi/mitmproxy) [![Supported Python versions](https://shields.mitmproxy.org/pypi/pyversions/mitmproxy.svg)](https://pypi.python.org/pypi/mitmproxy) @@ -72,7 +74,7 @@ General information, tutorials, and precompiled binaries can be found on the mit The documentation for mitmproxy is available on our website: [![mitmproxy documentation stable](https://shields.mitmproxy.org/badge/docs-stable-brightgreen.svg)](https://docs.mitmproxy.org/stable/) -[![mitmproxy documentation dev](https://shields.mitmproxy.org/badge/docs-dev-brightgreen.svg)](https://docs.mitmproxy.org/main/) +[![mitmproxy documentation dev](https://shields.mitmproxy.org/badge/docs-dev-brightgreen.svg)](https://docs.mitmproxy.org/dev/) If you have questions on how to use mitmproxy, please use GitHub Discussions! @@ -84,7 +86,3 @@ use GitHub Discussions! As an open source project, mitmproxy welcomes contributions of all forms. [![Dev Guide](https://shields.mitmproxy.org/badge/dev_docs-CONTRIBUTING.md-blue)](./CONTRIBUTING.md) - -Also, please feel free to join our developer Slack! - -[![Slack Developer Chat](https://shields.mitmproxy.org/badge/slack-mitmproxy-E01563.svg)](http://slack.mitmproxy.org/) diff --git a/browserup-proxy.schema.json b/browserup-proxy.schema.json index 552df8abfc..7e54d02df7 100644 --- a/browserup-proxy.schema.json +++ b/browserup-proxy.schema.json @@ -25,7 +25,7 @@ "url": "logo.png" }, "title": "BrowserUp MitmProxy", - "version": "1.23" + "version": "1.24" }, "paths": { "/har": { @@ -634,77 +634,6 @@ } } }, - "WebSocketMessage": { - "type": "object", - "required": [ - "type", - "opcode", - "data", - "time" - ], - "properties": { - "type": { - "type": "string" - }, - "opcode": { - "type": "number" - }, - "data": { - "type": "string" - }, - "time": { - "type": "number" - } - } - }, - "Header": { - "type": "object", - "required": [ - "name", - "value" - ], - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - }, - "comment": { - "type": "string" - } - } - }, - "Action": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "id": { - "type": "string" - }, - "className": { - "type": "string" - }, - "tagName": { - "type": "string" - }, - "xpath": { - "type": "string" - }, - "dataAttributes": { - "type": "string" - }, - "formName": { - "type": "string" - }, - "content": { - "type": "string" - } - }, - "additionalProperties": false - }, "PageTimings": { "type": "object", "required": [ @@ -785,6 +714,28 @@ } } }, + "CustomHarData": { + "type": "object", + "minProperties": 1 + }, + "Header": { + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "comment": { + "type": "string" + } + } + }, "Har": { "type": "object", "required": [ @@ -863,6 +814,59 @@ } } }, + "WebSocketMessage": { + "type": "object", + "required": [ + "type", + "opcode", + "data", + "time" + ], + "properties": { + "type": { + "type": "string" + }, + "opcode": { + "type": "number" + }, + "data": { + "type": "string" + }, + "time": { + "type": "number" + } + } + }, + "Action": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "className": { + "type": "string" + }, + "tagName": { + "type": "string" + }, + "xpath": { + "type": "string" + }, + "dataAttributes": { + "type": "string" + }, + "formName": { + "type": "string" + }, + "content": { + "type": "string" + } + }, + "additionalProperties": false + }, "HarEntry": { "type": "object", "required": [ @@ -1322,10 +1326,6 @@ } } }, - "CustomHarData": { - "type": "object", - "minProperties": 1 - }, "Page": { "type": "object", "required": [ diff --git a/docs/build.py b/docs/build.py index f393901e93..d8c3e944c2 100755 --- a/docs/build.py +++ b/docs/build.py @@ -3,7 +3,6 @@ import subprocess from pathlib import Path - here = Path(__file__).parent for script in sorted((here / "scripts").glob("*.py")): diff --git a/docs/scripts/api-events.py b/docs/scripts/api-events.py index 7d9971b62f..95bd7f111a 100644 --- a/docs/scripts/api-events.py +++ b/docs/scripts/api-events.py @@ -2,18 +2,30 @@ import contextlib import inspect import textwrap +import typing from pathlib import Path -from mitmproxy import hooks, log, addonmanager -from mitmproxy.proxy import server_hooks, layer -from mitmproxy.proxy.layers import dns, http, modes, tcp, tls, udp, websocket +from mitmproxy import addonmanager +from mitmproxy import hooks +from mitmproxy import log +from mitmproxy.proxy import layer +from mitmproxy.proxy import server_hooks +from mitmproxy.proxy.layers import dns +from mitmproxy.proxy.layers import modes +from mitmproxy.proxy.layers import quic +from mitmproxy.proxy.layers import tcp +from mitmproxy.proxy.layers import tls +from mitmproxy.proxy.layers import udp +from mitmproxy.proxy.layers import websocket +from mitmproxy.proxy.layers.http import _hooks as http known = set() def category(name: str, desc: str, hooks: list[type[hooks.Hook]]) -> None: all_params = [ - list(inspect.signature(hook.__init__, eval_str=True).parameters.values())[1:] for hook in hooks + list(inspect.signature(hook.__init__, eval_str=True).parameters.values())[1:] + for hook in hooks ] # slightly overengineered, but this was fun to write. ¯\_(ツ)_/¯ @@ -22,15 +34,9 @@ def category(name: str, desc: str, hooks: list[type[hooks.Hook]]) -> None: for params in all_params: for param in params: try: - mod = inspect.getmodule(param.annotation).__name__ - if mod == "typing": - # this is ugly, but can be removed once we are on Python 3.9+ only - imports.add( - inspect.getmodule(param.annotation.__args__[0]).__name__ - ) - types.add(param.annotation._name) - else: - imports.add(mod) + imports.add(inspect.getmodule(param.annotation).__name__) + for t in typing.get_args(param.annotation): + imports.add(inspect.getmodule(t).__name__) except AttributeError: raise ValueError(f"Missing type annotation: {params}") imports.discard("builtins") @@ -54,7 +60,8 @@ def category(name: str, desc: str, hooks: list[type[hooks.Hook]]) -> None: raise RuntimeError(f"Already documented: {hook}") known.add(hook.name) doc = inspect.getdoc(hook) - print(f" def {hook.name}({', '.join(str(p) for p in ['self'] + params)}):") + print(f" @staticmethod") + print(f" def {hook.name}({', '.join(str(p) for p in params)}):") print(textwrap.indent(f'"""\n{doc}\n"""', " ")) if params: print( @@ -90,6 +97,7 @@ def category(name: str, desc: str, hooks: list[type[hooks.Hook]]) -> None: server_hooks.ServerConnectHook, server_hooks.ServerConnectedHook, server_hooks.ServerDisconnectedHook, + server_hooks.ServerConnectErrorHook, ], ) @@ -104,6 +112,8 @@ def category(name: str, desc: str, hooks: list[type[hooks.Hook]]) -> None: http.HttpErrorHook, http.HttpConnectHook, http.HttpConnectUpstreamHook, + http.HttpConnectedHook, + http.HttpConnectErrorHook, ], ) @@ -139,6 +149,15 @@ def category(name: str, desc: str, hooks: list[type[hooks.Hook]]) -> None: ], ) + category( + "QUIC", + "", + [ + quic.QuicStartClientHook, + quic.QuicStartServerHook, + ], + ) + category( "TLS", "", diff --git a/docs/scripts/clirecording/clidirector.py b/docs/scripts/clirecording/clidirector.py index db286b2b2a..973ca610ff 100644 --- a/docs/scripts/clirecording/clidirector.py +++ b/docs/scripts/clirecording/clidirector.py @@ -1,11 +1,11 @@ import json -from typing import NamedTuple, Optional - -import libtmux import random import subprocess import threading import time +from typing import NamedTuple + +import libtmux class InstructionSpec(NamedTuple): @@ -73,7 +73,7 @@ def end_session(self) -> None: self.tmux_session.kill_session() def press_key( - self, keys: str, count=1, pause: Optional[float] = None, target=None + self, keys: str, count=1, pause: float | None = None, target=None ) -> None: if pause is None: pause = self.pause_between_keys @@ -96,7 +96,7 @@ def press_key( real_pause += 2 * pause self.pause(real_pause) - def type(self, keys: str, pause: Optional[float] = None, target=None) -> None: + def type(self, keys: str, pause: float | None = None, target=None) -> None: if pause is None: pause = self.pause_between_keys if target is None: @@ -127,7 +127,7 @@ def run_external(self, command: str) -> None: def message( self, msg: str, - duration: Optional[int] = None, + duration: int | None = None, add_instruction: bool = True, instruction_html: str = "", ) -> None: @@ -160,7 +160,7 @@ def close_popup(self, duration: float = 0) -> None: self.tmux_pane.cmd("display-popup", "-C") def instruction( - self, instruction: str, duration: float = 3, time_from: Optional[float] = None + self, instruction: str, duration: float = 3, time_from: float | None = None ) -> None: if time_from is None: time_from = self.current_time diff --git a/docs/scripts/clirecording/record.py b/docs/scripts/clirecording/record.py index 54ba1be2a7..46f4748224 100644 --- a/docs/scripts/clirecording/record.py +++ b/docs/scripts/clirecording/record.py @@ -1,8 +1,6 @@ #!/usr/bin/env python3 - -from clidirector import CliDirector import screenplays - +from clidirector import CliDirector if __name__ == "__main__": director = CliDirector() diff --git a/docs/scripts/clirecording/screenplays.py b/docs/scripts/clirecording/screenplays.py index ea871e7a7f..5f916dac1d 100644 --- a/docs/scripts/clirecording/screenplays.py +++ b/docs/scripts/clirecording/screenplays.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 - from clidirector import CliDirector diff --git a/docs/scripts/examples.py b/docs/scripts/examples.py index 4dd742d500..953cd1fccf 100755 --- a/docs/scripts/examples.py +++ b/docs/scripts/examples.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 - import re from pathlib import Path diff --git a/docs/scripts/filters.py b/docs/scripts/filters.py index 32634196a8..a228593082 100755 --- a/docs/scripts/filters.py +++ b/docs/scripts/filters.py @@ -1,8 +1,6 @@ #!/usr/bin/env python3 - from mitmproxy import flowfilter - print('') for i in flowfilter.help: print("" % i) diff --git a/docs/scripts/options.py b/docs/scripts/options.py index 3747d3fb77..6ee4c34af4 100755 --- a/docs/scripts/options.py +++ b/docs/scripts/options.py @@ -1,8 +1,11 @@ #!/usr/bin/env python3 import asyncio -from mitmproxy import options, optmanager -from mitmproxy.tools import dump, console, web +from mitmproxy import options +from mitmproxy import optmanager +from mitmproxy.tools import console +from mitmproxy.tools import dump +from mitmproxy.tools import web masters = { "mitmproxy": console.master.ConsoleMaster, diff --git a/docs/scripts/pdoc-template/custom.css b/docs/scripts/pdoc-template/custom.css new file mode 100644 index 0000000000..bae027af50 --- /dev/null +++ b/docs/scripts/pdoc-template/custom.css @@ -0,0 +1,18 @@ +.pdoc h1 { + margin-bottom: 1em; +} + +{% if module and module.name == "events" %} + +.pdoc .class { + padding-left: 0; + --name: var(--text); + background: none; + font-size: 1.2em; +} + +.pdoc .classattr { + margin-left: 0; +} + +{% endif %} diff --git a/docs/scripts/pdoc-template/frame.html.jinja2 b/docs/scripts/pdoc-template/frame.html.jinja2 index d7ef3ab4d7..71ec15052c 100644 --- a/docs/scripts/pdoc-template/frame.html.jinja2 +++ b/docs/scripts/pdoc-template/frame.html.jinja2 @@ -2,7 +2,7 @@ {% block style %} - + {% endblock %} {% endfilter %} {% block content %}{% endblock %} diff --git a/docs/scripts/pdoc-template/module.html.jinja2 b/docs/scripts/pdoc-template/module.html.jinja2 index 8cb53dbabe..b78f260c8e 100644 --- a/docs/scripts/pdoc-template/module.html.jinja2 +++ b/docs/scripts/pdoc-template/module.html.jinja2 @@ -14,6 +14,7 @@ To document all event hooks, we do a bit of hackery: {% macro view_source_state(doc) %}{% endmacro %} {% macro view_source_button(doc) %}{% endmacro %} {% macro view_source_code(doc) %}{% endmacro %} + {% macro decorators(doc) %}{% endmacro %} {% macro is_public(doc) %} {% if doc.name != "__init__" %} {{ default_is_public(doc) }} diff --git a/docs/src/content/_index.md b/docs/src/content/_index.md index d2b89489c3..5ac62761b6 100644 --- a/docs/src/content/_index.md +++ b/docs/src/content/_index.md @@ -14,7 +14,7 @@ mitmproxy is a set of tools that provide an interactive, SSL/TLS-capable interce - Intercept HTTP & HTTPS requests and responses and modify them on the fly - Save complete HTTP conversations for later replay and analysis -- Replay the client-side of an HTTP conversations +- Replay the client-side of an HTTP conversation - Replay HTTP responses of a previously recorded server - Reverse proxy mode to forward traffic to a specified server - Transparent proxy mode on macOS and Linux diff --git a/docs/src/content/addons-api-changelog.md b/docs/src/content/addons-api-changelog.md index c4dfad60ed..7fd2327f67 100644 --- a/docs/src/content/addons-api-changelog.md +++ b/docs/src/content/addons-api-changelog.md @@ -10,6 +10,10 @@ menu: We try to avoid them, but this page lists breaking changes in the mitmproxy addon API. +## mitmproxy >= 9.1 + +`mitmproxy.connection.Client` and `mitmproxy.connection.Server` now accept keyword arguments only. + ## mitmproxy 9.0 #### Logging diff --git a/docs/src/content/addons-overview.md b/docs/src/content/addons-overview.md index a4f48efed0..e8bdba7b60 100644 --- a/docs/src/content/addons-overview.md +++ b/docs/src/content/addons-overview.md @@ -25,9 +25,9 @@ them to keys in the interactive tools. {{< example src="examples/addons/anatomy.py" lang="py" >}} Above is a simple addon that keeps track of the number of flows (or more -specifically HTTP requests) we've seen. Every time it sees a new flow, it uses -mitmproxy's internal logging mechanism to announce its tally. The output can be -found in the event log in the interactive tools, or on the console in mitmdump. +specifically HTTP requests) we've seen. Every time it sees a new flow, it +increments and logs its tally. The output can be found in the event log in the +interactive tools, or on the console in mitmdump. Take it for a spin and make sure that it does what it's supposed to, by loading it into your mitmproxy tool of choice. We'll use mitmdump in these examples, @@ -45,11 +45,6 @@ Here are a few things to note about the code above: - The `request` method is an example of an *event*. Addons simply implement a method for each event they want to handle. Each event and its signature are documented in the [API documentation]({{< relref "addons-api" >}}). -- Finally, the `ctx` module is a holdall module that exposes a set of standard - objects that are commonly used in addons. We could pass a `ctx` object as the - first parameter to every event, but we've found it neater to just expose it as - an importable global. In this case, we're using the `ctx.log` object to do our - logging. # Abbreviated Scripting Syntax diff --git a/docs/src/content/concepts-certificates.md b/docs/src/content/concepts-certificates.md index 78f5e77f74..5759e614ef 100644 --- a/docs/src/content/concepts-certificates.md +++ b/docs/src/content/concepts-certificates.md @@ -1,8 +1,8 @@ --- title: "Certificates" menu: - concepts: - weight: 3 + concepts: + weight: 3 --- # About Certificates @@ -45,11 +45,9 @@ For security reasons, the mitmproxy CA is generated uniquely on the first start is not shared between mitmproxy installations on different devices. This makes sure that other mitmproxy users cannot intercept your traffic. - - ### Installing the mitmproxy CA certificate manually -Sometimes using the [quick install app](#quick-setup) is not an option and you need to install the CA manually. +Sometimes using the [quick install app](#quick-setup) is not an option and you need to install the CA manually. Below is a list of pointers to manual certificate installation documentation for some common platforms. The mitmproxy CA cert is located in `~/.mitmproxy` after it has been generated at the first start of mitmproxy. @@ -62,6 +60,7 @@ documentation for some common platforms. The mitmproxy CA cert is located in - [macOS (automated)](https://www.dssw.co.uk/reference/security.html): `sudo security add-trusted-cert -d -p ssl -p basic -k /Library/Keychains/System.keychain ~/.mitmproxy/mitmproxy-ca-cert.pem` - [Ubuntu/Debian]( https://askubuntu.com/questions/73287/how-do-i-install-a-root-certificate/94861#94861) +- [Fedora](https://docs.fedoraproject.org/en-US/quick-docs/using-shared-system-certificates/#proc_adding-new-certificates) - [Mozilla Firefox](https://wiki.mozilla.org/MozillaRootCertificate#Mozilla_Firefox) - [Chrome on Linux](https://stackoverflow.com/a/15076602/198996) - [iOS](http://jasdev.me/intercepting-ios-traffic) @@ -82,18 +81,17 @@ documentation for some common platforms. The mitmproxy CA cert is located in When mitmproxy receives a request to establish TLS (in the form of a ClientHello message), it puts the client on hold and first makes a connection to the upstream server to "sniff" the contents of its TLS certificate. -The information gained -- Common Name, Organization, Subject Alternative Names -- is then used to generate a new +The information gained -- Common Name, Organization, Subject Alternative Names -- is then used to generate a new interception certificate on-the-fly, signed by the mitmproxy CA. Mitmproxy then returns to the client and continues the handshake with the newly-forged certificate. Upstream cert sniffing is on by default, and can optionally be disabled by turning the `upstream_cert` option off. - ### Certificate Pinning Some applications employ [Certificate Pinning](https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning) to prevent -man-in-the-middle attacks. This means that **mitmproxy's** +man-in-the-middle attacks. This means that **mitmproxy's** certificates will not be accepted by these applications without modifying them. If the contents of these connections are not important, it is recommended to use the [ignore_hosts]({{< relref "howto-ignoredomains">}}) feature to prevent @@ -108,6 +106,7 @@ exist to accomplish this: which supports certificate pinning bypasses on iOS and Android. - [ssl-kill-switch2](https://github.com/nabla-c0d3/ssl-kill-switch2) is a blackbox tool to disable certificate pinning within iOS and macOS applications. + - [android-unpinner](https://github.com/mitmproxy/android-unpinner) modifies Android APKs to inject Frida and HTTP Toolkit's unpinning scripts. *Please propose other useful tools using the "Edit on GitHub" button on the top right of this page.* @@ -178,9 +177,9 @@ The `mitmproxy-ca.pem` certificate file has to look roughly like this: -----END CERTIFICATE----- -When looking at the certificate with +When looking at the certificate with `openssl x509 -noout -text -in ~/.mitmproxy/mitmproxy-ca.pem` -it should have at least the following X509v3 extensions so mitmproxy can +it should have at least the following X509v3 extensions so mitmproxy can use it to generate certificates: X509v3 extensions: @@ -189,8 +188,33 @@ use it to generate certificates: X509v3 Basic Constraints: critical CA:TRUE +For example, when using OpenSSL, you can create a CA authority as follows: + +```shell +openssl req -x509 -new -nodes -key ca.key -sha256 -out ca.crt -addext keyUsage=critical,keyCertSign +cat ca.key ca.crt > mitmproxy-ca.pem +``` + +## Mutual TLS (mTLS) and client certificates + +TLS is typically used in a way where the client verifies the server's identity +using the server's certificate during the handshake, but the server does not +verify the client's identity using the TLS protocol. Instead, the client +transmits cookies or other access tokens over the established secure channel to +authenticate itself. + +Mutual TLS (mTLS) is a mode where the server verifies the client's identity +not using cookies or access tokens, but using a certificate presented by the +client during the TLS handshake. With mTLS, both client and server use a +certificate to authenticate each other. + +If a server wants to verify the clients identity using mTLS, it sends an +additional `CertificateRequest` message to the client during the handshake. The +client then provides its certificate and proves ownership of the private key +with a matching signature. This part works just like server authentication, only +the other way around. -## Using a client side certificate +### mTLS between mitmproxy and upstream server You can use a client certificate by passing the `--set client_certs=DIRECTORY|FILE` option to mitmproxy. Using a directory allows certs to be selected based on @@ -198,9 +222,30 @@ hostname, while using a filename allows a single specific certificate to be used for all TLS connections. Certificate files must be in the PEM format and should contain both the unencrypted private key and the certificate. -### Multiple client certificates - You can specify a directory to `--set client_certs=DIRECTORY`, in which case the matching certificate is looked up by filename. So, if you visit example.org, mitmproxy looks for a file named `example.org.pem` in the specified directory and uses this as the client cert. + +### mTLS between client and mitmproxy + +By default, mitmproxy does not send the `CertificateRequest` TLS handshake +message to connecting clients. This is because it trips up some clients that do +not expect a certificate request (most famously old Android versions). However, +there are other clients -- in particular in the MQTT / IoT environment -- that +do expect a certificate request and will otherwise fail the TLS handshake. + +To instruct mitmproxy to request a client certificate from the connecting +client, you can pass the `--set request_client_cert=True` option. This will +generate a `CertificateRequest` TLS handshake message and (if successful) +establish an mTLS connection. This option only requests a certificate from the +client, it does not validate the presented identity in any way. For the purposes +of testing and developing client and server software, this is typically not an +issue. If you operate mitmproxy in an environment where untrusted clients might +connect, you need to safeguard against them. + +The `request_client_cert` option is typically paired with `client_certs` like so: + +```bash +mitmproxy --set request_client_cert=True --set client_certs=client-cert.pem +``` diff --git a/docs/src/content/concepts-filters.md b/docs/src/content/concepts-filters.md index 4f75cb7d03..1c198bca72 100644 --- a/docs/src/content/concepts-filters.md +++ b/docs/src/content/concepts-filters.md @@ -50,4 +50,4 @@ Anything but requests with a text/html content type: Replace entire GET string in a request (quotes required to make it work): - ":~q ~m GET:.*:/replacement.html" + ":~q ~m GET:.*:/replacement.html" diff --git a/docs/src/content/concepts-modes.md b/docs/src/content/concepts-modes.md index 3f3486c53b..a8b6f8fed4 100644 --- a/docs/src/content/concepts-modes.md +++ b/docs/src/content/concepts-modes.md @@ -211,19 +211,43 @@ allowed IP addresses. mitmdump --mode reverse:https://example.com ``` -mitmproxy is usually used with a client that uses the proxy to access -the Internet. Using reverse proxy mode, you can use mitmproxy to act -like a normal HTTP server: +In reverse proxy mode, mitmproxy acts as a normal server. +Requests by clients will be forwarded to a preconfigured target server, +and responses will be forwarded back to the client: {{< figure src="/schematics/proxy-modes-reverse.png" >}} -Locally, reverse mode instances will listen on the same port as their regular -equivalent, which is 8080 by default (except for DNS, which uses port 53). -To specify a different port, append "`@`" followed by the number to the mode. -For example, to listen on port 8081 for HTTP proxy request use -`--mode reverse:https://example.com@8081`. +### Listen Port -There are various use-cases: +With the exception of DNS, reverse proxy servers will listen on port 8080 by default (DNS uses 53). +To listen on a different port, append `@portnumber` to the mode. You can +also pass `--mode` repeatedly to run multiple reverse proxy servers on different ports. For example, +the following command will run a reverse proxy server to example.com on port 80 and 443: + +```text +mitmdump --mode reverse:https://example.com@80 --mode reverse:https://example.com@443 +``` + +### Protocol Specification + +The examples above have focused on HTTP reverse proxying, but mitmproxy can also reverse proxy other protocols. +To adjust the protocol, adjust the scheme in the proxy specification. For example, `--mode reverse:tcp://example.com:80` +would establish a raw TCP proxy. + +| Scheme | client ↔ mitmproxy | mitmproxy ↔ server | +|----------|-----------------------------------------|--------------------| +| http:// | HTTP or HTTPS (autodetected) | HTTP | +| https:// | HTTP or HTTPS (autodetected) | HTTPS | +| dns:// | DNS | DNS | +| http3:// | HTTP/3 | HTTP/3 | +| quic:// | Raw QUIC | Raw QUIC | +| tcp:// | Raw TCP or TCP-over-TLS (autodetected) | Raw TCP | +| tls:// | Raw TCP or TCP-over-TLS (autodetected) | Raw TCP-over-TLS | +| udp:// | Raw UDP or UDP-over-DTLS (autodetected) | Raw UDP | +| dtls:// | Raw UDP or UDP-over-DTLS (autodetected) | Raw UDP-over-DTLS | + + +### Reverse Proxy Examples - Say you have an internal API running at . You could now set up mitmproxy in reverse proxy mode at and @@ -236,16 +260,11 @@ There are various use-cases: your hosts file so that example.com points to 127.0.0.1 and then run mitmproxy in reverse proxy mode on port 80. You can test your app on the example.com domain and get all requests recorded in mitmproxy. -- Say you have some toy project that should get SSL support. Simply set up +- Say you have some toy project that should get TLS support. Simply set up mitmproxy as a reverse proxy on port 443 and you're done (`mitmdump -p 443 --mode reverse:http://localhost:80/`). Mitmproxy auto-detects TLS traffic and intercepts it dynamically. There are better tools for this specific task, but mitmproxy - is very quick and simple way to set up an SSL-speaking server. -- Want to add a non-SSL-capable compression proxy in front of your server? You - could even spawn a mitmproxy instance that terminates SSL (`--mode reverse:http://...`), - point it to the compression proxy and let the compression proxy point to a - SSL-initiating mitmproxy (`--mode reverse:https://...`), which then points to the real - server. As you see, it's a fairly flexible thing. + is very quick and simple way to set up an TLS-speaking server. - Want to know what goes on over (D)TLS (without HTTP)? With mitmproxy's raw traffic support you can. Use `--mode reverse:tls://example.com:1234` to spawn a TCP instance that connects to `example.com:1234` using TLS, and diff --git a/docs/src/content/concepts-options.md b/docs/src/content/concepts-options.md index 8a32b35b14..d10990537a 100644 --- a/docs/src/content/concepts-options.md +++ b/docs/src/content/concepts-options.md @@ -34,7 +34,11 @@ persistent by saving the settings out to a YAML configuration file (please see the specific tool's interactive help for details on how to do this). For all tools, options can be set directly by name using the `--set` -command-line option. Please see the command-line help (`--help`) for usage. +command-line option. Please see the command-line help (`--help`) for usage. Example: +``` +mitmproxy --set anticomp=true +mitmweb --set ignore_hosts=example.com --set ignore_hosts=example.org +``` ## Available Options diff --git a/docs/src/content/concepts-protocols.md b/docs/src/content/concepts-protocols.md index c689d8bff1..e01fa8ad27 100644 --- a/docs/src/content/concepts-protocols.md +++ b/docs/src/content/concepts-protocols.md @@ -11,7 +11,7 @@ mitmproxy not only supports HTTP, but also other important web protocols. This page lists details and known limitations of the respective protocol implementations. Most protocols can be disabled by toggling the respective [option]({{< relref concepts-options >}}). -## HTTP/1.x +## HTTP/1 HTTP/1.0 and HTTP/1.1 support in mitmproxy is based on our custom HTTP stack based on [h11](https://github.com/python-hyper/h11), which is particularly robust to HTTP syntax @@ -33,6 +33,18 @@ server does not speak HTTP/2, mitmproxy seamlessly translates messages to HTTP/1 - *Push Promises*: mitmproxy currently does not advertise support for HTTP/2 Push Promises. - *Cleartext HTTP/2*: mitmproxy currently does not support unencrypted HTTP/2 (h2c). +## HTTP/3 + +HTTP/3 support in mitmproxy is based on [aioquic](https://github.com/aiortc/aioquic). Mitmproxy's HTTP/3 functionality +is still experimental and only available in reverse proxy mode. + +##### Known Limitations + +- *Replay*: Client Replay is currently broken. +- *Supported Versions*: mitmproxy currently only supports QUIC Version 1. Version 2 (RFC 9369) is not supported yet. +- *Implementation Compatibility*: mitmproxy's HTTP/3 support has only been extensively tested with cURL. + Other implementations are likely to exhibit bugs. + ## WebSocket WebSocket support in mitmproxy is based on [wsproto](https://github.com/python-hyper/wsproto) project, including support diff --git a/docs/src/content/howto-ignoredomains.md b/docs/src/content/howto-ignoredomains.md index bcdff4f072..a5954717e5 100644 --- a/docs/src/content/howto-ignoredomains.md +++ b/docs/src/content/howto-ignoredomains.md @@ -15,9 +15,7 @@ mitmproxy's interception mechanism: and mitmproxy's interception leads to errors. For example, the Twitter app, Windows Update or the Apple App Store fail to work if mitmproxy is active. - **Convenience:** You really don't care about some parts of the traffic and - just want them to go away. Note that mitmproxy's "Limit" option is often the - better alternative here, as it is not affected by the limitations listed - below. + just want them to go away. Note that mitmproxy's [view_filter]({{< relref "concepts-options/#view_filter" >}}) option is often the better alternative here, as it is not affected by the limitations listed below. If you want to peek into (SSL-protected) non-HTTP connections, check out the **tcp_proxy** feature. If you want to ignore traffic from mitmproxy's processing diff --git a/docs/src/content/howto-install-system-trusted-ca-android.md b/docs/src/content/howto-install-system-trusted-ca-android.md index b3bf5576ef..f18bc62021 100644 --- a/docs/src/content/howto-install-system-trusted-ca-android.md +++ b/docs/src/content/howto-install-system-trusted-ca-android.md @@ -16,7 +16,7 @@ Please note, that apps can decide to ignore the system certificate store and mai - [Android Studio/Android Sdk](https://developer.android.com/studio) is installed (tested with Version 4.1.3 for Linux 64-bit) - An Android Virtual Device (AVD) was created. Setup documentation available [here](https://developer.android.com/studio/run/managing-avds) - - The AVD must not run a production build (these will prevent you from using `adb root`) + - AVD production builds (those labeled with "Google Play") will prevent you from using `adb root`. You need to use [the Magisk method]({{< ref "#instructions-when-using-magisk" >}}) if you need Google Play installed. - The proxy settings of the AVD are configured to use mitmproxy. Documentation [here](https://developer.android.com/studio/run/emulator-networking#proxy) - Emulator and adb executables from Android Sdk have been added to $PATH variable @@ -45,10 +45,70 @@ By default, the mitmproxy CA certificate is located in this file: `~/.mitmproxy/ ## 3. Insert certificate into system certificate store -Now we have to place our CA certificate inside the system certificate store located at `/system/etc/security/cacerts/` in the Android filesystem. By default, the `/system` partition is mounted as read-only. The following steps describe how to gain write permissions on the `/system` partition and how to copy the certificate created in the previous step. +Now we have to place our CA certificate inside the system certificate store located at `/system/etc/security/cacerts/` in the Android filesystem. By default, the `/system` partition is mounted as read-only. The following steps describe how to gain write permissions on the `/system` partition and how to copy the certificate created in [the previous step]({{< ref "#2-rename-certificate" >}}). -### Instructions for API LEVEL > 28 - Starting from API LEVEL 29 (Android 10), it seems to be impossible to mount the "/" partition as read-write. Google provided a [workaround for this issue](https://android.googlesource.com/platform/system/core/+/master/fs_mgr/README.overlayfs.md) using OverlayFS. Unfortunately, at the time of writing this (11. April 2021), the instructions in this workaround will result in your emulator getting stuck in a [boot loop](https://issuetracker.google.com/issues/144891973). Some smart guy on Stackoverflow [found a way](https://stackoverflow.com/questions/60867956/android-emulator-sdk-10-api-29-wont-start-after-remount-and-reboot) to get the `/system` directory writable anyway. +### Instructions when using Magisk +If you want to use a production build (labeled "Google Play"; it's those builds that have Google Play installed) you can use Magisk to obtain root in your AVD. +[Magisk](https://github.com/topjohnwu/Magisk) allows root on your Android device or emulator. + +See the [instructions here](https://github.com/shakalaca/MagiskOnEmulator) for installing Magisk on your AVD. +The instructions have been tested with API level 30, but are reportedly working with API levels 22 up to and including 30 and 'S' (except API level 28). +Note: the instructions say to start your AVD. Do not supply an `-http-proxy` directive to mitmproxy at this point. + +When you are done with that, your emulator will allow root. You can check this by running a terminal emulator and typing `su`. +Magisk should ask you if you want to grant root to the program. After granting this, typing `whoami` would display `root`. + +However, after you have installed Magisk, you can no longer start your emulator with `-writable-system`. It will cause a boot loop. (Start your AVD with `-show-kernel` to see the error.) +But you can install your mitmproxy certificate by putting it in a Magisk module, and installing that module. +Magisk will take care of copying your certificate to `/system/etc/security/cacerts/` during boot. + +#### Downloading the Magisk module from mitmweb +If you run mitmweb, you can get simply download the Magisk module instead of handcrafting it. +Stop your AVD, and start it again with `-http-proxy 127.0.0.1:8080` (or whatever IP and port combination you are running mitmweb's proxy on). + +Then, *inside* the AVD, start a browser and navigate to `http://mitm.it/cert/magisk`. +You will be prompted to download `mitmproxy-magisk-module.zip`, which is the Magisk module you need. Store that file somewhere (like in 'Downloads'). + +Then open up Magisk, click on `Modules` and install your module. + +Reboot your AVD. + +#### Creating the Magisk module containing your certificate +If you do not run mitmweb, you'll need to create a Magisk module yourself. +See [here](https://topjohnwu.github.io/Magisk/guides.html#magisk-modules) for in-depth information on Magisk modules, but basically it boils down to this: + +Create the following directories: +- `mitmproxycert` (this will be the root of your module) +- `mitmproxycert/com/google/android` +- `mitmproxycert/system/etc/security/cacerts` + +Place your renamed certificate from [step 2]({{< ref "#2-rename-certificate" >}}) inside `mitmproxycert/system/etc/security/cacerts` and `chmod 664` it. + +Save the content of [https://github.com/topjohnwu/Magisk/blob/master/scripts/module_installer.sh](https://github.com/topjohnwu/Magisk/blob/master/scripts/module_installer.sh) as a local file `update-binary` and place it inside `mitmproxycert/com/google/android`. + +Create a file named `updater-script` containing only the string `#MAGISK` and place it inside `mitmproxycert/com/google/android`. + +Create a file named `module.prop` and place it inside `mitmproxycert`. The file should contain something like: + +``` +id=mitmproxycert +name=MITM proxy certificate +version=1 +versionCode=1 +author=mitmproxycert +description=My shiny MITM proxy certificate to reveal all secrets and obtain world domination! +``` + +Zip the module using something like `cd ./mitmproxycert ; zip -r ./../mitmproxycert.zip ./` and push it to your running AVD using `adb push ./../mitmproxycert.zip /storage/emulated/0/Download/`. + +The go to your AVD, open up Magisk, click on `Modules` and install your module (you'll find it in the Downloads folder). + +Reboot your AVD. + +### Instructions for API LEVEL > 28 using `-writable-system` +By default, the `/system` partition is mounted as read-only. The following steps describe how to gain write permissions on the `/system` partition and how to copy the certificate created in chapter 2. + +Starting from API LEVEL 29 (Android 10), it seems to be impossible to mount the "/" partition as read-write. Google provided a [workaround for this issue](https://android.googlesource.com/platform/system/core/+/master/fs_mgr/README.overlayfs.md) using OverlayFS. Unfortunately, at the time of writing this (11. April 2021), the instructions in this workaround will result in your emulator getting stuck in a [boot loop](https://issuetracker.google.com/issues/144891973). Some smart guy on Stackoverflow [found a way](https://stackoverflow.com/questions/60867956/android-emulator-sdk-10-api-29-wont-start-after-remount-and-reboot) to get the `/system` directory writable anyway. **Keep in mind:** You always have to start the emulator using the `-writable-system` option if you want to use your certificate. Otherwise Android will load a "clean" system image. @@ -62,11 +122,11 @@ Tested on emulators running API LEVEL 29 and 30 - reboot device: `adb reboot` - restart adb as root: `adb root` - perform remount of partitions as read-write: `adb remount`. (If adb tells you that you need to reboot, reboot again `adb reboot` and run `adb remount` again.) - - push your renamed certificate from step 2: `adb push /system/etc/security/cacerts` + - push your renamed certificate from [step 2]({{< ref "#2-rename-certificate" >}}): `adb push /system/etc/security/cacerts` - set certificate permissions: `adb shell chmod 664 /system/etc/security/cacerts/` - reboot device: `adb reboot` -### Instructions for API LEVEL <= 28 +### Instructions for API LEVEL <= 28 using `-writable-system` Tested on emulators running API LEVEL 26, 27 and 28 @@ -76,6 +136,10 @@ Tested on emulators running API LEVEL 26, 27 and 28 - Start the desired AVD: `emulator -avd -writable-system` (add `-show-kernel` flag for kernel logs) - restart adb as root: `adb root` - perform remount of partitions as read-write: `adb remount`. (If adb tells you that you need to reboot, reboot again `adb reboot` and run `adb remount` again.) - - push your renamed certificate from step 2: `adb push /system/etc/security/cacerts` + - push your renamed certificate from [step 2]({{< ref "#2-rename-certificate" >}}): `adb push /system/etc/security/cacerts` - set certificate permissions: `adb shell chmod 664 /system/etc/security/cacerts/` - reboot device: `adb reboot` + +### Testing that your certificate is loaded from the system certificate store + +In your AVD, go to Settings → Security → Advanced → Encryption & credentials → Trusted credentials. Find your certificate (default name is `mitmproxy`) in the list. diff --git a/docs/src/content/howto-transparent.md b/docs/src/content/howto-transparent.md index a74bfd0ac0..a5f9f84e15 100644 --- a/docs/src/content/howto-transparent.md +++ b/docs/src/content/howto-transparent.md @@ -275,6 +275,9 @@ tproxy_user = "nobody" rdr pass proto tcp from any to any port $redir_ports -> $tproxy pass out route-to (lo0 127.0.0.1) proto tcp from any to any port $redir_ports user { != $tproxy_user } + +# End the file with a blank newline + ``` Follow steps **3-5** above. This will redirect the packets from all users other than `nobody` on the machine to mitmproxy. To avoid circularity, run mitmproxy as the user `nobody`. Hence step **6** should look like: diff --git a/docs/src/content/howto-wireshark-tls.md b/docs/src/content/howto-wireshark-tls.md index 6b8dc890a7..d6784432b0 100644 --- a/docs/src/content/howto-wireshark-tls.md +++ b/docs/src/content/howto-wireshark-tls.md @@ -9,7 +9,7 @@ menu: The SSL/TLS master keys can be logged by mitmproxy so that external programs can decrypt SSL/TLS connections both from and to the proxy. Recent versions of -Wireshark can use these log files to decrypt packets. See the [Wireshark wiki](https://wiki.wireshark.org/SSL#Using_the_.28Pre.29-Master-Secret) for more information. +Wireshark can use these log files to decrypt packets. See the [Wireshark wiki](https://wiki.wireshark.org/TLS#using-the-pre-master-secret) for more information. Key logging is enabled by setting the environment variable `SSLKEYLOGFILE` so that it points to a writable text file: diff --git a/docs/src/content/overview-features.md b/docs/src/content/overview-features.md index f635436a2a..adb948511c 100644 --- a/docs/src/content/overview-features.md +++ b/docs/src/content/overview-features.md @@ -205,7 +205,9 @@ if a modify hook is triggered on server response, the replacement is only run on the Response object leaving the Request intact. You control whether the hook triggers on the request, response or both using the filter pattern. If you need finer-grained control than this, it's simple -to create a script using the replacement API on Flow components. +to create a script using the replacement API on Flow components. Body +modifications have no effect on streamed bodies. See +[Streaming]({{< relref "#streaming" >}}) for more detail. #### Examples @@ -359,8 +361,8 @@ indicated manipulations on it, and then send the message on to the other party. This can be problematic when downloading or uploading large files. When streaming is enabled, message bodies are not buffered on the proxy but instead sent directly to the server/client. This currently means that the message body -will not be accessible within mitmproxy. HTTP headers are still fully buffered before -being sent. +will not be accessible within mitmproxy, and body modifications will have no +effect. HTTP headers are still fully buffered before being sent. Request/response streaming is enabled by specifying a size cutoff in the `stream_large_bodies` option. diff --git a/docs/src/content/overview-getting-started.md b/docs/src/content/overview-getting-started.md index 7bcfd63598..944bb5f295 100644 --- a/docs/src/content/overview-getting-started.md +++ b/docs/src/content/overview-getting-started.md @@ -49,4 +49,3 @@ new flow and you can inspect it. * [**GitHub**](https://github.com/mitmproxy/mitmproxy): If you want to ask usage questions, contribute to mitmproxy, or submit a bug report, please use GitHub. -* [**Slack**](https://mitmproxy.slack.com): For ephemeral development questions/coordination, please use our Slack channel. diff --git a/docs/src/content/overview-installation.md b/docs/src/content/overview-installation.md index a65ae0fa49..8f7e86d5e8 100644 --- a/docs/src/content/overview-installation.md +++ b/docs/src/content/overview-installation.md @@ -66,7 +66,7 @@ While there are plenty of options around[^1], we recommend the installation usin packages. Most of them (pip, virtualenv, pipenv, etc.) should just work, but we don't have the capacity to provide support for it. -1. Install a recent version of Python (we require at least 3.9). +1. Install a recent version of Python (we require at least 3.10). 2. Install [pipx](https://pipxproject.github.io/pipx/). 3. `pipx install mitmproxy` diff --git a/examples/addons/anatomy.py b/examples/addons/anatomy.py index 011ccd0f1f..a03404a91e 100644 --- a/examples/addons/anatomy.py +++ b/examples/addons/anatomy.py @@ -3,6 +3,7 @@ Run as follows: mitmproxy -s anatomy.py """ + import logging diff --git a/examples/addons/commands-flows.py b/examples/addons/commands-flows.py index 9cc196ca9b..fbe0bebeaa 100644 --- a/examples/addons/commands-flows.py +++ b/examples/addons/commands-flows.py @@ -1,4 +1,5 @@ """Handle flows as command arguments.""" + import logging from collections.abc import Sequence diff --git a/examples/addons/commands-paths.py b/examples/addons/commands-paths.py index e80ace5cb8..908d564808 100644 --- a/examples/addons/commands-paths.py +++ b/examples/addons/commands-paths.py @@ -1,4 +1,5 @@ """Handle file paths as command arguments.""" + import logging from collections.abc import Sequence diff --git a/examples/addons/commands-simple.py b/examples/addons/commands-simple.py index 86750a4b92..4edef09e60 100644 --- a/examples/addons/commands-simple.py +++ b/examples/addons/commands-simple.py @@ -1,4 +1,5 @@ """Add a custom command to mitmproxy's command prompt.""" + import logging from mitmproxy import command diff --git a/examples/addons/contentview-custom-grpc.py b/examples/addons/contentview-custom-grpc.py index c84da91c89..5229d189ec 100644 --- a/examples/addons/contentview-custom-grpc.py +++ b/examples/addons/contentview-custom-grpc.py @@ -3,8 +3,12 @@ protobuf messages based on a user defined rule set. """ + from mitmproxy import contentviews -from mitmproxy.contentviews.grpc import ViewGrpcProtobuf, ViewConfig, ProtoParser +from mitmproxy.addonmanager import Loader +from mitmproxy.contentviews.grpc import ProtoParser +from mitmproxy.contentviews.grpc import ViewConfig +from mitmproxy.contentviews.grpc import ViewGrpcProtobuf config: ViewConfig = ViewConfig() config.parser_rules = [ @@ -68,13 +72,13 @@ tag_prefixes=["1.5.1", "1.5.3", "1.5.4", "1.5.5", "1.5.6"], name="latitude", intended_decoding=ProtoParser.DecodedTypes.double, - ), # noqa: E501 + ), ProtoParser.ParserFieldDefinition( tag=".2", tag_prefixes=["1.5.1", "1.5.3", "1.5.4", "1.5.5", "1.5.6"], name="longitude", intended_decoding=ProtoParser.DecodedTypes.double, - ), # noqa: E501 + ), ProtoParser.ParserFieldDefinition(tag="7", name="app"), ], ), @@ -100,7 +104,7 @@ def render_priority(self, *args, **kwargs) -> float: view = ViewGrpcWithRules() -def load(l): +def load(loader: Loader): contentviews.add(view) diff --git a/examples/addons/contentview.py b/examples/addons/contentview.py index a485c25a81..7d34874200 100644 --- a/examples/addons/contentview.py +++ b/examples/addons/contentview.py @@ -5,10 +5,11 @@ which is used to pretty-print HTTP bodies for example. The content view API is explained in the mitmproxy.contentviews module. """ -from typing import Optional -from mitmproxy import contentviews, flow +from mitmproxy import contentviews +from mitmproxy import flow from mitmproxy import http +from mitmproxy.addonmanager import Loader class ViewSwapCase(contentviews.View): @@ -18,9 +19,9 @@ def __call__( self, data: bytes, *, - content_type: Optional[str] = None, - flow: Optional[flow.Flow] = None, - http_message: Optional[http.Message] = None, + content_type: str | None = None, + flow: flow.Flow | None = None, + http_message: http.Message | None = None, **unknown_metadata, ) -> contentviews.TViewResult: return "case-swapped text", contentviews.format_text(data.swapcase()) @@ -29,9 +30,9 @@ def render_priority( self, data: bytes, *, - content_type: Optional[str] = None, - flow: Optional[flow.Flow] = None, - http_message: Optional[http.Message] = None, + content_type: str | None = None, + flow: flow.Flow | None = None, + http_message: http.Message | None = None, **unknown_metadata, ) -> float: if content_type == "text/plain": @@ -43,7 +44,7 @@ def render_priority( view = ViewSwapCase() -def load(l): +def load(loader: Loader): contentviews.add(view) diff --git a/examples/addons/duplicate-modify-replay.py b/examples/addons/duplicate-modify-replay.py index 7138e5b6f3..82f4ee4088 100644 --- a/examples/addons/duplicate-modify-replay.py +++ b/examples/addons/duplicate-modify-replay.py @@ -1,4 +1,5 @@ """Take incoming HTTP requests and replay them with modified parameters.""" + from mitmproxy import ctx @@ -10,6 +11,6 @@ def request(flow): # Only interactive tools have a view. If we have one, add a duplicate entry # for our flow. if "view" in ctx.master.addons: - ctx.master.commands.call("view.flows.add", [flow]) + ctx.master.commands.call("view.flows.duplicate", [flow]) flow.request.path = "/changed" ctx.master.commands.call("replay.client", [flow]) diff --git a/examples/addons/filter-flows.py b/examples/addons/filter-flows.py index 94ace9d99e..43d8737492 100644 --- a/examples/addons/filter-flows.py +++ b/examples/addons/filter-flows.py @@ -1,22 +1,25 @@ """ Use mitmproxy's filter pattern in scripts. """ + +from __future__ import annotations + import logging -from mitmproxy import ctx, http from mitmproxy import flowfilter +from mitmproxy import http +from mitmproxy.addonmanager import Loader class Filter: - def __init__(self): - self.filter: flowfilter.TFilter = None + filter: flowfilter.TFilter def configure(self, updated): if "flowfilter" in updated: - self.filter = flowfilter.parse(ctx.options.flowfilter) + self.filter = flowfilter.parse(".") - def load(self, l): - l.add_option("flowfilter", str, "", "Check that flow matches filter.") + def load(self, loader: Loader): + loader.add_option("flowfilter", str, "", "Check that flow matches filter.") def response(self, flow: http.HTTPFlow) -> None: if flowfilter.match(self.filter, flow): diff --git a/examples/addons/http-modify-form.py b/examples/addons/http-modify-form.py index b4ad178fdb..7586adba71 100644 --- a/examples/addons/http-modify-form.py +++ b/examples/addons/http-modify-form.py @@ -1,4 +1,5 @@ """Modify an HTTP form submission.""" + from mitmproxy import http @@ -9,4 +10,4 @@ def request(flow: http.HTTPFlow) -> None: else: # One can also just pass new form data. # This sets the proper content type and overrides the body. - flow.request.urlencoded_form = [("foo", "bar")] + flow.request.urlencoded_form = [("foo", "bar")] # type: ignore[assignment] diff --git a/examples/addons/http-modify-query-string.py b/examples/addons/http-modify-query-string.py index 0139769d17..6908b72ce1 100644 --- a/examples/addons/http-modify-query-string.py +++ b/examples/addons/http-modify-query-string.py @@ -1,4 +1,5 @@ """Modify HTTP query parameters.""" + from mitmproxy import http diff --git a/examples/addons/http-redirect-requests.py b/examples/addons/http-redirect-requests.py index c5908aa49f..48390d5ad8 100644 --- a/examples/addons/http-redirect-requests.py +++ b/examples/addons/http-redirect-requests.py @@ -1,4 +1,5 @@ """Redirect HTTP requests to another server.""" + from mitmproxy import http diff --git a/examples/addons/http-reply-from-proxy.py b/examples/addons/http-reply-from-proxy.py index 3ce35c5a4f..485fb499b8 100644 --- a/examples/addons/http-reply-from-proxy.py +++ b/examples/addons/http-reply-from-proxy.py @@ -1,4 +1,5 @@ -"""Send a reply from the proxy without sending any data to the remote server.""" +"""Send a reply from the proxy without sending the request to the remote server.""" + from mitmproxy import http diff --git a/examples/addons/http-stream-modify.py b/examples/addons/http-stream-modify.py index a200fe5631..11c26e4af2 100644 --- a/examples/addons/http-stream-modify.py +++ b/examples/addons/http-stream-modify.py @@ -7,10 +7,11 @@ - If you want to replace all occurrences of "foobar", make sure to catch the cases where one chunk ends with [...]foo" and the next starts with "bar[...]. """ -from typing import Iterable, Union +from collections.abc import Iterable -def modify(data: bytes) -> Union[bytes, Iterable[bytes]]: + +def modify(data: bytes) -> bytes | Iterable[bytes]: """ This function will be called for each chunk of request/response body data that arrives at the proxy, and once at the end of the message with an empty bytes argument (b""). diff --git a/examples/addons/http-trailers.py b/examples/addons/http-trailers.py index 26a51f23bd..5822389a1b 100644 --- a/examples/addons/http-trailers.py +++ b/examples/addons/http-trailers.py @@ -34,6 +34,7 @@ def request(flow: http.HTTPFlow): def response(flow: http.HTTPFlow): + assert flow.response if flow.response.trailers: print("HTTP Trailers detected! Response contains:", flow.response.trailers) diff --git a/examples/addons/internet-in-mirror.py b/examples/addons/internet-in-mirror.py index 749eb1f322..e0abf88177 100644 --- a/examples/addons/internet-in-mirror.py +++ b/examples/addons/internet-in-mirror.py @@ -3,6 +3,7 @@ Useful if you are living down under. """ + from mitmproxy import http diff --git a/examples/addons/io-read-saved-flows.py b/examples/addons/io-read-saved-flows.py index f6a177be4c..9fad123629 100644 --- a/examples/addons/io-read-saved-flows.py +++ b/examples/addons/io-read-saved-flows.py @@ -2,11 +2,14 @@ """ Read a mitmproxy dump file. """ -from mitmproxy import io, http -from mitmproxy.exceptions import FlowReadException + import pprint import sys +from mitmproxy import http +from mitmproxy import io +from mitmproxy.exceptions import FlowReadException + with open(sys.argv[1], "rb") as logfile: freader = io.FlowReader(logfile) pp = pprint.PrettyPrinter(indent=4) diff --git a/examples/addons/io-write-flow-file.py b/examples/addons/io-write-flow-file.py index ecc0528e7f..76fcbb46e5 100644 --- a/examples/addons/io-write-flow-file.py +++ b/examples/addons/io-write-flow-file.py @@ -7,16 +7,21 @@ flows should be saved and also allows you to rotate files or log to multiple files in parallel. """ + +import os import random -import sys from typing import BinaryIO -from mitmproxy import io, http +from mitmproxy import http +from mitmproxy import io class Writer: - def __init__(self, path: str) -> None: - self.f: BinaryIO = open(path, "wb") + def __init__(self) -> None: + # We are using an environment variable to keep the example as simple as possible, + # consider implementing this as a mitmproxy option instead. + filename = os.getenv("MITMPROXY_OUTFILE", "out.mitm") + self.f: BinaryIO = open(filename, "wb") self.w = io.FlowWriter(self.f) def response(self, flow: http.HTTPFlow) -> None: @@ -27,4 +32,4 @@ def done(self): self.f.close() -addons = [Writer(sys.argv[1])] +addons = [Writer()] diff --git a/examples/addons/log-events.py b/examples/addons/log-events.py index f5a1c91b29..08a6225be7 100644 --- a/examples/addons/log-events.py +++ b/examples/addons/log-events.py @@ -1,11 +1,18 @@ """Post messages to mitmproxy's event log.""" + import logging +from mitmproxy.addonmanager import Loader from mitmproxy.log import ALERT +logger = logging.getLogger(__name__) + -def load(l): - logging.info("This is some informative text.") - logging.warning("This is a warning.") - logging.error("This is an error.") - logging.log(ALERT, "This is an alert. It has the same urgency as info, but will also pop up in the status bar.") +def load(loader: Loader): + logger.info("This is some informative text.") + logger.warning("This is a warning.") + logger.error("This is an error.") + logger.log( + ALERT, + "This is an alert. It has the same urgency as info, but will also pop up in the status bar.", + ) diff --git a/examples/addons/nonblocking.py b/examples/addons/nonblocking.py index ae59db80a3..1be8cd7310 100644 --- a/examples/addons/nonblocking.py +++ b/examples/addons/nonblocking.py @@ -1,27 +1,27 @@ """ -Make events hooks non-blocking using async or @concurrent +Make events hooks non-blocking using async or @concurrent. """ + import asyncio import logging - import time from mitmproxy.script import concurrent +# Toggle between asyncio and thread-based alternatives. +if True: + # Hooks can be async, which allows the hook to call async functions and perform async I/O + # without blocking other requests. This is generally preferred for new addons. + async def request(flow): + logging.info(f"handle request: {flow.request.host}{flow.request.path}") + await asyncio.sleep(5) + logging.info(f"start request: {flow.request.host}{flow.request.path}") -# Hooks can be async, which allows the hook to call async functions and perform async I/O -# without blocking other requests. This is generally preferred for new addons. -async def request(flow): - logging.info(f"handle request: {flow.request.host}{flow.request.path}") - await asyncio.sleep(5) - logging.info(f"start request: {flow.request.host}{flow.request.path}") - - -# Another option is to use @concurrent, which launches the hook in its own thread. -# Please note that this generally opens the door to race conditions and decreases performance if not required. -# Rename the function below to request(flow) to try it out. -@concurrent # Remove this to make it synchronous and see what happens -def request_concurrent(flow): - logging.info(f"handle request: {flow.request.host}{flow.request.path}") - time.sleep(5) - logging.info(f"start request: {flow.request.host}{flow.request.path}") +else: + # Another option is to use @concurrent, which launches the hook in its own thread. + # Please note that this generally opens the door to race conditions and decreases performance if not required. + @concurrent # Remove this to make it synchronous and see what happens + def request(flow): + logging.info(f"handle request: {flow.request.host}{flow.request.path}") + time.sleep(5) + logging.info(f"start request: {flow.request.host}{flow.request.path}") diff --git a/examples/addons/options-configure.py b/examples/addons/options-configure.py index 81551b3dee..70efbf1e9c 100644 --- a/examples/addons/options-configure.py +++ b/examples/addons/options-configure.py @@ -1,4 +1,5 @@ """React to configuration changes.""" + from typing import Optional from mitmproxy import ctx diff --git a/examples/addons/options-simple.py b/examples/addons/options-simple.py index 6895a23be0..88ead1ee4e 100644 --- a/examples/addons/options-simple.py +++ b/examples/addons/options-simple.py @@ -5,6 +5,7 @@ mitmproxy -s options-simple.py --set addheader=true """ + from mitmproxy import ctx diff --git a/examples/addons/shutdown.py b/examples/addons/shutdown.py index 6a6d5069ad..cc8520037c 100644 --- a/examples/addons/shutdown.py +++ b/examples/addons/shutdown.py @@ -8,9 +8,11 @@ and then send a HTTP request to trigger the shutdown: curl --proxy localhost:8080 http://example.com/path """ + import logging -from mitmproxy import ctx, http +from mitmproxy import ctx +from mitmproxy import http def request(flow: http.HTTPFlow) -> None: diff --git a/examples/addons/tcp-simple.py b/examples/addons/tcp-simple.py index 242e971403..11a464522d 100644 --- a/examples/addons/tcp-simple.py +++ b/examples/addons/tcp-simple.py @@ -1,19 +1,20 @@ """ Process individual messages from a TCP connection. -This script replaces full occurences of "foo" with "bar" and prints various details for each message. +This script replaces full occurrences of "foo" with "bar" and prints various details for each message. Please note that TCP is stream-based and *not* message-based. mitmproxy splits stream contents into "messages" as they are received by socket.recv(). This is pretty arbitrary and should not be relied on. However, it is sometimes good enough as a quick hack. Example Invocation: - mitmdump --rawtcp --tcp-hosts ".*" -s examples/tcp-simple.py + mitmdump --tcp-hosts ".*" -s examples/tcp-simple.py """ + import logging -from mitmproxy.utils import strutils from mitmproxy import tcp +from mitmproxy.utils import strutils def tcp_message(flow: tcp.TCPFlow): diff --git a/examples/addons/websocket-inject-message.py b/examples/addons/websocket-inject-message.py index a0b73d24c2..a2aa771ea7 100644 --- a/examples/addons/websocket-inject-message.py +++ b/examples/addons/websocket-inject-message.py @@ -3,10 +3,11 @@ This example shows how to inject a WebSocket message into a running connection. """ -import asyncio -from mitmproxy import ctx, http +import asyncio +from mitmproxy import ctx +from mitmproxy import http # Simple example: Inject a message as a response to an event @@ -33,5 +34,12 @@ async def inject_async(flow: http.HTTPFlow): msg = msg[1:] + msg[:1] +# Python 3.11: replace with TaskGroup +tasks = set() + + def websocket_start(flow: http.HTTPFlow): - asyncio.create_task(inject_async(flow)) + # we need to hold a reference to the task, otherwise it will be garbage collected. + t = asyncio.create_task(inject_async(flow)) + tasks.add(t) + t.add_done_callback(tasks.remove) diff --git a/examples/addons/websocket-simple.py b/examples/addons/websocket-simple.py index 8ced159106..9ec1e6b956 100644 --- a/examples/addons/websocket-simple.py +++ b/examples/addons/websocket-simple.py @@ -1,4 +1,5 @@ """Process individual messages from a WebSocket connection.""" + import logging import re diff --git a/examples/addons/wsgi-flask-app.py b/examples/addons/wsgi-flask-app.py index 4f117f05ab..01da441dea 100644 --- a/examples/addons/wsgi-flask-app.py +++ b/examples/addons/wsgi-flask-app.py @@ -5,7 +5,9 @@ instance, we're using the Flask framework (http://flask.pocoo.org/) to expose a single simplest-possible page. """ + from flask import Flask + from mitmproxy.addons import asgiapp app = Flask("proxapp") @@ -24,5 +26,4 @@ def hello_world() -> str: # mitmproxy will connect to said domain and use its certificate but won't send any data. # By using `--set upstream_cert=false` and `--set connection_strategy_lazy` the local certificate is used instead. # asgiapp.WSGIApp(app, "example.com", 443), - ] diff --git a/examples/contrib/README.md b/examples/contrib/README.md index d15034c75b..72c56f4e45 100644 --- a/examples/contrib/README.md +++ b/examples/contrib/README.md @@ -6,6 +6,7 @@ If you developed something thats useful for a wider audience, please add it here ### Additional Examples Hosted Externally - [**wsreplay.py**](https://github.com/KOLANICH-tools/wsreplay.py): a simple tool to replay WebSocket streams + - [Mitmproxy Plugin for Hackers](https://git.sr.ht/~rek2/mitmproxy_hacking) A plugin useful of other Hackers,Pentesters,CTF players, BugHunters etc # Maintenance diff --git a/examples/contrib/all_markers.py b/examples/contrib/all_markers.py index 4e9043f330..a2614259d9 100644 --- a/examples/contrib/all_markers.py +++ b/examples/contrib/all_markers.py @@ -1,10 +1,13 @@ -from mitmproxy import ctx, command +from mitmproxy import command +from mitmproxy import ctx from mitmproxy.utils import emoji -@command.command('all.markers') +@command.command("all.markers") def all_markers(): - 'Create a new flow showing all marker values' + "Create a new flow showing all marker values" for marker in emoji.emoji: - ctx.master.commands.call('view.flows.create', 'get', f'https://example.com/{marker}') - ctx.master.commands.call('flow.mark', [ctx.master.view.focus.flow], marker) + ctx.master.commands.call( + "view.flows.create", "get", f"https://example.com/{marker}" + ) + ctx.master.commands.call("flow.mark", [ctx.master.view[-1]], marker) diff --git a/examples/contrib/block_dns_over_https.py b/examples/contrib/block_dns_over_https.py index 4fe71c44c7..86b172cc37 100644 --- a/examples/contrib/block_dns_over_https.py +++ b/examples/contrib/block_dns_over_https.py @@ -4,84 +4,256 @@ It loads a blocklist of IPs and hostnames that are known to serve DNS over HTTPS requests. It also uses headers, query params, and paths to detect DoH (and block it) """ + import logging # known DoH providers' hostnames and IP addresses to block default_blocklist: dict = { "hostnames": [ - "dns.adguard.com", "dns-family.adguard.com", "dns.google", "cloudflare-dns.com", - "mozilla.cloudflare-dns.com", "security.cloudflare-dns.com", "family.cloudflare-dns.com", - "dns.quad9.net", "dns9.quad9.net", "dns10.quad9.net", "dns11.quad9.net", "doh.opendns.com", - "doh.familyshield.opendns.com", "doh.cleanbrowsing.org", "doh.xfinity.com", "dohdot.coxlab.net", - "odvr.nic.cz", "doh.dnslify.com", "dns.nextdns.io", "dns.dnsoverhttps.net", "doh.crypto.sx", - "doh.powerdns.org", "doh-fi.blahdns.com", "doh-jp.blahdns.com", "doh-de.blahdns.com", - "doh.ffmuc.net", "dns.dns-over-https.com", "doh.securedns.eu", "dns.rubyfish.cn", - "dns.containerpi.com", "dns.containerpi.com", "dns.containerpi.com", "doh-2.seby.io", - "doh.seby.io", "commons.host", "doh.dnswarden.com", "doh.dnswarden.com", "doh.dnswarden.com", - "dns-nyc.aaflalo.me", "dns.aaflalo.me", "doh.applied-privacy.net", "doh.captnemo.in", - "doh.tiar.app", "doh.tiarap.org", "doh.dns.sb", "rdns.faelix.net", "doh.li", "doh.armadillodns.net", - "jp.tiar.app", "jp.tiarap.org", "doh.42l.fr", "dns.hostux.net", "dns.hostux.net", "dns.aa.net.uk", - "adblock.mydns.network", "ibksturm.synology.me", "jcdns.fun", "ibuki.cgnat.net", "dns.twnic.tw", - "example.doh.blockerdns.com", "dns.digitale-gesellschaft.ch", "doh.libredns.gr", - "doh.centraleu.pi-dns.com", "doh.northeu.pi-dns.com", "doh.westus.pi-dns.com", - "doh.eastus.pi-dns.com", "dns.flatuslifir.is", "private.canadianshield.cira.ca", - "protected.canadianshield.cira.ca", "family.canadianshield.cira.ca", "dns.google.com", - "dns.google.com" + "dns.adguard.com", + "dns-family.adguard.com", + "dns.google", + "cloudflare-dns.com", + "mozilla.cloudflare-dns.com", + "security.cloudflare-dns.com", + "family.cloudflare-dns.com", + "dns.quad9.net", + "dns9.quad9.net", + "dns10.quad9.net", + "dns11.quad9.net", + "doh.opendns.com", + "doh.familyshield.opendns.com", + "doh.cleanbrowsing.org", + "doh.xfinity.com", + "dohdot.coxlab.net", + "odvr.nic.cz", + "doh.dnslify.com", + "dns.nextdns.io", + "dns.dnsoverhttps.net", + "doh.crypto.sx", + "doh.powerdns.org", + "doh-fi.blahdns.com", + "doh-jp.blahdns.com", + "doh-de.blahdns.com", + "doh.ffmuc.net", + "dns.dns-over-https.com", + "doh.securedns.eu", + "dns.rubyfish.cn", + "dns.containerpi.com", + "dns.containerpi.com", + "dns.containerpi.com", + "doh-2.seby.io", + "doh.seby.io", + "commons.host", + "doh.dnswarden.com", + "doh.dnswarden.com", + "doh.dnswarden.com", + "dns-nyc.aaflalo.me", + "dns.aaflalo.me", + "doh.applied-privacy.net", + "doh.captnemo.in", + "doh.tiar.app", + "doh.tiarap.org", + "doh.dns.sb", + "rdns.faelix.net", + "doh.li", + "doh.armadillodns.net", + "jp.tiar.app", + "jp.tiarap.org", + "doh.42l.fr", + "dns.hostux.net", + "dns.hostux.net", + "dns.aa.net.uk", + "adblock.mydns.network", + "ibksturm.synology.me", + "jcdns.fun", + "ibuki.cgnat.net", + "dns.twnic.tw", + "example.doh.blockerdns.com", + "dns.digitale-gesellschaft.ch", + "doh.libredns.gr", + "doh.centraleu.pi-dns.com", + "doh.northeu.pi-dns.com", + "doh.westus.pi-dns.com", + "doh.eastus.pi-dns.com", + "dns.flatuslifir.is", + "private.canadianshield.cira.ca", + "protected.canadianshield.cira.ca", + "family.canadianshield.cira.ca", + "dns.google.com", + "dns.google.com", ], "ips": [ - "104.16.248.249", "104.16.248.249", "104.16.249.249", "104.16.249.249", "104.18.2.55", - "104.18.26.128", "104.18.27.128", "104.18.3.55", "104.18.44.204", "104.18.44.204", - "104.18.45.204", "104.18.45.204", "104.182.57.196", "104.236.178.232", "104.24.122.53", - "104.24.123.53", "104.28.0.106", "104.28.1.106", "104.31.90.138", "104.31.91.138", - "115.159.131.230", "116.202.176.26", "116.203.115.192", "136.144.215.158", "139.59.48.222", - "139.99.222.72", "146.112.41.2", "146.112.41.3", "146.185.167.43", "149.112.112.10", - "149.112.112.11", "149.112.112.112", "149.112.112.9", "149.112.121.10", "149.112.121.20", - "149.112.121.30", "149.112.122.10", "149.112.122.20", "149.112.122.30", "159.69.198.101", - "168.235.81.167", "172.104.93.80", "172.65.3.223", "174.138.29.175", "174.68.248.77", - "176.103.130.130", "176.103.130.131", "176.103.130.132", "176.103.130.134", "176.56.236.175", - "178.62.214.105", "185.134.196.54", "185.134.197.54", "185.213.26.187", "185.216.27.142", - "185.228.168.10", "185.228.168.168", "185.235.81.1", "185.26.126.37", "185.26.126.37", - "185.43.135.1", "185.95.218.42", "185.95.218.43", "195.30.94.28", "2001:148f:fffe::1", - "2001:19f0:7001:3259:5400:2ff:fe71:bc9", "2001:19f0:7001:5554:5400:2ff:fe57:3077", - "2001:19f0:7001:5554:5400:2ff:fe57:3077", "2001:19f0:7001:5554:5400:2ff:fe57:3077", - "2001:4860:4860::8844", "2001:4860:4860::8888", - "2001:4b98:dc2:43:216:3eff:fe86:1d28", "2001:558:fe21:6b:96:113:151:149", - "2001:608:a01::3", "2001:678:888:69:c45d:2738:c3f2:1878", "2001:8b0::2022", "2001:8b0::2023", - "2001:c50:ffff:1:101:101:101:101", "210.17.9.228", "217.169.20.22", "217.169.20.23", - "2400:6180:0:d0::5f73:4001", "2400:8902::f03c:91ff:feda:c514", "2604:180:f3::42", - "2604:a880:1:20::51:f001", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9", "2606:4700::6812:1a80", - "2606:4700::6812:1b80", "2606:4700::6812:237", "2606:4700::6812:337", "2606:4700:3033::6812:2ccc", - "2606:4700:3033::6812:2dcc", "2606:4700:3033::6818:7b35", "2606:4700:3034::681c:16a", - "2606:4700:3035::6818:7a35", "2606:4700:3035::681f:5a8a", "2606:4700:3036::681c:6a", - "2606:4700:3036::681f:5b8a", "2606:4700:60:0:a71e:6467:cef8:2a56", "2620:10a:80bb::10", - "2620:10a:80bb::20", "2620:10a:80bb::30" "2620:10a:80bc::10", "2620:10a:80bc::20", - "2620:10a:80bc::30", "2620:119:fc::2", "2620:119:fc::3", "2620:fe::10", "2620:fe::11", - "2620:fe::9", "2620:fe::fe:10", "2620:fe::fe:11", "2620:fe::fe:9", "2620:fe::fe", - "2a00:5a60::ad1:ff", "2a00:5a60::ad2:ff", "2a00:5a60::bad1:ff", "2a00:5a60::bad2:ff", - "2a00:d880:5:bf0::7c93", "2a01:4f8:1c0c:8233::1", "2a01:4f8:1c1c:6b4b::1", "2a01:4f8:c2c:52bf::1", - "2a01:4f9:c010:43ce::1", "2a01:4f9:c01f:4::abcd", "2a01:7c8:d002:1ef:5054:ff:fe40:3703", - "2a01:9e00::54", "2a01:9e00::55", "2a01:9e01::54", "2a01:9e01::55", - "2a02:1205:34d5:5070:b26e:bfff:fe1d:e19b", "2a03:4000:38:53c::2", - "2a03:b0c0:0:1010::e9a:3001", "2a04:bdc7:100:70::abcd", "2a05:fc84::42", "2a05:fc84::43", - "2a07:a8c0::", "2a0d:4d00:81::1", "2a0d:5600:33:3::abcd", "35.198.2.76", "35.231.247.227", - "45.32.55.94", "45.67.219.208", "45.76.113.31", "45.77.180.10", "45.90.28.0", - "46.101.66.244", "46.227.200.54", "46.227.200.55", "46.239.223.80", "8.8.4.4", - "8.8.8.8", "83.77.85.7", "88.198.91.187", "9.9.9.10", "9.9.9.11", "9.9.9.9", - "94.130.106.88", "95.216.181.228", "95.216.212.177", "96.113.151.148", - ] + "104.16.248.249", + "104.16.248.249", + "104.16.249.249", + "104.16.249.249", + "104.18.2.55", + "104.18.26.128", + "104.18.27.128", + "104.18.3.55", + "104.18.44.204", + "104.18.44.204", + "104.18.45.204", + "104.18.45.204", + "104.182.57.196", + "104.236.178.232", + "104.24.122.53", + "104.24.123.53", + "104.28.0.106", + "104.28.1.106", + "104.31.90.138", + "104.31.91.138", + "115.159.131.230", + "116.202.176.26", + "116.203.115.192", + "136.144.215.158", + "139.59.48.222", + "139.99.222.72", + "146.112.41.2", + "146.112.41.3", + "146.185.167.43", + "149.112.112.10", + "149.112.112.11", + "149.112.112.112", + "149.112.112.9", + "149.112.121.10", + "149.112.121.20", + "149.112.121.30", + "149.112.122.10", + "149.112.122.20", + "149.112.122.30", + "159.69.198.101", + "168.235.81.167", + "172.104.93.80", + "172.65.3.223", + "174.138.29.175", + "174.68.248.77", + "176.103.130.130", + "176.103.130.131", + "176.103.130.132", + "176.103.130.134", + "176.56.236.175", + "178.62.214.105", + "185.134.196.54", + "185.134.197.54", + "185.213.26.187", + "185.216.27.142", + "185.228.168.10", + "185.228.168.168", + "185.235.81.1", + "185.26.126.37", + "185.26.126.37", + "185.43.135.1", + "185.95.218.42", + "185.95.218.43", + "195.30.94.28", + "2001:148f:fffe::1", + "2001:19f0:7001:3259:5400:2ff:fe71:bc9", + "2001:19f0:7001:5554:5400:2ff:fe57:3077", + "2001:19f0:7001:5554:5400:2ff:fe57:3077", + "2001:19f0:7001:5554:5400:2ff:fe57:3077", + "2001:4860:4860::8844", + "2001:4860:4860::8888", + "2001:4b98:dc2:43:216:3eff:fe86:1d28", + "2001:558:fe21:6b:96:113:151:149", + "2001:608:a01::3", + "2001:678:888:69:c45d:2738:c3f2:1878", + "2001:8b0::2022", + "2001:8b0::2023", + "2001:c50:ffff:1:101:101:101:101", + "210.17.9.228", + "217.169.20.22", + "217.169.20.23", + "2400:6180:0:d0::5f73:4001", + "2400:8902::f03c:91ff:feda:c514", + "2604:180:f3::42", + "2604:a880:1:20::51:f001", + "2606:4700::6810:f8f9", + "2606:4700::6810:f9f9", + "2606:4700::6812:1a80", + "2606:4700::6812:1b80", + "2606:4700::6812:237", + "2606:4700::6812:337", + "2606:4700:3033::6812:2ccc", + "2606:4700:3033::6812:2dcc", + "2606:4700:3033::6818:7b35", + "2606:4700:3034::681c:16a", + "2606:4700:3035::6818:7a35", + "2606:4700:3035::681f:5a8a", + "2606:4700:3036::681c:6a", + "2606:4700:3036::681f:5b8a", + "2606:4700:60:0:a71e:6467:cef8:2a56", + "2620:10a:80bb::10", + "2620:10a:80bb::20", + "2620:10a:80bb::30" "2620:10a:80bc::10", + "2620:10a:80bc::20", + "2620:10a:80bc::30", + "2620:119:fc::2", + "2620:119:fc::3", + "2620:fe::10", + "2620:fe::11", + "2620:fe::9", + "2620:fe::fe:10", + "2620:fe::fe:11", + "2620:fe::fe:9", + "2620:fe::fe", + "2a00:5a60::ad1:ff", + "2a00:5a60::ad2:ff", + "2a00:5a60::bad1:ff", + "2a00:5a60::bad2:ff", + "2a00:d880:5:bf0::7c93", + "2a01:4f8:1c0c:8233::1", + "2a01:4f8:1c1c:6b4b::1", + "2a01:4f8:c2c:52bf::1", + "2a01:4f9:c010:43ce::1", + "2a01:4f9:c01f:4::abcd", + "2a01:7c8:d002:1ef:5054:ff:fe40:3703", + "2a01:9e00::54", + "2a01:9e00::55", + "2a01:9e01::54", + "2a01:9e01::55", + "2a02:1205:34d5:5070:b26e:bfff:fe1d:e19b", + "2a03:4000:38:53c::2", + "2a03:b0c0:0:1010::e9a:3001", + "2a04:bdc7:100:70::abcd", + "2a05:fc84::42", + "2a05:fc84::43", + "2a07:a8c0::", + "2a0d:4d00:81::1", + "2a0d:5600:33:3::abcd", + "35.198.2.76", + "35.231.247.227", + "45.32.55.94", + "45.67.219.208", + "45.76.113.31", + "45.77.180.10", + "45.90.28.0", + "46.101.66.244", + "46.227.200.54", + "46.227.200.55", + "46.239.223.80", + "8.8.4.4", + "8.8.8.8", + "83.77.85.7", + "88.198.91.187", + "9.9.9.10", + "9.9.9.11", + "9.9.9.9", + "94.130.106.88", + "95.216.181.228", + "95.216.212.177", + "96.113.151.148", + ], } # additional hostnames to block -additional_doh_names: list[str] = [ - 'dns.google.com' -] +additional_doh_names: list[str] = ["dns.google.com"] # additional IPs to block -additional_doh_ips: list[str] = [ - -] +additional_doh_ips: list[str] = [] -doh_hostnames, doh_ips = default_blocklist['hostnames'], default_blocklist['ips'] +doh_hostnames, doh_ips = default_blocklist["hostnames"], default_blocklist["ips"] # convert to sets for faster lookups doh_hostnames = set(doh_hostnames) @@ -95,9 +267,9 @@ def _has_dns_message_content_type(flow): :param flow: mitmproxy flow :return: True if 'Content-Type' header is DNS-looking, False otherwise """ - doh_content_types = ['application/dns-message'] - if 'Content-Type' in flow.request.headers: - if flow.request.headers['Content-Type'] in doh_content_types: + doh_content_types = ["application/dns-message"] + if "Content-Type" in flow.request.headers: + if flow.request.headers["Content-Type"] in doh_content_types: return True return False @@ -109,7 +281,7 @@ def _request_has_dns_query_string(flow): :param flow: mitmproxy flow :return: True is 'dns' is a parameter in the query string, False otherwise """ - return 'dns' in flow.request.query + return "dns" in flow.request.query def _request_is_dns_json(flow): @@ -127,12 +299,12 @@ def _request_is_dns_json(flow): """ # Header 'Accept: application/dns-json' is required in Cloudflare's DoH JSON API # or they return a 400 HTTP response code - if 'Accept' in flow.request.headers: - if flow.request.headers['Accept'] == 'application/dns-json': + if "Accept" in flow.request.headers: + if flow.request.headers["Accept"] == "application/dns-json": return True # Google's DoH JSON API is https://dns.google/resolve - path = flow.request.path.split('?')[0] - if flow.request.host == 'dns.google' and path == '/resolve': + path = flow.request.path.split("?")[0] + if flow.request.host == "dns.google" and path == "/resolve": return True return False @@ -146,9 +318,9 @@ def _request_has_doh_looking_path(flow): :return: True if path looks like it's DoH, otherwise False """ doh_paths = [ - '/dns-query', # used in example in RFC 8484 (see https://tools.ietf.org/html/rfc8484#section-4.1.1) + "/dns-query", # used in example in RFC 8484 (see https://tools.ietf.org/html/rfc8484#section-4.1.1) ] - path = flow.request.path.split('?')[0] + path = flow.request.path.split("?")[0] return path in doh_paths @@ -171,7 +343,7 @@ def _requested_hostname_is_in_doh_blocklist(flow): _request_has_dns_query_string, _request_is_dns_json, _requested_hostname_is_in_doh_blocklist, - _request_has_doh_looking_path + _request_has_doh_looking_path, ] @@ -179,6 +351,9 @@ def request(flow): for check in doh_request_detection_checks: is_doh = check(flow) if is_doh: - logging.warning("[DoH Detection] DNS over HTTPS request detected via method \"%s\"" % check.__name__) + logging.warning( + '[DoH Detection] DNS over HTTPS request detected via method "%s"' + % check.__name__ + ) flow.kill() break diff --git a/examples/contrib/change_upstream_proxy.py b/examples/contrib/change_upstream_proxy.py index ddcbabf100..682f02e86f 100644 --- a/examples/contrib/change_upstream_proxy.py +++ b/examples/contrib/change_upstream_proxy.py @@ -1,9 +1,7 @@ - from mitmproxy import http from mitmproxy.connection import Server from mitmproxy.net.server_spec import ServerSpec - # This scripts demonstrates how mitmproxy can switch to a second/different upstream proxy # in upstream proxy mode. # @@ -32,5 +30,5 @@ def request(flow: http.HTTPFlow) -> None: if is_proxy_change and server_connection_already_open: # server_conn already refers to an existing connection (which cannot be modified), # so we need to replace it with a new server connection object. - flow.server_conn = Server(flow.server_conn.address) - flow.server_conn.via = ServerSpec("http", address) + flow.server_conn = Server(address=flow.server_conn.address) + flow.server_conn.via = ServerSpec(("http", address)) diff --git a/examples/contrib/check_ssl_pinning.py b/examples/contrib/check_ssl_pinning.py index 8bc0b24aab..b70a62e8f0 100644 --- a/examples/contrib/check_ssl_pinning.py +++ b/examples/contrib/check_ssl_pinning.py @@ -1,14 +1,16 @@ -import mitmproxy -from mitmproxy import ctx -from mitmproxy.certs import Cert import ipaddress -import OpenSSL import time +import OpenSSL + +import mitmproxy +from mitmproxy import ctx +from mitmproxy.certs import Cert # Certificate for client connection is generated in dummy_cert() in certs.py. Monkeypatching # the function to generate test cases for SSL Pinning. + def monkey_dummy_cert(privkey, cacert, commonname, sans): ss = [] for i in sans: @@ -42,7 +44,7 @@ def monkey_dummy_cert(privkey, cacert, commonname, sans): if ctx.options.certwrongCN: # append an extra char to make certs common name different than original one. # APpending a char in the end of the domain name. - new_cn = commonname + b'm' + new_cn = commonname + b"m" cert.get_subject().CN = new_cn else: @@ -52,7 +54,8 @@ def monkey_dummy_cert(privkey, cacert, commonname, sans): if ss: cert.set_version(2) cert.add_extensions( - [OpenSSL.crypto.X509Extension(b"subjectAltName", False, ss)]) + [OpenSSL.crypto.X509Extension(b"subjectAltName", False, ss)] + ) cert.set_pubkey(cacert.get_pubkey()) cert.sign(privkey, "sha256") return Cert(cert) @@ -61,23 +64,29 @@ def monkey_dummy_cert(privkey, cacert, commonname, sans): class CheckSSLPinning: def load(self, loader): loader.add_option( - "certbeginon", bool, False, + "certbeginon", + bool, + False, """ Sets SSL Certificate's 'Begins On' time in future. - """ + """, ) loader.add_option( - "certexpire", bool, False, + "certexpire", + bool, + False, """ Sets SSL Certificate's 'Expires On' time in the past. - """ + """, ) loader.add_option( - "certwrongCN", bool, False, + "certwrongCN", + bool, + False, """ Sets SSL Certificate's CommonName(CN) different from the domain name. - """ + """, ) def clientconnect(self, layer): diff --git a/examples/contrib/custom_next_layer.py b/examples/contrib/custom_next_layer.py index 917272dcbd..ffc6f1bb99 100644 --- a/examples/contrib/custom_next_layer.py +++ b/examples/contrib/custom_next_layer.py @@ -8,10 +8,12 @@ - mitmdump -s custom_next_layer.py - curl -x localhost:8080 -k https://example.com """ + import logging from mitmproxy import ctx -from mitmproxy.proxy import layer, layers +from mitmproxy.proxy import layer +from mitmproxy.proxy import layers def running(): diff --git a/examples/contrib/dns_spoofing.py b/examples/contrib/dns_spoofing.py index 63222eae2d..304b9a5084 100644 --- a/examples/contrib/dns_spoofing.py +++ b/examples/contrib/dns_spoofing.py @@ -23,6 +23,7 @@ (Setting up a single proxy instance and using iptables to redirect to it works as well) """ + import re # This regex extracts splits the host header into host and port. @@ -35,7 +36,7 @@ class Rerouter: def request(self, flow): if flow.client_conn.tls_established: flow.request.scheme = "https" - sni = flow.client_conn.connection.get_servername() + sni = flow.client_conn.sni port = 443 else: flow.request.scheme = "http" diff --git a/examples/contrib/domain_fronting.py b/examples/contrib/domain_fronting.py index fd73d29856..b9a052d189 100644 --- a/examples/contrib/domain_fronting.py +++ b/examples/contrib/domain_fronting.py @@ -1,11 +1,10 @@ -from typing import Optional, Union import json from dataclasses import dataclass + from mitmproxy import ctx from mitmproxy.addonmanager import Loader from mitmproxy.http import HTTPFlow - """ This extension implements support for domain fronting. @@ -53,12 +52,11 @@ @dataclass class Mapping: - server: Union[str, None] - host: Union[str, None] + server: str | None + host: str | None class HttpsDomainFronting: - # configurations for regular ("foo.example.com") mappings: star_mappings: dict[str, Mapping] @@ -69,7 +67,7 @@ def __init__(self) -> None: self.strict_mappings = {} self.star_mappings = {} - def _resolve_addresses(self, host: str) -> Optional[Mapping]: + def _resolve_addresses(self, host: str) -> Mapping | None: mapping = self.strict_mappings.get(host) if mapping is not None: return mapping @@ -79,7 +77,7 @@ def _resolve_addresses(self, host: str) -> Optional[Mapping]: index = host.find(".", index) if index == -1: break - super_domain = host[(index + 1):] + super_domain = host[(index + 1) :] mapping = self.star_mappings.get(super_domain) if mapping is not None: return mapping diff --git a/examples/contrib/har_dump.py b/examples/contrib/har_dump.py index e1337af467..455597f186 100644 --- a/examples/contrib/har_dump.py +++ b/examples/contrib/har_dump.py @@ -1,256 +1,3 @@ """ -This inline script can be used to dump flows as HAR files. - -example cmdline invocation: -mitmdump -s ./har_dump.py --set hardump=./dump.har - -filename endwith '.zhar' will be compressed: -mitmdump -s ./har_dump.py --set hardump=./dump.zhar +This addon is now part of mitmproxy! See mitmproxy/addons/savehar.py. """ - -import base64 -import json -import logging -import os -from datetime import datetime -from datetime import timezone - -import zlib - -import mitmproxy -from mitmproxy import connection -from mitmproxy import ctx -from mitmproxy import version -from mitmproxy.net.http import cookies -from mitmproxy.utils import strutils - -HAR: dict = {} - -# A list of server seen till now is maintained so we can avoid -# using 'connect' time for entries that use an existing connection. -SERVERS_SEEN: set[connection.Server] = set() - - -def load(l): - l.add_option( - "hardump", str, "", "HAR dump path.", - ) - - -def configure(updated): - HAR.update({ - "log": { - "version": "1.2", - "creator": { - "name": "mitmproxy har_dump", - "version": "0.1", - "comment": "mitmproxy version %s" % version.MITMPROXY - }, - "pages": [ - { - "pageTimings": {} - } - ], - "entries": [] - } - }) - # The `pages` attribute is needed for Firefox Dev Tools to load the HAR file. - # An empty value works fine. - - -def flow_entry(flow: mitmproxy.http.HTTPFlow) -> dict: - - # -1 indicates that these values do not apply to current request - ssl_time = -1 - connect_time = -1 - - if flow.server_conn and flow.server_conn not in SERVERS_SEEN: - connect_time = (flow.server_conn.timestamp_tcp_setup - - flow.server_conn.timestamp_start) - - if flow.server_conn.timestamp_tls_setup is not None: - ssl_time = (flow.server_conn.timestamp_tls_setup - - flow.server_conn.timestamp_tcp_setup) - - SERVERS_SEEN.add(flow.server_conn) - - # Calculate raw timings from timestamps. DNS timings can not be calculated - # for lack of a way to measure it. The same goes for HAR blocked. - # mitmproxy will open a server connection as soon as it receives the host - # and port from the client connection. So, the time spent waiting is actually - # spent waiting between request.timestamp_end and response.timestamp_start - # thus it correlates to HAR wait instead. - timings_raw = { - 'send': flow.request.timestamp_end - flow.request.timestamp_start, - 'receive': flow.response.timestamp_end - flow.response.timestamp_start, - 'wait': flow.response.timestamp_start - flow.request.timestamp_end, - 'connect': connect_time, - 'ssl': ssl_time, - } - - # HAR timings are integers in ms, so we re-encode the raw timings to that format. - timings = { - k: int(1000 * v) if v != -1 else -1 - for k, v in timings_raw.items() - } - - # full_time is the sum of all timings. - # Timings set to -1 will be ignored as per spec. - full_time = sum(v for v in timings.values() if v > -1) - - started_date_time = datetime.fromtimestamp(flow.request.timestamp_start, timezone.utc).isoformat() - - # Response body size and encoding - response_body_size = len(flow.response.raw_content) if flow.response.raw_content else 0 - response_body_decoded_size = len(flow.response.content) if flow.response.content else 0 - response_body_compression = response_body_decoded_size - response_body_size - - entry = { - "startedDateTime": started_date_time, - "time": full_time, - "request": { - "method": flow.request.method, - "url": flow.request.pretty_url, - "httpVersion": flow.request.http_version, - "cookies": format_request_cookies(flow.request.cookies.fields), - "headers": name_value(flow.request.headers), - "queryString": name_value(flow.request.query or {}), - "headersSize": len(str(flow.request.headers)), - "bodySize": len(flow.request.content), - }, - "response": { - "status": flow.response.status_code, - "statusText": flow.response.reason, - "httpVersion": flow.response.http_version, - "cookies": format_response_cookies(flow.response.cookies.fields), - "headers": name_value(flow.response.headers), - "content": { - "size": response_body_size, - "compression": response_body_compression, - "mimeType": flow.response.headers.get('Content-Type', '') - }, - "redirectURL": flow.response.headers.get('Location', ''), - "headersSize": len(str(flow.response.headers)), - "bodySize": response_body_size, - }, - "cache": {}, - "timings": timings, - } - - # Store binary data as base64 - if strutils.is_mostly_bin(flow.response.content): - entry["response"]["content"]["text"] = base64.b64encode(flow.response.content).decode() - entry["response"]["content"]["encoding"] = "base64" - else: - entry["response"]["content"]["text"] = flow.response.get_text(strict=False) - - if flow.request.method in ["POST", "PUT", "PATCH"]: - params = [ - {"name": a, "value": b} - for a, b in flow.request.urlencoded_form.items(multi=True) - ] - entry["request"]["postData"] = { - "mimeType": flow.request.headers.get("Content-Type", ""), - "text": flow.request.get_text(strict=False), - "params": params - } - - if flow.server_conn.connected: - entry["serverIPAddress"] = str(flow.server_conn.peername[0]) - - HAR["log"]["entries"].append(entry) - - return entry - - -def response(flow: mitmproxy.http.HTTPFlow): - """ - Called when a server response has been received. - """ - if flow.websocket is None: - flow_entry(flow) - - -def websocket_end(flow: mitmproxy.http.HTTPFlow): - entry = flow_entry(flow) - - websocket_messages = [] - - for message in flow.websocket.messages: - if message.is_text: - data = message.text - else: - data = base64.b64encode(message.content).decode() - websocket_message = { - 'type': 'send' if message.from_client else 'receive', - 'time': message.timestamp, - 'opcode': message.type.value, - 'data': data - } - websocket_messages.append(websocket_message) - - entry['_resourceType'] = 'websocket' - entry['_webSocketMessages'] = websocket_messages - - -def done(): - """ - Called once on script shutdown, after any other events. - """ - if ctx.options.hardump: - json_dump: str = json.dumps(HAR, indent=2) - - if ctx.options.hardump == '-': - print(json_dump) - else: - raw: bytes = json_dump.encode() - if ctx.options.hardump.endswith('.zhar'): - raw = zlib.compress(raw, 9) - - with open(os.path.expanduser(ctx.options.hardump), "wb") as f: - f.write(raw) - - logging.info("HAR dump finished (wrote %s bytes to file)" % len(json_dump)) - - -def format_cookies(cookie_list): - rv = [] - - for name, value, attrs in cookie_list: - cookie_har = { - "name": name, - "value": value, - } - - # HAR only needs some attributes - for key in ["path", "domain", "comment"]: - if key in attrs: - cookie_har[key] = attrs[key] - - # These keys need to be boolean! - for key in ["httpOnly", "secure"]: - cookie_har[key] = bool(key in attrs) - - # Expiration time needs to be formatted - expire_ts = cookies.get_expiration_ts(attrs) - if expire_ts is not None: - cookie_har["expires"] = datetime.fromtimestamp(expire_ts, timezone.utc).isoformat() - - rv.append(cookie_har) - - return rv - - -def format_request_cookies(fields): - return format_cookies(cookies.group_cookies(fields)) - - -def format_response_cookies(fields): - return format_cookies((c[0], c[1][0], c[1][1]) for c in fields) - - -def name_value(obj): - """ - Convert (key, value) pairs to HAR format. - """ - return [{"name": k, "value": v} for k, v in obj.items()] diff --git a/examples/contrib/http_manipulate_cookies.py b/examples/contrib/http_manipulate_cookies.py index b91018c6e1..953ffa33a9 100644 --- a/examples/contrib/http_manipulate_cookies.py +++ b/examples/contrib/http_manipulate_cookies.py @@ -14,10 +14,10 @@ https://stackoverflow.com/questions/55358072/cookie-manipulation-in-mitmproxy-requests-and-responses """ + import json -from mitmproxy import http -from typing import Union +from mitmproxy import http PATH_TO_COOKIES = "./cookies.json" # insert your path to the cookie file here FILTER_COOKIES = { @@ -28,7 +28,7 @@ # -- Helper functions -- -def load_json_cookies() -> list[dict[str, Union[str, None]]]: +def load_json_cookies() -> list[dict[str, str | None]]: """ Load a particular json file containing a list of cookies. """ @@ -39,20 +39,29 @@ def load_json_cookies() -> list[dict[str, Union[str, None]]]: # NOTE: or just hardcode the cookies as [{"name": "", "value": ""}] -def stringify_cookies(cookies: list[dict[str, Union[str, None]]]) -> str: +def stringify_cookies(cookies: list[dict[str, str | None]]) -> str: """ Creates a cookie string from a list of cookie dicts. """ - return "; ".join([f"{c['name']}={c['value']}" if c.get("value", None) is not None else f"{c['name']}" for c in cookies]) + return "; ".join( + [ + f"{c['name']}={c['value']}" + if c.get("value", None) is not None + else f"{c['name']}" + for c in cookies + ] + ) -def parse_cookies(cookie_string: str) -> list[dict[str, Union[str, None]]]: +def parse_cookies(cookie_string: str) -> list[dict[str, str | None]]: """ Parses a cookie string into a list of cookie dicts. """ return [ {"name": g[0], "value": g[1]} if len(g) == 2 else {"name": g[0], "value": None} - for g in [k.split("=", 1) for k in [c.strip() for c in cookie_string.split(";")] if k] + for g in [ + k.split("=", 1) for k in [c.strip() for c in cookie_string.split(";")] if k + ] ] diff --git a/examples/contrib/httpdump.py b/examples/contrib/httpdump.py index e8c1665578..420667f730 100644 --- a/examples/contrib/httpdump.py +++ b/examples/contrib/httpdump.py @@ -6,7 +6,7 @@ # - dumper_folder: content dump destination folder (default: ./httpdump) # - open_browser: open integrated browser with proxy configured at start (default: true) # -# remember to add your own mitmproxy authorative certs in your browser/os! +# remember to add your own mitmproxy authoritative certs in your browser/os! # certs docs: https://docs.mitmproxy.org/stable/concepts-certificates/ # filter expressions docs: https://docs.mitmproxy.org/stable/concepts-filters/ import logging @@ -14,8 +14,9 @@ import os from pathlib import Path -from mitmproxy import ctx, http +from mitmproxy import ctx from mitmproxy import flowfilter +from mitmproxy import http class HTTPDump: @@ -32,7 +33,7 @@ def load(self, loader): name="open_browser", typespec=bool, default=True, - help="open integrated browser at start" + help="open integrated browser at start", ) def running(self): diff --git a/examples/contrib/jsondump.py b/examples/contrib/jsondump.py index 7617902f32..c25f724900 100644 --- a/examples/contrib/jsondump.py +++ b/examples/contrib/jsondump.py @@ -30,11 +30,13 @@ dump_destination: "/user/rastley/output.log" EOF """ + import base64 import json import logging from queue import Queue -from threading import Lock, Thread +from threading import Lock +from threading import Thread import requests @@ -66,76 +68,77 @@ def done(self): self.outfile.close() fields = { - 'timestamp': ( - ('error', 'timestamp'), - - ('request', 'timestamp_start'), - ('request', 'timestamp_end'), - - ('response', 'timestamp_start'), - ('response', 'timestamp_end'), - - ('client_conn', 'timestamp_start'), - ('client_conn', 'timestamp_end'), - ('client_conn', 'timestamp_tls_setup'), - - ('server_conn', 'timestamp_start'), - ('server_conn', 'timestamp_end'), - ('server_conn', 'timestamp_tls_setup'), - ('server_conn', 'timestamp_tcp_setup'), + "timestamp": ( + ("error", "timestamp"), + ("request", "timestamp_start"), + ("request", "timestamp_end"), + ("response", "timestamp_start"), + ("response", "timestamp_end"), + ("client_conn", "timestamp_start"), + ("client_conn", "timestamp_end"), + ("client_conn", "timestamp_tls_setup"), + ("server_conn", "timestamp_start"), + ("server_conn", "timestamp_end"), + ("server_conn", "timestamp_tls_setup"), + ("server_conn", "timestamp_tcp_setup"), ), - 'ip': ( - ('server_conn', 'source_address'), - ('server_conn', 'ip_address'), - ('server_conn', 'address'), - ('client_conn', 'address'), + "ip": ( + ("server_conn", "source_address"), + ("server_conn", "ip_address"), + ("server_conn", "address"), + ("client_conn", "address"), ), - 'ws_messages': ( - ('messages',), + "ws_messages": (("messages",),), + "headers": ( + ("request", "headers"), + ("response", "headers"), ), - 'headers': ( - ('request', 'headers'), - ('response', 'headers'), - ), - 'content': ( - ('request', 'content'), - ('response', 'content'), + "content": ( + ("request", "content"), + ("response", "content"), ), } def _init_transformations(self): self.transformations = [ { - 'fields': self.fields['headers'], - 'func': dict, + "fields": self.fields["headers"], + "func": dict, }, { - 'fields': self.fields['timestamp'], - 'func': lambda t: int(t * 1000), + "fields": self.fields["timestamp"], + "func": lambda t: int(t * 1000), }, { - 'fields': self.fields['ip'], - 'func': lambda addr: { - 'host': addr[0].replace('::ffff:', ''), - 'port': addr[1], + "fields": self.fields["ip"], + "func": lambda addr: { + "host": addr[0].replace("::ffff:", ""), + "port": addr[1], }, }, { - 'fields': self.fields['ws_messages'], - 'func': lambda ms: [{ - 'type': m[0], - 'from_client': m[1], - 'content': base64.b64encode(bytes(m[2], 'utf-8')) if self.encode else m[2], - 'timestamp': int(m[3] * 1000), - } for m in ms], - } + "fields": self.fields["ws_messages"], + "func": lambda ms: [ + { + "type": m[0], + "from_client": m[1], + "content": base64.b64encode(bytes(m[2], "utf-8")) + if self.encode + else m[2], + "timestamp": int(m[3] * 1000), + } + for m in ms + ], + }, ] if self.encode: - self.transformations.append({ - 'fields': self.fields['content'], - 'func': base64.b64encode, - }) + self.transformations.append( + { + "fields": self.fields["content"], + "func": base64.b64encode, + } + ) @staticmethod def transform_field(obj, path, func): @@ -156,8 +159,10 @@ def convert_to_strings(cls, obj): Recursively convert all list/dict elements of type `bytes` into strings. """ if isinstance(obj, dict): - return {cls.convert_to_strings(key): cls.convert_to_strings(value) - for key, value in obj.items()} + return { + cls.convert_to_strings(key): cls.convert_to_strings(value) + for key, value in obj.items() + } elif isinstance(obj, list) or isinstance(obj, tuple): return [cls.convert_to_strings(element) for element in obj] elif isinstance(obj, bytes): @@ -175,8 +180,8 @@ def dump(self, frame): Transform and dump (write / send) a data frame. """ for tfm in self.transformations: - for field in tfm['fields']: - self.transform_field(frame, field, tfm['func']) + for field in tfm["fields"]: + self.transform_field(frame, field, tfm["func"]) frame = self.convert_to_strings(frame) if self.outfile: @@ -191,14 +196,21 @@ def load(loader): """ Extra options to be specified in `~/.mitmproxy/config.yaml`. """ - loader.add_option('dump_encodecontent', bool, False, - 'Encode content as base64.') - loader.add_option('dump_destination', str, 'jsondump.out', - 'Output destination: path to a file or URL.') - loader.add_option('dump_username', str, '', - 'Basic auth username for URL destinations.') - loader.add_option('dump_password', str, '', - 'Basic auth password for URL destinations.') + loader.add_option( + "dump_encodecontent", bool, False, "Encode content as base64." + ) + loader.add_option( + "dump_destination", + str, + "jsondump.out", + "Output destination: path to a file or URL.", + ) + loader.add_option( + "dump_username", str, "", "Basic auth username for URL destinations." + ) + loader.add_option( + "dump_password", str, "", "Basic auth password for URL destinations." + ) def configure(self, _): """ @@ -207,18 +219,18 @@ def configure(self, _): """ self.encode = ctx.options.dump_encodecontent - if ctx.options.dump_destination.startswith('http'): + if ctx.options.dump_destination.startswith("http"): self.outfile = None self.url = ctx.options.dump_destination - logging.info('Sending all data frames to %s' % self.url) + logging.info("Sending all data frames to %s" % self.url) if ctx.options.dump_username and ctx.options.dump_password: self.auth = (ctx.options.dump_username, ctx.options.dump_password) - logging.info('HTTP Basic auth enabled.') + logging.info("HTTP Basic auth enabled.") else: - self.outfile = open(ctx.options.dump_destination, 'a') + self.outfile = open(ctx.options.dump_destination, "a") self.url = None self.lock = Lock() - logging.info('Writing all data frames to %s' % ctx.options.dump_destination) + logging.info("Writing all data frames to %s" % ctx.options.dump_destination) self._init_transformations() diff --git a/examples/contrib/link_expander.py b/examples/contrib/link_expander.py index 0edf7c9866..e62aab5e9d 100644 --- a/examples/contrib/link_expander.py +++ b/examples/contrib/link_expander.py @@ -2,27 +2,32 @@ # relative links () and expands them to absolute links # In practice this can be used to front an indexing spider that may not have the capability to expand relative page links. # Usage: mitmdump -s link_expander.py or mitmproxy -s link_expander.py - import re from urllib.parse import urljoin def response(flow): - - if "Content-Type" in flow.response.headers and flow.response.headers["Content-Type"].find("text/html") != -1: + if ( + "Content-Type" in flow.response.headers + and flow.response.headers["Content-Type"].find("text/html") != -1 + ): pageUrl = flow.request.url pageText = flow.response.text - pattern = (r"]*?\s+)?href=(?P[\"'])" - r"(?P(?!https?:\/\/|ftps?:\/\/|\/\/|#|javascript:|mailto:).*?)(?P=delimiter)") + pattern = ( + r"]*?\s+)?href=(?P[\"'])" + r"(?P(?!https?:\/\/|ftps?:\/\/|\/\/|#|javascript:|mailto:).*?)(?P=delimiter)" + ) rel_matcher = re.compile(pattern, flags=re.IGNORECASE) rel_matches = rel_matcher.finditer(pageText) map_dict = {} for match_num, match in enumerate(rel_matches): (delimiter, rel_link) = match.group("delimiter", "link") abs_link = urljoin(pageUrl, rel_link) - map_dict["{0}{1}{0}".format(delimiter, rel_link)] = "{0}{1}{0}".format(delimiter, abs_link) + map_dict["{0}{1}{0}".format(delimiter, rel_link)] = "{0}{1}{0}".format( + delimiter, abs_link + ) for map in map_dict.items(): pageText = pageText.replace(*map) # Uncomment the following to print the expansion mapping # print("{0} -> {1}".format(*map)) - flow.response.text = pageText \ No newline at end of file + flow.response.text = pageText diff --git a/examples/contrib/mitmproxywrapper.py b/examples/contrib/mitmproxywrapper.py index 361093e7b5..5ae9db610f 100644 --- a/examples/contrib/mitmproxywrapper.py +++ b/examples/contrib/mitmproxywrapper.py @@ -6,74 +6,69 @@ # # mitmproxywrapper.py -h # - -import subprocess -import re import argparse import contextlib import os +import re +import signal +import socketserver +import subprocess import sys class Wrapper: - def __init__(self, port, extra_arguments=None): + def __init__(self, port, use_mitmweb, extra_arguments=None): self.port = port + self.use_mitmweb = use_mitmweb self.extra_arguments = extra_arguments def run_networksetup_command(self, *arguments): return subprocess.check_output( - ['sudo', 'networksetup'] + list(arguments)) + ["sudo", "networksetup"] + list(arguments) + ).decode() def proxy_state_for_service(self, service): - state = self.run_networksetup_command( - '-getwebproxy', - service).splitlines() - return dict([re.findall(r'([^:]+): (.*)', line)[0] for line in state]) + state = self.run_networksetup_command("-getwebproxy", service).splitlines() + return dict([re.findall(r"([^:]+): (.*)", line)[0] for line in state]) def enable_proxy_for_service(self, service): - print(f'Enabling proxy on {service}...') - for subcommand in ['-setwebproxy', '-setsecurewebproxy']: + print(f"Enabling proxy on {service}...") + for subcommand in ["-setwebproxy", "-setsecurewebproxy"]: self.run_networksetup_command( - subcommand, service, '127.0.0.1', str( - self.port)) + subcommand, service, "127.0.0.1", str(self.port) + ) def disable_proxy_for_service(self, service): - print(f'Disabling proxy on {service}...') - for subcommand in ['-setwebproxystate', '-setsecurewebproxystate']: - self.run_networksetup_command(subcommand, service, 'Off') + print(f"Disabling proxy on {service}...") + for subcommand in ["-setwebproxystate", "-setsecurewebproxystate"]: + self.run_networksetup_command(subcommand, service, "Off") def interface_name_to_service_name_map(self): - order = self.run_networksetup_command('-listnetworkserviceorder') + order = self.run_networksetup_command("-listnetworkserviceorder") mapping = re.findall( - r'\(\d+\)\s(.*)$\n\(.*Device: (.+)\)$', - order, - re.MULTILINE) + r"\(\d+\)\s(.*)$\n\(.*Device: (.+)\)$", order, re.MULTILINE + ) return {b: a for (a, b) in mapping} def run_command_with_input(self, command, input): - popen = subprocess.Popen( - command, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE) - (stdout, stderr) = popen.communicate(input) - return stdout + popen = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + (stdout, stderr) = popen.communicate(input.encode()) + return stdout.decode() def primary_interace_name(self): - scutil_script = 'get State:/Network/Global/IPv4\nd.show\n' - stdout = self.run_command_with_input('/usr/sbin/scutil', scutil_script) - interface, = re.findall(r'PrimaryInterface\s*:\s*(.+)', stdout) + scutil_script = "get State:/Network/Global/IPv4\nd.show\n" + stdout = self.run_command_with_input("/usr/sbin/scutil", scutil_script) + (interface,) = re.findall(r"PrimaryInterface\s*:\s*(.+)", stdout) return interface def primary_service_name(self): - return self.interface_name_to_service_name_map()[ - self.primary_interace_name()] + return self.interface_name_to_service_name_map()[self.primary_interace_name()] def proxy_enabled_for_service(self, service): - return self.proxy_state_for_service(service)['Enabled'] == 'Yes' + return self.proxy_state_for_service(service)["Enabled"] == "Yes" def toggle_proxy(self): - new_state = not self.proxy_enabled_for_service( - self.primary_service_name()) + new_state = not self.proxy_enabled_for_service(self.primary_service_name()) for service_name in self.connected_service_names(): if self.proxy_enabled_for_service(service_name) and not new_state: self.disable_proxy_for_service(service_name) @@ -81,32 +76,29 @@ def toggle_proxy(self): self.enable_proxy_for_service(service_name) def connected_service_names(self): - scutil_script = 'list\n' - stdout = self.run_command_with_input('/usr/sbin/scutil', scutil_script) - service_ids = re.findall(r'State:/Network/Service/(.+)/IPv4', stdout) + scutil_script = "list\n" + stdout = self.run_command_with_input("/usr/sbin/scutil", scutil_script) + service_ids = re.findall(r"State:/Network/Service/(.+)/IPv4", stdout) service_names = [] for service_id in service_ids: - scutil_script = 'show Setup:/Network/Service/{}\n'.format( - service_id) - stdout = self.run_command_with_input( - '/usr/sbin/scutil', - scutil_script) - service_name, = re.findall(r'UserDefinedName\s*:\s*(.+)', stdout) + scutil_script = f"show Setup:/Network/Service/{service_id}\n" + stdout = self.run_command_with_input("/usr/sbin/scutil", scutil_script) + (service_name,) = re.findall(r"UserDefinedName\s*:\s*(.+)", stdout) service_names.append(service_name) return service_names def wrap_mitmproxy(self): with self.wrap_proxy(): - cmd = ['mitmproxy', '-p', str(self.port)] + cmd = ["mitmweb" if self.use_mitmweb else "mitmproxy", "-p", str(self.port)] if self.extra_arguments: cmd.extend(self.extra_arguments) subprocess.check_call(cmd) def wrap_honeyproxy(self): with self.wrap_proxy(): - popen = subprocess.Popen('honeyproxy.sh') + popen = subprocess.Popen("honeyproxy.sh") try: popen.wait() except KeyboardInterrupt: @@ -128,29 +120,58 @@ def wrap_proxy(self): @classmethod def ensure_superuser(cls): if os.getuid() != 0: - print('Relaunching with sudo...') - os.execv('/usr/bin/sudo', ['/usr/bin/sudo'] + sys.argv) + print("Relaunching with sudo...") + os.execv("/usr/bin/sudo", ["/usr/bin/sudo"] + sys.argv) @classmethod def main(cls): parser = argparse.ArgumentParser( - description='Helper tool for OS X proxy configuration and mitmproxy.', - epilog='Any additional arguments will be passed on unchanged to mitmproxy.') + description="Helper tool for OS X proxy configuration and mitmproxy.", + epilog="Any additional arguments will be passed on unchanged to mitmproxy/mitmweb.", + ) parser.add_argument( - '-t', - '--toggle', - action='store_true', - help='just toggle the proxy configuration') + "-t", + "--toggle", + action="store_true", + help="just toggle the proxy configuration", + ) # parser.add_argument('--honeyproxy', action='store_true', help='run honeyproxy instead of mitmproxy') parser.add_argument( - '-p', - '--port', + "-p", + "--port", type=int, - help='override the default port of 8080', - default=8080) + help="override the default port of 8080", + default=8080, + ) + parser.add_argument( + "-P", + "--port-random", + action="store_true", + help="choose a random unused port", + ) + parser.add_argument( + "-w", + "--web", + action="store_true", + help="web interface: run mitmweb instead of mitmproxy", + ) args, extra_arguments = parser.parse_known_args() + port = args.port + + # Allocate a random unused port, and hope no other process steals it before mitmproxy/mitmweb uses it. + # Passing the allocated socket to mitmproxy/mitmweb would be nicer of course. + if args.port_random: + with socketserver.TCPServer(("localhost", 0), None) as s: + port = s.server_address[1] + print(f"Using random port {port}...") + + wrapper = cls(port=port, use_mitmweb=args.web, extra_arguments=extra_arguments) + + def handler(signum, frame): + print("Cleaning up proxy settings...") + wrapper.toggle_proxy() - wrapper = cls(port=args.port, extra_arguments=extra_arguments) + signal.signal(signal.SIGINT, handler) if args.toggle: wrapper.toggle_proxy() @@ -160,6 +181,6 @@ def main(cls): wrapper.wrap_mitmproxy() -if __name__ == '__main__': +if __name__ == "__main__": Wrapper.ensure_superuser() Wrapper.main() diff --git a/examples/contrib/modify_body_inject_iframe.py b/examples/contrib/modify_body_inject_iframe.py index 595bd9f281..1736efd34e 100644 --- a/examples/contrib/modify_body_inject_iframe.py +++ b/examples/contrib/modify_body_inject_iframe.py @@ -1,24 +1,21 @@ # (this script works best with --anticache) from bs4 import BeautifulSoup -from mitmproxy import ctx, http + +from mitmproxy import ctx +from mitmproxy import http class Injector: def load(self, loader): - loader.add_option( - "iframe", str, "", "IFrame to inject" - ) + loader.add_option("iframe", str, "", "IFrame to inject") def response(self, flow: http.HTTPFlow) -> None: if ctx.options.iframe: html = BeautifulSoup(flow.response.content, "html.parser") if html.body: iframe = html.new_tag( - "iframe", - src=ctx.options.iframe, - frameborder=0, - height=0, - width=0) + "iframe", src=ctx.options.iframe, frameborder=0, height=0, width=0 + ) html.body.insert(0, iframe) flow.response.content = str(html).encode("utf8") diff --git a/examples/contrib/ntlm_upstream_proxy.py b/examples/contrib/ntlm_upstream_proxy.py index 656d48b3ad..f7de3b63ce 100644 --- a/examples/contrib/ntlm_upstream_proxy.py +++ b/examples/contrib/ntlm_upstream_proxy.py @@ -1,28 +1,34 @@ import base64 +import binascii import logging import socket -from typing import Any, Optional +from typing import Any +from typing import Optional -import binascii -from ntlm_auth import gss_channel_bindings, ntlm +from ntlm_auth import gss_channel_bindings +from ntlm_auth import ntlm -from mitmproxy import addonmanager, http +from mitmproxy import addonmanager from mitmproxy import ctx +from mitmproxy import http from mitmproxy.net.http import http1 -from mitmproxy.proxy import commands, layer +from mitmproxy.proxy import commands +from mitmproxy.proxy import layer from mitmproxy.proxy.context import Context -from mitmproxy.proxy.layers.http import HttpConnectUpstreamHook, HttpLayer, HttpStream +from mitmproxy.proxy.layers.http import HttpConnectUpstreamHook +from mitmproxy.proxy.layers.http import HttpLayer +from mitmproxy.proxy.layers.http import HttpStream from mitmproxy.proxy.layers.http._upstream_proxy import HttpUpstreamProxy class NTLMUpstreamAuth: """ - This addon handles authentication to systems upstream from us for the - upstream proxy and reverse proxy mode. There are 3 cases: - - Upstream proxy CONNECT requests should have authentication added, and - subsequent already connected requests should not. - - Upstream proxy regular requests - - Reverse proxy regular requests (CONNECT is invalid in this mode) + This addon handles authentication to systems upstream from us for the + upstream proxy and reverse proxy mode. There are 3 cases: + - Upstream proxy CONNECT requests should have authentication added, and + subsequent already connected requests should not. + - Upstream proxy regular requests + - Reverse proxy regular requests (CONNECT is invalid in this mode) """ def load(self, loader: addonmanager.Loader) -> None: @@ -34,7 +40,7 @@ def load(self, loader: addonmanager.Loader) -> None: help=""" Add HTTP NTLM authentication to upstream proxy requests. Format: username:password. - """ + """, ) loader.add_option( name="upstream_ntlm_domain", @@ -42,7 +48,7 @@ def load(self, loader: addonmanager.Loader) -> None: default=None, help=""" Add HTTP NTLM domain for authentication to upstream proxy requests. - """ + """, ) loader.add_option( name="upstream_proxy_address", @@ -50,7 +56,7 @@ def load(self, loader: addonmanager.Loader) -> None: default=None, help=""" upstream poxy address. - """ + """, ) loader.add_option( name="upstream_ntlm_compatibility", @@ -59,19 +65,23 @@ def load(self, loader: addonmanager.Loader) -> None: help=""" Add HTTP NTLM compatibility for authentication to upstream proxy requests. Valid values are 0-5 (Default: 3) - """ + """, ) logging.debug("AddOn: NTLM Upstream Authentication - Loaded") def running(self): def extract_flow_from_context(context: Context) -> http.HTTPFlow: if context and context.layers: - for l in context.layers: - if isinstance(l, HttpLayer): - for _, stream in l.streams.items(): - return stream.flow if isinstance(stream, HttpStream) else None - - def build_connect_flow(context: Context, connect_header: tuple) -> http.HTTPFlow: + for x in context.layers: + if isinstance(x, HttpLayer): + for _, stream in x.streams.items(): + return ( + stream.flow if isinstance(stream, HttpStream) else None + ) + + def build_connect_flow( + context: Context, connect_header: tuple + ) -> http.HTTPFlow: flow = extract_flow_from_context(context) if not flow: logging.error("failed to build connect flow") @@ -85,23 +95,27 @@ def patched_start_handshake(self) -> layer.CommandGenerator[None]: assert self.conn.address self.ntlm_context = CustomNTLMContext(ctx) proxy_authorization = self.ntlm_context.get_ntlm_start_negotiate_message() - self.flow = build_connect_flow(self.context, ("Proxy-Authorization", proxy_authorization)) + self.flow = build_connect_flow( + self.context, ("Proxy-Authorization", proxy_authorization) + ) yield HttpConnectUpstreamHook(self.flow) raw = http1.assemble_request(self.flow.request) yield commands.SendData(self.tunnel_connection, raw) def extract_proxy_authenticate_msg(response_head: list) -> str: for header in response_head: - if b'Proxy-Authenticate' in header: - challenge_message = str(bytes(header).decode('utf-8')) + if b"Proxy-Authenticate" in header: + challenge_message = str(bytes(header).decode("utf-8")) try: - token = challenge_message.split(': ')[1] + token = challenge_message.split(": ")[1] except IndexError: logging.error("Failed to extract challenge_message") raise return token - def patched_receive_handshake_data(self, data) -> layer.CommandGenerator[tuple[bool, Optional[str]]]: + def patched_receive_handshake_data( + self, data + ) -> layer.CommandGenerator[tuple[bool, str | None]]: self.buf += data response_head = self.buf.maybe_extract_lines() if response_head: @@ -119,8 +133,14 @@ def patched_receive_handshake_data(self, data) -> layer.CommandGenerator[tuple[b else: if not challenge_message: return True, None - proxy_authorization = self.ntlm_context.get_ntlm_challenge_response_message(challenge_message) - self.flow = build_connect_flow(self.context, ("Proxy-Authorization", proxy_authorization)) + proxy_authorization = ( + self.ntlm_context.get_ntlm_challenge_response_message( + challenge_message + ) + ) + self.flow = build_connect_flow( + self.context, ("Proxy-Authorization", proxy_authorization) + ) raw = http1.assemble_request(self.flow.request) yield commands.SendData(self.tunnel_connection, raw) return False, None @@ -131,19 +151,19 @@ def patched_receive_handshake_data(self, data) -> layer.CommandGenerator[tuple[b HttpUpstreamProxy.receive_handshake_data = patched_receive_handshake_data def done(self): - logging.info('close ntlm session') + logging.info("close ntlm session") -addons = [ - NTLMUpstreamAuth() -] +addons = [NTLMUpstreamAuth()] class CustomNTLMContext: - def __init__(self, - ctx, - preferred_type: str = 'NTLM', - cbt_data: gss_channel_bindings.GssChannelBindingsStruct = None): + def __init__( + self, + ctx, + preferred_type: str = "NTLM", + cbt_data: gss_channel_bindings.GssChannelBindingsStruct = None, + ): # TODO:// take care the cbt_data auth: str = ctx.options.upstream_ntlm_auth domain: str = str(ctx.options.upstream_ntlm_domain).upper() @@ -158,29 +178,39 @@ def __init__(self, domain=domain, workstation=workstation, ntlm_compatibility=ntlm_compatibility, - cbt_data=cbt_data) + cbt_data=cbt_data, + ) def get_ntlm_start_negotiate_message(self) -> str: negotiate_message = self.ntlm_context.step() negotiate_message_base_64_in_bytes = base64.b64encode(negotiate_message) - negotiate_message_base_64_ascii = negotiate_message_base_64_in_bytes.decode("ascii") - negotiate_message_base_64_final = f'{self.preferred_type} {negotiate_message_base_64_ascii}' + negotiate_message_base_64_ascii = negotiate_message_base_64_in_bytes.decode( + "ascii" + ) + negotiate_message_base_64_final = ( + f"{self.preferred_type} {negotiate_message_base_64_ascii}" + ) logging.debug( - f'{self.preferred_type} Authentication, negotiate message: {negotiate_message_base_64_final}' + f"{self.preferred_type} Authentication, negotiate message: {negotiate_message_base_64_final}" ) return negotiate_message_base_64_final def get_ntlm_challenge_response_message(self, challenge_message: str) -> Any: challenge_message = challenge_message.replace(self.preferred_type + " ", "", 1) try: - challenge_message_ascii_bytes = base64.b64decode(challenge_message, validate=True) + challenge_message_ascii_bytes = base64.b64decode( + challenge_message, validate=True + ) except binascii.Error as err: - logging.debug(f'{self.preferred_type} Authentication fail with error {err.__str__()}') + logging.debug( + f"{self.preferred_type} Authentication fail with error {err.__str__()}" + ) return False authenticate_message = self.ntlm_context.step(challenge_message_ascii_bytes) - negotiate_message_base_64 = '{} {}'.format(self.preferred_type, - base64.b64encode(authenticate_message).decode('ascii')) + negotiate_message_base_64 = "{} {}".format( + self.preferred_type, base64.b64encode(authenticate_message).decode("ascii") + ) logging.debug( - f'{self.preferred_type} Authentication, response to challenge message: {negotiate_message_base_64}' + f"{self.preferred_type} Authentication, response to challenge message: {negotiate_message_base_64}" ) return negotiate_message_base_64 diff --git a/examples/contrib/remote-debug.py b/examples/contrib/remote-debug.py index 767d828cde..cd4187b011 100644 --- a/examples/contrib/remote-debug.py +++ b/examples/contrib/remote-debug.py @@ -16,6 +16,9 @@ """ -def load(l): +def load(_): import pydevd_pycharm - pydevd_pycharm.settrace("localhost", port=5678, stdoutToServer=True, stderrToServer=True, suspend=False) + + pydevd_pycharm.settrace( + "localhost", port=5678, stdoutToServer=True, stderrToServer=True, suspend=False + ) diff --git a/examples/contrib/save_streamed_data.py b/examples/contrib/save_streamed_data.py index 283a6b52bd..7053b75aab 100644 --- a/examples/contrib/save_streamed_data.py +++ b/examples/contrib/save_streamed_data.py @@ -18,6 +18,7 @@ This addon is not compatible with addons that use the same mechanism to capture streamed data, http-stream-modify.py for instance. """ + import logging import os from datetime import datetime @@ -58,12 +59,13 @@ def __call__(self, data): return data if not self.fh: - self.path = datetime.fromtimestamp(self.flow.request.timestamp_start).strftime( - ctx.options.save_streamed_data) - self.path = self.path.replace('%+T', str(self.flow.request.timestamp_start)) - self.path = self.path.replace('%+I', str(self.flow.client_conn.id)) - self.path = self.path.replace('%+D', self.direction) - self.path = self.path.replace('%+C', self.flow.client_conn.address[0]) + self.path = datetime.fromtimestamp( + self.flow.request.timestamp_start + ).strftime(ctx.options.save_streamed_data) + self.path = self.path.replace("%+T", str(self.flow.request.timestamp_start)) + self.path = self.path.replace("%+I", str(self.flow.client_conn.id)) + self.path = self.path.replace("%+D", self.direction) + self.path = self.path.replace("%+C", self.flow.client_conn.address[0]) self.path = os.path.expanduser(self.path) parent = Path(self.path).parent @@ -89,25 +91,27 @@ def __call__(self, data): def load(loader): loader.add_option( - "save_streamed_data", Optional[str], None, + "save_streamed_data", + Optional[str], + None, "Format string for saving streamed data to files. If set each streamed request or response is written " "to a file with a name derived from the string. In addition to formating supported by python " "strftime() (using the request start time) the code '%+T' is replaced with the time stamp of the request, " "'%+D' by 'req' or 'rsp' depending on the direction of the data, '%+C' by the client IP addresses and " - "'%+I' by the client connection ID." + "'%+I' by the client connection ID.", ) def requestheaders(flow): if ctx.options.save_streamed_data and flow.request.stream: - flow.request.stream = StreamSaver(flow, 'req') + flow.request.stream = StreamSaver(flow, "req") def responseheaders(flow): if isinstance(flow.request.stream, StreamSaver): flow.request.stream.done() if ctx.options.save_streamed_data and flow.response.stream: - flow.response.stream = StreamSaver(flow, 'rsp') + flow.response.stream = StreamSaver(flow, "rsp") def response(flow): diff --git a/examples/contrib/search.py b/examples/contrib/search.py index e9c935ac61..73d775d2be 100644 --- a/examples/contrib/search.py +++ b/examples/contrib/search.py @@ -3,20 +3,19 @@ from collections.abc import Sequence from json import dumps -from mitmproxy import command, flow +from mitmproxy import command +from mitmproxy import flow -MARKER = ':mag:' -RESULTS_STR = 'Search Results: ' +MARKER = ":mag:" +RESULTS_STR = "Search Results: " class Search: def __init__(self): self.exp = None - @command.command('search') - def _search(self, - flows: Sequence[flow.Flow], - regex: str) -> None: + @command.command("search") + def _search(self, flows: Sequence[flow.Flow], regex: str) -> None: """ Defines a command named "search" that matches the given regular expression against most parts @@ -49,11 +48,11 @@ def _search(self, for _flow in flows: # Erase previous results while preserving other comments: comments = list() - for c in _flow.comment.split('\n'): + for c in _flow.comment.split("\n"): if c.startswith(RESULTS_STR): break comments.append(c) - _flow.comment = '\n'.join(comments) + _flow.comment = "\n".join(comments) if _flow.marked == MARKER: _flow.marked = False @@ -62,7 +61,7 @@ def _search(self, if results: comments.append(RESULTS_STR) comments.append(dumps(results, indent=2)) - _flow.comment = '\n'.join(comments) + _flow.comment = "\n".join(comments) _flow.marked = MARKER def header_results(self, message): @@ -71,22 +70,16 @@ def header_results(self, message): def flow_results(self, _flow): results = dict() - results.update( - {'flow_comment': self.exp.findall(_flow.comment)}) + results.update({"flow_comment": self.exp.findall(_flow.comment)}) if _flow.request is not None: - results.update( - {'request_path': self.exp.findall(_flow.request.path)}) - results.update( - {'request_headers': self.header_results(_flow.request)}) + results.update({"request_path": self.exp.findall(_flow.request.path)}) + results.update({"request_headers": self.header_results(_flow.request)}) if _flow.request.text: - results.update( - {'request_body': self.exp.findall(_flow.request.text)}) + results.update({"request_body": self.exp.findall(_flow.request.text)}) if _flow.response is not None: - results.update( - {'response_headers': self.header_results(_flow.response)}) + results.update({"response_headers": self.header_results(_flow.response)}) if _flow.response.text: - results.update( - {'response_body': self.exp.findall(_flow.response.text)}) + results.update({"response_body": self.exp.findall(_flow.response.text)}) return results diff --git a/examples/contrib/sslstrip.py b/examples/contrib/sslstrip.py index 05aa5f3e5f..76344aa9eb 100644 --- a/examples/contrib/sslstrip.py +++ b/examples/contrib/sslstrip.py @@ -2,6 +2,7 @@ This script implements an sslstrip-like attack based on mitmproxy. https://moxie.org/software/sslstrip/ """ + import re import urllib.parse @@ -12,15 +13,15 @@ def request(flow: http.HTTPFlow) -> None: - flow.request.headers.pop('If-Modified-Since', None) - flow.request.headers.pop('Cache-Control', None) + flow.request.headers.pop("If-Modified-Since", None) + flow.request.headers.pop("Cache-Control", None) # do not force https redirection - flow.request.headers.pop('Upgrade-Insecure-Requests', None) + flow.request.headers.pop("Upgrade-Insecure-Requests", None) # proxy connections to SSL-enabled hosts if flow.request.pretty_host in secure_hosts: - flow.request.scheme = 'https' + flow.request.scheme = "https" flow.request.port = 443 # We need to update the request destination to whatever is specified in the host header: @@ -31,32 +32,36 @@ def request(flow: http.HTTPFlow) -> None: def response(flow: http.HTTPFlow) -> None: assert flow.response - flow.response.headers.pop('Strict-Transport-Security', None) - flow.response.headers.pop('Public-Key-Pins', None) + flow.response.headers.pop("Strict-Transport-Security", None) + flow.response.headers.pop("Public-Key-Pins", None) # strip links in response body - flow.response.content = flow.response.content.replace(b'https://', b'http://') + flow.response.content = flow.response.content.replace(b"https://", b"http://") # strip meta tag upgrade-insecure-requests in response body - csp_meta_tag_pattern = br'' - flow.response.content = re.sub(csp_meta_tag_pattern, b'', flow.response.content, flags=re.IGNORECASE) + csp_meta_tag_pattern = rb'' + flow.response.content = re.sub( + csp_meta_tag_pattern, b"", flow.response.content, flags=re.IGNORECASE + ) # strip links in 'Location' header - if flow.response.headers.get('Location', '').startswith('https://'): - location = flow.response.headers['Location'] + if flow.response.headers.get("Location", "").startswith("https://"): + location = flow.response.headers["Location"] hostname = urllib.parse.urlparse(location).hostname if hostname: secure_hosts.add(hostname) - flow.response.headers['Location'] = location.replace('https://', 'http://', 1) + flow.response.headers["Location"] = location.replace("https://", "http://", 1) # strip upgrade-insecure-requests in Content-Security-Policy header - csp_header = flow.response.headers.get('Content-Security-Policy', '') - if re.search('upgrade-insecure-requests', csp_header, flags=re.IGNORECASE): - csp = flow.response.headers['Content-Security-Policy'] - new_header = re.sub(r'upgrade-insecure-requests[;\s]*', '', csp, flags=re.IGNORECASE) - flow.response.headers['Content-Security-Policy'] = new_header + csp_header = flow.response.headers.get("Content-Security-Policy", "") + if re.search("upgrade-insecure-requests", csp_header, flags=re.IGNORECASE): + csp = flow.response.headers["Content-Security-Policy"] + new_header = re.sub( + r"upgrade-insecure-requests[;\s]*", "", csp, flags=re.IGNORECASE + ) + flow.response.headers["Content-Security-Policy"] = new_header # strip secure flag from 'Set-Cookie' headers - cookies = flow.response.headers.get_all('Set-Cookie') - cookies = [re.sub(r';\s*secure\s*', '', s) for s in cookies] - flow.response.headers.set_all('Set-Cookie', cookies) + cookies = flow.response.headers.get_all("Set-Cookie") + cookies = [re.sub(r";\s*secure\s*", "", s) for s in cookies] + flow.response.headers.set_all("Set-Cookie", cookies) diff --git a/examples/contrib/suppress_error_responses.py b/examples/contrib/suppress_error_responses.py index e087a78da8..df4e50626b 100644 --- a/examples/contrib/suppress_error_responses.py +++ b/examples/contrib/suppress_error_responses.py @@ -4,13 +4,14 @@ Without this script, if the web application under test crashes, mitmproxy will send 502 Bad Gateway responses. These responses are irritating the web application scanner since they obfuscate the actual problem. """ + from mitmproxy import http from mitmproxy.exceptions import HttpSyntaxException def error(self, flow: http.HTTPFlow): """Kills the flow if it has an error different to HTTPSyntaxException. - Sometimes, web scanners generate malformed HTTP syntax on purpose and we do not want to kill these requests. + Sometimes, web scanners generate malformed HTTP syntax on purpose and we do not want to kill these requests. """ if flow.error is not None and not isinstance(flow.error, HttpSyntaxException): flow.kill() diff --git a/examples/contrib/test_har_dump.py b/examples/contrib/test_har_dump.py deleted file mode 100644 index 88c27a9b4c..0000000000 --- a/examples/contrib/test_har_dump.py +++ /dev/null @@ -1,88 +0,0 @@ -import json - -from mitmproxy.test import tflow -from mitmproxy.test import tutils -from mitmproxy.test import taddons -from mitmproxy.net.http import cookies - - -class TestHARDump: - def flow(self, resp_content=b'message'): - times = dict( - timestamp_start=746203272, - timestamp_end=746203272, - ) - - # Create a dummy flow for testing - return tflow.tflow( - req=tutils.treq(method=b'GET', **times), - resp=tutils.tresp(content=resp_content, **times) - ) - - def test_simple(self, tmpdir, tdata): - # context is needed to provide ctx.log function that - # is invoked if there are exceptions - with taddons.context() as tctx: - a = tctx.script(tdata.path("../examples/contrib/har_dump.py")) - # check script is read without errors - assert tctx.master.logs == [] - assert a.name_value # last function in har_dump.py - - path = str(tmpdir.join("somefile")) - tctx.configure(a, hardump=path) - a.response(self.flow()) - a.done() - with open(path) as inp: - har = json.load(inp) - assert len(har["log"]["entries"]) == 1 - - def test_base64(self, tmpdir, tdata): - with taddons.context() as tctx: - a = tctx.script(tdata.path("../examples/contrib/har_dump.py")) - path = str(tmpdir.join("somefile")) - tctx.configure(a, hardump=path) - - a.response(self.flow(resp_content=b"foo" + b"\xFF" * 10)) - a.done() - with open(path) as inp: - har = json.load(inp) - assert har["log"]["entries"][0]["response"]["content"]["encoding"] == "base64" - - def test_format_cookies(self, tdata): - with taddons.context() as tctx: - a = tctx.script(tdata.path("../examples/contrib/har_dump.py")) - - CA = cookies.CookieAttrs - - f = a.format_cookies([("n", "v", CA([("k", "v")]))])[0] - assert f['name'] == "n" - assert f['value'] == "v" - assert not f['httpOnly'] - assert not f['secure'] - - f = a.format_cookies([("n", "v", CA([("httponly", None), ("secure", None)]))])[0] - assert f['httpOnly'] - assert f['secure'] - - f = a.format_cookies([("n", "v", CA([("expires", "Mon, 24-Aug-2037 00:00:00 GMT")]))])[0] - assert f['expires'] - - def test_binary(self, tmpdir, tdata): - with taddons.context() as tctx: - a = tctx.script(tdata.path("../examples/contrib/har_dump.py")) - path = str(tmpdir.join("somefile")) - tctx.configure(a, hardump=path) - - f = self.flow() - f.request.method = "POST" - f.request.headers["content-type"] = "application/x-www-form-urlencoded" - f.request.content = b"foo=bar&baz=s%c3%bc%c3%9f" - f.response.headers["random-junk"] = bytes(range(256)) - f.response.content = bytes(range(256)) - - a.response(f) - a.done() - - with open(path) as inp: - har = json.load(inp) - assert len(har["log"]["entries"]) == 1 diff --git a/examples/contrib/test_jsondump.py b/examples/contrib/test_jsondump.py index 106a0ecbf2..0000b272fc 100644 --- a/examples/contrib/test_jsondump.py +++ b/examples/contrib/test_jsondump.py @@ -1,21 +1,21 @@ -import json import base64 +import json +import requests_mock + +from mitmproxy.test import taddons from mitmproxy.test import tflow from mitmproxy.test import tutils -from mitmproxy.test import taddons - -import requests_mock example_dir = tutils.test_data.push("../examples") class TestJSONDump: def echo_response(self, request, context): - self.request = {'json': request.json(), 'headers': request.headers} - return '' + self.request = {"json": request.json(), "headers": request.headers} + return "" - def flow(self, resp_content=b'message'): + def flow(self, resp_content=b"message"): times = dict( timestamp_start=746203272, timestamp_end=746203272, @@ -23,8 +23,8 @@ def flow(self, resp_content=b'message'): # Create a dummy flow for testing return tflow.tflow( - req=tutils.treq(method=b'GET', **times), - resp=tutils.tresp(content=resp_content, **times) + req=tutils.treq(method=b"GET", **times), + resp=tutils.tresp(content=resp_content, **times), ) def test_simple(self, tmpdir): @@ -36,33 +36,37 @@ def test_simple(self, tmpdir): tctx.invoke(a, "done") with open(path) as inp: entry = json.loads(inp.readline()) - assert entry['response']['content'] == 'message' + assert entry["response"]["content"] == "message" def test_contentencode(self, tmpdir): with taddons.context() as tctx: a = tctx.script(example_dir.path("complex/jsondump.py")) path = str(tmpdir.join("jsondump.out")) - content = b"foo" + b"\xFF" * 10 + content = b"foo" + b"\xff" * 10 tctx.configure(a, dump_destination=path, dump_encodecontent=True) - tctx.invoke( - a, "response", self.flow(resp_content=content) - ) + tctx.invoke(a, "response", self.flow(resp_content=content)) tctx.invoke(a, "done") with open(path) as inp: entry = json.loads(inp.readline()) - assert entry['response']['content'] == base64.b64encode(content).decode('utf-8') + assert entry["response"]["content"] == base64.b64encode(content).decode( + "utf-8" + ) def test_http(self, tmpdir): with requests_mock.Mocker() as mock: - mock.post('http://my-server', text=self.echo_response) + mock.post("http://my-server", text=self.echo_response) with taddons.context() as tctx: a = tctx.script(example_dir.path("complex/jsondump.py")) - tctx.configure(a, dump_destination='http://my-server', - dump_username='user', dump_password='pass') + tctx.configure( + a, + dump_destination="http://my-server", + dump_username="user", + dump_password="pass", + ) tctx.invoke(a, "response", self.flow()) tctx.invoke(a, "done") - assert self.request['json']['response']['content'] == 'message' - assert self.request['headers']['Authorization'] == 'Basic dXNlcjpwYXNz' + assert self.request["json"]["response"]["content"] == "message" + assert self.request["headers"]["Authorization"] == "Basic dXNlcjpwYXNz" diff --git a/examples/contrib/test_xss_scanner.py b/examples/contrib/test_xss_scanner.py index f277528256..8aeb524047 100644 --- a/examples/contrib/test_xss_scanner.py +++ b/examples/contrib/test_xss_scanner.py @@ -1,229 +1,331 @@ import pytest import requests + from examples.complex import xss_scanner as xss -from mitmproxy.test import tflow, tutils +from mitmproxy.test import tflow +from mitmproxy.test import tutils -class TestXSSScanner(): +class TestXSSScanner: def test_get_XSS_info(self): # First type of exploit: # Exploitable: - xss_info = xss.get_XSS_data(b"" % - xss.FULL_PAYLOAD, - "https://example.com", - "End of URL") - expected_xss_info = xss.XSSData('https://example.com', - "End of URL", - '" % xss.FULL_PAYLOAD, + "https://example.com", + "End of URL", + ) + expected_xss_info = xss.XSSData( + "https://example.com", + "End of URL", + "" % - xss.FULL_PAYLOAD.replace(b"'", b"%27").replace(b'"', b"%22"), - "https://example.com", - "End of URL") - expected_xss_info = xss.XSSData("https://example.com", - "End of URL", - '" + % xss.FULL_PAYLOAD.replace(b"'", b"%27").replace(b'"', b"%22"), + "https://example.com", + "End of URL", + ) + expected_xss_info = xss.XSSData( + "https://example.com", + "End of URL", + "" % - xss.FULL_PAYLOAD.replace(b"'", b"%27").replace(b'"', b"%22").replace(b"/", b"%2F"), - "https://example.com", - "End of URL") + xss_info = xss.get_XSS_data( + b"" + % xss.FULL_PAYLOAD.replace(b"'", b"%27") + .replace(b'"', b"%22") + .replace(b"/", b"%2F"), + "https://example.com", + "End of URL", + ) assert xss_info is None # Second type of exploit: # Exploitable: - xss_info = xss.get_XSS_data(b"" % - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E").replace(b"\"", b"%22"), - "https://example.com", - "End of URL") - expected_xss_info = xss.XSSData("https://example.com", - "End of URL", - "';alert(0);g='", - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E") - .replace(b"\"", b"%22").decode('utf-8')) + xss_info = xss.get_XSS_data( + b"" + % xss.FULL_PAYLOAD.replace(b"<", b"%3C") + .replace(b">", b"%3E") + .replace(b'"', b"%22"), + "https://example.com", + "End of URL", + ) + expected_xss_info = xss.XSSData( + "https://example.com", + "End of URL", + "';alert(0);g='", + xss.FULL_PAYLOAD.replace(b"<", b"%3C") + .replace(b">", b"%3E") + .replace(b'"', b"%22") + .decode("utf-8"), + ) assert xss_info == expected_xss_info # Non-Exploitable: - xss_info = xss.get_XSS_data(b"" % - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b"\"", b"%22").replace(b"'", b"%22"), - "https://example.com", - "End of URL") + xss_info = xss.get_XSS_data( + b"" + % xss.FULL_PAYLOAD.replace(b"<", b"%3C") + .replace(b'"', b"%22") + .replace(b"'", b"%22"), + "https://example.com", + "End of URL", + ) assert xss_info is None # Third type of exploit: # Exploitable: - xss_info = xss.get_XSS_data(b"" % - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E").replace(b"'", b"%27"), - "https://example.com", - "End of URL") - expected_xss_info = xss.XSSData("https://example.com", - "End of URL", - '";alert(0);g="', - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E") - .replace(b"'", b"%27").decode('utf-8')) + xss_info = xss.get_XSS_data( + b'' + % xss.FULL_PAYLOAD.replace(b"<", b"%3C") + .replace(b">", b"%3E") + .replace(b"'", b"%27"), + "https://example.com", + "End of URL", + ) + expected_xss_info = xss.XSSData( + "https://example.com", + "End of URL", + '";alert(0);g="', + xss.FULL_PAYLOAD.replace(b"<", b"%3C") + .replace(b">", b"%3E") + .replace(b"'", b"%27") + .decode("utf-8"), + ) assert xss_info == expected_xss_info # Non-Exploitable: - xss_info = xss.get_XSS_data(b"" % - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b"'", b"%27").replace(b"\"", b"%22"), - "https://example.com", - "End of URL") + xss_info = xss.get_XSS_data( + b'' + % xss.FULL_PAYLOAD.replace(b"<", b"%3C") + .replace(b"'", b"%27") + .replace(b'"', b"%22"), + "https://example.com", + "End of URL", + ) assert xss_info is None # Fourth type of exploit: Test # Exploitable: - xss_info = xss.get_XSS_data(b"Test" % - xss.FULL_PAYLOAD, - "https://example.com", - "End of URL") - expected_xss_info = xss.XSSData("https://example.com", - "End of URL", - "'>", - xss.FULL_PAYLOAD.decode('utf-8')) + xss_info = xss.get_XSS_data( + b"Test" % xss.FULL_PAYLOAD, + "https://example.com", + "End of URL", + ) + expected_xss_info = xss.XSSData( + "https://example.com", + "End of URL", + "'>", + xss.FULL_PAYLOAD.decode("utf-8"), + ) assert xss_info == expected_xss_info # Non-Exploitable: - xss_info = xss.get_XSS_data(b"Test" % - xss.FULL_PAYLOAD.replace(b"'", b"%27"), - "https://example.com", - "End of URL") + xss_info = xss.get_XSS_data( + b"Test" + % xss.FULL_PAYLOAD.replace(b"'", b"%27"), + "https://example.com", + "End of URL", + ) assert xss_info is None # Fifth type of exploit: Test # Exploitable: - xss_info = xss.get_XSS_data(b"Test" % - xss.FULL_PAYLOAD.replace(b"'", b"%27"), - "https://example.com", - "End of URL") - expected_xss_info = xss.XSSData("https://example.com", - "End of URL", - "\">", - xss.FULL_PAYLOAD.replace(b"'", b"%27").decode('utf-8')) + xss_info = xss.get_XSS_data( + b'Test' + % xss.FULL_PAYLOAD.replace(b"'", b"%27"), + "https://example.com", + "End of URL", + ) + expected_xss_info = xss.XSSData( + "https://example.com", + "End of URL", + '">', + xss.FULL_PAYLOAD.replace(b"'", b"%27").decode("utf-8"), + ) assert xss_info == expected_xss_info # Non-Exploitable: - xss_info = xss.get_XSS_data(b"Test" % - xss.FULL_PAYLOAD.replace(b"'", b"%27").replace(b"\"", b"%22"), - "https://example.com", - "End of URL") + xss_info = xss.get_XSS_data( + b'Test' + % xss.FULL_PAYLOAD.replace(b"'", b"%27").replace(b'"', b"%22"), + "https://example.com", + "End of URL", + ) assert xss_info is None # Sixth type of exploit: Test # Exploitable: - xss_info = xss.get_XSS_data(b"Test" % - xss.FULL_PAYLOAD, - "https://example.com", - "End of URL") - expected_xss_info = xss.XSSData("https://example.com", - "End of URL", - ">", - xss.FULL_PAYLOAD.decode('utf-8')) + xss_info = xss.get_XSS_data( + b"Test" % xss.FULL_PAYLOAD, + "https://example.com", + "End of URL", + ) + expected_xss_info = xss.XSSData( + "https://example.com", + "End of URL", + ">", + xss.FULL_PAYLOAD.decode("utf-8"), + ) assert xss_info == expected_xss_info # Non-Exploitable - xss_info = xss.get_XSS_data(b"Test" % - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E") - .replace(b"=", b"%3D"), - "https://example.com", - "End of URL") + xss_info = xss.get_XSS_data( + b"Test" + % xss.FULL_PAYLOAD.replace(b"<", b"%3C") + .replace(b">", b"%3E") + .replace(b"=", b"%3D"), + "https://example.com", + "End of URL", + ) assert xss_info is None # Seventh type of exploit: PAYLOAD # Exploitable: - xss_info = xss.get_XSS_data(b"%s" % - xss.FULL_PAYLOAD, - "https://example.com", - "End of URL") - expected_xss_info = xss.XSSData("https://example.com", - "End of URL", - "", - xss.FULL_PAYLOAD.decode('utf-8')) + xss_info = xss.get_XSS_data( + b"%s" % xss.FULL_PAYLOAD, + "https://example.com", + "End of URL", + ) + expected_xss_info = xss.XSSData( + "https://example.com", + "End of URL", + "", + xss.FULL_PAYLOAD.decode("utf-8"), + ) assert xss_info == expected_xss_info # Non-Exploitable - xss_info = xss.get_XSS_data(b"%s" % - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E").replace(b"/", b"%2F"), - "https://example.com", - "End of URL") + xss_info = xss.get_XSS_data( + b"%s" + % xss.FULL_PAYLOAD.replace(b"<", b"%3C") + .replace(b">", b"%3E") + .replace(b"/", b"%2F"), + "https://example.com", + "End of URL", + ) assert xss_info is None # Eighth type of exploit: Test # Exploitable: - xss_info = xss.get_XSS_data(b"Test" % - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E"), - "https://example.com", - "End of URL") - expected_xss_info = xss.XSSData("https://example.com", - "End of URL", - "Javascript:alert(0)", - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E").decode('utf-8')) + xss_info = xss.get_XSS_data( + b"Test" + % xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E"), + "https://example.com", + "End of URL", + ) + expected_xss_info = xss.XSSData( + "https://example.com", + "End of URL", + "Javascript:alert(0)", + xss.FULL_PAYLOAD.replace(b"<", b"%3C") + .replace(b">", b"%3E") + .decode("utf-8"), + ) assert xss_info == expected_xss_info # Non-Exploitable: - xss_info = xss.get_XSS_data(b"Test" % - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E") - .replace(b"=", b"%3D"), - "https://example.com", - "End of URL") + xss_info = xss.get_XSS_data( + b"Test" + % xss.FULL_PAYLOAD.replace(b"<", b"%3C") + .replace(b">", b"%3E") + .replace(b"=", b"%3D"), + "https://example.com", + "End of URL", + ) assert xss_info is None # Ninth type of exploit: Test # Exploitable: - xss_info = xss.get_XSS_data(b"Test" % - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E"), - "https://example.com", - "End of URL") - expected_xss_info = xss.XSSData("https://example.com", - "End of URL", - '" onmouseover="alert(0)" t="', - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E").decode('utf-8')) + xss_info = xss.get_XSS_data( + b'Test' + % xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E"), + "https://example.com", + "End of URL", + ) + expected_xss_info = xss.XSSData( + "https://example.com", + "End of URL", + '" onmouseover="alert(0)" t="', + xss.FULL_PAYLOAD.replace(b"<", b"%3C") + .replace(b">", b"%3E") + .decode("utf-8"), + ) assert xss_info == expected_xss_info # Non-Exploitable: - xss_info = xss.get_XSS_data(b"Test" % - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E") - .replace(b'"', b"%22"), - "https://example.com", - "End of URL") + xss_info = xss.get_XSS_data( + b'Test' + % xss.FULL_PAYLOAD.replace(b"<", b"%3C") + .replace(b">", b"%3E") + .replace(b'"', b"%22"), + "https://example.com", + "End of URL", + ) assert xss_info is None # Tenth type of exploit: Test # Exploitable: - xss_info = xss.get_XSS_data(b"Test" % - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E"), - "https://example.com", - "End of URL") - expected_xss_info = xss.XSSData("https://example.com", - "End of URL", - "' onmouseover='alert(0)' t='", - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E").decode('utf-8')) + xss_info = xss.get_XSS_data( + b"Test" + % xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E"), + "https://example.com", + "End of URL", + ) + expected_xss_info = xss.XSSData( + "https://example.com", + "End of URL", + "' onmouseover='alert(0)' t='", + xss.FULL_PAYLOAD.replace(b"<", b"%3C") + .replace(b">", b"%3E") + .decode("utf-8"), + ) assert xss_info == expected_xss_info # Non-Exploitable: - xss_info = xss.get_XSS_data(b"Test" % - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E") - .replace(b"'", b"%22"), - "https://example.com", - "End of URL") + xss_info = xss.get_XSS_data( + b"Test" + % xss.FULL_PAYLOAD.replace(b"<", b"%3C") + .replace(b">", b"%3E") + .replace(b"'", b"%22"), + "https://example.com", + "End of URL", + ) assert xss_info is None # Eleventh type of exploit: Test # Exploitable: - xss_info = xss.get_XSS_data(b"Test" % - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E"), - "https://example.com", - "End of URL") - expected_xss_info = xss.XSSData("https://example.com", - "End of URL", - " onmouseover=alert(0) t=", - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E").decode('utf-8')) + xss_info = xss.get_XSS_data( + b"Test" + % xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E"), + "https://example.com", + "End of URL", + ) + expected_xss_info = xss.XSSData( + "https://example.com", + "End of URL", + " onmouseover=alert(0) t=", + xss.FULL_PAYLOAD.replace(b"<", b"%3C") + .replace(b">", b"%3E") + .decode("utf-8"), + ) assert xss_info == expected_xss_info # Non-Exploitable: - xss_info = xss.get_XSS_data(b"Test" % - xss.FULL_PAYLOAD.replace(b"<", b"%3C").replace(b">", b"%3E") - .replace(b"=", b"%3D"), - "https://example.com", - "End of URL") + xss_info = xss.get_XSS_data( + b"Test" + % xss.FULL_PAYLOAD.replace(b"<", b"%3C") + .replace(b">", b"%3E") + .replace(b"=", b"%3D"), + "https://example.com", + "End of URL", + ) assert xss_info is None def test_get_SQLi_data(self): - sqli_data = xss.get_SQLi_data("SQL syntax MySQL", - "", - "https://example.com", - "End of URL") - expected_sqli_data = xss.SQLiData("https://example.com", - "End of URL", - "SQL syntax.*MySQL", - "MySQL") + sqli_data = xss.get_SQLi_data( + "SQL syntax MySQL", + "", + "https://example.com", + "End of URL", + ) + expected_sqli_data = xss.SQLiData( + "https://example.com", "End of URL", "SQL syntax.*MySQL", "MySQL" + ) assert sqli_data == expected_sqli_data - sqli_data = xss.get_SQLi_data("SQL syntax MySQL", - "SQL syntax MySQL", - "https://example.com", - "End of URL") + sqli_data = xss.get_SQLi_data( + "SQL syntax MySQL", + "SQL syntax MySQL", + "https://example.com", + "End of URL", + ) assert sqli_data is None def test_inside_quote(self): @@ -233,9 +335,12 @@ def test_inside_quote(self): assert not xss.inside_quote("'", b"longStringNotInIt", 1, b"short") def test_paths_to_text(self): - text = xss.paths_to_text("""

STRING

+ text = xss.paths_to_text( + """

STRING

- """, "STRING") + """, + "STRING", + ) expected_text = ["/html/head/h1", "/html/script"] assert text == expected_text assert xss.paths_to_text("""""", "STRING") == [] @@ -244,114 +349,155 @@ def mocked_requests_vuln(*args, headers=None, cookies=None): class MockResponse: def __init__(self, html, headers=None, cookies=None): self.text = html + return MockResponse("%s" % xss.FULL_PAYLOAD) def mocked_requests_invuln(*args, headers=None, cookies=None): class MockResponse: def __init__(self, html, headers=None, cookies=None): self.text = html + return MockResponse("") def test_test_end_of_url_injection(self, get_request_vuln): - xss_info = xss.test_end_of_URL_injection("", "https://example.com/index.html", {})[0] - expected_xss_info = xss.XSSData('https://example.com/index.html/1029zxcs\'d"aoso[sb]po(pc)se;sl/bsl\\eq=3847asd', - 'End of URL', - '', - '1029zxcs\\\'d"aoso[sb]po(pc)se;sl/bsl\\\\eq=3847asd') - sqli_info = xss.test_end_of_URL_injection("", "https://example.com/", {})[1] + xss_info = xss.test_end_of_URL_injection( + "", "https://example.com/index.html", {} + )[0] + expected_xss_info = xss.XSSData( + "https://example.com/index.html/1029zxcs'd\"aoso[sb]po(pc)se;sl/bsl\\eq=3847asd", + "End of URL", + "", + "1029zxcs\\'d\"aoso[sb]po(pc)se;sl/bsl\\\\eq=3847asd", + ) + sqli_info = xss.test_end_of_URL_injection( + "", "https://example.com/", {} + )[1] assert xss_info == expected_xss_info assert sqli_info is None def test_test_referer_injection(self, get_request_vuln): - xss_info = xss.test_referer_injection("", "https://example.com/", {})[0] - expected_xss_info = xss.XSSData('https://example.com/', - 'Referer', - '', - '1029zxcs\\\'d"aoso[sb]po(pc)se;sl/bsl\\\\eq=3847asd') - sqli_info = xss.test_referer_injection("", "https://example.com/", {})[1] + xss_info = xss.test_referer_injection( + "", "https://example.com/", {} + )[0] + expected_xss_info = xss.XSSData( + "https://example.com/", + "Referer", + "", + "1029zxcs\\'d\"aoso[sb]po(pc)se;sl/bsl\\\\eq=3847asd", + ) + sqli_info = xss.test_referer_injection( + "", "https://example.com/", {} + )[1] assert xss_info == expected_xss_info assert sqli_info is None def test_test_user_agent_injection(self, get_request_vuln): - xss_info = xss.test_user_agent_injection("", "https://example.com/", {})[0] - expected_xss_info = xss.XSSData('https://example.com/', - 'User Agent', - '', - '1029zxcs\\\'d"aoso[sb]po(pc)se;sl/bsl\\\\eq=3847asd') - sqli_info = xss.test_user_agent_injection("", "https://example.com/", {})[1] + xss_info = xss.test_user_agent_injection( + "", "https://example.com/", {} + )[0] + expected_xss_info = xss.XSSData( + "https://example.com/", + "User Agent", + "", + "1029zxcs\\'d\"aoso[sb]po(pc)se;sl/bsl\\\\eq=3847asd", + ) + sqli_info = xss.test_user_agent_injection( + "", "https://example.com/", {} + )[1] assert xss_info == expected_xss_info assert sqli_info is None def test_test_query_injection(self, get_request_vuln): - - xss_info = xss.test_query_injection("", "https://example.com/vuln.php?cmd=ls", {})[0] - expected_xss_info = xss.XSSData('https://example.com/vuln.php?cmd=1029zxcs\'d"aoso[sb]po(pc)se;sl/bsl\\eq=3847asd', - 'Query', - '', - '1029zxcs\\\'d"aoso[sb]po(pc)se;sl/bsl\\\\eq=3847asd') - sqli_info = xss.test_query_injection("", "https://example.com/vuln.php?cmd=ls", {})[1] + xss_info = xss.test_query_injection( + "", "https://example.com/vuln.php?cmd=ls", {} + )[0] + expected_xss_info = xss.XSSData( + "https://example.com/vuln.php?cmd=1029zxcs'd\"aoso[sb]po(pc)se;sl/bsl\\eq=3847asd", + "Query", + "", + "1029zxcs\\'d\"aoso[sb]po(pc)se;sl/bsl\\\\eq=3847asd", + ) + sqli_info = xss.test_query_injection( + "", "https://example.com/vuln.php?cmd=ls", {} + )[1] assert xss_info == expected_xss_info assert sqli_info is None - @pytest.fixture(scope='function') + @pytest.fixture(scope="function") def get_request_vuln(self, monkeypatch): - monkeypatch.setattr(requests, 'get', self.mocked_requests_vuln) + monkeypatch.setattr(requests, "get", self.mocked_requests_vuln) - @pytest.fixture(scope='function') + @pytest.fixture(scope="function") def get_request_invuln(self, monkeypatch): - monkeypatch.setattr(requests, 'get', self.mocked_requests_invuln) + monkeypatch.setattr(requests, "get", self.mocked_requests_invuln) - @pytest.fixture(scope='function') + @pytest.fixture(scope="function") def mock_gethostbyname(self, monkeypatch): def gethostbyname(domain): claimed_domains = ["google.com"] if domain not in claimed_domains: from socket import gaierror + raise gaierror("[Errno -2] Name or service not known") else: - return '216.58.221.46' + return "216.58.221.46" monkeypatch.setattr("socket.gethostbyname", gethostbyname) def test_find_unclaimed_URLs(self, logger, mock_gethostbyname): - xss.find_unclaimed_URLs("", - "https://example.com") + xss.find_unclaimed_URLs( + '', + "https://example.com", + ) assert logger.args == [] - xss.find_unclaimed_URLs("", - "https://example.com") - assert logger.args[0] == 'XSS found in https://example.com due to unclaimed URL "http://unclaimedDomainName.com".' - xss.find_unclaimed_URLs("", - "https://example.com") - assert logger.args[1] == 'XSS found in https://example.com due to unclaimed URL "http://unclaimedDomainName.com".' - xss.find_unclaimed_URLs("", - "https://example.com") - assert logger.args[2] == 'XSS found in https://example.com due to unclaimed URL "http://unclaimedDomainName.com".' + xss.find_unclaimed_URLs( + '', + "https://example.com", + ) + assert ( + logger.args[0] + == 'XSS found in https://example.com due to unclaimed URL "http://unclaimedDomainName.com".' + ) + xss.find_unclaimed_URLs( + '', + "https://example.com", + ) + assert ( + logger.args[1] + == 'XSS found in https://example.com due to unclaimed URL "http://unclaimedDomainName.com".' + ) + xss.find_unclaimed_URLs( + '', + "https://example.com", + ) + assert ( + logger.args[2] + == 'XSS found in https://example.com due to unclaimed URL "http://unclaimedDomainName.com".' + ) def test_log_XSS_data(self, logger): xss.log_XSS_data(None) assert logger.args == [] # self, url: str, injection_point: str, exploit: str, line: str - xss.log_XSS_data(xss.XSSData('https://example.com', - 'Location', - 'String', - 'Line of HTML')) - assert logger.args[0] == '===== XSS Found ====' - assert logger.args[1] == 'XSS URL: https://example.com' - assert logger.args[2] == 'Injection Point: Location' - assert logger.args[3] == 'Suggested Exploit: String' - assert logger.args[4] == 'Line: Line of HTML' + xss.log_XSS_data( + xss.XSSData("https://example.com", "Location", "String", "Line of HTML") + ) + assert logger.args[0] == "===== XSS Found ====" + assert logger.args[1] == "XSS URL: https://example.com" + assert logger.args[2] == "Injection Point: Location" + assert logger.args[3] == "Suggested Exploit: String" + assert logger.args[4] == "Line: Line of HTML" def test_log_SQLi_data(self, logger): xss.log_SQLi_data(None) assert logger.args == [] - xss.log_SQLi_data(xss.SQLiData('https://example.com', - 'Location', - 'Oracle.*Driver', - 'Oracle')) - assert logger.args[0] == '===== SQLi Found =====' - assert logger.args[1] == 'SQLi URL: https://example.com' - assert logger.args[2] == 'Injection Point: Location' - assert logger.args[3] == 'Regex used: Oracle.*Driver' + xss.log_SQLi_data( + xss.SQLiData("https://example.com", "Location", "Oracle.*Driver", "Oracle") + ) + assert logger.args[0] == "===== SQLi Found =====" + assert logger.args[1] == "SQLi URL: https://example.com" + assert logger.args[2] == "Injection Point: Location" + assert logger.args[3] == "Regex used: Oracle.*Driver" def test_get_cookies(self): mocked_req = tutils.treq() @@ -363,7 +509,7 @@ def test_get_cookies(self): def test_response(self, get_request_invuln, logger): mocked_flow = tflow.tflow( req=tutils.treq(path=b"index.html?q=1"), - resp=tutils.tresp(content=b'') + resp=tutils.tresp(content=b""), ) xss.response(mocked_flow) assert logger.args == [] diff --git a/examples/contrib/tls_passthrough.py b/examples/contrib/tls_passthrough.py index 16d90ddad2..6f43430d39 100644 --- a/examples/contrib/tls_passthrough.py +++ b/examples/contrib/tls_passthrough.py @@ -14,13 +14,18 @@ 3. curl --proxy http://localhost:8080 https://example.com // works again, but mitmproxy does not intercept and we do *not* see the contents """ + import collections import logging import random -from abc import ABC, abstractmethod +from abc import ABC +from abc import abstractmethod from enum import Enum -from mitmproxy import connection, ctx, tls +from mitmproxy import connection +from mitmproxy import ctx +from mitmproxy import tls +from mitmproxy.addonmanager import Loader from mitmproxy.utils import human @@ -54,6 +59,7 @@ class ConservativeStrategy(TlsStrategy): Conservative Interception Strategy - only intercept if there haven't been any failed attempts in the history. """ + def should_intercept(self, server_address: connection.Address) -> bool: return InterceptionResult.FAILURE not in self.history[server_address] @@ -62,6 +68,7 @@ class ProbabilisticStrategy(TlsStrategy): """ Fixed probability that we intercept a given connection. """ + def __init__(self, p: float): self.p = p super().__init__() @@ -73,9 +80,11 @@ def should_intercept(self, server_address: connection.Address) -> bool: class MaybeTls: strategy: TlsStrategy - def load(self, l): - l.add_option( - "tls_strategy", int, 0, + def load(self, loader: Loader): + loader.add_option( + "tls_strategy", + int, + 0, "TLS passthrough strategy. If set to 0, connections will be passed through after the first unsuccessful " "handshake. If set to 0 < p <= 100, connections with be passed through with probability p.", ) @@ -88,20 +97,27 @@ def configure(self, updated): else: self.strategy = ConservativeStrategy() + @staticmethod + def get_addr(server: connection.Server): + # .peername may be unset in upstream proxy mode, so we need a fallback. + return server.peername or server.address + def tls_clienthello(self, data: tls.ClientHelloData): - server_address = data.context.server.peername + server_address = self.get_addr(data.context.server) if not self.strategy.should_intercept(server_address): logging.info(f"TLS passthrough: {human.format_address(server_address)}.") data.ignore_connection = True self.strategy.record_skipped(server_address) def tls_established_client(self, data: tls.TlsData): - server_address = data.context.server.peername - logging.info(f"TLS handshake successful: {human.format_address(server_address)}") + server_address = self.get_addr(data.context.server) + logging.info( + f"TLS handshake successful: {human.format_address(server_address)}" + ) self.strategy.record_success(server_address) def tls_failed_client(self, data: tls.TlsData): - server_address = data.context.server.peername + server_address = self.get_addr(data.context.server) logging.info(f"TLS handshake failed: {human.format_address(server_address)}") self.strategy.record_failure(server_address) diff --git a/examples/contrib/webscanner_helper/mapping.py b/examples/contrib/webscanner_helper/mapping.py index 333809f537..0e465a8bf2 100644 --- a/examples/contrib/webscanner_helper/mapping.py +++ b/examples/contrib/webscanner_helper/mapping.py @@ -3,8 +3,8 @@ from bs4 import BeautifulSoup -from mitmproxy.http import HTTPFlow from examples.contrib.webscanner_helper.urldict import URLDict +from mitmproxy.http import HTTPFlow NO_CONTENT = object() @@ -14,7 +14,7 @@ class MappingAddonConfig: class MappingAddon: - """ The mapping add-on can be used in combination with web application scanners to reduce their false positives. + """The mapping add-on can be used in combination with web application scanners to reduce their false positives. Many web application scanners produce false positives caused by dynamically changing content of web applications such as the current time or current measurements. When testing for injection vulnerabilities, web application @@ -45,7 +45,7 @@ class MappingAddon: """Whether to store all new content in the configuration file.""" def __init__(self, filename: str, persistent: bool = False) -> None: - """ Initializes the mapping add-on + """Initializes the mapping add-on Args: filename: str that provides the name of the file in which the urls and css selectors to mapped content is @@ -71,12 +71,16 @@ def __init__(self, filename: str, persistent: bool = False) -> None: def load(self, loader): loader.add_option( - self.OPT_MAPPING_FILE, str, "", - "File where replacement configuration is stored." + self.OPT_MAPPING_FILE, + str, + "", + "File where replacement configuration is stored.", ) loader.add_option( - self.OPT_MAP_PERSISTENT, bool, False, - "Whether to store all new content in the configuration file." + self.OPT_MAP_PERSISTENT, + bool, + False, + "Whether to store all new content in the configuration file.", ) def configure(self, updated): @@ -88,23 +92,33 @@ def configure(self, updated): if self.OPT_MAP_PERSISTENT in updated: self.persistent = updated[self.OPT_MAP_PERSISTENT] - def replace(self, soup: BeautifulSoup, css_sel: str, replace: BeautifulSoup) -> None: + def replace( + self, soup: BeautifulSoup, css_sel: str, replace: BeautifulSoup + ) -> None: """Replaces the content of soup that matches the css selector with the given replace content.""" for content in soup.select(css_sel): - self.logger.debug(f"replace \"{content}\" with \"{replace}\"") + self.logger.debug(f'replace "{content}" with "{replace}"') content.replace_with(copy.copy(replace)) - def apply_template(self, soup: BeautifulSoup, template: dict[str, BeautifulSoup]) -> None: + def apply_template( + self, soup: BeautifulSoup, template: dict[str, BeautifulSoup] + ) -> None: """Applies the given mapping template to the given soup.""" for css_sel, replace in template.items(): mapped = soup.select(css_sel) if not mapped: - self.logger.warning(f"Could not find \"{css_sel}\", can not freeze anything.") + self.logger.warning( + f'Could not find "{css_sel}", can not freeze anything.' + ) else: - self.replace(soup, css_sel, BeautifulSoup(replace, features=MappingAddonConfig.HTML_PARSER)) + self.replace( + soup, + css_sel, + BeautifulSoup(replace, features=MappingAddonConfig.HTML_PARSER), + ) def response(self, flow: HTTPFlow) -> None: - """If a response is received, check if we should replace some content. """ + """If a response is received, check if we should replace some content.""" try: templates = self.mapping_templates[flow] res = flow.response @@ -118,14 +132,15 @@ def response(self, flow: HTTPFlow) -> None: self.apply_template(content, template) res.content = content.encode(encoding) else: - self.logger.warning(f"Unsupported content type '{content_type}' or content encoding '{encoding}'") + self.logger.warning( + f"Unsupported content type '{content_type}' or content encoding '{encoding}'" + ) except KeyError: pass def done(self) -> None: """Dumps all new content into the configuration file if self.persistent is set.""" if self.persistent: - # make sure that all items are strings and not soups. def value_dumper(value): store = {} @@ -134,7 +149,7 @@ def value_dumper(value): try: for css_sel, soup in value.items(): store[css_sel] = str(soup) - except: + except Exception: raise RuntimeError(value) return store diff --git a/examples/contrib/webscanner_helper/proxyauth_selenium.py b/examples/contrib/webscanner_helper/proxyauth_selenium.py index 6ac1d94de6..579fcc3d20 100644 --- a/examples/contrib/webscanner_helper/proxyauth_selenium.py +++ b/examples/contrib/webscanner_helper/proxyauth_selenium.py @@ -3,13 +3,15 @@ import random import string import time -from typing import Any, cast +from typing import Any +from typing import cast + +from selenium import webdriver import mitmproxy.http from mitmproxy import flowfilter from mitmproxy import master from mitmproxy.script import concurrent -from selenium import webdriver logger = logging.getLogger(__name__) @@ -18,14 +20,14 @@ "expires": "Expires", "domain": "Domain", "is_http_only": "HttpOnly", - "is_secure": "Secure" + "is_secure": "Secure", } def randomString(string_length=10): - """Generate a random string of fixed length """ + """Generate a random string of fixed length""" letters = string.ascii_lowercase - return ''.join(random.choice(letters) for i in range(string_length)) + return "".join(random.choice(letters) for i in range(string_length)) class AuthorizationOracle(abc.ABC): @@ -41,7 +43,7 @@ def is_unauthorized_response(self, flow: mitmproxy.http.HTTPFlow) -> bool: class SeleniumAddon: - """ This Addon can be used in combination with web application scanners in order to help them to authenticate + """This Addon can be used in combination with web application scanners in order to help them to authenticate against a web application. Since the authentication is highly dependant on the web application, this add-on includes the abstract method @@ -50,8 +52,7 @@ class SeleniumAddon: application. In addition, an authentication oracle which inherits from AuthorizationOracle should be created. """ - def __init__(self, fltr: str, domain: str, - auth_oracle: AuthorizationOracle): + def __init__(self, fltr: str, domain: str, auth_oracle: AuthorizationOracle): self.filter = flowfilter.parse(fltr) self.auth_oracle = auth_oracle self.domain = domain @@ -62,9 +63,8 @@ def __init__(self, fltr: str, domain: str, options.headless = True profile = webdriver.FirefoxProfile() - profile.set_preference('network.proxy.type', 0) - self.browser = webdriver.Firefox(firefox_profile=profile, - options=options) + profile.set_preference("network.proxy.type", 0) + self.browser = webdriver.Firefox(firefox_profile=profile, options=options) self.cookies: list[dict[str, str]] = [] def _login(self, flow): @@ -76,7 +76,9 @@ def _login(self, flow): def request(self, flow: mitmproxy.http.HTTPFlow): if flow.request.is_replay: logger.warning("Caught replayed request: " + str(flow)) - if (not self.filter or self.filter(flow)) and self.auth_oracle.is_unauthorized_request(flow): + if ( + not self.filter or self.filter(flow) + ) and self.auth_oracle.is_unauthorized_request(flow): logger.debug("unauthorized request detected, perform login") self._login(flow) @@ -88,7 +90,7 @@ def response(self, flow: mitmproxy.http.HTTPFlow): if self.auth_oracle.is_unauthorized_response(flow): self._login(flow) new_flow = flow.copy() - if master and hasattr(master, 'commands'): + if master and hasattr(master, "commands"): # cast necessary for mypy cast(Any, master).commands.call("replay.client", [new_flow]) count = 0 @@ -99,7 +101,9 @@ def response(self, flow: mitmproxy.http.HTTPFlow): if new_flow.response: flow.response = new_flow.response else: - logger.warning("Could not call 'replay.client' command since master was not initialized yet.") + logger.warning( + "Could not call 'replay.client' command since master was not initialized yet." + ) if self.set_cookies and flow.response: logger.debug("set set-cookie header for response") @@ -124,7 +128,8 @@ def _set_set_cookie_headers(self, flow: mitmproxy.http.HTTPFlow): def _set_request_cookies(self, flow: mitmproxy.http.HTTPFlow): if self.cookies: cookies = "; ".join( - map(lambda c: f"{c['name']}={c['value']}", self.cookies)) + map(lambda c: f"{c['name']}={c['value']}", self.cookies) + ) flow.request.headers["cookie"] = cookies @abc.abstractmethod diff --git a/examples/contrib/webscanner_helper/test_mapping.py b/examples/contrib/webscanner_helper/test_mapping.py index 340522837c..89ed27c219 100644 --- a/examples/contrib/webscanner_helper/test_mapping.py +++ b/examples/contrib/webscanner_helper/test_mapping.py @@ -1,15 +1,15 @@ -from typing import TextIO, Callable +from collections.abc import Callable +from typing import TextIO from unittest import mock from unittest.mock import MagicMock +from examples.contrib.webscanner_helper.mapping import MappingAddon +from examples.contrib.webscanner_helper.mapping import MappingAddonConfig from mitmproxy.test import tflow from mitmproxy.test import tutils -from examples.contrib.webscanner_helper.mapping import MappingAddon, MappingAddonConfig - class TestConfig: - def test_config(self): assert MappingAddonConfig.HTML_PARSER == "html.parser" @@ -20,7 +20,6 @@ def test_config(self): class TestMappingAddon: - def test_init(self, tmpdir): tmpfile = tmpdir.join("tmpfile") with open(tmpfile, "w") as tfile: @@ -36,8 +35,8 @@ def test_load(self, tmpdir): loader = MagicMock() mapping.load(loader) - assert 'mapping_file' in str(loader.add_option.call_args_list) - assert 'map_persistent' in str(loader.add_option.call_args_list) + assert "mapping_file" in str(loader.add_option.call_args_list) + assert "map_persistent" in str(loader.add_option.call_args_list) def test_configure(self, tmpdir): tmpfile = tmpdir.join("tmpfile") @@ -45,7 +44,10 @@ def test_configure(self, tmpdir): tfile.write(mapping_content) mapping = MappingAddon(tmpfile) new_filename = "My new filename" - updated = {str(mapping.OPT_MAPPING_FILE): new_filename, str(mapping.OPT_MAP_PERSISTENT): True} + updated = { + str(mapping.OPT_MAPPING_FILE): new_filename, + str(mapping.OPT_MAP_PERSISTENT): True, + } open_mock = mock.mock_open(read_data="{}") with mock.patch("builtins.open", open_mock): @@ -161,5 +163,8 @@ def test_dump(selfself, tmpdir): with open(tmpfile, "w") as tfile: tfile.write("{}") mapping = MappingAddon(tmpfile, persistent=True) - with mock.patch('examples.complex.webscanner_helper.urldict.URLDict.dump', selfself.mock_dump): + with mock.patch( + "examples.complex.webscanner_helper.urldict.URLDict.dump", + selfself.mock_dump, + ): mapping.done() diff --git a/examples/contrib/webscanner_helper/test_proxyauth_selenium.py b/examples/contrib/webscanner_helper/test_proxyauth_selenium.py index 58e035068f..e755c776c6 100644 --- a/examples/contrib/webscanner_helper/test_proxyauth_selenium.py +++ b/examples/contrib/webscanner_helper/test_proxyauth_selenium.py @@ -3,16 +3,16 @@ import pytest +from examples.contrib.webscanner_helper.proxyauth_selenium import AuthorizationOracle +from examples.contrib.webscanner_helper.proxyauth_selenium import logger +from examples.contrib.webscanner_helper.proxyauth_selenium import randomString +from examples.contrib.webscanner_helper.proxyauth_selenium import SeleniumAddon +from mitmproxy.http import HTTPFlow from mitmproxy.test import tflow from mitmproxy.test import tutils -from mitmproxy.http import HTTPFlow - -from examples.contrib.webscanner_helper.proxyauth_selenium import logger, randomString, AuthorizationOracle, \ - SeleniumAddon class TestRandomString: - def test_random_string(self): res = randomString() assert isinstance(res, str) @@ -36,8 +36,11 @@ def is_unauthorized_response(self, flow: HTTPFlow) -> bool: @pytest.fixture(scope="module", autouse=True) def selenium_addon(request): - addon = SeleniumAddon(fltr=r"~u http://example\.com/login\.php", domain=r"~d http://example\.com", - auth_oracle=oracle) + addon = SeleniumAddon( + fltr=r"~u http://example\.com/login\.php", + domain=r"~d http://example\.com", + auth_oracle=oracle, + ) browser = MagicMock() addon.browser = browser yield addon @@ -49,11 +52,10 @@ def fin(): class TestSeleniumAddon: - def test_request_replay(self, selenium_addon): f = tflow.tflow(resp=tutils.tresp()) f.request.is_replay = True - with mock.patch.object(logger, 'warning') as mock_warning: + with mock.patch.object(logger, "warning") as mock_warning: selenium_addon.request(f) mock_warning.assert_called() @@ -62,7 +64,7 @@ def test_request(self, selenium_addon): f.request.url = "http://example.com/login.php" selenium_addon.set_cookies = False assert not selenium_addon.set_cookies - with mock.patch.object(logger, 'debug') as mock_debug: + with mock.patch.object(logger, "debug") as mock_debug: selenium_addon.request(f) mock_debug.assert_called() assert selenium_addon.set_cookies @@ -79,9 +81,11 @@ def test_request_cookies(self, selenium_addon): f.request.url = "http://example.com/login.php" selenium_addon.set_cookies = False assert not selenium_addon.set_cookies - with mock.patch.object(logger, 'debug') as mock_debug: - with mock.patch('examples.complex.webscanner_helper.proxyauth_selenium.SeleniumAddon.login', - return_value=[{"name": "cookie", "value": "test"}]) as mock_login: + with mock.patch.object(logger, "debug") as mock_debug: + with mock.patch( + "examples.complex.webscanner_helper.proxyauth_selenium.SeleniumAddon.login", + return_value=[{"name": "cookie", "value": "test"}], + ) as mock_login: selenium_addon.request(f) mock_debug.assert_called() assert selenium_addon.set_cookies @@ -95,7 +99,7 @@ def test_request_filter_None(self, selenium_addon): selenium_addon.set_cookies = False assert not selenium_addon.set_cookies - with mock.patch.object(logger, 'debug') as mock_debug: + with mock.patch.object(logger, "debug") as mock_debug: selenium_addon.request(f) mock_debug.assert_called() selenium_addon.filter = fltr @@ -105,8 +109,10 @@ def test_response(self, selenium_addon): f = tflow.tflow(resp=tutils.tresp()) f.request.url = "http://example.com/login.php" selenium_addon.set_cookies = False - with mock.patch('examples.complex.webscanner_helper.proxyauth_selenium.SeleniumAddon.login', - return_value=[]) as mock_login: + with mock.patch( + "examples.complex.webscanner_helper.proxyauth_selenium.SeleniumAddon.login", + return_value=[], + ) as mock_login: selenium_addon.response(f) mock_login.assert_called() @@ -114,7 +120,9 @@ def test_response_cookies(self, selenium_addon): f = tflow.tflow(resp=tutils.tresp()) f.request.url = "http://example.com/login.php" selenium_addon.set_cookies = False - with mock.patch('examples.complex.webscanner_helper.proxyauth_selenium.SeleniumAddon.login', - return_value=[{"name": "cookie", "value": "test"}]) as mock_login: + with mock.patch( + "examples.complex.webscanner_helper.proxyauth_selenium.SeleniumAddon.login", + return_value=[{"name": "cookie", "value": "test"}], + ) as mock_login: selenium_addon.response(f) mock_login.assert_called() diff --git a/examples/contrib/webscanner_helper/test_urldict.py b/examples/contrib/webscanner_helper/test_urldict.py index 102c9ee35f..066566237c 100644 --- a/examples/contrib/webscanner_helper/test_urldict.py +++ b/examples/contrib/webscanner_helper/test_urldict.py @@ -1,5 +1,6 @@ -from mitmproxy.test import tflow, tutils from examples.contrib.webscanner_helper.urldict import URLDict +from mitmproxy.test import tflow +from mitmproxy.test import tutils url = "http://10.10.10.10" new_content_body = "New Body" @@ -11,11 +12,10 @@ class TestUrlDict: - def test_urldict_empty(self): urldict = URLDict() dump = urldict.dumps() - assert dump == '{}' + assert dump == "{}" def test_urldict_loads(self): urldict = URLDict.loads(input_file_content) diff --git a/examples/contrib/webscanner_helper/test_urlindex.py b/examples/contrib/webscanner_helper/test_urlindex.py index 058a36068f..d3dd5f4807 100644 --- a/examples/contrib/webscanner_helper/test_urlindex.py +++ b/examples/contrib/webscanner_helper/test_urlindex.py @@ -4,17 +4,18 @@ from unittest import mock from unittest.mock import patch +from examples.contrib.webscanner_helper.urlindex import filter_404 +from examples.contrib.webscanner_helper.urlindex import JSONUrlIndexWriter +from examples.contrib.webscanner_helper.urlindex import SetEncoder +from examples.contrib.webscanner_helper.urlindex import TextUrlIndexWriter +from examples.contrib.webscanner_helper.urlindex import UrlIndexAddon +from examples.contrib.webscanner_helper.urlindex import UrlIndexWriter +from examples.contrib.webscanner_helper.urlindex import WRITER from mitmproxy.test import tflow from mitmproxy.test import tutils -from examples.contrib.webscanner_helper.urlindex import UrlIndexWriter, SetEncoder, JSONUrlIndexWriter, \ - TextUrlIndexWriter, WRITER, \ - filter_404, \ - UrlIndexAddon - class TestBaseClass: - @patch.multiple(UrlIndexWriter, __abstractmethods__=set()) def test_base_class(self, tmpdir): tmpfile = tmpdir.join("tmpfile") @@ -25,14 +26,13 @@ def test_base_class(self, tmpdir): class TestSetEncoder: - def test_set_encoder_set(self): test_set = {"foo", "bar", "42"} result = SetEncoder.default(SetEncoder(), test_set) assert isinstance(result, list) - assert 'foo' in result - assert 'bar' in result - assert '42' in result + assert "foo" in result + assert "bar" in result + assert "42" in result def test_set_encoder_str(self): test_str = "test" @@ -45,18 +45,18 @@ def test_set_encoder_str(self): class TestJSONUrlIndexWriter: - def test_load(self, tmpdir): tmpfile = tmpdir.join("tmpfile") with open(tmpfile, "w") as tfile: tfile.write( - "{\"http://example.com:80\": {\"/\": {\"GET\": [301]}}, \"http://www.example.com:80\": {\"/\": {\"GET\": [302]}}}") + '{"http://example.com:80": {"/": {"GET": [301]}}, "http://www.example.com:80": {"/": {"GET": [302]}}}' + ) writer = JSONUrlIndexWriter(filename=tmpfile) writer.load() - assert 'http://example.com:80' in writer.host_urls - assert '/' in writer.host_urls['http://example.com:80'] - assert 'GET' in writer.host_urls['http://example.com:80']['/'] - assert 301 in writer.host_urls['http://example.com:80']['/']['GET'] + assert "http://example.com:80" in writer.host_urls + assert "/" in writer.host_urls["http://example.com:80"] + assert "GET" in writer.host_urls["http://example.com:80"]["/"] + assert 301 in writer.host_urls["http://example.com:80"]["/"]["GET"] def test_load_empty(self, tmpdir): tmpfile = tmpdir.join("tmpfile") @@ -102,7 +102,8 @@ def test_load(self, tmpdir): tmpfile = tmpdir.join("tmpfile") with open(tmpfile, "w") as tfile: tfile.write( - "2020-04-22T05:41:08.679231 STATUS: 200 METHOD: GET URL:http://example.com") + "2020-04-22T05:41:08.679231 STATUS: 200 METHOD: GET URL:http://example.com" + ) writer = TextUrlIndexWriter(filename=tmpfile) writer.load() assert True @@ -173,7 +174,6 @@ def test_filter_false(self): class TestUrlIndexAddon: - def test_init(self, tmpdir): tmpfile = tmpdir.join("tmpfile") UrlIndexAddon(tmpfile) @@ -202,7 +202,9 @@ def test_init_append(self, tmpdir): tfile.write("") url_index = UrlIndexAddon(tmpfile, append=False) f = tflow.tflow(resp=tutils.tresp()) - with mock.patch('examples.complex.webscanner_helper.urlindex.JSONUrlIndexWriter.add_url'): + with mock.patch( + "examples.complex.webscanner_helper.urlindex.JSONUrlIndexWriter.add_url" + ): url_index.response(f) assert not Path(tmpfile).exists() @@ -210,7 +212,9 @@ def test_response(self, tmpdir): tmpfile = tmpdir.join("tmpfile") url_index = UrlIndexAddon(tmpfile) f = tflow.tflow(resp=tutils.tresp()) - with mock.patch('examples.complex.webscanner_helper.urlindex.JSONUrlIndexWriter.add_url') as mock_add_url: + with mock.patch( + "examples.complex.webscanner_helper.urlindex.JSONUrlIndexWriter.add_url" + ) as mock_add_url: url_index.response(f) mock_add_url.assert_called() @@ -229,6 +233,8 @@ def test_response_None(self, tmpdir): def test_done(self, tmpdir): tmpfile = tmpdir.join("tmpfile") url_index = UrlIndexAddon(tmpfile) - with mock.patch('examples.complex.webscanner_helper.urlindex.JSONUrlIndexWriter.save') as mock_save: + with mock.patch( + "examples.complex.webscanner_helper.urlindex.JSONUrlIndexWriter.save" + ) as mock_save: url_index.done() mock_save.assert_called() diff --git a/examples/contrib/webscanner_helper/test_urlinjection.py b/examples/contrib/webscanner_helper/test_urlinjection.py index b1c412d21a..b6a841721f 100644 --- a/examples/contrib/webscanner_helper/test_urlinjection.py +++ b/examples/contrib/webscanner_helper/test_urlinjection.py @@ -1,20 +1,22 @@ import json from unittest import mock +from examples.contrib.webscanner_helper.urlinjection import HTMLInjection +from examples.contrib.webscanner_helper.urlinjection import InjectionGenerator +from examples.contrib.webscanner_helper.urlinjection import logger +from examples.contrib.webscanner_helper.urlinjection import RobotsInjection +from examples.contrib.webscanner_helper.urlinjection import SitemapInjection +from examples.contrib.webscanner_helper.urlinjection import UrlInjectionAddon from mitmproxy import flowfilter from mitmproxy.test import tflow from mitmproxy.test import tutils -from examples.contrib.webscanner_helper.urlinjection import InjectionGenerator, HTMLInjection, RobotsInjection, \ - SitemapInjection, \ - UrlInjectionAddon, logger - index = json.loads( - "{\"http://example.com:80\": {\"/\": {\"GET\": [301]}}, \"http://www.example.com:80\": {\"/test\": {\"POST\": [302]}}}") + '{"http://example.com:80": {"/": {"GET": [301]}}, "http://www.example.com:80": {"/test": {"POST": [302]}}}' +) class TestInjectionGenerator: - def test_inject(self): f = tflow.tflow(resp=tutils.tresp()) injection_generator = InjectionGenerator() @@ -23,12 +25,11 @@ def test_inject(self): class TestHTMLInjection: - def test_inject_not404(self): html_injection = HTMLInjection() f = tflow.tflow(resp=tutils.tresp()) - with mock.patch.object(logger, 'warning') as mock_warning: + with mock.patch.object(logger, "warning") as mock_warning: html_injection.inject(index, f) assert mock_warning.called @@ -57,12 +58,11 @@ def test_inject_404(self): class TestRobotsInjection: - def test_inject_not404(self): robots_injection = RobotsInjection() f = tflow.tflow(resp=tutils.tresp()) - with mock.patch.object(logger, 'warning') as mock_warning: + with mock.patch.object(logger, "warning") as mock_warning: robots_injection.inject(index, f) assert mock_warning.called @@ -76,12 +76,11 @@ def test_inject_404(self): class TestSitemapInjection: - def test_inject_not404(self): sitemap_injection = SitemapInjection() f = tflow.tflow(resp=tutils.tresp()) - with mock.patch.object(logger, 'warning') as mock_warning: + with mock.patch.object(logger, "warning") as mock_warning: sitemap_injection.inject(index, f) assert mock_warning.called @@ -89,19 +88,22 @@ def test_inject_404(self): sitemap_injection = SitemapInjection() f = tflow.tflow(resp=tutils.tresp()) f.response.status_code = 404 - assert "http://example.com:80/" not in str(f.response.content) + assert "http://example.com:80/" not in str( + f.response.content + ) sitemap_injection.inject(index, f) assert "http://example.com:80/" in str(f.response.content) class TestUrlInjectionAddon: - def test_init(self, tmpdir): tmpfile = tmpdir.join("tmpfile") with open(tmpfile, "w") as tfile: json.dump(index, tfile) flt = f"~u .*/site.html$" - url_injection = UrlInjectionAddon(f"~u .*/site.html$", tmpfile, HTMLInjection(insert=True)) + url_injection = UrlInjectionAddon( + f"~u .*/site.html$", tmpfile, HTMLInjection(insert=True) + ) assert "http://example.com:80" in url_injection.url_store fltr = flowfilter.parse(flt) f = tflow.tflow(resp=tutils.tresp()) diff --git a/examples/contrib/webscanner_helper/test_watchdog.py b/examples/contrib/webscanner_helper/test_watchdog.py index f6a34a61b1..d5382072bd 100644 --- a/examples/contrib/webscanner_helper/test_watchdog.py +++ b/examples/contrib/webscanner_helper/test_watchdog.py @@ -1,18 +1,17 @@ +import multiprocessing import time from pathlib import Path from unittest import mock +from examples.contrib.webscanner_helper.watchdog import logger +from examples.contrib.webscanner_helper.watchdog import WatchdogAddon from mitmproxy.connections import ServerConnection from mitmproxy.exceptions import HttpSyntaxException from mitmproxy.test import tflow from mitmproxy.test import tutils -import multiprocessing - -from examples.contrib.webscanner_helper.watchdog import WatchdogAddon, logger class TestWatchdog: - def test_init_file(self, tmpdir): tmpfile = tmpdir.join("tmpfile") with open(tmpfile, "w") as tfile: @@ -35,14 +34,18 @@ def test_init_dir(self, tmpdir): def test_serverconnect(self, tmpdir): event = multiprocessing.Event() w = WatchdogAddon(event, Path(tmpdir), timeout=10) - with mock.patch('mitmproxy.connections.ServerConnection.settimeout') as mock_set_timeout: + with mock.patch( + "mitmproxy.connections.ServerConnection.settimeout" + ) as mock_set_timeout: w.serverconnect(ServerConnection("127.0.0.1")) mock_set_timeout.assert_called() def test_serverconnect_None(self, tmpdir): event = multiprocessing.Event() w = WatchdogAddon(event, Path(tmpdir)) - with mock.patch('mitmproxy.connections.ServerConnection.settimeout') as mock_set_timeout: + with mock.patch( + "mitmproxy.connections.ServerConnection.settimeout" + ) as mock_set_timeout: w.serverconnect(ServerConnection("127.0.0.1")) assert not mock_set_timeout.called @@ -52,7 +55,7 @@ def test_trigger(self, tmpdir): f = tflow.tflow(resp=tutils.tresp()) f.error = "Test Error" - with mock.patch.object(logger, 'error') as mock_error: + with mock.patch.object(logger, "error") as mock_error: open_mock = mock.mock_open() with mock.patch("pathlib.Path.open", open_mock, create=True): w.error(f) @@ -66,7 +69,7 @@ def test_trigger_http_synatx(self, tmpdir): f.error = HttpSyntaxException() assert isinstance(f.error, HttpSyntaxException) - with mock.patch.object(logger, 'error') as mock_error: + with mock.patch.object(logger, "error") as mock_error: open_mock = mock.mock_open() with mock.patch("pathlib.Path.open", open_mock, create=True): w.error(f) @@ -79,6 +82,6 @@ def test_timeout(self, tmpdir): assert w.not_in_timeout(None, None) assert w.not_in_timeout(time.time, None) - with mock.patch('time.time', return_value=5): + with mock.patch("time.time", return_value=5): assert not w.not_in_timeout(3, 20) assert w.not_in_timeout(3, 1) diff --git a/examples/contrib/webscanner_helper/urldict.py b/examples/contrib/webscanner_helper/urldict.py index 7e990f1afc..e527667631 100644 --- a/examples/contrib/webscanner_helper/urldict.py +++ b/examples/contrib/webscanner_helper/urldict.py @@ -1,7 +1,11 @@ import itertools import json +from collections.abc import Callable +from collections.abc import Generator from collections.abc import MutableMapping -from typing import Any, Callable, Generator, TextIO, Union, cast +from typing import Any +from typing import cast +from typing import TextIO from mitmproxy import flowfilter from mitmproxy.http import HTTPFlow @@ -45,7 +49,6 @@ def __len__(self): return self.store.__len__() def get_generator(self, flow: HTTPFlow) -> Generator[Any, None, None]: - for fltr, value in self.store.items(): if flowfilter.match(fltr, flow): yield value @@ -74,9 +77,9 @@ def loads(cls, json_str: str, value_loader: Callable = f_id): return cls._load(json_obj, value_loader) def _dump(self, value_dumper: Callable = f_id) -> dict: - dumped: dict[Union[flowfilter.TFilter, str], Any] = {} + dumped: dict[flowfilter.TFilter | str, Any] = {} for fltr, value in self.store.items(): - if hasattr(fltr, 'pattern'): + if hasattr(fltr, "pattern"): # cast necessary for mypy dumped[cast(Any, fltr).pattern] = value_dumper(value) else: diff --git a/examples/contrib/webscanner_helper/urlindex.py b/examples/contrib/webscanner_helper/urlindex.py index 650e47c015..db43bbe326 100644 --- a/examples/contrib/webscanner_helper/urlindex.py +++ b/examples/contrib/webscanner_helper/urlindex.py @@ -3,7 +3,6 @@ import json import logging from pathlib import Path -from typing import Optional, Union from mitmproxy import flowfilter from mitmproxy.http import HTTPFlow @@ -67,7 +66,9 @@ def add_url(self, flow: HTTPFlow): res = flow.response if req is not None and res is not None: - urls = self.host_urls.setdefault(f"{req.scheme}://{req.host}:{req.port}", dict()) + urls = self.host_urls.setdefault( + f"{req.scheme}://{req.host}:{req.port}", dict() + ) methods = urls.setdefault(req.path, {}) codes = methods.setdefault(req.method, set()) codes.add(res.status_code) @@ -88,8 +89,10 @@ def add_url(self, flow: HTTPFlow): req = flow.request if res is not None and req is not None: with self.filepath.open("a+") as f: - f.write(f"{datetime.datetime.utcnow().isoformat()} STATUS: {res.status_code} METHOD: " - f"{req.method} URL:{req.url}\n") + f.write( + f"{datetime.datetime.utcnow().isoformat()} STATUS: {res.status_code} METHOD: " + f"{req.method} URL:{req.url}\n" + ) def save(self): pass @@ -113,16 +116,21 @@ class UrlIndexAddon: The injection can be done using the URLInjection Add-on. """ - index_filter: Optional[Union[str, flowfilter.TFilter]] + index_filter: str | flowfilter.TFilter | None writer: UrlIndexWriter OPT_FILEPATH = "URLINDEX_FILEPATH" OPT_APPEND = "URLINDEX_APPEND" OPT_INDEX_FILTER = "URLINDEX_FILTER" - def __init__(self, file_path: Union[str, Path], append: bool = True, - index_filter: Union[str, flowfilter.TFilter] = filter_404, index_format: str = "json"): - """ Initializes the urlindex add-on. + def __init__( + self, + file_path: str | Path, + append: bool = True, + index_filter: str | flowfilter.TFilter = filter_404, + index_format: str = "json", + ): + """Initializes the urlindex add-on. Args: file_path: Path to file to which the URL index will be written. Can either be given as str or Path. @@ -153,7 +161,7 @@ def __init__(self, file_path: Union[str, Path], append: bool = True, def response(self, flow: HTTPFlow): """Checks if the response should be included in the URL based on the index_filter and adds it to the URL index - if appropriate. + if appropriate. """ if isinstance(self.index_filter, str) or self.index_filter is None: raise ValueError("Invalid filter expression.") diff --git a/examples/contrib/webscanner_helper/urlinjection.py b/examples/contrib/webscanner_helper/urlinjection.py index 6c4f982915..8cd96313db 100644 --- a/examples/contrib/webscanner_helper/urlinjection.py +++ b/examples/contrib/webscanner_helper/urlinjection.py @@ -11,6 +11,7 @@ class InjectionGenerator: """Abstract class for an generator of the injection content in order to inject the URL index.""" + ENCODING = "UTF8" @abc.abstractmethod @@ -32,11 +33,11 @@ def __init__(self, insert: bool = False): @classmethod def _form_html(cls, url): - return f"
" + return f'
' @classmethod def _link_html(cls, url): - return f"link to {url}" + return f'link to {url}' @classmethod def index_html(cls, index): @@ -54,9 +55,9 @@ def index_html(cls, index): @classmethod def landing_page(cls, index): return ( - "" - + cls.index_html(index) - + "" + '' + + cls.index_html(index) + + "" ) def inject(self, index, flow: HTTPFlow): @@ -64,19 +65,21 @@ def inject(self, index, flow: HTTPFlow): if flow.response.status_code != 404 and not self.insert: logger.warning( f"URL '{flow.request.url}' didn't return 404 status, " - f"index page would overwrite valid page.") + f"index page would overwrite valid page." + ) elif self.insert: - content = (flow.response - .content - .decode(self.ENCODING, "backslashreplace")) + content = flow.response.content.decode( + self.ENCODING, "backslashreplace" + ) if "" in content: - content = content.replace("", self.index_html(index) + "") + content = content.replace( + "", self.index_html(index) + "" + ) else: content += self.index_html(index) flow.response.content = content.encode(self.ENCODING) else: - flow.response.content = (self.landing_page(index) - .encode(self.ENCODING)) + flow.response.content = self.landing_page(index).encode(self.ENCODING) class RobotsInjection(InjectionGenerator): @@ -98,11 +101,12 @@ def inject(self, index, flow: HTTPFlow): if flow.response.status_code != 404: logger.warning( f"URL '{flow.request.url}' didn't return 404 status, " - f"index page would overwrite valid page.") + f"index page would overwrite valid page." + ) else: - flow.response.content = self.robots_txt(index, - self.directive).encode( - self.ENCODING) + flow.response.content = self.robots_txt(index, self.directive).encode( + self.ENCODING + ) class SitemapInjection(InjectionGenerator): @@ -111,7 +115,8 @@ class SitemapInjection(InjectionGenerator): @classmethod def sitemap(cls, index): lines = [ - ""] + '' + ] for scheme_netloc, paths in index.items(): for path, methods in paths.items(): url = scheme_netloc + path @@ -124,13 +129,14 @@ def inject(self, index, flow: HTTPFlow): if flow.response.status_code != 404: logger.warning( f"URL '{flow.request.url}' didn't return 404 status, " - f"index page would overwrite valid page.") + f"index page would overwrite valid page." + ) else: flow.response.content = self.sitemap(index).encode(self.ENCODING) class UrlInjectionAddon: - """ The UrlInjection add-on can be used in combination with web application scanners to improve their crawling + """The UrlInjection add-on can be used in combination with web application scanners to improve their crawling performance. The given URls will be injected into the web application. With this, web application scanners can find pages to @@ -143,8 +149,9 @@ class UrlInjectionAddon: The URL index needed for the injection can be generated by the UrlIndex Add-on. """ - def __init__(self, flt: str, url_index_file: str, - injection_gen: InjectionGenerator): + def __init__( + self, flt: str, url_index_file: str, injection_gen: InjectionGenerator + ): """Initializes the UrlIndex add-on. Args: @@ -168,5 +175,7 @@ def response(self, flow: HTTPFlow): self.injection_gen.inject(self.url_store, flow) flow.response.status_code = 200 flow.response.headers["content-type"] = "text/html" - logger.debug(f"Set status code to 200 and set content to logged " - f"urls. Method: {self.injection_gen}") + logger.debug( + f"Set status code to 200 and set content to logged " + f"urls. Method: {self.injection_gen}" + ) diff --git a/examples/contrib/webscanner_helper/watchdog.py b/examples/contrib/webscanner_helper/watchdog.py index 48f58d9c08..ee61b5c6ec 100644 --- a/examples/contrib/webscanner_helper/watchdog.py +++ b/examples/contrib/webscanner_helper/watchdog.py @@ -1,19 +1,19 @@ +import logging import pathlib import time -import logging from datetime import datetime -from typing import Union import mitmproxy.connections import mitmproxy.http -from mitmproxy.addons.export import curl_command, raw +from mitmproxy.addons.export import curl_command +from mitmproxy.addons.export import raw from mitmproxy.exceptions import HttpSyntaxException logger = logging.getLogger(__name__) -class WatchdogAddon(): - """ The Watchdog Add-on can be used in combination with web application scanners in oder to check if the device +class WatchdogAddon: + """The Watchdog Add-on can be used in combination with web application scanners in oder to check if the device under test responds correctls to the scanner's responses. The Watchdog Add-on checks if the device under test responds correctly to the scanner's responses. @@ -35,8 +35,8 @@ def __init__(self, event, outdir: pathlib.Path, timeout=None): raise RuntimeError("Watchtdog output path must be a directory.") elif not self.flow_dir.exists(): self.flow_dir.mkdir(parents=True) - self.last_trigger: Union[None, float] = None - self.timeout: Union[None, float] = timeout + self.last_trigger: None | float = None + self.timeout: None | float = timeout def serverconnect(self, conn: mitmproxy.connections.ServerConnection): if self.timeout is not None: @@ -45,10 +45,14 @@ def serverconnect(self, conn: mitmproxy.connections.ServerConnection): @classmethod def not_in_timeout(cls, last_triggered, timeout): """Checks if current error lies not in timeout after last trigger (potential reset of connection).""" - return last_triggered is None or timeout is None or (time.time() - last_triggered > timeout) + return ( + last_triggered is None + or timeout is None + or (time.time() - last_triggered > timeout) + ) def error(self, flow): - """ Checks if the watchdog will be triggered. + """Checks if the watchdog will be triggered. Only triggers watchdog for timeouts after last reset and if flow.error is set (shows that error is a server error). Ignores HttpSyntaxException Errors since this can be triggered on purpose by web application scanner. @@ -56,16 +60,22 @@ def error(self, flow): Args: flow: mitmproxy.http.flow """ - if (self.not_in_timeout(self.last_trigger, self.timeout) - and flow.error is not None and not isinstance(flow.error, HttpSyntaxException)): - + if ( + self.not_in_timeout(self.last_trigger, self.timeout) + and flow.error is not None + and not isinstance(flow.error, HttpSyntaxException) + ): self.last_trigger = time.time() logger.error(f"Watchdog triggered! Cause: {flow}") self.error_event.set() # save the request which might have caused the problem if flow.request: - with (self.flow_dir / f"{datetime.utcnow().isoformat()}.curl").open("w") as f: + with (self.flow_dir / f"{datetime.utcnow().isoformat()}.curl").open( + "w" + ) as f: f.write(curl_command(flow)) - with (self.flow_dir / f"{datetime.utcnow().isoformat()}.raw").open("wb") as f: + with (self.flow_dir / f"{datetime.utcnow().isoformat()}.raw").open( + "wb" + ) as f: f.write(raw(flow)) diff --git a/examples/contrib/xss_scanner.py b/examples/contrib/xss_scanner.py index 287982fb34..7d25d06e31 100644 --- a/examples/contrib/xss_scanner.py +++ b/examples/contrib/xss_scanner.py @@ -34,11 +34,13 @@ Line: 1029zxcs'd"aoso[sb]po(pc)se;sl/bsl\eq=3847asd """ + import logging import re import socket from html.parser import HTMLParser -from typing import NamedTuple, Optional, Union +from typing import NamedTuple +from typing import Optional from urllib.parse import urlparse import requests @@ -82,16 +84,16 @@ class SQLiData(NamedTuple): def get_cookies(flow: http.HTTPFlow) -> Cookies: - """ Return a dict going from cookie names to cookie values - - Note that it includes both the cookies sent in the original request and - the cookies sent by the server """ + """Return a dict going from cookie names to cookie values + - Note that it includes both the cookies sent in the original request and + the cookies sent by the server""" return {name: value for name, value in flow.request.cookies.fields} def find_unclaimed_URLs(body, requestUrl): - """ Look for unclaimed URLs in script tags and log them if found""" + """Look for unclaimed URLs in script tags and log them if found""" - def getValue(attrs: list[tuple[str, str]], attrName: str) -> Optional[str]: + def getValue(attrs: list[tuple[str, str]], attrName: str) -> str | None: for name, value in attrs: if attrName == name: return value @@ -101,9 +103,15 @@ class ScriptURLExtractor(HTMLParser): script_URLs: list[str] = [] def handle_starttag(self, tag, attrs): - if (tag == "script" or tag == "iframe") and "src" in [name for name, value in attrs]: + if (tag == "script" or tag == "iframe") and "src" in [ + name for name, value in attrs + ]: self.script_URLs.append(getValue(attrs, "src")) - if tag == "link" and getValue(attrs, "rel") == "stylesheet" and "href" in [name for name, value in attrs]: + if ( + tag == "link" + and getValue(attrs, "rel") == "stylesheet" + and "href" in [name for name, value in attrs] + ): self.script_URLs.append(getValue(attrs, "href")) parser = ScriptURLExtractor() @@ -114,17 +122,21 @@ def handle_starttag(self, tag, attrs): try: socket.gethostbyname(domain) except socket.gaierror: - logging.error(f"XSS found in {requestUrl} due to unclaimed URL \"{url}\".") + logging.error(f'XSS found in {requestUrl} due to unclaimed URL "{url}".') -def test_end_of_URL_injection(original_body: str, request_URL: str, cookies: Cookies) -> VulnData: - """ Test the given URL for XSS via injection onto the end of the URL and - log the XSS if found """ +def test_end_of_URL_injection( + original_body: str, request_URL: str, cookies: Cookies +) -> VulnData: + """Test the given URL for XSS via injection onto the end of the URL and + log the XSS if found""" parsed_URL = urlparse(request_URL) path = parsed_URL.path if path != "" and path[-1] != "/": # ensure the path ends in a / path += "/" - path += FULL_PAYLOAD.decode('utf-8') # the path must be a string while the payload is bytes + path += FULL_PAYLOAD.decode( + "utf-8" + ) # the path must be a string while the payload is bytes url = parsed_URL._replace(path=path).geturl() body = requests.get(url, cookies=cookies).text.lower() xss_info = get_XSS_data(body, url, "End of URL") @@ -132,31 +144,42 @@ def test_end_of_URL_injection(original_body: str, request_URL: str, cookies: Coo return xss_info, sqli_info -def test_referer_injection(original_body: str, request_URL: str, cookies: Cookies) -> VulnData: - """ Test the given URL for XSS via injection into the referer and - log the XSS if found """ - body = requests.get(request_URL, headers={'referer': FULL_PAYLOAD}, cookies=cookies).text.lower() +def test_referer_injection( + original_body: str, request_URL: str, cookies: Cookies +) -> VulnData: + """Test the given URL for XSS via injection into the referer and + log the XSS if found""" + body = requests.get( + request_URL, headers={"referer": FULL_PAYLOAD}, cookies=cookies + ).text.lower() xss_info = get_XSS_data(body, request_URL, "Referer") sqli_info = get_SQLi_data(body, original_body, request_URL, "Referer") return xss_info, sqli_info -def test_user_agent_injection(original_body: str, request_URL: str, cookies: Cookies) -> VulnData: - """ Test the given URL for XSS via injection into the user agent and - log the XSS if found """ - body = requests.get(request_URL, headers={'User-Agent': FULL_PAYLOAD}, cookies=cookies).text.lower() +def test_user_agent_injection( + original_body: str, request_URL: str, cookies: Cookies +) -> VulnData: + """Test the given URL for XSS via injection into the user agent and + log the XSS if found""" + body = requests.get( + request_URL, headers={"User-Agent": FULL_PAYLOAD}, cookies=cookies + ).text.lower() xss_info = get_XSS_data(body, request_URL, "User Agent") sqli_info = get_SQLi_data(body, original_body, request_URL, "User Agent") return xss_info, sqli_info def test_query_injection(original_body: str, request_URL: str, cookies: Cookies): - """ Test the given URL for XSS via injection into URL queries and - log the XSS if found """ + """Test the given URL for XSS via injection into URL queries and + log the XSS if found""" parsed_URL = urlparse(request_URL) query_string = parsed_URL.query # queries is a list of parameters where each parameter is set to the payload - queries = [query.split("=")[0] + "=" + FULL_PAYLOAD.decode('utf-8') for query in query_string.split("&")] + queries = [ + query.split("=")[0] + "=" + FULL_PAYLOAD.decode("utf-8") + for query in query_string.split("&") + ] new_query_string = "&".join(queries) new_URL = parsed_URL._replace(query=new_query_string).geturl() body = requests.get(new_URL, cookies=cookies).text.lower() @@ -165,8 +188,8 @@ def test_query_injection(original_body: str, request_URL: str, cookies: Cookies) return xss_info, sqli_info -def log_XSS_data(xss_info: Optional[XSSData]) -> None: - """ Log information about the given XSS to mitmproxy """ +def log_XSS_data(xss_info: XSSData | None) -> None: + """Log information about the given XSS to mitmproxy""" # If it is None, then there is no info to log if not xss_info: return @@ -177,8 +200,8 @@ def log_XSS_data(xss_info: Optional[XSSData]) -> None: logging.error("Line: %s" % xss_info.line) -def log_SQLi_data(sqli_info: Optional[SQLiData]) -> None: - """ Log information about the given SQLi to mitmproxy """ +def log_SQLi_data(sqli_info: SQLiData | None) -> None: + """Log information about the given SQLi to mitmproxy""" if not sqli_info: return logging.error("===== SQLi Found =====") @@ -189,51 +212,88 @@ def log_SQLi_data(sqli_info: Optional[SQLiData]) -> None: return -def get_SQLi_data(new_body: str, original_body: str, request_URL: str, injection_point: str) -> Optional[SQLiData]: - """ Return a SQLiDict if there is a SQLi otherwise return None - String String URL String -> (SQLiDict or None) """ +def get_SQLi_data( + new_body: str, original_body: str, request_URL: str, injection_point: str +) -> SQLiData | None: + """Return a SQLiDict if there is a SQLi otherwise return None + String String URL String -> (SQLiDict or None)""" # Regexes taken from Damn Small SQLi Scanner: https://github.com/stamparm/DSSS/blob/master/dsss.py#L17 DBMS_ERRORS = { - "MySQL": (r"SQL syntax.*MySQL", r"Warning.*mysql_.*", r"valid MySQL result", r"MySqlClient\."), - "PostgreSQL": (r"PostgreSQL.*ERROR", r"Warning.*\Wpg_.*", r"valid PostgreSQL result", r"Npgsql\."), - "Microsoft SQL Server": (r"Driver.* SQL[\-\_\ ]*Server", r"OLE DB.* SQL Server", r"(\W|\A)SQL Server.*Driver", - r"Warning.*mssql_.*", r"(\W|\A)SQL Server.*[0-9a-fA-F]{8}", - r"(?s)Exception.*\WSystem\.Data\.SqlClient\.", r"(?s)Exception.*\WRoadhouse\.Cms\."), - "Microsoft Access": (r"Microsoft Access Driver", r"JET Database Engine", r"Access Database Engine"), - "Oracle": (r"\bORA-[0-9][0-9][0-9][0-9]", r"Oracle error", r"Oracle.*Driver", r"Warning.*\Woci_.*", r"Warning.*\Wora_.*"), + "MySQL": ( + r"SQL syntax.*MySQL", + r"Warning.*mysql_.*", + r"valid MySQL result", + r"MySqlClient\.", + ), + "PostgreSQL": ( + r"PostgreSQL.*ERROR", + r"Warning.*\Wpg_.*", + r"valid PostgreSQL result", + r"Npgsql\.", + ), + "Microsoft SQL Server": ( + r"Driver.* SQL[\-\_\ ]*Server", + r"OLE DB.* SQL Server", + r"(\W|\A)SQL Server.*Driver", + r"Warning.*mssql_.*", + r"(\W|\A)SQL Server.*[0-9a-fA-F]{8}", + r"(?s)Exception.*\WSystem\.Data\.SqlClient\.", + r"(?s)Exception.*\WRoadhouse\.Cms\.", + ), + "Microsoft Access": ( + r"Microsoft Access Driver", + r"JET Database Engine", + r"Access Database Engine", + ), + "Oracle": ( + r"\bORA-[0-9][0-9][0-9][0-9]", + r"Oracle error", + r"Oracle.*Driver", + r"Warning.*\Woci_.*", + r"Warning.*\Wora_.*", + ), "IBM DB2": (r"CLI Driver.*DB2", r"DB2 SQL error", r"\bdb2_\w+\("), - "SQLite": (r"SQLite/JDBCDriver", r"SQLite.Exception", r"System.Data.SQLite.SQLiteException", r"Warning.*sqlite_.*", - r"Warning.*SQLite3::", r"\[SQLITE_ERROR\]"), - "Sybase": (r"(?i)Warning.*sybase.*", r"Sybase message", r"Sybase.*Server message.*"), + "SQLite": ( + r"SQLite/JDBCDriver", + r"SQLite.Exception", + r"System.Data.SQLite.SQLiteException", + r"Warning.*sqlite_.*", + r"Warning.*SQLite3::", + r"\[SQLITE_ERROR\]", + ), + "Sybase": ( + r"(?i)Warning.*sybase.*", + r"Sybase message", + r"Sybase.*Server message.*", + ), } for dbms, regexes in DBMS_ERRORS.items(): for regex in regexes: # type: ignore - if re.search(regex, new_body, re.IGNORECASE) and not re.search(regex, original_body, re.IGNORECASE): - return SQLiData(request_URL, - injection_point, - regex, - dbms) + if re.search(regex, new_body, re.IGNORECASE) and not re.search( + regex, original_body, re.IGNORECASE + ): + return SQLiData(request_URL, injection_point, regex, dbms) return None # A qc is either ' or " -def inside_quote(qc: str, substring_bytes: bytes, text_index: int, body_bytes: bytes) -> bool: - """ Whether the Numberth occurrence of the first string in the second - string is inside quotes as defined by the supplied QuoteChar """ - substring = substring_bytes.decode('utf-8') - body = body_bytes.decode('utf-8') +def inside_quote( + qc: str, substring_bytes: bytes, text_index: int, body_bytes: bytes +) -> bool: + """Whether the Numberth occurrence of the first string in the second + string is inside quotes as defined by the supplied QuoteChar""" + substring = substring_bytes.decode("utf-8") + body = body_bytes.decode("utf-8") num_substrings_found = 0 in_quote = False for index, char in enumerate(body): # Whether the next chunk of len(substring) chars is the substring - next_part_is_substring = ( - (not (index + len(substring) > len(body))) and - (body[index:index + len(substring)] == substring) + next_part_is_substring = (not (index + len(substring) > len(body))) and ( + body[index : index + len(substring)] == substring ) # Whether this char is escaped with a \ - is_not_escaped = ( - (index - 1 < 0 or index - 1 > len(body)) or - (body[index - 1] != "\\") + is_not_escaped = (index - 1 < 0 or index - 1 > len(body)) or ( + body[index - 1] != "\\" ) if char == qc and is_not_escaped: in_quote = not in_quote @@ -245,25 +305,27 @@ def inside_quote(qc: str, substring_bytes: bytes, text_index: int, body_bytes: b def paths_to_text(html: str, string: str) -> list[str]: - """ Return list of Paths to a given str in the given HTML tree - - Note that it does a BFS """ + """Return list of Paths to a given str in the given HTML tree + - Note that it does a BFS""" def remove_last_occurence_of_sub_string(string: str, substr: str) -> str: - """ Delete the last occurrence of substr from str + """Delete the last occurrence of substr from str String String -> String """ index = string.rfind(substr) - return string[:index] + string[index + len(substr):] + return string[:index] + string[index + len(substr) :] class PathHTMLParser(HTMLParser): currentPath = "" paths: list[str] = [] def handle_starttag(self, tag, attrs): - self.currentPath += ("/" + tag) + self.currentPath += "/" + tag def handle_endtag(self, tag): - self.currentPath = remove_last_occurence_of_sub_string(self.currentPath, "/" + tag) + self.currentPath = remove_last_occurence_of_sub_string( + self.currentPath, "/" + tag + ) def handle_data(self, data): if string in data: @@ -274,13 +336,15 @@ def handle_data(self, data): return parser.paths -def get_XSS_data(body: Union[str, bytes], request_URL: str, injection_point: str) -> Optional[XSSData]: - """ Return a XSSDict if there is a XSS otherwise return None """ +def get_XSS_data( + body: str | bytes, request_URL: str, injection_point: str +) -> XSSData | None: + """Return a XSSDict if there is a XSS otherwise return None""" def in_script(text, index, body) -> bool: - """ Whether the Numberth occurrence of the first string in the second - string is inside a script tag """ - paths = paths_to_text(body.decode('utf-8'), text.decode("utf-8")) + """Whether the Numberth occurrence of the first string in the second + string is inside a script tag""" + paths = paths_to_text(body.decode("utf-8"), text.decode("utf-8")) try: path = paths[index] return "script" in path @@ -288,12 +352,12 @@ def in_script(text, index, body) -> bool: return False def in_HTML(text: bytes, index: int, body: bytes) -> bool: - """ Whether the Numberth occurrence of the first string in the second - string is inside the HTML but not inside a script tag or part of - a HTML attribute""" + """Whether the Numberth occurrence of the first string in the second + string is inside the HTML but not inside a script tag or part of + a HTML attribute""" # if there is a < then lxml will interpret that as a tag, so only search for the stuff before it text = text.split(b"<")[0] - paths = paths_to_text(body.decode('utf-8'), text.decode("utf-8")) + paths = paths_to_text(body.decode("utf-8"), text.decode("utf-8")) try: path = paths[index] return "script" not in path @@ -301,14 +365,14 @@ def in_HTML(text: bytes, index: int, body: bytes) -> bool: return False def inject_javascript_handler(html: str) -> bool: - """ Whether you can inject a Javascript:alert(0) as a link """ + """Whether you can inject a Javascript:alert(0) as a link""" class injectJSHandlerHTMLParser(HTMLParser): injectJSHandler = False def handle_starttag(self, tag, attrs): for name, value in attrs: - if name == "href" and value.startswith(FRONT_WALL.decode('utf-8')): + if name == "href" and value.startswith(FRONT_WALL.decode("utf-8")): self.injectJSHandler = True parser = injectJSHandlerHTMLParser() @@ -317,7 +381,7 @@ def handle_starttag(self, tag, attrs): # Only convert the body to bytes if needed if isinstance(body, str): - body = bytes(body, 'utf-8') + body = bytes(body, "utf-8") # Regex for between 24 and 72 (aka 24*3) characters encapsulated by the walls regex = re.compile(b"""%s.{24,72}?%s""" % (FRONT_WALL, BACK_WALL)) matches = regex.findall(body) @@ -336,64 +400,121 @@ def handle_starttag(self, tag, attrs): inject_slash = b"sl/bsl" in match # forward slashes inject_semi = b"se;sl" in match # semicolons inject_equals = b"eq=" in match # equals sign - if in_script_val and inject_slash and inject_open_angle and inject_close_angle: # e.g. - return XSSData(request_URL, - injection_point, - ' - return XSSData(request_URL, - injection_point, - "';alert(0);g='", - match.decode('utf-8')) - elif in_script_val and in_double_quotes and inject_double_quotes and inject_semi: # e.g. - return XSSData(request_URL, - injection_point, - '";alert(0);g="', - match.decode('utf-8')) - elif in_tag and in_single_quotes and inject_single_quotes and inject_open_angle and inject_close_angle and inject_slash: + if ( + in_script_val and inject_slash and inject_open_angle and inject_close_angle + ): # e.g. + return XSSData( + request_URL, + injection_point, + " + return XSSData( + request_URL, injection_point, "';alert(0);g='", match.decode("utf-8") + ) + elif ( + in_script_val and in_double_quotes and inject_double_quotes and inject_semi + ): # e.g. + return XSSData( + request_URL, injection_point, '";alert(0);g="', match.decode("utf-8") + ) + elif ( + in_tag + and in_single_quotes + and inject_single_quotes + and inject_open_angle + and inject_close_angle + and inject_slash + ): # e.g. Test - return XSSData(request_URL, - injection_point, - "'>", - match.decode('utf-8')) - elif in_tag and in_double_quotes and inject_double_quotes and inject_open_angle and inject_close_angle and inject_slash: + return XSSData( + request_URL, + injection_point, + "'>", + match.decode("utf-8"), + ) + elif ( + in_tag + and in_double_quotes + and inject_double_quotes + and inject_open_angle + and inject_close_angle + and inject_slash + ): # e.g. Test - return XSSData(request_URL, - injection_point, - '">', - match.decode('utf-8')) - elif in_tag and not in_double_quotes and not in_single_quotes and inject_open_angle and inject_close_angle and inject_slash: + return XSSData( + request_URL, + injection_point, + '">', + match.decode("utf-8"), + ) + elif ( + in_tag + and not in_double_quotes + and not in_single_quotes + and inject_open_angle + and inject_close_angle + and inject_slash + ): # e.g. Test - return XSSData(request_URL, - injection_point, - '>', - match.decode('utf-8')) - elif inject_javascript_handler(body.decode('utf-8')): # e.g. Test - return XSSData(request_URL, - injection_point, - 'Javascript:alert(0)', - match.decode('utf-8')) - elif in_tag and in_double_quotes and inject_double_quotes and inject_equals: # e.g. Test - return XSSData(request_URL, - injection_point, - '" onmouseover="alert(0)" t="', - match.decode('utf-8')) - elif in_tag and in_single_quotes and inject_single_quotes and inject_equals: # e.g. Test - return XSSData(request_URL, - injection_point, - "' onmouseover='alert(0)' t='", - match.decode('utf-8')) - elif in_tag and not in_single_quotes and not in_double_quotes and inject_equals: # e.g. Test - return XSSData(request_URL, - injection_point, - " onmouseover=alert(0) t=", - match.decode('utf-8')) - elif in_HTML_val and not in_script_val and inject_open_angle and inject_close_angle and inject_slash: # e.g. PAYLOAD - return XSSData(request_URL, - injection_point, - '', - match.decode('utf-8')) + return XSSData( + request_URL, + injection_point, + ">", + match.decode("utf-8"), + ) + elif inject_javascript_handler( + body.decode("utf-8") + ): # e.g. Test + return XSSData( + request_URL, + injection_point, + "Javascript:alert(0)", + match.decode("utf-8"), + ) + elif ( + in_tag and in_double_quotes and inject_double_quotes and inject_equals + ): # e.g. Test + return XSSData( + request_URL, + injection_point, + '" onmouseover="alert(0)" t="', + match.decode("utf-8"), + ) + elif ( + in_tag and in_single_quotes and inject_single_quotes and inject_equals + ): # e.g. Test + return XSSData( + request_URL, + injection_point, + "' onmouseover='alert(0)' t='", + match.decode("utf-8"), + ) + elif ( + in_tag and not in_single_quotes and not in_double_quotes and inject_equals + ): # e.g. Test + return XSSData( + request_URL, + injection_point, + " onmouseover=alert(0) t=", + match.decode("utf-8"), + ) + elif ( + in_HTML_val + and not in_script_val + and inject_open_angle + and inject_close_angle + and inject_slash + ): # e.g. PAYLOAD + return XSSData( + request_URL, + injection_point, + "", + match.decode("utf-8"), + ) else: return None return None diff --git a/mitmproxy/addonmanager.py b/mitmproxy/addonmanager.py index 265c811139..5f918b75b7 100644 --- a/mitmproxy/addonmanager.py +++ b/mitmproxy/addonmanager.py @@ -2,13 +2,13 @@ import inspect import logging import pprint +import sys import traceback import types -from collections.abc import Callable, Sequence +from collections.abc import Callable +from collections.abc import Sequence from dataclasses import dataclass -from typing import Any, Optional - -import sys +from typing import Any from mitmproxy import exceptions from mitmproxy import flow @@ -51,8 +51,11 @@ def safecall(): etype, value, tb = sys.exc_info() tb = cut_traceback(tb, "invoke_addon_sync") tb = cut_traceback(tb, "invoke_addon") + assert etype + assert value logger.error( - "Addon error: %s" % "".join(traceback.format_exception(etype, value, tb)) + f"Addon error: {value}", + exc_info=(etype, value, tb), ) @@ -70,7 +73,7 @@ def add_option( typespec: type, default: Any, help: str, - choices: Optional[Sequence[str]] = None, + choices: Sequence[str] | None = None, ) -> None: """ Add an option to mitmproxy. @@ -79,6 +82,7 @@ def add_option( reflowed by tools. Information on the data type should be omitted - it will be generated and added by tools as needed. """ + assert not isinstance(choices, str) if name in self.master.options: existing = self.master.options._options[name] same_signature = ( @@ -181,8 +185,8 @@ def register(self, addon): raise exceptions.AddonManagerError( "An addon called '%s' already exists." % name ) - l = Loader(self.master) - self.invoke_addon_sync(addon, LoadHook(l)) + loader = Loader(self.master) + self.invoke_addon_sync(addon, LoadHook(loader)) for a in traverse([addon]): name = _get_name(a) self.lookup[name] = a diff --git a/mitmproxy/addons/__init__.py b/mitmproxy/addons/__init__.py index 36ea9bfe55..c09c922984 100644 --- a/mitmproxy/addons/__init__.py +++ b/mitmproxy/addons/__init__.py @@ -11,20 +11,23 @@ from mitmproxy.addons import disable_h2c from mitmproxy.addons import dns_resolver from mitmproxy.addons import export +from mitmproxy.addons import maplocal +from mitmproxy.addons import mapremote +from mitmproxy.addons import modifybody +from mitmproxy.addons import modifyheaders from mitmproxy.addons import next_layer from mitmproxy.addons import onboarding -from mitmproxy.addons import proxyserver from mitmproxy.addons import proxyauth +from mitmproxy.addons import proxyserver +from mitmproxy.addons import save +from mitmproxy.addons import savehar from mitmproxy.addons import script from mitmproxy.addons import serverplayback -from mitmproxy.addons import mapremote -from mitmproxy.addons import maplocal -from mitmproxy.addons import modifybody -from mitmproxy.addons import modifyheaders from mitmproxy.addons import stickyauth from mitmproxy.addons import stickycookie -from mitmproxy.addons import save +from mitmproxy.addons import strip_dns_https_records from mitmproxy.addons import tlsconfig +from mitmproxy.addons import update_alt_svc from mitmproxy.addons import upstream_auth @@ -33,6 +36,7 @@ def default_addons(): core.Core(), browser.Browser(), block.Block(), + strip_dns_https_records.StripDnsHttpsRecords(), blocklist.BlockList(), anticache.AntiCache(), anticomp.AntiComp(), @@ -56,6 +60,8 @@ def default_addons(): stickyauth.StickyAuth(), stickycookie.StickyCookie(), save.Save(), + savehar.SaveHar(), tlsconfig.TlsConfig(), upstream_auth.UpstreamAuth(), + update_alt_svc.UpdateAltSvc(), ] diff --git a/mitmproxy/addons/asgiapp.py b/mitmproxy/addons/asgiapp.py index 9c9b1ca299..785d7982b7 100644 --- a/mitmproxy/addons/asgiapp.py +++ b/mitmproxy/addons/asgiapp.py @@ -1,12 +1,12 @@ import asyncio import logging -import traceback import urllib.parse import asgiref.compatibility import asgiref.wsgi -from mitmproxy import ctx, http +from mitmproxy import ctx +from mitmproxy import http logger = logging.getLogger(__name__) @@ -20,7 +20,7 @@ class ASGIApp: - It currently only implements the HTTP protocol (Lifespan and WebSocket are unimplemented). """ - def __init__(self, asgi_app, host: str, port: int): + def __init__(self, asgi_app, host: str, port: int | None): asgi_app = asgiref.compatibility.guarantee_single_callable(asgi_app) self.asgi_app, self.host, self.port = asgi_app, host, port @@ -30,7 +30,8 @@ def name(self) -> str: def should_serve(self, flow: http.HTTPFlow) -> bool: return bool( - (flow.request.pretty_host, flow.request.port) == (self.host, self.port) + flow.request.pretty_host == self.host + and (self.port is None or flow.request.port == self.port) and flow.live and not flow.error and not flow.response @@ -42,7 +43,7 @@ async def request(self, flow: http.HTTPFlow) -> None: class WSGIApp(ASGIApp): - def __init__(self, wsgi_app, host: str, port: int): + def __init__(self, wsgi_app, host: str, port: int | None): asgi_app = asgiref.wsgi.WsgiToAsgi(wsgi_app) super().__init__(asgi_app, host, port) @@ -124,6 +125,7 @@ async def send(event): ) flow.response.decode() elif event["type"] == "http.response.body": + assert flow.response flow.response.content += event.get("body", b"") if not event.get("more_body", False): nonlocal sent_response @@ -135,8 +137,8 @@ async def send(event): await app(scope, receive, send) if not sent_response: raise RuntimeError(f"no response sent.") - except Exception: - logger.error(f"Error in asgi app:\n{traceback.format_exc(limit=-5)}") + except Exception as e: + logger.exception(f"Error in asgi app: {e}") flow.response = http.Response.make(500, b"ASGI Error.") finally: done.set() diff --git a/mitmproxy/addons/block.py b/mitmproxy/addons/block.py index 8f6e37c195..cdd68bf97e 100644 --- a/mitmproxy/addons/block.py +++ b/mitmproxy/addons/block.py @@ -2,6 +2,7 @@ import logging from mitmproxy import ctx +from mitmproxy.proxy import mode_specs class Block: @@ -31,7 +32,7 @@ def client_connected(self, client): if isinstance(address, ipaddress.IPv6Address): address = address.ipv4_mapped or address - if address.is_loopback: + if address.is_loopback or isinstance(client.proxy_mode, mode_specs.LocalMode): return if ctx.options.block_private and address.is_private: diff --git a/mitmproxy/addons/blocklist.py b/mitmproxy/addons/blocklist.py index d0f00e3341..6c99bca067 100644 --- a/mitmproxy/addons/blocklist.py +++ b/mitmproxy/addons/blocklist.py @@ -1,9 +1,11 @@ from collections.abc import Sequence from typing import NamedTuple -import logging - -from mitmproxy import ctx, exceptions, flowfilter, http, version +from mitmproxy import ctx +from mitmproxy import exceptions +from mitmproxy import flowfilter +from mitmproxy import http +from mitmproxy import version from mitmproxy.net.http.status_codes import NO_RESPONSE from mitmproxy.net.http.status_codes import RESPONSES @@ -38,7 +40,7 @@ def parse_spec(option: str) -> BlockSpec: class BlockList: - def __init__(self): + def __init__(self) -> None: self.items: list[BlockSpec] = [] def load(self, loader): @@ -75,12 +77,7 @@ def request(self, flow: http.HTTPFlow) -> None: if spec.matches(flow): flow.metadata["blocklisted"] = True if spec.status_code == NO_RESPONSE: - if flow.killable: - flow.kill() - else: - logging.error( - "Cannot kill flow, not killable: %s", flow.request.pretty_url - ) + flow.kill() else: flow.response = http.Response.make( spec.status_code, headers={"Server": version.MITMPROXY} diff --git a/mitmproxy/addons/browser.py b/mitmproxy/addons/browser.py index 7354f7c1e9..ccc9209536 100644 --- a/mitmproxy/addons/browser.py +++ b/mitmproxy/addons/browser.py @@ -2,41 +2,23 @@ import shutil import subprocess import tempfile -from typing import Optional from mitmproxy import command from mitmproxy import ctx from mitmproxy.log import ALERT -def get_chrome_executable() -> Optional[str]: - for browser in ( - "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", - # https://stackoverflow.com/questions/40674914/google-chrome-path-in-windows-10 - r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe", - r"C:\Program Files (x86)\Google\Application\chrome.exe", - # Linux binary names from Python's webbrowser module. - "google-chrome", - "google-chrome-stable", - "chrome", - "chromium", - "chromium-browser", - "google-chrome-unstable", - ): +def find_executable_cmd(*search_paths) -> list[str] | None: + for browser in search_paths: if shutil.which(browser): - return browser + return [browser] return None -def get_chrome_flatpak() -> Optional[str]: +def find_flatpak_cmd(*search_paths) -> list[str] | None: if shutil.which("flatpak"): - for browser in ( - "com.google.Chrome", - "org.chromium.Chromium", - "com.github.Eloston.UngoogledChromium", - "com.google.ChromeDev", - ): + for browser in search_paths: if ( subprocess.run( ["flatpak", "info", browser], @@ -45,16 +27,7 @@ def get_chrome_flatpak() -> Optional[str]: ).returncode == 0 ): - return browser - - return None - - -def get_browser_cmd() -> Optional[list[str]]: - if browser := get_chrome_executable(): - return [browser] - elif browser := get_chrome_flatpak(): - return ["flatpak", "run", "-p", browser] + return ["flatpak", "run", "-p", browser] return None @@ -64,17 +37,45 @@ class Browser: tdir: list[tempfile.TemporaryDirectory] = [] @command.command("browser.start") - def start(self) -> None: + def start(self, browser: str = "chrome") -> None: + if len(self.browser) > 0: + logging.log(ALERT, "Starting additional browser") + + if browser in ("chrome", "chromium"): + self.launch_chrome() + elif browser == "firefox": + self.launch_firefox() + else: + logging.log(ALERT, "Invalid browser name.") + + def launch_chrome(self) -> None: """ Start an isolated instance of Chrome that points to the currently running proxy. """ - if len(self.browser) > 0: - logging.log(ALERT, "Starting additional browser") + cmd = find_executable_cmd( + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + # https://stackoverflow.com/questions/40674914/google-chrome-path-in-windows-10 + r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe", + r"C:\Program Files (x86)\Google\Application\chrome.exe", + # Linux binary names from Python's webbrowser module. + "google-chrome", + "google-chrome-stable", + "chrome", + "chromium", + "chromium-browser", + "google-chrome-unstable", + ) or find_flatpak_cmd( + "com.google.Chrome", + "org.chromium.Chromium", + "com.github.Eloston.UngoogledChromium", + "com.google.ChromeDev", + ) - cmd = get_browser_cmd() if not cmd: - logging.log(ALERT, "Your platform is not supported yet - please submit a patch.") + logging.log( + ALERT, "Your platform is not supported yet - please submit a patch." + ) return tdir = tempfile.TemporaryDirectory() @@ -85,7 +86,8 @@ def start(self) -> None: *cmd, "--user-data-dir=%s" % str(tdir.name), "--proxy-server={}:{}".format( - ctx.options.listen_host or "127.0.0.1", ctx.options.listen_port or "8080" + ctx.options.listen_host or "127.0.0.1", + ctx.options.listen_port or "8080", ), "--disable-fre", "--no-default-browser-check", @@ -98,6 +100,83 @@ def start(self) -> None: ) ) + def launch_firefox(self) -> None: + """ + Start an isolated instance of Firefox that points to the currently + running proxy. + """ + cmd = find_executable_cmd( + "/Applications/Firefox.app/Contents/MacOS/firefox", + r"C:\Program Files\Mozilla Firefox\firefox.exe", + "firefox", + "mozilla-firefox", + "mozilla", + ) or find_flatpak_cmd("org.mozilla.firefox") + + if not cmd: + logging.log( + ALERT, "Your platform is not supported yet - please submit a patch." + ) + return + + host = ctx.options.listen_host or "127.0.0.1" + port = ctx.options.listen_port or 8080 + prefs = [ + 'user_pref("datareporting.policy.firstRunURL", "");', + 'user_pref("network.proxy.type", 1);', + 'user_pref("network.proxy.share_proxy_settings", true);', + 'user_pref("datareporting.healthreport.uploadEnabled", false);', + 'user_pref("app.normandy.enabled", false);', + 'user_pref("app.update.auto", false);', + 'user_pref("app.update.enabled", false);', + 'user_pref("app.update.autoInstallEnabled", false);', + 'user_pref("app.shield.optoutstudies.enabled", false);' + 'user_pref("extensions.blocklist.enabled", false);', + 'user_pref("browser.safebrowsing.downloads.remote.enabled", false);', + 'user_pref("browser.region.network.url", "");', + 'user_pref("browser.region.update.enabled", false);', + 'user_pref("browser.region.local-geocoding", false);', + 'user_pref("extensions.pocket.enabled", false);', + 'user_pref("network.captive-portal-service.enabled", false);', + 'user_pref("network.connectivity-service.enabled", false);', + 'user_pref("toolkit.telemetry.server", "");', + 'user_pref("dom.push.serverURL", "");', + 'user_pref("services.settings.enabled", false);', + 'user_pref("browser.newtab.preload", false);', + 'user_pref("browser.safebrowsing.provider.google4.updateURL", "");', + 'user_pref("browser.safebrowsing.provider.mozilla.updateURL", "");', + 'user_pref("browser.newtabpage.activity-stream.feeds.topsites", false);', + 'user_pref("browser.newtabpage.activity-stream.default.sites", "");', + 'user_pref("browser.newtabpage.activity-stream.showSponsoredTopSites", false);', + 'user_pref("browser.bookmarks.restore_default_bookmarks", false);', + 'user_pref("browser.bookmarks.file", "");', + ] + for service in ("http", "ssl"): + prefs += [ + f'user_pref("network.proxy.{service}", "{host}");', + f'user_pref("network.proxy.{service}_port", {port});', + ] + + tdir = tempfile.TemporaryDirectory() + + with open(tdir.name + "/prefs.js", "w") as file: + file.writelines(prefs) + + self.tdir.append(tdir) + self.browser.append( + subprocess.Popen( + [ + *cmd, + "--profile", + str(tdir.name), + "--new-window", + "about:blank", + ], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + ) + def done(self): for browser in self.browser: browser.kill() diff --git a/mitmproxy/addons/browserup/browser_data_addon.py b/mitmproxy/addons/browserup/browser_data_addon.py index b734676423..74ea9e061f 100644 --- a/mitmproxy/addons/browserup/browser_data_addon.py +++ b/mitmproxy/addons/browserup/browser_data_addon.py @@ -1,12 +1,13 @@ -import mitmproxy.http +import base64 +import hashlib +import json import logging -import re import os import pathlib +import re import time -import hashlib -import base64 -import json + +import mitmproxy.http # Inject a script into browser-responses for html that lets us get DOM timings, first paint time, and other metrics. diff --git a/mitmproxy/addons/browserup/browserup_addons_manager.py b/mitmproxy/addons/browserup/browserup_addons_manager.py index e7d761c12e..7e38d3ace8 100644 --- a/mitmproxy/addons/browserup/browserup_addons_manager.py +++ b/mitmproxy/addons/browserup/browserup_addons_manager.py @@ -2,19 +2,24 @@ import asyncio import json import logging -import falcon import os - +from pathlib import Path from typing import Optional from wsgiref.simple_server import make_server + +import falcon from apispec import APISpec from apispec.ext.marshmallow import MarshmallowPlugin from falcon_apispec import FalconPlugin -from mitmproxy.addons.browserup.har.har_schemas import MatchCriteriaSchema, VerifyResultSchema, ErrorSchema, CounterSchema, PageTimingSchema -from mitmproxy.addons.browserup.har_capture_addon import HarCaptureAddOn -from mitmproxy.addons.browserup.browser_data_addon import BrowserDataAddOn + from mitmproxy import ctx -from pathlib import Path +from mitmproxy.addons.browserup.browser_data_addon import BrowserDataAddOn +from mitmproxy.addons.browserup.har.har_schemas import (CounterSchema, + ErrorSchema, + MatchCriteriaSchema, + PageTimingSchema, + VerifyResultSchema) +from mitmproxy.addons.browserup.har_capture_addon import HarCaptureAddOn # https://marshmallow.readthedocs.io/en/stable/quickstart.html diff --git a/mitmproxy/addons/browserup/har/flow_capture.py b/mitmproxy/addons/browserup/har/flow_capture.py index fea57634f2..bdb76dde28 100644 --- a/mitmproxy/addons/browserup/har/flow_capture.py +++ b/mitmproxy/addons/browserup/har/flow_capture.py @@ -1,13 +1,12 @@ import base64 -import typing import logging +import typing +from datetime import datetime, timezone from mitmproxy import connection -from mitmproxy.utils import strutils from mitmproxy.addons.browserup.har.har_builder import HarBuilder from mitmproxy.addons.browserup.har.har_capture_types import HarCaptureTypes -from datetime import datetime -from datetime import timezone +from mitmproxy.utils import strutils # all the specifics to do with converting a flow into a HAR # A list of server seen till now is maintained so we can avoid diff --git a/mitmproxy/addons/browserup/har/har_builder.py b/mitmproxy/addons/browserup/har/har_builder.py index 02186db7a9..de0613302c 100644 --- a/mitmproxy/addons/browserup/har/har_builder.py +++ b/mitmproxy/addons/browserup/har/har_builder.py @@ -1,5 +1,4 @@ -from datetime import datetime -from datetime import timezone +from datetime import datetime, timezone DEFAULT_PAGE_TITLE = "Default" DEFAULT_PAGE_REF = "page_1" diff --git a/mitmproxy/addons/browserup/har/har_manager.py b/mitmproxy/addons/browserup/har/har_manager.py index 420a18cd95..015fedcfe5 100644 --- a/mitmproxy/addons/browserup/har/har_manager.py +++ b/mitmproxy/addons/browserup/har/har_manager.py @@ -1,14 +1,14 @@ -from datetime import datetime -from datetime import timezone -from mitmproxy.net.http import cookies -from mitmproxy.addons.browserup.har.har_builder import HarBuilder -from mitmproxy.addons.browserup.har.har_capture_types import HarCaptureTypes -import json import copy -import tempfile +import json import logging +import tempfile +from datetime import datetime, timezone from threading import Lock +from mitmproxy.addons.browserup.har.har_builder import HarBuilder +from mitmproxy.addons.browserup.har.har_capture_types import HarCaptureTypes +from mitmproxy.net.http import cookies + mutex = Lock() SUBMITTED_FLAG = "_submitted" diff --git a/mitmproxy/addons/browserup/har/har_resources.py b/mitmproxy/addons/browserup/har/har_resources.py index c83be2dd02..acef2488f5 100644 --- a/mitmproxy/addons/browserup/har/har_resources.py +++ b/mitmproxy/addons/browserup/har/har_resources.py @@ -1,16 +1,18 @@ -from pathlib import Path -import os import glob import json -import falcon import logging +import os +from pathlib import Path -from mitmproxy.addons.browserup.har.har_verifications import HarVerifications -from mitmproxy.addons.browserup.har.har_capture_types import HarCaptureTypes - -from mitmproxy.addons.browserup.har.har_schemas import ErrorSchema, CounterSchema, MatchCriteriaSchema +import falcon from marshmallow import ValidationError +from mitmproxy.addons.browserup.har.har_capture_types import HarCaptureTypes +from mitmproxy.addons.browserup.har.har_schemas import (CounterSchema, + ErrorSchema, + MatchCriteriaSchema) +from mitmproxy.addons.browserup.har.har_verifications import HarVerifications + class HealthCheckResource: def addon_path(self): @@ -30,7 +32,7 @@ def on_get(self, req, resp): 200: description: OK means all is well. """ - resp.body = 'OK' + resp.text = 'OK' resp.content_type = falcon.MEDIA_TEXT resp.status = falcon.HTTP_200 diff --git a/mitmproxy/addons/browserup/har/har_verifications.py b/mitmproxy/addons/browserup/har/har_verifications.py index effdaccf27..b881a22236 100644 --- a/mitmproxy/addons/browserup/har/har_verifications.py +++ b/mitmproxy/addons/browserup/har/har_verifications.py @@ -1,9 +1,9 @@ +import json import re + from glom import glom -import json -from jsonschema import validate -from jsonschema import ValidationError from jsonpath_ng import parse +from jsonschema import ValidationError, validate class HarVerifications: diff --git a/mitmproxy/addons/browserup/har_capture_addon.py b/mitmproxy/addons/browserup/har_capture_addon.py index 275eeeb163..32bf03c097 100644 --- a/mitmproxy/addons/browserup/har_capture_addon.py +++ b/mitmproxy/addons/browserup/har_capture_addon.py @@ -1,12 +1,14 @@ -import mitmproxy.http import logging -from mitmproxy.addons.browserup.har.har_resources import HarResource, HarPageResource, HarCaptureTypesResource, \ - PresentResource, NotPresentResource, SizeResource, \ - SLAResource, ErrorResource, CounterResource, HealthCheckResource -from mitmproxy.addons.browserup.har.har_manager import HarManagerMixin -from mitmproxy.addons.browserup.har.flow_capture import FlowCaptureMixin +import mitmproxy.http from mitmproxy.addons.browserup.har import flow_har_entry_patch +from mitmproxy.addons.browserup.har.flow_capture import FlowCaptureMixin +from mitmproxy.addons.browserup.har.har_manager import HarManagerMixin +from mitmproxy.addons.browserup.har.har_resources import ( + CounterResource, ErrorResource, HarCaptureTypesResource, HarPageResource, + HarResource, HealthCheckResource, NotPresentResource, PresentResource, + SizeResource, SLAResource) + flow_har_entry_patch.patch_flow() # patch flow object with a har entry method diff --git a/mitmproxy/addons/browserup/schemas/__init__.py b/mitmproxy/addons/browserup/schemas/__init__.py index 6e5097f877..47d1474dff 100644 --- a/mitmproxy/addons/browserup/schemas/__init__.py +++ b/mitmproxy/addons/browserup/schemas/__init__.py @@ -1,29 +1,10 @@ -from mitmproxy.addons import anticache -from mitmproxy.addons import anticomp -from mitmproxy.addons import block -from mitmproxy.addons import blocklist -from mitmproxy.addons import browser -from mitmproxy.addons import clientplayback -from mitmproxy.addons import command_history -from mitmproxy.addons import core -from mitmproxy.addons import cut -from mitmproxy.addons import disable_h2c -from mitmproxy.addons import export -from mitmproxy.addons import next_layer -from mitmproxy.addons import onboarding -from mitmproxy.addons import proxyserver -from mitmproxy.addons import proxyauth -from mitmproxy.addons import script -from mitmproxy.addons import serverplayback -from mitmproxy.addons import mapremote -from mitmproxy.addons import maplocal -from mitmproxy.addons import modifybody -from mitmproxy.addons import modifyheaders -from mitmproxy.addons import stickyauth -from mitmproxy.addons import stickycookie -from mitmproxy.addons import save -from mitmproxy.addons import tlsconfig -from mitmproxy.addons import upstream_auth +from mitmproxy.addons import (anticache, anticomp, block, blocklist, browser, + clientplayback, command_history, core, cut, + disable_h2c, export, maplocal, mapremote, + modifybody, modifyheaders, next_layer, + onboarding, proxyauth, proxyserver, save, script, + serverplayback, stickyauth, stickycookie, + tlsconfig, upstream_auth) def default_addons(): diff --git a/mitmproxy/addons/clientplayback.py b/mitmproxy/addons/clientplayback.py index ac3ee8de1d..b956bc600c 100644 --- a/mitmproxy/addons/clientplayback.py +++ b/mitmproxy/addons/clientplayback.py @@ -1,10 +1,12 @@ +from __future__ import annotations + import asyncio import logging -import traceback -from collections.abc import Sequence -from typing import Optional, cast - import time +from collections.abc import Sequence +from types import TracebackType +from typing import cast +from typing import Literal import mitmproxy.types from mitmproxy import command @@ -13,11 +15,15 @@ from mitmproxy import flow from mitmproxy import http from mitmproxy import io -from mitmproxy.connection import ConnectionState, Server +from mitmproxy.connection import ConnectionState +from mitmproxy.connection import Server from mitmproxy.hooks import UpdateHook from mitmproxy.log import ALERT from mitmproxy.options import Options -from mitmproxy.proxy import commands, events, layers, server +from mitmproxy.proxy import commands +from mitmproxy.proxy import events +from mitmproxy.proxy import layers +from mitmproxy.proxy import server from mitmproxy.proxy.context import Context from mitmproxy.proxy.layer import CommandGenerator from mitmproxy.proxy.layers.http import HTTPMode @@ -42,9 +48,9 @@ def __init__(self, flow: http.HTTPFlow, context: Context): def _handle_event(self, event: events.Event) -> CommandGenerator[None]: if isinstance(event, events.Start): content = self.flow.request.raw_content - self.flow.request.timestamp_start = ( - self.flow.request.timestamp_end - ) = time.time() + self.flow.request.timestamp_start = self.flow.request.timestamp_end = ( + time.time() + ) yield layers.http.ReceiveHttp( layers.http.RequestHeaders( 1, @@ -84,8 +90,10 @@ def __init__(self, flow: http.HTTPFlow, options: Options) -> None: client.state = ConnectionState.OPEN context = Context(client, options) - context.server = Server((flow.request.host, flow.request.port)) - context.server.tls = flow.request.scheme == "https" + context.server = Server(address=(flow.request.host, flow.request.port)) + if flow.request.scheme == "https": + context.server.tls = True + context.server.sni = flow.request.pretty_host if options.mode and options.mode[0].startswith("upstream:"): mode = UpstreamMode.parse(options.mode[0]) assert isinstance(mode, UpstreamMode) # remove once mypy supports Self. @@ -102,10 +110,17 @@ def __init__(self, flow: http.HTTPFlow, options: Options) -> None: self.done = asyncio.Event() async def replay(self) -> None: - self.server_event(events.Start()) + await self.server_event(events.Start()) await self.done.wait() - def log(self, message: str, level: int = logging.INFO) -> None: + def log( + self, + message: str, + level: int = logging.INFO, + exc_info: Literal[True] + | tuple[type[BaseException] | None, BaseException | None, TracebackType | None] + | None = None, + ) -> None: assert isinstance(level, int) logger.log(level=level, msg=f"[replay] {message}") @@ -128,45 +143,53 @@ async def handle_hook(self, hook: commands.StartHook) -> None: class ClientPlayback: - playback_task: Optional[asyncio.Task] = None - inflight: Optional[http.HTTPFlow] + playback_task: asyncio.Task | None = None + inflight: http.HTTPFlow | None queue: asyncio.Queue options: Options + replay_tasks: set[asyncio.Task] def __init__(self): self.queue = asyncio.Queue() self.inflight = None self.task = None + self.replay_tasks = set() def running(self): + self.options = ctx.options self.playback_task = asyncio_utils.create_task( self.playback(), name="client playback" ) - self.options = ctx.options - def done(self): + async def done(self): if self.playback_task: self.playback_task.cancel() + try: + await self.playback_task + except asyncio.CancelledError: + pass async def playback(self): while True: self.inflight = await self.queue.get() try: + assert self.inflight h = ReplayHandler(self.inflight, self.options) if ctx.options.client_replay_concurrency == -1: - asyncio_utils.create_task( + t = asyncio_utils.create_task( h.replay(), name="client playback awaiting response" ) + # keep a reference so this is not garbage collected + self.replay_tasks.add(t) + t.add_done_callback(self.replay_tasks.remove) else: await h.replay() except Exception: - logger.error( - f"Client replay has crashed!\n{traceback.format_exc()}" - ) + logger.exception(f"Client replay has crashed!") self.queue.task_done() self.inflight = None - def check(self, f: flow.Flow) -> Optional[str]: + def check(self, f: flow.Flow) -> str | None: if f.live or f == self.inflight: return "Can't replay live flow." if f.intercepted: diff --git a/mitmproxy/addons/command_history.py b/mitmproxy/addons/command_history.py index d75d3cade5..507b60e500 100644 --- a/mitmproxy/addons/command_history.py +++ b/mitmproxy/addons/command_history.py @@ -41,7 +41,7 @@ def configure(self, updated): def done(self): if ctx.options.command_history and len(self.history) >= self.VACUUM_SIZE: # vacuum history so that it doesn't grow indefinitely. - history_str = "\n".join(self.history[-self.VACUUM_SIZE // 2:]) + "\n" + history_str = "\n".join(self.history[-self.VACUUM_SIZE // 2 :]) + "\n" try: self.history_file.write_text(history_str) except Exception as e: diff --git a/mitmproxy/addons/comment.py b/mitmproxy/addons/comment.py index 3e9b549c72..ecb303b0c0 100644 --- a/mitmproxy/addons/comment.py +++ b/mitmproxy/addons/comment.py @@ -1,6 +1,8 @@ from collections.abc import Sequence -from mitmproxy import command, flow, ctx +from mitmproxy import command +from mitmproxy import ctx +from mitmproxy import flow from mitmproxy.hooks import UpdateHook diff --git a/mitmproxy/addons/core.py b/mitmproxy/addons/core.py index 87eda62cd5..3056543798 100644 --- a/mitmproxy/addons/core.py +++ b/mitmproxy/addons/core.py @@ -1,18 +1,17 @@ import logging import os from collections.abc import Sequence -from typing import Union -from mitmproxy.log import ALERT -from mitmproxy.utils import emoji -from mitmproxy import ctx, hooks -from mitmproxy import exceptions +import mitmproxy.types from mitmproxy import command +from mitmproxy import ctx +from mitmproxy import exceptions from mitmproxy import flow +from mitmproxy import hooks from mitmproxy import optmanager +from mitmproxy.log import ALERT from mitmproxy.net.http import status_codes -import mitmproxy.types - +from mitmproxy.utils import emoji logger = logging.getLogger(__name__) @@ -69,7 +68,7 @@ def mark(self, flows: Sequence[flow.Flow], marker: mitmproxy.types.Marker) -> No Mark flows. """ updated = [] - if marker not in emoji.emoji: + if not (marker == "" or marker in emoji.emoji): raise exceptions.CommandError(f"invalid marker value") for i in flows: @@ -134,7 +133,7 @@ def flow_set(self, flows: Sequence[flow.Flow], attr: str, value: str) -> None: """ Quickly set a number of common values on flows. """ - val: Union[int, str] = value + val: int | str = value if attr == "status_code": try: val = int(val) # type: ignore diff --git a/mitmproxy/addons/cut.py b/mitmproxy/addons/cut.py index 52a21df527..c8be911272 100644 --- a/mitmproxy/addons/cut.py +++ b/mitmproxy/addons/cut.py @@ -1,18 +1,18 @@ -import io import csv +import io import logging import os.path from collections.abc import Sequence -from typing import Any, Union +from typing import Any + +import pyperclip +import mitmproxy.types +from mitmproxy import certs from mitmproxy import command from mitmproxy import exceptions from mitmproxy import flow -from mitmproxy import certs -import mitmproxy.types - -import pyperclip - +from mitmproxy import http from mitmproxy.log import ALERT logger = logging.getLogger(__name__) @@ -28,7 +28,17 @@ def is_addr(v): return isinstance(v, tuple) and len(v) > 1 -def extract(cut: str, f: flow.Flow) -> Union[str, bytes]: +def extract(cut: str, f: flow.Flow) -> str | bytes: + # Hack for https://github.com/mitmproxy/mitmproxy/issues/6721: + # Make "save body" keybind work for WebSocket flows. + # Ideally the keybind would be smarter and this here can get removed. + if ( + isinstance(f, http.HTTPFlow) + and f.websocket + and cut in ("request.content", "response.content") + ): + return f.websocket._get_formatted_messages() + path = cut.split(".") current: Any = f for i, spec in enumerate(path): @@ -86,7 +96,7 @@ def cut( or "false", "bytes" are preserved, and all other values are converted to strings. """ - ret: list[list[Union[str, bytes]]] = [] + ret: list[list[str | bytes]] = [] for f in flows: ret.append([extract(c, f) for c in cuts]) return ret # type: ignore @@ -132,7 +142,7 @@ def save( writer.writerow(vals) logger.log( ALERT, - "Saved %s cuts over %d flows as CSV." % (len(cuts), len(flows)) + "Saved %s cuts over %d flows as CSV." % (len(cuts), len(flows)), ) except OSError as e: logger.error(str(e)) @@ -148,7 +158,7 @@ def clip( format is UTF-8 encoded CSV. If there is exactly one row and one column, the data is written to file as-is, with raw bytes preserved. """ - v: Union[str, bytes] + v: str | bytes fp = io.StringIO(newline="") if len(cuts) == 1 and len(flows) == 1: v = extract_str(cuts[0], flows[0]) diff --git a/mitmproxy/addons/disable_h2c.py b/mitmproxy/addons/disable_h2c.py index 595ce2b3f0..ae9e3f94e6 100644 --- a/mitmproxy/addons/disable_h2c.py +++ b/mitmproxy/addons/disable_h2c.py @@ -2,7 +2,6 @@ class DisableH2C: - """ We currently only support HTTP/2 over a TLS connection. diff --git a/mitmproxy/addons/dns_resolver.py b/mitmproxy/addons/dns_resolver.py index fcfb153f2d..7001bf1ecc 100644 --- a/mitmproxy/addons/dns_resolver.py +++ b/mitmproxy/addons/dns_resolver.py @@ -1,144 +1,108 @@ import asyncio import ipaddress +import logging import socket -from typing import Callable, Iterable, Union +from collections.abc import Awaitable +from collections.abc import Callable +from collections.abc import Sequence +from functools import cache + +import mitmproxy_rs +from mitmproxy import ctx from mitmproxy import dns +from mitmproxy.flow import Error from mitmproxy.proxy import mode_specs -IP4_PTR_SUFFIX = ".in-addr.arpa" -IP6_PTR_SUFFIX = ".ip6.arpa" - - -class ResolveError(Exception): - """Exception thrown by different resolve methods.""" - - def __init__(self, response_code: int) -> None: - assert response_code != dns.response_codes.NOERROR - self.response_code = response_code - - -async def resolve_question_by_name( - question: dns.Question, - loop: asyncio.AbstractEventLoop, - family: socket.AddressFamily, - ip: Callable[[str], Union[ipaddress.IPv4Address, ipaddress.IPv6Address]], -) -> Iterable[dns.ResourceRecord]: - try: - addrinfos = await loop.getaddrinfo(host=question.name, port=0, family=family) - except socket.gaierror as e: - if e.errno == socket.EAI_NONAME: - raise ResolveError(dns.response_codes.NXDOMAIN) - else: - # NOTE might fail on Windows for IPv6 queries: - # https://stackoverflow.com/questions/66755681/getaddrinfo-c-on-windows-not-handling-ipv6-correctly-returning-error-code-1 - raise ResolveError(dns.response_codes.SERVFAIL) # pragma: no cover - return map( - lambda addrinfo: dns.ResourceRecord( - name=question.name, - type=question.type, - class_=question.class_, - ttl=dns.ResourceRecord.DEFAULT_TTL, - data=ip(addrinfo[4][0]).packed, - ), - addrinfos, - ) +logger = logging.getLogger(__name__) -async def resolve_question_by_addr( - question: dns.Question, - loop: asyncio.AbstractEventLoop, - suffix: str, - sockaddr: Callable[[list[str]], Union[tuple[str, int], tuple[str, int, int, int]]], -) -> Iterable[dns.ResourceRecord]: - try: - addr = sockaddr(question.name[: -len(suffix)].split(".")[::-1]) - except ValueError: - raise ResolveError(dns.response_codes.FORMERR) - try: - name, _ = await loop.getnameinfo(addr, flags=socket.NI_NAMEREQD) - except socket.gaierror as e: - raise ResolveError( - dns.response_codes.NXDOMAIN - if e.errno == socket.EAI_NONAME - else dns.response_codes.SERVFAIL - ) - return [ - dns.ResourceRecord( - name=question.name, - type=question.type, - class_=question.class_, - ttl=dns.ResourceRecord.DEFAULT_TTL, - data=dns.domain_names.pack(name), +class DnsResolver: + def load(self, loader): + loader.add_option( + "dns_use_hosts_file", + bool, + True, + "Use the hosts file for DNS lookups in regular DNS mode/wireguard mode.", ) - ] + loader.add_option( + "dns_name_servers", + Sequence[str], + [], + "Name servers to use for lookups. Default: operating system's name servers", + ) -async def resolve_question( - question: dns.Question, loop: asyncio.AbstractEventLoop -) -> Iterable[dns.ResourceRecord]: - """Resolve the question into resource record(s), throwing ResolveError if an error condition occurs.""" + def configure(self, updated): + if "dns_use_hosts_file" in updated or "dns_name_servers" in updated: + self.resolver.cache_clear() + self.name_servers.cache_clear() - if question.class_ != dns.classes.IN: - raise ResolveError(dns.response_codes.NOTIMP) - if question.type == dns.types.A: - return await resolve_question_by_name( - question, loop, socket.AddressFamily.AF_INET, ipaddress.IPv4Address - ) - elif question.type == dns.types.AAAA: - return await resolve_question_by_name( - question, loop, socket.AddressFamily.AF_INET6, ipaddress.IPv6Address - ) - elif question.type == dns.types.PTR: - name_lower = question.name.lower() - if name_lower.endswith(IP4_PTR_SUFFIX): - return await resolve_question_by_addr( - question=question, - loop=loop, - suffix=IP4_PTR_SUFFIX, - sockaddr=lambda x: (str(ipaddress.IPv4Address(".".join(x))), 0), + @cache + def name_servers(self) -> list[str]: + """ + The Operating System name servers, + or `[]` if they cannot be determined. + """ + try: + return ( + ctx.options.dns_name_servers + or mitmproxy_rs.dns.get_system_dns_servers() ) - elif name_lower.endswith(IP6_PTR_SUFFIX): - return await resolve_question_by_addr( - question=question, - loop=loop, - suffix=IP6_PTR_SUFFIX, - sockaddr=lambda x: ( - str(ipaddress.IPv6Address(bytes.fromhex("".join(x)))), - 0, - 0, - 0, - ), + except RuntimeError as e: + logger.warning( + f"Failed to get system dns servers: {e}\n" + f"The dns_name_servers option needs to be set manually." ) - else: - raise ResolveError(dns.response_codes.FORMERR) - else: - raise ResolveError(dns.response_codes.NOTIMP) + return [] + @cache + def resolver(self) -> mitmproxy_rs.dns.DnsResolver: + """ + Our mitmproxy_rs DNS resolver. + """ + ns = self.name_servers() + assert ns + return mitmproxy_rs.dns.DnsResolver( + name_servers=ns, + use_hosts_file=ctx.options.dns_use_hosts_file, + ) -async def resolve_message( - message: dns.Message, loop: asyncio.AbstractEventLoop -) -> dns.Message: - try: - if not message.query: - raise ResolveError( - dns.response_codes.REFUSED - ) # we cannot resolve an answer - if message.op_code != dns.op_codes.QUERY: - raise ResolveError( - dns.response_codes.NOTIMP - ) # inverse queries and others are not supported - rrs: list[dns.ResourceRecord] = [] - for question in message.questions: - rrs.extend(await resolve_question(question, loop)) - except ResolveError as e: - return message.fail(e.response_code) - else: - return message.succeed(rrs) + async def dns_request(self, flow: dns.DNSFlow) -> None: + if self._should_resolve(flow): + all_ip_lookups = ( + flow.request.query + and flow.request.op_code == dns.op_codes.QUERY + and flow.request.question + and flow.request.question.class_ == dns.classes.IN + and flow.request.question.type in (dns.types.A, dns.types.AAAA) + ) + name_servers = self.name_servers() + if all_ip_lookups: + # For A/AAAA records, we try to use our own resolver + # (with a fallback to getaddrinfo) + if name_servers: + flow.response = await self.resolve( + flow.request, self._with_resolver + ) + elif ctx.options.dns_use_hosts_file: + # Fallback to getaddrinfo as hickory's resolver isn't as reliable + # as we would like it to be (https://github.com/mitmproxy/mitmproxy/issues/7064). + flow.response = await self.resolve( + flow.request, self._with_getaddrinfo + ) + else: + flow.error = Error("Cannot resolve, dns_name_servers unknown.") + elif name_servers: + # For other records, the best we can do is to forward the query + # to an upstream server. + flow.server_conn.address = (name_servers[0], 53) + else: + flow.error = Error("Cannot resolve, dns_name_servers unknown.") -class DnsResolver: - async def dns_request(self, flow: dns.DNSFlow) -> None: - should_resolve = ( + @staticmethod + def _should_resolve(flow: dns.DNSFlow) -> bool: + return ( ( isinstance(flow.client_conn.proxy_mode, mode_specs.DnsMode) or ( @@ -150,8 +114,54 @@ async def dns_request(self, flow: dns.DNSFlow) -> None: and not flow.response and not flow.error ) - if should_resolve: - # TODO: We need to handle overly long responses here. - flow.response = await resolve_message( - flow.request, asyncio.get_running_loop() - ) + + async def resolve( + self, + message: dns.Message, + resolve_func: Callable[[dns.Question], Awaitable[list[str]]], + ) -> dns.Message: + assert message.question + try: + ip_addrs = await resolve_func(message.question) + except socket.gaierror as e: + match e.args[0]: + case socket.EAI_NONAME: + return message.fail(dns.response_codes.NXDOMAIN) + case socket.EAI_NODATA: + ip_addrs = [] + case _: + return message.fail(dns.response_codes.SERVFAIL) + + return message.succeed( + [ + dns.ResourceRecord( + name=message.question.name, + type=message.question.type, + class_=message.question.class_, + ttl=dns.ResourceRecord.DEFAULT_TTL, + data=ipaddress.ip_address(ip).packed, + ) + for ip in ip_addrs + ] + ) + + async def _with_resolver(self, question: dns.Question) -> list[str]: + """Resolve an A/AAAA question using the mitmproxy_rs DNS resolver.""" + if question.type == dns.types.A: + return await self.resolver().lookup_ipv4(question.name) + else: + return await self.resolver().lookup_ipv6(question.name) + + async def _with_getaddrinfo(self, question: dns.Question) -> list[str]: + """Resolve an A/AAAA question using getaddrinfo.""" + if question.type == dns.types.A: + family = socket.AF_INET + else: + family = socket.AF_INET6 + addrinfos = await asyncio.get_running_loop().getaddrinfo( + host=question.name, + port=None, + family=family, + type=socket.SOCK_STREAM, + ) + return [addrinfo[4][0] for addrinfo in addrinfos] diff --git a/mitmproxy/addons/dumper.py b/mitmproxy/addons/dumper.py index 73e45da5da..5964f0e7fa 100644 --- a/mitmproxy/addons/dumper.py +++ b/mitmproxy/addons/dumper.py @@ -1,9 +1,11 @@ -import logging +from __future__ import annotations import itertools +import logging import shutil import sys -from typing import IO, Optional, Union +from typing import IO +from typing import Optional from wsproto.frame_protocol import CloseReason @@ -15,21 +17,26 @@ from mitmproxy import flowfilter from mitmproxy import http from mitmproxy.contrib import click as miniclick -from mitmproxy.tcp import TCPFlow, TCPMessage -from mitmproxy.udp import UDPFlow, UDPMessage +from mitmproxy.net.dns import response_codes +from mitmproxy.options import CONTENT_VIEW_LINES_CUTOFF +from mitmproxy.tcp import TCPFlow +from mitmproxy.tcp import TCPMessage +from mitmproxy.udp import UDPFlow +from mitmproxy.udp import UDPMessage from mitmproxy.utils import human from mitmproxy.utils import strutils from mitmproxy.utils import vt_codes -from mitmproxy.websocket import WebSocketData, WebSocketMessage +from mitmproxy.websocket import WebSocketData +from mitmproxy.websocket import WebSocketMessage def indent(n: int, text: str) -> str: - l = str(text).strip().splitlines() + lines = str(text).strip().splitlines() pad = " " * n - return "\n".join(pad + i for i in l) + return "\n".join(pad + i for i in lines) -CONTENTVIEW_STYLES = { +CONTENTVIEW_STYLES: dict[str, dict[str, str | bool]] = { "highlight": dict(bold=True), "offset": dict(fg="blue"), "header": dict(fg="green", bold=True), @@ -38,8 +45,8 @@ def indent(n: int, text: str) -> str: class Dumper: - def __init__(self, outfile: Optional[IO[str]] = None): - self.filter: Optional[flowfilter.TFilter] = None + def __init__(self, outfile: IO[str] | None = None): + self.filter: flowfilter.TFilter | None = None self.outfp: IO[str] = outfile or sys.stdout self.out_has_vt_codes = vt_codes.ensure_supported(self.outfp) @@ -48,12 +55,12 @@ def load(self, loader): "flow_detail", int, 1, - """ + f""" The display detail level for flows in mitmdump: 0 (quiet) to 4 (very verbose). 0: no output 1: shortened request URL with response status code 2: full request URL with response status code and HTTP headers - 3: 2 + truncated response content, content of WebSocket and TCP messages + 3: 2 + truncated response content, content of WebSocket and TCP messages (content_view_lines_cutoff: {CONTENT_VIEW_LINES_CUTOFF}) 4: 3 + nothing is truncated """, ) @@ -96,7 +103,7 @@ def _echo_headers(self, headers: http.Headers): vs = strutils.bytes_to_escaped_str(v) self.echo(f"{ks}: {vs}", ident=4) - def _echo_trailers(self, trailers: Optional[http.Headers]): + def _echo_trailers(self, trailers: http.Headers | None): if not trailers: return self.echo("--- HTTP Trailers", fg="magenta", ident=4) @@ -104,13 +111,13 @@ def _echo_trailers(self, trailers: Optional[http.Headers]): def _colorful(self, line): yield " " # we can already indent here - for (style, text) in line: + for style, text in line: yield self.style(text, **CONTENTVIEW_STYLES.get(style, {})) def _echo_message( self, - message: Union[http.Message, TCPMessage, UDPMessage, WebSocketMessage], - flow: Union[http.HTTPFlow, TCPFlow, UDPFlow], + message: http.Message | TCPMessage | UDPMessage | WebSocketMessage, + flow: http.HTTPFlow | TCPFlow | UDPFlow, ): _, lines, error = contentviews.get_message_content_view( ctx.options.dumper_default_contentview, message, flow @@ -119,7 +126,9 @@ def _echo_message( logging.debug(error) if ctx.options.flow_detail == 3: - lines_to_echo = itertools.islice(lines, 70) + lines_to_echo = itertools.islice( + lines, ctx.options.content_view_lines_cutoff + ) else: lines_to_echo = lines @@ -203,7 +212,7 @@ def _echo_response_line(self, flow: http.HTTPFlow) -> None: blink=(code_int == 418), ) - if not flow.response.is_http2: + if not (flow.response.is_http2 or flow.response.is_http3): reason = flow.response.reason else: reason = http.status_codes.RESPONSES.get(flow.response.status_code, "") @@ -334,16 +343,29 @@ def tcp_error(self, f): def udp_error(self, f): self._proto_error(f) - def _proto_message(self, f): + def _proto_message(self, f: TCPFlow | UDPFlow) -> None: if self.match(f): message = f.messages[-1] direction = "->" if message.from_client else "<-" + if f.client_conn.tls_version == "QUICv1": + if f.type == "tcp": + quic_type = "stream" + else: + quic_type = "dgrams" + # TODO: This should not be metadata, this should be typed attributes. + flow_type = ( + f"quic {quic_type} {f.metadata.get('quic_stream_id_client','')} " + f"{direction} mitmproxy {direction} " + f"quic {quic_type} {f.metadata.get('quic_stream_id_server','')}" + ) + else: + flow_type = f.type self.echo( "{client} {direction} {type} {direction} {server}".format( client=human.format_address(f.client_conn.peername), server=human.format_address(f.server_conn.address), direction=direction, - type=f.type, + type=flow_type, ) ) if ctx.options.flow_detail >= 3: @@ -376,9 +398,17 @@ def dns_response(self, f: dns.DNSFlow): self._echo_dns_query(f) arrows = self.style(" <<", bold=True) - answers = ", ".join( - self.style(str(x), fg="bright_blue") for x in f.response.answers - ) + if f.response.answers: + answers = ", ".join( + self.style(str(x), fg="bright_blue") for x in f.response.answers + ) + else: + answers = self.style( + response_codes.to_str( + f.response.response_code, + ), + fg="red", + ) self.echo(f"{arrows} {answers}") def dns_error(self, f: dns.DNSFlow): diff --git a/mitmproxy/addons/errorcheck.py b/mitmproxy/addons/errorcheck.py index f82748ad76..c46f920354 100644 --- a/mitmproxy/addons/errorcheck.py +++ b/mitmproxy/addons/errorcheck.py @@ -1,16 +1,23 @@ import asyncio import logging - import sys from mitmproxy import log +from mitmproxy.contrib import click as miniclick +from mitmproxy.utils import vt_codes class ErrorCheck: """Monitor startup for error log entries, and terminate immediately if there are some.""" - def __init__(self, log_to_stderr: bool = False): - self.log_to_stderr = log_to_stderr + repeat_errors_on_stderr: bool + """ + Repeat all errors on stderr before exiting. + This is useful for the console UI, which otherwise swallows all output. + """ + + def __init__(self, repeat_errors_on_stderr: bool = False) -> None: + self.repeat_errors_on_stderr = repeat_errors_on_stderr self.logger = ErrorCheckHandler() self.logger.install() @@ -22,16 +29,25 @@ async def shutdown_if_errored(self): # don't run immediately, wait for all logging tasks to finish. await asyncio.sleep(0) if self.logger.has_errored: - if self.log_to_stderr: - plural = "s" if len(self.logger.has_errored) > 1 else "" - msg = "\n".join(r.msg for r in self.logger.has_errored) - print(f"Error{plural} on startup: {msg}", file=sys.stderr) + plural = "s" if len(self.logger.has_errored) > 1 else "" + if self.repeat_errors_on_stderr: + message = f"Error{plural} logged during startup:" + if vt_codes.ensure_supported(sys.stderr): # pragma: no cover + message = miniclick.style(message, fg="red") + details = "\n".join( + self.logger.format(r) for r in self.logger.has_errored + ) + print(f"{message}\n{details}", file=sys.stderr) + else: + print( + f"Error{plural} logged during startup, exiting...", file=sys.stderr + ) sys.exit(1) class ErrorCheckHandler(log.MitmLogHandler): - def __init__(self): + def __init__(self) -> None: super().__init__(logging.ERROR) self.has_errored: list[logging.LogRecord] = [] diff --git a/mitmproxy/addons/eventstore.py b/mitmproxy/addons/eventstore.py index d973842c4d..b474a5f7c0 100644 --- a/mitmproxy/addons/eventstore.py +++ b/mitmproxy/addons/eventstore.py @@ -2,15 +2,15 @@ import collections import logging from collections.abc import Callable -from typing import Optional -from mitmproxy import command, log +from mitmproxy import command +from mitmproxy import log from mitmproxy.log import LogEntry from mitmproxy.utils import signals class EventStore: - def __init__(self, size=10000): + def __init__(self, size: int = 10000) -> None: self.data: collections.deque[LogEntry] = collections.deque(maxlen=size) self.sig_add = signals.SyncSignal(lambda entry: None) self.sig_refresh = signals.SyncSignal(lambda: None) @@ -26,7 +26,7 @@ def _add_log(self, entry: LogEntry) -> None: self.sig_add.send(entry) @property - def size(self) -> Optional[int]: + def size(self) -> int | None: return self.data.maxlen @command.command("eventstore.clear") diff --git a/mitmproxy/addons/export.py b/mitmproxy/addons/export.py index 1e2267bffb..f1ac553f15 100644 --- a/mitmproxy/addons/export.py +++ b/mitmproxy/addons/export.py @@ -1,15 +1,17 @@ import logging import shlex -from collections.abc import Callable, Sequence -from typing import Any, Union +from collections.abc import Callable +from collections.abc import Sequence +from typing import Any import pyperclip import mitmproxy.types from mitmproxy import command -from mitmproxy import ctx, http +from mitmproxy import ctx from mitmproxy import exceptions from mitmproxy import flow +from mitmproxy import http from mitmproxy.net.http.http1 import assemble from mitmproxy.utils import strutils @@ -130,7 +132,10 @@ def raw(f: flow.Flow, separator=b"\r\n\r\n") -> bytes: ) if request_present and response_present: - return b"".join([raw_request(f), separator, raw_response(f)]) + parts = [raw_request(f), raw_response(f)] + if isinstance(f, http.HTTPFlow) and f.websocket: + parts.append(f.websocket._get_formatted_messages()) + return separator.join(parts) elif request_present: return raw_request(f) elif response_present: @@ -139,7 +144,7 @@ def raw(f: flow.Flow, separator=b"\r\n\r\n") -> bytes: raise exceptions.CommandError("Can't export flow with no request or response.") -formats: dict[str, Callable[[flow.Flow], Union[str, bytes]]] = dict( +formats: dict[str, Callable[[flow.Flow], str | bytes]] = dict( curl=curl_command, httpie=httpie_command, raw=raw, diff --git a/mitmproxy/addons/intercept.py b/mitmproxy/addons/intercept.py index f918762692..0749c3b17d 100644 --- a/mitmproxy/addons/intercept.py +++ b/mitmproxy/addons/intercept.py @@ -1,12 +1,13 @@ from typing import Optional -from mitmproxy import flow, flowfilter -from mitmproxy import exceptions from mitmproxy import ctx +from mitmproxy import exceptions +from mitmproxy import flow +from mitmproxy import flowfilter class Intercept: - filt: Optional[flowfilter.TFilter] = None + filt: flowfilter.TFilter | None = None def load(self, loader): loader.add_option("intercept_active", bool, False, "Intercept toggle") @@ -57,3 +58,6 @@ def dns_request(self, f): def dns_response(self, f): self.process_flow(f) + + def websocket_message(self, f): + self.process_flow(f) diff --git a/mitmproxy/addons/keepserving.py b/mitmproxy/addons/keepserving.py index 5a149fdec1..04554c9f34 100644 --- a/mitmproxy/addons/keepserving.py +++ b/mitmproxy/addons/keepserving.py @@ -1,8 +1,14 @@ +from __future__ import annotations + import asyncio + from mitmproxy import ctx +from mitmproxy.utils import asyncio_utils class KeepServing: + _watch_task: asyncio.Task | None = None + def load(self, loader): loader.add_option( "keepserving", @@ -39,4 +45,6 @@ def running(self): ctx.options.rfile, ] if any(opts) and not ctx.options.keepserving: - asyncio.get_running_loop().create_task(self.watch()) + self._watch_task = asyncio_utils.create_task( + self.watch(), name="keepserving" + ) diff --git a/mitmproxy/addons/maplocal.py b/mitmproxy/addons/maplocal.py index 2701f14737..5b1abd0b53 100644 --- a/mitmproxy/addons/maplocal.py +++ b/mitmproxy/addons/maplocal.py @@ -8,7 +8,11 @@ from werkzeug.security import safe_join -from mitmproxy import ctx, exceptions, flowfilter, http, version +from mitmproxy import ctx +from mitmproxy import exceptions +from mitmproxy import flowfilter +from mitmproxy import http +from mitmproxy import version from mitmproxy.utils.spec import parse_spec @@ -76,7 +80,7 @@ def file_candidates(url: str, spec: MapLocalSpec) -> list[Path]: class MapLocal: - def __init__(self): + def __init__(self) -> None: self.replacements: list[MapLocalSpec] = [] def load(self, loader): diff --git a/mitmproxy/addons/mapremote.py b/mitmproxy/addons/mapremote.py index 2fe6c2d2ef..31a759ada4 100644 --- a/mitmproxy/addons/mapremote.py +++ b/mitmproxy/addons/mapremote.py @@ -2,7 +2,10 @@ from collections.abc import Sequence from typing import NamedTuple -from mitmproxy import ctx, exceptions, flowfilter, http +from mitmproxy import ctx +from mitmproxy import exceptions +from mitmproxy import flowfilter +from mitmproxy import http from mitmproxy.utils.spec import parse_spec @@ -24,7 +27,7 @@ def parse_map_remote_spec(option: str) -> MapRemoteSpec: class MapRemote: - def __init__(self): + def __init__(self) -> None: self.replacements: list[MapRemoteSpec] = [] def load(self, loader): diff --git a/mitmproxy/addons/modifybody.py b/mitmproxy/addons/modifybody.py index 7d3287aecc..657dbe07ce 100644 --- a/mitmproxy/addons/modifybody.py +++ b/mitmproxy/addons/modifybody.py @@ -2,12 +2,17 @@ import re from collections.abc import Sequence -from mitmproxy import ctx, exceptions -from mitmproxy.addons.modifyheaders import parse_modify_spec, ModifySpec +from mitmproxy import ctx +from mitmproxy import exceptions +from mitmproxy.addons.modifyheaders import ModifySpec +from mitmproxy.addons.modifyheaders import parse_modify_spec +from mitmproxy.log import ALERT + +logger = logging.getLogger(__name__) class ModifyBody: - def __init__(self): + def __init__(self) -> None: self.replacements: list[ModifySpec] = [] def load(self, loader): @@ -35,6 +40,18 @@ def configure(self, updated): self.replacements.append(spec) + stream_and_modify_conflict = ( + ctx.options.modify_body + and ctx.options.stream_large_bodies + and ("modify_body" in updated or "stream_large_bodies" in updated) + ) + if stream_and_modify_conflict: + logger.log( + ALERT, + "Both modify_body and stream_large_bodies are active. " + "Streamed bodies will not be modified.", + ) + def request(self, flow): if flow.response or flow.error or not flow.live: return diff --git a/mitmproxy/addons/modifyheaders.py b/mitmproxy/addons/modifyheaders.py index 370b9abb92..503ba2282b 100644 --- a/mitmproxy/addons/modifyheaders.py +++ b/mitmproxy/addons/modifyheaders.py @@ -4,7 +4,10 @@ from pathlib import Path from typing import NamedTuple -from mitmproxy import ctx, exceptions, flowfilter, http +from mitmproxy import ctx +from mitmproxy import exceptions +from mitmproxy import flowfilter +from mitmproxy import http from mitmproxy.http import Headers from mitmproxy.utils import strutils from mitmproxy.utils.spec import parse_spec @@ -51,7 +54,7 @@ def parse_modify_spec(option: str, subject_is_regex: bool) -> ModifySpec: class ModifyHeaders: - def __init__(self): + def __init__(self) -> None: self.replacements: list[ModifySpec] = [] def load(self, loader): @@ -78,12 +81,12 @@ def configure(self, updated): ) from e self.replacements.append(spec) - def request(self, flow): + def requestheaders(self, flow): if flow.response or flow.error or not flow.live: return self.run(flow, flow.request.headers) - def response(self, flow): + def responseheaders(self, flow): if flow.error or not flow.live: return self.run(flow, flow.response.headers) diff --git a/mitmproxy/addons/next_layer.py b/mitmproxy/addons/next_layer.py index 04fb95db8c..6d158745f5 100644 --- a/mitmproxy/addons/next_layer.py +++ b/mitmproxy/addons/next_layer.py @@ -8,30 +8,61 @@ For a typical HTTPS request, this addon is called a couple of times: First to determine that we start with an HTTP layer which processes the `CONNECT` request, a second time to determine that the client then starts negotiating TLS, and a -third time where we check if the protocol within that TLS stream is actually HTTP or something else. +third time when we check if the protocol within that TLS stream is actually HTTP or something else. Sometimes it's useful to hardcode specific logic in next_layer when one wants to do fancy things. In that case it's not necessary to modify mitmproxy's source, adding a custom addon with a next_layer event hook that sets nextlayer.layer works just as well. """ + +from __future__ import annotations + +import logging import re +import sys +from collections.abc import Iterable from collections.abc import Sequence -import struct -from typing import Any, Callable, Iterable, Optional, Union - -from mitmproxy import ctx, dns, exceptions, connection -from mitmproxy.net.tls import is_tls_record_magic -from mitmproxy.proxy.layers.http import HTTPMode -from mitmproxy.proxy import context, layer, layers +from typing import Any +from typing import cast + +from mitmproxy import ctx +from mitmproxy.connection import Address +from mitmproxy.net.tls import starts_like_dtls_record +from mitmproxy.net.tls import starts_like_tls_record +from mitmproxy.proxy import layer +from mitmproxy.proxy import layers +from mitmproxy.proxy import mode_specs +from mitmproxy.proxy import tunnel +from mitmproxy.proxy.context import Context +from mitmproxy.proxy.layer import Layer +from mitmproxy.proxy.layers import ClientQuicLayer +from mitmproxy.proxy.layers import ClientTLSLayer +from mitmproxy.proxy.layers import DNSLayer +from mitmproxy.proxy.layers import HttpLayer from mitmproxy.proxy.layers import modes -from mitmproxy.proxy.layers.tls import HTTP_ALPNS, dtls_parse_client_hello, parse_client_hello +from mitmproxy.proxy.layers import RawQuicLayer +from mitmproxy.proxy.layers import ServerQuicLayer +from mitmproxy.proxy.layers import ServerTLSLayer +from mitmproxy.proxy.layers import TCPLayer +from mitmproxy.proxy.layers import UDPLayer +from mitmproxy.proxy.layers.http import HTTPMode +from mitmproxy.proxy.layers.quic import quic_parse_client_hello_from_datagrams +from mitmproxy.proxy.layers.tls import dtls_parse_client_hello +from mitmproxy.proxy.layers.tls import HTTP1_ALPNS +from mitmproxy.proxy.layers.tls import HTTP_ALPNS +from mitmproxy.proxy.layers.tls import parse_client_hello from mitmproxy.tls import ClientHello -LayerCls = type[layer.Layer] +if sys.version_info < (3, 11): + from typing_extensions import assert_never +else: + from typing import assert_never + +logger = logging.getLogger(__name__) def stack_match( - context: context.Context, layers: Sequence[Union[LayerCls, tuple[LayerCls, ...]]] + context: Context, layers: Sequence[type[Layer] | tuple[type[Layer], ...]] ) -> bool: if len(context.layers) != len(layers): return False @@ -41,11 +72,15 @@ def stack_match( ) +class NeedsMoreData(Exception): + """Signal that the decision on which layer to put next needs to be deferred within the NextLayer addon.""" + + class NextLayer: - ignore_hosts: Iterable[re.Pattern] = () - allow_hosts: Iterable[re.Pattern] = () - tcp_hosts: Iterable[re.Pattern] = () - udp_hosts: Iterable[re.Pattern] = () + ignore_hosts: Sequence[re.Pattern] = () + allow_hosts: Sequence[re.Pattern] = () + tcp_hosts: Sequence[re.Pattern] = () + udp_hosts: Sequence[re.Pattern] = () def configure(self, updated): if "tcp_hosts" in updated: @@ -57,10 +92,6 @@ def configure(self, updated): re.compile(x, re.IGNORECASE) for x in ctx.options.udp_hosts ] if "allow_hosts" in updated or "ignore_hosts" in updated: - if ctx.options.allow_hosts and ctx.options.ignore_hosts: - raise exceptions.OptionsError( - "The allow_hosts and ignore_hosts options are mutually exclusive." - ) self.ignore_hosts = [ re.compile(x, re.IGNORECASE) for x in ctx.options.ignore_hosts ] @@ -68,191 +99,377 @@ def configure(self, updated): re.compile(x, re.IGNORECASE) for x in ctx.options.allow_hosts ] - def ignore_connection( + def next_layer(self, nextlayer: layer.NextLayer): + if nextlayer.layer: + return # do not override something another addon has set. + try: + nextlayer.layer = self._next_layer( + nextlayer.context, + nextlayer.data_client(), + nextlayer.data_server(), + ) + except NeedsMoreData: + logger.debug( + f"Deferring layer decision, not enough data: {nextlayer.data_client().hex()!r}" + ) + + def _next_layer( + self, context: Context, data_client: bytes, data_server: bytes + ) -> Layer | None: + assert context.layers + + def s(*layers): + return stack_match(context, layers) + + tcp_based = context.client.transport_protocol == "tcp" + udp_based = context.client.transport_protocol == "udp" + + # 1) check for --ignore/--allow + if self._ignore_connection(context, data_client, data_server): + return ( + layers.TCPLayer(context, ignore=not ctx.options.show_ignored_hosts) + if tcp_based + else layers.UDPLayer(context, ignore=not ctx.options.show_ignored_hosts) + ) + + # 2) Handle proxy modes with well-defined next protocol + # 2a) Reverse proxy: derive from spec + if s(modes.ReverseProxy): + return self._setup_reverse_proxy(context, data_client) + # 2b) Explicit HTTP proxies + if s((modes.HttpProxy, modes.HttpUpstreamProxy)): + return self._setup_explicit_http_proxy(context, data_client) + + # 3) Handle security protocols + # 3a) TLS/DTLS + is_tls_or_dtls = ( + tcp_based + and starts_like_tls_record(data_client) + or udp_based + and starts_like_dtls_record(data_client) + ) + if is_tls_or_dtls: + server_tls = ServerTLSLayer(context) + server_tls.child_layer = ClientTLSLayer(context) + return server_tls + # 3b) QUIC + if udp_based and _starts_like_quic(data_client, context.server.address): + server_quic = ServerQuicLayer(context) + server_quic.child_layer = ClientQuicLayer(context) + return server_quic + + # 4) Check for --tcp/--udp + if tcp_based and self._is_destination_in_hosts(context, self.tcp_hosts): + return layers.TCPLayer(context) + if udp_based and self._is_destination_in_hosts(context, self.udp_hosts): + return layers.UDPLayer(context) + + # 5) Handle application protocol + # 5a) Do we have a known ALPN negotiation? + if context.client.alpn: + if context.client.alpn in HTTP_ALPNS: + return layers.HttpLayer(context, HTTPMode.transparent) + elif context.client.tls_version == "QUICv1": + # TODO: Once we support more QUIC-based protocols, relax force_raw here. + return layers.RawQuicLayer(context, force_raw=True) + # 5b) Is it DNS? + if context.server.address and context.server.address[1] in (53, 5353): + return layers.DNSLayer(context) + # 5c) We have no other specialized layers for UDP, so we fall back to raw forwarding. + if udp_based: + return layers.UDPLayer(context) + # 5d) Check for raw tcp mode. + probably_no_http = ( + # the first three bytes should be the HTTP verb, so A-Za-z is expected. + len(data_client) < 3 + # HTTP would require whitespace... + or b" " not in data_client + # ...and that whitespace needs to be in the first line. + or (data_client.find(b" ") > data_client.find(b"\n")) + or not data_client[:3].isalpha() + # a server greeting would be uncharacteristic. + or data_server + or data_client.startswith(b"SSH") + ) + if ctx.options.rawtcp and probably_no_http: + return layers.TCPLayer(context) + # 5c) Assume HTTP by default. + return layers.HttpLayer(context, HTTPMode.transparent) + + def _ignore_connection( self, - server_address: Optional[connection.Address], + context: Context, data_client: bytes, - *, - is_tls: Callable[[bytes], bool] = is_tls_record_magic, - client_hello: Callable[[bytes], Optional[ClientHello]] = parse_client_hello - ) -> Optional[bool]: + data_server: bytes, + ) -> bool | None: """ Returns: True, if the connection should be ignored. False, if it should not be ignored. - None, if we need to wait for more input data. + + Raises: + NeedsMoreData, if we need to wait for more input data. """ if not ctx.options.ignore_hosts and not ctx.options.allow_hosts: return False - + # Special handling for wireguard mode: if the hostname is "10.0.0.53", do not ignore the connection + if isinstance( + context.client.proxy_mode, mode_specs.WireGuardMode + ) and context.server.address == ("10.0.0.53", 53): + return False hostnames: list[str] = [] - if server_address is not None: - hostnames.append(server_address[0]) - if is_tls(data_client): - try: - ch = client_hello(data_client) - if ch is None: # not complete yet - return None - sni = ch.sni - except ValueError: - pass - else: - if sni: - hostnames.append(sni) + if context.server.peername: + host, port, *_ = context.server.peername + hostnames.append(f"{host}:{port}") + if context.server.address: + host, port, *_ = context.server.address + hostnames.append(f"{host}:{port}") + + # We also want to check for TLS SNI and HTTP host headers, but in order to ignore connections based on that + # they must have a destination address. If they don't, we don't know how to establish an upstream connection + # if we ignore. + if host_header := self._get_host_header(context, data_client, data_server): + if not re.search(r":\d+$", host_header): + host_header = f"{host_header}:{port}" + hostnames.append(host_header) + if ( + client_hello := self._get_client_hello(context, data_client) + ) and client_hello.sni: + hostnames.append(f"{client_hello.sni}:{port}") + if context.client.sni: + # Hostname may be allowed, TLS is already established, and we have another next layer decision. + hostnames.append(f"{context.client.sni}:{port}") if not hostnames: return False - if ctx.options.ignore_hosts: - return any( + if ctx.options.allow_hosts: + not_allowed = not any( re.search(rex, host, re.IGNORECASE) for host in hostnames - for rex in ctx.options.ignore_hosts + for rex in ctx.options.allow_hosts ) - elif ctx.options.allow_hosts: - return not any( + if not_allowed: + return True + + if ctx.options.ignore_hosts: + ignored = any( re.search(rex, host, re.IGNORECASE) for host in hostnames - for rex in ctx.options.allow_hosts + for rex in ctx.options.ignore_hosts ) - else: # pragma: no cover - raise AssertionError() + if ignored: + return True - def setup_tls_layer(self, context: context.Context) -> layer.Layer: - def s(*layers): - return stack_match(context, layers) + return False - # client tls usually requires a server tls layer as parent layer, except: - # - a secure web proxy doesn't have a server part. - # - an upstream proxy uses the mode spec - # - reverse proxy mode manages this itself. - if ( - s(modes.HttpProxy) - or s(modes.HttpUpstreamProxy) - or s(modes.ReverseProxy) - or s(modes.ReverseProxy, layers.ServerTLSLayer) - ): - return layers.ClientTLSLayer(context) - else: - # We already assign the next layer here so that ServerTLSLayer - # knows that it can safely wait for a ClientHello. - ret = layers.ServerTLSLayer(context) - ret.child_layer = layers.ClientTLSLayer(context) - return ret + @staticmethod + def _get_host_header( + context: Context, + data_client: bytes, + data_server: bytes, + ) -> str | None: + """ + Try to read a host header from data_client. - def is_destination_in_hosts(self, context: context.Context, hosts: Iterable[re.Pattern]) -> bool: - return any( - (context.server.address and rex.search(context.server.address[0])) - or (context.client.sni and rex.search(context.client.sni)) - for rex in hosts - ) + Returns: + The host header value, or None, if no host header was found. - def next_layer(self, nextlayer: layer.NextLayer): - if nextlayer.layer is None: - nextlayer.layer = self._next_layer( - nextlayer.context, - nextlayer.data_client(), - nextlayer.data_server(), - ) + Raises: + NeedsMoreData, if the HTTP request is incomplete. + """ + if context.client.transport_protocol != "tcp" or data_server: + return None - def _next_layer( - self, context: context.Context, data_client: bytes, data_server: bytes - ) -> Optional[layer.Layer]: - assert context.layers + host_header_expected = context.client.alpn in HTTP1_ALPNS or re.match( + rb"[A-Z]{3,}.+HTTP/", data_client, re.IGNORECASE + ) + if host_header_expected: + if m := re.search( + rb"\r\n(?:Host:\s+(.+?)\s*)?\r\n", data_client, re.IGNORECASE + ): + if host := m.group(1): + return host.decode("utf-8", "surrogateescape") + else: + return None # \r\n\r\n - header end came first. + else: + raise NeedsMoreData + else: + return None - # helper function to quickly check if the existing layer stack matches a particular configuration. - def s(*layers): - return stack_match(context, layers) + @staticmethod + def _get_client_hello(context: Context, data_client: bytes) -> ClientHello | None: + """ + Try to read a TLS/DTLS/QUIC ClientHello from data_client. - if context.client.transport_protocol == "tcp": - if len(data_client) < 3 and not data_server: - return None # not enough data yet to make a decision + Returns: + A complete ClientHello, or None, if no ClientHello was found. - # 1. check for --ignore/--allow - ignore = self.ignore_connection(context.server.address, data_client) - if ignore is True: - return layers.TCPLayer(context, ignore=True) - if ignore is None: + Raises: + NeedsMoreData, if the ClientHello is incomplete. + """ + match context.client.transport_protocol: + case "tcp": + if starts_like_tls_record(data_client): + try: + ch = parse_client_hello(data_client) + except ValueError: + pass + else: + if ch is None: + raise NeedsMoreData + return ch return None + case "udp": + try: + return quic_parse_client_hello_from_datagrams([data_client]) + except ValueError: + pass + + try: + ch = dtls_parse_client_hello(data_client) + except ValueError: + pass + else: + if ch is None: + raise NeedsMoreData + return ch + return None + case _: # pragma: no cover + assert_never(context.client.transport_protocol) + + @staticmethod + def _setup_reverse_proxy(context: Context, data_client: bytes) -> Layer: + spec = cast(mode_specs.ReverseMode, context.client.proxy_mode) + stack = tunnel.LayerStack() + + match spec.scheme: + case "http": + if starts_like_tls_record(data_client): + stack /= ClientTLSLayer(context) + stack /= HttpLayer(context, HTTPMode.transparent) + case "https": + if context.client.transport_protocol == "udp": + stack /= ServerQuicLayer(context) + stack /= ClientQuicLayer(context) + stack /= HttpLayer(context, HTTPMode.transparent) + else: + stack /= ServerTLSLayer(context) + if starts_like_tls_record(data_client): + stack /= ClientTLSLayer(context) + stack /= HttpLayer(context, HTTPMode.transparent) + + case "tcp": + if starts_like_tls_record(data_client): + stack /= ClientTLSLayer(context) + stack /= TCPLayer(context) + case "tls": + stack /= ServerTLSLayer(context) + if starts_like_tls_record(data_client): + stack /= ClientTLSLayer(context) + stack /= TCPLayer(context) + + case "udp": + if starts_like_dtls_record(data_client): + stack /= ClientTLSLayer(context) + stack /= UDPLayer(context) + case "dtls": + stack /= ServerTLSLayer(context) + if starts_like_dtls_record(data_client): + stack /= ClientTLSLayer(context) + stack /= UDPLayer(context) + + case "dns": + # TODO: DNS-over-TLS / DNS-over-DTLS + # is_tls_or_dtls = ( + # context.client.transport_protocol == "tcp" and starts_like_tls_record(data_client) + # or + # context.client.transport_protocol == "udp" and starts_like_dtls_record(data_client) + # ) + # if is_tls_or_dtls: + # stack /= ClientTLSLayer(context) + stack /= DNSLayer(context) + + case "http3": + stack /= ServerQuicLayer(context) + stack /= ClientQuicLayer(context) + stack /= HttpLayer(context, HTTPMode.transparent) + case "quic": + stack /= ServerQuicLayer(context) + stack /= ClientQuicLayer(context) + stack /= RawQuicLayer(context, force_raw=True) + + case _: # pragma: no cover + assert_never(spec.scheme) + + return stack[0] + + @staticmethod + def _setup_explicit_http_proxy(context: Context, data_client: bytes) -> Layer: + stack = tunnel.LayerStack() + + if context.client.transport_protocol == "udp": + stack /= layers.ClientQuicLayer(context) + elif starts_like_tls_record(data_client): + stack /= layers.ClientTLSLayer(context) + + if isinstance(context.layers[0], modes.HttpUpstreamProxy): + stack /= layers.HttpLayer(context, HTTPMode.upstream) + else: + stack /= layers.HttpLayer(context, HTTPMode.regular) - # 2. Check for TLS - if is_tls_record_magic(data_client): - return self.setup_tls_layer(context) - - # 3. Setup the HTTP layer for a regular HTTP proxy - if ( - s(modes.HttpProxy) - or - # or a "Secure Web Proxy", see https://www.chromium.org/developers/design-documents/secure-web-proxy - s(modes.HttpProxy, layers.ClientTLSLayer) - ): - return layers.HttpLayer(context, HTTPMode.regular) - # 3b. ... or an upstream proxy. - if ( - s(modes.HttpUpstreamProxy) - or - s(modes.HttpUpstreamProxy, layers.ClientTLSLayer) - ): - return layers.HttpLayer(context, HTTPMode.upstream) - - # 4. Check for --tcp - if self.is_destination_in_hosts(context, self.tcp_hosts): - return layers.TCPLayer(context) - - # 5. Check for raw tcp mode. - very_likely_http = context.client.alpn and context.client.alpn in HTTP_ALPNS - probably_no_http = not very_likely_http and ( - not data_client[ - :3 - ].isalpha() # the first three bytes should be the HTTP verb, so A-Za-z is expected. - or data_server # a server greeting would be uncharacteristic. - ) - if ctx.options.rawtcp and probably_no_http: - return layers.TCPLayer(context) - - # 6. Assume HTTP by default. - return layers.HttpLayer(context, HTTPMode.transparent) - - elif context.client.transport_protocol == "udp": - # unlike TCP, we make a decision immediately - try: - dtls_client_hello = dtls_parse_client_hello(data_client) - except ValueError: - dtls_client_hello = None - - # 1. check for --ignore/--allow - if self.ignore_connection( - context.server.address, - data_client, - is_tls=lambda _: dtls_client_hello is not None, - client_hello=lambda _: dtls_client_hello - ): - return layers.UDPLayer(context, ignore=True) + return stack[0] - # 2. Check for DTLS - if dtls_client_hello is not None: - return self.setup_tls_layer(context) + @staticmethod + def _is_destination_in_hosts(context: Context, hosts: Iterable[re.Pattern]) -> bool: + return any( + (context.server.address and rex.search(context.server.address[0])) + or (context.client.sni and rex.search(context.client.sni)) + for rex in hosts + ) - # 3. (skipped for now, until we support HTTP/3) - # 4. Check for --udp - if self.is_destination_in_hosts(context, self.udp_hosts): - return layers.UDPLayer(context) +# https://www.iana.org/assignments/quic/quic.xhtml +KNOWN_QUIC_VERSIONS = { + 0x00000001, # QUIC v1 + 0x51303433, # Google QUIC Q043 + 0x51303436, # Google QUIC Q046 + 0x51303530, # Google QUIC Q050 + 0x6B3343CF, # QUIC v2 + 0x709A50C4, # QUIC v2 draft codepoint +} - # 5. Check for DNS - try: - dns.Message.unpack(data_client) - except struct.error: - pass - else: - return layers.DNSLayer(context) +TYPICAL_QUIC_PORTS = {80, 443, 8443} - # 6. Check for raw udp mode. - if ctx.options.rawudp: - return layers.UDPLayer(context) - # 7. Ignore the connection by default. (In the future, we'll assume HTTP/3) - return layers.UDPLayer(context, ignore=True) +def _starts_like_quic(data_client: bytes, server_address: Address | None) -> bool: + """ + Make an educated guess on whether this could be QUIC. + This turns out to be quite hard in practice as 1-RTT packets are hardly distinguishable from noise. - else: - raise AssertionError(context.client.transport_protocol) + Returns: + True, if the passed bytes could be the start of a QUIC packet. + False, otherwise. + """ + # Minimum size: 1 flag byte + 1+ packet number bytes + 16+ bytes encrypted payload + if len(data_client) < 18: + return False + if starts_like_dtls_record(data_client): + return False + # TODO: Add more checks here to detect true negatives. + + # Long Header Packets + if data_client[0] & 0x80: + version = int.from_bytes(data_client[1:5], "big") + if version in KNOWN_QUIC_VERSIONS: + return True + # https://www.rfc-editor.org/rfc/rfc9000.html#name-versions + # Versions that follow the pattern 0x?a?a?a?a are reserved for use in forcing version negotiation + if version & 0x0F0F0F0F == 0x0A0A0A0A: + return True + else: + # ¯\_(ツ)_/¯ + # We can't even rely on the QUIC bit, see https://datatracker.ietf.org/doc/rfc9287/. + pass + + return bool(server_address and server_address[1] in TYPICAL_QUIC_PORTS) diff --git a/mitmproxy/addons/onboarding.py b/mitmproxy/addons/onboarding.py index 272068cf96..02cf4bd204 100644 --- a/mitmproxy/addons/onboarding.py +++ b/mitmproxy/addons/onboarding.py @@ -1,16 +1,15 @@ +from mitmproxy import ctx from mitmproxy.addons import asgiapp from mitmproxy.addons.onboardingapp import app -from mitmproxy import ctx APP_HOST = "mitm.it" -APP_PORT = 80 class Onboarding(asgiapp.WSGIApp): name = "onboarding" def __init__(self): - super().__init__(app, APP_HOST, APP_PORT) + super().__init__(app, APP_HOST, None) def load(self, loader): loader.add_option( @@ -25,13 +24,9 @@ def load(self, loader): entry for the app domain is not present. """, ) - loader.add_option( - "onboarding_port", int, APP_PORT, "Port to serve the onboarding app from." - ) def configure(self, updated): self.host = ctx.options.onboarding_host - self.port = ctx.options.onboarding_port app.config["CONFDIR"] = ctx.options.confdir async def request(self, f): diff --git a/mitmproxy/addons/onboardingapp/__init__.py b/mitmproxy/addons/onboardingapp/__init__.py index f8fafd20c2..a4aed19858 100644 --- a/mitmproxy/addons/onboardingapp/__init__.py +++ b/mitmproxy/addons/onboardingapp/__init__.py @@ -1,8 +1,10 @@ import os -from flask import Flask, render_template +from flask import Flask +from flask import render_template -from mitmproxy.options import CONF_BASENAME, CONF_DIR +from mitmproxy.options import CONF_BASENAME +from mitmproxy.options import CONF_DIR from mitmproxy.utils.magisk import write_magisk_module app = Flask(__name__) diff --git a/mitmproxy/addons/onboardingapp/templates/index.html b/mitmproxy/addons/onboardingapp/templates/index.html index 8f4a11e5ab..f96870fd06 100644 --- a/mitmproxy/addons/onboardingapp/templates/index.html +++ b/mitmproxy/addons/onboardingapp/templates/index.html @@ -50,6 +50,11 @@
Ubuntu/Debian
  • mv mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy.crt
  • sudo update-ca-certificates
  • +
    Fedora
    +
      +
    1. mv mitmproxy-ca-cert.pem /etc/pki/ca-trust/source/anchors/
    2. +
    3. sudo update-ca-trust
    4. +
    {% endcall %} {% call entry('macOS', 'apple') %}
    Manual Installation
    diff --git a/mitmproxy/addons/onboardingapp/templates/layout.html b/mitmproxy/addons/onboardingapp/templates/layout.html index 1048e36b97..26bb8eb427 100644 --- a/mitmproxy/addons/onboardingapp/templates/layout.html +++ b/mitmproxy/addons/onboardingapp/templates/layout.html @@ -5,9 +5,9 @@ mitmproxy - - - + + + diff --git a/mitmproxy/addons/proxyauth.py b/mitmproxy/addons/proxyauth.py index 96013d8d49..9250251da3 100644 --- a/mitmproxy/addons/proxyauth.py +++ b/mitmproxy/addons/proxyauth.py @@ -2,14 +2,16 @@ import binascii import weakref -from abc import ABC, abstractmethod -from typing import MutableMapping +from abc import ABC +from abc import abstractmethod +from collections.abc import MutableMapping from typing import Optional import ldap3 import passlib.apache -from mitmproxy import connection, ctx +from mitmproxy import connection +from mitmproxy import ctx from mitmproxy import exceptions from mitmproxy import http from mitmproxy.net.http import status_codes @@ -22,10 +24,10 @@ class ProxyAuth: validator: Validator | None = None - def __init__(self): - self.authenticated: MutableMapping[ - connection.Client, tuple[str, str] - ] = weakref.WeakKeyDictionary() + def __init__(self) -> None: + self.authenticated: MutableMapping[connection.Client, tuple[str, str]] = ( + weakref.WeakKeyDictionary() + ) """Contains all connections that are permanently authenticated after an HTTP CONNECT""" def load(self, loader): @@ -38,7 +40,7 @@ def load(self, loader): "username:pass", "any" to accept any user/pass combination, "@path" to use an Apache htpasswd file, - or "ldap[s]:url_server_ldap[:port]:dn_auth:password:dn_subtree" for LDAP authentication. + or "ldap[s]:url_server_ldap[:port]:dn_auth:password:dn_subtree[?search_filter_key=...]" for LDAP authentication. """, ) @@ -74,6 +76,8 @@ def requestheaders(self, f: http.HTTPFlow) -> None: # Is this connection authenticated by a previous HTTP CONNECT? if f.client_conn in self.authenticated: f.metadata["proxyauth"] = self.authenticated[f.client_conn] + elif f.is_replay: + pass else: self.authenticate_http(f) @@ -141,7 +145,9 @@ def is_http_proxy(f: http.HTTPFlow) -> bool: - True, if authentication is done as if mitmproxy is a proxy - False, if authentication is done as if mitmproxy is an HTTP server """ - return isinstance(f.client_conn.proxy_mode, (mode_specs.RegularMode, mode_specs.UpstreamMode)) + return isinstance( + f.client_conn.proxy_mode, (mode_specs.RegularMode, mode_specs.UpstreamMode) + ) def mkauth(username: str, password: str, scheme: str = "basic") -> str: @@ -209,6 +215,7 @@ class Ldap(Validator): conn: ldap3.Connection server: ldap3.Server dn_subtree: str + filter_key: str def __init__(self, proxyauth: str): ( @@ -218,6 +225,7 @@ def __init__(self, proxyauth: str): ldap_user, ldap_pass, self.dn_subtree, + self.filter_key, ) = self.parse_spec(proxyauth) server = ldap3.Server(url, port=port, use_ssl=use_ssl) conn = ldap3.Connection(server, ldap_user, ldap_pass, auto_bind=True) @@ -225,7 +233,7 @@ def __init__(self, proxyauth: str): self.server = server @staticmethod - def parse_spec(spec: str) -> tuple[bool, str, int | None, str, str, str]: + def parse_spec(spec: str) -> tuple[bool, str, int | None, str, str, str, str]: try: if spec.count(":") > 4: ( @@ -241,6 +249,16 @@ def parse_spec(spec: str) -> tuple[bool, str, int | None, str, str, str]: security, url, ldap_user, ldap_pass, dn_subtree = spec.split(":") port = None + if "?" in dn_subtree: + dn_subtree, search_str = dn_subtree.split("?") + key, value = search_str.split("=") + if key == "search_filter_key": + search_filter_key = value + else: + raise ValueError + else: + search_filter_key = "cn" + if security == "ldaps": use_ssl = True elif security == "ldap": @@ -248,14 +266,22 @@ def parse_spec(spec: str) -> tuple[bool, str, int | None, str, str, str]: else: raise ValueError - return use_ssl, url, port, ldap_user, ldap_pass, dn_subtree + return ( + use_ssl, + url, + port, + ldap_user, + ldap_pass, + dn_subtree, + search_filter_key, + ) except ValueError: raise exceptions.OptionsError(f"Invalid LDAP specification: {spec}") def __call__(self, username: str, password: str) -> bool: if not username or not password: return False - self.conn.search(self.dn_subtree, f"(cn={username})") + self.conn.search(self.dn_subtree, f"({self.filter_key}={username})") if self.conn.response: c = ldap3.Connection( self.server, self.conn.response[0]["dn"], password, auto_bind=True diff --git a/mitmproxy/addons/proxyserver.py b/mitmproxy/addons/proxyserver.py index 9d592ccd79..5b76e38d07 100644 --- a/mitmproxy/addons/proxyserver.py +++ b/mitmproxy/addons/proxyserver.py @@ -1,33 +1,42 @@ """ This addon is responsible for starting/stopping the proxy server sockets/instances specified by the mode option. """ + from __future__ import annotations import asyncio import collections import ipaddress import logging +from collections.abc import Iterable +from collections.abc import Iterator from contextlib import contextmanager -from typing import Iterable, Iterator, Optional +from typing import Optional from wsproto.frame_protocol import Opcode -from mitmproxy import ( - command, - ctx, - exceptions, - http, - platform, - tcp, - websocket, -) +from mitmproxy import command +from mitmproxy import ctx +from mitmproxy import exceptions +from mitmproxy import http +from mitmproxy import platform +from mitmproxy import tcp +from mitmproxy import udp +from mitmproxy import websocket from mitmproxy.connection import Address from mitmproxy.flow import Flow -from mitmproxy.proxy import events, mode_specs, server_hooks +from mitmproxy.proxy import events +from mitmproxy.proxy import mode_specs +from mitmproxy.proxy import server_hooks from mitmproxy.proxy.layers.tcp import TcpMessageInjected +from mitmproxy.proxy.layers.udp import UdpMessageInjected from mitmproxy.proxy.layers.websocket import WebSocketMessageInjected -from mitmproxy.proxy.mode_servers import ProxyConnectionHandler, ServerInstance, ServerManager -from mitmproxy.utils import human, signals +from mitmproxy.proxy.mode_servers import ProxyConnectionHandler +from mitmproxy.proxy.mode_servers import ServerInstance +from mitmproxy.proxy.mode_servers import ServerManager +from mitmproxy.utils import asyncio_utils +from mitmproxy.utils import human +from mitmproxy.utils import signals logger = logging.getLogger(__name__) @@ -62,10 +71,16 @@ async def update(self, modes: Iterable[mode_specs.ProxyMode]) -> bool: # Shutdown modes that have been removed from the list. stop_tasks = [ - s.stop() for spec, s in self._instances.items() + s.stop() + for spec, s in self._instances.items() if spec not in new_instances ] + if not start_tasks and not stop_tasks: + return ( + True # nothing to do, so we don't need to trigger `self.changed`. + ) + self._instances = new_instances # Notify listeners about the new not-yet-started servers. await self.changed.send() @@ -99,22 +114,28 @@ class Proxyserver(ServerManager): """ This addon runs the actual proxy server. """ - connections: dict[tuple, ProxyConnectionHandler] + + connections: dict[tuple | str, ProxyConnectionHandler] servers: Servers is_running: bool - _connect_addr: Optional[Address] = None + _connect_addr: Address | None = None + _update_task: asyncio.Task | None = None + _inject_tasks: set[asyncio.Task] def __init__(self): self.connections = {} self.servers = Servers(self) self.is_running = False + self._inject_tasks = set() def __repr__(self): return f"Proxyserver({len(self.connections)} active conns)" @contextmanager - def register_connection(self, connection_id: tuple, handler: ProxyConnectionHandler): + def register_connection( + self, connection_id: tuple | str, handler: ProxyConnectionHandler + ): self.connections[connection_id] = handler try: yield @@ -138,8 +159,9 @@ def load(self, loader): None, """ Stream data to the client if response body exceeds the given - threshold. If streamed, the body will not be stored in any way. - Understands k/m/g suffixes, i.e. 3m for 3 megabytes. + threshold. If streamed, the body will not be stored in any way, + and such responses cannot be modified. Understands k/m/g + suffixes, i.e. 3m for 3 megabytes. """, ) loader.add_option( @@ -195,7 +217,7 @@ def load(self, loader): def running(self): self.is_running = True - def configure(self, updated): + def configure(self, updated) -> None: if "stream_large_bodies" in updated: try: human.parse_size(ctx.options.stream_large_bodies) @@ -215,7 +237,10 @@ def configure(self, updated): if "connect_addr" in updated: try: if ctx.options.connect_addr: - self._connect_addr = str(ipaddress.ip_address(ctx.options.connect_addr)), 0 + self._connect_addr = ( + str(ipaddress.ip_address(ctx.options.connect_addr)), + 0, + ) else: self._connect_addr = None except ValueError: @@ -227,25 +252,31 @@ def configure(self, updated): modes: list[mode_specs.ProxyMode] = [] for mode in ctx.options.mode: try: - modes.append( - mode_specs.ProxyMode.parse(mode) - ) + modes.append(mode_specs.ProxyMode.parse(mode)) except ValueError as e: - raise exceptions.OptionsError(f"Invalid proxy mode specification: {mode} ({e})") + raise exceptions.OptionsError( + f"Invalid proxy mode specification: {mode} ({e})" + ) # ...and don't listen on the same address. - listen_addrs = [ - ( - m.listen_host(ctx.options.listen_host), - m.listen_port(ctx.options.listen_port), - m.transport_protocol - ) - for m in modes - ] + listen_addrs = [] + for m in modes: + if m.transport_protocol == "both": + protocols = ["tcp", "udp"] + else: + protocols = [m.transport_protocol] + host = m.listen_host(ctx.options.listen_host) + port = m.listen_port(ctx.options.listen_port) + if port is None: + continue + for proto in protocols: + listen_addrs.append((host, port, proto)) if len(set(listen_addrs)) != len(listen_addrs): (host, port, _) = collections.Counter(listen_addrs).most_common(1)[0][0] dup_addr = human.format_address((host or "0.0.0.0", port)) - raise exceptions.OptionsError(f"Cannot spawn multiple servers on the same address: {dup_addr}") + raise exceptions.OptionsError( + f"Cannot spawn multiple servers on the same address: {dup_addr}" + ) if ctx.options.mode and not ctx.master.addons.get("nextlayer"): logger.warning("Warning: Running proxyserver without nextlayer addon!") @@ -253,30 +284,45 @@ def configure(self, updated): if platform.original_addr: platform.init_transparent_mode() else: - raise exceptions.OptionsError("Transparent mode not supported on this platform.") + raise exceptions.OptionsError( + "Transparent mode not supported on this platform." + ) if self.is_running: - asyncio.create_task(self.servers.update(modes)) + self._update_task = asyncio_utils.create_task( + self.servers.update(modes), name="update servers" + ) async def setup_servers(self) -> bool: - return await self.servers.update([mode_specs.ProxyMode.parse(m) for m in ctx.options.mode]) + """Setup proxy servers. This may take an indefinite amount of time to complete (e.g. on permission prompts).""" + return await self.servers.update( + [mode_specs.ProxyMode.parse(m) for m in ctx.options.mode] + ) def listen_addrs(self) -> list[Address]: - return [ - addr - for server in self.servers - for addr in server.listen_addrs - ] + return [addr for server in self.servers for addr in server.listen_addrs] def inject_event(self, event: events.MessageInjected): - connection_id = ( - event.flow.client_conn.transport_protocol, - event.flow.client_conn.peername, - event.flow.client_conn.sockname, - ) + connection_id: str | tuple + if event.flow.client_conn.transport_protocol != "udp": + connection_id = event.flow.client_conn.id + else: # pragma: no cover + # temporary workaround: for UDP we don't have persistent client IDs yet. + connection_id = ( + event.flow.client_conn.peername, + event.flow.client_conn.sockname, + ) if connection_id not in self.connections: raise ValueError("Flow is not from a live connection.") - self.connections[connection_id].server_event(event) + + t = asyncio_utils.create_task( + self.connections[connection_id].server_event(event), + name=f"inject_event", + client=event.flow.client_conn.peername, + ) + # Python 3.11 Use TaskGroup instead. + self._inject_tasks.add(t) + t.add_done_callback(self._inject_tasks.remove) @command.command("inject.websocket") def inject_websocket( @@ -305,6 +351,17 @@ def inject_tcp(self, flow: Flow, to_client: bool, message: bytes): except ValueError as e: logger.warning(str(e)) + @command.command("inject.udp") + def inject_udp(self, flow: Flow, to_client: bool, message: bytes): + if not isinstance(flow, udp.UDPFlow): + logger.warning("Cannot inject UDP messages into non-UDP flows.") + + event = UdpMessageInjected(flow, udp.UDPMessage(not to_client, message)) + try: + self.inject_event(event) + except ValueError as e: + logger.warning(str(e)) + def server_connect(self, data: server_hooks.ServerConnectionHookData): if data.server.sockname is None: data.server.sockname = self._connect_addr @@ -317,12 +374,7 @@ def server_connect(self, data: server_hooks.ServerConnectionHookData): for listen_host, listen_port, *_ in server.listen_addrs: self_connect = ( connect_port == listen_port - and connect_host in ( - "localhost", - "127.0.0.1", - "::1", - listen_host - ) + and connect_host in ("localhost", "127.0.0.1", "::1", listen_host) and server.mode.transport_protocol == data.server.transport_protocol ) if self_connect: diff --git a/mitmproxy/addons/readfile.py b/mitmproxy/addons/readfile.py index f54708659e..1a702a91f0 100644 --- a/mitmproxy/addons/readfile.py +++ b/mitmproxy/addons/readfile.py @@ -2,13 +2,16 @@ import logging import os.path import sys -from typing import BinaryIO, Optional +from typing import BinaryIO +from typing import Optional +from mitmproxy import command from mitmproxy import ctx from mitmproxy import exceptions from mitmproxy import flowfilter from mitmproxy import io -from mitmproxy import command + +logger = logging.getLogger(__name__) class ReadFile: @@ -18,7 +21,7 @@ class ReadFile: def __init__(self): self.filter = None - self.is_reading = False + self._read_task: asyncio.Task | None = None def load(self, loader): loader.add_option("rfile", Optional[str], None, "Read flows from file.") @@ -63,22 +66,19 @@ async def load_flows_from_path(self, path: str) -> int: logging.error(f"Cannot load flows: {e}") raise exceptions.FlowReadException(str(e)) from e - async def doread(self, rfile): - self.is_reading = True + async def doread(self, rfile: str) -> None: try: - await self.load_flows_from_path(ctx.options.rfile) + await self.load_flows_from_path(rfile) except exceptions.FlowReadException as e: - raise exceptions.OptionsError(e) from e - finally: - self.is_reading = False + logger.exception(f"Failed to read {ctx.options.rfile}: {e}") def running(self): if ctx.options.rfile: - asyncio.get_running_loop().create_task(self.doread(ctx.options.rfile)) + self._read_task = asyncio.create_task(self.doread(ctx.options.rfile)) @command.command("readfile.reading") def reading(self) -> bool: - return self.is_reading + return bool(self._read_task and not self._read_task.done()) class ReadFileStdin(ReadFile): diff --git a/mitmproxy/addons/save.py b/mitmproxy/addons/save.py index 8faa188ee6..0103bde60a 100644 --- a/mitmproxy/addons/save.py +++ b/mitmproxy/addons/save.py @@ -5,10 +5,11 @@ from datetime import datetime from functools import lru_cache from pathlib import Path -from typing import Literal, Optional +from typing import Literal +from typing import Optional import mitmproxy.types -from mitmproxy import command, tcp, udp +from mitmproxy import command from mitmproxy import ctx from mitmproxy import dns from mitmproxy import exceptions @@ -16,6 +17,8 @@ from mitmproxy import flowfilter from mitmproxy import http from mitmproxy import io +from mitmproxy import tcp +from mitmproxy import udp from mitmproxy.log import ALERT @@ -38,10 +41,10 @@ def _mode(path: str) -> Literal["ab", "wb"]: class Save: def __init__(self) -> None: - self.stream: Optional[io.FilteredFlowWriter] = None - self.filt: Optional[flowfilter.TFilter] = None + self.stream: io.FilteredFlowWriter | None = None + self.filt: flowfilter.TFilter | None = None self.active_flows: set[flow.Flow] = set() - self.current_path: Optional[str] = None + self.current_path: str | None = None def load(self, loader): loader.add_option( @@ -77,6 +80,7 @@ def configure(self, updated): self.maybe_rotate_to_new_file() except OSError as e: raise exceptions.OptionsError(str(e)) from e + assert self.stream self.stream.flt = self.filt else: self.done() @@ -138,7 +142,13 @@ def save(self, flows: Sequence[flow.Flow], path: mitmproxy.types.Path) -> None: stream.add(i) except OSError as e: raise exceptions.CommandError(e) from e - logging.log(ALERT, f"Saved {len(flows)} flows.") + if path.endswith(".har") or path.endswith(".zhar"): # pragma: no cover + logging.log( + ALERT, + f"Saved as mitmproxy dump file. To save HAR files, use the `save.har` command.", + ) + else: + logging.log(ALERT, f"Saved {len(flows)} flows.") def tcp_start(self, flow: tcp.TCPFlow): if self.stream: diff --git a/mitmproxy/addons/savehar.py b/mitmproxy/addons/savehar.py new file mode 100644 index 0000000000..7d55e0c19b --- /dev/null +++ b/mitmproxy/addons/savehar.py @@ -0,0 +1,310 @@ +"""Write flow objects to a HAR file""" + +import base64 +import json +import logging +import zlib +from collections.abc import Sequence +from datetime import datetime +from datetime import timezone +from typing import Any + +from mitmproxy import command +from mitmproxy import ctx +from mitmproxy import exceptions +from mitmproxy import flow +from mitmproxy import flowfilter +from mitmproxy import http +from mitmproxy import types +from mitmproxy import version +from mitmproxy.addonmanager import Loader +from mitmproxy.connection import Server +from mitmproxy.coretypes.multidict import _MultiDict +from mitmproxy.log import ALERT +from mitmproxy.utils import human +from mitmproxy.utils import strutils + +logger = logging.getLogger(__name__) + + +class SaveHar: + def __init__(self) -> None: + self.flows: list[flow.Flow] = [] + self.filt: flowfilter.TFilter | None = None + + @command.command("save.har") + def export_har(self, flows: Sequence[flow.Flow], path: types.Path) -> None: + """Export flows to an HAR (HTTP Archive) file.""" + + har = json.dumps(self.make_har(flows), indent=4).encode() + + if path.endswith(".zhar"): + har = zlib.compress(har, 9) + + with open(path, "wb") as f: + f.write(har) + + logging.log(ALERT, f"HAR file saved ({human.pretty_size(len(har))} bytes).") + + def make_har(self, flows: Sequence[flow.Flow]) -> dict: + entries = [] + skipped = 0 + # A list of server seen till now is maintained so we can avoid + # using 'connect' time for entries that use an existing connection. + servers_seen: set[Server] = set() + + for f in flows: + if isinstance(f, http.HTTPFlow): + entries.append(self.flow_entry(f, servers_seen)) + else: + skipped += 1 + + if skipped > 0: + logger.info(f"Skipped {skipped} flows that weren't HTTP flows.") + + return { + "log": { + "version": "1.2", + "creator": { + "name": "mitmproxy", + "version": version.VERSION, + "comment": "", + }, + "pages": [], + "entries": entries, + } + } + + def load(self, loader: Loader): + loader.add_option( + "hardump", + str, + "", + """ + Save a HAR file with all flows on exit. + You may select particular flows by setting save_stream_filter. + For mitmdump, enabling this option will mean that flows are kept in memory. + """, + ) + + def configure(self, updated): + if "save_stream_filter" in updated: + if ctx.options.save_stream_filter: + try: + self.filt = flowfilter.parse(ctx.options.save_stream_filter) + except ValueError as e: + raise exceptions.OptionsError(str(e)) from e + else: + self.filt = None + + if "hardump" in updated: + if not ctx.options.hardump: + self.flows = [] + + def response(self, flow: http.HTTPFlow) -> None: + # websocket flows will receive a websocket_end, + # we don't want to persist them here already + if flow.websocket is None: + self._save_flow(flow) + + def error(self, flow: http.HTTPFlow) -> None: + self.response(flow) + + def websocket_end(self, flow: http.HTTPFlow) -> None: + self._save_flow(flow) + + def _save_flow(self, flow: http.HTTPFlow) -> None: + if ctx.options.hardump: + flow_matches = self.filt is None or self.filt(flow) + if flow_matches: + self.flows.append(flow) + + def done(self): + if ctx.options.hardump: + if ctx.options.hardump == "-": + har = self.make_har(self.flows) + print(json.dumps(har, indent=4)) + else: + self.export_har(self.flows, ctx.options.hardump) + + def flow_entry(self, flow: http.HTTPFlow, servers_seen: set[Server]) -> dict: + """Creates HAR entry from flow""" + + if flow.server_conn in servers_seen: + connect_time = -1.0 + ssl_time = -1.0 + elif flow.server_conn.timestamp_tcp_setup: + assert flow.server_conn.timestamp_start + connect_time = 1000 * ( + flow.server_conn.timestamp_tcp_setup - flow.server_conn.timestamp_start + ) + + if flow.server_conn.timestamp_tls_setup: + ssl_time = 1000 * ( + flow.server_conn.timestamp_tls_setup + - flow.server_conn.timestamp_tcp_setup + ) + else: + ssl_time = -1.0 + servers_seen.add(flow.server_conn) + else: + connect_time = -1.0 + ssl_time = -1.0 + + if flow.request.timestamp_end: + send = 1000 * (flow.request.timestamp_end - flow.request.timestamp_start) + else: + send = 0 + + if flow.response and flow.request.timestamp_end: + wait = 1000 * (flow.response.timestamp_start - flow.request.timestamp_end) + else: + wait = 0 + + if flow.response and flow.response.timestamp_end: + receive = 1000 * ( + flow.response.timestamp_end - flow.response.timestamp_start + ) + + else: + receive = 0 + + timings: dict[str, float | None] = { + "connect": connect_time, + "ssl": ssl_time, + "send": send, + "receive": receive, + "wait": wait, + } + + if flow.response: + try: + content = flow.response.content + except ValueError: + content = flow.response.raw_content + response_body_size = ( + len(flow.response.raw_content) if flow.response.raw_content else 0 + ) + response_body_decoded_size = len(content) if content else 0 + response_body_compression = response_body_decoded_size - response_body_size + response = { + "status": flow.response.status_code, + "statusText": flow.response.reason, + "httpVersion": flow.response.http_version, + "cookies": self.format_response_cookies(flow.response), + "headers": self.format_multidict(flow.response.headers), + "content": { + "size": response_body_size, + "compression": response_body_compression, + "mimeType": flow.response.headers.get("Content-Type", ""), + }, + "redirectURL": flow.response.headers.get("Location", ""), + "headersSize": len(str(flow.response.headers)), + "bodySize": response_body_size, + } + if content and strutils.is_mostly_bin(content): + response["content"]["text"] = base64.b64encode(content).decode() + response["content"]["encoding"] = "base64" + else: + text_content = flow.response.get_text(strict=False) + if text_content is None: + response["content"]["text"] = "" + else: + response["content"]["text"] = text_content + else: + response = { + "status": 0, + "statusText": "", + "httpVersion": "", + "headers": [], + "cookies": [], + "content": {}, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1, + "_transferSize": 0, + "_error": None, + } + if flow.error: + response["_error"] = flow.error.msg + + if flow.request.method == "CONNECT": + url = f"https://{flow.request.pretty_url}/" + else: + url = flow.request.pretty_url + + entry: dict[str, Any] = { + "startedDateTime": datetime.fromtimestamp( + flow.request.timestamp_start, timezone.utc + ).isoformat(), + "time": sum(v for v in timings.values() if v is not None and v >= 0), + "request": { + "method": flow.request.method, + "url": url, + "httpVersion": flow.request.http_version, + "cookies": self.format_multidict(flow.request.cookies), + "headers": self.format_multidict(flow.request.headers), + "queryString": self.format_multidict(flow.request.query), + "headersSize": len(str(flow.request.headers)), + "bodySize": len(flow.request.content) if flow.request.content else 0, + }, + "response": response, + "cache": {}, + "timings": timings, + } + + if flow.request.method in ["POST", "PUT", "PATCH"]: + params = self.format_multidict(flow.request.urlencoded_form) + entry["request"]["postData"] = { + "mimeType": flow.request.headers.get("Content-Type", ""), + "text": flow.request.get_text(strict=False), + "params": params, + } + + if flow.server_conn.peername: + entry["serverIPAddress"] = str(flow.server_conn.peername[0]) + + websocket_messages = [] + if flow.websocket: + for message in flow.websocket.messages: + if message.is_text: + data = message.text + else: + data = base64.b64encode(message.content).decode() + websocket_message = { + "type": "send" if message.from_client else "receive", + "time": message.timestamp, + "opcode": message.type.value, + "data": data, + } + websocket_messages.append(websocket_message) + + entry["_resourceType"] = "websocket" + entry["_webSocketMessages"] = websocket_messages + return entry + + def format_response_cookies(self, response: http.Response) -> list[dict]: + """Formats the response's cookie header to list of cookies""" + cookie_list = response.cookies.items(multi=True) + rv = [] + for name, (value, attrs) in cookie_list: + cookie = { + "name": name, + "value": value, + "path": attrs.get("path", "/"), + "domain": attrs.get("domain", ""), + "httpOnly": "httpOnly" in attrs, + "secure": "secure" in attrs, + } + # TODO: handle expires attribute here. + # This is not quite trivial because we need to parse random date formats. + # For now, we just ignore the attribute. + + if "sameSite" in attrs: + cookie["sameSite"] = attrs["sameSite"] + + rv.append(cookie) + return rv + + def format_multidict(self, obj: _MultiDict[str, str]) -> list[dict]: + return [{"name": k, "value": v} for k, v in obj.items(multi=True)] diff --git a/mitmproxy/addons/script.py b/mitmproxy/addons/script.py index d39fda4eb4..69a7bc805d 100644 --- a/mitmproxy/addons/script.py +++ b/mitmproxy/addons/script.py @@ -1,27 +1,26 @@ import asyncio +import importlib.machinery +import importlib.util import logging import os -import importlib.util -import importlib.machinery import sys import types -import traceback from collections.abc import Sequence -from typing import Optional -from mitmproxy import addonmanager, hooks -from mitmproxy import exceptions -from mitmproxy import flow +import mitmproxy.types as mtypes +from mitmproxy import addonmanager from mitmproxy import command -from mitmproxy import eventsequence from mitmproxy import ctx -import mitmproxy.types as mtypes +from mitmproxy import eventsequence +from mitmproxy import exceptions +from mitmproxy import flow +from mitmproxy import hooks from mitmproxy.utils import asyncio_utils logger = logging.getLogger(__name__) -def load_script(path: str) -> Optional[types.ModuleType]: +def load_script(path: str) -> types.ModuleType | None: fullname = "__mitmproxy_script__.{}".format( os.path.splitext(os.path.basename(path))[0] ) @@ -39,32 +38,35 @@ def load_script(path: str) -> Optional[types.ModuleType]: loader.exec_module(m) if not getattr(m, "name", None): m.name = path # type: ignore + except ImportError as e: + if getattr(sys, "frozen", False): + e.msg += ( + f".\n" + f"Note that mitmproxy's binaries include their own Python environment. " + f"If your addon requires the installation of additional dependencies, " + f"please install mitmproxy from PyPI " + f"(https://docs.mitmproxy.org/stable/overview-installation/#installation-from-the-python-package-index-pypi)." + ) + script_error_handler(path, e) except Exception as e: - script_error_handler(path, e, msg=str(e)) + script_error_handler(path, e) finally: sys.path[:] = oldpath return m -def script_error_handler(path, exc, msg="", tb=False): +def script_error_handler(path: str, exc: Exception) -> None: """ - Handles all the user's script errors with - an optional traceback + Log errors during script loading. """ - exception = type(exc).__name__ - if msg: - exception = msg - lineno = "" - if hasattr(exc, "lineno"): - lineno = str(exc.lineno) - log_msg = f"in script {path}:{lineno} {exception}" - if tb: - etype, value, tback = sys.exc_info() - tback = addonmanager.cut_traceback(tback, "invoke_addon_sync") - log_msg = ( - log_msg + "\n" + "".join(traceback.format_exception(etype, value, tback)) - ) - logger.error(log_msg) + tback = exc.__traceback__ + tback = addonmanager.cut_traceback( + tback, "invoke_addon_sync" + ) # we're calling configure() on load + tback = addonmanager.cut_traceback( + tback, "_call_with_frames_removed" + ) # module execution from importlib + logger.error(f"error in script {path}", exc_info=(type(exc), exc, tback)) ReloadInterval = 1 @@ -79,11 +81,11 @@ def __init__(self, path: str, reload: bool) -> None: self.name = "scriptmanager:" + path self.path = path self.fullpath = os.path.expanduser(path.strip("'\" ")) - self.ns = None + self.ns: types.ModuleType | None = None self.is_running = False if not os.path.isfile(self.fullpath): - raise exceptions.OptionsError("No such script") + raise exceptions.OptionsError(f"No such script: {self.fullpath}") self.reloadtask = None if reload: @@ -119,14 +121,17 @@ def loadscript(self): ctx.master.addons.invoke_addon_sync( self.ns, hooks.ConfigureHook(ctx.options.keys()) ) - except exceptions.OptionsError as e: - script_error_handler(self.fullpath, e, msg=str(e)) + except Exception as e: + script_error_handler(self.fullpath, e) if self.is_running: # We're already running, so we call that on the addon now. ctx.master.addons.invoke_addon_sync(self.ns, hooks.RunningHook()) async def watcher(self): - last_mtime = 0 + # Script loading is terminally confused at the moment. + # This here is a stopgap workaround to defer loading. + await asyncio.sleep(0) + last_mtime = 0.0 while True: try: mtime = os.stat(self.fullpath).st_mtime diff --git a/mitmproxy/addons/serverplayback.py b/mitmproxy/addons/serverplayback.py index 1973547c23..c5b13b1a20 100644 --- a/mitmproxy/addons/serverplayback.py +++ b/mitmproxy/addons/serverplayback.py @@ -1,16 +1,30 @@ import hashlib import logging import urllib -from collections.abc import Hashable, Sequence -from typing import Any, Optional +from collections.abc import Hashable +from collections.abc import Sequence +from typing import Any import mitmproxy.types -from mitmproxy import command, hooks -from mitmproxy import ctx, http +from mitmproxy import command +from mitmproxy import ctx from mitmproxy import exceptions from mitmproxy import flow +from mitmproxy import hooks +from mitmproxy import http from mitmproxy import io +logger = logging.getLogger(__name__) + +HASH_OPTIONS = [ + "server_replay_ignore_content", + "server_replay_ignore_host", + "server_replay_ignore_params", + "server_replay_ignore_payload_params", + "server_replay_ignore_port", + "server_replay_use_headers", +] + class ServerPlayback: flowmap: dict[Hashable, list[http.HTTPFlow]] @@ -25,10 +39,19 @@ def load(self, loader): "server_replay_kill_extra", bool, False, - "Kill extra requests during replay (for which no replayable response was found).", + "Kill extra requests during replay (for which no replayable response was found)." + "[Deprecated, prefer to use server_replay_extra='kill']", ) loader.add_option( - "server_replay_nopop", + "server_replay_extra", + str, + "forward", + "Behaviour for extra requests during replay for which no replayable response was found. " + "Setting a numeric string value will return an empty HTTP response with the respective status code.", + choices=["forward", "kill", "204", "400", "404", "500"], + ) + loader.add_option( + "server_replay_reuse", bool, False, """ @@ -36,6 +59,14 @@ def load(self, loader): possible to replay same response multiple times. """, ) + loader.add_option( + "server_replay_nopop", + bool, + False, + """ + Deprecated alias for `server_replay_reuse`. + """, + ) loader.add_option( "server_replay_refresh", bool, @@ -110,6 +141,13 @@ def load_flows(self, flows: Sequence[flow.Flow]) -> None: Replay server responses from flows. """ self.flowmap = {} + self.add_flows(flows) + + @command.command("replay.server.add") + def add_flows(self, flows: Sequence[flow.Flow]) -> None: + """ + Add responses from flows to server replay list. + """ for f in flows: if isinstance(f, http.HTTPFlow): lst = self.flowmap.setdefault(self._hash(f), []) @@ -184,14 +222,14 @@ def _hash(self, flow: http.HTTPFlow) -> Hashable: key.append(headers) return hashlib.sha256(repr(key).encode("utf8", "surrogateescape")).digest() - def next_flow(self, flow: http.HTTPFlow) -> Optional[http.HTTPFlow]: + def next_flow(self, flow: http.HTTPFlow) -> http.HTTPFlow | None: """ Returns the next flow object, or None if no matching flow was found. """ hash = self._hash(flow) if hash in self.flowmap: - if ctx.options.server_replay_nopop: + if ctx.options.server_replay_reuse or ctx.options.server_replay_nopop: return next( (flow for flow in self.flowmap[hash] if flow.response), None ) @@ -210,6 +248,15 @@ def next_flow(self, flow: http.HTTPFlow) -> Optional[http.HTTPFlow]: return None def configure(self, updated): + if ctx.options.server_replay_kill_extra: + logger.warning( + "server_replay_kill_extra has been deprecated, " + "please update your config to use server_replay_extra='kill'." + ) + if ctx.options.server_replay_nopop: # pragma: no cover + logger.error( + "server_replay_nopop has been renamed to server_replay_reuse, please update your config." + ) if not self.configured and ctx.options.server_replay: self.configured = True try: @@ -217,6 +264,16 @@ def configure(self, updated): except exceptions.FlowReadException as e: raise exceptions.OptionsError(str(e)) self.load_flows(flows) + if any(option in updated for option in HASH_OPTIONS): + self.recompute_hashes() + + def recompute_hashes(self) -> None: + """ + Rebuild flowmap if the hashing method has changed during execution, + see https://github.com/mitmproxy/mitmproxy/issues/4506 + """ + flows = [flow for lst in self.flowmap.values() for flow in lst] + self.load_flows(flows) def request(self, f: http.HTTPFlow) -> None: if self.flowmap: @@ -228,10 +285,21 @@ def request(self, f: http.HTTPFlow) -> None: response.refresh() f.response = response f.is_replay = "response" - elif ctx.options.server_replay_kill_extra: + elif ( + ctx.options.server_replay_kill_extra + or ctx.options.server_replay_extra == "kill" + ): logging.warning( "server_playback: killed non-replay request {}".format( f.request.url ) ) f.kill() + elif ctx.options.server_replay_extra != "forward": + logging.warning( + "server_playback: returned {} non-replay request {}".format( + ctx.options.server_replay_extra, f.request.url + ) + ) + f.response = http.Response.make(int(ctx.options.server_replay_extra)) + f.is_replay = "response" diff --git a/mitmproxy/addons/stickyauth.py b/mitmproxy/addons/stickyauth.py index 15d98c33d2..bd3b4e49d2 100644 --- a/mitmproxy/addons/stickyauth.py +++ b/mitmproxy/addons/stickyauth.py @@ -1,8 +1,8 @@ from typing import Optional +from mitmproxy import ctx from mitmproxy import exceptions from mitmproxy import flowfilter -from mitmproxy import ctx class StickyAuth: diff --git a/mitmproxy/addons/stickycookie.py b/mitmproxy/addons/stickycookie.py index df0abfbd98..11b4773bf5 100644 --- a/mitmproxy/addons/stickycookie.py +++ b/mitmproxy/addons/stickycookie.py @@ -2,7 +2,10 @@ from http import cookiejar from typing import Optional -from mitmproxy import http, flowfilter, ctx, exceptions +from mitmproxy import ctx +from mitmproxy import exceptions +from mitmproxy import flowfilter +from mitmproxy import http from mitmproxy.net.http import cookies TOrigin = tuple[str, int, str] @@ -30,9 +33,11 @@ def domain_match(a: str, b: str) -> bool: class StickyCookie: - def __init__(self): - self.jar: dict[TOrigin, dict[str, str]] = collections.defaultdict(dict) - self.flt: Optional[flowfilter.TFilter] = None + def __init__(self) -> None: + self.jar: collections.defaultdict[TOrigin, dict[str, str]] = ( + collections.defaultdict(dict) + ) + self.flt: flowfilter.TFilter | None = None def load(self, loader): loader.add_option( diff --git a/mitmproxy/addons/strip_dns_https_records.py b/mitmproxy/addons/strip_dns_https_records.py new file mode 100644 index 0000000000..b43383426d --- /dev/null +++ b/mitmproxy/addons/strip_dns_https_records.py @@ -0,0 +1,37 @@ +from mitmproxy import ctx +from mitmproxy import dns +from mitmproxy.net.dns import types + + +class StripDnsHttpsRecords: + def load(self, loader): + loader.add_option( + "strip_ech", + bool, + True, + "Strip Encrypted ClientHello (ECH) data from DNS HTTPS records so that mitmproxy can generate matching certificates.", + ) + + def dns_response(self, flow: dns.DNSFlow): + assert flow.response + if ctx.options.strip_ech: + for answer in flow.response.answers: + if answer.type == types.HTTPS: + answer.https_ech = None + if not ctx.options.http3: + for answer in flow.response.answers: + if ( + answer.type == types.HTTPS + and answer.https_alpn is not None + and any( + # HTTP/3 or any of the spec drafts (h3-...)? + a == b"h3" or a.startswith(b"h3-") + for a in answer.https_alpn + ) + ): + alpns = tuple( + a + for a in answer.https_alpn + if a != b"h3" and not a.startswith(b"h3-") + ) + answer.https_alpn = alpns or None diff --git a/mitmproxy/addons/termlog.py b/mitmproxy/addons/termlog.py index aa15ae39b4..0bf1b63848 100644 --- a/mitmproxy/addons/termlog.py +++ b/mitmproxy/addons/termlog.py @@ -1,19 +1,19 @@ from __future__ import annotations + import asyncio import logging -from typing import IO - import sys +from typing import IO -from mitmproxy import ctx, log +from mitmproxy import ctx +from mitmproxy import log from mitmproxy.utils import vt_codes class TermLog: - def __init__( - self, - out: IO[str] | None = None - ): + _teardown_task: asyncio.Task | None = None + + def __init__(self, out: IO[str] | None = None): self.logger = TermLogHandler(out) self.logger.install() @@ -27,24 +27,15 @@ def configure(self, updated): if "termlog_verbosity" in updated: self.logger.setLevel(ctx.options.termlog_verbosity.upper()) - def done(self): - t = self._teardown() - try: - # try to delay teardown a bit. - asyncio.create_task(t) - except RuntimeError: - # no event loop, we're in a test. - asyncio.run(t) - - async def _teardown(self): + def uninstall(self) -> None: + # uninstall the log dumper. + # This happens at the very very end after done() is completed, + # because we don't want to uninstall while other addons are still logging. self.logger.uninstall() class TermLogHandler(log.MitmLogHandler): - def __init__( - self, - out: IO[str] | None = None - ): + def __init__(self, out: IO[str] | None = None): super().__init__() self.file: IO[str] = out or sys.stdout self.has_vt_codes = vt_codes.ensure_supported(self.file) diff --git a/mitmproxy/addons/tlsconfig.py b/mitmproxy/addons/tlsconfig.py index ef015562f7..52003fc9ef 100644 --- a/mitmproxy/addons/tlsconfig.py +++ b/mitmproxy/addons/tlsconfig.py @@ -1,21 +1,36 @@ import ipaddress import logging import os +import ssl from pathlib import Path -from typing import Any, Optional, TypedDict - -from OpenSSL import SSL, crypto -from mitmproxy import certs, ctx, exceptions, connection, tls +from typing import Any +from typing import Literal +from typing import TypedDict + +from aioquic.h3.connection import H3_ALPN +from aioquic.tls import CipherSuite +from cryptography import x509 +from OpenSSL import crypto +from OpenSSL import SSL + +from mitmproxy import certs +from mitmproxy import connection +from mitmproxy import ctx +from mitmproxy import exceptions +from mitmproxy import tls from mitmproxy.net import tls as net_tls from mitmproxy.options import CONF_BASENAME from mitmproxy.proxy import context from mitmproxy.proxy.layers import modes +from mitmproxy.proxy.layers import quic from mitmproxy.proxy.layers import tls as proxy_tls +logger = logging.getLogger(__name__) + # We manually need to specify this, otherwise OpenSSL may select a non-HTTP2 cipher by default. # https://ssl-config.mozilla.org/#config=old -DEFAULT_CIPHERS = ( +_DEFAULT_CIPHERS = ( "ECDHE-ECDSA-AES128-GCM-SHA256", "ECDHE-RSA-AES128-GCM-SHA256", "ECDHE-ECDSA-AES256-GCM-SHA384", @@ -44,6 +59,22 @@ "DES-CBC3-SHA", ) +_DEFAULT_CIPHERS_WITH_SECLEVEL_0 = ("@SECLEVEL=0", *_DEFAULT_CIPHERS) + + +def _default_ciphers( + min_tls_version: net_tls.Version, +) -> tuple[str, ...]: + """ + @SECLEVEL=0 is necessary for TLS 1.1 and below to work, + see https://github.com/pyca/cryptography/issues/9523 + """ + if min_tls_version in net_tls.INSECURE_TLS_MIN_VERSIONS: + return _DEFAULT_CIPHERS_WITH_SECLEVEL_0 + else: + return _DEFAULT_CIPHERS + + # 2022/05: X509_CHECK_FLAG_NEVER_CHECK_SUBJECT is not available in LibreSSL, ignore gracefully as it's not critical. DEFAULT_HOSTFLAGS = ( SSL._lib.X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS # type: ignore @@ -52,8 +83,8 @@ class AppData(TypedDict): - client_alpn: Optional[bytes] - server_alpn: Optional[bytes] + client_alpn: bytes | None + server_alpn: bytes | None http2: bool @@ -95,8 +126,6 @@ class TlsConfig: # TODO: This addon should manage the following options itself, which are current defined in mitmproxy/options.py: # - upstream_cert # - add_upstream_certs_to_client_chain - # - ciphers_client - # - ciphers_server # - key_size # - certs # - cert_passphrase @@ -104,12 +133,17 @@ class TlsConfig: # - ssl_verify_upstream_trusted_confdir def load(self, loader): + insecure_tls_min_versions = ( + ", ".join(x.name for x in net_tls.INSECURE_TLS_MIN_VERSIONS[:-1]) + + f" and {net_tls.INSECURE_TLS_MIN_VERSIONS[-1].name}" + ) loader.add_option( name="tls_version_client_min", typespec=str, default=net_tls.DEFAULT_MIN_VERSION.name, choices=[x.name for x in net_tls.Version], - help=f"Set the minimum TLS version for client connections.", + help=f"Set the minimum TLS version for client connections. " + f"{insecure_tls_min_versions} are insecure.", ) loader.add_option( name="tls_version_client_max", @@ -123,7 +157,8 @@ def load(self, loader): typespec=str, default=net_tls.DEFAULT_MIN_VERSION.name, choices=[x.name for x in net_tls.Version], - help=f"Set the minimum TLS version for server connections.", + help=f"Set the minimum TLS version for server connections. " + f"{insecure_tls_min_versions} are insecure.", ) loader.add_option( name="tls_version_server_max", @@ -132,6 +167,38 @@ def load(self, loader): choices=[x.name for x in net_tls.Version], help=f"Set the maximum TLS version for server connections.", ) + loader.add_option( + name="tls_ecdh_curve_client", + typespec=str | None, + default=None, + help="Use a specific elliptic curve for ECDHE key exchange on client connections. " + 'OpenSSL syntax, for example "prime256v1" (see `openssl ecparam -list_curves`).', + ) + loader.add_option( + name="tls_ecdh_curve_server", + typespec=str | None, + default=None, + help="Use a specific elliptic curve for ECDHE key exchange on server connections. " + 'OpenSSL syntax, for example "prime256v1" (see `openssl ecparam -list_curves`).', + ) + loader.add_option( + name="request_client_cert", + typespec=bool, + default=False, + help=f"Requests a client certificate (TLS message 'CertificateRequest') to establish a mutual TLS connection between client and mitmproxy (combined with 'client_certs' option for mitmproxy and upstream).", + ) + loader.add_option( + "ciphers_client", + str | None, + None, + "Set supported ciphers for client <-> mitmproxy connections using OpenSSL syntax.", + ) + loader.add_option( + "ciphers_server", + str | None, + None, + "Set supported ciphers for mitmproxy <-> server connections using OpenSSL syntax.", + ) def tls_clienthello(self, tls_clienthello: tls.ClientHelloData): conn_context = tls_clienthello.context @@ -154,7 +221,9 @@ def tls_start_client(self, tls_start: tls.TlsData) -> None: if not client.cipher_list and ctx.options.ciphers_client: client.cipher_list = ctx.options.ciphers_client.split(":") # don't assign to client.cipher_list, doesn't need to be stored. - cipher_list = client.cipher_list or DEFAULT_CIPHERS + cipher_list = client.cipher_list or _default_ciphers( + net_tls.Version[ctx.options.tls_version_client_min] + ) if ctx.options.add_upstream_certs_to_client_chain: # pragma: no cover # exempted from coverage until https://bugs.python.org/issue18233 is fixed. @@ -163,12 +232,15 @@ def tls_start_client(self, tls_start: tls.TlsData) -> None: extra_chain_certs = [] ssl_ctx = net_tls.create_client_proxy_context( - method=net_tls.Method.DTLS_SERVER_METHOD if tls_start.is_dtls else net_tls.Method.TLS_SERVER_METHOD, + method=net_tls.Method.DTLS_SERVER_METHOD + if tls_start.is_dtls + else net_tls.Method.TLS_SERVER_METHOD, min_version=net_tls.Version[ctx.options.tls_version_client_min], max_version=net_tls.Version[ctx.options.tls_version_client_max], cipher_list=tuple(cipher_list), + ecdh_curve=ctx.options.tls_ecdh_curve_client, chain_file=entry.chain_file, - request_client_cert=False, + request_client_cert=ctx.options.request_client_cert, alpn_select_callback=alpn_select_callback, extra_chain_certs=tuple(extra_chain_certs), dhparams=self.certstore.dhparams, @@ -176,7 +248,9 @@ def tls_start_client(self, tls_start: tls.TlsData) -> None: tls_start.ssl_conn = SSL.Connection(ssl_ctx) tls_start.ssl_conn.use_certificate(entry.cert.to_pyopenssl()) - tls_start.ssl_conn.use_privatekey(crypto.PKey.from_cryptography_key(entry.privatekey)) + tls_start.ssl_conn.use_privatekey( + crypto.PKey.from_cryptography_key(entry.privatekey) + ) # Force HTTP/1 for secure web proxies, we currently don't support CONNECT over HTTP/2. # There is a proof-of-concept branch at https://github.com/mhils/mitmproxy/tree/http2-proxy, @@ -184,7 +258,7 @@ def tls_start_client(self, tls_start: tls.TlsData) -> None: if len(tls_start.context.layers) == 2 and isinstance( tls_start.context.layers[0], modes.HttpProxy ): - client_alpn: Optional[bytes] = b"http/1.1" + client_alpn: bytes | None = b"http/1.1" else: client_alpn = client.alpn @@ -239,9 +313,11 @@ def tls_start_server(self, tls_start: tls.TlsData) -> None: if not server.cipher_list and ctx.options.ciphers_server: server.cipher_list = ctx.options.ciphers_server.split(":") # don't assign to client.cipher_list, doesn't need to be stored. - cipher_list = server.cipher_list or DEFAULT_CIPHERS + cipher_list = server.cipher_list or _default_ciphers( + net_tls.Version[ctx.options.tls_version_server_min] + ) - client_cert: Optional[str] = None + client_cert: str | None = None if ctx.options.client_certs: client_certs = os.path.expanduser(ctx.options.client_certs) if os.path.isfile(client_certs): @@ -253,14 +329,18 @@ def tls_start_server(self, tls_start: tls.TlsData) -> None: client_cert = p ssl_ctx = net_tls.create_proxy_server_context( - method=net_tls.Method.DTLS_CLIENT_METHOD if tls_start.is_dtls else net_tls.Method.TLS_CLIENT_METHOD, + method=net_tls.Method.DTLS_CLIENT_METHOD + if tls_start.is_dtls + else net_tls.Method.TLS_CLIENT_METHOD, min_version=net_tls.Version[ctx.options.tls_version_server_min], max_version=net_tls.Version[ctx.options.tls_version_server_max], cipher_list=tuple(cipher_list), + ecdh_curve=ctx.options.tls_ecdh_curve_server, verify=verify, ca_path=ctx.options.ssl_verify_upstream_trusted_confdir, ca_pemfile=ctx.options.ssl_verify_upstream_trusted_ca, client_cert=client_cert, + legacy_server_connect=ctx.options.ssl_insecure, ) tls_start.ssl_conn = SSL.Connection(ssl_ctx) @@ -280,7 +360,9 @@ def tls_start_server(self, tls_start: tls.TlsData) -> None: except ValueError: host_name = server.sni.encode("idna") tls_start.ssl_conn.set_tlsext_host_name(host_name) - ok = SSL._lib.X509_VERIFY_PARAM_set1_host(param, host_name, len(host_name)) # type: ignore + ok = SSL._lib.X509_VERIFY_PARAM_set1_host( # type: ignore + param, host_name, len(host_name) + ) # type: ignore SSL._openssl_assert(ok == 1) # type: ignore else: # RFC 6066: Literal IPv4 and IPv6 addresses are not permitted in "HostName", @@ -295,86 +377,253 @@ def tls_start_server(self, tls_start: tls.TlsData) -> None: tls_start.ssl_conn.set_connect_state() + def quic_start_client(self, tls_start: quic.QuicTlsData) -> None: + """Establish QUIC between client and proxy.""" + if tls_start.settings is not None: + return # a user addon has already provided the settings. + tls_start.settings = quic.QuicTlsSettings() + + # keep the following part in sync with `tls_start_client` + assert isinstance(tls_start.conn, connection.Client) + + client: connection.Client = tls_start.conn + server: connection.Server = tls_start.context.server + + entry = self.get_cert(tls_start.context) + + if not client.cipher_list and ctx.options.ciphers_client: + client.cipher_list = ctx.options.ciphers_client.split(":") + + if ctx.options.add_upstream_certs_to_client_chain: # pragma: no cover + extra_chain_certs = server.certificate_list + else: + extra_chain_certs = [] + + # set context parameters + if client.cipher_list: + tls_start.settings.cipher_suites = [ + CipherSuite[cipher] for cipher in client.cipher_list + ] + # if we don't have upstream ALPN, we allow all offered by the client + tls_start.settings.alpn_protocols = [ + alpn.decode("ascii") + for alpn in [alpn for alpn in (client.alpn, server.alpn) if alpn] + or client.alpn_offers + ] + + # set the certificates + tls_start.settings.certificate = entry.cert._cert + tls_start.settings.certificate_private_key = entry.privatekey + tls_start.settings.certificate_chain = [ + cert._cert for cert in (*entry.chain_certs, *extra_chain_certs) + ] + + def quic_start_server(self, tls_start: quic.QuicTlsData) -> None: + """Establish QUIC between proxy and server.""" + if tls_start.settings is not None: + return # a user addon has already provided the settings. + tls_start.settings = quic.QuicTlsSettings() + + # keep the following part in sync with `tls_start_server` + assert isinstance(tls_start.conn, connection.Server) + + client: connection.Client = tls_start.context.client + server: connection.Server = tls_start.conn + assert server.address + + if ctx.options.ssl_insecure: + tls_start.settings.verify_mode = ssl.CERT_NONE + else: + tls_start.settings.verify_mode = ssl.CERT_REQUIRED + + if server.sni is None: + server.sni = client.sni or server.address[0] + + if not server.alpn_offers: + if client.alpn_offers: + server.alpn_offers = tuple(client.alpn_offers) + else: + # aioquic fails if no ALPN is offered, so use H3 + server.alpn_offers = tuple(alpn.encode("ascii") for alpn in H3_ALPN) + + if not server.cipher_list and ctx.options.ciphers_server: + server.cipher_list = ctx.options.ciphers_server.split(":") + + # set context parameters + if server.cipher_list: + tls_start.settings.cipher_suites = [ + CipherSuite[cipher] for cipher in server.cipher_list + ] + if server.alpn_offers: + tls_start.settings.alpn_protocols = [ + alpn.decode("ascii") for alpn in server.alpn_offers + ] + + # set the certificates + # NOTE client certificates are not supported + tls_start.settings.ca_path = ctx.options.ssl_verify_upstream_trusted_confdir + tls_start.settings.ca_file = ctx.options.ssl_verify_upstream_trusted_ca + def running(self): # FIXME: We have a weird bug where the contract for configure is not followed and it is never called with # confdir or command_history as updated. self.configure("confdir") # pragma: no cover def configure(self, updated): - if "confdir" not in updated and "certs" not in updated: - return - - certstore_path = os.path.expanduser(ctx.options.confdir) - self.certstore = certs.CertStore.from_store( - path=certstore_path, - basename=CONF_BASENAME, - key_size=ctx.options.key_size, - passphrase=ctx.options.cert_passphrase.encode("utf8") - if ctx.options.cert_passphrase - else None, - ) - if self.certstore.default_ca.has_expired(): - logging.warning( - "The mitmproxy certificate authority has expired!\n" - "Please delete all CA-related files in your ~/.mitmproxy folder.\n" - "The CA will be regenerated automatically after restarting mitmproxy.\n" - "See https://docs.mitmproxy.org/stable/concepts-certificates/ for additional help.", + if ( + "certs" in updated + or "confdir" in updated + or "key_size" in updated + or "cert_passphrase" in updated + ): + certstore_path = os.path.expanduser(ctx.options.confdir) + self.certstore = certs.CertStore.from_store( + path=certstore_path, + basename=CONF_BASENAME, + key_size=ctx.options.key_size, + passphrase=ctx.options.cert_passphrase.encode("utf8") + if ctx.options.cert_passphrase + else None, ) + if self.certstore.default_ca.has_expired(): + logger.warning( + "The mitmproxy certificate authority has expired!\n" + "Please delete all CA-related files in your ~/.mitmproxy folder.\n" + "The CA will be regenerated automatically after restarting mitmproxy.\n" + "See https://docs.mitmproxy.org/stable/concepts-certificates/ for additional help.", + ) - for certspec in ctx.options.certs: - parts = certspec.split("=", 1) - if len(parts) == 1: - parts = ["*", parts[0]] + for certspec in ctx.options.certs: + parts = certspec.split("=", 1) + if len(parts) == 1: + parts = ["*", parts[0]] - cert = Path(parts[1]).expanduser() - if not cert.exists(): - raise exceptions.OptionsError( - f"Certificate file does not exist: {cert}" - ) - try: - self.certstore.add_cert_file( - parts[0], - cert, - passphrase=ctx.options.cert_passphrase.encode("utf8") - if ctx.options.cert_passphrase - else None, + cert = Path(parts[1]).expanduser() + if not cert.exists(): + raise exceptions.OptionsError( + f"Certificate file does not exist: {cert}" + ) + try: + self.certstore.add_cert_file( + parts[0], + cert, + passphrase=ctx.options.cert_passphrase.encode("utf8") + if ctx.options.cert_passphrase + else None, + ) + except ValueError as e: + raise exceptions.OptionsError( + f"Invalid certificate format for {cert}: {e}" + ) from e + + if "tls_ecdh_curve_client" in updated or "tls_ecdh_curve_server" in updated: + for ecdh_curve in [ + ctx.options.tls_ecdh_curve_client, + ctx.options.tls_ecdh_curve_server, + ]: + if ecdh_curve is not None: + try: + crypto.get_elliptic_curve(ecdh_curve) + except Exception as e: + raise exceptions.OptionsError( + f"Invalid ECDH curve: {ecdh_curve!r}" + ) from e + + if "tls_version_client_min" in updated: + self._warn_unsupported_version("tls_version_client_min", True) + if "tls_version_client_max" in updated: + self._warn_unsupported_version("tls_version_client_max", False) + if "tls_version_server_min" in updated: + self._warn_unsupported_version("tls_version_server_min", True) + if "tls_version_server_max" in updated: + self._warn_unsupported_version("tls_version_server_max", False) + if "tls_version_client_min" in updated or "ciphers_client" in updated: + self._warn_seclevel_missing("client") + if "tls_version_server_min" in updated or "ciphers_server" in updated: + self._warn_seclevel_missing("server") + + def _warn_unsupported_version(self, attribute: str, warn_unbound: bool): + val = net_tls.Version[getattr(ctx.options, attribute)] + supported_versions = [ + v for v in net_tls.Version if net_tls.is_supported_version(v) + ] + supported_versions_str = ", ".join(v.name for v in supported_versions) + + if val is net_tls.Version.UNBOUNDED: + if warn_unbound: + logger.info( + f"{attribute} has been set to {val.name}. Note that your " + f"OpenSSL build only supports the following TLS versions: {supported_versions_str}" ) - except ValueError as e: - raise exceptions.OptionsError( - f"Invalid certificate format for {cert}: {e}" - ) from e + elif val not in supported_versions: + logger.warning( + f"{attribute} has been set to {val.name}, which is not supported by the current OpenSSL build. " + f"The current build only supports the following versions: {supported_versions_str}" + ) + + def _warn_seclevel_missing(self, side: Literal["client", "server"]) -> None: + """ + OpenSSL cipher spec need to specify @SECLEVEL for old TLS versions to work, + see https://github.com/pyca/cryptography/issues/9523. + """ + if side == "client": + custom_ciphers = ctx.options.ciphers_client + min_tls_version = ctx.options.tls_version_client_min + else: + custom_ciphers = ctx.options.ciphers_server + min_tls_version = ctx.options.tls_version_server_min + + if ( + custom_ciphers + and net_tls.Version[min_tls_version] in net_tls.INSECURE_TLS_MIN_VERSIONS + and "@SECLEVEL=0" not in custom_ciphers + ): + logger.warning( + f'With tls_version_{side}_min set to {min_tls_version}, ciphers_{side} must include "@SECLEVEL=0" ' + f"for insecure TLS versions to work." + ) def get_cert(self, conn_context: context.Context) -> certs.CertStoreEntry: """ This function determines the Common Name (CN), Subject Alternative Names (SANs) and Organization Name our certificate should have and then fetches a matching cert from the certstore. """ - altnames: list[str] = [] - organization: Optional[str] = None + altnames: list[x509.GeneralName] = [] + organization: str | None = None # Use upstream certificate if available. if ctx.options.upstream_cert and conn_context.server.certificate_list: upstream_cert = conn_context.server.certificate_list[0] if upstream_cert.cn: - altnames.append(upstream_cert.cn) + altnames.append(_ip_or_dns_name(upstream_cert.cn)) altnames.extend(upstream_cert.altnames) if upstream_cert.organization: organization = upstream_cert.organization - # Add SNI. If not available, try the server address as well. + # Add SNI or our local IP address. if conn_context.client.sni: - altnames.append(conn_context.client.sni) - elif conn_context.server.address: - altnames.append(conn_context.server.address[0]) + altnames.append(_ip_or_dns_name(conn_context.client.sni)) + else: + altnames.append(_ip_or_dns_name(conn_context.client.sockname[0])) - # As a last resort, add our local IP address. This may be necessary for HTTPS Proxies which are addressed - # via IP. Here we neither have an upstream cert, nor can an IP be included in the server name indication. - if not altnames: - altnames.append(conn_context.client.sockname[0]) + # If we already know of a server address, include that in the SANs as well. + if conn_context.server.address: + altnames.append(_ip_or_dns_name(conn_context.server.address[0])) # only keep first occurrence of each hostname altnames = list(dict.fromkeys(altnames)) # RFC 2818: If a subjectAltName extension of type dNSName is present, that MUST be used as the identity. # In other words, the Common Name is irrelevant then. - return self.certstore.get_cert(altnames[0], altnames, organization) + cn = next((str(x.value) for x in altnames), None) + return self.certstore.get_cert(cn, altnames, organization) + + +def _ip_or_dns_name(val: str) -> x509.GeneralName: + """Convert a string into either an x509.IPAddress or x509.DNSName object.""" + try: + ip = ipaddress.ip_address(val) + except ValueError: + return x509.DNSName(val.encode("idna").decode()) + else: + return x509.IPAddress(ip) diff --git a/mitmproxy/addons/update_alt_svc.py b/mitmproxy/addons/update_alt_svc.py new file mode 100644 index 0000000000..fa514e28b1 --- /dev/null +++ b/mitmproxy/addons/update_alt_svc.py @@ -0,0 +1,33 @@ +import re + +from mitmproxy import ctx +from mitmproxy.http import HTTPFlow +from mitmproxy.proxy import mode_specs + +ALT_SVC = "alt-svc" +HOST_PATTERN = r"([a-zA-Z0-9.-]*:\d{1,5})" + + +def update_alt_svc_header(header: str, port: int) -> str: + return re.sub(HOST_PATTERN, f":{port}", header) + + +class UpdateAltSvc: + def load(self, loader): + loader.add_option( + "keep_alt_svc_header", + bool, + False, + "Reverse Proxy: Keep Alt-Svc headers as-is, even if they do not point to mitmproxy. Enabling this option may cause clients to bypass the proxy.", + ) + + def responseheaders(self, flow: HTTPFlow): + assert flow.response + if ( + not ctx.options.keep_alt_svc_header + and isinstance(flow.client_conn.proxy_mode, mode_specs.ReverseMode) + and ALT_SVC in flow.response.headers + ): + _, listen_port, *_ = flow.client_conn.sockname + headers = flow.response.headers + headers[ALT_SVC] = update_alt_svc_header(headers[ALT_SVC], listen_port) diff --git a/mitmproxy/addons/upstream_auth.py b/mitmproxy/addons/upstream_auth.py index da9b395348..655ca7d752 100644 --- a/mitmproxy/addons/upstream_auth.py +++ b/mitmproxy/addons/upstream_auth.py @@ -1,9 +1,9 @@ -import re import base64 +import re from typing import Optional -from mitmproxy import exceptions from mitmproxy import ctx +from mitmproxy import exceptions from mitmproxy import http from mitmproxy.proxy import mode_specs from mitmproxy.utils import strutils @@ -27,7 +27,7 @@ class UpstreamAuth: - Reverse proxy regular requests (CONNECT is invalid in this mode) """ - auth: Optional[bytes] = None + auth: bytes | None = None def load(self, loader): loader.add_option( @@ -53,7 +53,10 @@ def http_connect_upstream(self, f: http.HTTPFlow): def requestheaders(self, f: http.HTTPFlow): if self.auth: - if isinstance(f.client_conn.proxy_mode, mode_specs.UpstreamMode) and f.request.scheme == "http": + if ( + isinstance(f.client_conn.proxy_mode, mode_specs.UpstreamMode) + and f.request.scheme == "http" + ): f.request.headers["Proxy-Authorization"] = self.auth elif isinstance(f.client_conn.proxy_mode, mode_specs.ReverseMode): f.request.headers["Authorization"] = self.auth diff --git a/mitmproxy/addons/view.py b/mitmproxy/addons/view.py index 7b683d4a24..5561be5720 100644 --- a/mitmproxy/addons/view.py +++ b/mitmproxy/addons/view.py @@ -8,29 +8,33 @@ - Exposes a settings store for flows that automatically expires if the flow is removed from the store. """ + import collections import logging import re -from collections.abc import Iterator, MutableMapping, Sequence -from typing import Any, Optional +from collections.abc import Iterator +from collections.abc import MutableMapping +from collections.abc import Sequence +from typing import Any +from typing import Optional import sortedcontainers import mitmproxy.flow from mitmproxy import command +from mitmproxy import connection from mitmproxy import ctx from mitmproxy import dns from mitmproxy import exceptions -from mitmproxy import hooks -from mitmproxy import connection from mitmproxy import flowfilter +from mitmproxy import hooks from mitmproxy import http from mitmproxy import io from mitmproxy import tcp from mitmproxy import udp from mitmproxy.log import ALERT -from mitmproxy.utils import human, signals - +from mitmproxy.utils import human +from mitmproxy.utils import signals # The underlying sorted list implementation expects the sort key to be stable # for the lifetime of the object. However, if we sort by size, for instance, @@ -133,18 +137,18 @@ def generate(self, f: mitmproxy.flow.Flow) -> int: ] -def _signal_with_flow(flow: mitmproxy.flow.Flow) -> None: - ... +def _signal_with_flow(flow: mitmproxy.flow.Flow) -> None: ... -def _sig_view_remove(flow: mitmproxy.flow.Flow, index: int) -> None: - ... +def _sig_view_remove(flow: mitmproxy.flow.Flow, index: int) -> None: ... class View(collections.abc.Sequence): - def __init__(self): + def __init__(self) -> None: super().__init__() - self._store = collections.OrderedDict() + self._store: collections.OrderedDict[str, mitmproxy.flow.Flow] = ( + collections.OrderedDict() + ) self.filter = flowfilter.match_all # Should we show only marked flows? self.show_marked = False @@ -156,7 +160,7 @@ def __init__(self): url=OrderRequestURL(self), size=OrderKeySize(self), ) - self.order_key = self.default_order + self.order_key: _OrderKey = self.default_order self.order_reversed = False self.focus_follow = False @@ -229,7 +233,7 @@ def _bisect(self, f: mitmproxy.flow.Flow) -> int: return self._rev(v - 1) + 1 def index( - self, f: mitmproxy.flow.Flow, start: int = 0, stop: Optional[int] = None + self, f: mitmproxy.flow.Flow, start: int = 0, stop: int | None = None ) -> int: return self._rev(self._view.index(f, start, stop)) @@ -316,9 +320,9 @@ def set_order(self, order_key: str) -> None: """ if order_key not in self.orders: raise exceptions.CommandError("Unknown flow order: %s" % order_key) - order_key = self.orders[order_key] - self.order_key = order_key - newview = sortedcontainers.SortedListWithKey(key=order_key) + key = self.orders[order_key] + self.order_key = key + newview = sortedcontainers.SortedListWithKey(key=key) newview.update(self._view) self._view = newview @@ -347,7 +351,7 @@ def set_filter_cmd(self, filter_expr: str) -> None: raise exceptions.CommandError(str(e)) from e self.set_filter(filt) - def set_filter(self, flt: Optional[flowfilter.TFilter]): + def set_filter(self, flt: flowfilter.TFilter | None): self.filter = flt or flowfilter.match_all self._refilter() @@ -475,8 +479,12 @@ def create(self, method: str, url: str) -> None: except ValueError as e: raise exceptions.CommandError("Invalid URL: %s" % e) - c = connection.Client(("", 0), ("", 0), req.timestamp_start - 0.0001) - s = connection.Server((req.host, req.port)) + c = connection.Client( + peername=("", 0), + sockname=("", 0), + timestamp_start=req.timestamp_start - 0.0001, + ) + s = connection.Server(address=(req.host, req.port)) f = http.HTTPFlow(c, s) f.request = req @@ -514,7 +522,7 @@ def add(self, flows: Sequence[mitmproxy.flow.Flow]) -> None: self.focus.flow = f self.sig_view_add.send(flow=f) - def get_by_id(self, flow_id: str) -> Optional[mitmproxy.flow.Flow]: + def get_by_id(self, flow_id: str) -> mitmproxy.flow.Flow | None: """ Get flow with the given id from the store. Returns None if the flow is not found. @@ -659,7 +667,7 @@ class Focus: def __init__(self, v: View) -> None: self.view = v - self._flow: Optional[mitmproxy.flow.Flow] = None + self._flow: mitmproxy.flow.Flow | None = None self.sig_change = signals.SyncSignal(lambda: None) if len(self.view): self.flow = self.view[0] @@ -668,18 +676,18 @@ def __init__(self, v: View) -> None: v.sig_view_refresh.connect(self._sig_view_refresh) @property - def flow(self) -> Optional[mitmproxy.flow.Flow]: + def flow(self) -> mitmproxy.flow.Flow | None: return self._flow @flow.setter - def flow(self, f: Optional[mitmproxy.flow.Flow]): + def flow(self, f: mitmproxy.flow.Flow | None): if f is not None and f not in self.view: raise ValueError("Attempt to set focus to flow not in view") self._flow = f self.sig_change.send() @property - def index(self) -> Optional[int]: + def index(self) -> int | None: if self.flow: return self.view.index(self.flow) return None diff --git a/mitmproxy/certs.py b/mitmproxy/certs.py index dc3787a8b7..a5a81ea1ae 100644 --- a/mitmproxy/certs.py +++ b/mitmproxy/certs.py @@ -1,22 +1,34 @@ import contextlib import datetime import ipaddress +import logging import os -import re import sys +import warnings +from collections.abc import Iterable from dataclasses import dataclass from pathlib import Path -from typing import NewType, Optional, Union +from typing import cast +from typing import NewType +from typing import Optional +from typing import Union +import OpenSSL from cryptography import x509 -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import rsa, dsa, ec +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import dsa +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.asymmetric.types import CertificatePublicKeyTypes from cryptography.hazmat.primitives.serialization import pkcs12 -from cryptography.x509 import NameOID, ExtendedKeyUsageOID +from cryptography.x509 import ExtendedKeyUsageOID +from cryptography.x509 import NameOID -import OpenSSL from mitmproxy.coretypes import serializable +logger = logging.getLogger(__name__) + # Default expiry must not be too long: https://github.com/mitmproxy/mitmproxy/issues/815 CA_EXPIRY = datetime.timedelta(days=10 * 365) CERT_EXPIRY = datetime.timedelta(days=365) @@ -52,7 +64,8 @@ def __eq__(self, other): return self.fingerprint() == other.fingerprint() def __repr__(self): - return f"" + altnames = [str(x.value) for x in self.altnames] + return f"" def __hash__(self): return self._cert.__hash__() @@ -82,6 +95,9 @@ def from_pyopenssl(self, x509: OpenSSL.crypto.X509) -> "Cert": def to_pyopenssl(self) -> OpenSSL.crypto.X509: return OpenSSL.crypto.X509.from_cryptography(self._cert) + def public_key(self) -> CertificatePublicKeyTypes: + return self._cert.public_key() + def fingerprint(self) -> bytes: return self._cert.fingerprint(hashes.SHA256()) @@ -91,16 +107,24 @@ def issuer(self) -> list[tuple[str, str]]: @property def notbefore(self) -> datetime.datetime: - # x509.Certificate.not_valid_before is a naive datetime in UTC - return self._cert.not_valid_before.replace(tzinfo=datetime.timezone.utc) + try: + # type definitions haven't caught up with new API yet. + return self._cert.not_valid_before_utc # type: ignore + except AttributeError: # pragma: no cover + # cryptography < 42.0 + return self._cert.not_valid_before.replace(tzinfo=datetime.timezone.utc) @property def notafter(self) -> datetime.datetime: - # x509.Certificate.not_valid_after is a naive datetime in UTC - return self._cert.not_valid_after.replace(tzinfo=datetime.timezone.utc) + try: + return self._cert.not_valid_after_utc # type: ignore + except AttributeError: # pragma: no cover + return self._cert.not_valid_after.replace(tzinfo=datetime.timezone.utc) def has_expired(self) -> bool: - return datetime.datetime.utcnow() > self._cert.not_valid_after + if sys.version_info < (3, 11): # pragma: no cover + return datetime.datetime.now(datetime.timezone.utc) > self.notafter + return datetime.datetime.now(datetime.UTC) > self.notafter @property def subject(self) -> list[tuple[str, str]]: @@ -110,6 +134,17 @@ def subject(self) -> list[tuple[str, str]]: def serial(self) -> int: return self._cert.serial_number + @property + def is_ca(self) -> bool: + constraints: x509.BasicConstraints + try: + constraints = self._cert.extensions.get_extension_for_class( + x509.BasicConstraints + ).value + return constraints.ca + except x509.ExtensionNotFound: + return False + @property def keyinfo(self) -> tuple[str, int]: public_key = self._cert.public_key() @@ -125,47 +160,41 @@ def keyinfo(self) -> tuple[str, int]: ) # pragma: no cover @property - def cn(self) -> Optional[str]: + def cn(self) -> str | None: attrs = self._cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME) if attrs: - return attrs[0].value + return cast(str, attrs[0].value) return None @property - def organization(self) -> Optional[str]: + def organization(self) -> str | None: attrs = self._cert.subject.get_attributes_for_oid( x509.NameOID.ORGANIZATION_NAME ) if attrs: - return attrs[0].value + return cast(str, attrs[0].value) return None @property - def altnames(self) -> list[str]: + def altnames(self) -> x509.GeneralNames: """ Get all SubjectAlternativeName DNS altnames. """ try: - ext = self._cert.extensions.get_extension_for_class( + sans = self._cert.extensions.get_extension_for_class( x509.SubjectAlternativeName ).value except x509.ExtensionNotFound: - return [] + return x509.GeneralNames([]) else: - return ext.get_values_for_type(x509.DNSName) + [ - str(x) for x in ext.get_values_for_type(x509.IPAddress) - ] + return x509.GeneralNames(sans) def _name_to_keyval(name: x509.Name) -> list[tuple[str, str]]: parts = [] for attr in name: - # pyca cryptography <35.0.0 backwards compatiblity - if hasattr(name, "rfc4514_attribute_name"): # pragma: no cover - k = attr.rfc4514_attribute_name # type: ignore - else: # pragma: no cover - k = attr.rfc4514_string().partition("=")[0] - v = attr.value + k = attr.rfc4514_string().partition("=")[0] + v = cast(str, attr.value) parts.append((k, v)) return parts @@ -222,12 +251,42 @@ def create_ca( return private_key, cert +def _fix_legacy_sans(sans: Iterable[x509.GeneralName] | list[str]) -> x509.GeneralNames: + """ + SANs used to be a list of strings in mitmproxy 10.1 and below, but now they're a list of GeneralNames. + This function converts the old format to the new one. + """ + if isinstance(sans, x509.GeneralNames): + return sans + elif ( + isinstance(sans, list) and len(sans) > 0 and isinstance(sans[0], str) + ): # pragma: no cover + warnings.warn( + "Passing SANs as a list of strings is deprecated.", + DeprecationWarning, + stacklevel=2, + ) + + ss: list[x509.GeneralName] = [] + for x in cast(list[str], sans): + try: + ip = ipaddress.ip_address(x) + except ValueError: + x = x.encode("idna").decode() + ss.append(x509.DNSName(x)) + else: + ss.append(x509.IPAddress(ip)) + return x509.GeneralNames(ss) + else: + return x509.GeneralNames(sans) + + def dummy_cert( privkey: rsa.RSAPrivateKey, cacert: x509.Certificate, - commonname: Optional[str], - sans: list[str], - organization: Optional[str] = None, + commonname: str | None, + sans: Iterable[x509.GeneralName], + organization: str | None = None, ) -> Cert: """ Generates a dummy certificate. @@ -262,18 +321,22 @@ def dummy_cert( builder = builder.subject_name(x509.Name(subject)) builder = builder.serial_number(x509.random_serial_number()) - ss: list[x509.GeneralName] = [] - for x in sans: - try: - ip = ipaddress.ip_address(x) - except ValueError: - ss.append(x509.DNSName(x)) - else: - ss.append(x509.IPAddress(ip)) # RFC 5280 §4.2.1.6: subjectAltName is critical if subject is empty. builder = builder.add_extension( - x509.SubjectAlternativeName(ss), critical=not is_valid_commonname + x509.SubjectAlternativeName(_fix_legacy_sans(sans)), + critical=not is_valid_commonname, + ) + + # https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.1 + builder = builder.add_extension( + x509.AuthorityKeyIdentifier.from_issuer_public_key(cacert.public_key()), + critical=False, ) + # If CA and leaf cert have the same Subject Key Identifier, SChannel breaks in funny ways, + # see https://github.com/mitmproxy/mitmproxy/issues/6494. + # https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.2 states + # that SKI is optional for the leaf cert, so we skip that. + cert = builder.sign(private_key=privkey, algorithm=hashes.SHA256()) # type: ignore return Cert(cert) @@ -282,11 +345,12 @@ def dummy_cert( class CertStoreEntry: cert: Cert privatekey: rsa.RSAPrivateKey - chain_file: Optional[Path] + chain_file: Path | None + chain_certs: list[Cert] TCustomCertId = str # manually provided certs (e.g. mitmproxy's --certs) -TGeneratedCertId = tuple[Optional[str], tuple[str, ...]] # (common_name, sans) +TGeneratedCertId = tuple[Optional[str], x509.GeneralNames] # (common_name, sans) TCertId = Union[TCustomCertId, TGeneratedCertId] DHParams = NewType("DHParams", bytes) @@ -305,12 +369,17 @@ def __init__( self, default_privatekey: rsa.RSAPrivateKey, default_ca: Cert, - default_chain_file: Optional[Path], + default_chain_file: Path | None, dhparams: DHParams, ): self.default_privatekey = default_privatekey self.default_ca = default_ca self.default_chain_file = default_chain_file + self.default_chain_certs = ( + x509.load_pem_x509_certificates(self.default_chain_file.read_bytes()) + if self.default_chain_file + else [default_ca] + ) self.dhparams = dhparams self.certs = {} self.expire_queue = [] @@ -330,7 +399,9 @@ def load_dhparam(path: Path) -> DHParams: # we could use cryptography for this, but it's unclear how to convert cryptography's object to pyOpenSSL's # expected format. - bio = OpenSSL.SSL._lib.BIO_new_file(str(path).encode(sys.getfilesystemencoding()), b"r") # type: ignore + bio = OpenSSL.SSL._lib.BIO_new_file( # type: ignore + str(path).encode(sys.getfilesystemencoding()), b"r" + ) if bio != OpenSSL.SSL._ffi.NULL: # type: ignore bio = OpenSSL.SSL._ffi.gc(bio, OpenSSL.SSL._lib.BIO_free) # type: ignore dh = OpenSSL.SSL._lib.PEM_read_bio_DHparams( # type: ignore @@ -346,10 +417,10 @@ def load_dhparam(path: Path) -> DHParams: @classmethod def from_store( cls, - path: Union[Path, str], + path: Path | str, basename: str, key_size: int, - passphrase: Optional[bytes] = None, + passphrase: bytes | None = None, ) -> "CertStore": path = Path(path) ca_file = path / f"{basename}-ca.pem" @@ -360,15 +431,15 @@ def from_store( @classmethod def from_files( - cls, ca_file: Path, dhparam_file: Path, passphrase: Optional[bytes] = None + cls, ca_file: Path, dhparam_file: Path, passphrase: bytes | None = None ) -> "CertStore": raw = ca_file.read_bytes() key = load_pem_private_key(raw, passphrase) dh = cls.load_dhparam(dhparam_file) - certs = re.split(rb"(?=-----BEGIN CERTIFICATE-----)", raw) - ca = Cert.from_pem(certs[1]) - if len(certs) > 2: - chain_file: Optional[Path] = ca_file + certs = x509.load_pem_x509_certificates(raw) + ca = Cert(certs[0]) + if len(certs) > 1: + chain_file: Path | None = ca_file else: chain_file = None return cls(key, ca, chain_file, dh) @@ -444,16 +515,39 @@ def create_store( (path / f"{basename}-dhparam.pem").write_bytes(DEFAULT_DHPARAM) def add_cert_file( - self, spec: str, path: Path, passphrase: Optional[bytes] = None + self, spec: str, path: Path, passphrase: bytes | None = None ) -> None: raw = path.read_bytes() cert = Cert.from_pem(raw) try: - key = load_pem_private_key(raw, password=passphrase) - except ValueError: - key = self.default_privatekey + private_key = load_pem_private_key(raw, password=passphrase) + except ValueError as e: + private_key = self.default_privatekey + if cert.public_key() != private_key.public_key(): + raise ValueError( + f'Unable to find private key in "{path.absolute()}": {e}' + ) from e + else: + if cert.public_key() != private_key.public_key(): + raise ValueError( + f'Private and public keys in "{path.absolute()}" do not match:\n' + f"{cert.public_key()=}\n" + f"{private_key.public_key()=}" + ) + + try: + chain = [Cert(x) for x in x509.load_pem_x509_certificates(raw)] + except ValueError as e: + logger.warning(f"Failed to read certificate chain: {e}") + chain = [cert] + + if cert.is_ca: + logger.warning( + f'"{path.absolute()}" is a certificate authority and not a leaf certificate. ' + f"This indicates a misconfiguration, see https://docs.mitmproxy.org/stable/concepts-certificates/." + ) - self.add_cert(CertStoreEntry(cert, key, path), spec) + self.add_cert(CertStoreEntry(cert, private_key, path, chain), spec) def add_cert(self, entry: CertStoreEntry, *names: str) -> None: """ @@ -463,27 +557,32 @@ def add_cert(self, entry: CertStoreEntry, *names: str) -> None: if entry.cert.cn: self.certs[entry.cert.cn] = entry for i in entry.cert.altnames: - self.certs[i] = entry + self.certs[str(i.value)] = entry for i in names: self.certs[i] = entry @staticmethod - def asterisk_forms(dn: str) -> list[str]: + def asterisk_forms(dn: str | x509.GeneralName) -> list[str]: """ Return all asterisk forms for a domain. For example, for www.example.com this will return [b"www.example.com", b"*.example.com", b"*.com"]. The single wildcard "*" is omitted. """ - parts = dn.split(".") - ret = [dn] - for i in range(1, len(parts)): - ret.append("*." + ".".join(parts[i:])) - return ret + if isinstance(dn, str): + parts = dn.split(".") + ret = [dn] + for i in range(1, len(parts)): + ret.append("*." + ".".join(parts[i:])) + return ret + elif isinstance(dn, x509.DNSName): + return CertStore.asterisk_forms(dn.value) + else: + return [str(dn.value)] def get_cert( self, - commonname: Optional[str], - sans: list[str], - organization: Optional[str] = None, + commonname: str | None, + sans: Iterable[x509.GeneralName], + organization: str | None = None, ) -> CertStoreEntry: """ commonname: Common name for the generated certificate. Must be a @@ -493,6 +592,7 @@ def get_cert( organization: Organization name for the generated certificate. """ + sans = _fix_legacy_sans(sans) potential_keys: list[TCertId] = [] if commonname: @@ -500,7 +600,7 @@ def get_cert( for s in sans: potential_keys.extend(self.asterisk_forms(s)) potential_keys.append("*") - potential_keys.append((commonname, tuple(sans))) + potential_keys.append((commonname, sans)) name = next(filter(lambda key: key in self.certs, potential_keys), None) if name: @@ -516,14 +616,15 @@ def get_cert( ), privatekey=self.default_privatekey, chain_file=self.default_chain_file, + chain_certs=self.default_chain_certs, ) - self.certs[(commonname, tuple(sans))] = entry + self.certs[(commonname, sans)] = entry self.expire(entry) return entry -def load_pem_private_key(data: bytes, password: Optional[bytes]) -> rsa.RSAPrivateKey: +def load_pem_private_key(data: bytes, password: bytes | None) -> rsa.RSAPrivateKey: """ like cryptography's load_pem_private_key, but silently falls back to not using a password if the private key is unencrypted. diff --git a/mitmproxy/command.py b/mitmproxy/command.py index 950fa44ef3..822c40eb64 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -1,24 +1,29 @@ """ - This module manages and invokes typed commands. +This module manages and invokes typed commands. """ + import functools import inspect import logging - -import pyparsing import sys import textwrap import types -from collections.abc import Sequence, Callable, Iterable -from typing import Any, NamedTuple, Optional +from collections.abc import Callable +from collections.abc import Iterable +from collections.abc import Sequence +from typing import Any +from typing import NamedTuple + +import pyparsing import mitmproxy.types -from mitmproxy import exceptions, command_lexer +from mitmproxy import command_lexer +from mitmproxy import exceptions from mitmproxy.command_lexer import unquote def verify_arg_signature(f: Callable, args: Iterable[Any], kwargs: dict) -> None: - sig = inspect.signature(f) + sig = inspect.signature(f, eval_str=True) try: sig.bind(*args, **kwargs) except TypeError as v: @@ -61,13 +66,13 @@ class Command: name: str manager: "CommandManager" signature: inspect.Signature - help: Optional[str] + help: str | None def __init__(self, manager: "CommandManager", name: str, func: Callable) -> None: self.name = name self.manager = manager self.func = func - self.signature = inspect.signature(self.func) + self.signature = inspect.signature(self.func, eval_str=True) if func.__doc__: txt = func.__doc__.strip() @@ -90,7 +95,7 @@ def __init__(self, manager: "CommandManager", name: str, func: Callable) -> None ) @property - def return_type(self) -> Optional[type]: + def return_type(self) -> type | None: return _empty_as_none(self.signature.return_annotation) @property @@ -195,14 +200,16 @@ def parse_partial( Parse a possibly partial command. Return a sequence of ParseResults and a sequence of remainder type help items. """ - parts: pyparsing.ParseResults = command_lexer.expr.parseString(cmdstr, parseAll=True) + parts: pyparsing.ParseResults = command_lexer.expr.parseString( + cmdstr, parseAll=True + ) parsed: list[ParseResult] = [] next_params: list[CommandParameter] = [ CommandParameter("", mitmproxy.types.Cmd), CommandParameter("", mitmproxy.types.CmdArgs), ] - expected: Optional[CommandParameter] = None + expected: CommandParameter | None = None for part in parts: if part.isspace(): parsed.append( @@ -307,7 +314,7 @@ def parsearg(manager: CommandManager, spec: str, argtype: type) -> Any: raise exceptions.CommandError(str(e)) from e -def command(name: Optional[str] = None): +def command(name: str | None = None): def decorator(function): @functools.wraps(function) def wrapper(*args, **kwargs): diff --git a/mitmproxy/connection.py b/mitmproxy/connection.py index 042fa9437b..7215a7a794 100644 --- a/mitmproxy/connection.py +++ b/mitmproxy/connection.py @@ -1,14 +1,18 @@ +import dataclasses +import time import uuid import warnings from abc import ABCMeta from collections.abc import Sequence +from dataclasses import dataclass +from dataclasses import field from enum import Flag -from typing import Literal, Optional +from typing import Literal from mitmproxy import certs from mitmproxy.coretypes import serializable -from mitmproxy.proxy import mode_specs from mitmproxy.net import server_spec +from mitmproxy.proxy import mode_specs from mitmproxy.utils import human @@ -23,14 +27,30 @@ class ConnectionState(Flag): TransportProtocol = Literal["tcp", "udp"] +# https://docs.openssl.org/master/man3/SSL_get_version/#return-values +TlsVersion = Literal[ + "SSLv3", + "TLSv1", + "TLSv1.1", + "TLSv1.2", + "TLSv1.3", + "DTLSv0.9", + "DTLSv1", + "DTLSv1.2", + "QUICv1", +] # practically speaking we may have IPv6 addresses with flowinfo and scope_id, # but type checking isn't good enough to properly handle tuple unions. # this version at least provides useful type checking messages. Address = tuple[str, int] +kw_only = {"kw_only": True} -class Connection(serializable.Serializable, metaclass=ABCMeta): + +# noinspection PyDataclass +@dataclass(**kw_only) +class Connection(serializable.SerializableDataclass, metaclass=ABCMeta): """ Base class for client and server connections. @@ -38,20 +58,24 @@ class Connection(serializable.Serializable, metaclass=ABCMeta): This is intentional, all I/O should be handled by `mitmproxy.proxy.server` exclusively. """ + peername: Address | None + """The remote's `(ip, port)` tuple for this connection.""" + sockname: Address | None + """Our local `(ip, port)` tuple for this connection.""" + + state: ConnectionState = field( + default=ConnectionState.CLOSED, metadata={"serialize": False} + ) + """The current connection state.""" + # all connections have a unique id. While # f.client_conn == f2.client_conn already holds true for live flows (where we have object identity), # we also want these semantics for recorded flows. - id: str + id: str = field(default_factory=lambda: str(uuid.uuid4())) """A unique UUID to identify the connection.""" - state: ConnectionState - """The current connection state.""" - transport_protocol: TransportProtocol + transport_protocol: TransportProtocol = field(default="tcp") """The connection protocol in use.""" - peername: Optional[Address] - """The remote's `(ip, port)` tuple for this connection.""" - sockname: Optional[Address] - """Our local `(ip, port)` tuple for this connection.""" - error: Optional[str] = None + error: str | None = None """ A string describing a general error with connections to this address. @@ -82,27 +106,27 @@ class Connection(serializable.Serializable, metaclass=ABCMeta): > TLS version, with the exception of the end-entity certificate which > MUST be first. """ - alpn: Optional[bytes] = None + alpn: bytes | None = None """The application-layer protocol as negotiated using [ALPN](https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation).""" alpn_offers: Sequence[bytes] = () """The ALPN offers as sent in the ClientHello.""" # we may want to add SSL_CIPHER_description here, but that's currently not exposed by cryptography - cipher: Optional[str] = None + cipher: str | None = None """The active cipher name as returned by OpenSSL's `SSL_CIPHER_get_name`.""" cipher_list: Sequence[str] = () """Ciphers accepted by the proxy server on this connection.""" - tls_version: Optional[str] = None + tls_version: TlsVersion | None = None """The active TLS version.""" - sni: Optional[str] = None + sni: str | None = None """ The [Server Name Indication (SNI)](https://en.wikipedia.org/wiki/Server_Name_Indication) sent in the ClientHello. """ - timestamp_start: Optional[float] - timestamp_end: Optional[float] = None + timestamp_start: float | None = None + timestamp_end: float | None = None """*Timestamp:* Connection has been closed.""" - timestamp_tls_setup: Optional[float] = None + timestamp_tls_setup: float | None = None """*Timestamp:* TLS handshake has been completed successfully.""" @property @@ -124,19 +148,23 @@ def __hash__(self): return hash(self.id) def __repr__(self): - attrs = repr( - { - k: { - "cipher_list": lambda: f"<{len(v)} ciphers>", - "id": lambda: f"…{v[-6:]}", - }.get(k, lambda: v)() - for k, v in self.__dict__.items() - } - ) - return f"{type(self).__name__}({attrs})" + attrs = { + # ensure these come first. + "id": None, + "address": None, + } + for f in dataclasses.fields(self): + val = getattr(self, f.name) + if val != f.default: + if f.name == "cipher_list": + val = f"<{len(val)} ciphers>" + elif f.name == "id": + val = f"…{val[-6:]}" + attrs[f.name] = val + return f"{type(self).__name__}({attrs!r})" @property - def alpn_proto_negotiated(self) -> Optional[bytes]: # pragma: no cover + def alpn_proto_negotiated(self) -> bytes | None: # pragma: no cover """*Deprecated:* An outdated alias for Connection.alpn.""" warnings.warn( "Connection.alpn_proto_negotiated is deprecated, use Connection.alpn instead.", @@ -146,6 +174,8 @@ def alpn_proto_negotiated(self) -> Optional[bytes]: # pragma: no cover return self.alpn +# noinspection PyDataclass +@dataclass(eq=False, repr=False, **kw_only) class Client(Connection): """A connection between a client and mitmproxy.""" @@ -154,34 +184,19 @@ class Client(Connection): sockname: Address """The local address we received this connection on.""" - mitmcert: Optional[certs.Cert] = None + mitmcert: certs.Cert | None = None """ The certificate used by mitmproxy to establish TLS with the client. """ - proxy_mode: mode_specs.ProxyMode + proxy_mode: mode_specs.ProxyMode = field( + default=mode_specs.ProxyMode.parse("regular") + ) """The proxy server type this client has been connecting to.""" - timestamp_start: float + timestamp_start: float = field(default_factory=time.time) """*Timestamp:* TCP SYN received""" - def __init__( - self, - peername: Address, - sockname: Address, - timestamp_start: float, - *, - transport_protocol: TransportProtocol = "tcp", - proxy_mode: mode_specs.ProxyMode = mode_specs.ProxyMode.parse("regular"), - ): - self.id = str(uuid.uuid4()) - self.peername = peername - self.sockname = sockname - self.timestamp_start = timestamp_start - self.state = ConnectionState.OPEN - self.transport_protocol = transport_protocol - self.proxy_mode = proxy_mode - def __str__(self): if self.alpn: tls_state = f", alpn={self.alpn.decode(errors='replace')}" @@ -189,69 +204,9 @@ def __str__(self): tls_state = ", tls" else: tls_state = "" - return f"Client({human.format_address(self.peername)}, state={self.state.name.lower()}{tls_state})" - - def get_state(self): - # Important: Retain full compatibility with old proxy core for now! - # This means we need to add all new fields to the old implementation. - return { - "address": self.peername, - "alpn": self.alpn, - "cipher_name": self.cipher, - "id": self.id, - "mitmcert": self.mitmcert.get_state() - if self.mitmcert is not None - else None, - "sni": self.sni, - "timestamp_end": self.timestamp_end, - "timestamp_start": self.timestamp_start, - "timestamp_tls_setup": self.timestamp_tls_setup, - "tls_established": self.tls_established, - "tls_extensions": [], - "tls_version": self.tls_version, - # only used in sans-io - "state": self.state.value, - "sockname": self.sockname, - "error": self.error, - "tls": self.tls, - "certificate_list": [x.get_state() for x in self.certificate_list], - "alpn_offers": self.alpn_offers, - "cipher_list": self.cipher_list, - "proxy_mode": self.proxy_mode.get_state(), - } - - @classmethod - def from_state(cls, state) -> "Client": - client = Client(state["address"], ("mitmproxy", 8080), state["timestamp_start"]) - client.set_state(state) - return client - - def set_state(self, state): - self.peername = tuple(state["address"]) if state["address"] else None - self.alpn = state["alpn"] - self.cipher = state["cipher_name"] - self.id = state["id"] - self.sni = state["sni"] - self.timestamp_end = state["timestamp_end"] - self.timestamp_start = state["timestamp_start"] - self.timestamp_tls_setup = state["timestamp_tls_setup"] - self.tls_version = state["tls_version"] - # only used in sans-io - self.state = ConnectionState(state["state"]) - self.sockname = tuple(state["sockname"]) if state["sockname"] else None - self.error = state["error"] - self.tls = state["tls"] - self.certificate_list = [ - certs.Cert.from_state(x) for x in state["certificate_list"] - ] - self.mitmcert = ( - certs.Cert.from_state(state["mitmcert"]) - if state["mitmcert"] is not None - else None - ) - self.alpn_offers = state["alpn_offers"] - self.cipher_list = state["cipher_list"] - self.proxy_mode = mode_specs.ProxyMode.from_state(state["proxy_mode"]) + state = self.state.name + assert state + return f"Client({human.format_address(self.peername)}, state={state.lower()}{tls_state})" @property def address(self): # pragma: no cover @@ -273,7 +228,7 @@ def address(self, x): # pragma: no cover self.peername = x @property - def cipher_name(self) -> Optional[str]: # pragma: no cover + def cipher_name(self) -> str | None: # pragma: no cover """*Deprecated:* An outdated alias for Connection.cipher.""" warnings.warn( "Client.cipher_name is deprecated, use Client.cipher instead.", @@ -283,7 +238,7 @@ def cipher_name(self) -> Optional[str]: # pragma: no cover return self.cipher @property - def clientcert(self) -> Optional[certs.Cert]: # pragma: no cover + def clientcert(self) -> certs.Cert | None: # pragma: no cover """*Deprecated:* An outdated alias for Connection.certificate_list[0].""" warnings.warn( "Client.clientcert is deprecated, use Client.certificate_list instead.", @@ -308,34 +263,41 @@ def clientcert(self, val): # pragma: no cover self.certificate_list = [] +# noinspection PyDataclass +@dataclass(eq=False, repr=False, **kw_only) class Server(Connection): """A connection between mitmproxy and an upstream server.""" - peername: Optional[Address] = None - """The server's resolved `(ip, port)` tuple. Will be set during connection establishment.""" - sockname: Optional[Address] = None - address: Optional[Address] - """The server's `(host, port)` address tuple. The host can either be a domain or a plain IP address.""" + address: Address | None # type: ignore + """ + The server's `(host, port)` address tuple. + + The host can either be a domain or a plain IP address. + Which of those two will be present depends on the proxy mode and the client. + For explicit proxies, this value will reflect what the client instructs mitmproxy to connect to. + For example, if the client starts off a connection with `CONNECT example.com HTTP/1.1`, it will be `example.com`. + For transparent proxies such as WireGuard mode, this value will be an IP address. + """ + + peername: Address | None = None + """ + The server's resolved `(ip, port)` tuple. Will be set during connection establishment. + May be `None` in upstream proxy mode when the address is resolved by the upstream proxy only. + """ + sockname: Address | None = None - timestamp_start: Optional[float] = None - """*Timestamp:* TCP SYN sent.""" - timestamp_tcp_setup: Optional[float] = None + timestamp_start: float | None = None + """ + *Timestamp:* Connection establishment started. + + For IP addresses, this corresponds to sending a TCP SYN; for domains, this corresponds to starting a DNS lookup. + """ + timestamp_tcp_setup: float | None = None """*Timestamp:* TCP ACK received.""" - via: Optional[server_spec.ServerSpec] = None + via: server_spec.ServerSpec | None = None """An optional proxy server specification via which the connection should be established.""" - def __init__( - self, - address: Optional[Address], - *, - transport_protocol: TransportProtocol = "tcp", - ): - self.id = str(uuid.uuid4()) - self.address = address - self.state = ConnectionState.CLOSED - self.transport_protocol = transport_protocol - def __str__(self): if self.alpn: tls_state = f", alpn={self.alpn.decode(errors='replace')}" @@ -347,7 +309,9 @@ def __str__(self): local_port = f", src_port={self.sockname[1]}" else: local_port = "" - return f"Server({human.format_address(self.address)}, state={self.state.name.lower()}{tls_state}{local_port})" + state = self.state.name + assert state + return f"Server({human.format_address(self.address)}, state={state.lower()}{tls_state}{local_port})" def __setattr__(self, name, value): if name in ("address", "via"): @@ -361,65 +325,8 @@ def __setattr__(self, name, value): raise RuntimeError(f"Cannot change server.{name} on open connection.") return super().__setattr__(name, value) - def get_state(self): - return { - "address": self.address, - "alpn": self.alpn, - "id": self.id, - "ip_address": self.peername, - "sni": self.sni, - "source_address": self.sockname, - "timestamp_end": self.timestamp_end, - "timestamp_start": self.timestamp_start, - "timestamp_tcp_setup": self.timestamp_tcp_setup, - "timestamp_tls_setup": self.timestamp_tls_setup, - "tls_established": self.tls_established, - "tls_version": self.tls_version, - "via": None, - # only used in sans-io - "state": self.state.value, - "error": self.error, - "tls": self.tls, - "certificate_list": [x.get_state() for x in self.certificate_list], - "alpn_offers": self.alpn_offers, - "cipher_name": self.cipher, - "cipher_list": self.cipher_list, - "via2": self.via, - } - - @classmethod - def from_state(cls, state) -> "Server": - server = Server(None) - server.set_state(state) - return server - - def set_state(self, state): - self.address = tuple(state["address"]) if state["address"] else None - self.alpn = state["alpn"] - self.id = state["id"] - self.peername = tuple(state["ip_address"]) if state["ip_address"] else None - self.sni = state["sni"] - self.sockname = ( - tuple(state["source_address"]) if state["source_address"] else None - ) - self.timestamp_end = state["timestamp_end"] - self.timestamp_start = state["timestamp_start"] - self.timestamp_tcp_setup = state["timestamp_tcp_setup"] - self.timestamp_tls_setup = state["timestamp_tls_setup"] - self.tls_version = state["tls_version"] - self.state = ConnectionState(state["state"]) - self.error = state["error"] - self.tls = state["tls"] - self.certificate_list = [ - certs.Cert.from_state(x) for x in state["certificate_list"] - ] - self.alpn_offers = state["alpn_offers"] - self.cipher = state["cipher_name"] - self.cipher_list = state["cipher_list"] - self.via = state["via2"] - @property - def ip_address(self) -> Optional[Address]: # pragma: no cover + def ip_address(self) -> Address | None: # pragma: no cover """*Deprecated:* An outdated alias for `Server.peername`.""" warnings.warn( "Server.ip_address is deprecated, use Server.peername instead.", @@ -429,7 +336,7 @@ def ip_address(self) -> Optional[Address]: # pragma: no cover return self.peername @property - def cert(self) -> Optional[certs.Cert]: # pragma: no cover + def cert(self) -> certs.Cert | None: # pragma: no cover """*Deprecated:* An outdated alias for `Connection.certificate_list[0]`.""" warnings.warn( "Server.cert is deprecated, use Server.certificate_list instead.", diff --git a/mitmproxy/contentviews/__init__.py b/mitmproxy/contentviews/__init__.py index 86fff00b25..399b3e0b65 100644 --- a/mitmproxy/contentviews/__init__.py +++ b/mitmproxy/contentviews/__init__.py @@ -11,49 +11,47 @@ metadata depend on the protocol in use. Known attributes can be found in `base.View`. """ + import traceback -from typing import Union -from typing import Optional -from mitmproxy import flow, tcp, udp -from mitmproxy import http -from mitmproxy.utils import signals, strutils -from . import ( - auto, - raw, - hex, - json, - xml_html, - wbxml, - javascript, - css, - urlencoded, - multipart, - image, - query, - protobuf, - msgpack, - graphql, - grpc, - mqtt, -) - -try: - from . import http3 -except ImportError: - # FIXME: Remove once QUIC is merged. - http3 = None # type: ignore -from .base import View, KEY_MAX, format_text, format_dict, TViewResult -from ..http import HTTPFlow -from ..tcp import TCPMessage, TCPFlow -from ..udp import UDPMessage, UDPFlow +from ..tcp import TCPMessage +from ..udp import UDPMessage from ..websocket import WebSocketMessage +from . import auto +from . import css +from . import dns +from . import graphql +from . import grpc +from . import hex +from . import http3 +from . import image +from . import javascript +from . import json +from . import mqtt +from . import msgpack +from . import multipart +from . import protobuf +from . import query +from . import raw +from . import urlencoded +from . import wbxml +from . import xml_html +from .base import format_dict +from .base import format_text +from .base import KEY_MAX +from .base import TViewResult +from .base import View +from mitmproxy import flow +from mitmproxy import http +from mitmproxy import tcp +from mitmproxy import udp +from mitmproxy.utils import signals +from mitmproxy.utils import strutils views: list[View] = [] -def _update(view: View) -> None: - ... +def _update(view: View) -> None: ... on_add = signals.SyncSignal(_update) @@ -62,7 +60,7 @@ def _update(view: View) -> None: """A contentview has been removed.""" -def get(name: str) -> Optional[View]: +def get(name: str) -> View | None: for i in views: if i.name.lower() == name.lower(): return i @@ -90,7 +88,7 @@ def safe_to_print(lines, encoding="utf8"): """ for line in lines: clean_line = [] - for (style, text) in line: + for style, text in line: if isinstance(text, bytes): text = text.decode(encoding, "replace") text = strutils.escape_control_characters(text) @@ -100,8 +98,8 @@ def safe_to_print(lines, encoding="utf8"): def get_message_content_view( viewname: str, - message: Union[http.Message, TCPMessage, UDPMessage, WebSocketMessage], - flow: Union[HTTPFlow, TCPFlow, UDPFlow], + message: http.Message | TCPMessage | UDPMessage | WebSocketMessage, + flow: flow.Flow, ): """ Like get_content_view, but also handles message encoding. @@ -111,7 +109,7 @@ def get_message_content_view( viewmode = get("auto") assert viewmode - content: Optional[bytes] + content: bytes | None try: content = message.content except ValueError: @@ -143,6 +141,10 @@ def get_message_content_view( if isinstance(message, UDPMessage): udp_message = message + websocket_message = None + if isinstance(message, WebSocketMessage): + websocket_message = message + description, lines, error = get_content_view( viewmode, content, @@ -151,6 +153,7 @@ def get_message_content_view( http_message=http_message, tcp_message=tcp_message, udp_message=udp_message, + websocket_message=websocket_message, ) if enc: @@ -163,11 +166,12 @@ def get_content_view( viewmode: View, data: bytes, *, - content_type: Optional[str] = None, - flow: Optional[flow.Flow] = None, - http_message: Optional[http.Message] = None, - tcp_message: Optional[tcp.TCPMessage] = None, - udp_message: Optional[udp.UDPMessage] = None, + content_type: str | None = None, + flow: flow.Flow | None = None, + http_message: http.Message | None = None, + tcp_message: tcp.TCPMessage | None = None, + udp_message: udp.UDPMessage | None = None, + websocket_message: WebSocketMessage | None = None, ): """ Args: @@ -188,6 +192,7 @@ def get_content_view( http_message=http_message, tcp_message=tcp_message, udp_message=udp_message, + websocket_message=websocket_message, ) if ret is None: ret = ( @@ -199,6 +204,7 @@ def get_content_view( http_message=http_message, tcp_message=tcp_message, udp_message=udp_message, + websocket_message=websocket_message, )[1], ) desc, content = ret @@ -215,6 +221,7 @@ def get_content_view( http_message=http_message, tcp_message=tcp_message, udp_message=udp_message, + websocket_message=websocket_message, )[1] error = f"{getattr(viewmode, 'name')} content viewer failed: \n{traceback.format_exc()}" @@ -224,7 +231,8 @@ def get_content_view( # The order in which ContentViews are added is important! add(auto.ViewAuto()) add(raw.ViewRaw()) -add(hex.ViewHex()) +add(hex.ViewHexStream()) +add(hex.ViewHexDump()) add(graphql.ViewGraphQL()) add(json.ViewJSON()) add(xml_html.ViewXmlHtml()) @@ -239,8 +247,8 @@ def get_content_view( add(msgpack.ViewMsgPack()) add(grpc.ViewGrpcProtobuf()) add(mqtt.ViewMQTT()) -if http3 is not None: - add(http3.ViewHttp3()) +add(http3.ViewHttp3()) +add(dns.ViewDns()) __all__ = [ "View", diff --git a/mitmproxy/contentviews/auto.py b/mitmproxy/contentviews/auto.py index d86dcf8108..e8acfd94e3 100644 --- a/mitmproxy/contentviews/auto.py +++ b/mitmproxy/contentviews/auto.py @@ -1,5 +1,5 @@ -from mitmproxy import contentviews from . import base +from mitmproxy import contentviews class ViewAuto(base.View): diff --git a/mitmproxy/contentviews/base.py b/mitmproxy/contentviews/base.py index d8baa9f25a..9ee0451574 100644 --- a/mitmproxy/contentviews/base.py +++ b/mitmproxy/contentviews/base.py @@ -1,7 +1,11 @@ # Default view cutoff *in lines* -from abc import ABC, abstractmethod -from collections.abc import Iterable, Iterator, Mapping -from typing import ClassVar, Optional, Union +from abc import ABC +from abc import abstractmethod +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Mapping +from typing import ClassVar +from typing import Union from mitmproxy import flow from mitmproxy import http @@ -21,9 +25,9 @@ def __call__( self, data: bytes, *, - content_type: Optional[str] = None, - flow: Optional[flow.Flow] = None, - http_message: Optional[http.Message] = None, + content_type: str | None = None, + flow: flow.Flow | None = None, + http_message: http.Message | None = None, **unknown_metadata, ) -> TViewResult: """ @@ -47,9 +51,9 @@ def render_priority( self, data: bytes, *, - content_type: Optional[str] = None, - flow: Optional[flow.Flow] = None, - http_message: Optional[http.Message] = None, + content_type: str | None = None, + flow: flow.Flow | None = None, + http_message: http.Message | None = None, **unknown_metadata, ) -> float: """ @@ -81,7 +85,6 @@ def format_pairs(items: Iterable[tuple[TTextType, TTextType]]) -> Iterator[TView for key, value in items: if isinstance(key, bytes): - key += b":" else: key += ":" diff --git a/mitmproxy/contentviews/css.py b/mitmproxy/contentviews/css.py index fd878c0afd..0adde59d5e 100644 --- a/mitmproxy/contentviews/css.py +++ b/mitmproxy/contentviews/css.py @@ -1,6 +1,5 @@ import re import time -from typing import Optional from mitmproxy.contentviews import base from mitmproxy.utils import strutils @@ -58,7 +57,7 @@ def __call__(self, data, **metadata): return "CSS", base.format_text(beautified) def render_priority( - self, data: bytes, *, content_type: Optional[str] = None, **metadata + self, data: bytes, *, content_type: str | None = None, **metadata ) -> float: return float(bool(data) and content_type == "text/css") diff --git a/mitmproxy/contentviews/dns.py b/mitmproxy/contentviews/dns.py new file mode 100644 index 0000000000..58e6b52f47 --- /dev/null +++ b/mitmproxy/contentviews/dns.py @@ -0,0 +1,20 @@ +from mitmproxy.contentviews import base +from mitmproxy.contentviews.json import format_json +from mitmproxy.dns import Message + + +class ViewDns(base.View): + name = "DNS-over-HTTPS" + + def __call__(self, data, **metadata): + try: + message = Message.unpack(data) + except Exception: + pass + else: + return "DoH", format_json(message.to_json()) + + def render_priority( + self, data: bytes, *, content_type: str | None = None, **metadata + ) -> float: + return float(content_type == "application/dns-message") diff --git a/mitmproxy/contentviews/graphql.py b/mitmproxy/contentviews/graphql.py index c179828e87..b76b7bf054 100644 --- a/mitmproxy/contentviews/graphql.py +++ b/mitmproxy/contentviews/graphql.py @@ -1,8 +1,9 @@ import json -from typing import Any, Optional +from typing import Any from mitmproxy.contentviews import base -from mitmproxy.contentviews.json import parse_json, PARSE_ERROR +from mitmproxy.contentviews.json import PARSE_ERROR +from mitmproxy.contentviews.json import parse_json def format_graphql(data): @@ -12,9 +13,7 @@ def format_graphql(data): return """{header} --- {query} -""".format( - header=json.dumps(header_data, indent=2), query=query - ) +""".format(header=json.dumps(header_data, indent=2), query=query) def format_query_list(data: list[Any]): @@ -31,7 +30,12 @@ def is_graphql_query(data): def is_graphql_batch_query(data): - return isinstance(data, list) and isinstance(data[0], dict) and "query" in data[0] + return ( + isinstance(data, list) + and len(data) > 0 + and isinstance(data[0], dict) + and "query" in data[0] + ) class ViewGraphQL(base.View): @@ -46,7 +50,7 @@ def __call__(self, data, **metadata): return "GraphQL", base.format_text(format_query_list(data)) def render_priority( - self, data: bytes, *, content_type: Optional[str] = None, **metadata + self, data: bytes, *, content_type: str | None = None, **metadata ) -> float: if content_type != "application/json" or not data: return 0 diff --git a/mitmproxy/contentviews/grpc.py b/mitmproxy/contentviews/grpc.py index 9db6c6b40b..4c0307b315 100644 --- a/mitmproxy/contentviews/grpc.py +++ b/mitmproxy/contentviews/grpc.py @@ -2,11 +2,17 @@ import logging import struct -from dataclasses import dataclass, field +from collections.abc import Generator +from collections.abc import Iterable +from collections.abc import Iterator +from dataclasses import dataclass +from dataclasses import field from enum import Enum -from typing import Generator, Iterable, Iterator -from mitmproxy import contentviews, flow, flowfilter, http +from mitmproxy import contentviews +from mitmproxy import flow +from mitmproxy import flowfilter +from mitmproxy import http from mitmproxy.contentviews import base from mitmproxy.net.encoding import decode @@ -259,7 +265,9 @@ def read_packed_fields( packed_field: ProtoParser.Field, ) -> list[ProtoParser.Field]: if not isinstance(packed_field.wire_value, bytes): - raise ValueError(f"can not unpack field with data other than bytes: {type(packed_field.wire_value)}") + raise ValueError( + f"can not unpack field with data other than bytes: {type(packed_field.wire_value)}" + ) wire_data: bytes = packed_field.wire_value tag: int = packed_field.tag options: ProtoParser.ParserOptions = packed_field.options @@ -504,9 +512,11 @@ def apply_rules(self, only_first_hit=True): if match: if only_first_hit: # only first match - self.name = fd.name - self.preferred_decoding = fd.intended_decoding - self.try_unpack = fd.as_packed + if fd.name is not None: + self.name = fd.name + if fd.intended_decoding is not None: + self.preferred_decoding = fd.intended_decoding + self.try_unpack = bool(fd.as_packed) return else: # overwrite matches till last rule was inspected @@ -551,7 +561,7 @@ def safe_decode_as( return intended_decoding, self.decode_as( intended_decoding, try_as_packed ) - except: + except Exception: if int(self.wire_value).bit_length() > 32: # ignore the fact that varint could exceed 64bit (would violate the specs) return ProtoParser.DecodedTypes.uint64, self.wire_value @@ -562,21 +572,21 @@ def safe_decode_as( return intended_decoding, self.decode_as( intended_decoding, try_as_packed ) - except: + except Exception: return ProtoParser.DecodedTypes.fixed64, self.wire_value elif self.wire_type == ProtoParser.WireTypes.bit_32: try: return intended_decoding, self.decode_as( intended_decoding, try_as_packed ) - except: + except Exception: return ProtoParser.DecodedTypes.fixed32, self.wire_value elif self.wire_type == ProtoParser.WireTypes.len_delimited: try: return intended_decoding, self.decode_as( intended_decoding, try_as_packed ) - except: + except Exception: # failover strategy: message --> string (valid UTF-8) --> bytes len_delimited_strategy: list[ProtoParser.DecodedTypes] = [ ProtoParser.DecodedTypes.message, @@ -591,7 +601,7 @@ def safe_decode_as( return failover_decoding, self.decode_as( failover_decoding, False ) - except: + except Exception: pass # we should never get here (could not be added to tests) @@ -773,8 +783,8 @@ def gen_flat_decoded_field_dicts(self) -> Generator[dict, None, None]: def __init__( self, data: bytes, - rules: list[ProtoParser.ParserRule] = None, - parser_options: ParserOptions = None, + rules: list[ProtoParser.ParserRule] | None = None, + parser_options: ParserOptions | None = None, ) -> None: self.data: bytes = data if parser_options is None: @@ -951,7 +961,9 @@ def format_grpc( @dataclass class ViewConfig: - parser_options: ProtoParser.ParserOptions = field(default_factory=ProtoParser.ParserOptions) + parser_options: ProtoParser.ParserOptions = field( + default_factory=ProtoParser.ParserOptions + ) parser_rules: list[ProtoParser.ParserRule] = field(default_factory=list) @@ -976,10 +988,11 @@ class ViewGrpcProtobuf(base.View): "gzip", "identity", "deflate", + "zstd", ] # allows to take external ParserOptions object. goes with defaults otherwise - def __init__(self, config: ViewConfig = None) -> None: + def __init__(self, config: ViewConfig | None = None) -> None: super().__init__() if config is None: config = ViewConfig() @@ -1062,7 +1075,7 @@ def __call__( if h in self.__valid_grpc_encodings else self.__valid_grpc_encodings[0] ) - except: + except Exception: grpc_encoding = self.__valid_grpc_encodings[0] text_iter = format_grpc( @@ -1101,7 +1114,6 @@ def render_priority( http_message: http.Message | None = None, **unknown_metadata, ) -> float: - if bool(data) and content_type in self.__content_types_grpc: return 1 if bool(data) and content_type in self.__content_types_pb: diff --git a/mitmproxy/contentviews/hex.py b/mitmproxy/contentviews/hex.py index 5b53202c6b..94f88560fe 100644 --- a/mitmproxy/contentviews/hex.py +++ b/mitmproxy/contentviews/hex.py @@ -1,9 +1,9 @@ -from mitmproxy.utils import strutils from . import base +from mitmproxy.utils import strutils -class ViewHex(base.View): - name = "Hex" +class ViewHexDump(base.View): + name = "Hex Dump" @staticmethod def _format(data): @@ -11,7 +11,17 @@ def _format(data): yield [("offset", offset + " "), ("text", hexa + " "), ("text", s)] def __call__(self, data, **metadata): - return "Hex", self._format(data) + return "Hexdump", self._format(data) def render_priority(self, data: bytes, **metadata) -> float: return 0.2 * strutils.is_mostly_bin(data) + + +class ViewHexStream(base.View): + name = "Raw Hex Stream" + + def __call__(self, data, **metadata): + return "Raw Hex Stream", base.format_text(data.hex()) + + def render_priority(self, data: bytes, **metadata) -> float: + return 0.15 * strutils.is_mostly_bin(data) diff --git a/mitmproxy/contentviews/http3.py b/mitmproxy/contentviews/http3.py index a354a901cf..d1c93915f5 100644 --- a/mitmproxy/contentviews/http3.py +++ b/mitmproxy/contentviews/http3.py @@ -1,21 +1,25 @@ from collections import defaultdict from collections.abc import Iterator -from dataclasses import dataclass, field +from dataclasses import dataclass +from dataclasses import field -from aioquic.h3.connection import Setting, parse_settings +import pylsqpack +from aioquic.buffer import Buffer +from aioquic.buffer import BufferReadError +from aioquic.h3.connection import parse_settings +from aioquic.h3.connection import Setting -from mitmproxy import flow, tcp +from ..proxy.layers.http import is_h3_alpn from . import base -from .hex import ViewHex -from ..proxy.layers.http import is_h3_alpn # type: ignore - -from aioquic.buffer import Buffer, BufferReadError -import pylsqpack +from .hex import ViewHexDump +from mitmproxy import flow +from mitmproxy import tcp @dataclass(frozen=True) class Frame: """Representation of an HTTP/3 frame.""" + type: int data: bytes @@ -26,10 +30,7 @@ def pretty(self): elif self.type == 1: try: hdrs = pylsqpack.Decoder(4096, 16).feed_header(0, self.data)[1] - return [ - [("header", "HEADERS Frame")], - *base.format_pairs(hdrs) - ] + return [[("header", "HEADERS Frame")], *base.format_pairs(hdrs)] except Exception as e: frame_name = f"HEADERS Frame (error: {e})" elif self.type == 4: @@ -45,19 +46,17 @@ def pretty(self): except ValueError: key = f"0x{k:x}" settings.append((key, f"0x{v:x}")) - return [ - [("header", "SETTINGS Frame")], - *base.format_pairs(settings) - ] + return [[("header", "SETTINGS Frame")], *base.format_pairs(settings)] return [ [("header", frame_name)], - *ViewHex._format(self.data), + *ViewHexDump._format(self.data), ] @dataclass(frozen=True) class StreamType: """Representation of an HTTP/3 stream types.""" + type: int def pretty(self): @@ -67,9 +66,7 @@ def pretty(self): 0x02: "QPACK Encoder Stream", 0x03: "QPACK Decoder Stream", }.get(self.type, f"0x{self.type:x} Stream") - return [ - [("header", stream_type)] - ] + return [[("header", stream_type)]] @dataclass @@ -83,22 +80,24 @@ class ConnectionState: class ViewHttp3(base.View): name = "HTTP/3 Frames" - def __init__(self): - self.connections: defaultdict[tcp.TCPFlow, ConnectionState] = defaultdict(ConnectionState) + def __init__(self) -> None: + self.connections: defaultdict[tcp.TCPFlow, ConnectionState] = defaultdict( + ConnectionState + ) def __call__( self, data, flow: flow.Flow | None = None, tcp_message: tcp.TCPMessage | None = None, - **metadata + **metadata, ): assert isinstance(flow, tcp.TCPFlow) assert tcp_message state = self.connections[flow] - for message in flow.messages[state.message_count:]: + for message in flow.messages[state.message_count :]: if message.from_client: buf = state.client_buf else: @@ -109,11 +108,8 @@ def __call__( h3_buf = Buffer(data=bytes(buf[:8])) stream_type = h3_buf.pull_uint_var() consumed = h3_buf.tell() - assert consumed == 1 del buf[:consumed] - state.frames[0] = [ - StreamType(stream_type) - ] + state.frames[0] = [StreamType(stream_type)] while True: h3_buf = Buffer(data=bytes(buf[:16])) @@ -128,29 +124,33 @@ def __call__( if len(buf) < consumed + frame_size: break - frame_data = bytes(buf[consumed:consumed + frame_size]) + frame_data = bytes(buf[consumed : consumed + frame_size]) frame = Frame(frame_type, frame_data) state.frames.setdefault(state.message_count, []).append(frame) - del buf[:consumed + frame_size] + del buf[: consumed + frame_size] state.message_count += 1 frames = state.frames.get(flow.messages.index(tcp_message), []) if not frames: - return "HTTP/3", [] # base.format_text(f"(no complete frames here, {state=})") + return ( + "HTTP/3", + [], + ) # base.format_text(f"(no complete frames here, {state=})") else: return "HTTP/3", fmt_frames(frames) def render_priority( - self, - data: bytes, - flow: flow.Flow | None = None, - **metadata + self, data: bytes, flow: flow.Flow | None = None, **metadata ) -> float: - return 2 * float(bool(flow and is_h3_alpn(flow.client_conn.alpn))) * float(isinstance(flow, tcp.TCPFlow)) + return ( + 2 + * float(bool(flow and is_h3_alpn(flow.client_conn.alpn))) + * float(isinstance(flow, tcp.TCPFlow)) + ) def fmt_frames(frames: list[Frame | StreamType]) -> Iterator[base.TViewLine]: diff --git a/mitmproxy/contentviews/image/image_parser.py b/mitmproxy/contentviews/image/image_parser.py index 709851fedb..0bbee9f2f4 100644 --- a/mitmproxy/contentviews/image/image_parser.py +++ b/mitmproxy/contentviews/image/image_parser.py @@ -105,9 +105,7 @@ def parse_ico(data: bytes) -> Metadata: parts.append( ( f"Image {i + 1}", - "Size: {} x {}\n" - "{: >18}Bits per pixel: {}\n" - "{: >18}PNG: {}".format( + "Size: {} x {}\n" "{: >18}Bits per pixel: {}\n" "{: >18}PNG: {}".format( 256 if not image.width else image.width, 256 if not image.height else image.height, "", diff --git a/mitmproxy/contentviews/image/view.py b/mitmproxy/contentviews/image/view.py index a414a1a7f0..181c12758f 100644 --- a/mitmproxy/contentviews/image/view.py +++ b/mitmproxy/contentviews/image/view.py @@ -1,9 +1,7 @@ -import imghdr -from typing import Optional - +from . import image_parser from mitmproxy.contentviews import base +from mitmproxy.contrib import imghdr from mitmproxy.coretypes import multidict -from . import image_parser def test_ico(h, f): @@ -36,7 +34,7 @@ def __call__(self, data, **metadata): return view_name, base.format_dict(multidict.MultiDict(image_metadata)) def render_priority( - self, data: bytes, *, content_type: Optional[str] = None, **metadata + self, data: bytes, *, content_type: str | None = None, **metadata ) -> float: return float( bool( diff --git a/mitmproxy/contentviews/javascript.py b/mitmproxy/contentviews/javascript.py index de04668386..02b47d53e8 100644 --- a/mitmproxy/contentviews/javascript.py +++ b/mitmproxy/contentviews/javascript.py @@ -1,9 +1,8 @@ import io import re -from typing import Optional -from mitmproxy.utils import strutils from mitmproxy.contentviews import base +from mitmproxy.utils import strutils DELIMITERS = "{};\n" SPECIAL_AREAS = ( @@ -55,6 +54,6 @@ def __call__(self, data, **metadata): return "JavaScript", base.format_text(res) def render_priority( - self, data: bytes, *, content_type: Optional[str] = None, **metadata + self, data: bytes, *, content_type: str | None = None, **metadata ) -> float: return float(bool(data) and content_type in self.__content_types) diff --git a/mitmproxy/contentviews/json.py b/mitmproxy/contentviews/json.py index d8952e80bc..9a6bedfa3b 100644 --- a/mitmproxy/contentviews/json.py +++ b/mitmproxy/contentviews/json.py @@ -1,8 +1,8 @@ -import re import json +import re from collections.abc import Iterator from functools import lru_cache -from typing import Any, Optional +from typing import Any from mitmproxy.contentviews import base @@ -18,7 +18,7 @@ def parse_json(s: bytes) -> Any: def format_json(data: Any) -> Iterator[base.TViewLine]: - encoder = json.JSONEncoder(indent=4, sort_keys=True, ensure_ascii=False) + encoder = json.JSONEncoder(indent=4, ensure_ascii=False) current_line: base.TViewLine = [] for chunk in encoder.iterencode(data): if "\n" in chunk: @@ -28,7 +28,11 @@ def format_json(data: Any) -> Iterator[base.TViewLine]: yield current_line current_line = [] if re.match(r'\s*"', chunk): - if len(current_line) == 1 and current_line[0][0] == "text" and current_line[0][1].isspace(): + if ( + len(current_line) == 1 + and current_line[0][0] == "text" + and current_line[0][1].isspace() + ): current_line.append(("Token_Name_Tag", chunk)) else: current_line.append(("Token_Literal_String", chunk)) @@ -50,7 +54,7 @@ def __call__(self, data, **metadata): return "JSON", format_json(data) def render_priority( - self, data: bytes, *, content_type: Optional[str] = None, **metadata + self, data: bytes, *, content_type: str | None = None, **metadata ) -> float: if not data: return 0 diff --git a/mitmproxy/contentviews/mqtt.py b/mitmproxy/contentviews/mqtt.py index c344fed20f..ad40482601 100644 --- a/mitmproxy/contentviews/mqtt.py +++ b/mitmproxy/contentviews/mqtt.py @@ -1,10 +1,8 @@ -from typing import Optional +import struct from mitmproxy.contentviews import base from mitmproxy.utils import strutils -import struct - # from https://github.com/nikitastupin/mitmproxy-mqtt-script @@ -89,6 +87,7 @@ def pprint(self): s = f"[{self.Names[self.packet_type]}]" if self.packet_type == self.CONNECT: + assert self.payload s += f""" Client Id: {self.payload['ClientId']} @@ -101,6 +100,7 @@ def pprint(self): s += " sent topic filters: " s += ", ".join([f"'{tf}'" for tf in self.topic_filters]) elif self.packet_type == self.PUBLISH: + assert self.payload topic_name = strutils.bytes_to_escaped_str(self.topic_name) payload = strutils.bytes_to_escaped_str(self.payload) @@ -209,9 +209,13 @@ def _parse_connect_payload(self): self.payload["WillTopic"] = f.decode("utf-8") elif self.connect_flags["Will"] and "WillMessage" not in self.payload: self.payload["WillMessage"] = f - elif self.connect_flags["UserName"] and "UserName" not in self.payload: # pragma: no cover + elif ( + self.connect_flags["UserName"] and "UserName" not in self.payload + ): # pragma: no cover self.payload["UserName"] = f.decode("utf-8") - elif self.connect_flags["Password"] and "Password" not in self.payload: # pragma: no cover + elif ( + self.connect_flags["Password"] and "Password" not in self.payload + ): # pragma: no cover self.payload["Password"] = f else: raise AssertionError(f"Unknown field in CONNECT payload: {f}") @@ -268,6 +272,6 @@ def __call__(self, data, **metadata): return "MQTT", base.format_text(text) def render_priority( - self, data: bytes, *, content_type: Optional[str] = None, **metadata + self, data: bytes, *, content_type: str | None = None, **metadata ) -> float: return 0 diff --git a/mitmproxy/contentviews/msgpack.py b/mitmproxy/contentviews/msgpack.py index 7e845bd118..3b0212d515 100644 --- a/mitmproxy/contentviews/msgpack.py +++ b/mitmproxy/contentviews/msgpack.py @@ -1,8 +1,7 @@ -from typing import Any, Optional +from typing import Any import msgpack - from mitmproxy.contentviews import base PARSE_ERROR = object() @@ -15,35 +14,44 @@ def parse_msgpack(s: bytes) -> Any: return PARSE_ERROR -def format_msgpack(data: Any, output = None, indent_count: int = 0) -> list[base.TViewLine]: +def format_msgpack( + data: Any, output=None, indent_count: int = 0 +) -> list[base.TViewLine]: if output is None: output = [[]] indent = ("text", " " * indent_count) - if type(data) is str: - token = [("Token_Literal_String", f"\"{data}\"")] + if isinstance(data, str): + token = [("Token_Literal_String", f'"{data}"')] output[-1] += token # Need to return if single value, but return is discarded in dict/list loop return output - elif type(data) is float or type(data) is int: - token = [("Token_Literal_Number", repr(data))] + elif isinstance(data, bool): + token = [("Token_Keyword_Constant", repr(data))] output[-1] += token return output - elif type(data) is bool: - token = [("Token_Keyword_Constant", repr(data))] + elif isinstance(data, float | int): + token = [("Token_Literal_Number", repr(data))] output[-1] += token return output - elif type(data) is dict: + elif isinstance(data, dict): output[-1] += [("text", "{")] for key in data: - output.append([indent, ("text", " "), ("Token_Name_Tag", f'"{key}"'), ("text", ": ")]) + output.append( + [ + indent, + ("text", " "), + ("Token_Name_Tag", f'"{key}"'), + ("text", ": "), + ] + ) format_msgpack(data[key], output, indent_count + 1) if key != list(data)[-1]: @@ -53,13 +61,13 @@ def format_msgpack(data: Any, output = None, indent_count: int = 0) -> list[base return output - elif type(data) is list: + elif isinstance(data, list): output[-1] += [("text", "[")] - for item in data: + + for count, item in enumerate(data): output.append([indent, ("text", " ")]) format_msgpack(item, output, indent_count + 1) - - if item != data[-1]: + if count != len(data) - 1: output[-1] += [("text", ",")] output.append([indent, ("text", "]")]) @@ -86,6 +94,6 @@ def __call__(self, data, **metadata): return "MsgPack", format_msgpack(data) def render_priority( - self, data: bytes, *, content_type: Optional[str] = None, **metadata + self, data: bytes, *, content_type: str | None = None, **metadata ) -> float: return float(bool(data) and content_type in self.__content_types) diff --git a/mitmproxy/contentviews/multipart.py b/mitmproxy/contentviews/multipart.py index 9485824ca0..450b427411 100644 --- a/mitmproxy/contentviews/multipart.py +++ b/mitmproxy/contentviews/multipart.py @@ -1,8 +1,7 @@ -from typing import Optional - +from .. import http +from . import base from mitmproxy.coretypes import multidict from mitmproxy.net.http import multipart -from . import base class ViewMultipart(base.View): @@ -13,14 +12,24 @@ def _format(v): yield [("highlight", "Form data:\n")] yield from base.format_dict(multidict.MultiDict(v)) - def __call__(self, data: bytes, content_type: Optional[str] = None, **metadata): + def __call__( + self, + data: bytes, + content_type: str | None = None, + http_message: http.Message | None = None, + **metadata, + ): + # The content_type doesn't have the boundary, so we get it from the header again + headers = getattr(http_message, "headers", None) + if headers: + content_type = headers.get("content-type") if content_type is None: return - v = multipart.decode(content_type, data) + v = multipart.decode_multipart(content_type, data) if v: return "Multipart form", self._format(v) def render_priority( - self, data: bytes, *, content_type: Optional[str] = None, **metadata + self, data: bytes, *, content_type: str | None = None, **metadata ) -> float: return float(bool(data) and content_type == "multipart/form-data") diff --git a/mitmproxy/contentviews/protobuf.py b/mitmproxy/contentviews/protobuf.py index 7aea0c1760..7447d3384c 100644 --- a/mitmproxy/contentviews/protobuf.py +++ b/mitmproxy/contentviews/protobuf.py @@ -1,7 +1,7 @@ import io -from typing import Optional from kaitaistruct import KaitaiStream + from . import base from mitmproxy.contrib.kaitaistruct import google_protobuf @@ -26,7 +26,9 @@ def _parse_proto(raw: bytes) -> list[google_protobuf.GoogleProtobuf.Pair]: """Parse a bytestring into protobuf pairs and make sure that all pairs have a valid wire type.""" buf = google_protobuf.GoogleProtobuf(KaitaiStream(io.BytesIO(raw))) for pair in buf.pairs: - if not isinstance(pair.wire_type, google_protobuf.GoogleProtobuf.Pair.WireTypes): + if not isinstance( + pair.wire_type, google_protobuf.GoogleProtobuf.Pair.WireTypes + ): raise ValueError("Not a protobuf.") return buf.pairs @@ -37,7 +39,7 @@ def format_pbuf(raw): try: pairs = _parse_proto(raw) - except: + except Exception: return False stack.extend([(pair, 0) for pair in pairs[::-1]]) @@ -57,10 +59,10 @@ def format_pbuf(raw): body = pair.value try: - pairs = _parse_proto(body) + pairs = _parse_proto(body) # type: ignore stack.extend([(pair, indent_level + 2) for pair in pairs[::-1]]) write_buf(out, pair.field_tag, None, indent_level) - except: + except Exception: write_buf(out, pair.field_tag, body, indent_level) if stack: @@ -95,6 +97,6 @@ def __call__(self, data, **metadata): return "Protobuf", base.format_text(decoded) def render_priority( - self, data: bytes, *, content_type: Optional[str] = None, **metadata + self, data: bytes, *, content_type: str | None = None, **metadata ) -> float: return float(bool(data) and content_type in self.__content_types) diff --git a/mitmproxy/contentviews/query.py b/mitmproxy/contentviews/query.py index 49c9107028..bcbb39cfd1 100644 --- a/mitmproxy/contentviews/query.py +++ b/mitmproxy/contentviews/query.py @@ -1,14 +1,12 @@ -from typing import Optional - -from . import base from .. import http +from . import base class ViewQuery(base.View): name = "Query" def __call__( - self, data: bytes, http_message: Optional[http.Message] = None, **metadata + self, data: bytes, http_message: http.Message | None = None, **metadata ): query = getattr(http_message, "query", None) if query: @@ -17,6 +15,6 @@ def __call__( return "Query", base.format_text("") def render_priority( - self, data: bytes, *, http_message: Optional[http.Message] = None, **metadata + self, data: bytes, *, http_message: http.Message | None = None, **metadata ) -> float: return 0.3 * float(bool(getattr(http_message, "query", False) and not data)) diff --git a/mitmproxy/contentviews/raw.py b/mitmproxy/contentviews/raw.py index a0b0884ecb..0c7b77d923 100644 --- a/mitmproxy/contentviews/raw.py +++ b/mitmproxy/contentviews/raw.py @@ -1,4 +1,3 @@ -from mitmproxy.utils import strutils from . import base @@ -6,7 +5,7 @@ class ViewRaw(base.View): name = "Raw" def __call__(self, data, **metadata): - return "Raw", base.format_text(strutils.bytes_to_escaped_str(data, True)) + return "Raw", base.format_text(data) def render_priority(self, data: bytes, **metadata) -> float: return 0.1 * float(bool(data)) diff --git a/mitmproxy/contentviews/urlencoded.py b/mitmproxy/contentviews/urlencoded.py index 2988d85271..5065dca338 100644 --- a/mitmproxy/contentviews/urlencoded.py +++ b/mitmproxy/contentviews/urlencoded.py @@ -1,7 +1,5 @@ -from typing import Optional - -from mitmproxy.net.http import url from . import base +from mitmproxy.net.http import url class ViewURLEncoded(base.View): @@ -16,6 +14,6 @@ def __call__(self, data, **metadata): return "URLEncoded form", base.format_pairs(d) def render_priority( - self, data: bytes, *, content_type: Optional[str] = None, **metadata + self, data: bytes, *, content_type: str | None = None, **metadata ) -> float: return float(bool(data) and content_type == "application/x-www-form-urlencoded") diff --git a/mitmproxy/contentviews/wbxml.py b/mitmproxy/contentviews/wbxml.py index 4cd7fda89b..77bceed17b 100644 --- a/mitmproxy/contentviews/wbxml.py +++ b/mitmproxy/contentviews/wbxml.py @@ -1,7 +1,5 @@ -from typing import Optional - -from mitmproxy.contrib.wbxml import ASCommandResponse from . import base +from mitmproxy.contrib.wbxml import ASCommandResponse class ViewWBXML(base.View): @@ -14,10 +12,10 @@ def __call__(self, data, **metadata): parsedContent = parser.xmlString if parsedContent: return "WBXML", base.format_text(parsedContent) - except: + except Exception: return None def render_priority( - self, data: bytes, *, content_type: Optional[str] = None, **metadata + self, data: bytes, *, content_type: str | None = None, **metadata ) -> float: return float(bool(data) and content_type in self.__content_types) diff --git a/mitmproxy/contentviews/xml_html.py b/mitmproxy/contentviews/xml_html.py index b8e8f05f6f..f402bbe568 100644 --- a/mitmproxy/contentviews/xml_html.py +++ b/mitmproxy/contentviews/xml_html.py @@ -1,10 +1,11 @@ import io import re import textwrap -from typing import Iterable, Optional +from collections.abc import Iterable from mitmproxy.contentviews import base -from mitmproxy.utils import sliding_window, strutils +from mitmproxy.utils import sliding_window +from mitmproxy.utils import strutils """ A custom XML/HTML prettifier. Compared to other prettifiers, its main features are: @@ -46,7 +47,7 @@ def __init__(self, data): self.data = data def __repr__(self): - return "{}({})".format(type(self).__name__, self.data) + return f"{type(self).__name__}({self.data})" class Text(Token): @@ -138,7 +139,7 @@ def indent_text(data: str, prefix: str) -> str: return textwrap.indent(dedented, prefix[:32]) -def is_inline_text(a: Optional[Token], b: Optional[Token], c: Optional[Token]) -> bool: +def is_inline_text(a: Token | None, b: Token | None, c: Token | None) -> bool: if isinstance(a, Tag) and isinstance(b, Text) and isinstance(c, Tag): if a.is_opening and "\n" not in b.data and c.is_closing and a.tag == c.tag: return True @@ -146,11 +147,11 @@ def is_inline_text(a: Optional[Token], b: Optional[Token], c: Optional[Token]) - def is_inline( - prev2: Optional[Token], - prev1: Optional[Token], - t: Optional[Token], - next1: Optional[Token], - next2: Optional[Token], + prev2: Token | None, + prev1: Token | None, + t: Token | None, + next1: Token | None, + next2: Token | None, ) -> bool: if isinstance(t, Text): return is_inline_text(prev1, t, next1) @@ -265,7 +266,7 @@ def __call__(self, data, **metadata): return t, pretty def render_priority( - self, data: bytes, *, content_type: Optional[str] = None, **metadata + self, data: bytes, *, content_type: str | None = None, **metadata ) -> float: if not data: return 0 diff --git a/mitmproxy/contrib/README b/mitmproxy/contrib/README deleted file mode 100644 index 9924f9368a..0000000000 --- a/mitmproxy/contrib/README +++ /dev/null @@ -1,8 +0,0 @@ - -Contribs: - -wbxml - - https://github.com/davidpshaw/PyWBXMLDecoder - -urwid - - Patches vendored from https://github.com/urwid/urwid/pull/448. \ No newline at end of file diff --git a/mitmproxy/contrib/README.md b/mitmproxy/contrib/README.md new file mode 100644 index 0000000000..d327a1f9ae --- /dev/null +++ b/mitmproxy/contrib/README.md @@ -0,0 +1,4 @@ +# mitmproxy/contrib + +This directory includes vendored code from other sources. +See the respective README and LICENSE files for details. diff --git a/mitmproxy/contrib/click/LICENSE.BSD-3 b/mitmproxy/contrib/click/LICENSE.BSD-3 new file mode 100644 index 0000000000..d12a849186 --- /dev/null +++ b/mitmproxy/contrib/click/LICENSE.BSD-3 @@ -0,0 +1,28 @@ +Copyright 2014 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/mitmproxy/contrib/imghdr.py b/mitmproxy/contrib/imghdr.py new file mode 100644 index 0000000000..4634819405 --- /dev/null +++ b/mitmproxy/contrib/imghdr.py @@ -0,0 +1,142 @@ +# A vendored copy of Python's imghdr module, which is slated for removal in Python 3.13. +# +# Source: https://github.com/python/cpython/blob/3.12/Lib/imghdr.py +# SPDX-License-Identifier: PSF-2.0 + +"""Recognize image file formats based on their first few bytes.""" + +from os import PathLike +import warnings + +__all__ = ["what"] + + +# warnings._deprecated(__name__, remove=(3, 13)) + + +#-------------------------# +# Recognize image headers # +#-------------------------# + +def what(file, h=None): + """Return the type of image contained in a file or byte stream.""" + f = None + try: + if h is None: + if isinstance(file, (str, PathLike)): + f = open(file, 'rb') + h = f.read(32) + else: + location = file.tell() + h = file.read(32) + file.seek(location) + for tf in tests: + res = tf(h, f) + if res: + return res + finally: + if f: f.close() + return None + + +#---------------------------------# +# Subroutines per image file type # +#---------------------------------# + +tests = [] + +def test_jpeg(h, f): + """Test for JPEG data with JFIF or Exif markers; and raw JPEG.""" + if h[6:10] in (b'JFIF', b'Exif'): + return 'jpeg' + elif h[:4] == b'\xff\xd8\xff\xdb': + return 'jpeg' + +tests.append(test_jpeg) + +def test_png(h, f): + """Verify if the image is a PNG.""" + if h.startswith(b'\211PNG\r\n\032\n'): + return 'png' + +tests.append(test_png) + +def test_gif(h, f): + """Verify if the image is a GIF ('87 or '89 variants).""" + if h[:6] in (b'GIF87a', b'GIF89a'): + return 'gif' + +tests.append(test_gif) + +def test_tiff(h, f): + """Verify if the image is a TIFF (can be in Motorola or Intel byte order).""" + if h[:2] in (b'MM', b'II'): + return 'tiff' + +tests.append(test_tiff) + +def test_rgb(h, f): + """test for the SGI image library.""" + if h.startswith(b'\001\332'): + return 'rgb' + +tests.append(test_rgb) + +def test_pbm(h, f): + """Verify if the image is a PBM (portable bitmap).""" + if len(h) >= 3 and \ + h[0] == ord(b'P') and h[1] in b'14' and h[2] in b' \t\n\r': + return 'pbm' + +tests.append(test_pbm) + +def test_pgm(h, f): + """Verify if the image is a PGM (portable graymap).""" + if len(h) >= 3 and \ + h[0] == ord(b'P') and h[1] in b'25' and h[2] in b' \t\n\r': + return 'pgm' + +tests.append(test_pgm) + +def test_ppm(h, f): + """Verify if the image is a PPM (portable pixmap).""" + if len(h) >= 3 and \ + h[0] == ord(b'P') and h[1] in b'36' and h[2] in b' \t\n\r': + return 'ppm' + +tests.append(test_ppm) + +def test_rast(h, f): + """test for the Sun raster file.""" + if h.startswith(b'\x59\xA6\x6A\x95'): + return 'rast' + +tests.append(test_rast) + +def test_xbm(h, f): + """Verify if the image is a X bitmap (X10 or X11).""" + if h.startswith(b'#define '): + return 'xbm' + +tests.append(test_xbm) + +def test_bmp(h, f): + """Verify if the image is a BMP file.""" + if h.startswith(b'BM'): + return 'bmp' + +tests.append(test_bmp) + +def test_webp(h, f): + """Verify if the image is a WebP.""" + if h.startswith(b'RIFF') and h[8:12] == b'WEBP': + return 'webp' + +tests.append(test_webp) + +def test_exr(h, f): + """verify is the image ia a OpenEXR fileOpenEXR.""" + if h.startswith(b'\x76\x2f\x31\x01'): + return 'exr' + +tests.append(test_exr) diff --git a/mitmproxy/contrib/kaitaistruct/LICENSE b/mitmproxy/contrib/kaitaistruct/LICENSE new file mode 100644 index 0000000000..45da807914 --- /dev/null +++ b/mitmproxy/contrib/kaitaistruct/LICENSE @@ -0,0 +1 @@ +Either MIT or CC-0 - see the individual .ksy files for the respective license. diff --git a/mitmproxy/contrib/kaitaistruct/README.md b/mitmproxy/contrib/kaitaistruct/README.md new file mode 100644 index 0000000000..ed7104cd94 --- /dev/null +++ b/mitmproxy/contrib/kaitaistruct/README.md @@ -0,0 +1,3 @@ +# Kaitai Struct Formats + +Most of the formats here are vendored from https://github.com/kaitai-io/kaitai_struct_formats/. diff --git a/mitmproxy/contrib/kaitaistruct/dtls_client_hello.ksy b/mitmproxy/contrib/kaitaistruct/dtls_client_hello.ksy index b0a271eca6..43a77cf972 100644 --- a/mitmproxy/contrib/kaitaistruct/dtls_client_hello.ksy +++ b/mitmproxy/contrib/kaitaistruct/dtls_client_hello.ksy @@ -1,6 +1,7 @@ meta: id: dtls_client_hello endian: be + license: MIT seq: - id: version diff --git a/mitmproxy/contrib/kaitaistruct/exif.py b/mitmproxy/contrib/kaitaistruct/exif.py index f0cadd4ae6..464849e420 100644 --- a/mitmproxy/contrib/kaitaistruct/exif.py +++ b/mitmproxy/contrib/kaitaistruct/exif.py @@ -1,7 +1,7 @@ # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild import kaitaistruct -from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO +from kaitaistruct import KaitaiStream, KaitaiStruct from enum import Enum diff --git a/mitmproxy/contrib/kaitaistruct/google_protobuf.py b/mitmproxy/contrib/kaitaistruct/google_protobuf.py index de06e8d267..48f5e0ec9b 100644 --- a/mitmproxy/contrib/kaitaistruct/google_protobuf.py +++ b/mitmproxy/contrib/kaitaistruct/google_protobuf.py @@ -1,7 +1,7 @@ # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild import kaitaistruct -from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO +from kaitaistruct import KaitaiStream, KaitaiStruct from enum import Enum diff --git a/mitmproxy/contrib/kaitaistruct/ico.py b/mitmproxy/contrib/kaitaistruct/ico.py index 966bfb9481..39b34ae49a 100644 --- a/mitmproxy/contrib/kaitaistruct/ico.py +++ b/mitmproxy/contrib/kaitaistruct/ico.py @@ -1,7 +1,7 @@ # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild import kaitaistruct -from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO +from kaitaistruct import KaitaiStruct if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): diff --git a/mitmproxy/contrib/kaitaistruct/vlq_base128_le.py b/mitmproxy/contrib/kaitaistruct/vlq_base128_le.py index 6f7a37f800..78f28c28d3 100644 --- a/mitmproxy/contrib/kaitaistruct/vlq_base128_le.py +++ b/mitmproxy/contrib/kaitaistruct/vlq_base128_le.py @@ -1,7 +1,7 @@ # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild import kaitaistruct -from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO +from kaitaistruct import KaitaiStruct if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): diff --git a/mitmproxy/contrib/tornado/__init__.py b/mitmproxy/contrib/tornado/__init__.py deleted file mode 100644 index c7000a7524..0000000000 --- a/mitmproxy/contrib/tornado/__init__.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -SPDX-License-Identifier: Apache-2.0 - -Vendored partial copy of https://github.com/tornadoweb/tornado/blob/master/tornado/platform/asyncio.py @ e18ea03 -to fix https://github.com/tornadoweb/tornado/issues/3092. Can be removed once tornado >6.1 is out. -""" -import errno - -import select -import tornado -import tornado.platform.asyncio - - -def patch_tornado(): - if tornado.version != "6.1": - return - - def _run_select(self) -> None: - while True: - with self._select_cond: - while self._select_args is None and not self._closing_selector: - self._select_cond.wait() - if self._closing_selector: - return - assert self._select_args is not None - to_read, to_write = self._select_args - self._select_args = None - - # We use the simpler interface of the select module instead of - # the more stateful interface in the selectors module because - # this class is only intended for use on windows, where - # select.select is the only option. The selector interface - # does not have well-documented thread-safety semantics that - # we can rely on so ensuring proper synchronization would be - # tricky. - try: - # On windows, selecting on a socket for write will not - # return the socket when there is an error (but selecting - # for reads works). Also select for errors when selecting - # for writes, and merge the results. - # - # This pattern is also used in - # https://github.com/python/cpython/blob/v3.8.0/Lib/selectors.py#L312-L317 - rs, ws, xs = select.select(to_read, to_write, to_write) - ws = ws + xs - except OSError as e: - # After remove_reader or remove_writer is called, the file - # descriptor may subsequently be closed on the event loop - # thread. It's possible that this select thread hasn't - # gotten into the select system call by the time that - # happens in which case (at least on macOS), select may - # raise a "bad file descriptor" error. If we get that - # error, check and see if we're also being woken up by - # polling the waker alone. If we are, just return to the - # event loop and we'll get the updated set of file - # descriptors on the next iteration. Otherwise, raise the - # original error. - if e.errno == getattr(errno, "WSAENOTSOCK", errno.EBADF): - rs, _, _ = select.select([self._waker_r.fileno()], [], [], 0) - if rs: - ws = [] - else: - raise - else: - raise - - try: - self._real_loop.call_soon_threadsafe(self._handle_select, rs, ws) - except RuntimeError: - # "Event loop is closed". Swallow the exception for - # consistency with PollIOLoop (and logical consistency - # with the fact that we can't guarantee that an - # add_callback that completes without error will - # eventually execute). - pass - except AttributeError: - # ProactorEventLoop may raise this instead of RuntimeError - # if call_soon_threadsafe races with a call to close(). - # Swallow it too for consistency. - pass - - tornado.platform.asyncio.AddThreadSelectorEventLoop._run_select = _run_select diff --git a/mitmproxy/contrib/urwid/__init__.py b/mitmproxy/contrib/urwid/__init__.py deleted file mode 100644 index c83ecbdda8..0000000000 --- a/mitmproxy/contrib/urwid/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import escape_patches diff --git a/mitmproxy/contrib/urwid/escape_patches.py b/mitmproxy/contrib/urwid/escape_patches.py deleted file mode 100644 index ac17c3a422..0000000000 --- a/mitmproxy/contrib/urwid/escape_patches.py +++ /dev/null @@ -1,254 +0,0 @@ -# monkeypatch https://github.com/urwid/urwid/commit/e2423b5069f51d318ea1ac0f355a0efe5448f7eb into the urwid sources. -import urwid.escape - -if urwid.__version__ in ("2.1.1", "2.1.2"): - # fmt: off - urwid.escape.input_sequences = [ - ('[A','up'),('[B','down'),('[C','right'),('[D','left'), - ('[E','5'),('[F','end'),('[G','5'),('[H','home'), - - ('[1~','home'),('[2~','insert'),('[3~','delete'),('[4~','end'), - ('[5~','page up'),('[6~','page down'), - ('[7~','home'),('[8~','end'), - - ('[[A','f1'),('[[B','f2'),('[[C','f3'),('[[D','f4'),('[[E','f5'), - - ('[11~','f1'),('[12~','f2'),('[13~','f3'),('[14~','f4'), - ('[15~','f5'),('[17~','f6'),('[18~','f7'),('[19~','f8'), - ('[20~','f9'),('[21~','f10'),('[23~','f11'),('[24~','f12'), - ('[25~','f13'),('[26~','f14'),('[28~','f15'),('[29~','f16'), - ('[31~','f17'),('[32~','f18'),('[33~','f19'),('[34~','f20'), - - ('OA','up'),('OB','down'),('OC','right'),('OD','left'), - ('OH','home'),('OF','end'), - ('OP','f1'),('OQ','f2'),('OR','f3'),('OS','f4'), - ('Oo','/'),('Oj','*'),('Om','-'),('Ok','+'), - - ('[Z','shift tab'), - ('On', '.'), - - ('[200~', 'begin paste'), ('[201~', 'end paste'), - ] + [ - (prefix + letter, modifier + key) - for prefix, modifier in zip('O[', ('meta ', 'shift ')) - for letter, key in zip('abcd', ('up', 'down', 'right', 'left')) - ] + [ - ("[" + digit + symbol, modifier + key) - for modifier, symbol in zip(('shift ', 'meta '), '$^') - for digit, key in zip('235678', - ('insert', 'delete', 'page up', 'page down', 'home', 'end')) - ] + [ - ('O' + chr(ord('p')+n), str(n)) for n in range(10) - ] + [ - # modified cursor keys + home, end, 5 -- [#X and [1;#X forms - (prefix+digit+letter, urwid.escape.escape_modifier(digit) + key) - for prefix in ("[", "[1;") - for digit in "12345678" - for letter,key in zip("ABCDEFGH", - ('up','down','right','left','5','end','5','home')) - ] + [ - # modified F1-F4 keys -- O#X form - ("O"+digit+letter, urwid.escape.escape_modifier(digit) + key) - for digit in "12345678" - for letter,key in zip("PQRS",('f1','f2','f3','f4')) - ] + [ - # modified F1-F13 keys -- [XX;#~ form - ("["+str(num)+";"+digit+"~", urwid.escape.escape_modifier(digit) + key) - for digit in "12345678" - for num,key in zip( - (3,5,6,11,12,13,14,15,17,18,19,20,21,23,24,25,26,28,29,31,32,33,34), - ('delete', 'page up', 'page down', - 'f1','f2','f3','f4','f5','f6','f7','f8','f9','f10','f11', - 'f12','f13','f14','f15','f16','f17','f18','f19','f20')) - ] + [ - # mouse reporting (special handling done in KeyqueueTrie) - ('[M', 'mouse'), - - # mouse reporting for SGR 1006 - ('[<', 'sgrmouse'), - - # report status response - ('[0n', 'status ok') - ] - - - class KeyqueueTrie(object): - def __init__( self, sequences ): - self.data = {} - for s, result in sequences: - assert type(result) != dict - self.add(self.data, s, result) - - def add(self, root, s, result): - assert type(root) == dict, "trie conflict detected" - assert len(s) > 0, "trie conflict detected" - - if ord(s[0]) in root: - return self.add(root[ord(s[0])], s[1:], result) - if len(s)>1: - d = {} - root[ord(s[0])] = d - return self.add(d, s[1:], result) - root[ord(s)] = result - - def get(self, keys, more_available): - result = self.get_recurse(self.data, keys, more_available) - if not result: - result = self.read_cursor_position(keys, more_available) - return result - - def get_recurse(self, root, keys, more_available): - if type(root) != dict: - if root == "mouse": - return self.read_mouse_info(keys, - more_available) - elif root == "sgrmouse": - return self.read_sgrmouse_info (keys, more_available) - return (root, keys) - if not keys: - # get more keys - if more_available: - raise urwid.escape.MoreInputRequired() - return None - if keys[0] not in root: - return None - return self.get_recurse(root[keys[0]], keys[1:], more_available) - - def read_mouse_info(self, keys, more_available): - if len(keys) < 3: - if more_available: - raise urwid.escape.MoreInputRequired() - return None - - b = keys[0] - 32 - x, y = (keys[1] - 33)%256, (keys[2] - 33)%256 # supports 0-255 - - prefix = "" - if b & 4: prefix = prefix + "shift " - if b & 8: prefix = prefix + "meta " - if b & 16: prefix = prefix + "ctrl " - if (b & urwid.escape.MOUSE_MULTIPLE_CLICK_MASK)>>9 == 1: prefix = prefix + "double " - if (b & urwid.escape.MOUSE_MULTIPLE_CLICK_MASK)>>9 == 2: prefix = prefix + "triple " - - # 0->1, 1->2, 2->3, 64->4, 65->5 - button = ((b&64)//64*3) + (b & 3) + 1 - - if b & 3 == 3: - action = "release" - button = 0 - elif b & urwid.escape.MOUSE_RELEASE_FLAG: - action = "release" - elif b & urwid.escape.MOUSE_DRAG_FLAG: - action = "drag" - elif b & urwid.escape.MOUSE_MULTIPLE_CLICK_MASK: - action = "click" - else: - action = "press" - - return ( (prefix + "mouse " + action, button, x, y), keys[3:] ) - - def read_sgrmouse_info(self, keys, more_available): - # Helpful links: - # https://stackoverflow.com/questions/5966903/how-to-get-mousemove-and-mouseclick-in-bash - # http://invisible-island.net/xterm/ctlseqs/ctlseqs.pdf - - if not keys: - if more_available: - raise urwid.escape.MoreInputRequired() - return None - - value = '' - pos_m = 0 - found_m = False - for k in keys: - value = value + chr(k); - if ((k is ord('M')) or (k is ord('m'))): - found_m = True - break; - pos_m += 1 - if not found_m: - if more_available: - raise urwid.escape.MoreInputRequired() - return None - - (b, x, y) = value[:-1].split(';') - - # shift, meta, ctrl etc. is not communicated on my machine, so I - # can't and won't be able to add support for it. - # Double and triple clicks are not supported as well. They can be - # implemented by using a timer. This timer can check if the last - # registered click is below a certain threshold. This threshold - # is normally set in the operating system itself, so setting one - # here will cause an inconsistent behaviour. I do not plan to use - # that feature, so I won't implement it. - - button = ((int(b) & 64) // 64 * 3) + (int(b) & 3) + 1 - x = int(x) - 1 - y = int(y) - 1 - - if (value[-1] == 'M'): - if int(b) & urwid.escape.MOUSE_DRAG_FLAG: - action = "drag" - else: - action = "press" - else: - action = "release" - - return ( ("mouse " + action, button, x, y), keys[pos_m + 1:] ) - - - def read_cursor_position(self, keys, more_available): - """ - Interpret cursor position information being sent by the - user's terminal. Returned as ('cursor position', x, y) - where (x, y) == (0, 0) is the top left of the screen. - """ - if not keys: - if more_available: - raise urwid.escape.MoreInputRequired() - return None - if keys[0] != ord('['): - return None - # read y value - y = 0 - i = 1 - for k in keys[i:]: - i += 1 - if k == ord(';'): - if not y: - return None - break - if k < ord('0') or k > ord('9'): - return None - if not y and k == ord('0'): - return None - y = y * 10 + k - ord('0') - if not keys[i:]: - if more_available: - raise urwid.escape.MoreInputRequired() - return None - # read x value - x = 0 - for k in keys[i:]: - i += 1 - if k == ord('R'): - if not x: - return None - return (("cursor position", x-1, y-1), keys[i:]) - if k < ord('0') or k > ord('9'): - return None - if not x and k == ord('0'): - return None - x = x * 10 + k - ord('0') - if not keys[i:]: - if more_available: - raise urwid.escape.MoreInputRequired() - return None - - urwid.escape.KeyqueueTrie = KeyqueueTrie - urwid.escape.input_trie = KeyqueueTrie(urwid.escape.input_sequences) - - - ESC = urwid.escape.ESC - urwid.escape.MOUSE_TRACKING_ON = ESC+"[?1000h"+ESC+"[?1002h"+ESC+"[?1006h" - urwid.escape.MOUSE_TRACKING_OFF = ESC+"[?1006l"+ESC+"[?1002l"+ESC+"[?1000l" diff --git a/mitmproxy/contrib/urwid/raw_display.py b/mitmproxy/contrib/urwid/raw_display.py deleted file mode 100644 index 8a0aaa451d..0000000000 --- a/mitmproxy/contrib/urwid/raw_display.py +++ /dev/null @@ -1,1183 +0,0 @@ -#!/usr/bin/python -# -# Urwid raw display module -# Copyright (C) 2004-2009 Ian Ward -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# Urwid web site: http://excess.org/urwid/ - -from __future__ import division, print_function - -""" -Direct terminal UI implementation -""" - -import os -import select -import struct -import sys -import signal -import socket -import threading - - -if os.name == "nt": - IS_WINDOWS = True - from . import win32 - from ctypes import byref -else: - IS_WINDOWS = False - import fcntl - import termios - import tty - -from urwid import util -from urwid import escape -from urwid.display_common import BaseScreen, RealTerminal, \ - UPDATE_PALETTE_ENTRY, AttrSpec, UNPRINTABLE_TRANS_TABLE, \ - INPUT_DESCRIPTORS_CHANGED -from urwid import signals -from urwid.compat import PYTHON3, bytes, B - -from subprocess import Popen, PIPE - -STDIN = object() - - -class Screen(BaseScreen, RealTerminal): - def __init__(self, input=STDIN, output=sys.stdout): - """Initialize a screen that directly prints escape codes to an output - terminal. - """ - super(Screen, self).__init__() - self._pal_escape = {} - self._pal_attrspec = {} - signals.connect_signal(self, UPDATE_PALETTE_ENTRY, - self._on_update_palette_entry) - self.colors = 16 # FIXME: detect this - self.has_underline = True # FIXME: detect this - self._keyqueue = [] - self.prev_input_resize = 0 - self.set_input_timeouts() - self.screen_buf = None - self._screen_buf_canvas = None - self._resized = False - self.maxrow = None - self.gpm_mev = None - self.gpm_event_pending = False - self._mouse_tracking_enabled = False - self.last_bstate = 0 - self._setup_G1_done = False - self._rows_used = None - self._cy = 0 - self.term = os.environ.get('TERM', '') - self.fg_bright_is_bold = not self.term.startswith("xterm") - self.bg_bright_is_blink = (self.term == "linux") - self.back_color_erase = not self.term.startswith("screen") - self.register_palette_entry( None, 'default','default') - self._next_timeout = None - self.signal_handler_setter = signal.signal - - # Our connections to the world - self._term_output_file = output - if input is STDIN: - if IS_WINDOWS: - input, self._send_input = socket.socketpair() - else: - input = sys.stdin - self._term_input_file = input - - # pipe for signalling external event loops about resize events - self._resize_pipe_rd, self._resize_pipe_wr = socket.socketpair() - self._resize_pipe_rd.setblocking(False) - - def _input_fileno(self): - """Returns the fileno of the input stream, or None if it doesn't have one. A stream without a fileno can't participate in whatever. - """ - if hasattr(self._term_input_file, 'fileno'): - return self._term_input_file.fileno() - else: - return None - - def _on_update_palette_entry(self, name, *attrspecs): - # copy the attribute to a dictionary containing the escape seqences - a = attrspecs[{16:0,1:1,88:2,256:3,2**24:4}[self.colors]] - self._pal_attrspec[name] = a - self._pal_escape[name] = self._attrspec_to_escape(a) - - def set_input_timeouts(self, max_wait=None, complete_wait=0.125, - resize_wait=0.125): - """ - Set the get_input timeout values. All values are in floating - point numbers of seconds. - - max_wait -- amount of time in seconds to wait for input when - there is no input pending, wait forever if None - complete_wait -- amount of time in seconds to wait when - get_input detects an incomplete escape sequence at the - end of the available input - resize_wait -- amount of time in seconds to wait for more input - after receiving two screen resize requests in a row to - stop Urwid from consuming 100% cpu during a gradual - window resize operation - """ - self.max_wait = max_wait - if max_wait is not None: - if self._next_timeout is None: - self._next_timeout = max_wait - else: - self._next_timeout = min(self._next_timeout, self.max_wait) - self.complete_wait = complete_wait - self.resize_wait = resize_wait - - def _sigwinch_handler(self, signum, frame=None): - """ - frame -- will always be None when the GLib event loop is being used. - """ - if not self._resized: - self._resize_pipe_wr.send(B("R")) - self._resized = True - self.screen_buf = None - - def _sigcont_handler(self, signum, frame=None): - """ - frame -- will always be None when the GLib event loop is being used. - """ - - self.stop() - self.start() - self._sigwinch_handler(None, None) - - def signal_init(self): - """ - Called in the startup of run wrapper to set the SIGWINCH - and SIGCONT signal handlers. - - Override this function to call from main thread in threaded - applications. - """ - self.signal_handler_setter(signal.SIGWINCH, self._sigwinch_handler) - self.signal_handler_setter(signal.SIGCONT, self._sigcont_handler) - - def signal_restore(self): - """ - Called in the finally block of run wrapper to restore the - SIGWINCH and SIGCONT signal handlers. - - Override this function to call from main thread in threaded - applications. - """ - self.signal_handler_setter(signal.SIGCONT, signal.SIG_DFL) - self.signal_handler_setter(signal.SIGWINCH, signal.SIG_DFL) - - def set_mouse_tracking(self, enable=True): - """ - Enable (or disable) mouse tracking. - - After calling this function get_input will include mouse - click events along with keystrokes. - """ - enable = bool(enable) - if enable == self._mouse_tracking_enabled: - return - - self._mouse_tracking(enable) - self._mouse_tracking_enabled = enable - - def _mouse_tracking(self, enable): - if enable: - self.write(escape.MOUSE_TRACKING_ON) - self._start_gpm_tracking() - else: - self.write(escape.MOUSE_TRACKING_OFF) - self._stop_gpm_tracking() - - def _start_gpm_tracking(self): - if not os.path.isfile("/usr/bin/mev"): - return - if not os.environ.get('TERM',"").lower().startswith("linux"): - return - if not Popen: - return - m = Popen(["/usr/bin/mev","-e","158"], stdin=PIPE, stdout=PIPE, - close_fds=True) - fcntl.fcntl(m.stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK) - self.gpm_mev = m - - def _stop_gpm_tracking(self): - if not self.gpm_mev: - return - os.kill(self.gpm_mev.pid, signal.SIGINT) - os.waitpid(self.gpm_mev.pid, 0) - self.gpm_mev = None - - _dwOriginalOutMode = None - _dwOriginalInMode = None - - def _start(self, alternate_buffer=True): - """ - Initialize the screen and input mode. - - alternate_buffer -- use alternate screen buffer - """ - if alternate_buffer: - self.write(escape.SWITCH_TO_ALTERNATE_BUFFER) - self._rows_used = None - else: - self._rows_used = 0 - - fd = self._input_fileno() - if fd is not None and os.isatty(fd) and not IS_WINDOWS: - self._old_termios_settings = termios.tcgetattr(fd) - tty.setcbreak(fd) - - if IS_WINDOWS: - hOut = win32.GetStdHandle(win32.STD_OUTPUT_HANDLE) - hIn = win32.GetStdHandle(win32.STD_INPUT_HANDLE) - self._dwOriginalOutMode = win32.DWORD() - self._dwOriginalInMode = win32.DWORD() - win32.GetConsoleMode(hOut, byref(self._dwOriginalOutMode)) - win32.GetConsoleMode(hIn, byref(self._dwOriginalInMode)) - # TODO: Restore on exit - - dwOutMode = win32.DWORD( - self._dwOriginalOutMode.value | win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING | win32.DISABLE_NEWLINE_AUTO_RETURN) - dwInMode = win32.DWORD( - self._dwOriginalInMode.value | win32.ENABLE_WINDOW_INPUT | win32.ENABLE_VIRTUAL_TERMINAL_INPUT - ) - - ok = win32.SetConsoleMode(hOut, dwOutMode) - if not ok: - raise RuntimeError("Error enabling virtual terminal processing, " - "mitmproxy's console interface requires Windows 10 Build 10586 or above.") - ok = win32.SetConsoleMode(hIn, dwInMode) - assert ok - else: - self.signal_init() - self._alternate_buffer = alternate_buffer - self._next_timeout = self.max_wait - - if not self._signal_keys_set and not IS_WINDOWS: - self._old_signal_keys = self.tty_signal_keys(fileno=fd) - - signals.emit_signal(self, INPUT_DESCRIPTORS_CHANGED) - # restore mouse tracking to previous state - self._mouse_tracking(self._mouse_tracking_enabled) - - return super(Screen, self)._start() - - def _stop(self): - """ - Restore the screen. - """ - self.clear() - - signals.emit_signal(self, INPUT_DESCRIPTORS_CHANGED) - - if not IS_WINDOWS: - self.signal_restore() - - fd = self._input_fileno() - if fd is not None and os.isatty(fd) and not IS_WINDOWS: - termios.tcsetattr(fd, termios.TCSADRAIN, self._old_termios_settings) - - self._mouse_tracking(False) - - move_cursor = "" - if self._alternate_buffer: - move_cursor = escape.RESTORE_NORMAL_BUFFER - elif self.maxrow is not None: - move_cursor = escape.set_cursor_position( - 0, self.maxrow) - self.write( - self._attrspec_to_escape(AttrSpec('','')) - + escape.SI - + move_cursor - + escape.SHOW_CURSOR) - self.flush() - - if self._old_signal_keys: - self.tty_signal_keys(*(self._old_signal_keys + (fd,))) - - if IS_WINDOWS: - hOut = win32.GetStdHandle(win32.STD_OUTPUT_HANDLE) - hIn = win32.GetStdHandle(win32.STD_INPUT_HANDLE) - ok = win32.SetConsoleMode(hOut, self._dwOriginalOutMode) - assert ok - ok = win32.SetConsoleMode(hIn, self._dwOriginalInMode) - assert ok - - super(Screen, self)._stop() - - - def write(self, data): - """Write some data to the terminal. - - You may wish to override this if you're using something other than - regular files for input and output. - """ - self._term_output_file.write(data) - - def flush(self): - """Flush the output buffer. - - You may wish to override this if you're using something other than - regular files for input and output. - """ - self._term_output_file.flush() - - def get_input(self, raw_keys=False): - """Return pending input as a list. - - raw_keys -- return raw keycodes as well as translated versions - - This function will immediately return all the input since the - last time it was called. If there is no input pending it will - wait before returning an empty list. The wait time may be - configured with the set_input_timeouts function. - - If raw_keys is False (default) this function will return a list - of keys pressed. If raw_keys is True this function will return - a ( keys pressed, raw keycodes ) tuple instead. - - Examples of keys returned: - - * ASCII printable characters: " ", "a", "0", "A", "-", "/" - * ASCII control characters: "tab", "enter" - * Escape sequences: "up", "page up", "home", "insert", "f1" - * Key combinations: "shift f1", "meta a", "ctrl b" - * Window events: "window resize" - - When a narrow encoding is not enabled: - - * "Extended ASCII" characters: "\\xa1", "\\xb2", "\\xfe" - - When a wide encoding is enabled: - - * Double-byte characters: "\\xa1\\xea", "\\xb2\\xd4" - - When utf8 encoding is enabled: - - * Unicode characters: u"\\u00a5", u'\\u253c" - - Examples of mouse events returned: - - * Mouse button press: ('mouse press', 1, 15, 13), - ('meta mouse press', 2, 17, 23) - * Mouse drag: ('mouse drag', 1, 16, 13), - ('mouse drag', 1, 17, 13), - ('ctrl mouse drag', 1, 18, 13) - * Mouse button release: ('mouse release', 0, 18, 13), - ('ctrl mouse release', 0, 17, 23) - """ - assert self._started - - self._wait_for_input_ready(self._next_timeout) - keys, raw = self.parse_input(None, None, self.get_available_raw_input()) - - # Avoid pegging CPU at 100% when slowly resizing - if keys==['window resize'] and self.prev_input_resize: - while True: - self._wait_for_input_ready(self.resize_wait) - keys, raw2 = self.parse_input(None, None, self.get_available_raw_input()) - raw += raw2 - #if not keys: - # keys, raw2 = self._get_input( - # self.resize_wait) - # raw += raw2 - if keys!=['window resize']: - break - if keys[-1:]!=['window resize']: - keys.append('window resize') - - if keys==['window resize']: - self.prev_input_resize = 2 - elif self.prev_input_resize == 2 and not keys: - self.prev_input_resize = 1 - else: - self.prev_input_resize = 0 - - if raw_keys: - return keys, raw - return keys - - def get_input_descriptors(self): - """ - Return a list of integer file descriptors that should be - polled in external event loops to check for user input. - - Use this method if you are implementing your own event loop. - - This method is only called by `hook_event_loop`, so if you override - that, you can safely ignore this. - """ - if not self._started: - return [] - - fd_list = [self._resize_pipe_rd] - fd = self._input_fileno() - if fd is not None: - fd_list.append(fd) - if self.gpm_mev is not None: - fd_list.append(self.gpm_mev.stdout.fileno()) - return fd_list - - _current_event_loop_handles = () - - def unhook_event_loop(self, event_loop): - """ - Remove any hooks added by hook_event_loop. - """ - if self._input_thread is not None: - self._input_thread.should_exit = True - self._input_thread = None - - for handle in self._current_event_loop_handles: - try: - event_loop.remove_watch_file(handle) - except KeyError: - pass - - if self._input_timeout: - event_loop.remove_alarm(self._input_timeout) - self._input_timeout = None - - def hook_event_loop(self, event_loop, callback): - """ - Register the given callback with the event loop, to be called with new - input whenever it's available. The callback should be passed a list of - processed keys and a list of unprocessed keycodes. - - Subclasses may wish to use parse_input to wrap the callback. - """ - if IS_WINDOWS: - self._input_thread = ReadInputThread(self._send_input, lambda: self._sigwinch_handler(0)) - self._input_thread.start() - if hasattr(self, 'get_input_nonblocking'): - wrapper = self._make_legacy_input_wrapper(event_loop, callback) - else: - wrapper = lambda: self.parse_input( - event_loop, callback, self.get_available_raw_input()) - fds = self.get_input_descriptors() - handles = [event_loop.watch_file(fd, wrapper) for fd in fds] - self._current_event_loop_handles = handles - - _input_thread = None - _input_timeout = None - _partial_codes = None - - def _make_legacy_input_wrapper(self, event_loop, callback): - """ - Support old Screen classes that still have a get_input_nonblocking and - expect it to work. - """ - def wrapper(): - if self._input_timeout: - event_loop.remove_alarm(self._input_timeout) - self._input_timeout = None - timeout, keys, raw = self.get_input_nonblocking() - if timeout is not None: - self._input_timeout = event_loop.alarm(timeout, wrapper) - - callback(keys, raw) - - return wrapper - - def get_available_raw_input(self): - """ - Return any currently-available input. Does not block. - - This method is only used by the default `hook_event_loop` - implementation; you can safely ignore it if you implement your own. - """ - codes = self._get_gpm_codes() + self._get_keyboard_codes() - - if self._partial_codes: - codes = self._partial_codes + codes - self._partial_codes = None - - # clean out the pipe used to signal external event loops - # that a resize has occurred - try: - while True: self._resize_pipe_rd.recv(1) - except OSError: - pass - - return codes - - def parse_input(self, event_loop, callback, codes, wait_for_more=True): - """ - Read any available input from get_available_raw_input, parses it into - keys, and calls the given callback. - - The current implementation tries to avoid any assumptions about what - the screen or event loop look like; it only deals with parsing keycodes - and setting a timeout when an incomplete one is detected. - - `codes` should be a sequence of keycodes, i.e. bytes. A bytearray is - appropriate, but beware of using bytes, which only iterates as integers - on Python 3. - """ - # Note: event_loop may be None for 100% synchronous support, only used - # by get_input. Not documented because you shouldn't be doing it. - if self._input_timeout and event_loop: - event_loop.remove_alarm(self._input_timeout) - self._input_timeout = None - - original_codes = codes - processed = [] - try: - while codes: - run, codes = escape.process_keyqueue( - codes, wait_for_more) - processed.extend(run) - except escape.MoreInputRequired: - # Set a timer to wait for the rest of the input; if it goes off - # without any new input having come in, use the partial input - k = len(original_codes) - len(codes) - processed_codes = original_codes[:k] - self._partial_codes = codes - - def _parse_incomplete_input(): - self._input_timeout = None - self._partial_codes = None - self.parse_input( - event_loop, callback, codes, wait_for_more=False) - if event_loop: - self._input_timeout = event_loop.alarm( - self.complete_wait, _parse_incomplete_input) - - else: - processed_codes = original_codes - self._partial_codes = None - - if self._resized: - processed.append('window resize') - self._resized = False - - if callback: - callback(processed, processed_codes) - else: - # For get_input - return processed, processed_codes - - def _get_keyboard_codes(self): - codes = [] - while True: - code = self._getch_nodelay() - if code < 0: - break - codes.append(code) - return codes - - def _get_gpm_codes(self): - codes = [] - try: - while self.gpm_mev is not None and self.gpm_event_pending: - codes.extend(self._encode_gpm_event()) - except IOError as e: - if e.args[0] != 11: - raise - return codes - - def _wait_for_input_ready(self, timeout): - ready = None - fd_list = [] - fd = self._input_fileno() - if fd is not None: - fd_list.append(fd) - if self.gpm_mev is not None: - fd_list.append(self.gpm_mev.stdout.fileno()) - while True: - try: - if timeout is None: - ready,w,err = select.select( - fd_list, [], fd_list) - else: - ready,w,err = select.select( - fd_list,[],fd_list, timeout) - break - except select.error as e: - if e.args[0] != 4: - raise - if self._resized: - ready = [] - break - return ready - - def _getch(self, timeout): - ready = self._wait_for_input_ready(timeout) - if self.gpm_mev is not None: - if self.gpm_mev.stdout.fileno() in ready: - self.gpm_event_pending = True - fd = self._input_fileno() - if fd is not None and fd in ready: - if IS_WINDOWS: - return ord(self._term_input_file.recv(1)) - else: - return ord(os.read(fd, 1)) - return -1 - - def _encode_gpm_event( self ): - self.gpm_event_pending = False - s = self.gpm_mev.stdout.readline().decode('ascii') - l = s.split(",") - if len(l) != 6: - # unexpected output, stop tracking - self._stop_gpm_tracking() - signals.emit_signal(self, INPUT_DESCRIPTORS_CHANGED) - return [] - ev, x, y, ign, b, m = s.split(",") - ev = int( ev.split("x")[-1], 16) - x = int( x.split(" ")[-1] ) - y = int( y.lstrip().split(" ")[0] ) - b = int( b.split(" ")[-1] ) - m = int( m.split("x")[-1].rstrip(), 16 ) - - # convert to xterm-like escape sequence - - last = next = self.last_bstate - l = [] - - mod = 0 - if m & 1: mod |= 4 # shift - if m & 10: mod |= 8 # alt - if m & 4: mod |= 16 # ctrl - - def append_button( b ): - b |= mod - l.extend([ 27, ord('['), ord('M'), b+32, x+32, y+32 ]) - - def determine_button_release( flag ): - if b & 4 and last & 1: - append_button( 0 + flag ) - next |= 1 - if b & 2 and last & 2: - append_button( 1 + flag ) - next |= 2 - if b & 1 and last & 4: - append_button( 2 + flag ) - next |= 4 - - if ev == 20 or ev == 36 or ev == 52: # press - if b & 4 and last & 1 == 0: - append_button( 0 ) - next |= 1 - if b & 2 and last & 2 == 0: - append_button( 1 ) - next |= 2 - if b & 1 and last & 4 == 0: - append_button( 2 ) - next |= 4 - elif ev == 146: # drag - if b & 4: - append_button( 0 + escape.MOUSE_DRAG_FLAG ) - elif b & 2: - append_button( 1 + escape.MOUSE_DRAG_FLAG ) - elif b & 1: - append_button( 2 + escape.MOUSE_DRAG_FLAG ) - else: # release - if b & 4 and last & 1: - append_button( 0 + escape.MOUSE_RELEASE_FLAG ) - next &= ~ 1 - if b & 2 and last & 2: - append_button( 1 + escape.MOUSE_RELEASE_FLAG ) - next &= ~ 2 - if b & 1 and last & 4: - append_button( 2 + escape.MOUSE_RELEASE_FLAG ) - next &= ~ 4 - if ev == 40: # double click (release) - if b & 4 and last & 1: - append_button( 0 + escape.MOUSE_MULTIPLE_CLICK_FLAG ) - if b & 2 and last & 2: - append_button( 1 + escape.MOUSE_MULTIPLE_CLICK_FLAG ) - if b & 1 and last & 4: - append_button( 2 + escape.MOUSE_MULTIPLE_CLICK_FLAG ) - elif ev == 52: - if b & 4 and last & 1: - append_button( 0 + escape.MOUSE_MULTIPLE_CLICK_FLAG*2 ) - if b & 2 and last & 2: - append_button( 1 + escape.MOUSE_MULTIPLE_CLICK_FLAG*2 ) - if b & 1 and last & 4: - append_button( 2 + escape.MOUSE_MULTIPLE_CLICK_FLAG*2 ) - - self.last_bstate = next - return l - - def _getch_nodelay(self): - return self._getch(0) - - - def get_cols_rows(self): - """Return the terminal dimensions (num columns, num rows).""" - y, x = 24, 80 - try: - if hasattr(self._term_output_file, 'fileno'): - if IS_WINDOWS: - assert self._term_output_file == sys.stdout - handle = win32.GetStdHandle(win32.STD_OUTPUT_HANDLE) - info = win32.CONSOLE_SCREEN_BUFFER_INFO() - ok = win32.GetConsoleScreenBufferInfo(handle, byref(info)) - if ok == 0: - raise IOError() - y, x = info.dwSize.Y, info.dwSize.X - else: - buf = fcntl.ioctl(self._term_output_file.fileno(), - termios.TIOCGWINSZ, ' '*4) - y, x = struct.unpack('hh', buf) - except IOError: - # Term size could not be determined - pass - self.maxrow = y - return x, y - - def _setup_G1(self): - """ - Initialize the G1 character set to graphics mode if required. - """ - if self._setup_G1_done: - return - - while True: - try: - self.write(escape.DESIGNATE_G1_SPECIAL) - self.flush() - break - except IOError: - pass - self._setup_G1_done = True - - - def draw_screen(self, maxres, r ): - """Paint screen with rendered canvas.""" - - (maxcol, maxrow) = maxres - - assert self._started - - assert maxrow == r.rows() - - # quick return if nothing has changed - if self.screen_buf and r is self._screen_buf_canvas: - return - - self._setup_G1() - - if self._resized: - # handle resize before trying to draw screen - return - - o = [escape.HIDE_CURSOR, self._attrspec_to_escape(AttrSpec('',''))] - - def partial_display(): - # returns True if the screen is in partial display mode - # ie. only some rows belong to the display - return self._rows_used is not None - - if not partial_display(): - o.append(escape.CURSOR_HOME) - - if self.screen_buf: - osb = self.screen_buf - else: - osb = [] - sb = [] - cy = self._cy - y = -1 - - def set_cursor_home(): - if not partial_display(): - return escape.set_cursor_position(0, 0) - return (escape.CURSOR_HOME_COL + - escape.move_cursor_up(cy)) - - def set_cursor_row(y): - if not partial_display(): - return escape.set_cursor_position(0, y) - return escape.move_cursor_down(y - cy) - - def set_cursor_position(x, y): - if not partial_display(): - return escape.set_cursor_position(x, y) - if cy > y: - return ('\b' + escape.CURSOR_HOME_COL + - escape.move_cursor_up(cy - y) + - escape.move_cursor_right(x)) - return ('\b' + escape.CURSOR_HOME_COL + - escape.move_cursor_down(y - cy) + - escape.move_cursor_right(x)) - - def is_blank_row(row): - if len(row) > 1: - return False - if row[0][2].strip(): - return False - return True - - def attr_to_escape(a): - if a in self._pal_escape: - return self._pal_escape[a] - elif isinstance(a, AttrSpec): - return self._attrspec_to_escape(a) - # undefined attributes use default/default - # TODO: track and report these - return self._attrspec_to_escape( - AttrSpec('default','default')) - - def using_standout_or_underline(a): - a = self._pal_attrspec.get(a, a) - return isinstance(a, AttrSpec) and (a.standout or a.underline) - - ins = None - o.append(set_cursor_home()) - cy = 0 - for row in r.content(): - y += 1 - if osb and y < len(osb) and osb[y] == row: - # this row of the screen buffer matches what is - # currently displayed, so we can skip this line - sb.append( osb[y] ) - continue - - sb.append(row) - - # leave blank lines off display when we are using - # the default screen buffer (allows partial screen) - if partial_display() and y > self._rows_used: - if is_blank_row(row): - continue - self._rows_used = y - - if y or partial_display(): - o.append(set_cursor_position(0, y)) - # after updating the line we will be just over the - # edge, but terminals still treat this as being - # on the same line - cy = y - - whitespace_at_end = False - if row: - a, cs, run = row[-1] - if (run[-1:] == B(' ') and self.back_color_erase - and not using_standout_or_underline(a)): - whitespace_at_end = True - row = row[:-1] + [(a, cs, run.rstrip(B(' ')))] - elif y == maxrow-1 and maxcol > 1: - row, back, ins = self._last_row(row) - - first = True - lasta = lastcs = None - for (a,cs, run) in row: - assert isinstance(run, bytes) # canvases should render with bytes - if cs != 'U': - run = run.translate(UNPRINTABLE_TRANS_TABLE) - if first or lasta != a: - o.append(attr_to_escape(a)) - lasta = a - if first or lastcs != cs: - assert cs in [None, "0", "U"], repr(cs) - if lastcs == "U": - o.append( escape.IBMPC_OFF ) - - if cs is None: - o.append( escape.SI ) - elif cs == "U": - o.append( escape.IBMPC_ON ) - else: - o.append( escape.SO ) - lastcs = cs - o.append( run ) - first = False - if ins: - (inserta, insertcs, inserttext) = ins - ias = attr_to_escape(inserta) - assert insertcs in [None, "0", "U"], repr(insertcs) - if cs is None: - icss = escape.SI - elif cs == "U": - icss = escape.IBMPC_ON - else: - icss = escape.SO - o += [ "\x08"*back, - ias, icss, - escape.INSERT_ON, inserttext, - escape.INSERT_OFF ] - - if cs == "U": - o.append(escape.IBMPC_OFF) - if whitespace_at_end: - o.append(escape.ERASE_IN_LINE_RIGHT) - - if r.cursor is not None: - x,y = r.cursor - o += [set_cursor_position(x, y), - escape.SHOW_CURSOR ] - self._cy = y - - if self._resized: - # handle resize before trying to draw screen - return - try: - for l in o: - if isinstance(l, bytes) and PYTHON3: - l = l.decode('utf-8', 'replace') - self.write(l) - self.flush() - except IOError as e: - # ignore interrupted syscall - if e.args[0] != 4: - raise - - self.screen_buf = sb - self._screen_buf_canvas = r - - - def _last_row(self, row): - """On the last row we need to slide the bottom right character - into place. Calculate the new line, attr and an insert sequence - to do that. - - eg. last row: - XXXXXXXXXXXXXXXXXXXXYZ - - Y will be drawn after Z, shifting Z into position. - """ - - new_row = row[:-1] - z_attr, z_cs, last_text = row[-1] - last_cols = util.calc_width(last_text, 0, len(last_text)) - last_offs, z_col = util.calc_text_pos(last_text, 0, - len(last_text), last_cols-1) - if last_offs == 0: - z_text = last_text - del new_row[-1] - # we need another segment - y_attr, y_cs, nlast_text = row[-2] - nlast_cols = util.calc_width(nlast_text, 0, - len(nlast_text)) - z_col += nlast_cols - nlast_offs, y_col = util.calc_text_pos(nlast_text, 0, - len(nlast_text), nlast_cols-1) - y_text = nlast_text[nlast_offs:] - if nlast_offs: - new_row.append((y_attr, y_cs, - nlast_text[:nlast_offs])) - else: - z_text = last_text[last_offs:] - y_attr, y_cs = z_attr, z_cs - nlast_cols = util.calc_width(last_text, 0, - last_offs) - nlast_offs, y_col = util.calc_text_pos(last_text, 0, - last_offs, nlast_cols-1) - y_text = last_text[nlast_offs:last_offs] - if nlast_offs: - new_row.append((y_attr, y_cs, - last_text[:nlast_offs])) - - new_row.append((z_attr, z_cs, z_text)) - return new_row, z_col-y_col, (y_attr, y_cs, y_text) - - - - def clear(self): - """ - Force the screen to be completely repainted on the next - call to draw_screen(). - """ - self.screen_buf = None - self.setup_G1 = True - - - def _attrspec_to_escape(self, a): - """ - Convert AttrSpec instance a to an escape sequence for the terminal - - >>> s = Screen() - >>> s.set_terminal_properties(colors=256) - >>> a2e = s._attrspec_to_escape - >>> a2e(s.AttrSpec('brown', 'dark green')) - '\\x1b[0;33;42m' - >>> a2e(s.AttrSpec('#fea,underline', '#d0d')) - '\\x1b[0;38;5;229;4;48;5;164m' - """ - if self.term == 'fbterm': - fg = escape.ESC + '[1;%d}' % (a.foreground_number,) - bg = escape.ESC + '[2;%d}' % (a.background_number,) - return fg + bg - - if a.foreground_true: - fg = "38;2;%d;%d;%d" %(a.get_rgb_values()[0:3]) - elif a.foreground_high: - fg = "38;5;%d" % a.foreground_number - elif a.foreground_basic: - if a.foreground_number > 7: - if self.fg_bright_is_bold: - fg = "1;%d" % (a.foreground_number - 8 + 30) - else: - fg = "%d" % (a.foreground_number - 8 + 90) - else: - fg = "%d" % (a.foreground_number + 30) - else: - fg = "39" - st = ("1;" * a.bold + "3;" * a.italics + - "4;" * a.underline + "5;" * a.blink + - "7;" * a.standout + "9;" * a.strikethrough) - if a.background_true: - bg = "48;2;%d;%d;%d" %(a.get_rgb_values()[3:6]) - elif a.background_high: - bg = "48;5;%d" % a.background_number - elif a.background_basic: - if a.background_number > 7: - if self.bg_bright_is_blink: - bg = "5;%d" % (a.background_number - 8 + 40) - else: - # this doesn't work on most terminals - bg = "%d" % (a.background_number - 8 + 100) - else: - bg = "%d" % (a.background_number + 40) - else: - bg = "49" - return escape.ESC + "[0;%s;%s%sm" % (fg, st, bg) - - - def set_terminal_properties(self, colors=None, bright_is_bold=None, - has_underline=None): - """ - colors -- number of colors terminal supports (1, 16, 88, 256, or 2**24) - or None to leave unchanged - bright_is_bold -- set to True if this terminal uses the bold - setting to create bright colors (numbers 8-15), set to False - if this Terminal can create bright colors without bold or - None to leave unchanged - has_underline -- set to True if this terminal can use the - underline setting, False if it cannot or None to leave - unchanged - """ - if colors is None: - colors = self.colors - if bright_is_bold is None: - bright_is_bold = self.fg_bright_is_bold - if has_underline is None: - has_underline = self.has_underline - - if colors == self.colors and bright_is_bold == self.fg_bright_is_bold \ - and has_underline == self.has_underline: - return - - self.colors = colors - self.fg_bright_is_bold = bright_is_bold - self.has_underline = has_underline - - self.clear() - self._pal_escape = {} - for p,v in self._palette.items(): - self._on_update_palette_entry(p, *v) - - - - def reset_default_terminal_palette(self): - """ - Attempt to set the terminal palette to default values as taken - from xterm. Uses number of colors from current - set_terminal_properties() screen setting. - """ - if self.colors == 1: - return - elif self.colors == 2**24: - colors = 256 - else: - colors = self.colors - - def rgb_values(n): - if colors == 16: - aspec = AttrSpec("h%d"%n, "", 256) - else: - aspec = AttrSpec("h%d"%n, "", colors) - return aspec.get_rgb_values()[:3] - - entries = [(n,) + rgb_values(n) for n in range(min(colors, 256))] - self.modify_terminal_palette(entries) - - - def modify_terminal_palette(self, entries): - """ - entries - list of (index, red, green, blue) tuples. - - Attempt to set part of the terminal palette (this does not work - on all terminals.) The changes are sent as a single escape - sequence so they should all take effect at the same time. - - 0 <= index < 256 (some terminals will only have 16 or 88 colors) - 0 <= red, green, blue < 256 - """ - - if self.term == 'fbterm': - modify = ["%d;%d;%d;%d" % (index, red, green, blue) - for index, red, green, blue in entries] - self.write("\x1b[3;"+";".join(modify)+"}") - else: - modify = ["%d;rgb:%02x/%02x/%02x" % (index, red, green, blue) - for index, red, green, blue in entries] - self.write("\x1b]4;"+";".join(modify)+"\x1b\\") - self.flush() - - - # shortcut for creating an AttrSpec with this screen object's - # number of colors - AttrSpec = lambda self, fg, bg: AttrSpec(fg, bg, self.colors) - - -class ReadInputThread(threading.Thread): - name = "urwid Windows input reader" - daemon = True - should_exit: bool = False - _input: socket.socket - - def __init__(self, input, resize): - self._input = input - self._resize = resize - super().__init__() - - def run(self) -> None: - hIn = win32.GetStdHandle(win32.STD_INPUT_HANDLE) - MAX = 2048 - - read = win32.DWORD(0) - arrtype = win32.INPUT_RECORD * MAX - input_records = arrtype() - - while True: - win32.ReadConsoleInputW(hIn, byref(input_records), MAX, byref(read)) - if self.should_exit: - return - for i in range(read.value): - inp = input_records[i] - if inp.EventType == win32.EventType.KEY_EVENT: - if not inp.Event.KeyEvent.bKeyDown: - continue - self._input.send(inp.Event.KeyEvent.uChar.UnicodeChar.encode("utf8")) - elif inp.EventType == win32.EventType.WINDOW_BUFFER_SIZE_EVENT: - self._resize() - else: - pass # TODO: handle mouse events - - -def _test(): - import doctest - doctest.testmod() - -if __name__=='__main__': - _test() diff --git a/mitmproxy/contrib/urwid/win32.py b/mitmproxy/contrib/urwid/win32.py deleted file mode 100644 index 581fcdfd2d..0000000000 --- a/mitmproxy/contrib/urwid/win32.py +++ /dev/null @@ -1,149 +0,0 @@ -from ctypes import Structure, Union, windll, POINTER -from ctypes.wintypes import BOOL, DWORD, WCHAR, WORD, SHORT, UINT, HANDLE, LPDWORD, CHAR - -# https://docs.microsoft.com/de-de/windows/console/getstdhandle -STD_INPUT_HANDLE = -10 -STD_OUTPUT_HANDLE = -11 -STD_ERROR_HANDLE = -12 - -# https://docs.microsoft.com/de-de/windows/console/setconsolemode -ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 -DISABLE_NEWLINE_AUTO_RETURN = 0x0008 -ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200 -ENABLE_WINDOW_INPUT = 0x0008 - - -class COORD(Structure): - """https://docs.microsoft.com/en-us/windows/console/coord-str""" - - _fields_ = [ - ("X", SHORT), - ("Y", SHORT), - ] - - -class SMALL_RECT(Structure): - """https://docs.microsoft.com/en-us/windows/console/small-rect-str""" - - _fields_ = [ - ("Left", SHORT), - ("Top", SHORT), - ("Right", SHORT), - ("Bottom", SHORT), - ] - - -class CONSOLE_SCREEN_BUFFER_INFO(Structure): - """https://docs.microsoft.com/en-us/windows/console/console-screen-buffer-info-str""" - - _fields_ = [ - ("dwSize", COORD), - ("dwCursorPosition", COORD), - ("wAttributes", WORD), - ("srWindow", SMALL_RECT), - ("dwMaximumWindowSize", COORD), - ] - - -class uChar(Union): - """https://docs.microsoft.com/en-us/windows/console/key-event-record-str""" - _fields_ = [ - ("AsciiChar", CHAR), - ("UnicodeChar", WCHAR), - ] - - -class KEY_EVENT_RECORD(Structure): - """https://docs.microsoft.com/en-us/windows/console/key-event-record-str""" - - _fields_ = [ - ("bKeyDown", BOOL), - ("wRepeatCount", WORD), - ("wVirtualKeyCode", WORD), - ("wVirtualScanCode", WORD), - ("uChar", uChar), - ("dwControlKeyState", DWORD), - ] - - -class MOUSE_EVENT_RECORD(Structure): - """https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str""" - - _fields_ = [ - ("dwMousePosition", COORD), - ("dwButtonState", DWORD), - ("dwControlKeyState", DWORD), - ("dwEventFlags", DWORD), - ] - - -class WINDOW_BUFFER_SIZE_RECORD(Structure): - """https://docs.microsoft.com/en-us/windows/console/window-buffer-size-record-str""" - - _fields_ = [("dwSize", COORD)] - - -class MENU_EVENT_RECORD(Structure): - """https://docs.microsoft.com/en-us/windows/console/menu-event-record-str""" - - _fields_ = [("dwCommandId", UINT)] - - -class FOCUS_EVENT_RECORD(Structure): - """https://docs.microsoft.com/en-us/windows/console/focus-event-record-str""" - - _fields_ = [("bSetFocus", BOOL)] - - -class Event(Union): - """https://docs.microsoft.com/en-us/windows/console/input-record-str""" - _fields_ = [ - ("KeyEvent", KEY_EVENT_RECORD), - ("MouseEvent", MOUSE_EVENT_RECORD), - ("WindowBufferSizeEvent", WINDOW_BUFFER_SIZE_RECORD), - ("MenuEvent", MENU_EVENT_RECORD), - ("FocusEvent", FOCUS_EVENT_RECORD), - ] - - -class INPUT_RECORD(Structure): - """https://docs.microsoft.com/en-us/windows/console/input-record-str""" - - _fields_ = [ - ("EventType", WORD), - ("Event", Event) - ] - - -class EventType: - FOCUS_EVENT = 0x0010 - KEY_EVENT = 0x0001 - MENU_EVENT = 0x0008 - MOUSE_EVENT = 0x0002 - WINDOW_BUFFER_SIZE_EVENT = 0x0004 - - -# https://docs.microsoft.com/de-de/windows/console/getstdhandle -GetStdHandle = windll.kernel32.GetStdHandle -GetStdHandle.argtypes = [DWORD] -GetStdHandle.restype = HANDLE - -# https://docs.microsoft.com/de-de/windows/console/getconsolemode -GetConsoleMode = windll.kernel32.GetConsoleMode -GetConsoleMode.argtypes = [HANDLE, LPDWORD] -GetConsoleMode.restype = BOOL - -# https://docs.microsoft.com/de-de/windows/console/setconsolemode -SetConsoleMode = windll.kernel32.SetConsoleMode -SetConsoleMode.argtypes = [HANDLE, DWORD] -SetConsoleMode.restype = BOOL - -# https://docs.microsoft.com/de-de/windows/console/readconsoleinput -ReadConsoleInputW = windll.kernel32.ReadConsoleInputW -# ReadConsoleInputW.argtypes = [HANDLE, POINTER(INPUT_RECORD), DWORD, LPDWORD] -ReadConsoleInputW.restype = BOOL - -# https://docs.microsoft.com/en-us/windows/console/getconsolescreenbufferinfo -GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo -GetConsoleScreenBufferInfo.argtypes = [HANDLE, POINTER(CONSOLE_SCREEN_BUFFER_INFO)] -GetConsoleScreenBufferInfo.restype = BOOL diff --git a/mitmproxy/coretypes/bidi.py b/mitmproxy/coretypes/bidi.py index 943062898c..5423f68c9c 100644 --- a/mitmproxy/coretypes/bidi.py +++ b/mitmproxy/coretypes/bidi.py @@ -1,5 +1,4 @@ class BiDi: - """ A wee utility class for keeping bi-directional mappings, like field constants in protocols. Names are attributes on the object, dict-like diff --git a/mitmproxy/coretypes/multidict.py b/mitmproxy/coretypes/multidict.py index 15f24568a9..8a90c7327b 100644 --- a/mitmproxy/coretypes/multidict.py +++ b/mitmproxy/coretypes/multidict.py @@ -1,6 +1,8 @@ from abc import ABCMeta from abc import abstractmethod -from collections.abc import Iterator, MutableMapping, Sequence +from collections.abc import Iterator +from collections.abc import MutableMapping +from collections.abc import Sequence from typing import TypeVar from mitmproxy.coretypes import serializable @@ -148,7 +150,7 @@ class MultiDict(_MultiDict[KT, VT], serializable.Serializable): def __init__(self, fields=()): super().__init__() - self.fields = tuple(tuple(i) for i in fields) + self.fields = tuple(tuple(i) for i in fields) # type: ignore @staticmethod def _reduce_values(values): @@ -162,7 +164,7 @@ def get_state(self): return self.fields def set_state(self, state): - self.fields = tuple(tuple(x) for x in state) + self.fields = tuple(tuple(x) for x in state) # type: ignore @classmethod def from_state(cls, state): diff --git a/mitmproxy/coretypes/serializable.py b/mitmproxy/coretypes/serializable.py index df09598b6e..e771f611e3 100644 --- a/mitmproxy/coretypes/serializable.py +++ b/mitmproxy/coretypes/serializable.py @@ -1,9 +1,26 @@ import abc +import collections.abc +import dataclasses +import enum +import typing import uuid +from functools import cache from typing import TypeVar +try: + from types import NoneType + from types import UnionType +except ImportError: # pragma: no cover + + class UnionType: # type: ignore + pass + + NoneType = type(None) # type: ignore + T = TypeVar("T", bound="Serializable") +State = typing.Any + class Serializable(metaclass=abc.ABCMeta): """ @@ -15,11 +32,12 @@ class Serializable(metaclass=abc.ABCMeta): def from_state(cls: type[T], state) -> T: """ Create a new object from the given state. + Consumes the passed state. """ raise NotImplementedError() @abc.abstractmethod - def get_state(self): + def get_state(self) -> State: """ Retrieve object state. """ @@ -28,7 +46,8 @@ def get_state(self): @abc.abstractmethod def set_state(self, state): """ - Set object state to the given state. + Set object state to the given state. Consumes the passed state. + May return a `dataclasses.FrozenInstanceError` if the object is immutable. """ raise NotImplementedError() @@ -37,3 +56,144 @@ def copy(self: T) -> T: if isinstance(state, dict) and "id" in state: state["id"] = str(uuid.uuid4()) return self.from_state(state) + + +U = TypeVar("U", bound="SerializableDataclass") + + +class SerializableDataclass(Serializable): + @classmethod + @cache + def __fields(cls) -> tuple[dataclasses.Field, ...]: + # with from __future__ import annotations, `field.type` is a string, + # see https://github.com/python/cpython/issues/83623. + hints = typing.get_type_hints(cls) + fields = [] + # noinspection PyDataclass + for field in dataclasses.fields(cls): # type: ignore[arg-type] + if field.metadata.get("serialize", True) is False: + continue + if isinstance(field.type, str): + field.type = hints[field.name] + fields.append(field) + return tuple(fields) + + def get_state(self) -> State: + state: dict[str, State] = {} + for field in self.__fields(): + val = getattr(self, field.name) + state[field.name] = _to_state(val, field.type, field.name) + return state + + @classmethod + def from_state(cls: type[U], state) -> U: + # state = state.copy() + for field in cls.__fields(): + state[field.name] = _to_val(state[field.name], field.type, field.name) + try: + return cls(**state) # type: ignore + except TypeError as e: + raise ValueError(f"Invalid state for {cls}: {e} ({state=})") from e + + def set_state(self, state: State) -> None: + for field in self.__fields(): + current = getattr(self, field.name) + f_state = state.pop(field.name) + if isinstance(current, Serializable) and f_state is not None: + try: + current.set_state(f_state) + continue + except dataclasses.FrozenInstanceError: + pass + val: typing.Any = _to_val(f_state, field.type, field.name) + try: + setattr(self, field.name, val) + except dataclasses.FrozenInstanceError: + state[field.name] = f_state # restore state dict. + raise + + if state: + raise ValueError( + f"Unexpected fields in {type(self).__name__}.set_state: {state}" + ) + + +def _process( + attr_val: typing.Any, attr_type: typing.Any, attr_name: str, make: bool +) -> typing.Any: + origin = typing.get_origin(attr_type) + if origin is typing.Literal: + if attr_val not in typing.get_args(attr_type): + raise ValueError( + f"Invalid value for {attr_name}: {attr_val!r} does not match any literal value." + ) + return attr_val + if origin in (UnionType, typing.Union): + attr_type, nt = typing.get_args(attr_type) + assert ( + nt is NoneType + ), f"{attr_name}: only `x | None` union types are supported`" + if attr_val is None: + return None # type: ignore + else: + return _process(attr_val, attr_type, attr_name, make) + else: + if attr_val is None: + raise ValueError(f"Attribute {attr_name} must not be None.") + + if make and hasattr(attr_type, "from_state"): + return attr_type.from_state(attr_val) # type: ignore + elif not make and hasattr(attr_type, "get_state"): + return attr_val.get_state() + + if origin in (list, collections.abc.Sequence): + (T,) = typing.get_args(attr_type) + return [_process(x, T, attr_name, make) for x in attr_val] # type: ignore + elif origin is tuple: + # We don't have a good way to represent tuple[str,int] | tuple[str,int,int,int], so we do a dirty hack here. + if attr_name in ("peername", "sockname"): + return tuple( + _process(x, T, attr_name, make) + for x, T in zip(attr_val, [str, int, int, int]) + ) # type: ignore + Ts = typing.get_args(attr_type) + if len(Ts) != len(attr_val): + raise ValueError( + f"Invalid data for {attr_name}. Expected {Ts}, got {attr_val}." + ) + return tuple(_process(x, T, attr_name, make) for T, x in zip(Ts, attr_val)) # type: ignore + elif origin is dict: + k_cls, v_cls = typing.get_args(attr_type) + return { + _process(k, k_cls, attr_name, make): _process(v, v_cls, attr_name, make) + for k, v in attr_val.items() + } # type: ignore + elif attr_type in (int, float): + if not isinstance(attr_val, (int, float)): + raise ValueError( + f"Invalid value for {attr_name}. Expected {attr_type}, got {attr_val} ({type(attr_val)})." + ) + return attr_type(attr_val) # type: ignore + elif attr_type in (str, bytes, bool): + if not isinstance(attr_val, attr_type): + raise ValueError( + f"Invalid value for {attr_name}. Expected {attr_type}, got {attr_val} ({type(attr_val)})." + ) + return attr_type(attr_val) # type: ignore + elif isinstance(attr_type, type) and issubclass(attr_type, enum.Enum): + if make: + return attr_type(attr_val) # type: ignore + else: + return attr_val.value + else: + raise TypeError(f"Unexpected type for {attr_name}: {attr_type!r}") + + +def _to_val(state: typing.Any, attr_type: typing.Any, attr_name: str) -> typing.Any: + """Create an object based on the state given in val.""" + return _process(state, attr_type, attr_name, True) + + +def _to_state(value: typing.Any, attr_type: typing.Any, attr_name: str) -> typing.Any: + """Get the state of the object given as val.""" + return _process(value, attr_type, attr_name, False) diff --git a/mitmproxy/ctx.py b/mitmproxy/ctx.py index 2ce9c7c232..e83983a4cb 100644 --- a/mitmproxy/ctx.py +++ b/mitmproxy/ctx.py @@ -1,7 +1,14 @@ -import mitmproxy.log -import mitmproxy.master -import mitmproxy.options +from __future__ import annotations -log: "mitmproxy.log.Log" -master: "mitmproxy.master.Master" -options: "mitmproxy.options.Options" +import typing + +if typing.TYPE_CHECKING: + import mitmproxy.log + import mitmproxy.master + import mitmproxy.options + +master: mitmproxy.master.Master +options: mitmproxy.options.Options + +log: mitmproxy.log.Log +"""Deprecated: Use Python's builtin `logging` module instead.""" diff --git a/mitmproxy/dns.py b/mitmproxy/dns.py index 4bfe25f7b2..cd79cdcb2b 100644 --- a/mitmproxy/dns.py +++ b/mitmproxy/dns.py @@ -1,32 +1,38 @@ from __future__ import annotations -from dataclasses import dataclass + +import base64 import itertools import random import struct -from ipaddress import IPv4Address, IPv6Address import time +from collections.abc import Iterable +from dataclasses import dataclass +from ipaddress import IPv4Address +from ipaddress import IPv6Address from typing import ClassVar -from mitmproxy import flow, stateobject -from mitmproxy.net.dns import classes, domain_names, op_codes, response_codes, types +from mitmproxy import flow +from mitmproxy.coretypes import serializable +from mitmproxy.net.dns import classes +from mitmproxy.net.dns import domain_names +from mitmproxy.net.dns import https_records +from mitmproxy.net.dns import op_codes +from mitmproxy.net.dns import response_codes +from mitmproxy.net.dns import types +from mitmproxy.net.dns.https_records import HTTPSRecord +from mitmproxy.net.dns.https_records import SVCParamKeys # DNS parameters taken from https://www.iana.org/assignments/dns-parameters/dns-parameters.xml @dataclass -class Question(stateobject.StateObject): +class Question(serializable.SerializableDataclass): HEADER: ClassVar[struct.Struct] = struct.Struct("!HH") name: str type: int class_: int - _stateobject_attributes = dict(name=str, type=int, class_=int) - - @classmethod - def from_state(cls, state): - return cls(**state) - def __str__(self) -> str: return self.name @@ -43,7 +49,7 @@ def to_json(self) -> dict: @dataclass -class ResourceRecord(stateobject.StateObject): +class ResourceRecord(serializable.SerializableDataclass): DEFAULT_TTL: ClassVar[int] = 60 HEADER: ClassVar[struct.Struct] = struct.Struct("!HHIH") @@ -53,12 +59,6 @@ class ResourceRecord(stateobject.StateObject): ttl: int data: bytes - _stateobject_attributes = dict(name=str, type=int, class_=int, ttl=int, data=bytes) - - @classmethod - def from_state(cls, state): - return cls(**state) - def __str__(self) -> str: try: if self.type == types.A: @@ -69,7 +69,9 @@ def __str__(self) -> str: return self.domain_name if self.type == types.TXT: return self.text - except: + if self.type == types.HTTPS: + return str(https_records.unpack(self.data)) + except Exception: return f"0x{self.data.hex()} (invalid {types.to_str(self.type)} data)" return f"0x{self.data.hex()}" @@ -105,6 +107,50 @@ def domain_name(self) -> str: def domain_name(self, name: str) -> None: self.data = domain_names.pack(name) + @property + def https_alpn(self) -> tuple[bytes, ...] | None: + record = https_records.unpack(self.data) + alpn_bytes = record.params.get(SVCParamKeys.ALPN.value, None) + if alpn_bytes is not None: + i = 0 + ret = [] + while i < len(alpn_bytes): + token_len = alpn_bytes[i] + ret.append(alpn_bytes[i + 1 : i + 1 + token_len]) + i += token_len + 1 + return tuple(ret) + else: + return None + + @https_alpn.setter + def https_alpn(self, alpn: Iterable[bytes] | None) -> None: + record = https_records.unpack(self.data) + if alpn is None: + record.params.pop(SVCParamKeys.ALPN.value, None) + else: + alpn_bytes = b"".join(bytes([len(a)]) + a for a in alpn) + record.params[SVCParamKeys.ALPN.value] = alpn_bytes + self.data = https_records.pack(record) + + @property + def https_ech(self) -> str | None: + record = https_records.unpack(self.data) + ech_bytes = record.params.get(SVCParamKeys.ECH.value, None) + if ech_bytes is not None: + return base64.b64encode(ech_bytes).decode("utf-8") + else: + return None + + @https_ech.setter + def https_ech(self, ech: str | None) -> None: + record = https_records.unpack(self.data) + if ech is None: + record.params.pop(SVCParamKeys.ECH.value, None) + else: + ech_bytes = base64.b64decode(ech.encode("utf-8")) + record.params[SVCParamKeys.ECH.value] = ech_bytes + self.data = https_records.pack(record) + def to_json(self) -> dict: """ Converts the resource record into json for mitmweb. @@ -147,10 +193,17 @@ def TXT(cls, name: str, text: str, *, ttl: int = DEFAULT_TTL) -> ResourceRecord: """Create a textual resource record.""" return cls(name, types.TXT, classes.IN, ttl, text.encode("utf-8")) + @classmethod + def HTTPS( + cls, name: str, record: HTTPSRecord, ttl: int = DEFAULT_TTL + ) -> ResourceRecord: + """Create a HTTPS resource record""" + return cls(name, types.HTTPS, classes.IN, ttl, https_records.pack(record)) + # comments are taken from rfc1035 @dataclass -class Message(stateobject.StateObject): +class Message(serializable.SerializableDataclass): HEADER: ClassVar[struct.Struct] = struct.Struct("!HHHHHH") timestamp: float @@ -194,29 +247,6 @@ class Message(stateobject.StateObject): additionals: list[ResourceRecord] """Third resource record section.""" - _stateobject_attributes = dict( - timestamp=float, - id=int, - query=bool, - op_code=int, - authoritative_answer=bool, - truncation=bool, - recursion_desired=bool, - recursion_available=bool, - reserved=int, - response_code=int, - questions=list[Question], - answers=list[ResourceRecord], - authorities=list[ResourceRecord], - additionals=list[ResourceRecord], - ) - - @classmethod - def from_state(cls, state): - obj = cls.__new__(cls) # `cls(**state)` won't work recursively - obj.set_state(state) - return obj - def __str__(self) -> str: return "\r\n".join( map( @@ -232,6 +262,14 @@ def content(self) -> bytes: """Returns the user-friendly content of all parts as encoded bytes.""" return str(self).encode() + @property + def question(self) -> Question | None: + """DNS practically only supports a single question at the + same time, so this is a shorthand for this.""" + if len(self.questions) == 1: + return self.questions[0] + return None + @property def size(self) -> int: """Returns the cumulative data size of all resource record sections.""" @@ -352,19 +390,12 @@ def unpack_rrs( f"unpack requires a data buffer of {len_data} bytes" ) data = buffer[offset:end_data] - if 0b11000000 in data: - # the resource record might contains a compressed domain name, if so, uncompressed in advance - try: - ( - rr_name, - rr_name_len, - ) = domain_names.unpack_from_with_compression( - buffer, offset, cached_names - ) - if rr_name_len == len_data: - data = domain_names.pack(rr_name) - except struct.error: - pass + + if domain_names.record_data_can_have_compression(type): + data = domain_names.decompress_from_record_data( + buffer, offset, end_data, cached_names + ) + section.append(ResourceRecord(name, type, class_, ttl, data)) offset += len_data except struct.error as e: @@ -465,9 +496,17 @@ class DNSFlow(flow.Flow): response: Message | None = None """The DNS response.""" - _stateobject_attributes = flow.Flow._stateobject_attributes.copy() - _stateobject_attributes["request"] = Message - _stateobject_attributes["response"] = Message + def get_state(self) -> serializable.State: + return { + **super().get_state(), + "request": self.request.get_state(), + "response": self.response.get_state() if self.response else None, + } + + def set_state(self, state: serializable.State) -> None: + self.request = Message.from_state(state.pop("request")) + self.response = Message.from_state(r) if (r := state.pop("response")) else None + super().set_state(state) def __repr__(self) -> str: return f"" diff --git a/mitmproxy/eventsequence.py b/mitmproxy/eventsequence.py index b00feaa34f..40b4767b9e 100644 --- a/mitmproxy/eventsequence.py +++ b/mitmproxy/eventsequence.py @@ -1,4 +1,6 @@ -from typing import Any, Callable, Iterator +from collections.abc import Callable +from collections.abc import Iterator +from typing import Any from mitmproxy import dns from mitmproxy import flow diff --git a/mitmproxy/flow.py b/mitmproxy/flow.py index 3f4db28db9..0415ee1f3f 100644 --- a/mitmproxy/flow.py +++ b/mitmproxy/flow.py @@ -1,15 +1,22 @@ +from __future__ import annotations + import asyncio +import copy import time import uuid -from typing import Any, ClassVar, Optional +from dataclasses import dataclass +from dataclasses import field +from typing import Any +from typing import ClassVar from mitmproxy import connection from mitmproxy import exceptions -from mitmproxy import stateobject from mitmproxy import version +from mitmproxy.coretypes import serializable -class Error(stateobject.StateObject): +@dataclass +class Error(serializable.SerializableDataclass): """ An Error. @@ -22,34 +29,19 @@ class Error(stateobject.StateObject): msg: str """Message describing the error.""" - timestamp: float + timestamp: float = field(default_factory=time.time) """Unix timestamp of when this error happened.""" KILLED_MESSAGE: ClassVar[str] = "Connection killed." - def __init__(self, msg: str, timestamp: Optional[float] = None) -> None: - """Create an error. If no timestamp is passed, the current time is used.""" - self.msg = msg - self.timestamp = timestamp or time.time() - - _stateobject_attributes = dict(msg=str, timestamp=float) - def __str__(self): return self.msg def __repr__(self): return self.msg - @classmethod - def from_state(cls, state): - # the default implementation assumes an empty constructor. Override - # accordingly. - f = cls(None) - f.set_state(state) - return f - -class Flow(stateobject.StateObject): +class Flow(serializable.Serializable): """ Base class for network flows. A flow is a collection of objects, for example HTTP request/response pairs or a list of TCP messages. @@ -73,7 +65,7 @@ class Flow(stateobject.StateObject): with a `timestamp_start` set to `None`. """ - error: Optional[Error] = None + error: Error | None = None """A connection or protocol error affecting this flow.""" intercepted: bool @@ -96,7 +88,7 @@ class Flow(stateobject.StateObject): The default marker for the view will be used if the Unicode emoji name can not be interpreted. """ - is_replay: Optional[str] + is_replay: str | None """ This attribute indicates if this flow has been replayed in either direction. @@ -130,57 +122,73 @@ def __init__( self.timestamp_created = time.time() self.intercepted: bool = False - self._resume_event: Optional[asyncio.Event] = None - self._backup: Optional[Flow] = None + self._resume_event: asyncio.Event | None = None + self._backup: Flow | None = None self.marked: str = "" - self.is_replay: Optional[str] = None + self.is_replay: str | None = None self.metadata: dict[str, Any] = dict() self.comment: str = "" - _stateobject_attributes = dict( - id=str, - error=Error, - client_conn=connection.Client, - server_conn=connection.Server, - intercepted=bool, - is_replay=str, - marked=str, - metadata=dict[str, Any], - comment=str, - timestamp_created=float, - ) - - __types: dict[str, type["Flow"]] = {} - - type: ClassVar[str] # automatically derived from the class name in __init_subclass__ + __types: dict[str, type[Flow]] = {} + + type: ClassVar[ + str + ] # automatically derived from the class name in __init_subclass__ """The flow type, for example `http`, `tcp`, or `dns`.""" def __init_subclass__(cls, **kwargs): cls.type = cls.__name__.removesuffix("Flow").lower() Flow.__types[cls.type] = cls - def get_state(self): - d = super().get_state() - d.update(version=version.FLOW_FORMAT_VERSION, type=self.type) - if self._backup and self._backup != d: - d.update(backup=self._backup) - return d - - def set_state(self, state): - state = state.copy() - state.pop("version") - state.pop("type") - if "backup" in state: - self._backup = state.pop("backup") - super().set_state(state) + def get_state(self) -> serializable.State: + state = { + "version": version.FLOW_FORMAT_VERSION, + "type": self.type, + "id": self.id, + "error": self.error.get_state() if self.error else None, + "client_conn": self.client_conn.get_state(), + "server_conn": self.server_conn.get_state(), + "intercepted": self.intercepted, + "is_replay": self.is_replay, + "marked": self.marked, + "metadata": copy.deepcopy(self.metadata), + "comment": self.comment, + "timestamp_created": self.timestamp_created, + } + state["backup"] = copy.deepcopy(self._backup) if self._backup != state else None + return state + + def set_state(self, state: serializable.State) -> None: + assert state.pop("version") == version.FLOW_FORMAT_VERSION + assert state.pop("type") == self.type + self.id = state.pop("id") + if state["error"]: + if self.error: + self.error.set_state(state.pop("error")) + else: + self.error = Error.from_state(state.pop("error")) + else: + self.error = state.pop("error") + self.client_conn.set_state(state.pop("client_conn")) + self.server_conn.set_state(state.pop("server_conn")) + self.intercepted = state.pop("intercepted") + self.is_replay = state.pop("is_replay") + self.marked = state.pop("marked") + self.metadata = state.pop("metadata") + self.comment = state.pop("comment") + self.timestamp_created = state.pop("timestamp_created") + self._backup = state.pop("backup", None) + assert state == {} @classmethod - def from_state(cls, state): + def from_state(cls, state: serializable.State) -> Flow: try: flow_cls = Flow.__types[state["type"]] except KeyError: raise ValueError(f"Unknown flow type: {state['type']}") - f = flow_cls(None, None) # noqa + client = connection.Client(peername=("", 0), sockname=("", 0)) + server = connection.Server(address=None) + f = flow_cls(client, server) f.set_state(state) return f diff --git a/mitmproxy/flowfilter.py b/mitmproxy/flowfilter.py index b0aa45a899..b9a6f286c3 100644 --- a/mitmproxy/flowfilter.py +++ b/mitmproxy/flowfilter.py @@ -1,46 +1,52 @@ """ - The following operators are understood: - - ~q Request - ~s Response - - Headers: - - Patterns are matched against "name: value" strings. Field names are - all-lowercase. - - ~a Asset content-type in response. Asset content types are: - text/javascript - application/x-javascript - application/javascript - text/css - image/* - font/* - application/font-* - ~h rex Header line in either request or response - ~hq rex Header in request - ~hs rex Header in response - - ~b rex Expression in the body of either request or response - ~bq rex Expression in the body of request - ~bs rex Expression in the body of response - ~t rex Shortcut for content-type header. - - ~d rex Request domain - ~m rex Method - ~u rex URL - ~c CODE Response code. - rex Equivalent to ~u rex +The following operators are understood: + + ~q Request + ~s Response + +Headers: + + Patterns are matched against "name: value" strings. Field names are + all-lowercase. + + ~a Asset content-type in response. Asset content types are: + text/javascript + application/x-javascript + application/javascript + text/css + image/* + font/* + application/font-* + ~h rex Header line in either request or response + ~hq rex Header in request + ~hs rex Header in response + + ~b rex Expression in the body of either request or response + ~bq rex Expression in the body of request + ~bs rex Expression in the body of response + ~t rex Shortcut for content-type header. + + ~d rex Request domain + ~m rex Method + ~u rex URL + ~c CODE Response code. + rex Equivalent to ~u rex """ import functools import re import sys from collections.abc import Sequence -from typing import ClassVar, Protocol, Union +from typing import ClassVar +from typing import Protocol + import pyparsing as pp -from mitmproxy import dns, flow, http, tcp, udp +from mitmproxy import dns +from mitmproxy import flow +from mitmproxy import http +from mitmproxy import tcp +from mitmproxy import udp def only(*types): @@ -288,19 +294,25 @@ class FBod(_Rex): @only(http.HTTPFlow, tcp.TCPFlow, udp.UDPFlow, dns.DNSFlow) def __call__(self, f): if isinstance(f, http.HTTPFlow): - if f.request and f.request.raw_content: - if self.re.search(f.request.get_content(strict=False)): + if ( + f.request + and (content := f.request.get_content(strict=False)) is not None + ): + if self.re.search(content): return True - if f.response and f.response.raw_content: - if self.re.search(f.response.get_content(strict=False)): + if ( + f.response + and (content := f.response.get_content(strict=False)) is not None + ): + if self.re.search(content): return True if f.websocket: - for msg in f.websocket.messages: - if self.re.search(msg.content): + for wmsg in f.websocket.messages: + if wmsg.content is not None and self.re.search(wmsg.content): return True elif isinstance(f, (tcp.TCPFlow, udp.UDPFlow)): for msg in f.messages: - if self.re.search(msg.content): + if msg.content is not None and self.re.search(msg.content): return True elif isinstance(f, dns.DNSFlow): if f.request and self.re.search(f.request.content): @@ -318,12 +330,15 @@ class FBodRequest(_Rex): @only(http.HTTPFlow, tcp.TCPFlow, udp.UDPFlow, dns.DNSFlow) def __call__(self, f): if isinstance(f, http.HTTPFlow): - if f.request and f.request.raw_content: - if self.re.search(f.request.get_content(strict=False)): + if ( + f.request + and (content := f.request.get_content(strict=False)) is not None + ): + if self.re.search(content): return True if f.websocket: - for msg in f.websocket.messages: - if msg.from_client and self.re.search(msg.content): + for wmsg in f.websocket.messages: + if wmsg.from_client and self.re.search(wmsg.content): return True elif isinstance(f, (tcp.TCPFlow, udp.UDPFlow)): for msg in f.messages: @@ -342,12 +357,15 @@ class FBodResponse(_Rex): @only(http.HTTPFlow, tcp.TCPFlow, udp.UDPFlow, dns.DNSFlow) def __call__(self, f): if isinstance(f, http.HTTPFlow): - if f.response and f.response.raw_content: - if self.re.search(f.response.get_content(strict=False)): + if ( + f.response + and (content := f.response.get_content(strict=False)) is not None + ): + if self.re.search(content): return True if f.websocket: - for msg in f.websocket.messages: - if not msg.from_client and self.re.search(msg.content): + for wmsg in f.websocket.messages: + if not wmsg.from_client and self.re.search(wmsg.content): return True elif isinstance(f, (tcp.TCPFlow, udp.UDPFlow)): for msg in f.messages: @@ -385,6 +403,7 @@ class FUrl(_Rex): code = "u" help = "URL" is_binary = False + flags = re.IGNORECASE # FUrl is special, because it can be "naked". @@ -625,8 +644,7 @@ def _make(): class TFilter(Protocol): pattern: str - def __call__(self, f: flow.Flow) -> bool: - ... # pragma: no cover + def __call__(self, f: flow.Flow) -> bool: ... # pragma: no cover def parse(s: str) -> TFilter: @@ -644,7 +662,7 @@ def parse(s: str) -> TFilter: raise ValueError(f"Invalid filter expression: {s!r}") from e -def match(flt: Union[str, TFilter], flow: flow.Flow) -> bool: +def match(flt: str | TFilter, flow: flow.Flow) -> bool: """ Matches a flow against a compiled filter expression. Returns True if matched, False if not. diff --git a/mitmproxy/hooks.py b/mitmproxy/hooks.py index d0c2934fa1..870c6c208f 100644 --- a/mitmproxy/hooks.py +++ b/mitmproxy/hooks.py @@ -1,8 +1,12 @@ import re import warnings from collections.abc import Sequence -from dataclasses import dataclass, is_dataclass, fields -from typing import Any, ClassVar, TYPE_CHECKING +from dataclasses import dataclass +from dataclasses import fields +from dataclasses import is_dataclass +from typing import Any +from typing import ClassVar +from typing import TYPE_CHECKING import mitmproxy.flow @@ -16,7 +20,7 @@ class Hook: def args(self) -> list[Any]: args = [] - for field in fields(self): + for field in fields(self): # type: ignore[arg-type] args.append(getattr(self, field.name)) return args @@ -43,8 +47,8 @@ def __init_subclass__(cls, **kwargs): all_hooks[cls.name] = cls # define a custom hash and __eq__ function so that events are hashable and not comparable. - cls.__hash__ = object.__hash__ - cls.__eq__ = object.__eq__ + cls.__hash__ = object.__hash__ # type: ignore + cls.__eq__ = object.__eq__ # type: ignore all_hooks: dict[str, type[Hook]] = {} diff --git a/mitmproxy/http.py b/mitmproxy/http.py index e88242530c..a6c35b3f65 100644 --- a/mitmproxy/http.py +++ b/mitmproxy/http.py @@ -1,26 +1,23 @@ import binascii +import json import os -import re import time import urllib.parse -import json import warnings +from collections.abc import Callable +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Mapping +from collections.abc import Sequence from dataclasses import dataclass from dataclasses import fields from email.utils import formatdate from email.utils import mktime_tz from email.utils import parsedate_tz -from typing import Callable -from typing import Iterable -from typing import Iterator -from typing import Mapping -from typing import Optional -from typing import Union -from typing import cast from typing import Any +from typing import cast from mitmproxy import flow -from mitmproxy.websocket import WebSocketData from mitmproxy.coretypes import multidict from mitmproxy.coretypes import serializable from mitmproxy.net import encoding @@ -29,12 +26,14 @@ from mitmproxy.net.http import status_codes from mitmproxy.net.http import url from mitmproxy.net.http.headers import assemble_content_type +from mitmproxy.net.http.headers import infer_content_encoding from mitmproxy.net.http.headers import parse_content_type from mitmproxy.utils import human from mitmproxy.utils import strutils from mitmproxy.utils import typecheck from mitmproxy.utils.strutils import always_bytes from mitmproxy.utils.strutils import always_str +from mitmproxy.websocket import WebSocketData # While headers _should_ be ASCII, it's not uncommon for certain headers to be utf-8 encoded. @@ -42,7 +41,7 @@ def _native(x: bytes) -> str: return x.decode("utf-8", "surrogateescape") -def _always_bytes(x: Union[str, bytes]) -> bytes: +def _always_bytes(x: str | bytes) -> bytes: return strutils.always_bytes(x, "utf-8", "surrogateescape") @@ -135,7 +134,7 @@ def __bytes__(self) -> bytes: else: return b"" - def __delitem__(self, key: Union[str, bytes]) -> None: + def __delitem__(self, key: str | bytes) -> None: key = _always_bytes(key) super().__delitem__(key) @@ -143,7 +142,7 @@ def __iter__(self) -> Iterator[str]: for x in super().__iter__(): yield _native(x) - def get_all(self, name: Union[str, bytes]) -> list[str]: + def get_all(self, name: str | bytes) -> list[str]: """ Like `Headers.get`, but does not fold multiple headers into a single one. This is useful for Set-Cookie and Cookie headers, which do not support folding. @@ -156,7 +155,7 @@ def get_all(self, name: Union[str, bytes]) -> list[str]: name = _always_bytes(name) return [_native(x) for x in super().get_all(name)] - def set_all(self, name: Union[str, bytes], values: list[Union[str, bytes]]): + def set_all(self, name: str | bytes, values: Iterable[str | bytes]): """ Explicitly set multiple headers for the given key. See `Headers.get_all`. @@ -165,7 +164,7 @@ def set_all(self, name: Union[str, bytes], values: list[Union[str, bytes]]): values = [_always_bytes(x) for x in values] return super().set_all(name, values) - def insert(self, index: int, key: Union[str, bytes], value: Union[str, bytes]): + def insert(self, index: int, key: str | bytes, value: str | bytes): key = _always_bytes(key) value = _always_bytes(value) super().insert(index, key, value) @@ -181,10 +180,10 @@ def items(self, multi=False): class MessageData(serializable.Serializable): http_version: bytes headers: Headers - content: Optional[bytes] - trailers: Optional[Headers] + content: bytes | None + trailers: Headers | None timestamp_start: float - timestamp_end: Optional[float] + timestamp_end: float | None # noinspection PyUnreachableCode if __debug__: @@ -245,7 +244,7 @@ def set_state(self, state): self.data.set_state(state) data: MessageData - stream: Union[Callable[[bytes], Union[Iterable[bytes], bytes]], bool] = False + stream: Callable[[bytes], Iterable[bytes] | bytes] | bool = False """ This attribute controls if the message body should be streamed. @@ -268,7 +267,7 @@ def http_version(self) -> str: return self.data.http_version.decode("utf-8", "surrogateescape") @http_version.setter - def http_version(self, http_version: Union[str, bytes]) -> None: + def http_version(self, http_version: str | bytes) -> None: self.data.http_version = strutils.always_bytes( http_version, "utf-8", "surrogateescape" ) @@ -285,6 +284,10 @@ def is_http11(self) -> bool: def is_http2(self) -> bool: return self.data.http_version == b"HTTP/2.0" + @property + def is_http3(self) -> bool: + return self.data.http_version == b"HTTP/3" + @property def headers(self) -> Headers: """ @@ -297,18 +300,18 @@ def headers(self, h: Headers) -> None: self.data.headers = h @property - def trailers(self) -> Optional[Headers]: + def trailers(self) -> Headers | None: """ The [HTTP trailers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Trailer). """ return self.data.trailers @trailers.setter - def trailers(self, h: Optional[Headers]) -> None: + def trailers(self, h: Headers | None) -> None: self.data.trailers = h @property - def raw_content(self) -> Optional[bytes]: + def raw_content(self) -> bytes | None: """ The raw (potentially compressed) HTTP message body. @@ -319,11 +322,11 @@ def raw_content(self) -> Optional[bytes]: return self.data.content @raw_content.setter - def raw_content(self, content: Optional[bytes]) -> None: + def raw_content(self, content: bytes | None) -> None: self.data.content = content @property - def content(self) -> Optional[bytes]: + def content(self) -> bytes | None: """ The uncompressed HTTP message body as bytes. @@ -334,11 +337,11 @@ def content(self) -> Optional[bytes]: return self.get_content() @content.setter - def content(self, value: Optional[bytes]) -> None: + def content(self, value: bytes | None) -> None: self.set_content(value) @property - def text(self) -> Optional[str]: + def text(self) -> str | None: """ The uncompressed and decoded HTTP message body as text. @@ -349,10 +352,10 @@ def text(self) -> Optional[str]: return self.get_text() @text.setter - def text(self, value: Optional[str]) -> None: + def text(self, value: str | None) -> None: self.set_text(value) - def set_content(self, value: Optional[bytes]) -> None: + def set_content(self, value: bytes | None) -> None: if value is None: self.raw_content = None return @@ -377,7 +380,7 @@ def set_content(self, value: Optional[bytes]) -> None: else: self.headers["content-length"] = str(len(self.raw_content)) - def get_content(self, strict: bool = True) -> Optional[bytes]: + def get_content(self, strict: bool = True) -> bytes | None: """ Similar to `Message.content`, but does not raise if `strict` is `False`. Instead, the compressed message body is returned as-is. @@ -399,45 +402,11 @@ def get_content(self, strict: bool = True) -> Optional[bytes]: else: return self.raw_content - def _get_content_type_charset(self) -> Optional[str]: - ct = parse_content_type(self.headers.get("content-type", "")) - if ct: - return ct[2].get("charset") - return None - - def _guess_encoding(self, content: bytes = b"") -> str: - enc = self._get_content_type_charset() - if not enc: - if "json" in self.headers.get("content-type", ""): - enc = "utf8" - if not enc: - if "html" in self.headers.get("content-type", ""): - meta_charset = re.search( - rb"""]+charset=['"]?([^'">]+)""", content, re.IGNORECASE - ) - if meta_charset: - enc = meta_charset.group(1).decode("ascii", "ignore") - if not enc: - if "text/css" in self.headers.get("content-type", ""): - # @charset rule must be the very first thing. - css_charset = re.match( - rb"""@charset "([^"]+)";""", content, re.IGNORECASE - ) - if css_charset: - enc = css_charset.group(1).decode("ascii", "ignore") - if not enc: - enc = "latin-1" - # Use GB 18030 as the superset of GB2312 and GBK to fix common encoding problems on Chinese websites. - if enc.lower() in ("gb2312", "gbk"): - enc = "gb18030" - - return enc - - def set_text(self, text: Optional[str]) -> None: + def set_text(self, text: str | None) -> None: if text is None: self.content = None return - enc = self._guess_encoding() + enc = infer_content_encoding(self.headers.get("content-type", "")) try: self.content = cast(bytes, encoding.encode(text, enc)) @@ -453,7 +422,7 @@ def set_text(self, text: Optional[str]) -> None: enc = "utf8" self.content = text.encode(enc, "surrogateescape") - def get_text(self, strict: bool = True) -> Optional[str]: + def get_text(self, strict: bool = True) -> str | None: """ Similar to `Message.text`, but does not raise if `strict` is `False`. Instead, the message body is returned as surrogate-escaped UTF-8. @@ -461,7 +430,7 @@ def get_text(self, strict: bool = True) -> Optional[str]: content = self.get_content(strict) if content is None: return None - enc = self._guess_encoding(content) + enc = infer_content_encoding(self.headers.get("content-type", ""), content) try: return cast(str, encoding.decode(content, enc)) except ValueError: @@ -481,14 +450,14 @@ def timestamp_start(self, timestamp_start: float) -> None: self.data.timestamp_start = timestamp_start @property - def timestamp_end(self) -> Optional[float]: + def timestamp_end(self) -> float | None: """ *Timestamp:* Last byte received. """ return self.data.timestamp_end @timestamp_end.setter - def timestamp_end(self, timestamp_end: Optional[float]): + def timestamp_end(self, timestamp_end: float | None): self.data.timestamp_end = timestamp_end def decode(self, strict: bool = True) -> None: @@ -553,11 +522,11 @@ def __init__( authority: bytes, path: bytes, http_version: bytes, - headers: Union[Headers, tuple[tuple[bytes, bytes], ...]], - content: Optional[bytes], - trailers: Union[Headers, tuple[tuple[bytes, bytes], ...], None], + headers: Headers | tuple[tuple[bytes, bytes], ...], + content: bytes | None, + trailers: Headers | tuple[tuple[bytes, bytes], ...] | None, timestamp_start: float, - timestamp_end: Optional[float], + timestamp_end: float | None, ): # auto-convert invalid types to retain compatibility with older code. if isinstance(host, bytes): @@ -608,12 +577,10 @@ def make( cls, method: str, url: str, - content: Union[bytes, str] = "", - headers: Union[ - Headers, - dict[Union[str, bytes], Union[str, bytes]], - Iterable[tuple[bytes, bytes]], - ] = (), + content: bytes | str = "", + headers: ( + Headers | dict[str | bytes, str | bytes] | Iterable[tuple[bytes, bytes]] + ) = (), ) -> "Request": """ Simplified API for creating request objects. @@ -688,7 +655,7 @@ def method(self) -> str: return self.data.method.decode("utf-8", "surrogateescape").upper() @method.setter - def method(self, val: Union[str, bytes]) -> None: + def method(self, val: str | bytes) -> None: self.data.method = always_bytes(val, "utf-8", "surrogateescape") @property @@ -699,7 +666,7 @@ def scheme(self) -> str: return self.data.scheme.decode("utf-8", "surrogateescape") @scheme.setter - def scheme(self, val: Union[str, bytes]) -> None: + def scheme(self, val: str | bytes) -> None: self.data.scheme = always_bytes(val, "utf-8", "surrogateescape") @property @@ -721,7 +688,7 @@ def authority(self) -> str: return self.data.authority.decode("utf8", "surrogateescape") @authority.setter - def authority(self, val: Union[str, bytes]) -> None: + def authority(self, val: str | bytes) -> None: if isinstance(val, str): try: val = val.encode("idna", "strict") @@ -743,18 +710,12 @@ def host(self) -> str: return self.data.host @host.setter - def host(self, val: Union[str, bytes]) -> None: + def host(self, val: str | bytes) -> None: self.data.host = always_str(val, "idna", "strict") - - # Update host header - if "Host" in self.data.headers: - self.data.headers["Host"] = val - # Update authority - if self.data.authority: - self.authority = url.hostport(self.scheme, self.host, self.port) + self._update_host_and_authority() @property - def host_header(self) -> Optional[str]: + def host_header(self) -> str | None: """ The request's host/authority header. @@ -763,21 +724,21 @@ def host_header(self) -> Optional[str]: *See also:* `Request.authority`,`Request.host`, `Request.pretty_host` """ - if self.is_http2: + if self.is_http2 or self.is_http3: return self.authority or self.data.headers.get("Host", None) else: return self.data.headers.get("Host", None) @host_header.setter - def host_header(self, val: Union[None, str, bytes]) -> None: + def host_header(self, val: None | str | bytes) -> None: if val is None: - if self.is_http2: + if self.is_http2 or self.is_http3: self.data.authority = b"" self.headers.pop("Host", None) else: - if self.is_http2: + if self.is_http2 or self.is_http3: self.authority = val # type: ignore - if not self.is_http2 or "Host" in self.headers: + if not (self.is_http2 or self.is_http3) or "Host" in self.headers: # For h2, we only overwrite, but not create, as :authority is the h2 host header. self.headers["Host"] = val @@ -790,18 +751,35 @@ def port(self) -> int: @port.setter def port(self, port: int) -> None: + if not isinstance(port, int): + raise ValueError(f"Port must be an integer, not {port!r}.") + self.data.port = port + self._update_host_and_authority() + + def _update_host_and_authority(self) -> None: + val = url.hostport(self.scheme, self.host, self.port) + + # Update host header + if "Host" in self.data.headers: + self.data.headers["Host"] = val + # Update authority + if self.data.authority: + self.authority = val @property def path(self) -> str: """ - HTTP request path, e.g. "/index.html". + HTTP request path, e.g. "/index.html" or "/index.html?a=b". Usually starts with a slash, except for OPTIONS requests, which may just be "*". + + This attribute includes both path and query parts of the target URI + (see Sections 3.3 and 3.4 of [RFC3986](https://datatracker.ietf.org/doc/html/rfc3986)). """ return self.data.path.decode("utf-8", "surrogateescape") @path.setter - def path(self, val: Union[str, bytes]) -> None: + def path(self, val: str | bytes) -> None: self.data.path = always_bytes(val, "utf-8", "surrogateescape") @property @@ -816,7 +794,7 @@ def url(self) -> str: return url.unparse(self.scheme, self.host, self.port, self.path) @url.setter - def url(self, val: Union[str, bytes]) -> None: + def url(self, val: str | bytes) -> None: val = always_str(val, "utf-8", "surrogateescape") self.scheme, self.host, self.port, self.path = url.parse(val) @@ -951,7 +929,7 @@ def _get_urlencoded_form(self): return tuple(url.decode(self.get_text(strict=False))) return () - def _set_urlencoded_form(self, form_data): + def _set_urlencoded_form(self, form_data: Sequence[tuple[str, str]]) -> None: """ Sets the body to the URL-encoded form data, and adds the appropriate content-type header. This will overwrite the existing content if there is one. @@ -977,23 +955,22 @@ def urlencoded_form(self) -> multidict.MultiDictView[str, str]: def urlencoded_form(self, value): self._set_urlencoded_form(value) - def _get_multipart_form(self): + def _get_multipart_form(self) -> list[tuple[bytes, bytes]]: is_valid_content_type = ( "multipart/form-data" in self.headers.get("content-type", "").lower() ) - if is_valid_content_type: + if is_valid_content_type and self.content is not None: try: - return multipart.decode(self.headers.get("content-type"), self.content) + return multipart.decode_multipart( + self.headers.get("content-type"), self.content + ) except ValueError: pass - return () + return [] - def _set_multipart_form(self, value): - is_valid_content_type = ( - self.headers.get("content-type", "") - .lower() - .startswith("multipart/form-data") - ) + def _set_multipart_form(self, value: list[tuple[bytes, bytes]]) -> None: + ct = self.headers.get("content-type", "") + is_valid_content_type = ct.lower().startswith("multipart/form-data") if not is_valid_content_type: """ Generate a random boundary here. @@ -1002,8 +979,10 @@ def _set_multipart_form(self, value): on generating the boundary. """ boundary = "-" * 20 + binascii.hexlify(os.urandom(16)).decode() - self.headers["content-type"] = f"multipart/form-data; boundary={boundary}" - self.content = multipart.encode(self.headers, value) + self.headers["content-type"] = ct = ( + f"multipart/form-data; boundary={boundary}" + ) + self.content = multipart.encode_multipart(ct, value) @property def multipart_form(self) -> multidict.MultiDictView[bytes, bytes]: @@ -1020,7 +999,7 @@ def multipart_form(self) -> multidict.MultiDictView[bytes, bytes]: ) @multipart_form.setter - def multipart_form(self, value): + def multipart_form(self, value: list[tuple[bytes, bytes]]) -> None: self._set_multipart_form(value) @@ -1036,11 +1015,11 @@ def __init__( http_version: bytes, status_code: int, reason: bytes, - headers: Union[Headers, tuple[tuple[bytes, bytes], ...]], - content: Optional[bytes], - trailers: Union[None, Headers, tuple[tuple[bytes, bytes], ...]], + headers: Headers | tuple[tuple[bytes, bytes], ...], + content: bytes | None, + trailers: None | Headers | tuple[tuple[bytes, bytes], ...], timestamp_start: float, - timestamp_end: Optional[float], + timestamp_end: float | None, ): # auto-convert invalid types to retain compatibility with older code. if isinstance(http_version, str): @@ -1079,10 +1058,10 @@ def __repr__(self) -> str: def make( cls, status_code: int = 200, - content: Union[bytes, str] = b"", - headers: Union[ - Headers, Mapping[str, Union[str, bytes]], Iterable[tuple[bytes, bytes]] - ] = (), + content: bytes | str = b"", + headers: ( + Headers | Mapping[str, str | bytes] | Iterable[tuple[bytes, bytes]] + ) = (), ) -> "Response": """ Simplified API for creating response objects. @@ -1151,7 +1130,7 @@ def reason(self) -> str: return self.data.reason.decode("ISO-8859-1") @reason.setter - def reason(self, reason: Union[str, bytes]) -> None: + def reason(self, reason: str | bytes) -> None: self.data.reason = strutils.always_bytes(reason, "ISO-8859-1") def _get_cookies(self): @@ -1169,9 +1148,7 @@ def _set_cookies(self, value): @property def cookies( self, - ) -> multidict.MultiDictView[ - str, tuple[str, multidict.MultiDict[str, Optional[str]]] - ]: + ) -> multidict.MultiDictView[str, tuple[str, multidict.MultiDict[str, str | None]]]: """ The response cookies. A possibly empty `MultiDictView`, where the keys are cookie name strings, and values are `(cookie value, attributes)` tuples. Within @@ -1231,9 +1208,9 @@ class HTTPFlow(flow.Flow): request: Request """The client's HTTP request.""" - response: Optional[Response] = None + response: Response | None = None """The server's HTTP response.""" - error: Optional[flow.Error] = None + error: flow.Error | None = None """ A connection or protocol error affecting this flow. @@ -1242,16 +1219,26 @@ class HTTPFlow(flow.Flow): from the server, but there was an error sending it back to the client. """ - websocket: Optional[WebSocketData] = None + websocket: WebSocketData | None = None """ If this HTTP flow initiated a WebSocket connection, this attribute contains all associated WebSocket data. """ - _stateobject_attributes = flow.Flow._stateobject_attributes.copy() - # mypy doesn't support update with kwargs - _stateobject_attributes.update( - dict(request=Request, response=Response, websocket=WebSocketData) - ) + def get_state(self) -> serializable.State: + return { + **super().get_state(), + "request": self.request.get_state(), + "response": self.response.get_state() if self.response else None, + "websocket": self.websocket.get_state() if self.websocket else None, + } + + def set_state(self, state: serializable.State) -> None: + self.request = Request.from_state(state.pop("request")) + self.response = Response.from_state(r) if (r := state.pop("response")) else None + self.websocket = ( + WebSocketData.from_state(w) if (w := state.pop("websocket")) else None + ) + super().set_state(state) def __repr__(self): s = " Any: if isinstance(o, dict): return {strutils.always_str(k): _convert_dict_keys(v) for k, v in o.items()} @@ -448,12 +495,13 @@ def convert_unicode(data: dict) -> dict: 15: convert_15_16, 16: convert_16_17, 17: convert_17_18, + 18: convert_18_19, + 19: convert_19_20, + 20: convert_20_21, } -def migrate_flow( - flow_data: dict[Union[bytes, str], Any] -) -> dict[Union[bytes, str], Any]: +def migrate_flow(flow_data: dict[bytes | str, Any]) -> dict[bytes | str, Any]: while True: flow_version = flow_data.get(b"version", flow_data.get("version")) diff --git a/mitmproxy/io/har.py b/mitmproxy/io/har.py new file mode 100644 index 0000000000..80f4e933e3 --- /dev/null +++ b/mitmproxy/io/har.py @@ -0,0 +1,155 @@ +"""Reads HAR files into flow objects""" + +import base64 +import logging +import time +from datetime import datetime + +from mitmproxy import connection +from mitmproxy import exceptions +from mitmproxy import http +from mitmproxy.net.http.headers import infer_content_encoding + +logger = logging.getLogger(__name__) + + +def fix_headers( + request_headers: list[dict[str, str]] | list[tuple[str, str]], +) -> http.Headers: + """Converts provided headers into (b"header-name", b"header-value") tuples""" + flow_headers: list[tuple[bytes, bytes]] = [] + for header in request_headers: + # Applications that use the {"name":item,"value":item} notation are Brave,Chrome,Edge,Firefox,Charles,Fiddler,Insomnia,Safari + if isinstance(header, dict): + key = header["name"] + value = header["value"] + + # Application that uses the [name, value] notation is Slack + + else: + try: + key = header[0] + value = header[1] + except IndexError as e: + raise exceptions.OptionsError(str(e)) from e + flow_headers.append((key.encode(), value.encode())) + + return http.Headers(flow_headers) + + +def request_to_flow(request_json: dict) -> http.HTTPFlow: + """ + Creates a HTTPFlow object from a given entry in HAR file + """ + + timestamp_start = datetime.fromisoformat( + request_json["startedDateTime"].replace("Z", "+00:00") + ).timestamp() + timestamp_end = timestamp_start + request_json["time"] / 1000.0 + request_method = request_json["request"]["method"] + request_url = request_json["request"]["url"] + server_address = request_json.get("serverIPAddress", None) + request_headers = fix_headers(request_json["request"]["headers"]) + + http_version_req = request_json["request"]["httpVersion"] + http_version_resp = request_json["response"]["httpVersion"] + + request_content = "" + # List contains all the representations of an http request across different HAR files + if request_url.startswith("http://"): + port = 80 + else: + port = 443 + + client_conn = connection.Client( + peername=("127.0.0.1", 0), + sockname=("127.0.0.1", 0), + # TODO Get time info from HAR File + timestamp_start=time.time(), + ) + + if server_address: + server_conn = connection.Server(address=(server_address, port)) + else: + server_conn = connection.Server(address=None) + + new_flow = http.HTTPFlow(client_conn, server_conn) + + if "postData" in request_json["request"]: + request_content = request_json["request"]["postData"]["text"] + + new_flow.request = http.Request.make( + request_method, request_url, request_content, request_headers + ) + + response_code = request_json["response"]["status"] + + # In Firefox HAR files images don't include response bodies + response_content = request_json["response"]["content"].get("text", "") + content_encoding = request_json["response"]["content"].get("encoding", None) + response_headers = fix_headers(request_json["response"]["headers"]) + + if content_encoding == "base64": + response_content = base64.b64decode(response_content) + elif isinstance(response_content, str): + # Convert text to bytes, as in `Response.set_text` + try: + response_content = http.encoding.encode( + response_content, + ( + content_encoding + or infer_content_encoding(response_headers.get("content-type", "")) + ), + ) + except ValueError: + # Fallback to UTF-8 + response_content = response_content.encode( + "utf-8", errors="surrogateescape" + ) + + # Then encode the content, as in `Response.set_content` + response_content = http.encoding.encode( + response_content, response_headers.get("content-encoding") or "identity" + ) + + new_flow.response = http.Response( + b"HTTP/1.1", + response_code, + http.status_codes.RESPONSES.get(response_code, "").encode(), + response_headers, + response_content, + None, + timestamp_start, + timestamp_end, + ) + + # Update timestamps + + new_flow.request.timestamp_start = timestamp_start + new_flow.request.timestamp_end = timestamp_end + + new_flow.client_conn.timestamp_start = timestamp_start + new_flow.client_conn.timestamp_end = timestamp_end + + # Update HTTP version + + match http_version_req: + case "http/2.0": + new_flow.request.http_version = "HTTP/2" + case "HTTP/2": + new_flow.request.http_version = "HTTP/2" + case "HTTP/3": + new_flow.request.http_version = "HTTP/3" + case _: + new_flow.request.http_version = "HTTP/1.1" + match http_version_resp: + case "http/2.0": + new_flow.response.http_version = "HTTP/2" + case "HTTP/2": + new_flow.response.http_version = "HTTP/2" + case "HTTP/3": + new_flow.response.http_version = "HTTP/3" + case _: + new_flow.response.http_version = "HTTP/1.1" + + return new_flow diff --git a/mitmproxy/io/io.py b/mitmproxy/io/io.py index 402d3630c0..a388aa2c60 100644 --- a/mitmproxy/io/io.py +++ b/mitmproxy/io/io.py @@ -1,11 +1,18 @@ +import json import os -from typing import Any, BinaryIO, Iterable, Union, cast +from collections.abc import Iterable +from io import BufferedReader +from typing import Any +from typing import BinaryIO +from typing import cast +from typing import Union from mitmproxy import exceptions from mitmproxy import flow from mitmproxy import flowfilter from mitmproxy.io import compat from mitmproxy.io import tnetstring +from mitmproxy.io.har import request_to_flow class FlowWriter: @@ -18,28 +25,58 @@ def add(self, f: flow.Flow) -> None: class FlowReader: + fo: BinaryIO + def __init__(self, fo: BinaryIO): - self.fo: BinaryIO = fo + self.fo = fo + + def peek(self, n: int) -> bytes: + try: + return cast(BufferedReader, self.fo).peek(n) + except AttributeError: + # https://github.com/python/cpython/issues/90533: io.BytesIO does not have peek() + pos = self.fo.tell() + ret = self.fo.read(n) + self.fo.seek(pos) + return ret def stream(self) -> Iterable[flow.Flow]: """ Yields Flow objects from the dump. """ - try: - while True: - # FIXME: This cast hides a lack of dynamic type checking - loaded = cast( - dict[Union[bytes, str], Any], - tnetstring.load(self.fo), + + if self.peek(4).startswith( + b"\xef\xbb\xbf{" + ): # skip BOM, usually added by Fiddler + self.fo.read(3) + if self.peek(1).startswith(b"{"): + try: + har_file = json.loads(self.fo.read().decode("utf-8")) + + for request_json in har_file["log"]["entries"]: + yield request_to_flow(request_json) + + except Exception: + raise exceptions.FlowReadException( + "Unable to read HAR file. Please provide a valid HAR file" ) - try: - yield flow.Flow.from_state(compat.migrate_flow(loaded)) - except ValueError as e: - raise exceptions.FlowReadException(e) - except (ValueError, TypeError, IndexError) as e: - if str(e) == "not a tnetstring: empty file": - return # Error is due to EOF - raise exceptions.FlowReadException("Invalid data format.") + + else: + try: + while True: + # FIXME: This cast hides a lack of dynamic type checking + loaded = cast( + dict[Union[bytes, str], Any], + tnetstring.load(self.fo), + ) + try: + yield flow.Flow.from_state(compat.migrate_flow(loaded)) + except ValueError as e: + raise exceptions.FlowReadException(e) from e + except (ValueError, TypeError, IndexError) as e: + if str(e) == "not a tnetstring: empty file": + return # Error is due to EOF + raise exceptions.FlowReadException("Invalid data format.") from e class FilteredFlowWriter: diff --git a/mitmproxy/io/tnetstring.py b/mitmproxy/io/tnetstring.py index e08a729eba..e5d62a47d9 100644 --- a/mitmproxy/io/tnetstring.py +++ b/mitmproxy/io/tnetstring.py @@ -41,7 +41,8 @@ """ import collections -from typing import BinaryIO, Union +from typing import BinaryIO +from typing import Union TSerializable = Union[None, str, bool, int, float, bytes, list, tuple, dict] @@ -138,7 +139,7 @@ def _rdumpq(q: collections.deque, size: int, value: TSerializable) -> int: elif isinstance(value, dict): write(b"}") init_size = size = size + 1 - for (k, v) in value.items(): + for k, v in value.items(): size = _rdumpq(q, size, v) size = _rdumpq(q, size, k) span = str(size - init_size).encode() @@ -153,7 +154,7 @@ def loads(string: bytes) -> TSerializable: """ This function parses a tnetstring into a python object. """ - return pop(string)[0] + return pop(memoryview(string))[0] def load(file_handle: BinaryIO) -> TSerializable: @@ -177,17 +178,17 @@ def load(file_handle: BinaryIO) -> TSerializable: if c != b":": raise ValueError("not a tnetstring: missing or invalid length prefix") - data = file_handle.read(int(data_length)) + data = memoryview(file_handle.read(int(data_length))) data_type = file_handle.read(1)[0] return parse(data_type, data) -def parse(data_type: int, data: bytes) -> TSerializable: +def parse(data_type: int, data: memoryview) -> TSerializable: if data_type == ord(b","): - return data + return data.tobytes() if data_type == ord(b";"): - return data.decode("utf8") + return str(data, "utf8") if data_type == ord(b"#"): try: return int(data) @@ -210,11 +211,11 @@ def parse(data_type: int, data: bytes) -> TSerializable: raise ValueError(f"not a tnetstring: invalid null literal: {data!r}") return None if data_type == ord(b"]"): - l = [] + lst = [] while data: item, data = pop(data) - l.append(item) # type: ignore - return l + lst.append(item) # type: ignore + return lst if data_type == ord(b"}"): d = {} while data: @@ -225,20 +226,28 @@ def parse(data_type: int, data: bytes) -> TSerializable: raise ValueError(f"unknown type tag: {data_type}") -def pop(data: bytes) -> tuple[TSerializable, bytes]: +def split(data: memoryview, sep: bytes) -> tuple[int, memoryview]: + i = 0 + try: + ord_sep = ord(sep) + while data[i] != ord_sep: + i += 1 + # here i is the position of b":" in the memoryview + return int(data[:i]), data[i + 1 :] + except (IndexError, ValueError): + raise ValueError( + f"not a tnetstring: missing or invalid length prefix: {data.tobytes()!r}" + ) + + +def pop(data: memoryview) -> tuple[TSerializable, memoryview]: """ This function parses a tnetstring into a python object. It returns a tuple giving the parsed object and a string containing any unparsed data from the end of the string. """ - # Parse out data length, type and remaining string. - try: - blength, data = data.split(b":", 1) - length = int(blength) - except ValueError: - raise ValueError( - f"not a tnetstring: missing or invalid length prefix: {data!r}" - ) + # Parse out data length, type and remaining string. + length, data = split(data, b":") try: data, data_type, remain = data[:length], data[length], data[length + 1 :] except IndexError: diff --git a/mitmproxy/log.py b/mitmproxy/log.py index 05b609c7d7..5eaa324ad4 100644 --- a/mitmproxy/log.py +++ b/mitmproxy/log.py @@ -1,13 +1,18 @@ from __future__ import annotations + import logging import os +import typing import warnings from dataclasses import dataclass -from mitmproxy import hooks, master +from mitmproxy import hooks from mitmproxy.contrib import click as miniclick from mitmproxy.utils import human +if typing.TYPE_CHECKING: + from mitmproxy import master + ALERT = logging.INFO + 1 """ The ALERT logging level has the same urgency as info, but @@ -42,11 +47,13 @@ def __init__(self, colorize: bool): self.without_client = f"{time} %s" default_time_format = "%H:%M:%S" - default_msec_format = '%s.%03d' + default_msec_format = "%s.%03d" def format(self, record: logging.LogRecord) -> str: time = self.formatTime(record) message = record.getMessage() + if record.exc_info: + message = f"{message}\n{self.formatException(record.exc_info)}" if self.colorize: message = miniclick.style( message, @@ -67,10 +74,9 @@ def __init__(self, *args, **kwargs): def filter(self, record: logging.LogRecord) -> bool: # We can't remove stale handlers here because that would modify .handlers during iteration! - return ( + return bool( super().filter(record) - and - ( + and ( not self._initiated_in_test or self._initiated_in_test == os.environ.get("PYTEST_CURRENT_TEST") ) @@ -79,7 +85,10 @@ def filter(self, record: logging.LogRecord) -> bool: def install(self) -> None: if self._initiated_in_test: for h in list(logging.getLogger().handlers): - if isinstance(h, MitmLogHandler) and h._initiated_in_test != self._initiated_in_test: + if ( + isinstance(h, MitmLogHandler) + and h._initiated_in_test != self._initiated_in_test + ): h.uninstall() logging.getLogger().addHandler(self) @@ -90,6 +99,7 @@ def uninstall(self) -> None: # everything below is deprecated! + class LogEntry: def __init__(self, msg, level): # it's important that we serialize to string here already so that we don't pick up changes @@ -194,6 +204,7 @@ def __call__(self, text, level="info"): class LegacyLogEvents(MitmLogHandler): """Emit deprecated `add_log` events from stdlib logging.""" + def __init__( self, master: master.Master, diff --git a/mitmproxy/master.py b/mitmproxy/master.py index 3e5f248b62..7ed6fde5c9 100644 --- a/mitmproxy/master.py +++ b/mitmproxy/master.py @@ -1,16 +1,17 @@ import asyncio import logging -import traceback -from typing import Optional -from mitmproxy import addonmanager, hooks +from . import ctx as mitmproxy_ctx +from .addons import termlog +from .proxy.mode_specs import ReverseMode +from .utils import asyncio_utils +from mitmproxy import addonmanager from mitmproxy import command from mitmproxy import eventsequence +from mitmproxy import hooks from mitmproxy import http from mitmproxy import log from mitmproxy import options -from . import ctx as mitmproxy_ctx -from .proxy.mode_specs import ReverseMode logger = logging.getLogger(__name__) @@ -21,12 +22,22 @@ class Master: """ event_loop: asyncio.AbstractEventLoop - - def __init__(self, opts, event_loop: Optional[asyncio.AbstractEventLoop] = None): + _termlog_addon: termlog.TermLog | None = None + + def __init__( + self, + opts: options.Options, + event_loop: asyncio.AbstractEventLoop | None = None, + with_termlog: bool = False, + ): self.options: options.Options = opts or options.Options() self.commands = command.CommandManager(self) self.addons = addonmanager.AddonManager(self) + if with_termlog: + self._termlog_addon = termlog.TermLog() + self.addons.add(self._termlog_addon) + self.log = log.Log(self) # deprecated, do not use. self._legacy_log_events = log.LegacyLogEvents(self) self._legacy_log_events.install() @@ -35,35 +46,48 @@ def __init__(self, opts, event_loop: Optional[asyncio.AbstractEventLoop] = None) # may want to spawn tasks during the initial configuration phase, # which happens before run(). self.event_loop = event_loop or asyncio.get_running_loop() - try: - self.should_exit = asyncio.Event() - except RuntimeError: # python 3.9 and below - self.should_exit = asyncio.Event(loop=self.event_loop) # type: ignore + self.should_exit = asyncio.Event() mitmproxy_ctx.master = self mitmproxy_ctx.log = self.log # deprecated, do not use. mitmproxy_ctx.options = self.options async def run(self) -> None: - old_handler = self.event_loop.get_exception_handler() - self.event_loop.set_exception_handler(self._asyncio_exception_handler) - try: + with ( + asyncio_utils.install_exception_handler(self._asyncio_exception_handler), + asyncio_utils.set_eager_task_factory(), + ): self.should_exit.clear() + # Can we exit before even bringing up servers? if ec := self.addons.get("errorcheck"): await ec.shutdown_if_errored() if ps := self.addons.get("proxyserver"): - await ps.setup_servers() - if ec := self.addons.get("errorcheck"): - await ec.shutdown_if_errored() - ec.finish() - await self.running() + # This may block for some proxy modes, so we also monitor should_exit. + await asyncio.wait( + [ + asyncio.create_task(ps.setup_servers()), + asyncio.create_task(self.should_exit.wait()), + ], + return_when=asyncio.FIRST_COMPLETED, + ) + if self.should_exit.is_set(): + return + # Did bringing up servers fail? + if ec := self.addons.get("errorcheck"): + await ec.shutdown_if_errored() + try: + await self.running() + # Any errors in the final part of startup? + if ec := self.addons.get("errorcheck"): + await ec.shutdown_if_errored() + ec.finish() + await self.should_exit.wait() finally: - # .wait might be cancelled (e.g. by sys.exit) + # if running() was called, we also always want to call done(). + # .wait might be cancelled (e.g. by sys.exit), so this needs to be in a finally block. await self.done() - finally: - self.event_loop.set_exception_handler(old_handler) def shutdown(self): """ @@ -78,23 +102,20 @@ async def running(self) -> None: async def done(self) -> None: await self.addons.trigger_event(hooks.DoneHook()) self._legacy_log_events.uninstall() + if self._termlog_addon is not None: + self._termlog_addon.uninstall() - def _asyncio_exception_handler(self, loop, context): + def _asyncio_exception_handler(self, loop, context) -> None: try: exc: Exception = context["exception"] except KeyError: - logger.error( - f"Unhandled asyncio error: {context}" - "\nPlease lodge a bug report at:" - + "\n\thttps://github.com/mitmproxy/mitmproxy/issues" - ) + logger.error(f"Unhandled asyncio error: {context}") else: if isinstance(exc, OSError) and exc.errno == 10038: return # suppress https://bugs.python.org/issue43253 logger.error( - "\n".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) - + "\nPlease lodge a bug report at:" - + "\n\thttps://github.com/mitmproxy/mitmproxy/issues" + "Unhandled error in task.", + exc_info=(type(exc), exc, exc.__traceback__), ) async def load_flow(self, f): @@ -102,12 +123,17 @@ async def load_flow(self, f): Loads a flow """ - if isinstance(f, http.HTTPFlow) and len(self.options.mode) == 1 and self.options.mode[0].startswith("reverse:"): + if ( + isinstance(f, http.HTTPFlow) + and len(self.options.mode) == 1 + and self.options.mode[0].startswith("reverse:") + ): # When we load flows in reverse proxy mode, we adjust the target host to # the reverse proxy destination for all flows we load. This makes it very # easy to replay saved flows against a different host. # We may change this in the future so that clientplayback always replays to the first mode. mode = ReverseMode.parse(self.options.mode[0]) + assert isinstance(mode, ReverseMode) f.request.host, f.request.port, *_ = mode.address f.request.scheme = mode.scheme diff --git a/mitmproxy/net/check.py b/mitmproxy/net/check.py index 9a0bdec496..476170032f 100644 --- a/mitmproxy/net/check.py +++ b/mitmproxy/net/check.py @@ -1,11 +1,11 @@ import ipaddress import re +from typing import AnyStr # Allow underscore in host name # Note: This could be a DNS label, a hostname, a FQDN, or an IP -from typing import AnyStr -_label_valid = re.compile(br"[A-Z\d\-_]{1,63}$", re.IGNORECASE) +_label_valid = re.compile(rb"[A-Z\d\-_]{1,63}$", re.IGNORECASE) def is_valid_host(host: AnyStr) -> bool: diff --git a/mitmproxy/net/dns/domain_names.py b/mitmproxy/net/dns/domain_names.py index cbb666ece0..520fb65e15 100644 --- a/mitmproxy/net/dns/domain_names.py +++ b/mitmproxy/net/dns/domain_names.py @@ -1,6 +1,7 @@ import struct from typing import Optional +from . import types _LABEL_SIZE = struct.Struct("!B") _POINTER_OFFSET = struct.Struct("!H") @@ -29,7 +30,7 @@ def _unpack_label_into(labels: list[str], buffer: bytes, offset: int) -> int: labels.append(buffer[offset:end_label].decode("idna")) except UnicodeDecodeError: raise struct.error( - f"unpack encountered a illegal characters at offset {offset}" + f"unpack encountered an illegal characters at offset {offset}" ) return _LABEL_SIZE.size + size @@ -106,3 +107,63 @@ def pack(name: str) -> bytes: buffer.extend(label) buffer.extend(_LABEL_SIZE.pack(0)) return bytes(buffer) + + +def record_data_can_have_compression(record_type: int) -> bool: + if record_type in ( + types.CNAME, + types.HINFO, + types.MB, + types.MD, + types.MF, + types.MG, + types.MINFO, + types.MR, + types.MX, + types.NS, + types.PTR, + types.SOA, + types.TXT, + types.RP, + types.AFSDB, + types.RT, + types.SIG, + types.PX, + types.NXT, + types.NAPTR, + types.SRV, + ): + return True + return False + + +def decompress_from_record_data( + buffer: bytes, offset: int, end_data: int, cached_names: Cache +) -> bytes: + # we decompress compression pointers in RDATA by iterating through each byte and checking + # if it has a leading 0b11, if so we try to decompress it and update it in the data variable. + data = bytearray(buffer[offset:end_data]) + data_offset = 0 + decompress_size = 0 + while data_offset < end_data - offset: + if buffer[offset + data_offset] & _POINTER_INDICATOR == _POINTER_INDICATOR: + try: + ( + rr_name, + rr_name_len, + ) = unpack_from_with_compression( + buffer, offset + data_offset, cached_names + ) + data[ + data_offset + decompress_size : data_offset + + decompress_size + + rr_name_len + ] = pack(rr_name) + decompress_size += len(rr_name) + data_offset += rr_name_len + continue + except struct.error: + # the byte isn't actually a domain name compression pointer but some other data type + pass + data_offset += 1 + return bytes(data) diff --git a/mitmproxy/net/dns/https_records.py b/mitmproxy/net/dns/https_records.py new file mode 100644 index 0000000000..165a4235ab --- /dev/null +++ b/mitmproxy/net/dns/https_records.py @@ -0,0 +1,108 @@ +import enum +import struct +from dataclasses import dataclass + +from . import domain_names + +""" +HTTPS records are formatted as follows (as per RFC9460): +- a 2-octet field for SvcPriority as an integer in network byte order. +- the uncompressed, fully qualified TargetName, represented as a sequence of length-prefixed labels per Section 3.1 of [RFC1035]. +- the SvcParams, consuming the remainder of the record (so smaller than 65535 octets and constrained by the RDATA and DNS message sizes). + +When the list of SvcParams is non-empty, it contains a series of SvcParamKey=SvcParamValue pairs, represented as: +- a 2-octet field containing the SvcParamKey as an integer in network byte order. (See Section 14.3.2 for the defined values.) +- a 2-octet field containing the length of the SvcParamValue as an integer between 0 and 65535 in network byte order. +- an octet string of this length whose contents are the SvcParamValue in a format determined by the SvcParamKey. + + https://datatracker.ietf.org/doc/rfc9460/ + https://datatracker.ietf.org/doc/rfc1035/ +""" + + +class SVCParamKeys(enum.Enum): + MANDATORY = 0 + ALPN = 1 + NO_DEFAULT_ALPN = 2 + PORT = 3 + IPV4HINT = 4 + ECH = 5 + IPV6HINT = 6 + + +@dataclass +class HTTPSRecord: + priority: int + target_name: str + params: dict[int, bytes] + + def __repr__(self): + params = {} + for param_type, param_value in self.params.items(): + try: + name = SVCParamKeys(param_type).name.lower() + except ValueError: + name = f"key{param_type}" + params[name] = param_value + return f"priority: {self.priority} target_name: '{self.target_name}' {params}" + + +def _unpack_params(data: bytes, offset: int) -> dict[int, bytes]: + """Unpacks the service parameters from the given offset.""" + params = {} + while offset < len(data): + param_type = struct.unpack("!H", data[offset : offset + 2])[0] + offset += 2 + param_length = struct.unpack("!H", data[offset : offset + 2])[0] + offset += 2 + if offset + param_length > len(data): + raise struct.error( + "unpack requires a buffer of %i bytes" % (offset + param_length) + ) + param_value = data[offset : offset + param_length] + offset += param_length + params[param_type] = param_value + return params + + +def unpack(data: bytes) -> HTTPSRecord: + """ + Unpacks HTTPS RDATA from byte data. + + Raises: + struct.error if the record is malformed. + """ + offset = 0 + + # Priority (2 bytes) + priority = struct.unpack("!h", data[offset : offset + 2])[0] + offset += 2 + + # TargetName (variable length) + target_name, offset = domain_names.unpack_from(data, offset) + + # Service Parameters (remaining bytes) + params = _unpack_params(data, offset) + + return HTTPSRecord(priority=priority, target_name=target_name, params=params) + + +def _pack_params(params: dict[int, bytes]) -> bytes: + """Converts the service parameters into the raw byte format""" + buffer = bytearray() + + for k, v in params.items(): + buffer.extend(struct.pack("!H", k)) + buffer.extend(struct.pack("!H", len(v))) + buffer.extend(v) + + return bytes(buffer) + + +def pack(record: HTTPSRecord) -> bytes: + """Packs the HTTPS record into its bytes form.""" + buffer = bytearray() + buffer.extend(struct.pack("!h", record.priority)) + buffer.extend(domain_names.pack(record.target_name)) + buffer.extend(_pack_params(record.params)) + return bytes(buffer) diff --git a/mitmproxy/net/encoding.py b/mitmproxy/net/encoding.py index 32e61b62c6..ca4a71ace1 100644 --- a/mitmproxy/net/encoding.py +++ b/mitmproxy/net/encoding.py @@ -7,7 +7,7 @@ import gzip import zlib from io import BytesIO -from typing import Union, overload +from typing import overload import brotli import zstandard as zstd @@ -21,23 +21,20 @@ @overload -def decode(encoded: None, encoding: str, errors: str = "strict") -> None: - ... +def decode(encoded: None, encoding: str, errors: str = "strict") -> None: ... @overload -def decode(encoded: str, encoding: str, errors: str = "strict") -> str: - ... +def decode(encoded: str, encoding: str, errors: str = "strict") -> str: ... @overload -def decode(encoded: bytes, encoding: str, errors: str = "strict") -> Union[str, bytes]: - ... +def decode(encoded: bytes, encoding: str, errors: str = "strict") -> str | bytes: ... def decode( - encoded: Union[None, str, bytes], encoding: str, errors: str = "strict" -) -> Union[None, str, bytes]: + encoded: None | str | bytes, encoding: str, errors: str = "strict" +) -> None | str | bytes: """ Decode the given input object @@ -82,23 +79,20 @@ def decode( @overload -def encode(decoded: None, encoding: str, errors: str = "strict") -> None: - ... +def encode(decoded: None, encoding: str, errors: str = "strict") -> None: ... @overload -def encode(decoded: str, encoding: str, errors: str = "strict") -> Union[str, bytes]: - ... +def encode(decoded: str, encoding: str, errors: str = "strict") -> str | bytes: ... @overload -def encode(decoded: bytes, encoding: str, errors: str = "strict") -> bytes: - ... +def encode(decoded: bytes, encoding: str, errors: str = "strict") -> bytes: ... def encode( - decoded: Union[None, str, bytes], encoding, errors="strict" -) -> Union[None, str, bytes]: + decoded: None | str | bytes, encoding, errors="strict" +) -> None | str | bytes: """ Encode the given input object @@ -153,15 +147,15 @@ def identity(content): def decode_gzip(content: bytes) -> bytes: if not content: return b"" - gfile = gzip.GzipFile(fileobj=BytesIO(content)) - return gfile.read() + with gzip.GzipFile(fileobj=BytesIO(content)) as f: + return f.read() def encode_gzip(content: bytes) -> bytes: s = BytesIO() - gf = gzip.GzipFile(fileobj=s, mode="wb") - gf.write(content) - gf.close() + # set mtime to 0 so that gzip encoding is deterministic. + with gzip.GzipFile(fileobj=s, mode="wb", mtime=0) as f: + f.write(content) return s.getvalue() @@ -179,12 +173,7 @@ def decode_zstd(content: bytes) -> bytes: if not content: return b"" zstd_ctx = zstd.ZstdDecompressor() - try: - return zstd_ctx.decompress(content) - except zstd.ZstdError: - # If the zstd stream is streamed without a size header, - # try decoding with a 10MiB output buffer - return zstd_ctx.decompress(content, max_output_size=10 * 2 ** 20) + return zstd_ctx.stream_reader(BytesIO(content), read_across_frames=True).read() def encode_zstd(content: bytes) -> bytes: diff --git a/mitmproxy/net/http/cookies.py b/mitmproxy/net/http/cookies.py index 4b2ddd9413..0c615febcb 100644 --- a/mitmproxy/net/http/cookies.py +++ b/mitmproxy/net/http/cookies.py @@ -1,7 +1,7 @@ import email.utils import re import time -from typing import Iterable +from collections.abc import Iterable from mitmproxy.coretypes import multidict @@ -48,8 +48,8 @@ def _reduce_values(values): return values[-1] -TSetCookie = tuple[str, str, CookieAttrs] -TPairs = list[list[str]] # TODO: Should be List[Tuple[str,str]]? +TSetCookie = tuple[str, str | None, CookieAttrs] +TPairs = list[tuple[str, str | None]] def _read_until(s, start, term): @@ -169,10 +169,10 @@ def _read_set_cookie_pairs(s: str, off=0) -> tuple[list[TPairs], int]: rhs = rhs + "," + trail # as long as there's a "=", we consider it a pair - pairs.append([lhs, rhs]) + pairs.append((lhs, rhs)) elif lhs: - pairs.append([lhs, rhs]) + pairs.append((lhs, None)) # comma marks the beginning of a new cookie if off < len(s) and s[off] == ",": @@ -206,10 +206,15 @@ def _format_pairs(pairs, specials=(), sep="; "): """ vals = [] for k, v in pairs: - if k.lower() not in specials and _has_special(v): + if v is None: + val = k + elif k.lower() not in specials and _has_special(v): v = ESCAPE.sub(r"\\\1", v) v = '"%s"' % v - vals.append(f"{k}={v}") + val = f"{k}={v}" + else: + val = f"{k}={v}" + vals.append(val) return sep.join(vals) @@ -274,7 +279,6 @@ def format_set_cookie_header(set_cookies: list[TSetCookie]) -> str: rv = [] for name, value, attrs in set_cookies: - pairs = [(name, value)] pairs.extend(attrs.fields if hasattr(attrs, "fields") else attrs) diff --git a/mitmproxy/net/http/headers.py b/mitmproxy/net/http/headers.py index e3c00994a7..7e14b2a77c 100644 --- a/mitmproxy/net/http/headers.py +++ b/mitmproxy/net/http/headers.py @@ -1,8 +1,8 @@ import collections -from typing import Optional +import re -def parse_content_type(c: str) -> Optional[tuple[str, str, dict[str, str]]]: +def parse_content_type(c: str) -> tuple[str, str, dict[str, str]] | None: """ A simple parser for content-type values. Returns a (type, subtype, parameters) tuple, where type and subtype are strings, and parameters @@ -33,4 +33,40 @@ def assemble_content_type(type, subtype, parameters): if not parameters: return f"{type}/{subtype}" params = "; ".join(f"{k}={v}" for k, v in parameters.items()) - return "{}/{}; {}".format(type, subtype, params) + return f"{type}/{subtype}; {params}" + + +def infer_content_encoding(content_type: str, content: bytes = b"") -> str: + """ + Infer the encoding of content from the content-type header. + """ + # Use the charset from the header if possible + parsed_content_type = parse_content_type(content_type) + enc = parsed_content_type[2].get("charset") if parsed_content_type else None + + # Otherwise, infer the encoding + if not enc and "json" in content_type: + enc = "utf8" + + if not enc and "html" in content_type: + meta_charset = re.search( + rb"""]+charset=['"]?([^'">]+)""", content, re.IGNORECASE + ) + if meta_charset: + enc = meta_charset.group(1).decode("ascii", "ignore") + + if not enc and "text/css" in content_type: + # @charset rule must be the very first thing. + css_charset = re.match(rb"""@charset "([^"]+)";""", content, re.IGNORECASE) + if css_charset: + enc = css_charset.group(1).decode("ascii", "ignore") + + # Fallback to latin-1 + if not enc: + enc = "latin-1" + + # Use GB 18030 as the superset of GB2312 and GBK to fix common encoding problems on Chinese websites. + if enc.lower() in ("gb2312", "gbk"): + enc = "gb18030" + + return enc diff --git a/mitmproxy/net/http/http1/__init__.py b/mitmproxy/net/http/http1/__init__.py index 3049e02fb9..01c543afb5 100644 --- a/mitmproxy/net/http/http1/__init__.py +++ b/mitmproxy/net/http/http1/__init__.py @@ -1,25 +1,18 @@ -from .read import ( - read_request_head, - read_response_head, - connection_close, - expected_http_body_size, - validate_headers, -) -from .assemble import ( - assemble_request, - assemble_request_head, - assemble_response, - assemble_response_head, - assemble_body, -) - +from .assemble import assemble_body +from .assemble import assemble_request +from .assemble import assemble_request_head +from .assemble import assemble_response +from .assemble import assemble_response_head +from .read import connection_close +from .read import expected_http_body_size +from .read import read_request_head +from .read import read_response_head __all__ = [ "read_request_head", "read_response_head", "connection_close", "expected_http_body_size", - "validate_headers", "assemble_request", "assemble_request_head", "assemble_response", diff --git a/mitmproxy/net/http/http1/read.py b/mitmproxy/net/http/http1/read.py index 1da4583e1d..f0876d6777 100644 --- a/mitmproxy/net/http/http1/read.py +++ b/mitmproxy/net/http/http1/read.py @@ -1,9 +1,13 @@ import re import time -from typing import Iterable, Optional +import typing +from collections.abc import Iterable -from mitmproxy.http import Request, Headers, Response +from mitmproxy.http import Headers +from mitmproxy.http import Request +from mitmproxy.http import Response from mitmproxy.net.http import url +from mitmproxy.net.http import validate def get_header_tokens(headers, key): @@ -40,43 +44,9 @@ def connection_close(http_version, headers): ) -# https://datatracker.ietf.org/doc/html/rfc7230#section-3.2: Header fields are tokens. -# "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA -_valid_header_name = re.compile(rb"^[!#$%&'*+\-.^_`|~0-9a-zA-Z]+$") - - -def validate_headers(headers: Headers) -> None: - """ - Validate headers to avoid request smuggling attacks. Raises a ValueError if they are malformed. - """ - - te_found = False - cl_found = False - - for (name, value) in headers.fields: - if not _valid_header_name.match(name): - raise ValueError( - f"Received an invalid header name: {name!r}. Invalid header names may introduce " - f"request smuggling vulnerabilities. Disable the validate_inbound_headers option " - f"to skip this security check." - ) - - name_lower = name.lower() - te_found = te_found or name_lower == b"transfer-encoding" - cl_found = cl_found or name_lower == b"content-length" - - if te_found and cl_found: - raise ValueError( - "Received both a Transfer-Encoding and a Content-Length header, " - "refusing as recommended in RFC 7230 Section 3.3.3. " - "See https://github.com/mitmproxy/mitmproxy/issues/4799 for details. " - "Disable the validate_inbound_headers option to skip this security check." - ) - - def expected_http_body_size( - request: Request, response: Optional[Response] = None -) -> Optional[int]: + request: Request, response: Response | None = None +) -> int | None: """ Returns: The expected body length: @@ -85,7 +55,7 @@ def expected_http_body_size( - -1, if all data should be read until end of stream. Raises: - ValueError, if the content length header is invalid + ValueError, if the content-length or transfer-encoding header is invalid """ # Determine response size according to http://tools.ietf.org/html/rfc7230#section-3.3, which is inlined below. if not response: @@ -135,39 +105,20 @@ def expected_http_body_size( # remove the received Content-Length field prior to forwarding such # a message downstream. # - if "transfer-encoding" in headers: - # we should make sure that there isn't also a content-length header. - # this is already handled in validate_headers. - - te: str = headers["transfer-encoding"] - if not te.isascii(): - # guard against .lower() transforming non-ascii to ascii - raise ValueError(f"Invalid transfer encoding: {te!r}") - te = te.lower().strip("\t ") - te = re.sub(r"[\t ]*,[\t ]*", ",", te) - if te in ( - "chunked", - "compress,chunked", - "deflate,chunked", - "gzip,chunked", - ): - return None - elif te in ( - "compress", - "deflate", - "gzip", - "identity", - ): - if response: - return -1 - else: - raise ValueError( - f"Invalid request transfer encoding, message body cannot be determined reliably." - ) - else: - raise ValueError( - f"Unknown transfer encoding: {headers['transfer-encoding']!r}" - ) + if te_str := headers.get("transfer-encoding"): + te = validate.parse_transfer_encoding(te_str) + match te: + case "chunked" | "compress,chunked" | "deflate,chunked" | "gzip,chunked": + return None + case "compress" | "deflate" | "gzip" | "identity": + if response: + return -1 + else: + raise ValueError( + "Invalid request transfer encoding, message body cannot be determined reliably." + ) + case other: # pragma: no cover + typing.assert_never(other) # 4. If a message is received without Transfer-Encoding and with # either multiple Content-Length header fields having differing @@ -188,19 +139,8 @@ def expected_http_body_size( # the recipient times out before the indicated number of octets are # received, the recipient MUST consider the message to be # incomplete and close the connection. - if "content-length" in headers: - sizes = headers.get_all("content-length") - different_content_length_headers = any(x != sizes[0] for x in sizes) - if different_content_length_headers: - raise ValueError(f"Conflicting Content-Length headers: {sizes!r}") - try: - size = int(sizes[0]) - except ValueError: - raise ValueError(f"Invalid Content-Length header: {sizes[0]!r}") - if size < 0: - raise ValueError(f"Negative Content-Length header: {sizes[0]!r}") - return size - + if cl := headers.get("content-length"): + return validate.parse_content_length(cl) # 6. If this is a request message and none of the above are true, then # the message body length is zero (no message body is present). if not response: @@ -214,7 +154,7 @@ def expected_http_body_size( def raise_if_http_version_unknown(http_version: bytes) -> None: - if not re.match(br"^HTTP/\d\.\d$", http_version): + if not re.match(rb"^HTTP/\d\.\d$", http_version): raise ValueError(f"Unknown HTTP version: {http_version!r}") @@ -223,7 +163,7 @@ def _read_request_line( ) -> tuple[str, int, bytes, bytes, bytes, bytes, bytes]: try: method, target, http_version = line.split() - port: Optional[int] + port: int | None if target == b"*" or target.startswith(b"/"): scheme, authority, path = b"", b"", target diff --git a/mitmproxy/net/http/multipart.py b/mitmproxy/net/http/multipart.py index 0079995875..c2c0a8bbfa 100644 --- a/mitmproxy/net/http/multipart.py +++ b/mitmproxy/net/http/multipart.py @@ -1,23 +1,24 @@ +from __future__ import annotations + import mimetypes import re -from typing import Optional +import warnings from urllib.parse import quote from mitmproxy.net.http import headers -def encode(head, l): - k = head.get("content-type") - if k: - k = headers.parse_content_type(k) - if k is not None: +def encode_multipart(content_type: str, parts: list[tuple[bytes, bytes]]) -> bytes: + if content_type: + ct = headers.parse_content_type(content_type) + if ct is not None: try: - boundary = k[2]["boundary"].encode("ascii") - boundary = quote(boundary) + raw_boundary = ct[2]["boundary"].encode("ascii") + boundary = quote(raw_boundary) except (KeyError, UnicodeError): return b"" hdrs = [] - for key, value in l: + for key, value in parts: file_type = ( mimetypes.guess_type(str(key))[0] or "text/plain; charset=utf-8" ) @@ -41,9 +42,12 @@ def encode(head, l): hdrs.append(b"--%b--\r\n" % boundary.encode("utf-8")) temp = b"\r\n".join(hdrs) return temp + return b"" -def decode(content_type: Optional[str], content: bytes) -> list[tuple[bytes, bytes]]: +def decode_multipart( + content_type: str | None, content: bytes +) -> list[tuple[bytes, bytes]]: """ Takes a multipart boundary encoded string and returns list of (key, value) tuples. """ @@ -56,7 +60,7 @@ def decode(content_type: Optional[str], content: bytes) -> list[tuple[bytes, byt except (KeyError, UnicodeError): return [] - rx = re.compile(br'\bname="([^"]+)"') + rx = re.compile(rb'\bname="([^"]+)"') r = [] if content is not None: for i in content.split(b"--" + boundary): @@ -69,3 +73,23 @@ def decode(content_type: Optional[str], content: bytes) -> list[tuple[bytes, byt r.append((key, value)) return r return [] + + +def encode(ct, parts): # pragma: no cover + # 2023-02 + warnings.warn( + "multipart.encode is deprecated, use multipart.encode_multipart instead.", + DeprecationWarning, + stacklevel=2, + ) + return encode_multipart(ct, parts) + + +def decode(ct, content): # pragma: no cover + # 2023-02 + warnings.warn( + "multipart.decode is deprecated, use multipart.decode_multipart instead.", + DeprecationWarning, + stacklevel=2, + ) + return encode_multipart(ct, content) diff --git a/mitmproxy/net/http/status_codes.py b/mitmproxy/net/http/status_codes.py index 66e7d49ab6..fe99485691 100644 --- a/mitmproxy/net/http/status_codes.py +++ b/mitmproxy/net/http/status_codes.py @@ -39,6 +39,7 @@ REQUESTED_RANGE_NOT_SATISFIABLE = 416 EXPECTATION_FAILED = 417 IM_A_TEAPOT = 418 +UNPROCESSABLE_CONTENT = 422 NO_RESPONSE = 444 CLIENT_CLOSED_REQUEST = 499 @@ -95,6 +96,7 @@ REQUESTED_RANGE_NOT_SATISFIABLE: "Requested Range not satisfiable", EXPECTATION_FAILED: "Expectation Failed", IM_A_TEAPOT: "I'm a teapot", + UNPROCESSABLE_CONTENT: "Unprocessable Content", NO_RESPONSE: "No Response", CLIENT_CLOSED_REQUEST: "Client Closed Request", # 500 diff --git a/mitmproxy/net/http/url.py b/mitmproxy/net/http/url.py index 9468302e12..c44228ec1a 100644 --- a/mitmproxy/net/http/url.py +++ b/mitmproxy/net/http/url.py @@ -1,15 +1,18 @@ +from __future__ import annotations + import re import urllib.parse from collections.abc import Sequence -from typing import AnyStr, Optional +from typing import AnyStr from mitmproxy.net import check +from mitmproxy.net.check import is_valid_host +from mitmproxy.net.check import is_valid_port +from mitmproxy.utils.strutils import always_str # This regex extracts & splits the host header into host and port. # Handles the edge case of IPv6 addresses containing colons. # https://bugzilla.mozilla.org/show_bug.cgi?id=45891 -from mitmproxy.net.check import is_valid_host, is_valid_port -from mitmproxy.utils.strutils import always_str _authority_re = re.compile(r"^(?P[^:]+|\[.+\])(?::(?P\d+))?$") @@ -34,8 +37,8 @@ def parse(url): # Size of Ascii character after encoding is 1 byte which is same as its size # But non-Ascii character's size after encoding will be more than its size - def ascii_check(l): - if len(l) == len(str(l).encode()): + def ascii_check(x): + if len(x) == len(str(x).encode()): return True return False @@ -85,7 +88,7 @@ def unparse(scheme: str, host: str, port: int, path: str = "") -> str: return f"{scheme}://{authority}{path}" -def encode(s: Sequence[tuple[str, str]], similar_to: str = None) -> str: +def encode(s: Sequence[tuple[str, str]], similar_to: str | None = None) -> str: """ Takes a list of (key, value) tuples and returns a urlencoded string. If similar_to is passed, the output is formatted similar to the provided urlencoded string. @@ -143,7 +146,7 @@ def hostport(scheme: AnyStr, host: AnyStr, port: int) -> AnyStr: return "%s:%d" % (host, port) -def default_port(scheme: AnyStr) -> Optional[int]: +def default_port(scheme: AnyStr) -> int | None: return { "http": 80, b"http": 80, @@ -152,7 +155,7 @@ def default_port(scheme: AnyStr) -> Optional[int]: }.get(scheme, None) -def parse_authority(authority: AnyStr, check: bool) -> tuple[str, Optional[int]]: +def parse_authority(authority: AnyStr, check: bool) -> tuple[str, int | None]: """Extract the host and port from host header/authority information Raises: diff --git a/mitmproxy/net/http/user_agents.py b/mitmproxy/net/http/user_agents.py index 58aa21eab9..65b8d0cfe4 100644 --- a/mitmproxy/net/http/user_agents.py +++ b/mitmproxy/net/http/user_agents.py @@ -1,10 +1,8 @@ """ - A small collection of useful user-agent header strings. These should be - kept reasonably current to reflect common usage. +A small collection of useful user-agent header strings. These should be +kept reasonably current to reflect common usage. """ - # pylint: line-too-long - # A collection of (name, shortcut, string) tuples. UASTRINGS = [ diff --git a/mitmproxy/net/http/validate.py b/mitmproxy/net/http/validate.py new file mode 100644 index 0000000000..bc5c140e8f --- /dev/null +++ b/mitmproxy/net/http/validate.py @@ -0,0 +1,126 @@ +import logging +import re +import typing + +from mitmproxy.http import Message +from mitmproxy.http import Response + +logger = logging.getLogger(__name__) + +# https://datatracker.ietf.org/doc/html/rfc7230#section-3.2: Header fields are tokens. +# "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA +_valid_header_name = re.compile(rb"^[!#$%&'*+\-.^_`|~0-9a-zA-Z]+$") + +_valid_content_length = re.compile(rb"^(?:0|[1-9][0-9]*)$") +_valid_content_length_str = re.compile(r"^(?:0|[1-9][0-9]*)$") + +# https://datatracker.ietf.org/doc/html/rfc9112#section-6.1: +# > A sender MUST NOT apply the chunked transfer coding more than once to a message body (i.e., chunking an already +# > chunked message is not allowed). If any transfer coding other than chunked is applied to a request's content, the +# > sender MUST apply chunked as the final transfer coding to ensure that the message is properly framed. If any +# > transfer coding other than chunked is applied to a response's content, the sender MUST either apply chunked as the +# > final transfer coding or terminate the message by closing the connection. +# +# The RFC technically still allows for fun encodings, we are a bit stricter and only accept a known subset by default. +TransferEncoding = typing.Literal[ + "chunked", + "compress,chunked", + "deflate,chunked", + "gzip,chunked", + "compress", + "deflate", + "gzip", + "identity", +] +_HTTP_1_1_TRANSFER_ENCODINGS = frozenset(typing.get_args(TransferEncoding)) + + +def parse_content_length(value: str | bytes) -> int: + """Parse a content-length header value, or raise a ValueError if it is invalid.""" + if isinstance(value, str): + valid = bool(_valid_content_length_str.match(value)) + else: + valid = bool(_valid_content_length.match(value)) + if not valid: + raise ValueError(f"invalid content-length header: {value!r}") + return int(value) + + +def parse_transfer_encoding(value: str | bytes) -> TransferEncoding: + """Parse a transfer-encoding header value, or raise a ValueError if it is invalid or unknown.""" + # guard against .lower() transforming non-ascii to ascii + if not value.isascii(): + raise ValueError(f"invalid transfer-encoding header: {value!r}") + if isinstance(value, str): + te = value + else: + te = value.decode() + te = te.lower() + te = re.sub(r"[\t ]*,[\t ]*", ",", te) + if te not in _HTTP_1_1_TRANSFER_ENCODINGS: + raise ValueError(f"unknown transfer-encoding header: {value!r}") + return typing.cast(TransferEncoding, te) + + +def validate_headers(message: Message) -> None: + """ + Validate HTTP message headers to avoid request smuggling attacks. + + Raises a ValueError if they are malformed. + """ + + te = [] + cl = [] + + for name, value in message.headers.fields: + if not _valid_header_name.match(name): + raise ValueError(f"invalid header name: {name!r}") + match name.lower(): + case b"transfer-encoding": + te.append(value) + case b"content-length": + cl.append(value) + + if te and cl: + # > A server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a + # > request in accordance with the Transfer-Encoding alone. + + # > A sender MUST NOT send a Content-Length header field in any message that contains a Transfer-Encoding header + # > field. + raise ValueError( + "message with both transfer-encoding and content-length headers" + ) + elif te: + if len(te) > 1: + raise ValueError(f"multiple transfer-encoding headers: {te!r}") + # > Transfer-Encoding was added in HTTP/1.1. It is generally assumed that implementations advertising only + # > HTTP/1.0 support will not understand how to process transfer-encoded content, and that an HTTP/1.0 message + # > received with a Transfer-Encoding is likely to have been forwarded without proper handling of the chunked + # > transfer coding in transit. + # + # > A client MUST NOT send a request containing Transfer-Encoding unless it knows the server will handle + # > HTTP/1.1 requests (or later minor revisions); such knowledge might be in the form of specific user + # > configuration or by remembering the version of a prior received response. A server MUST NOT send a response + # > containing Transfer-Encoding unless the corresponding request indicates HTTP/1.1 (or later minor revisions). + + # > A server MUST NOT send a Transfer-Encoding header field in any response with a status code of 1xx + # > (Informational) or 204 (No Content). + te_disallowed = not message.is_http11 or ( + isinstance(message, Response) + and (100 <= message.status_code <= 199 or message.status_code == 204) + ) + if te_disallowed: + raise ValueError( + f"Unexpected HTTP transfer encoding: {message.http_version!r}" + ) + parse_transfer_encoding(te[0]) + elif cl: + # > If a message is received without Transfer-Encoding and with an invalid Content-Length header field, then the + # > message framing is invalid and the recipient MUST treat it as an unrecoverable error, unless the field value + # > can be successfully parsed as a comma-separated list (Section 5.6.1 of [HTTP]), all values in the list are + # > valid, and all values in the list are the same (in which case, the message is processed with that single + # > value used as the Content-Length field value). + # We are stricter here and reject comma-separated lists. + if len(cl) > 1: + raise ValueError(f"multiple content-length headers: {cl!r}") + parse_content_length(cl[0]) diff --git a/mitmproxy/net/local_ip.py b/mitmproxy/net/local_ip.py index 27468c05c5..919d31dd33 100644 --- a/mitmproxy/net/local_ip.py +++ b/mitmproxy/net/local_ip.py @@ -1,4 +1,5 @@ from __future__ import annotations + import socket @@ -9,14 +10,16 @@ def get_local_ip(reachable: str = "8.8.8.8") -> str | None: We use Google DNS's IPv4 address as the default. """ # https://stackoverflow.com/questions/166506/finding-local-ip-addresses-using-pythons-stdlib - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s = None try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect((reachable, 80)) - return s.getsockname()[0] + return s.getsockname()[0] # pragma: no cover except OSError: - return None + return None # pragma: no cover finally: - s.close() + if s is not None: + s.close() def get_local_ip6(reachable: str = "2001:4860:4860::8888") -> str | None: @@ -25,11 +28,13 @@ def get_local_ip6(reachable: str = "2001:4860:4860::8888") -> str | None: This will fail if the target address is known to be unreachable. We use Google DNS's IPv6 address as the default. """ - s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + s = None try: + s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) s.connect((reachable, 80)) - return s.getsockname()[0] + return s.getsockname()[0] # pragma: no cover except OSError: - return None + return None # pragma: no cover finally: - s.close() + if s is not None: + s.close() diff --git a/mitmproxy/net/server_spec.py b/mitmproxy/net/server_spec.py index 2ab697802c..254811ff3f 100644 --- a/mitmproxy/net/server_spec.py +++ b/mitmproxy/net/server_spec.py @@ -1,6 +1,7 @@ """ Server specs are used to describe an upstream proxy or server. """ + import re from functools import cache from typing import Literal @@ -8,8 +9,8 @@ from mitmproxy.net import check ServerSpec = tuple[ - Literal["http", "https", "tls", "dtls", "tcp", "udp", "dns"], - tuple[str, int] + Literal["http", "https", "http3", "tls", "dtls", "tcp", "udp", "dns", "quic"], + tuple[str, int], ] server_spec_re = re.compile( @@ -45,7 +46,17 @@ def parse(server_spec: str, default_scheme: str) -> ServerSpec: scheme = m.group("scheme") else: scheme = default_scheme - if scheme not in ("http", "https", "tls", "dtls", "tcp", "udp", "dns"): + if scheme not in ( + "http", + "https", + "http3", + "tls", + "dtls", + "tcp", + "udp", + "dns", + "quic", + ): raise ValueError(f"Invalid server scheme: {scheme}") host = m.group("host") @@ -62,6 +73,8 @@ def parse(server_spec: str, default_scheme: str) -> ServerSpec: port = { "http": 80, "https": 443, + "quic": 443, + "http3": 443, "dns": 53, }[scheme] except KeyError: diff --git a/mitmproxy/net/tls.py b/mitmproxy/net/tls.py index d87dc1a67c..5fd6facb70 100644 --- a/mitmproxy/net/tls.py +++ b/mitmproxy/net/tls.py @@ -1,25 +1,35 @@ import os import threading +from collections.abc import Callable +from collections.abc import Iterable from enum import Enum +from functools import cache from functools import lru_cache from pathlib import Path -from typing import Any, BinaryIO, Callable, Iterable, Optional +from typing import Any +from typing import BinaryIO import certifi - +from OpenSSL import crypto +from OpenSSL import SSL from OpenSSL.crypto import X509 -from OpenSSL import SSL from mitmproxy import certs +# Remove once pyOpenSSL 23.3.0 is released and bump version in pyproject.toml. +try: # pragma: no cover + from OpenSSL.SSL import OP_LEGACY_SERVER_CONNECT # type: ignore +except ImportError: + OP_LEGACY_SERVER_CONNECT = 0x4 + # redeclared here for strict type checking class Method(Enum): TLS_SERVER_METHOD = SSL.TLS_SERVER_METHOD TLS_CLIENT_METHOD = SSL.TLS_CLIENT_METHOD # Type-pyopenssl does not know about these DTLS constants. - DTLS_SERVER_METHOD = SSL.DTLS_SERVER_METHOD # type: ignore - DTLS_CLIENT_METHOD = SSL.DTLS_CLIENT_METHOD # type: ignore + DTLS_SERVER_METHOD = SSL.DTLS_SERVER_METHOD # type: ignore + DTLS_CLIENT_METHOD = SSL.DTLS_CLIENT_METHOD # type: ignore try: @@ -39,6 +49,14 @@ class Version(Enum): TLS1_3 = SSL.TLS1_3_VERSION +INSECURE_TLS_MIN_VERSIONS: tuple[Version, ...] = ( + Version.UNBOUNDED, + Version.SSL3, + Version.TLS1, + Version.TLS1_1, +) + + class Verify(Enum): VERIFY_NONE = SSL.VERIFY_NONE VERIFY_PEER = SSL.VERIFY_PEER @@ -49,10 +67,29 @@ class Verify(Enum): DEFAULT_OPTIONS = SSL.OP_CIPHER_SERVER_PREFERENCE | SSL.OP_NO_COMPRESSION +@cache +def is_supported_version(version: Version): + client_ctx = SSL.Context(SSL.TLS_CLIENT_METHOD) + # Without SECLEVEL, recent OpenSSL versions forbid old TLS versions. + # https://github.com/pyca/cryptography/issues/9523 + client_ctx.set_cipher_list(b"@SECLEVEL=0:ALL") + client_ctx.set_min_proto_version(version.value) + client_ctx.set_max_proto_version(version.value) + client_conn = SSL.Connection(client_ctx) + client_conn.set_connect_state() + + try: + client_conn.recv(4096) + except SSL.WantReadError: + return True + except SSL.Error: + return False + + class MasterSecretLogger: def __init__(self, filename: Path): self.filename = filename.expanduser() - self.f: Optional[BinaryIO] = None + self.f: BinaryIO | None = None self.lock = threading.Lock() # required for functools.wraps, which pyOpenSSL uses. @@ -73,7 +110,7 @@ def close(self): self.f.close() -def make_master_secret_logger(filename: Optional[str]) -> Optional[MasterSecretLogger]: +def make_master_secret_logger(filename: str | None) -> MasterSecretLogger | None: if filename: return MasterSecretLogger(Path(filename)) return None @@ -89,7 +126,8 @@ def _create_ssl_context( method: Method, min_version: Version, max_version: Version, - cipher_list: Optional[Iterable[str]], + cipher_list: Iterable[str] | None, + ecdh_curve: str | None, ) -> SSL.Context: context = SSL.Context(method.value) @@ -104,6 +142,13 @@ def _create_ssl_context( # Options context.set_options(DEFAULT_OPTIONS) + # ECDHE for Key exchange + if ecdh_curve is not None: + try: + context.set_tmp_ecdh(crypto.get_elliptic_curve(ecdh_curve)) + except ValueError as e: + raise RuntimeError(f"Elliptic curve specification error: {e}") from e + # Cipher List if cipher_list is not None: try: @@ -124,17 +169,20 @@ def create_proxy_server_context( method: Method, min_version: Version, max_version: Version, - cipher_list: Optional[tuple[str, ...]], + cipher_list: tuple[str, ...] | None, + ecdh_curve: str | None, verify: Verify, - ca_path: Optional[str], - ca_pemfile: Optional[str], - client_cert: Optional[str], + ca_path: str | None, + ca_pemfile: str | None, + client_cert: str | None, + legacy_server_connect: bool, ) -> SSL.Context: context: SSL.Context = _create_ssl_context( method=method, min_version=min_version, max_version=max_version, cipher_list=cipher_list, + ecdh_curve=ecdh_curve, ) context.set_verify(verify.value, None) @@ -155,6 +203,9 @@ def create_proxy_server_context( except SSL.Error as e: raise RuntimeError(f"Cannot load TLS client certificate: {e}") from e + if legacy_server_connect: + context.set_options(OP_LEGACY_SERVER_CONNECT) + return context @@ -164,9 +215,10 @@ def create_client_proxy_context( method: Method, min_version: Version, max_version: Version, - cipher_list: Optional[tuple[str, ...]], - chain_file: Optional[Path], - alpn_select_callback: Optional[Callable[[SSL.Connection, list[bytes]], Any]], + cipher_list: tuple[str, ...] | None, + ecdh_curve: str | None, + chain_file: Path | None, + alpn_select_callback: Callable[[SSL.Connection, list[bytes]], Any] | None, request_client_cert: bool, extra_chain_certs: tuple[certs.Cert, ...], dhparams: certs.DHParams, @@ -176,6 +228,7 @@ def create_client_proxy_context( min_version=min_version, max_version=max_version, cipher_list=cipher_list, + ecdh_curve=ecdh_curve, ) if chain_file is not None: @@ -222,15 +275,28 @@ def accept_all( return True -def is_tls_record_magic(d): +def starts_like_tls_record(d: bytes) -> bool: """ Returns: - True, if the passed bytes start with the TLS record magic bytes. + True, if the passed bytes could be the start of a TLS record False, otherwise. """ - d = d[:3] - # TLS ClientHello magic, works for SSLv3, TLSv1.0, TLSv1.1, TLSv1.2, and TLSv1.3 # http://www.moserware.com/2009/06/first-few-milliseconds-of-https.html#client-hello # https://tls13.ulfheim.net/ - return len(d) == 3 and d[0] == 0x16 and d[1] == 0x03 and 0x0 <= d[2] <= 0x03 + # We assume that a client sending less than 3 bytes initially is not a TLS client. + return len(d) > 2 and d[0] == 0x16 and d[1] == 0x03 and 0x00 <= d[2] <= 0x03 + + +def starts_like_dtls_record(d: bytes) -> bool: + """ + Returns: + True, if the passed bytes could be the start of a DTLS record + False, otherwise. + """ + # TLS ClientHello magic, works for DTLS 1.1, DTLS 1.2, and DTLS 1.3. + # https://www.rfc-editor.org/rfc/rfc4347#section-4.1 + # https://www.rfc-editor.org/rfc/rfc6347#section-4.1 + # https://www.rfc-editor.org/rfc/rfc9147#section-4-6.2 + # We assume that a client sending less than 3 bytes initially is not a DTLS client. + return len(d) > 2 and d[0] == 0x16 and d[1] == 0xFE and 0xFD <= d[2] <= 0xFE diff --git a/mitmproxy/net/udp.py b/mitmproxy/net/udp.py deleted file mode 100644 index abec86a589..0000000000 --- a/mitmproxy/net/udp.py +++ /dev/null @@ -1,276 +0,0 @@ -from __future__ import annotations - -import asyncio -import logging -import socket -from typing import Any, Callable, Optional, Union, cast - -from mitmproxy.connection import Address -from mitmproxy.net import udp_wireguard -from mitmproxy.utils import human - -logger = logging.getLogger(__name__) - -MAX_DATAGRAM_SIZE = 65535 - 20 - -DatagramReceivedCallback = Callable[ - [asyncio.DatagramTransport, bytes, Address, Address], None -] -""" -Callable that gets invoked when a datagram is received. -The first argument is the outgoing transport. -The second argument is the received payload. -The third argument is the source address, also referred to as `remote_addr` or `peername`. -The fourth argument is the destination address, also referred to as `local_addr` or `sockname`. -""" - -# to make mypy happy -SockAddress = Union[tuple[str, int], tuple[str, int, int, int]] - - -class DrainableDatagramProtocol(asyncio.DatagramProtocol): - _loop: asyncio.AbstractEventLoop - _closed: asyncio.Event - _paused: int - _can_write: asyncio.Event - _sock: socket.socket | None - - def __init__(self, loop: asyncio.AbstractEventLoop | None) -> None: - self._loop = asyncio.get_running_loop() if loop is None else loop - self._closed = asyncio.Event() - self._paused = 0 - self._can_write = asyncio.Event() - self._can_write.set() - self._sock = None - - def __repr__(self) -> str: - return f"<{self.__class__.__name__} socket={self._sock!r}>" - - @property - def sockets(self) -> tuple[socket.socket, ...]: - return () if self._sock is None else (self._sock,) - - def connection_made(self, transport: asyncio.BaseTransport) -> None: - self._sock = transport.get_extra_info("socket") - - def connection_lost(self, exc: Exception | None) -> None: - self._closed.set() - if exc: - logger.warning(f"Connection lost on {self!r}: {exc!r}") # pragma: no cover - - def pause_writing(self) -> None: - self._paused = self._paused + 1 - if self._paused == 1: - self._can_write.clear() - - def resume_writing(self) -> None: - assert self._paused > 0 - self._paused = self._paused - 1 - if self._paused == 0: - self._can_write.set() - - async def drain(self) -> None: - await self._can_write.wait() - - def error_received(self, exc: Exception) -> None: - logger.warning(f"Send/receive on {self!r} failed: {exc!r}") # pragma: no cover - - async def wait_closed(self) -> None: - await self._closed.wait() - - -class UdpServer(DrainableDatagramProtocol): - """UDP server similar to base_events.Server""" - - # _datagram_received_cb: DatagramReceivedCallback - _transport: asyncio.DatagramTransport | None - _local_addr: Address | None - - def __init__( - self, - datagram_received_cb: DatagramReceivedCallback, - loop: asyncio.AbstractEventLoop | None, - ) -> None: - super().__init__(loop) - self._datagram_received_cb = datagram_received_cb - self._transport = None - self._local_addr = None - - def connection_made(self, transport: asyncio.BaseTransport) -> None: - if self._transport is None: - self._transport = cast(asyncio.DatagramTransport, transport) - self._transport.set_protocol(self) - self._local_addr = transport.get_extra_info("sockname") - super().connection_made(transport) - - def datagram_received(self, data: bytes, addr: Any) -> None: - assert self._transport is not None - assert self._local_addr is not None - self._datagram_received_cb(self._transport, data, addr, self._local_addr) - - def close(self) -> None: - if self._transport is not None: - self._transport.close() - - -class DatagramReader: - _packets: asyncio.Queue - _eof: bool - - def __init__(self) -> None: - self._packets = asyncio.Queue(42) # ~2.75MB - self._eof = False - - def feed_data(self, data: bytes, remote_addr: Address) -> None: - assert len(data) <= MAX_DATAGRAM_SIZE - if self._eof: - logger.info( - f"Received UDP packet from {human.format_address(remote_addr)} after EOF." - ) - else: - try: - self._packets.put_nowait(data) - except asyncio.QueueFull: - logger.debug( - f"Dropped UDP packet from {human.format_address(remote_addr)}." - ) - - def feed_eof(self) -> None: - self._eof = True - try: - self._packets.put_nowait(b"") - except asyncio.QueueFull: - pass - - async def read(self, n: int) -> bytes: - assert n >= MAX_DATAGRAM_SIZE - if self._eof: - try: - return self._packets.get_nowait() - except asyncio.QueueEmpty: - return b"" - else: - try: - return await self._packets.get() - except RuntimeError: # pragma: no cover - # event loop got closed - return b"" - - -class DatagramWriter: - _transport: asyncio.DatagramTransport - _remote_addr: Address - _reader: DatagramReader | None - _closed: asyncio.Event | None - - def __init__( - self, - transport: asyncio.DatagramTransport, - remote_addr: Address, - reader: DatagramReader | None = None, - ) -> None: - """ - Create a new datagram writer around the given transport. - Specify a reader to prevent closing the transport and instead only feed EOF to the reader. - """ - self._transport = transport - self._remote_addr = remote_addr - if reader is not None: - self._reader = reader - self._closed = asyncio.Event() - else: - self._reader = None - self._closed = None - - @property - def _protocol(self) -> DrainableDatagramProtocol | udp_wireguard.WireGuardDatagramTransport: - return self._transport.get_protocol() # type: ignore - - def write(self, data: bytes) -> None: - self._transport.sendto(data, self._remote_addr) - - def write_eof(self) -> None: - raise OSError("UDP does not support half-closing.") - - def get_extra_info(self, name: str, default: Any = None) -> Any: - if name == "peername": - return self._remote_addr - else: - return self._transport.get_extra_info(name, default) - - def close(self) -> None: - if self._closed is None: - self._transport.close() - else: - self._closed.set() - assert self._reader - self._reader.feed_eof() - - def is_closing(self) -> bool: - if self._closed is None: - return self._transport.is_closing() - else: - return self._closed.is_set() - - async def wait_closed(self) -> None: - if self._closed is None: - await self._protocol.wait_closed() - else: - await self._closed.wait() - - async def drain(self) -> None: - await self._protocol.drain() - - -class UdpClient(DrainableDatagramProtocol): - """UDP protocol for upstream connections.""" - - _reader: DatagramReader - - def __init__(self, reader: DatagramReader, loop: asyncio.AbstractEventLoop | None): - super().__init__(loop) - self._reader = reader - - def datagram_received(self, data: bytes, remote_addr: Address) -> None: - self._reader.feed_data(data, remote_addr) - - def connection_lost(self, exc: Exception | None) -> None: - self._reader.feed_eof() - super().connection_lost(exc) - - -async def start_server( - datagram_received_cb: DatagramReceivedCallback, - host: str, - port: int, -) -> UdpServer: - """UDP variant of asyncio.start_server.""" - - if host == "": - # binding to an empty string does not work on Windows or Ubuntu. - host = "0.0.0.0" - - loop = asyncio.get_running_loop() - _, protocol = await loop.create_datagram_endpoint( - lambda: UdpServer(datagram_received_cb, loop), - local_addr=(host, port), - ) - assert isinstance(protocol, UdpServer) - return protocol - - -async def open_connection( - host: str, port: int, *, local_addr: Optional[Address] = None -) -> tuple[DatagramReader, DatagramWriter]: - """UDP variant of asyncio.open_connection.""" - - loop = asyncio.get_running_loop() - reader = DatagramReader() - transport, _ = await loop.create_datagram_endpoint( - lambda: UdpClient(reader, loop), local_addr=local_addr, remote_addr=(host, port) - ) - writer = DatagramWriter( - cast(asyncio.DatagramTransport, transport), - remote_addr=transport.get_extra_info("peername"), - ) - return reader, writer diff --git a/mitmproxy/net/udp_wireguard.py b/mitmproxy/net/udp_wireguard.py deleted file mode 100644 index b857ec76ca..0000000000 --- a/mitmproxy/net/udp_wireguard.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -This module contains a mock DatagramTransport for use with mitmproxy-wireguard. -""" -import asyncio -from typing import Any - -import mitmproxy_wireguard as wg - -from mitmproxy.connection import Address - - -class WireGuardDatagramTransport(asyncio.DatagramTransport): - def __init__(self, server: wg.Server, local_addr: Address, remote_addr: Address): - self._server: wg.Server = server - self._local_addr: Address = local_addr - self._remote_addr: Address = remote_addr - super().__init__() - - def sendto(self, data, addr=None): - self._server.send_datagram(data, self._local_addr, addr or self._remote_addr) - - def get_extra_info(self, name: str, default: Any = None) -> Any: - if name == "sockname": - return self._server.getsockname() - else: - raise NotImplementedError - - def get_protocol(self): - return self - - async def drain(self) -> None: - pass - - async def wait_closed(self) -> None: - pass diff --git a/mitmproxy/options.py b/mitmproxy/options.py index bf21823ab7..5dedba484a 100644 --- a/mitmproxy/options.py +++ b/mitmproxy/options.py @@ -21,6 +21,16 @@ def __init__(self, **kwargs) -> None: False, "Use the Host header to construct URLs for display.", ) + self.add_option( + "show_ignored_hosts", + bool, + False, + """ + Record ignored flows in the UI even if we do not perform TLS interception. + This option will keep ignored flows' contents in memory, which can greatly increase memory usage. + A future release will fix this issue, record ignored flows by default, and remove this option. + """, + ) # Proxy options self.add_option( @@ -62,18 +72,6 @@ def __init__(self, **kwargs) -> None: process list. Specify it in config.yaml to avoid this. """, ) - self.add_option( - "ciphers_client", - Optional[str], - None, - "Set supported ciphers for client <-> mitmproxy connections using OpenSSL syntax.", - ) - self.add_option( - "ciphers_server", - Optional[str], - None, - "Set supported ciphers for mitmproxy <-> server connections using OpenSSL syntax.", - ) self.add_option( "client_certs", Optional[str], None, "Client certificate file or directory." ) @@ -90,11 +88,19 @@ def __init__(self, **kwargs) -> None: """, ) self.add_option("allow_hosts", Sequence[str], [], "Opposite of --ignore-hosts.") - self.add_option("listen_host", str, "", - "Address to bind proxy server(s) to (may be overridden for individual modes, see `mode`).") - self.add_option("listen_port", Optional[int], None, - "Port to bind proxy server(s) to (may be overridden for individual modes, see `mode`). " - "By default, the port is mode-specific. The default regular HTTP proxy spawns on port 8080.") + self.add_option( + "listen_host", + str, + "", + "Address to bind proxy server(s) to (may be overridden for individual modes, see `mode`).", + ) + self.add_option( + "listen_port", + Optional[int], + None, + "Port to bind proxy server(s) to (may be overridden for individual modes, see `mode`). " + "By default, the port is mode-specific. The default regular HTTP proxy spawns on port 8080.", + ) self.add_option( "mode", Sequence[str], @@ -103,8 +109,9 @@ def __init__(self, **kwargs) -> None: The proxy server type(s) to spawn. Can be passed multiple times. Mitmproxy supports "regular" (HTTP), "transparent", "socks5", "reverse:SPEC", - and "upstream:SPEC" proxy servers. For reverse and upstream proxy modes, SPEC - is host specification in the form of "http[s]://host[:port]". + "upstream:SPEC", and "wireguard[:PATH]" proxy servers. For reverse and upstream proxy modes, SPEC + is host specification in the form of "http[s]://host[:port]". For WireGuard mode, PATH may point to + a file containing key material. If no such file exists, it will be created on startup. You may append `@listen_port` or `@listen_host:listen_port` to override `listen_host` or `listen_port` for a specific proxy mode. Features such as client playback will use the first mode to determine @@ -122,7 +129,7 @@ def __init__(self, **kwargs) -> None: "http2", bool, True, - "Enable/disable HTTP/2 support. " "HTTP/2 support is enabled by default.", + "Enable/disable HTTP/2 support. HTTP/2 support is enabled by default.", ) self.add_option( "http2_ping_keepalive", @@ -134,6 +141,18 @@ def __init__(self, **kwargs) -> None: Set to 0 to disable this feature. """, ) + self.add_option( + "http3", + bool, + True, + "Enable/disable support for QUIC and HTTP/3. Enabled by default.", + ) + self.add_option( + "http_connect_send_host_header", + bool, + True, + "Include host header with CONNECT requests. Enabled by default.", + ) self.add_option( "websocket", bool, @@ -148,13 +167,6 @@ def __init__(self, **kwargs) -> None: "Enable/disable raw TCP connections. " "TCP connections are enabled by default. ", ) - self.add_option( - "rawudp", - bool, - True, - "Enable/disable raw UDP connections. " - "UDP connections are enabled by default. ", - ) self.add_option( "ssl_insecure", bool, diff --git a/mitmproxy/optmanager.py b/mitmproxy/optmanager.py index 033819be7c..c77b3945fe 100644 --- a/mitmproxy/optmanager.py +++ b/mitmproxy/optmanager.py @@ -1,18 +1,24 @@ from __future__ import annotations + import contextlib import copy -import weakref -from collections.abc import Callable, Iterable, Sequence -from dataclasses import dataclass -import os import pprint import textwrap -from typing import Any, Optional, TextIO, Union +import weakref +from collections.abc import Callable +from collections.abc import Iterable +from collections.abc import Sequence +from dataclasses import dataclass +from pathlib import Path +from typing import Any +from typing import Optional +from typing import TextIO import ruamel.yaml from mitmproxy import exceptions -from mitmproxy.utils import signals, typecheck +from mitmproxy.utils import signals +from mitmproxy.utils import typecheck """ The base implementation for Options. @@ -27,10 +33,10 @@ class _Option: def __init__( self, name: str, - typespec: Union[type, object], # object for Optional[x], which is not a type. + typespec: type | object, # object for Optional[x], which is not a type. default: Any, help: str, - choices: Optional[Sequence[str]], + choices: Sequence[str] | None, ) -> None: typecheck.check_option_type(name, default, typespec) self.name = name @@ -103,7 +109,7 @@ class OptManager: mutation doesn't change the option state inadvertently. """ - def __init__(self): + def __init__(self) -> None: self.deferred: dict[str, Any] = {} self.changed = signals.SyncSignal(_sig_changed_spec) self.changed.connect(self._notify_subscribers) @@ -116,10 +122,10 @@ def __init__(self): def add_option( self, name: str, - typespec: Union[type, object], + typespec: type | object, default: Any, help: str, - choices: Optional[Sequence[str]] = None, + choices: Sequence[str] | None = None, ) -> None: self._options[name] = _Option(name, typespec, default, help, choices) self.changed.send(updated={name}) @@ -150,13 +156,11 @@ def subscribe(self, func, opts): if i not in self._options: raise exceptions.OptionsError("No such option: %s" % i) - self._subscriptions.append( - (signals.make_weak_ref(func), set(opts)) - ) + self._subscriptions.append((signals.make_weak_ref(func), set(opts))) def _notify_subscribers(self, updated) -> None: cleanup = False - for (ref, opts) in self._subscriptions: + for ref, opts in self._subscriptions: callback = ref() if callback is not None: if opts & updated: @@ -263,7 +267,7 @@ def toggler(self, attr): if attr not in self._options: raise KeyError("No such option: %s" % attr) o = self._options[attr] - if o.typespec != bool: + if o.typespec is not bool: raise ValueError("Toggler can only be used with boolean options") def toggle(): @@ -368,14 +372,14 @@ def _parse_setval(self, o: _Option, values: list[str]) -> Any: f"Received multiple values for {o.name}: {values}" ) - optstr: Optional[str] + optstr: str | None if values: optstr = values[0] else: optstr = None if o.typespec in (str, Optional[str]): - if o.typespec == str and optstr is None: + if o.typespec is str and optstr is None: raise exceptions.OptionsError(f"Option is required: {o.name}") return optstr elif o.typespec in (int, Optional[int]): @@ -384,11 +388,11 @@ def _parse_setval(self, o: _Option, values: list[str]) -> Any: return int(optstr) except ValueError: raise exceptions.OptionsError(f"Not an integer: {optstr}") - elif o.typespec == int: + elif o.typespec is int: raise exceptions.OptionsError(f"Option is required: {o.name}") else: return None - elif o.typespec == bool: + elif o.typespec is bool: if optstr == "toggle": return not o.current() if not optstr or optstr == "true": @@ -411,16 +415,16 @@ def make_parser(self, parser, optname, metavar=None, short=None): o = self._options[optname] - def mkf(l, s): - l = l.replace("_", "-") - f = ["--%s" % l] + def mkf(x, s): + x = x.replace("_", "-") + f = ["--%s" % x] if s: f.append("-" + s) return f flags = mkf(optname, short) - if o.typespec == bool: + if o.typespec is bool: g = parser.add_mutually_exclusive_group(required=False) onf = mkf(optname, None) offf = mkf("no-" + optname, None) @@ -519,14 +523,14 @@ def parse(text): if not text: return {} try: - yaml = ruamel.yaml.YAML(typ="unsafe", pure=True) + yaml = ruamel.yaml.YAML(typ="safe", pure=True) data = yaml.load(text) except ruamel.yaml.error.YAMLError as v: if hasattr(v, "problem_mark"): snip = v.problem_mark.get_snippet() raise exceptions.OptionsError( "Config error at line %s:\n%s\n%s" - % (v.problem_mark.line + 1, snip, v.problem) + % (v.problem_mark.line + 1, snip, getattr(v, "problem", "")) ) else: raise exceptions.OptionsError("Could not parse options.") @@ -537,31 +541,38 @@ def parse(text): return data -def load(opts: OptManager, text: str) -> None: +def load(opts: OptManager, text: str, cwd: Path | str | None = None) -> None: """ Load configuration from text, over-writing options already set in this object. May raise OptionsError if the config file is invalid. """ data = parse(text) + + scripts = data.get("scripts") + if scripts is not None and cwd is not None: + data["scripts"] = [ + str(relative_path(Path(path), relative_to=Path(cwd))) for path in scripts + ] + opts.update_defer(**data) -def load_paths(opts: OptManager, *paths: str) -> None: +def load_paths(opts: OptManager, *paths: Path | str) -> None: """ Load paths in order. Each path takes precedence over the previous path. Paths that don't exist are ignored, errors raise an OptionsError. """ for p in paths: - p = os.path.expanduser(p) - if os.path.exists(p) and os.path.isfile(p): - with open(p, encoding="utf8") as f: + p = Path(p).expanduser() + if p.exists() and p.is_file(): + with p.open(encoding="utf8") as f: try: txt = f.read() except UnicodeDecodeError as e: raise exceptions.OptionsError(f"Error reading {p}: {e}") try: - load(opts, txt) + load(opts, txt, cwd=p.absolute().parent) except exceptions.OptionsError as e: raise exceptions.OptionsError(f"Error reading {p}: {e}") @@ -590,15 +601,15 @@ def serialize( ruamel.yaml.YAML().dump(data, file) -def save(opts: OptManager, path: str, defaults: bool = False) -> None: +def save(opts: OptManager, path: Path | str, defaults: bool = False) -> None: """ Save to path. If the destination file exists, modify it in-place. Raises OptionsError if the existing data is corrupt. """ - path = os.path.expanduser(path) - if os.path.exists(path) and os.path.isfile(path): - with open(path, encoding="utf8") as f: + path = Path(path).expanduser() + if path.exists() and path.is_file(): + with path.open(encoding="utf8") as f: try: data = f.read() except UnicodeDecodeError as e: @@ -606,5 +617,17 @@ def save(opts: OptManager, path: str, defaults: bool = False) -> None: else: data = "" - with open(path, "wt", encoding="utf8") as f: + with path.open("w", encoding="utf8") as f: serialize(opts, f, data, defaults) + + +def relative_path(script_path: Path | str, *, relative_to: Path | str) -> Path: + """ + Make relative paths found in config files relative to said config file, + instead of relative to where the command is ran. + """ + script_path = Path(script_path) + # Edge case when $HOME is not an absolute path + if script_path.expanduser() != script_path and not script_path.is_absolute(): + script_path = script_path.expanduser().absolute() + return (relative_to / script_path.expanduser()).absolute() diff --git a/mitmproxy/platform/__init__.py b/mitmproxy/platform/__init__.py index e6fdcd7c8d..8f3660b18c 100644 --- a/mitmproxy/platform/__init__.py +++ b/mitmproxy/platform/__init__.py @@ -1,7 +1,7 @@ import re import socket import sys -from typing import Callable, Optional +from collections.abc import Callable def init_transparent_mode() -> None: @@ -10,7 +10,7 @@ def init_transparent_mode() -> None: """ -original_addr: Optional[Callable[[socket.socket], tuple[str, int]]] +original_addr: Callable[[socket.socket], tuple[str, int]] | None """ Get the original destination for the given socket. This function will be None if transparent mode is not supported. diff --git a/mitmproxy/platform/windows.py b/mitmproxy/platform/windows.py index 0e0515bd51..2f34a29cd3 100644 --- a/mitmproxy/platform/windows.py +++ b/mitmproxy/platform/windows.py @@ -1,10 +1,10 @@ -import collections +from __future__ import annotations + import collections.abc import contextlib -import ctypes import ctypes.wintypes -import io import json +import logging import os import re import socket @@ -12,35 +12,41 @@ import threading import time from collections.abc import Callable -from typing import Any, ClassVar, Optional +from io import BufferedIOBase +from typing import Any +from typing import cast +from typing import ClassVar -import pydivert import pydivert.consts -from mitmproxy.net.local_ip import get_local_ip, get_local_ip6 +from mitmproxy.net.local_ip import get_local_ip +from mitmproxy.net.local_ip import get_local_ip6 REDIRECT_API_HOST = "127.0.0.1" REDIRECT_API_PORT = 8085 +logger = logging.getLogger(__name__) + + ########################## # Resolver -def read(rfile: io.BufferedReader) -> Any: +def read(rfile: BufferedIOBase) -> Any: x = rfile.readline().strip() if not x: return None return json.loads(x) -def write(data, wfile: io.BufferedWriter) -> None: +def write(data, wfile: BufferedIOBase) -> None: wfile.write(json.dumps(data).encode() + b"\n") wfile.flush() class Resolver: - sock: socket.socket + sock: socket.socket | None lock: threading.RLock def __init__(self): @@ -84,7 +90,9 @@ class APIRequestHandler(socketserver.StreamRequestHandler): for each received pickled client address, port tuple. """ - def handle(self): + server: APIServer + + def handle(self) -> None: proxifier: TransparentProxy = self.server.proxifier try: pid: int = read(self.rfile) @@ -96,7 +104,9 @@ def handle(self): if c is None: return try: - server = proxifier.client_server_map[tuple(c)] + server = proxifier.client_server_map[ + cast(tuple[str, int], tuple(c)) + ] except KeyError: server = None write(server, self.wfile) @@ -125,6 +135,7 @@ def __init__(self, proxifier, *args, **kwargs): # IPv6 # + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa366896(v=vs.85).aspx class MIB_TCP6ROW_OWNER_PID(ctypes.Structure): _fields_ = [ @@ -154,6 +165,7 @@ class _MIB_TCP6TABLE_OWNER_PID(ctypes.Structure): # IPv4 # + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa366913(v=vs.85).aspx class MIB_TCPROW_OWNER_PID(ctypes.Structure): _fields_ = [ @@ -205,7 +217,7 @@ def refresh(self): self._refresh_ipv6() def _refresh_ipv4(self): - ret = ctypes.windll.iphlpapi.GetExtendedTcpTable( + ret = ctypes.windll.iphlpapi.GetExtendedTcpTable( # type: ignore ctypes.byref(self._tcp), ctypes.byref(self._tcp_size), False, @@ -228,7 +240,7 @@ def _refresh_ipv4(self): ) def _refresh_ipv6(self): - ret = ctypes.windll.iphlpapi.GetExtendedTcpTable( + ret = ctypes.windll.iphlpapi.GetExtendedTcpTable( # type: ignore ctypes.byref(self._tcp6), ctypes.byref(self._tcp6_size), False, @@ -275,7 +287,7 @@ def run(self): try: packet = self.windivert.recv() except OSError as e: - if e.winerror == 995: + if getattr(e, "winerror", None) == 995: return else: raise @@ -285,7 +297,7 @@ def run(self): def shutdown(self): self.windivert.close() - def recv(self) -> Optional[pydivert.Packet]: + def recv(self) -> pydivert.Packet | None: """ Convenience function that receives a packet from the passed handler and handles error codes. If the process has been shut down, None is returned. @@ -393,9 +405,9 @@ class TransparentProxy: which mitmproxy sees, but this would remove the correct client info from mitmproxy. """ - local: Optional[RedirectLocal] = None + local: RedirectLocal | None = None # really weird linting error here. - forward: Optional[Redirect] = None # noqa + forward: Redirect | None = None response: Redirect icmp: Redirect @@ -409,7 +421,7 @@ def __init__( local: bool = True, forward: bool = True, proxy_port: int = 8080, - filter: Optional[str] = "tcp.DstPort == 80 or tcp.DstPort == 443", + filter: str | None = "tcp.DstPort == 80 or tcp.DstPort == 443", ) -> None: self.proxy_port = proxy_port self.filter = ( @@ -452,6 +464,10 @@ def __init__( def setup(cls): # TODO: Make sure that server can be killed cleanly. That's a bit difficult as we don't have access to # controller.should_exit when this is called. + logger.warning( + "Transparent mode on Windows is unsupported, flaky, and deprecated. " + "Consider using local redirect mode or WireGuard mode instead." + ) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_unavailable = s.connect_ex((REDIRECT_API_HOST, REDIRECT_API_PORT)) if server_unavailable: diff --git a/mitmproxy/proxy/__init__.py b/mitmproxy/proxy/__init__.py index acb219868e..a38937d805 100644 --- a/mitmproxy/proxy/__init__.py +++ b/mitmproxy/proxy/__init__.py @@ -6,12 +6,12 @@ - Layers: represent protocol layers, e.g. one for TCP, TLS, and so on. Layers are nested, so a typical configuration might be ReverseProxy/TLS/TCP. Most importantly, layers are implemented using the sans-io pattern (https://sans-io.readthedocs.io/). - This means that calls return immediately, their is no blocking sync or async code. - - Server: the proxy server handles all I/O. This is implemented using asyncio, but could be done any other way. - The ConnectionHandler is subclassed in the Proxyserver addon, which handles the communication with the + This means that calls return immediately, there is no blocking sync or async code. + - Server: the proxy server handles all I/O. This is implemented using `asyncio`, but could be done any other way. + The `ConnectionHandler` is subclassed in the `Proxyserver` addon, which handles the communication with the rest of mitmproxy. - Events: When I/O actions occur at the proxy server, they are passed to the outermost layer as events, - e.g. "DataReceived" or "ConnectionClosed". + e.g. `DataReceived` or `ConnectionClosed`. - Commands: In the other direction, layers can emit commands to higher layers or the proxy server. This is used to e.g. send data, request for new connections to be opened, or to call mitmproxy's event hooks. diff --git a/mitmproxy/proxy/commands.py b/mitmproxy/proxy/commands.py index d4173ebc9b..17e09248f7 100644 --- a/mitmproxy/proxy/commands.py +++ b/mitmproxy/proxy/commands.py @@ -6,12 +6,15 @@ The counterpart to commands are events. """ + import logging import warnings -from typing import Union, TYPE_CHECKING +from typing import TYPE_CHECKING +from typing import Union import mitmproxy.hooks -from mitmproxy.connection import Connection, Server +from mitmproxy.connection import Connection +from mitmproxy.connection import Server if TYPE_CHECKING: import mitmproxy.proxy.layer @@ -76,7 +79,7 @@ def __init__(self, connection: Connection, data: bytes): def __repr__(self): target = str(self.connection).split("(", 1)[0].lower() - return f"SendData({target}, {self.data})" + return f"SendData({target}, {self.data!r})" class OpenConnection(ConnectionCommand): @@ -94,6 +97,8 @@ class CloseConnection(ConnectionCommand): all other connections will ultimately be closed during cleanup. """ + +class CloseTcpConnection(CloseConnection): half_close: bool """ If True, only close our half of the connection by sending a FIN packet. @@ -131,6 +136,7 @@ class Log(Command): This could also be implemented with some more playbook magic in the future, but for now we keep the current approach as the fully sans-io one. """ + message: str level: int @@ -142,7 +148,8 @@ def __init__( if isinstance(level, str): # pragma: no cover warnings.warn( "commands.Log() now expects an integer log level, not a string.", - DeprecationWarning, stacklevel=2 + DeprecationWarning, + stacklevel=2, ) level = getattr(logging, level.upper()) self.message = message diff --git a/mitmproxy/proxy/context.py b/mitmproxy/proxy/context.py index 5edb977c25..29987418fc 100644 --- a/mitmproxy/proxy/context.py +++ b/mitmproxy/proxy/context.py @@ -38,7 +38,7 @@ def __init__( self.client = client self.options = options self.server = connection.Server( - None, transport_protocol=client.transport_protocol + address=None, transport_protocol=client.transport_protocol ) self.layers = [] diff --git a/mitmproxy/proxy/events.py b/mitmproxy/proxy/events.py index b571f3ad2f..0ed3e3cb7d 100644 --- a/mitmproxy/proxy/events.py +++ b/mitmproxy/proxy/events.py @@ -3,13 +3,18 @@ Events represent the only way for layers to receive new data from sockets. The counterpart to events are commands. """ + +import typing import warnings -from dataclasses import dataclass, is_dataclass -from typing import Any, Generic, Optional, TypeVar +from dataclasses import dataclass +from dataclasses import is_dataclass +from typing import Any +from typing import Generic +from typing import TypeVar from mitmproxy import flow -from mitmproxy.proxy import commands from mitmproxy.connection import Connection +from mitmproxy.proxy import commands class Event: @@ -47,7 +52,7 @@ class DataReceived(ConnectionEvent): def __repr__(self): target = type(self.connection).__name__.lower() - return f"DataReceived({target}, {self.data})" + return f"DataReceived({target}, {self.data!r})" class ConnectionClosed(ConnectionEvent): @@ -72,7 +77,7 @@ def __new__(cls, *args, **kwargs): return super().__new__(cls) def __init_subclass__(cls, **kwargs): - command_cls = cls.__annotations__.get("command", None) + command_cls = typing.get_type_hints(cls).get("command", None) valid_command_subclass = ( isinstance(command_cls, type) and issubclass(command_cls, commands.Command) @@ -80,7 +85,7 @@ def __init_subclass__(cls, **kwargs): ) if not valid_command_subclass: warnings.warn( - f"{command_cls} needs a properly annotated command attribute.", + f"{cls} needs a properly annotated command attribute.", RuntimeWarning, ) if command_cls in command_reply_subclasses: @@ -101,7 +106,7 @@ def __repr__(self): @dataclass(repr=False) class OpenConnectionCompleted(CommandCompleted): command: commands.OpenConnection - reply: Optional[str] + reply: str | None """error message""" diff --git a/mitmproxy/proxy/layer.py b/mitmproxy/proxy/layer.py index 2b2868fb51..14180fff38 100644 --- a/mitmproxy/proxy/layer.py +++ b/mitmproxy/proxy/layer.py @@ -1,16 +1,24 @@ """ Base class for protocol layers. """ + import collections import textwrap from abc import abstractmethod +from collections.abc import Callable +from collections.abc import Generator from dataclasses import dataclass from logging import DEBUG -from typing import Any, ClassVar, Generator, NamedTuple, Optional, TypeVar +from typing import Any +from typing import ClassVar +from typing import NamedTuple +from typing import TypeVar from mitmproxy.connection import Connection -from mitmproxy.proxy import commands, events -from mitmproxy.proxy.commands import Command, StartHook +from mitmproxy.proxy import commands +from mitmproxy.proxy import events +from mitmproxy.proxy.commands import Command +from mitmproxy.proxy.commands import StartHook from mitmproxy.proxy.context import Context T = TypeVar("T") @@ -20,6 +28,10 @@ """ +MAX_LOG_STATEMENT_SIZE = 2048 +"""Maximum size of individual log statements before they will be truncated.""" + + class Paused(NamedTuple): """ State of a layer that's paused because it is waiting for a command reply. @@ -52,7 +64,7 @@ def _handle_event(self, event): __last_debug_message: ClassVar[str] = "" context: Context - _paused: Optional[Paused] + _paused: Paused | None """ If execution is currently paused, this attribute stores the paused coroutine and the command for which we are expecting a reply. @@ -62,7 +74,7 @@ def _handle_event(self, event): All events that have occurred since execution was paused. These will be replayed to ._child_layer once we resume. """ - debug: Optional[str] = None + debug: str | None = None """ Enable debug logging by assigning a prefix string for log messages. Different amounts of whitespace for different layers work well. @@ -90,14 +102,15 @@ def __repr__(self): def __debug(self, message): """yield a Log command indicating what message is passing through this layer.""" - if len(message) > 512: - message = message[:512] + "…" + if len(message) > MAX_LOG_STATEMENT_SIZE: + message = message[:MAX_LOG_STATEMENT_SIZE] + "…" if Layer.__last_debug_message == message: message = message.split("\n", 1)[0].strip() if len(message) > 256: message = message[:256] + "…" else: Layer.__last_debug_message = message + assert self.debug is not None return commands.Log(textwrap.indent(message, self.debug), DEBUG) @property @@ -233,7 +246,7 @@ def __continue(self, event: events.CommandCompleted): class NextLayer(Layer): - layer: Optional[Layer] + layer: Layer | None """The next layer. To be set by an addon.""" events: list[mevents.Event] @@ -247,7 +260,7 @@ def __init__(self, context: Context, ask_on_start: bool = False) -> None: self.layer = None self.events = [] self._ask_on_start = ask_on_start - self._handle = None + self._handle: Callable[[mevents.Event], CommandGenerator[None]] | None = None def __repr__(self): return f"NextLayer:{repr(self.layer)}" @@ -296,8 +309,8 @@ def _ask(self): # 2. This layer is not needed anymore, so we directly reassign .handle_event. # 3. Some layers may however still have a reference to the old .handle_event. # ._handle is just an optimization to reduce the callstack in these cases. - self.handle_event = self.layer.handle_event - self._handle_event = self.layer.handle_event + self.handle_event = self.layer.handle_event # type: ignore + self._handle_event = self.layer.handle_event # type: ignore self._handle = self.layer.handle_event # Utility methods for whoever decides what the next layer is going to be. diff --git a/mitmproxy/proxy/layers/__init__.py b/mitmproxy/proxy/layers/__init__.py index 71143e1cf7..e21ba60e08 100644 --- a/mitmproxy/proxy/layers/__init__.py +++ b/mitmproxy/proxy/layers/__init__.py @@ -1,18 +1,27 @@ from . import modes from .dns import DNSLayer from .http import HttpLayer +from .quic import ClientQuicLayer +from .quic import QuicStreamLayer +from .quic import RawQuicLayer +from .quic import ServerQuicLayer from .tcp import TCPLayer +from .tls import ClientTLSLayer +from .tls import ServerTLSLayer from .udp import UDPLayer -from .tls import ClientTLSLayer, ServerTLSLayer from .websocket import WebsocketLayer __all__ = [ "modes", "DNSLayer", "HttpLayer", + "QuicStreamLayer", + "RawQuicLayer", "TCPLayer", "UDPLayer", + "ClientQuicLayer", "ClientTLSLayer", + "ServerQuicLayer", "ServerTLSLayer", "WebsocketLayer", ] diff --git a/mitmproxy/proxy/layers/dns.py b/mitmproxy/proxy/layers/dns.py index 0b85ad05ae..238b0feff1 100644 --- a/mitmproxy/proxy/layers/dns.py +++ b/mitmproxy/proxy/layers/dns.py @@ -1,11 +1,19 @@ -from dataclasses import dataclass import struct - -from mitmproxy import dns, flow as mflow -from mitmproxy.proxy import commands, events, layer +from dataclasses import dataclass +from typing import List +from typing import Literal + +from mitmproxy import dns +from mitmproxy import flow as mflow +from mitmproxy.net.dns import response_codes +from mitmproxy.proxy import commands +from mitmproxy.proxy import events +from mitmproxy.proxy import layer from mitmproxy.proxy.context import Context from mitmproxy.proxy.utils import expect +_LENGTH_LABEL = struct.Struct("!H") + @dataclass class DnsRequestHook(commands.StartHook): @@ -34,24 +42,44 @@ class DnsErrorHook(commands.StartHook): flow: dns.DNSFlow +def pack_message( + message: dns.Message, transport_protocol: Literal["tcp", "udp"] +) -> bytes: + packed = message.packed + if transport_protocol == "tcp": + return struct.pack("!H", len(packed)) + packed + else: + return packed + + class DNSLayer(layer.Layer): """ Layer that handles resolving DNS queries. """ flows: dict[int, dns.DNSFlow] + req_buf: bytearray + resp_buf: bytearray def __init__(self, context: Context): super().__init__(context) self.flows = {} + self.req_buf = bytearray() + self.resp_buf = bytearray() - def handle_request(self, flow: dns.DNSFlow, msg: dns.Message) -> layer.CommandGenerator[None]: + def handle_request( + self, flow: dns.DNSFlow, msg: dns.Message + ) -> layer.CommandGenerator[None]: flow.request = msg # if already set, continue and query upstream again yield DnsRequestHook(flow) if flow.response: yield from self.handle_response(flow, flow.response) + elif flow.error: + yield from self.handle_error(flow, flow.error.msg) elif not self.context.server.address: - yield from self.handle_error(flow, "No hook has set a response and there is no upstream server.") + yield from self.handle_error( + flow, "No hook has set a response and there is no upstream server." + ) else: if not self.context.server.connected: err = yield commands.OpenConnection(self.context.server) @@ -59,17 +87,57 @@ def handle_request(self, flow: dns.DNSFlow, msg: dns.Message) -> layer.CommandGe yield from self.handle_error(flow, str(err)) # cannot recover from this return - yield commands.SendData(self.context.server, flow.request.packed) + packed = pack_message(flow.request, flow.server_conn.transport_protocol) + yield commands.SendData(self.context.server, packed) - def handle_response(self, flow: dns.DNSFlow, msg: dns.Message) -> layer.CommandGenerator[None]: + def handle_response( + self, flow: dns.DNSFlow, msg: dns.Message + ) -> layer.CommandGenerator[None]: flow.response = msg yield DnsResponseHook(flow) if flow.response: - yield commands.SendData(self.context.client, flow.response.packed) + packed = pack_message(flow.response, flow.client_conn.transport_protocol) + yield commands.SendData(self.context.client, packed) def handle_error(self, flow: dns.DNSFlow, err: str) -> layer.CommandGenerator[None]: flow.error = mflow.Error(err) yield DnsErrorHook(flow) + servfail = flow.request.fail(response_codes.SERVFAIL) + yield commands.SendData( + self.context.client, + pack_message(servfail, flow.client_conn.transport_protocol), + ) + + def unpack_message(self, data: bytes, from_client: bool) -> List[dns.Message]: + msgs: List[dns.Message] = [] + + buf = self.req_buf if from_client else self.resp_buf + + if self.context.client.transport_protocol == "udp": + msgs.append(dns.Message.unpack(data)) + elif self.context.client.transport_protocol == "tcp": + buf.extend(data) + size = len(buf) + offset = 0 + + while True: + if size - offset < _LENGTH_LABEL.size: + break + (expected_size,) = _LENGTH_LABEL.unpack_from(buf, offset) + offset += _LENGTH_LABEL.size + if expected_size == 0: + raise struct.error("Message length field cannot be zero") + + if size - offset < expected_size: + offset -= _LENGTH_LABEL.size + break + + data = bytes(buf[offset : expected_size + offset]) + offset += expected_size + msgs.append(dns.Message.unpack(data)) + + del buf[:offset] + return msgs @expect(events.Start) def state_start(self, _) -> layer.CommandGenerator[None]: @@ -82,22 +150,26 @@ def state_query(self, event: events.Event) -> layer.CommandGenerator[None]: from_client = event.connection is self.context.client if isinstance(event, events.DataReceived): + msgs: List[dns.Message] = [] try: - msg = dns.Message.unpack(event.data) + msgs = self.unpack_message(event.data, from_client) except struct.error as e: yield commands.Log(f"{event.connection} sent an invalid message: {e}") yield commands.CloseConnection(event.connection) self._handle_event = self.state_done else: - try: - flow = self.flows[msg.id] - except KeyError: - flow = dns.DNSFlow(self.context.client, self.context.server, live=True) - self.flows[msg.id] = flow - if from_client: - yield from self.handle_request(flow, msg) - else: - yield from self.handle_response(flow, msg) + for msg in msgs: + try: + flow = self.flows[msg.id] + except KeyError: + flow = dns.DNSFlow( + self.context.client, self.context.server, live=True + ) + self.flows[msg.id] = flow + if from_client: + yield from self.handle_request(flow, msg) + else: + yield from self.handle_response(flow, msg) elif isinstance(event, events.ConnectionClosed): other_conn = self.context.server if from_client else self.context.client diff --git a/mitmproxy/proxy/layers/http/__init__.py b/mitmproxy/proxy/layers/http/__init__.py index 7cb84771bc..a9172ed579 100644 --- a/mitmproxy/proxy/layers/http/__init__.py +++ b/mitmproxy/proxy/layers/http/__init__.py @@ -1,51 +1,70 @@ import collections import enum -from logging import DEBUG, WARNING - import time from dataclasses import dataclass from functools import cached_property -from typing import Optional, Union +from logging import DEBUG +from logging import WARNING import wsproto.handshake -from mitmproxy import flow, http -from mitmproxy.connection import Connection, Server + +from ...context import Context +from ...mode_specs import ReverseMode +from ...mode_specs import UpstreamMode +from ..quic import QuicStreamEvent +from ._base import HttpCommand +from ._base import HttpConnection +from ._base import ReceiveHttp +from ._base import StreamId +from ._events import HttpEvent +from ._events import RequestData +from ._events import RequestEndOfMessage +from ._events import RequestHeaders +from ._events import RequestProtocolError +from ._events import RequestTrailers +from ._events import ResponseData +from ._events import ResponseEndOfMessage +from ._events import ResponseHeaders +from ._events import ResponseProtocolError +from ._events import ResponseTrailers +from ._hooks import HttpConnectedHook +from ._hooks import HttpConnectErrorHook +from ._hooks import HttpConnectHook +from ._hooks import HttpErrorHook +from ._hooks import HttpRequestHeadersHook +from ._hooks import HttpRequestHook +from ._hooks import HttpResponseHeadersHook +from ._hooks import HttpResponseHook +from ._http1 import Http1Client +from ._http1 import Http1Connection +from ._http1 import Http1Server +from ._http2 import Http2Client +from ._http2 import Http2Server +from ._http3 import Http3Client +from ._http3 import Http3Server +from mitmproxy import flow +from mitmproxy import http +from mitmproxy.connection import Connection +from mitmproxy.connection import Server +from mitmproxy.connection import TransportProtocol from mitmproxy.net import server_spec -from mitmproxy.net.http import status_codes, url +from mitmproxy.net.http import status_codes +from mitmproxy.net.http import url from mitmproxy.net.http.http1 import expected_http_body_size -from mitmproxy.proxy import commands, events, layer, tunnel -from mitmproxy.proxy.layers import tcp, tls, websocket +from mitmproxy.net.http.validate import validate_headers +from mitmproxy.proxy import commands +from mitmproxy.proxy import events +from mitmproxy.proxy import layer +from mitmproxy.proxy import tunnel +from mitmproxy.proxy.layers import quic +from mitmproxy.proxy.layers import tcp +from mitmproxy.proxy.layers import tls +from mitmproxy.proxy.layers import websocket from mitmproxy.proxy.layers.http import _upstream_proxy from mitmproxy.proxy.utils import expect +from mitmproxy.proxy.utils import ReceiveBuffer from mitmproxy.utils import human from mitmproxy.websocket import WebSocketData -from ._base import HttpCommand, HttpConnection, ReceiveHttp, StreamId -from ._events import ( - HttpEvent, - RequestData, - RequestEndOfMessage, - RequestHeaders, - RequestProtocolError, - RequestTrailers, - ResponseData, - ResponseEndOfMessage, - ResponseHeaders, - ResponseProtocolError, - ResponseTrailers, -) -from ._hooks import ( # noqa - HttpConnectHook, - HttpConnectUpstreamHook, - HttpErrorHook, - HttpRequestHeadersHook, - HttpRequestHook, - HttpResponseHeadersHook, - HttpResponseHook, -) -from ._http1 import Http1Client, Http1Connection, Http1Server -from ._http2 import Http2Client, Http2Server -from ...context import Context -from ...mode_specs import ReverseMode, UpstreamMode class HTTPMode(enum.Enum): @@ -54,7 +73,9 @@ class HTTPMode(enum.Enum): upstream = 3 -def validate_request(mode: HTTPMode, request: http.Request) -> Optional[str]: +def validate_request( + mode: HTTPMode, request: http.Request, validate_inbound_headers: bool +) -> str | None: if request.scheme not in ("http", "https", ""): return f"Invalid request scheme: {request.scheme}" if mode is HTTPMode.transparent and request.method == "CONNECT": @@ -62,9 +83,21 @@ def validate_request(mode: HTTPMode, request: http.Request) -> Optional[str]: f"mitmproxy received an HTTP CONNECT request even though it is not running in regular/upstream mode. " f"This usually indicates a misconfiguration, please see the mitmproxy mode documentation for details." ) + if validate_inbound_headers: + try: + validate_headers(request) + except ValueError as e: + return ( + f"Received {e} from client, refusing to prevent request smuggling attacks. " + "Disable the validate_inbound_headers option to skip this security check." + ) return None +def is_h3_alpn(alpn: bytes | None) -> bool: + return alpn == b"h3" or (alpn is not None and alpn.startswith(b"h3-")) + + @dataclass class GetHttpConnection(HttpCommand): """ @@ -74,7 +107,8 @@ class GetHttpConnection(HttpCommand): blocking = True address: tuple[str, int] tls: bool - via: Optional[server_spec.ServerSpec] + via: server_spec.ServerSpec | None + transport_protocol: TransportProtocol = "tcp" def __hash__(self): return id(self) @@ -85,13 +119,14 @@ def connection_spec_matches(self, connection: Connection) -> bool: and self.address == connection.address and self.tls == connection.tls and self.via == connection.via + and self.transport_protocol == connection.transport_protocol ) @dataclass class GetHttpConnectionCompleted(events.CommandCompleted): command: GetHttpConnection - reply: Union[tuple[None, str], tuple[Connection, None]] + reply: tuple[None, str] | tuple[Connection, None] """connection object, error message""" @@ -102,7 +137,7 @@ class RegisterHttpConnection(HttpCommand): """ connection: Connection - err: Optional[str] + err: str | None @dataclass @@ -122,22 +157,23 @@ class DropStream(HttpCommand): class HttpStream(layer.Layer): - request_body_buf: bytes - response_body_buf: bytes + request_body_buf: ReceiveBuffer + response_body_buf: ReceiveBuffer flow: http.HTTPFlow stream_id: StreamId - child_layer: Optional[layer.Layer] = None + child_layer: layer.Layer | None = None @cached_property - def mode(self): + def mode(self) -> HTTPMode: i = self.context.layers.index(self) - parent: HttpLayer = self.context.layers[i - 1] + parent = self.context.layers[i - 1] + assert isinstance(parent, HttpLayer) return parent.mode - def __init__(self, context: Context, stream_id: int): + def __init__(self, context: Context, stream_id: int) -> None: super().__init__(context) - self.request_body_buf = b"" - self.response_body_buf = b"" + self.request_body_buf = ReceiveBuffer() + self.response_body_buf = ReceiveBuffer() self.client_state = self.state_uninitialized self.server_state = self.state_uninitialized self.stream_id = stream_id @@ -179,10 +215,8 @@ def state_wait_for_request_headers( self.flow.request = event.request self.flow.live = True - if err := validate_request(self.mode, self.flow.request): - self.flow.response = http.Response.make(502, str(err)) - self.client_state = self.state_errored - return (yield from self.send_response()) + if (yield from self.check_invalid(True)): + return if self.flow.request.method == "CONNECT": return (yield from self.handle_connect()) @@ -219,7 +253,9 @@ def state_wait_for_request_headers( "https" if self.context.client.tls else "http" ) - if self.mode is HTTPMode.regular and not self.flow.request.is_http2: + if self.mode is HTTPMode.regular and not ( + self.flow.request.is_http2 or self.flow.request.is_http3 + ): # Set the request target to origin-form for HTTP/1, some servers don't support absolute-form requests. # see https://github.com/mitmproxy/mitmproxy/issues/1759 self.flow.request.authority = "" @@ -251,7 +287,7 @@ def state_wait_for_request_headers( ) self.flow.request.headers.pop("expect") - if self.flow.request.stream: + if self.flow.request.stream and not event.end_stream: yield from self.start_request_stream() else: self.client_state = self.state_consume_request_body @@ -275,7 +311,7 @@ def start_request_stream(self) -> layer.CommandGenerator[None]: @expect(RequestData, RequestTrailers, RequestEndOfMessage) def state_stream_request_body( - self, event: Union[RequestData, RequestEndOfMessage] + self, event: RequestData | RequestEndOfMessage ) -> layer.CommandGenerator[None]: if isinstance(event, RequestData): if callable(self.flow.request.stream): @@ -328,8 +364,8 @@ def state_consume_request_body( self.flow.request.trailers = event.trailers elif isinstance(event, RequestEndOfMessage): self.flow.request.timestamp_end = time.time() - self.flow.request.data.content = self.request_body_buf - self.request_body_buf = b"" + self.flow.request.data.content = bytes(self.request_body_buf) + self.request_body_buf.clear() self.client_state = self.state_done yield HttpRequestHook(self.flow) if (yield from self.check_killed(True)): @@ -374,12 +410,14 @@ def state_wait_for_response_headers( if not event.end_stream and (yield from self.check_body_size(False)): return + if (yield from self.check_invalid(False)): + return yield HttpResponseHeadersHook(self.flow) if (yield from self.check_killed(True)): return - elif self.flow.response.stream: + elif self.flow.response.stream and not event.end_stream: yield from self.start_response_stream() else: self.server_state = self.state_consume_response_body @@ -435,8 +473,8 @@ def state_consume_response_body( self.flow.response.trailers = event.trailers elif isinstance(event, ResponseEndOfMessage): assert self.flow.response - self.flow.response.data.content = self.response_body_buf - self.response_body_buf = b"" + self.flow.response.data.content = bytes(self.response_body_buf) + self.response_body_buf.clear() yield from self.send_response() def send_response(self, already_streamed: bool = False): @@ -482,10 +520,11 @@ def send_response(self, already_streamed: bool = False): if self.client_state == self.state_done: yield from self.flow_done() - def flow_done(self): + def flow_done(self) -> layer.CommandGenerator[None]: if not self.flow.websocket: self.flow.live = False + assert self.flow.response if self.flow.response.status_code == 101: if self.flow.websocket: self.child_layer = websocket.WebsocketLayer(self.context, self.flow) @@ -503,8 +542,8 @@ def flow_done(self): yield commands.Log( f"{self.debug}[http] upgrading to {self.child_layer}", DEBUG ) - yield from self.child_layer.handle_event(events.Start()) self._handle_event = self.passthrough + yield from self.child_layer.handle_event(events.Start()) else: yield DropStream(self.stream_id) @@ -527,7 +566,7 @@ def check_body_size(self, request: bool) -> layer.CommandGenerator[bool]: # Step 1: Determine the expected body size. This can either come from a known content-length header, # or from the amount of currently buffered bytes (e.g. for chunked encoding). response = not request - expected_size: Optional[int] + expected_size: int | None # the 'late' case: we already started consuming the body if request and self.request_body_buf: expected_size = len(self.request_body_buf) @@ -580,20 +619,62 @@ def check_body_size(self, request: bool) -> layer.CommandGenerator[bool]: self.flow.request.stream = True if self.request_body_buf: # clear buffer and then fake a DataReceived event with everything we had in the buffer so far. - body_buf = self.request_body_buf - self.request_body_buf = b"" + body_buf = bytes(self.request_body_buf) + self.request_body_buf.clear() yield from self.start_request_stream() yield from self.handle_event(RequestData(self.stream_id, body_buf)) if response: assert self.flow.response self.flow.response.stream = True if self.response_body_buf: - body_buf = self.response_body_buf - self.response_body_buf = b"" + body_buf = bytes(self.response_body_buf) + self.response_body_buf.clear() yield from self.start_response_stream() yield from self.handle_event(ResponseData(self.stream_id, body_buf)) return False + def check_invalid(self, request: bool) -> layer.CommandGenerator[bool]: + err: str | None = None + if request: + err = validate_request( + self.mode, + self.flow.request, + self.context.options.validate_inbound_headers, + ) + elif self.context.options.validate_inbound_headers: + assert self.flow.response is not None + try: + validate_headers(self.flow.response) + except ValueError as e: + err = ( + f"Received {e} from server, refusing to prevent request smuggling attacks. " + "Disable the validate_inbound_headers option to skip this security check." + ) + + if err: + self.flow.error = flow.Error(err) + + if request: + # flow has not been seen yet, register it. + yield HttpRequestHeadersHook(self.flow) + else: + # immediately kill of server connection + yield commands.CloseConnection(self.flow.server_conn) + yield HttpErrorHook(self.flow) + yield SendHttp( + ResponseProtocolError( + self.stream_id, + err, + status_codes.BAD_REQUEST if request else status_codes.BAD_GATEWAY, + ), + self.context.client, + ) + self.flow.live = False + self.client_state = self.server_state = self.state_errored + return True + else: + return False + def check_killed(self, emit_error_hook: bool) -> layer.CommandGenerator[bool]: killed_by_us = ( self.flow.error and self.flow.error.msg == flow.Error.KILLED_MESSAGE @@ -625,7 +706,7 @@ def check_killed(self, emit_error_hook: bool) -> layer.CommandGenerator[bool]: return False def handle_protocol_error( - self, event: Union[RequestProtocolError, ResponseProtocolError] + self, event: RequestProtocolError | ResponseProtocolError ) -> layer.CommandGenerator[None]: is_client_error_but_we_already_talk_upstream = ( isinstance(event, RequestProtocolError) @@ -663,6 +744,7 @@ def make_server_connection(self) -> layer.CommandGenerator[bool]: (self.flow.request.host, self.flow.request.port), self.flow.request.scheme == "https", self.flow.server_conn.via, + self.flow.server_conn.transport_protocol, ) if err: yield from self.handle_protocol_error( @@ -674,6 +756,7 @@ def make_server_connection(self) -> layer.CommandGenerator[bool]: return True def handle_connect(self) -> layer.CommandGenerator[None]: + self.client_state = self.state_done yield HttpConnectHook(self.flow) if (yield from self.check_killed(False)): return @@ -696,7 +779,7 @@ def handle_connect_regular(self): 502, f"Cannot connect to {human.format_address(self.context.server.address)}: {err} " f"If you plan to redirect requests away from this server, " - f"consider setting `connection_strategy` to `lazy` to suppress early connections." + f"consider setting `connection_strategy` to `lazy` to suppress early connections.", ) self.child_layer = layer.NextLayer(self.context) yield from self.handle_connect_finish() @@ -721,16 +804,30 @@ def handle_connect_finish(self): ) if 200 <= self.flow.response.status_code < 300: + yield HttpConnectedHook(self.flow) self.child_layer = self.child_layer or layer.NextLayer(self.context) - yield from self.child_layer.handle_event(events.Start()) self._handle_event = self.passthrough + yield from self.child_layer.handle_event(events.Start()) + else: + yield HttpConnectErrorHook(self.flow) + self.client_state = self.state_errored + self.flow.live = False + + content = self.flow.response.raw_content + done_after_headers = not (content or self.flow.response.trailers) + yield SendHttp( + ResponseHeaders(self.stream_id, self.flow.response, done_after_headers), + self.context.client, + ) + if content: + yield SendHttp(ResponseData(self.stream_id, content), self.context.client) + + if self.flow.response.trailers: yield SendHttp( - ResponseHeaders(self.stream_id, self.flow.response, True), + ResponseTrailers(self.stream_id, self.flow.response.trailers), self.context.client, ) - yield SendHttp(ResponseEndOfMessage(self.stream_id), self.context.client) - else: - yield from self.send_response() + yield SendHttp(ResponseEndOfMessage(self.stream_id), self.context.client) @expect(RequestData, RequestEndOfMessage, events.Event) def passthrough(self, event: events.Event) -> layer.CommandGenerator[None]: @@ -782,7 +879,8 @@ def passthrough(self, event: events.Event) -> layer.CommandGenerator[None]: # The easiest approach for this is to just always full close for now. # Alternatively, we could signal that we want a half close only through ResponseProtocolError, # but that is more complex to implement. - command.half_close = False + if isinstance(command, commands.CloseTcpConnection): + command = commands.CloseConnection(command.connection) yield command else: yield command @@ -825,28 +923,28 @@ def __init__(self, context: Context, mode: HTTPMode): self.waiting_for_establishment = collections.defaultdict(list) self.streams = {} self.command_sources = {} - - http_conn: HttpConnection - if self.context.client.alpn == b"h2": - http_conn = Http2Server(context.fork()) - else: - http_conn = Http1Server(context.fork()) - - self.connections = {context.client: http_conn} + self.connections = {} def __repr__(self): return f"HttpLayer({self.mode.name}, conns: {len(self.connections)})" def _handle_event(self, event: events.Event): if isinstance(event, events.Start): + http_conn: HttpConnection + if is_h3_alpn(self.context.client.alpn): + http_conn = Http3Server(self.context.fork()) + elif self.context.client.alpn == b"h2": + http_conn = Http2Server(self.context.fork()) + else: + http_conn = Http1Server(self.context.fork()) + + # may have been set by client playback. + self.connections.setdefault(self.context.client, http_conn) yield from self.event_to_child(self.connections[self.context.client], event) if self.mode is HTTPMode.upstream: proxy_mode = self.context.client.proxy_mode assert isinstance(proxy_mode, UpstreamMode) self.context.server.via = (proxy_mode.scheme, proxy_mode.address) - elif isinstance(event, events.Wakeup): - stream = self.command_sources.pop(event.command) - yield from self.event_to_child(stream, event) elif isinstance(event, events.CommandCompleted): stream = self.command_sources.pop(event.command) yield from self.event_to_child(stream, event) @@ -879,10 +977,13 @@ def _handle_event(self, event: events.Event): if isinstance(event, events.ConnectionClosed): # The peer has closed it - let's close it too! yield commands.CloseConnection(event.connection) - elif isinstance(event, events.DataReceived): - # The peer has sent data. This can happen with HTTP/2 servers that already send a settings frame. + elif isinstance(event, (events.DataReceived, QuicStreamEvent)): + # The peer has sent data or another connection activity occurred. + # This can happen with HTTP/2 servers that already send a settings frame. child_layer: HttpConnection - if self.context.server.alpn == b"h2": + if is_h3_alpn(self.context.server.alpn): + child_layer = Http3Client(self.context.fork()) + elif self.context.server.alpn == b"h2": child_layer = Http2Client(self.context.fork()) else: child_layer = Http1Client(self.context.fork()) @@ -899,7 +1000,7 @@ def _handle_event(self, event: events.Event): def event_to_child( self, - child: Union[layer.Layer, HttpStream], + child: layer.Layer | HttpStream, event: events.Event, ) -> layer.CommandGenerator[None]: for command in child.handle_event(event): @@ -998,8 +1099,9 @@ def get_connection( stack = tunnel.LayerStack() if not can_use_context_connection: - - context.server = Server(event.address) + context.server = Server( + address=event.address, transport_protocol=event.transport_protocol + ) if event.via: context.server.via = event.via @@ -1013,10 +1115,22 @@ def get_connection( self.mode == HTTPMode.transparent and event.address == self.context.server.address ): - context.server.sni = self.context.client.sni or event.address[0] + # reverse proxy mode may set self.context.server.sni, which takes precedence. + context.server.sni = ( + self.context.server.sni + or self.context.client.sni + or event.address[0] + ) else: context.server.sni = event.address[0] - stack /= tls.ServerTLSLayer(context) + if context.server.transport_protocol == "tcp": + stack /= tls.ServerTLSLayer(context) + elif context.server.transport_protocol == "udp": + stack /= quic.ServerQuicLayer(context) + else: + raise AssertionError( + context.server.transport_protocol + ) # pragma: no cover stack /= HttpClient(context) @@ -1030,7 +1144,7 @@ def register_connection( ) -> layer.CommandGenerator[None]: waiting = self.waiting_for_establishment.pop(command.connection) - reply: Union[tuple[None, str], tuple[Connection, None]] + reply: tuple[None, str] | tuple[Connection, None] if command.err: reply = (None, command.err) else: @@ -1060,13 +1174,15 @@ class HttpClient(layer.Layer): @expect(events.Start) def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: - err: Optional[str] + err: str | None if self.context.server.connected: err = None else: err = yield commands.OpenConnection(self.context.server) if not err: - if self.context.server.alpn == b"h2": + if is_h3_alpn(self.context.server.alpn): + self.child_layer = Http3Client(self.context) + elif self.context.server.alpn == b"h2": self.child_layer = Http2Client(self.context) else: self.child_layer = Http1Client(self.context) diff --git a/mitmproxy/proxy/layers/http/_base.py b/mitmproxy/proxy/layers/http/_base.py index b5f66d46ba..198fa77fb2 100644 --- a/mitmproxy/proxy/layers/http/_base.py +++ b/mitmproxy/proxy/layers/http/_base.py @@ -4,7 +4,9 @@ from mitmproxy import http from mitmproxy.connection import Connection -from mitmproxy.proxy import commands, events, layer +from mitmproxy.proxy import commands +from mitmproxy.proxy import events +from mitmproxy.proxy import layer from mitmproxy.proxy.context import Context StreamId = int diff --git a/mitmproxy/proxy/layers/http/_events.py b/mitmproxy/proxy/layers/http/_events.py index f67217b03b..d977135ff2 100644 --- a/mitmproxy/proxy/layers/http/_events.py +++ b/mitmproxy/proxy/layers/http/_events.py @@ -1,9 +1,8 @@ from dataclasses import dataclass -from typing import Optional +from ._base import HttpEvent from mitmproxy import http from mitmproxy.http import HTTPFlow -from ._base import HttpEvent @dataclass @@ -15,7 +14,7 @@ class RequestHeaders(HttpEvent): us to set END_STREAM on headers already (and some servers - Akamai - implicitly expect that). In either case, this event will nonetheless be followed by RequestEndOfMessage. """ - replay_flow: Optional[HTTPFlow] = None + replay_flow: HTTPFlow | None = None """If set, the current request headers belong to a replayed flow, which should be reused.""" diff --git a/mitmproxy/proxy/layers/http/_hooks.py b/mitmproxy/proxy/layers/http/_hooks.py index 949c9a7e7d..501491cb3f 100644 --- a/mitmproxy/proxy/layers/http/_hooks.py +++ b/mitmproxy/proxy/layers/http/_hooks.py @@ -96,3 +96,27 @@ class HttpConnectUpstreamHook(commands.StartHook): """ flow: http.HTTPFlow + + +@dataclass +class HttpConnectedHook(commands.StartHook): + """ + HTTP CONNECT was successful + + > [!WARNING] + > This may fire before an upstream connection has been established + > if `connection_strategy` is set to `lazy` (default) + """ + + flow: http.HTTPFlow + + +@dataclass +class HttpConnectErrorHook(commands.StartHook): + """ + HTTP CONNECT has failed. + This can happen when the upstream server is unreachable or proxy authentication is required. + In contrast to the `error` hook, `flow.error` is not guaranteed to be set. + """ + + flow: http.HTTPFlow diff --git a/mitmproxy/proxy/layers/http/_http1.py b/mitmproxy/proxy/layers/http/_http1.py index cae1513101..75838d0b7d 100644 --- a/mitmproxy/proxy/layers/http/_http1.py +++ b/mitmproxy/proxy/layers/http/_http1.py @@ -1,48 +1,56 @@ import abc -from typing import Callable, Optional, Union +from collections.abc import Callable +from typing import Union import h11 -from h11._readers import ChunkedReader, ContentLengthReader, Http10Reader +from h11._readers import ChunkedReader +from h11._readers import ContentLengthReader +from h11._readers import Http10Reader from h11._receivebuffer import ReceiveBuffer -from mitmproxy import http, version -from mitmproxy.connection import Connection, ConnectionState -from mitmproxy.net.http import http1, status_codes -from mitmproxy.proxy import commands, events, layer -from mitmproxy.proxy.layers.http._base import ReceiveHttp, StreamId +from ...context import Context +from ._base import format_error +from ._base import HttpConnection +from ._events import HttpEvent +from ._events import RequestData +from ._events import RequestEndOfMessage +from ._events import RequestHeaders +from ._events import RequestProtocolError +from ._events import ResponseData +from ._events import ResponseEndOfMessage +from ._events import ResponseHeaders +from ._events import ResponseProtocolError +from mitmproxy import http +from mitmproxy import version +from mitmproxy.connection import Connection +from mitmproxy.connection import ConnectionState +from mitmproxy.net.http import http1 +from mitmproxy.net.http import status_codes +from mitmproxy.proxy import commands +from mitmproxy.proxy import events +from mitmproxy.proxy import layer +from mitmproxy.proxy.layers.http._base import ReceiveHttp +from mitmproxy.proxy.layers.http._base import StreamId from mitmproxy.proxy.utils import expect from mitmproxy.utils import human -from ._base import HttpConnection, format_error -from ._events import ( - HttpEvent, - RequestData, - RequestEndOfMessage, - RequestHeaders, - RequestProtocolError, - ResponseData, - ResponseEndOfMessage, - ResponseHeaders, - ResponseProtocolError, -) -from ...context import Context TBodyReader = Union[ChunkedReader, Http10Reader, ContentLengthReader] class Http1Connection(HttpConnection, metaclass=abc.ABCMeta): - stream_id: Optional[StreamId] = None - request: Optional[http.Request] = None - response: Optional[http.Response] = None + stream_id: StreamId | None = None + request: http.Request | None = None + response: http.Response | None = None request_done: bool = False response_done: bool = False # this is a bit of a hack to make both mypy and PyCharm happy. - state: Union[Callable[[events.Event], layer.CommandGenerator[None]], Callable] + state: Callable[[events.Event], layer.CommandGenerator[None]] | Callable body_reader: TBodyReader buf: ReceiveBuffer - ReceiveProtocolError: type[Union[RequestProtocolError, ResponseProtocolError]] - ReceiveData: type[Union[RequestData, ResponseData]] - ReceiveEndOfMessage: type[Union[RequestEndOfMessage, ResponseEndOfMessage]] + ReceiveProtocolError: type[RequestProtocolError | ResponseProtocolError] + ReceiveData: type[RequestData | ResponseData] + ReceiveEndOfMessage: type[RequestEndOfMessage | ResponseEndOfMessage] def __init__(self, context: Context, conn: Connection): super().__init__(context, conn) @@ -77,7 +85,7 @@ def start(self, _) -> layer.CommandGenerator[None]: state = start def read_body(self, event: events.Event) -> layer.CommandGenerator[None]: - assert self.stream_id + assert self.stream_id is not None while True: try: if isinstance(event, events.DataReceived): @@ -189,7 +197,10 @@ def mark_done( # If we proxy HTTP/2 to HTTP/1, we only use upstream connections for one request. # This simplifies our connection management quite a bit as we can rely on # the proxyserver's max-connection-per-server throttling. - or (self.request.is_http2 and isinstance(self, Http1Client)) + or ( + (self.request.is_http2 or self.request.is_http3) + and isinstance(self, Http1Client) + ) ) if connection_done: yield commands.CloseConnection(self.conn) @@ -223,7 +234,7 @@ def send(self, event: HttpEvent) -> layer.CommandGenerator[None]: if isinstance(event, ResponseHeaders): self.response = response = event.response - if response.is_http2: + if response.is_http2 or response.is_http3: response = response.copy() # Convert to an HTTP/1 response. response.http_version = "HTTP/1.1" @@ -245,16 +256,21 @@ def send(self, event: HttpEvent) -> layer.CommandGenerator[None]: elif isinstance(event, ResponseEndOfMessage): assert self.request assert self.response - if self.request.method.upper() != "HEAD" and "chunked" in self.response.headers.get("transfer-encoding", "").lower(): + if ( + self.request.method.upper() != "HEAD" + and "chunked" + in self.response.headers.get("transfer-encoding", "").lower() + ): yield commands.SendData(self.conn, b"0\r\n\r\n") yield from self.mark_done(response=True) elif isinstance(event, ResponseProtocolError): + if not (self.conn.state & ConnectionState.CAN_WRITE): + return if not self.response and event.code != status_codes.NO_RESPONSE: yield commands.SendData( self.conn, make_error_response(event.code, event.message) ) - if self.conn.state & ConnectionState.CAN_WRITE: - yield commands.CloseConnection(self.conn) + yield commands.CloseConnection(self.conn) else: raise AssertionError(f"Unexpected event: {event}") @@ -268,8 +284,6 @@ def read_headers( self.request = http1.read_request_head( [bytes(x) for x in request_head] ) - if self.context.options.validate_inbound_headers: - http1.validate_headers(self.request.headers) expected_body_size = http1.expected_http_body_size(self.request) except ValueError as e: yield commands.SendData(self.conn, make_error_response(400, str(e))) @@ -331,7 +345,7 @@ def send(self, event: HttpEvent) -> layer.CommandGenerator[None]: yield commands.CloseConnection(self.conn) return - if not self.stream_id: + if self.stream_id is None: assert isinstance(event, RequestHeaders) self.stream_id = event.stream_id self.request = event.request @@ -339,7 +353,7 @@ def send(self, event: HttpEvent) -> layer.CommandGenerator[None]: if isinstance(event, RequestHeaders): request = event.request - if request.is_http2: + if request.is_http2 or request.is_http3: # Convert to an HTTP/1 request. request = ( request.copy() @@ -369,7 +383,7 @@ def send(self, event: HttpEvent) -> layer.CommandGenerator[None]: if "chunked" in self.request.headers.get("transfer-encoding", "").lower(): yield commands.SendData(self.conn, b"0\r\n\r\n") elif http1.expected_http_body_size(self.request, self.response) == -1: - yield commands.CloseConnection(self.conn, half_close=True) + yield commands.CloseTcpConnection(self.conn, half_close=True) yield from self.mark_done(request=True) else: raise AssertionError(f"Unexpected event: {event}") @@ -383,7 +397,7 @@ def read_headers( yield commands.Log(f"Unexpected data from server: {bytes(self.buf)!r}") yield commands.CloseConnection(self.conn) return - assert self.stream_id + assert self.stream_id is not None response_head = self.buf.maybe_extract_lines() if response_head: @@ -391,8 +405,6 @@ def read_headers( self.response = http1.read_response_head( [bytes(x) for x in response_head] ) - if self.context.options.validate_inbound_headers: - http1.validate_headers(self.response.headers) expected_size = http1.expected_http_body_size( self.request, self.response ) @@ -448,7 +460,7 @@ def should_make_pipe(request: http.Request, response: http.Response) -> bool: return False -def make_body_reader(expected_size: Optional[int]) -> TBodyReader: +def make_body_reader(expected_size: int | None) -> TBodyReader: if expected_size is None: return ChunkedReader() elif expected_size == -1: diff --git a/mitmproxy/proxy/layers/http/_http2.py b/mitmproxy/proxy/layers/http/_http2.py index 540606fb16..b2fa851e2c 100644 --- a/mitmproxy/proxy/layers/http/_http2.py +++ b/mitmproxy/proxy/layers/http/_http2.py @@ -1,10 +1,10 @@ import collections -from logging import DEBUG, ERROR - import time from collections.abc import Sequence from enum import Enum -from typing import ClassVar, Optional, Union +from logging import DEBUG +from logging import ERROR +from typing import ClassVar import h2.config import h2.connection @@ -15,29 +15,40 @@ import h2.stream import h2.utilities -from mitmproxy import http, version -from mitmproxy.connection import Connection -from mitmproxy.net.http import status_codes, url -from mitmproxy.utils import human -from . import ( - RequestData, - RequestEndOfMessage, - RequestHeaders, - RequestProtocolError, - ResponseData, - ResponseEndOfMessage, - ResponseHeaders, - RequestTrailers, - ResponseTrailers, - ResponseProtocolError, -) -from ._base import HttpConnection, HttpEvent, ReceiveHttp, format_error -from ._http_h2 import BufferedH2Connection, H2ConnectionLogger -from ...commands import CloseConnection, Log, SendData, RequestWakeup +from ...commands import CloseConnection +from ...commands import Log +from ...commands import RequestWakeup +from ...commands import SendData from ...context import Context -from ...events import ConnectionClosed, DataReceived, Event, Start, Wakeup +from ...events import ConnectionClosed +from ...events import DataReceived +from ...events import Event +from ...events import Start +from ...events import Wakeup from ...layer import CommandGenerator from ...utils import expect +from . import RequestData +from . import RequestEndOfMessage +from . import RequestHeaders +from . import RequestProtocolError +from . import RequestTrailers +from . import ResponseData +from . import ResponseEndOfMessage +from . import ResponseHeaders +from . import ResponseProtocolError +from . import ResponseTrailers +from ._base import format_error +from ._base import HttpConnection +from ._base import HttpEvent +from ._base import ReceiveHttp +from ._http_h2 import BufferedH2Connection +from ._http_h2 import H2ConnectionLogger +from mitmproxy import http +from mitmproxy import version +from mitmproxy.connection import Connection +from mitmproxy.net.http import status_codes +from mitmproxy.net.http import url +from mitmproxy.utils import human class StreamState(Enum): @@ -61,17 +72,16 @@ class Http2Connection(HttpConnection): streams: dict[int, StreamState] """keep track of all active stream ids to send protocol errors on teardown""" - ReceiveProtocolError: type[Union[RequestProtocolError, ResponseProtocolError]] - ReceiveData: type[Union[RequestData, ResponseData]] - ReceiveTrailers: type[Union[RequestTrailers, ResponseTrailers]] - ReceiveEndOfMessage: type[Union[RequestEndOfMessage, ResponseEndOfMessage]] + ReceiveProtocolError: type[RequestProtocolError | ResponseProtocolError] + ReceiveData: type[RequestData | ResponseData] + ReceiveTrailers: type[RequestTrailers | ResponseTrailers] + ReceiveEndOfMessage: type[RequestEndOfMessage | ResponseEndOfMessage] def __init__(self, context: Context, conn: Connection): super().__init__(context, conn) if self.debug: self.h2_conf.logger = H2ConnectionLogger( - self.context.client.peername, - self.__class__.__name__ + self.context.client.peername, self.__class__.__name__ ) self.h2_conf.validate_inbound_headers = ( self.context.options.validate_inbound_headers @@ -263,6 +273,10 @@ def handle_h2_event(self, event: h2.events.Event) -> CommandGenerator[bool]: # https://http2.github.io/http2-spec/#rfc.section.4.1 # Implementations MUST ignore and discard any frame that has a type that is unknown. yield Log(f"Ignoring unknown HTTP/2 frame type: {event.frame.type}") + elif isinstance(event, h2.events.AlternativeServiceAvailable): + yield Log( + "Received HTTP/2 Alt-Svc frame, which will not be forwarded.", DEBUG + ) else: raise AssertionError(f"Unexpected event: {event!r}") return False @@ -313,6 +327,48 @@ def normalize_h2_headers(headers: list[tuple[bytes, bytes]]) -> CommandGenerator headers[i] = (headers[i][0].lower(), headers[i][1]) +def format_h2_request_headers( + context: Context, + event: RequestHeaders, +) -> CommandGenerator[list[tuple[bytes, bytes]]]: + pseudo_headers = [ + (b":method", event.request.data.method), + (b":scheme", event.request.data.scheme), + (b":path", event.request.data.path), + ] + if event.request.authority: + pseudo_headers.append((b":authority", event.request.data.authority)) + + if event.request.is_http2 or event.request.is_http3: + hdrs = list(event.request.headers.fields) + if context.options.normalize_outbound_headers: + yield from normalize_h2_headers(hdrs) + else: + headers = event.request.headers + if not event.request.authority and "host" in headers: + headers = headers.copy() + pseudo_headers.append((b":authority", headers.pop(b"host"))) + hdrs = normalize_h1_headers(list(headers.fields), True) + + return pseudo_headers + hdrs + + +def format_h2_response_headers( + context: Context, + event: ResponseHeaders, +) -> CommandGenerator[list[tuple[bytes, bytes]]]: + headers = [ + (b":status", b"%d" % event.response.status_code), + *event.response.headers.fields, + ] + if event.response.is_http2 or event.response.is_http3: + if context.options.normalize_outbound_headers: + yield from normalize_h2_headers(headers) + else: + headers = normalize_h1_headers(headers, False) + return headers + + class Http2Server(Http2Connection): h2_conf = h2.config.H2Configuration( **Http2Connection.h2_conf_defaults, @@ -330,19 +386,11 @@ def __init__(self, context: Context): def _handle_event(self, event: Event) -> CommandGenerator[None]: if isinstance(event, ResponseHeaders): if self.is_open_for_us(event.stream_id): - headers = [ - (b":status", b"%d" % event.response.status_code), - *event.response.headers.fields, - ] - if event.response.is_http2: - if self.context.options.normalize_outbound_headers: - yield from normalize_h2_headers(headers) - else: - headers = normalize_h1_headers(headers, False) - self.h2_conn.send_headers( event.stream_id, - headers, + headers=( + yield from format_h2_response_headers(self.context, event) + ), end_stream=event.end_stream, ) yield SendData(self.conn, self.h2_conn.data_to_send()) @@ -404,7 +452,7 @@ class Http2Client(Http2Connection): their_stream_id: dict[int, int] stream_queue: collections.defaultdict[int, list[Event]] """Queue of streams that we haven't sent yet because we have reached MAX_CONCURRENT_STREAMS""" - provisional_max_concurrency: Optional[int] = 10 + provisional_max_concurrency: int | None = 10 """A provisional currency limit before we get the server's first settings frame.""" last_activity: float """Timestamp of when we've last seen network activity on this connection.""" @@ -485,28 +533,9 @@ def _handle_event2(self, event: Event) -> CommandGenerator[None]: yield RequestWakeup(self.context.options.http2_ping_keepalive) yield from super()._handle_event(event) elif isinstance(event, RequestHeaders): - pseudo_headers = [ - (b":method", event.request.data.method), - (b":scheme", event.request.data.scheme), - (b":path", event.request.data.path), - ] - if event.request.authority: - pseudo_headers.append((b":authority", event.request.data.authority)) - - if event.request.is_http2: - hdrs = list(event.request.headers.fields) - if self.context.options.normalize_outbound_headers: - yield from normalize_h2_headers(hdrs) - else: - headers = event.request.headers - if not event.request.authority and "host" in headers: - headers = headers.copy() - pseudo_headers.append((b":authority", headers.pop(b"host"))) - hdrs = normalize_h1_headers(list(headers.fields), True) - self.h2_conn.send_headers( event.stream_id, - pseudo_headers + hdrs, + headers=(yield from format_h2_request_headers(self.context, event)), end_stream=event.end_stream, ) self.streams[event.stream_id] = StreamState.EXPECTING_HEADERS @@ -552,7 +581,7 @@ def handle_h2_event(self, event: h2.events.Event) -> CommandGenerator[bool]: # - 102 Processing is WebDAV only and also ignorable. # - 103 Early Hints is not mission-critical. headers = http.Headers(event.headers) - status: Union[str, int] = "" + status: str | int = "" try: status = int(headers[":status"]) reason = status_codes.RESPONSES.get(status, "") @@ -575,11 +604,11 @@ def handle_h2_event(self, event: h2.events.Event) -> CommandGenerator[bool]: def split_pseudo_headers( - h2_headers: Sequence[tuple[bytes, bytes]] + h2_headers: Sequence[tuple[bytes, bytes]], ) -> tuple[dict[bytes, bytes], http.Headers]: pseudo_headers: dict[bytes, bytes] = {} i = 0 - for (header, value) in h2_headers: + for header, value in h2_headers: if header.startswith(b":"): if header in pseudo_headers: raise ValueError(f"Duplicate HTTP/2 pseudo header: {header!r}") @@ -595,7 +624,7 @@ def split_pseudo_headers( def parse_h2_request_headers( - h2_headers: Sequence[tuple[bytes, bytes]] + h2_headers: Sequence[tuple[bytes, bytes]], ) -> tuple[str, int, bytes, bytes, bytes, bytes, http.Headers]: """Split HTTP/2 pseudo-headers from the actual headers and parse them.""" pseudo_headers, headers = split_pseudo_headers(h2_headers) @@ -625,7 +654,7 @@ def parse_h2_request_headers( def parse_h2_response_headers( - h2_headers: Sequence[tuple[bytes, bytes]] + h2_headers: Sequence[tuple[bytes, bytes]], ) -> tuple[int, http.Headers]: """Split HTTP/2 pseudo-headers from the actual headers and parse them.""" pseudo_headers, headers = split_pseudo_headers(h2_headers) @@ -642,6 +671,10 @@ def parse_h2_response_headers( __all__ = [ + "format_h2_request_headers", + "format_h2_response_headers", + "parse_h2_request_headers", + "parse_h2_response_headers", "Http2Client", "Http2Server", ] diff --git a/mitmproxy/proxy/layers/http/_http3.py b/mitmproxy/proxy/layers/http/_http3.py new file mode 100644 index 0000000000..268f1c6c7c --- /dev/null +++ b/mitmproxy/proxy/layers/http/_http3.py @@ -0,0 +1,282 @@ +import time +from abc import abstractmethod + +from aioquic.h3.connection import ErrorCode as H3ErrorCode +from aioquic.h3.connection import FrameUnexpected as H3FrameUnexpected +from aioquic.h3.events import DataReceived +from aioquic.h3.events import HeadersReceived +from aioquic.h3.events import PushPromiseReceived + +from . import RequestData +from . import RequestEndOfMessage +from . import RequestHeaders +from . import RequestProtocolError +from . import RequestTrailers +from . import ResponseData +from . import ResponseEndOfMessage +from . import ResponseHeaders +from . import ResponseProtocolError +from . import ResponseTrailers +from ._base import format_error +from ._base import HttpConnection +from ._base import HttpEvent +from ._base import ReceiveHttp +from ._http2 import format_h2_request_headers +from ._http2 import format_h2_response_headers +from ._http2 import parse_h2_request_headers +from ._http2 import parse_h2_response_headers +from ._http_h3 import LayeredH3Connection +from ._http_h3 import StreamClosed +from ._http_h3 import TrailersReceived +from mitmproxy import connection +from mitmproxy import http +from mitmproxy import version +from mitmproxy.net.http import status_codes +from mitmproxy.proxy import commands +from mitmproxy.proxy import context +from mitmproxy.proxy import events +from mitmproxy.proxy import layer +from mitmproxy.proxy.layers.quic import error_code_to_str +from mitmproxy.proxy.layers.quic import QuicConnectionClosed +from mitmproxy.proxy.layers.quic import QuicStreamEvent +from mitmproxy.proxy.utils import expect + + +class Http3Connection(HttpConnection): + h3_conn: LayeredH3Connection + + ReceiveData: type[RequestData | ResponseData] + ReceiveEndOfMessage: type[RequestEndOfMessage | ResponseEndOfMessage] + ReceiveProtocolError: type[RequestProtocolError | ResponseProtocolError] + ReceiveTrailers: type[RequestTrailers | ResponseTrailers] + + def __init__(self, context: context.Context, conn: connection.Connection): + super().__init__(context, conn) + self.h3_conn = LayeredH3Connection( + self.conn, is_client=self.conn is self.context.server + ) + + def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: + if isinstance(event, events.Start): + yield from self.h3_conn.transmit() + + # send mitmproxy HTTP events over the H3 connection + elif isinstance(event, HttpEvent): + try: + if isinstance(event, (RequestData, ResponseData)): + self.h3_conn.send_data(event.stream_id, event.data) + elif isinstance(event, (RequestHeaders, ResponseHeaders)): + headers = yield from ( + format_h2_request_headers(self.context, event) + if isinstance(event, RequestHeaders) + else format_h2_response_headers(self.context, event) + ) + self.h3_conn.send_headers( + event.stream_id, headers, end_stream=event.end_stream + ) + elif isinstance(event, (RequestTrailers, ResponseTrailers)): + self.h3_conn.send_trailers( + event.stream_id, [*event.trailers.fields] + ) + elif isinstance(event, (RequestEndOfMessage, ResponseEndOfMessage)): + self.h3_conn.end_stream(event.stream_id) + elif isinstance(event, (RequestProtocolError, ResponseProtocolError)): + send_error_message = ( + isinstance(event, ResponseProtocolError) + and not self.h3_conn.has_sent_headers(event.stream_id) + and event.code != status_codes.NO_RESPONSE + ) + if send_error_message: + self.h3_conn.send_headers( + event.stream_id, + [ + (b":status", b"%d" % event.code), + (b"server", version.MITMPROXY.encode()), + (b"content-type", b"text/html"), + ], + ) + self.h3_conn.send_data( + event.stream_id, + format_error(event.code, event.message), + end_stream=True, + ) + else: + if event.code == status_codes.CLIENT_CLOSED_REQUEST: + code = H3ErrorCode.H3_REQUEST_CANCELLED.value + else: + code = H3ErrorCode.H3_INTERNAL_ERROR.value + self.h3_conn.close_stream(event.stream_id, code) + else: # pragma: no cover + raise AssertionError(f"Unexpected event: {event!r}") + + except H3FrameUnexpected as e: + # Http2Connection also ignores HttpEvents that violate the current stream state + yield commands.Log(f"Received {event!r} unexpectedly: {e}") + + else: + # transmit buffered data + yield from self.h3_conn.transmit() + + # forward stream messages from the QUIC layer to the H3 connection + elif isinstance(event, QuicStreamEvent): + h3_events = self.h3_conn.handle_stream_event(event) + for h3_event in h3_events: + if isinstance(h3_event, StreamClosed): + err_str = error_code_to_str(h3_event.error_code) + if h3_event.error_code == H3ErrorCode.H3_REQUEST_CANCELLED: + code = status_codes.CLIENT_CLOSED_REQUEST + else: + code = self.ReceiveProtocolError.code + yield ReceiveHttp( + self.ReceiveProtocolError( + h3_event.stream_id, + f"stream closed by client ({err_str})", + code=code, + ) + ) + elif isinstance(h3_event, DataReceived): + if h3_event.data: + yield ReceiveHttp( + self.ReceiveData(h3_event.stream_id, h3_event.data) + ) + if h3_event.stream_ended: + yield ReceiveHttp(self.ReceiveEndOfMessage(h3_event.stream_id)) + elif isinstance(h3_event, HeadersReceived): + try: + receive_event = self.parse_headers(h3_event) + except ValueError as e: + self.h3_conn.close_connection( + error_code=H3ErrorCode.H3_GENERAL_PROTOCOL_ERROR, + reason_phrase=f"Invalid HTTP/3 request headers: {e}", + ) + else: + yield ReceiveHttp(receive_event) + if h3_event.stream_ended: + yield ReceiveHttp( + self.ReceiveEndOfMessage(h3_event.stream_id) + ) + elif isinstance(h3_event, TrailersReceived): + yield ReceiveHttp( + self.ReceiveTrailers( + h3_event.stream_id, http.Headers(h3_event.trailers) + ) + ) + if h3_event.stream_ended: + yield ReceiveHttp(self.ReceiveEndOfMessage(h3_event.stream_id)) + elif isinstance(h3_event, PushPromiseReceived): # pragma: no cover + self.h3_conn.close_connection( + error_code=H3ErrorCode.H3_GENERAL_PROTOCOL_ERROR, + reason_phrase=f"Received HTTP/3 push promise, even though we signalled no support.", + ) + else: # pragma: no cover + raise AssertionError(f"Unexpected event: {event!r}") + yield from self.h3_conn.transmit() + + # report a protocol error for all remaining open streams when a connection is closed + elif isinstance(event, QuicConnectionClosed): + self._handle_event = self.done # type: ignore + self.h3_conn.handle_connection_closed(event) + msg = event.reason_phrase or error_code_to_str(event.error_code) + for stream_id in self.h3_conn.get_open_stream_ids(): + yield ReceiveHttp(self.ReceiveProtocolError(stream_id, msg)) + + else: # pragma: no cover + raise AssertionError(f"Unexpected event: {event!r}") + + @expect(HttpEvent, QuicStreamEvent, QuicConnectionClosed) + def done(self, _) -> layer.CommandGenerator[None]: + yield from () + + @abstractmethod + def parse_headers(self, event: HeadersReceived) -> RequestHeaders | ResponseHeaders: + pass # pragma: no cover + + +class Http3Server(Http3Connection): + ReceiveData = RequestData + ReceiveEndOfMessage = RequestEndOfMessage + ReceiveProtocolError = RequestProtocolError + ReceiveTrailers = RequestTrailers + + def __init__(self, context: context.Context): + super().__init__(context, context.client) + + def parse_headers(self, event: HeadersReceived) -> RequestHeaders | ResponseHeaders: + # same as HTTP/2 + ( + host, + port, + method, + scheme, + authority, + path, + headers, + ) = parse_h2_request_headers(event.headers) + request = http.Request( + host=host, + port=port, + method=method, + scheme=scheme, + authority=authority, + path=path, + http_version=b"HTTP/3", + headers=headers, + content=None, + trailers=None, + timestamp_start=time.time(), + timestamp_end=None, + ) + return RequestHeaders(event.stream_id, request, end_stream=event.stream_ended) + + +class Http3Client(Http3Connection): + ReceiveData = ResponseData + ReceiveEndOfMessage = ResponseEndOfMessage + ReceiveProtocolError = ResponseProtocolError + ReceiveTrailers = ResponseTrailers + + our_stream_id: dict[int, int] + their_stream_id: dict[int, int] + + def __init__(self, context: context.Context): + super().__init__(context, context.server) + self.our_stream_id = {} + self.their_stream_id = {} + + def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: + # QUIC and HTTP/3 would actually allow for direct stream ID mapping, but since we want + # to support H2<->H3, we need to translate IDs. + # NOTE: We always create bidirectional streams, as we can't safely infer unidirectionality. + if isinstance(event, HttpEvent): + ours = self.our_stream_id.get(event.stream_id, None) + if ours is None: + ours = self.h3_conn.get_next_available_stream_id() + self.our_stream_id[event.stream_id] = ours + self.their_stream_id[ours] = event.stream_id + event.stream_id = ours + + for cmd in super()._handle_event(event): + if isinstance(cmd, ReceiveHttp): + cmd.event.stream_id = self.their_stream_id[cmd.event.stream_id] + yield cmd + + def parse_headers(self, event: HeadersReceived) -> RequestHeaders | ResponseHeaders: + # same as HTTP/2 + status_code, headers = parse_h2_response_headers(event.headers) + response = http.Response( + http_version=b"HTTP/3", + status_code=status_code, + reason=b"", + headers=headers, + content=None, + trailers=None, + timestamp_start=time.time(), + timestamp_end=None, + ) + return ResponseHeaders(event.stream_id, response, event.stream_ended) + + +__all__ = [ + "Http3Client", + "Http3Server", +] diff --git a/mitmproxy/proxy/layers/http/_http_h2.py b/mitmproxy/proxy/layers/http/_http_h2.py index 8533b7afbf..d554eeb83d 100644 --- a/mitmproxy/proxy/layers/http/_http_h2.py +++ b/mitmproxy/proxy/layers/http/_http_h2.py @@ -1,6 +1,6 @@ import collections import logging -from typing import Dict, List, NamedTuple, Tuple +from typing import NamedTuple import h2.config import h2.connection @@ -9,7 +9,6 @@ import h2.settings import h2.stream - logger = logging.getLogger(__name__) @@ -21,9 +20,7 @@ def __init__(self, peername: tuple, conn_type: str): def debug(self, fmtstr, *args): logger.debug( - f"{self.conn_type} {fmtstr}", - *args, - extra={"client": self.peername} + f"{self.conn_type} {fmtstr}", *args, extra={"client": self.peername} ) def trace(self, fmtstr, *args): @@ -31,7 +28,7 @@ def trace(self, fmtstr, *args): logging.DEBUG - 1, f"{self.conn_type} {fmtstr}", *args, - extra={"client": self.peername} + extra={"client": self.peername}, ) @@ -48,13 +45,26 @@ class BufferedH2Connection(h2.connection.H2Connection): """ stream_buffers: collections.defaultdict[int, collections.deque[SendH2Data]] - stream_trailers: Dict[int, List[Tuple[bytes, bytes]]] + stream_trailers: dict[int, list[tuple[bytes, bytes]]] def __init__(self, config: h2.config.H2Configuration): super().__init__(config) + self.local_settings.initial_window_size = 2**31 - 1 + self.local_settings.max_frame_size = 2**17 + self.max_inbound_frame_size = 2**17 + # hyper-h2 pitfall: we need to acknowledge here, otherwise its sends out the old settings. + self.local_settings.acknowledge() self.stream_buffers = collections.defaultdict(collections.deque) self.stream_trailers = {} + def initiate_connection(self): + super().initiate_connection() + # We increase the flow-control window for new streams with a setting, + # but we need to increase the overall connection flow-control window as well. + self.increment_flow_control_window( + 2**31 - 1 - self.inbound_flow_control_window + ) # maximum - default + def send_data( self, stream_id: int, @@ -71,12 +81,12 @@ def send_data( frame_size = len(data) assert pad_length is None - while frame_size > self.max_outbound_frame_size: - chunk_data = data[: self.max_outbound_frame_size] - self.send_data(stream_id, chunk_data, end_stream=False) + if frame_size > self.max_outbound_frame_size: + for start in range(0, frame_size, self.max_outbound_frame_size): + chunk = data[start : start + self.max_outbound_frame_size] + self.send_data(stream_id, chunk, end_stream=False) - data = data[self.max_outbound_frame_size :] - frame_size -= len(chunk_data) + return if self.stream_buffers.get(stream_id, None): # We already have some data buffered, let's append. @@ -93,7 +103,7 @@ def send_data( # We can't send right now, so we buffer. self.stream_buffers[stream_id].append(SendH2Data(data, end_stream)) - def send_trailers(self, stream_id: int, trailers: List[Tuple[bytes, bytes]]): + def send_trailers(self, stream_id: int, trailers: list[tuple[bytes, bytes]]): if self.stream_buffers.get(stream_id, None): # Though trailers are not subject to flow control, we need to queue them and send strictly after data frames self.stream_trailers[stream_id] = trailers @@ -173,7 +183,9 @@ def stream_window_updated(self, stream_id: int) -> bool: if not self.stream_buffers[stream_id]: del self.stream_buffers[stream_id] if stream_id in self.stream_trailers: - self.send_headers(stream_id, self.stream_trailers.pop(stream_id), end_stream=True) + self.send_headers( + stream_id, self.stream_trailers.pop(stream_id), end_stream=True + ) sent_any_data = True return sent_any_data diff --git a/mitmproxy/proxy/layers/http/_http_h3.py b/mitmproxy/proxy/layers/http/_http_h3.py new file mode 100644 index 0000000000..3ab4b35af6 --- /dev/null +++ b/mitmproxy/proxy/layers/http/_http_h3.py @@ -0,0 +1,321 @@ +from collections.abc import Iterable +from dataclasses import dataclass + +from aioquic.h3.connection import FrameUnexpected +from aioquic.h3.connection import H3Connection +from aioquic.h3.connection import H3Event +from aioquic.h3.connection import H3Stream +from aioquic.h3.connection import Headers +from aioquic.h3.connection import HeadersState +from aioquic.h3.events import HeadersReceived +from aioquic.quic.configuration import QuicConfiguration +from aioquic.quic.events import StreamDataReceived +from aioquic.quic.packet import QuicErrorCode + +from mitmproxy import connection +from mitmproxy.proxy import commands +from mitmproxy.proxy import layer +from mitmproxy.proxy.layers.quic import CloseQuicConnection +from mitmproxy.proxy.layers.quic import QuicConnectionClosed +from mitmproxy.proxy.layers.quic import QuicStreamDataReceived +from mitmproxy.proxy.layers.quic import QuicStreamEvent +from mitmproxy.proxy.layers.quic import QuicStreamReset +from mitmproxy.proxy.layers.quic import QuicStreamStopSending +from mitmproxy.proxy.layers.quic import ResetQuicStream +from mitmproxy.proxy.layers.quic import SendQuicStreamData +from mitmproxy.proxy.layers.quic import StopSendingQuicStream + + +@dataclass +class TrailersReceived(H3Event): + """ + The TrailersReceived event is fired whenever trailers are received. + """ + + trailers: Headers + "The trailers." + + stream_id: int + "The ID of the stream the trailers were received for." + + stream_ended: bool + "Whether the STREAM frame had the FIN bit set." + + +@dataclass +class StreamClosed(H3Event): + """ + The StreamReset event is fired when the peer is sending a CLOSE_STREAM + or a STOP_SENDING frame. For HTTP/3, we don't differentiate between the two. + """ + + stream_id: int + "The ID of the stream that was reset." + + error_code: int + """The error code indicating why the stream was closed.""" + + +class MockQuic: + """ + aioquic intermingles QUIC and HTTP/3. This is something we don't want to do because that makes testing much harder. + Instead, we mock our QUIC connection object here and then take out the wire data to be sent. + """ + + def __init__(self, conn: connection.Connection, is_client: bool) -> None: + self.conn = conn + self.pending_commands: list[commands.Command] = [] + self._next_stream_id: list[int] = [0, 1, 2, 3] + self._is_client = is_client + + # the following fields are accessed by H3Connection + self.configuration = QuicConfiguration(is_client=is_client) + self._quic_logger = None + self._remote_max_datagram_frame_size = 0 + + def close( + self, + error_code: int = QuicErrorCode.NO_ERROR, + frame_type: int | None = None, + reason_phrase: str = "", + ) -> None: + # we'll get closed if a protocol error occurs in `H3Connection.handle_event` + # we note the error on the connection and yield a CloseConnection + # this will then call `QuicConnection.close` with the proper values + # once the `Http3Connection` receives `ConnectionClosed`, it will send out `ProtocolError` + self.pending_commands.append( + CloseQuicConnection(self.conn, error_code, frame_type, reason_phrase) + ) + + def get_next_available_stream_id(self, is_unidirectional: bool = False) -> int: + # since we always reserve the ID, we have to "find" the next ID like `QuicConnection` does + index = (int(is_unidirectional) << 1) | int(not self._is_client) + stream_id = self._next_stream_id[index] + self._next_stream_id[index] = stream_id + 4 + return stream_id + + def reset_stream(self, stream_id: int, error_code: int) -> None: + self.pending_commands.append(ResetQuicStream(self.conn, stream_id, error_code)) + + def stop_send(self, stream_id: int, error_code: int) -> None: + self.pending_commands.append( + StopSendingQuicStream(self.conn, stream_id, error_code) + ) + + def send_stream_data( + self, stream_id: int, data: bytes, end_stream: bool = False + ) -> None: + self.pending_commands.append( + SendQuicStreamData(self.conn, stream_id, data, end_stream) + ) + + +class LayeredH3Connection(H3Connection): + """ + Creates a H3 connection using a fake QUIC connection, which allows layer separation. + Also ensures that headers, data and trailers are sent in that order. + """ + + def __init__( + self, + conn: connection.Connection, + is_client: bool, + enable_webtransport: bool = False, + ) -> None: + self._mock = MockQuic(conn, is_client) + self._closed_streams: set[int] = set() + """ + We keep track of all stream IDs for which we have requested + STOP_SENDING to silently discard incoming data. + """ + super().__init__(self._mock, enable_webtransport) # type: ignore + + # aioquic's constructor sets and then uses _max_push_id. + # This is a hack to forcibly disable it. + @property + def _max_push_id(self) -> int | None: + return None + + @_max_push_id.setter + def _max_push_id(self, value): + pass + + def _after_send(self, stream_id: int, end_stream: bool) -> None: + # if the stream ended, `QuicConnection` has an assert that no further data is being sent + # to catch this more early on, we set the header state on the `H3Stream` + if end_stream: + self._stream[stream_id].headers_send_state = HeadersState.AFTER_TRAILERS + + def _handle_request_or_push_frame( + self, + frame_type: int, + frame_data: bytes | None, + stream: H3Stream, + stream_ended: bool, + ) -> list[H3Event]: + # turn HeadersReceived into TrailersReceived for trailers + events = super()._handle_request_or_push_frame( + frame_type, frame_data, stream, stream_ended + ) + for index, event in enumerate(events): + if ( + isinstance(event, HeadersReceived) + and self._stream[event.stream_id].headers_recv_state + == HeadersState.AFTER_TRAILERS + ): + events[index] = TrailersReceived( + event.headers, event.stream_id, event.stream_ended + ) + return events + + def close_connection( + self, + error_code: int = QuicErrorCode.NO_ERROR, + frame_type: int | None = None, + reason_phrase: str = "", + ) -> None: + """Closes the underlying QUIC connection and ignores any incoming events.""" + + self._is_done = True + self._quic.close(error_code, frame_type, reason_phrase) + + def end_stream(self, stream_id: int) -> None: + """Ends the given stream if not already done so.""" + + stream = self._get_or_create_stream(stream_id) + if stream.headers_send_state != HeadersState.AFTER_TRAILERS: + super().send_data(stream_id, b"", end_stream=True) + stream.headers_send_state = HeadersState.AFTER_TRAILERS + + def get_next_available_stream_id(self, is_unidirectional: bool = False): + """Reserves and returns the next available stream ID.""" + + return self._quic.get_next_available_stream_id(is_unidirectional) + + def get_open_stream_ids(self) -> Iterable[int]: + """Iterates over all non-special open streams""" + + return ( + stream.stream_id + for stream in self._stream.values() + if ( + stream.stream_type is None + and not ( + stream.headers_recv_state == HeadersState.AFTER_TRAILERS + and stream.headers_send_state == HeadersState.AFTER_TRAILERS + ) + ) + ) + + def handle_connection_closed(self, event: QuicConnectionClosed) -> None: + self._is_done = True + + def handle_stream_event(self, event: QuicStreamEvent) -> list[H3Event]: + # don't do anything if we're done + if self._is_done: + return [] + + elif isinstance(event, (QuicStreamReset, QuicStreamStopSending)): + self.close_stream( + event.stream_id, + event.error_code, + stop_send=isinstance(event, QuicStreamStopSending), + ) + stream = self._get_or_create_stream(event.stream_id) + stream.ended = True + stream.headers_recv_state = HeadersState.AFTER_TRAILERS + return [StreamClosed(event.stream_id, event.error_code)] + + # convert data events from the QUIC layer back to aioquic events + elif isinstance(event, QuicStreamDataReceived): + # Discard contents if we have already sent STOP_SENDING on this stream. + if event.stream_id in self._closed_streams: + return [] + elif self._get_or_create_stream(event.stream_id).ended: + # aioquic will not send us any data events once a stream has ended. + # Instead, it will close the connection. We simulate this here for H3 tests. + self.close_connection( + error_code=QuicErrorCode.PROTOCOL_VIOLATION, + reason_phrase="stream already ended", + ) + return [] + else: + return self.handle_event( + StreamDataReceived(event.data, event.end_stream, event.stream_id) + ) + + # should never happen + else: # pragma: no cover + raise AssertionError(f"Unexpected event: {event!r}") + + def has_sent_headers(self, stream_id: int) -> bool: + """Indicates whether headers have been sent over the given stream.""" + + try: + return self._stream[stream_id].headers_send_state != HeadersState.INITIAL + except KeyError: + return False + + def close_stream( + self, stream_id: int, error_code: int, stop_send: bool = True + ) -> None: + """Close a stream that hasn't been closed locally yet.""" + if stream_id not in self._closed_streams: + self._closed_streams.add(stream_id) + + stream = self._get_or_create_stream(stream_id) + stream.headers_send_state = HeadersState.AFTER_TRAILERS + # https://www.rfc-editor.org/rfc/rfc9000.html#section-3.5-8 + # An endpoint that wishes to terminate both directions of + # a bidirectional stream can terminate one direction by + # sending a RESET_STREAM frame, and it can encourage prompt + # termination in the opposite direction by sending a + # STOP_SENDING frame. + self._mock.reset_stream(stream_id=stream_id, error_code=error_code) + if stop_send: + self._mock.stop_send(stream_id=stream_id, error_code=error_code) + + def send_data(self, stream_id: int, data: bytes, end_stream: bool = False) -> None: + """Sends data over the given stream.""" + + super().send_data(stream_id, data, end_stream) + self._after_send(stream_id, end_stream) + + def send_datagram(self, flow_id: int, data: bytes) -> None: + # supporting datagrams would require additional information from the underlying QUIC connection + raise NotImplementedError() # pragma: no cover + + def send_headers( + self, stream_id: int, headers: Headers, end_stream: bool = False + ) -> None: + """Sends headers over the given stream.""" + + # ensure we haven't sent something before + stream = self._get_or_create_stream(stream_id) + if stream.headers_send_state != HeadersState.INITIAL: + raise FrameUnexpected("initial HEADERS frame is not allowed in this state") + super().send_headers(stream_id, headers, end_stream) + self._after_send(stream_id, end_stream) + + def send_trailers(self, stream_id: int, trailers: Headers) -> None: + """Sends trailers over the given stream and ends it.""" + + # ensure we got some headers first + stream = self._get_or_create_stream(stream_id) + if stream.headers_send_state != HeadersState.AFTER_HEADERS: + raise FrameUnexpected("trailing HEADERS frame is not allowed in this state") + super().send_headers(stream_id, trailers, end_stream=True) + self._after_send(stream_id, end_stream=True) + + def transmit(self) -> layer.CommandGenerator[None]: + """Yields all pending commands for the upper QUIC layer.""" + + while self._mock.pending_commands: + yield self._mock.pending_commands.pop(0) + + +__all__ = [ + "LayeredH3Connection", + "StreamClosed", + "TrailersReceived", +] diff --git a/mitmproxy/proxy/layers/http/_upstream_proxy.py b/mitmproxy/proxy/layers/http/_upstream_proxy.py index 62909bead2..f40b26ce6e 100644 --- a/mitmproxy/proxy/layers/http/_upstream_proxy.py +++ b/mitmproxy/proxy/layers/http/_upstream_proxy.py @@ -1,15 +1,17 @@ -from logging import DEBUG - import time -from typing import Optional +from logging import DEBUG from h11._receivebuffer import ReceiveBuffer -from mitmproxy import http, connection +from mitmproxy import connection +from mitmproxy import http from mitmproxy.net.http import http1 -from mitmproxy.proxy import commands, context, layer, tunnel -from mitmproxy.proxy.layers.http._hooks import HttpConnectUpstreamHook +from mitmproxy.proxy import commands +from mitmproxy.proxy import context +from mitmproxy.proxy import layer +from mitmproxy.proxy import tunnel from mitmproxy.proxy.layers import tls +from mitmproxy.proxy.layers.http._hooks import HttpConnectUpstreamHook from mitmproxy.utils import human @@ -32,7 +34,7 @@ def make(cls, ctx: context.Context, send_connect: bool) -> tunnel.LayerStack: scheme, address = ctx.server.via assert scheme in ("http", "https") - http_proxy = connection.Server(address) + http_proxy = connection.Server(address=address) stack = tunnel.LayerStack() if scheme == "https": @@ -48,7 +50,12 @@ def start_handshake(self) -> layer.CommandGenerator[None]: return (yield from super().start_handshake()) assert self.conn.address flow = http.HTTPFlow(self.context.client, self.tunnel_connection) - authority = self.conn.address[0].encode("idna") + f":{self.conn.address[1]}".encode() + authority = ( + self.conn.address[0].encode("idna") + f":{self.conn.address[1]}".encode() + ) + headers = http.Headers() + if self.context.options.http_connect_send_host_header: + headers.insert(0, b"Host", authority) flow.request = http.Request( host=self.conn.address[0], port=self.conn.address[1], @@ -57,7 +64,7 @@ def start_handshake(self) -> layer.CommandGenerator[None]: authority=authority, path=b"", http_version=b"HTTP/1.1", - headers=http.Headers(), + headers=headers, content=b"", trailers=None, timestamp_start=time.time(), @@ -69,16 +76,14 @@ def start_handshake(self) -> layer.CommandGenerator[None]: def receive_handshake_data( self, data: bytes - ) -> layer.CommandGenerator[tuple[bool, Optional[str]]]: + ) -> layer.CommandGenerator[tuple[bool, str | None]]: if not self.send_connect: return (yield from super().receive_handshake_data(data)) self.buf += data response_head = self.buf.maybe_extract_lines() if response_head: try: - response = http1.read_response_head([ - bytes(x) for x in response_head - ]) + response = http1.read_response_head([bytes(x) for x in response_head]) except ValueError as e: proxyaddr = human.format_address(self.tunnel_connection.address) yield commands.Log(f"{proxyaddr}: {e}") diff --git a/mitmproxy/proxy/layers/modes.py b/mitmproxy/proxy/layers/modes.py index d320b513ff..0f917cd78b 100644 --- a/mitmproxy/proxy/layers/modes.py +++ b/mitmproxy/proxy/layers/modes.py @@ -1,16 +1,25 @@ +from __future__ import annotations + import socket import struct +import sys from abc import ABCMeta +from collections.abc import Callable from dataclasses import dataclass -from typing import Optional from mitmproxy import connection -from mitmproxy.proxy import commands, events, layer +from mitmproxy.proxy import commands +from mitmproxy.proxy import events +from mitmproxy.proxy import layer from mitmproxy.proxy.commands import StartHook -from mitmproxy.proxy.layers import dns, tls from mitmproxy.proxy.mode_specs import ReverseMode from mitmproxy.proxy.utils import expect +if sys.version_info < (3, 11): + from typing_extensions import assert_never +else: + from typing import assert_never + class HttpProxy(layer.Layer): @expect(events.Start) @@ -33,7 +42,7 @@ class DestinationKnown(layer.Layer, metaclass=ABCMeta): child_layer: layer.Layer - def finish_start(self) -> layer.CommandGenerator[Optional[str]]: + def finish_start(self) -> layer.CommandGenerator[str | None]: if ( self.context.options.connection_strategy == "eager" and self.context.server.address @@ -60,16 +69,17 @@ def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: assert isinstance(spec, ReverseMode) self.context.server.address = spec.address - if spec.scheme == "https" or spec.scheme == "tls" or spec.scheme == "dtls": - if not self.context.options.keep_host_header: - self.context.server.sni = spec.address[0] - self.child_layer = tls.ServerTLSLayer(self.context) - elif spec.scheme == "http" or spec.scheme == "tcp" or spec.scheme == "udp": - self.child_layer = layer.NextLayer(self.context) - elif spec.scheme == "dns": - self.child_layer = dns.DNSLayer(self.context) - else: - raise AssertionError(self.context.client.transport_protocol) # pragma: no cover + self.child_layer = layer.NextLayer(self.context) + + # For secure protocols, set SNI if keep_host_header is false + match spec.scheme: + case "http3" | "quic" | "https" | "tls" | "dtls": + if not self.context.options.keep_host_header: + self.context.server.sni = spec.address[0] + case "tcp" | "http" | "udp" | "dns": + pass + case _: # pragma: no cover + assert_never(spec.scheme) err = yield from self.finish_start() if err: @@ -79,7 +89,7 @@ def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: class TransparentProxy(DestinationKnown): @expect(events.Start) def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: - assert self.context.server.address + assert self.context.server.address, "No server address set." self.child_layer = layer.NextLayer(self.context) err = yield from self.finish_start() if err: @@ -126,7 +136,7 @@ class Socks5Proxy(DestinationKnown): def socks_err( self, message: str, - reply_code: Optional[int] = None, + reply_code: int | None = None, ) -> layer.CommandGenerator[None]: if reply_code is not None: yield commands.SendData( @@ -154,7 +164,7 @@ def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: else: raise AssertionError(f"Unknown event: {event}") - def state_greet(self): + def state_greet(self) -> layer.CommandGenerator[None]: if len(self.buf) < 2: return @@ -194,9 +204,9 @@ def state_greet(self): self.buf = self.buf[2 + n_methods :] yield from self.state() - state = state_greet + state: Callable[..., layer.CommandGenerator[None]] = state_greet - def state_auth(self): + def state_auth(self) -> layer.CommandGenerator[None]: if len(self.buf) < 3: return @@ -225,7 +235,7 @@ def state_auth(self): self.state = self.state_connect yield from self.state() - def state_connect(self): + def state_connect(self) -> layer.CommandGenerator[None]: # Parse Connect Request if len(self.buf) < 5: return diff --git a/mitmproxy/proxy/layers/quic/__init__.py b/mitmproxy/proxy/layers/quic/__init__.py new file mode 100644 index 0000000000..28a3ce3be7 --- /dev/null +++ b/mitmproxy/proxy/layers/quic/__init__.py @@ -0,0 +1,41 @@ +from ._client_hello_parser import quic_parse_client_hello_from_datagrams +from ._commands import CloseQuicConnection +from ._commands import ResetQuicStream +from ._commands import SendQuicStreamData +from ._commands import StopSendingQuicStream +from ._events import QuicConnectionClosed +from ._events import QuicStreamDataReceived +from ._events import QuicStreamEvent +from ._events import QuicStreamReset +from ._events import QuicStreamStopSending +from ._hooks import QuicStartClientHook +from ._hooks import QuicStartServerHook +from ._hooks import QuicTlsData +from ._hooks import QuicTlsSettings +from ._raw_layers import QuicStreamLayer +from ._raw_layers import RawQuicLayer +from ._stream_layers import ClientQuicLayer +from ._stream_layers import error_code_to_str +from ._stream_layers import ServerQuicLayer + +__all__ = [ + "quic_parse_client_hello_from_datagrams", + "CloseQuicConnection", + "ResetQuicStream", + "SendQuicStreamData", + "StopSendingQuicStream", + "QuicConnectionClosed", + "QuicStreamDataReceived", + "QuicStreamEvent", + "QuicStreamReset", + "QuicStreamStopSending", + "QuicStartClientHook", + "QuicStartServerHook", + "QuicTlsData", + "QuicTlsSettings", + "QuicStreamLayer", + "RawQuicLayer", + "ClientQuicLayer", + "error_code_to_str", + "ServerQuicLayer", +] diff --git a/mitmproxy/proxy/layers/quic/_client_hello_parser.py b/mitmproxy/proxy/layers/quic/_client_hello_parser.py new file mode 100644 index 0000000000..46dd615084 --- /dev/null +++ b/mitmproxy/proxy/layers/quic/_client_hello_parser.py @@ -0,0 +1,111 @@ +""" +This module contains a very terrible QUIC client hello parser. + +Nothing is more permanent than a temporary solution! +""" + +from __future__ import annotations + +import time +from dataclasses import dataclass +from typing import Optional + +from aioquic.buffer import Buffer as QuicBuffer +from aioquic.quic.configuration import QuicConfiguration +from aioquic.quic.connection import QuicConnection +from aioquic.quic.connection import QuicConnectionError +from aioquic.quic.logger import QuicLogger +from aioquic.quic.packet import PACKET_TYPE_INITIAL +from aioquic.quic.packet import pull_quic_header +from aioquic.tls import HandshakeType + +from mitmproxy.tls import ClientHello + + +@dataclass +class QuicClientHello(Exception): + """Helper error only used in `quic_parse_client_hello_from_datagrams`.""" + + data: bytes + + +def quic_parse_client_hello_from_datagrams( + datagrams: list[bytes], +) -> Optional[ClientHello]: + """ + Check if the supplied bytes contain a full ClientHello message, + and if so, parse it. + + Args: + - msgs: list of ClientHello fragments received from client + + Returns: + - A ClientHello object on success + - None, if the QUIC record is incomplete + + Raises: + - A ValueError, if the passed ClientHello is invalid + """ + + # ensure the first packet is indeed the initial one + buffer = QuicBuffer(data=datagrams[0]) + header = pull_quic_header(buffer, 8) + if header.packet_type != PACKET_TYPE_INITIAL: + raise ValueError("Packet is not initial one.") + + # patch aioquic to intercept the client hello + quic = QuicConnection( + configuration=QuicConfiguration( + is_client=False, + certificate="", + private_key="", + quic_logger=QuicLogger(), + ), + original_destination_connection_id=header.destination_cid, + ) + _initialize = quic._initialize + + def server_handle_hello_replacement( + input_buf: QuicBuffer, + initial_buf: QuicBuffer, + handshake_buf: QuicBuffer, + onertt_buf: QuicBuffer, + ) -> None: + assert input_buf.pull_uint8() == HandshakeType.CLIENT_HELLO + length = 0 + for b in input_buf.pull_bytes(3): + length = (length << 8) | b + offset = input_buf.tell() + raise QuicClientHello(input_buf.data_slice(offset, offset + length)) + + def initialize_replacement(peer_cid: bytes) -> None: + try: + return _initialize(peer_cid) + finally: + quic.tls._server_handle_hello = server_handle_hello_replacement # type: ignore + + quic._initialize = initialize_replacement # type: ignore + try: + for dgm in datagrams: + quic.receive_datagram(dgm, ("0.0.0.0", 0), now=time.time()) + except QuicClientHello as hello: + try: + return ClientHello(hello.data) + except EOFError as e: + raise ValueError("Invalid ClientHello data.") from e + except QuicConnectionError as e: + raise ValueError(e.reason_phrase) from e + + quic_logger = quic._configuration.quic_logger + assert isinstance(quic_logger, QuicLogger) + traces = quic_logger.to_dict().get("traces") + assert isinstance(traces, list) + for trace in traces: + quic_events = trace.get("events") + for event in quic_events: + if event["name"] == "transport:packet_dropped": + raise ValueError( + f"Invalid ClientHello packet: {event['data']['trigger']}" + ) + + return None # pragma: no cover # FIXME: this should have test coverage diff --git a/mitmproxy/proxy/layers/quic/_commands.py b/mitmproxy/proxy/layers/quic/_commands.py new file mode 100644 index 0000000000..1f952c3c66 --- /dev/null +++ b/mitmproxy/proxy/layers/quic/_commands.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from mitmproxy import connection +from mitmproxy.proxy import commands + + +class QuicStreamCommand(commands.ConnectionCommand): + """Base class for all QUIC stream commands.""" + + stream_id: int + """The ID of the stream the command was issued for.""" + + def __init__(self, connection: connection.Connection, stream_id: int) -> None: + super().__init__(connection) + self.stream_id = stream_id + + +class SendQuicStreamData(QuicStreamCommand): + """Command that sends data on a stream.""" + + data: bytes + """The data which should be sent.""" + end_stream: bool + """Whether the FIN bit should be set in the STREAM frame.""" + + def __init__( + self, + connection: connection.Connection, + stream_id: int, + data: bytes, + end_stream: bool = False, + ) -> None: + super().__init__(connection, stream_id) + self.data = data + self.end_stream = end_stream + + def __repr__(self): + target = repr(self.connection).partition("(")[0].lower() + end_stream = "[end_stream] " if self.end_stream else "" + return f"SendQuicStreamData({target} on {self.stream_id}, {end_stream}{self.data!r})" + + +class ResetQuicStream(QuicStreamCommand): + """Abruptly terminate the sending part of a stream.""" + + error_code: int + """An error code indicating why the stream is being reset.""" + + def __init__( + self, connection: connection.Connection, stream_id: int, error_code: int + ) -> None: + super().__init__(connection, stream_id) + self.error_code = error_code + + +class StopSendingQuicStream(QuicStreamCommand): + """Request termination of the receiving part of a stream.""" + + error_code: int + """An error code indicating why the stream is being stopped.""" + + def __init__( + self, connection: connection.Connection, stream_id: int, error_code: int + ) -> None: + super().__init__(connection, stream_id) + self.error_code = error_code + + +class CloseQuicConnection(commands.CloseConnection): + """Close a QUIC connection.""" + + error_code: int + "The error code which was specified when closing the connection." + + frame_type: int | None + "The frame type which caused the connection to be closed, or `None`." + + reason_phrase: str + "The human-readable reason for which the connection was closed." + + # XXX: A bit much boilerplate right now. Should switch to dataclasses. + def __init__( + self, + conn: connection.Connection, + error_code: int, + frame_type: int | None, + reason_phrase: str, + ) -> None: + super().__init__(conn) + self.error_code = error_code + self.frame_type = frame_type + self.reason_phrase = reason_phrase diff --git a/mitmproxy/proxy/layers/quic/_events.py b/mitmproxy/proxy/layers/quic/_events.py new file mode 100644 index 0000000000..67a4d4a8e0 --- /dev/null +++ b/mitmproxy/proxy/layers/quic/_events.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from mitmproxy import connection +from mitmproxy.proxy import events + + +@dataclass +class QuicStreamEvent(events.ConnectionEvent): + """Base class for all QUIC stream events.""" + + stream_id: int + """The ID of the stream the event was fired for.""" + + +@dataclass +class QuicStreamDataReceived(QuicStreamEvent): + """Event that is fired whenever data is received on a stream.""" + + data: bytes + """The data which was received.""" + end_stream: bool + """Whether the STREAM frame had the FIN bit set.""" + + def __repr__(self): + target = repr(self.connection).partition("(")[0].lower() + end_stream = "[end_stream] " if self.end_stream else "" + return f"QuicStreamDataReceived({target} on {self.stream_id}, {end_stream}{self.data!r})" + + +@dataclass +class QuicStreamReset(QuicStreamEvent): + """Event that is fired when the remote peer resets a stream.""" + + error_code: int + """The error code that triggered the reset.""" + + +@dataclass +class QuicStreamStopSending(QuicStreamEvent): + """Event that is fired when the remote peer sends a STOP_SENDING frame.""" + + error_code: int + """The application protocol error code.""" + + +class QuicConnectionClosed(events.ConnectionClosed): + """QUIC connection has been closed.""" + + error_code: int + "The error code which was specified when closing the connection." + + frame_type: int | None + "The frame type which caused the connection to be closed, or `None`." + + reason_phrase: str + "The human-readable reason for which the connection was closed." + + def __init__( + self, + conn: connection.Connection, + error_code: int, + frame_type: int | None, + reason_phrase: str, + ) -> None: + super().__init__(conn) + self.error_code = error_code + self.frame_type = frame_type + self.reason_phrase = reason_phrase diff --git a/mitmproxy/proxy/layers/quic/_hooks.py b/mitmproxy/proxy/layers/quic/_hooks.py new file mode 100644 index 0000000000..9c84e74d52 --- /dev/null +++ b/mitmproxy/proxy/layers/quic/_hooks.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from dataclasses import dataclass +from dataclasses import field +from ssl import VerifyMode + +from aioquic.tls import CipherSuite +from cryptography import x509 +from cryptography.hazmat.primitives.asymmetric import dsa +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric import rsa + +from mitmproxy.proxy import commands +from mitmproxy.tls import TlsData + + +@dataclass +class QuicTlsSettings: + """ + Settings necessary to establish QUIC's TLS context. + """ + + alpn_protocols: list[str] | None = None + """A list of supported ALPN protocols.""" + certificate: x509.Certificate | None = None + """The certificate to use for the connection.""" + certificate_chain: list[x509.Certificate] = field(default_factory=list) + """A list of additional certificates to send to the peer.""" + certificate_private_key: ( + dsa.DSAPrivateKey | ec.EllipticCurvePrivateKey | rsa.RSAPrivateKey | None + ) = None + """The certificate's private key.""" + cipher_suites: list[CipherSuite] | None = None + """An optional list of allowed/advertised cipher suites.""" + ca_path: str | None = None + """An optional path to a directory that contains the necessary information to verify the peer certificate.""" + ca_file: str | None = None + """An optional path to a PEM file that will be used to verify the peer certificate.""" + verify_mode: VerifyMode | None = None + """An optional flag that specifies how/if the peer's certificate should be validated.""" + + +@dataclass +class QuicTlsData(TlsData): + """ + Event data for `quic_start_client` and `quic_start_server` event hooks. + """ + + settings: QuicTlsSettings | None = None + """ + The associated `QuicTlsSettings` object. + This will be set by an addon in the `quic_start_*` event hooks. + """ + + +@dataclass +class QuicStartClientHook(commands.StartHook): + """ + TLS negotiation between mitmproxy and a client over QUIC is about to start. + + An addon is expected to initialize data.settings. + (by default, this is done by `mitmproxy.addons.tlsconfig`) + """ + + data: QuicTlsData + + +@dataclass +class QuicStartServerHook(commands.StartHook): + """ + TLS negotiation between mitmproxy and a server over QUIC is about to start. + + An addon is expected to initialize data.settings. + (by default, this is done by `mitmproxy.addons.tlsconfig`) + """ + + data: QuicTlsData diff --git a/mitmproxy/proxy/layers/quic/_raw_layers.py b/mitmproxy/proxy/layers/quic/_raw_layers.py new file mode 100644 index 0000000000..81f34e7f82 --- /dev/null +++ b/mitmproxy/proxy/layers/quic/_raw_layers.py @@ -0,0 +1,433 @@ +""" +This module contains the proxy layers for raw QUIC proxying. +This is used if we want to speak QUIC, but we do not want to do HTTP. +""" + +from __future__ import annotations + +import time + +from aioquic.quic.connection import QuicErrorCode +from aioquic.quic.connection import stream_is_client_initiated +from aioquic.quic.connection import stream_is_unidirectional + +from ._commands import CloseQuicConnection +from ._commands import ResetQuicStream +from ._commands import SendQuicStreamData +from ._commands import StopSendingQuicStream +from ._events import QuicConnectionClosed +from ._events import QuicStreamDataReceived +from ._events import QuicStreamEvent +from ._events import QuicStreamReset +from mitmproxy import connection +from mitmproxy.connection import Connection +from mitmproxy.proxy import commands +from mitmproxy.proxy import context +from mitmproxy.proxy import events +from mitmproxy.proxy import layer +from mitmproxy.proxy import tunnel +from mitmproxy.proxy.layers.tcp import TCPLayer +from mitmproxy.proxy.layers.udp import UDPLayer + + +class QuicStreamNextLayer(layer.NextLayer): + """`NextLayer` variant that callbacks `QuicStreamLayer` after layer decision.""" + + def __init__( + self, + context: context.Context, + stream: QuicStreamLayer, + ask_on_start: bool = False, + ) -> None: + super().__init__(context, ask_on_start) + self._stream = stream + self._layer: layer.Layer | None = None + + @property # type: ignore + def layer(self) -> layer.Layer | None: # type: ignore + return self._layer + + @layer.setter + def layer(self, value: layer.Layer | None) -> None: + self._layer = value + if self._layer: + self._stream.refresh_metadata() + + +class QuicStreamLayer(layer.Layer): + """ + Layer for QUIC streams. + Serves as a marker for NextLayer and keeps track of the connection states. + """ + + client: connection.Client + """Virtual client connection for this stream. Use this in QuicRawLayer instead of `context.client`.""" + server: connection.Server + """Virtual server connection for this stream. Use this in QuicRawLayer instead of `context.server`.""" + child_layer: layer.Layer + """The stream's child layer.""" + + def __init__( + self, context: context.Context, force_raw: bool, stream_id: int + ) -> None: + # we mustn't reuse the client from the QUIC connection, as the state and protocol differs + self.client = context.client = context.client.copy() + self.client.transport_protocol = "tcp" + self.client.state = connection.ConnectionState.OPEN + + # unidirectional client streams are not fully open, set the appropriate state + if stream_is_unidirectional(stream_id): + self.client.state = ( + connection.ConnectionState.CAN_READ + if stream_is_client_initiated(stream_id) + else connection.ConnectionState.CAN_WRITE + ) + self._client_stream_id = stream_id + + # start with a closed server + self.server = context.server = connection.Server( + address=context.server.address, + transport_protocol="tcp", + ) + self._server_stream_id: int | None = None + + super().__init__(context) + self.child_layer = ( + TCPLayer(context) if force_raw else QuicStreamNextLayer(context, self) + ) + self.refresh_metadata() + + # we don't handle any events, pass everything to the child layer + self.handle_event = self.child_layer.handle_event # type: ignore + self._handle_event = self.child_layer._handle_event # type: ignore + + def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: + raise AssertionError # pragma: no cover + + def open_server_stream(self, server_stream_id) -> None: + assert self._server_stream_id is None + self._server_stream_id = server_stream_id + self.server.timestamp_start = time.time() + self.server.state = ( + ( + connection.ConnectionState.CAN_WRITE + if stream_is_client_initiated(server_stream_id) + else connection.ConnectionState.CAN_READ + ) + if stream_is_unidirectional(server_stream_id) + else connection.ConnectionState.OPEN + ) + self.refresh_metadata() + + def refresh_metadata(self) -> None: + # find the first transport layer + child_layer: layer.Layer | None = self.child_layer + while True: + if isinstance(child_layer, layer.NextLayer): + child_layer = child_layer.layer + elif isinstance(child_layer, tunnel.TunnelLayer): + child_layer = child_layer.child_layer + else: + break # pragma: no cover + if isinstance(child_layer, (UDPLayer, TCPLayer)) and child_layer.flow: + child_layer.flow.metadata["quic_is_unidirectional"] = ( + stream_is_unidirectional(self._client_stream_id) + ) + child_layer.flow.metadata["quic_initiator"] = ( + "client" + if stream_is_client_initiated(self._client_stream_id) + else "server" + ) + child_layer.flow.metadata["quic_stream_id_client"] = self._client_stream_id + child_layer.flow.metadata["quic_stream_id_server"] = self._server_stream_id + + def stream_id(self, client: bool) -> int | None: + return self._client_stream_id if client else self._server_stream_id + + +class RawQuicLayer(layer.Layer): + """ + This layer is responsible for de-multiplexing QUIC streams into an individual layer stack per stream. + """ + + force_raw: bool + """Indicates whether traffic should be treated as raw TCP/UDP without further protocol detection.""" + datagram_layer: layer.Layer + """ + The layer that is handling datagrams over QUIC. It's like a child_layer, but with a forked context. + Instead of having a datagram-equivalent for all `QuicStream*` classes, we use `SendData` and `DataReceived` instead. + There is also no need for another `NextLayer` marker, as a missing `QuicStreamLayer` implies UDP, + and the connection state is the same as the one of the underlying QUIC connection. + """ + client_stream_ids: dict[int, QuicStreamLayer] + """Maps stream IDs from the client connection to stream layers.""" + server_stream_ids: dict[int, QuicStreamLayer] + """Maps stream IDs from the server connection to stream layers.""" + connections: dict[connection.Connection, layer.Layer] + """Maps connections to layers.""" + command_sources: dict[commands.Command, layer.Layer] + """Keeps track of blocking commands and wakeup requests.""" + next_stream_id: list[int] + """List containing the next stream ID for all four is_unidirectional/is_client combinations.""" + + def __init__(self, context: context.Context, force_raw: bool = False) -> None: + super().__init__(context) + self.force_raw = force_raw + self.datagram_layer = ( + UDPLayer(self.context.fork()) + if force_raw + else layer.NextLayer(self.context.fork()) + ) + self.client_stream_ids = {} + self.server_stream_ids = {} + self.connections = { + context.client: self.datagram_layer, + context.server: self.datagram_layer, + } + self.command_sources = {} + self.next_stream_id = [0, 1, 2, 3] + + def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: + # we treat the datagram layer as child layer, so forward Start + if isinstance(event, events.Start): + if self.context.server.timestamp_start is None: + err = yield commands.OpenConnection(self.context.server) + if err: + yield commands.CloseConnection(self.context.client) + self._handle_event = self.done # type: ignore + return + yield from self.event_to_child(self.datagram_layer, event) + + # properly forward completion events based on their command + elif isinstance(event, events.CommandCompleted): + yield from self.event_to_child( + self.command_sources.pop(event.command), event + ) + + # route injected messages based on their connections (prefer client, fallback to server) + elif isinstance(event, events.MessageInjected): + if event.flow.client_conn in self.connections: + yield from self.event_to_child( + self.connections[event.flow.client_conn], event + ) + elif event.flow.server_conn in self.connections: + yield from self.event_to_child( + self.connections[event.flow.server_conn], event + ) + else: + raise AssertionError(f"Flow not associated: {event.flow!r}") + + # handle stream events targeting this context + elif isinstance(event, QuicStreamEvent) and ( + event.connection is self.context.client + or event.connection is self.context.server + ): + from_client = event.connection is self.context.client + + # fetch or create the layer + stream_ids = ( + self.client_stream_ids if from_client else self.server_stream_ids + ) + if event.stream_id in stream_ids: + stream_layer = stream_ids[event.stream_id] + else: + # ensure we haven't just forgotten to register the ID + assert stream_is_client_initiated(event.stream_id) == from_client + + # for server-initiated streams we need to open the client as well + if from_client: + client_stream_id = event.stream_id + server_stream_id = None + else: + client_stream_id = self.get_next_available_stream_id( + is_client=False, + is_unidirectional=stream_is_unidirectional(event.stream_id), + ) + server_stream_id = event.stream_id + + # create, register and start the layer + stream_layer = QuicStreamLayer( + self.context.fork(), + force_raw=self.force_raw, + stream_id=client_stream_id, + ) + self.client_stream_ids[client_stream_id] = stream_layer + if server_stream_id is not None: + stream_layer.open_server_stream(server_stream_id) + self.server_stream_ids[server_stream_id] = stream_layer + self.connections[stream_layer.client] = stream_layer + self.connections[stream_layer.server] = stream_layer + yield from self.event_to_child(stream_layer, events.Start()) + + # forward data and close events + conn: Connection = ( + stream_layer.client if from_client else stream_layer.server + ) + if isinstance(event, QuicStreamDataReceived): + if event.data: + yield from self.event_to_child( + stream_layer, events.DataReceived(conn, event.data) + ) + if event.end_stream: + yield from self.close_stream_layer(stream_layer, from_client) + elif isinstance(event, QuicStreamReset): + # preserve stream resets + for command in self.close_stream_layer(stream_layer, from_client): + if ( + isinstance(command, SendQuicStreamData) + and command.stream_id == stream_layer.stream_id(not from_client) + and command.end_stream + and not command.data + ): + yield ResetQuicStream( + command.connection, command.stream_id, event.error_code + ) + else: + yield command + else: + raise AssertionError(f"Unexpected stream event: {event!r}") + + # handle close events that target this context + elif isinstance(event, QuicConnectionClosed) and ( + event.connection is self.context.client + or event.connection is self.context.server + ): + from_client = event.connection is self.context.client + other_conn = self.context.server if from_client else self.context.client + + # be done if both connections are closed + if other_conn.connected: + yield CloseQuicConnection( + other_conn, event.error_code, event.frame_type, event.reason_phrase + ) + else: + self._handle_event = self.done # type: ignore + + # always forward to the datagram layer and swallow `CloseConnection` commands + for command in self.event_to_child(self.datagram_layer, event): + if ( + not isinstance(command, commands.CloseConnection) + or command.connection is not other_conn + ): + yield command + + # forward to either the client or server connection of stream layers and swallow empty stream end + for conn, child_layer in self.connections.items(): + if isinstance(child_layer, QuicStreamLayer) and ( + (conn is child_layer.client) + if from_client + else (conn is child_layer.server) + ): + conn.state &= ~connection.ConnectionState.CAN_WRITE + for command in self.close_stream_layer(child_layer, from_client): + if not isinstance(command, SendQuicStreamData) or command.data: + yield command + + # all other connection events are routed to their corresponding layer + elif isinstance(event, events.ConnectionEvent): + yield from self.event_to_child(self.connections[event.connection], event) + + else: + raise AssertionError(f"Unexpected event: {event!r}") + + def close_stream_layer( + self, stream_layer: QuicStreamLayer, client: bool + ) -> layer.CommandGenerator[None]: + """Closes the incoming part of a connection.""" + + conn = stream_layer.client if client else stream_layer.server + conn.state &= ~connection.ConnectionState.CAN_READ + assert conn.timestamp_start is not None + if conn.timestamp_end is None: + conn.timestamp_end = time.time() + yield from self.event_to_child(stream_layer, events.ConnectionClosed(conn)) + + def event_to_child( + self, child_layer: layer.Layer, event: events.Event + ) -> layer.CommandGenerator[None]: + """Forwards events to child layers and translates commands.""" + + for command in child_layer.handle_event(event): + # intercept commands for streams connections + if ( + isinstance(child_layer, QuicStreamLayer) + and isinstance(command, commands.ConnectionCommand) + and ( + command.connection is child_layer.client + or command.connection is child_layer.server + ) + ): + # get the target connection and stream ID + to_client = command.connection is child_layer.client + quic_conn = self.context.client if to_client else self.context.server + stream_id = child_layer.stream_id(to_client) + + # write data and check CloseConnection wasn't called before + if isinstance(command, commands.SendData): + assert stream_id is not None + if command.connection.state & connection.ConnectionState.CAN_WRITE: + yield SendQuicStreamData(quic_conn, stream_id, command.data) + + # send a FIN and optionally also a STOP frame + elif isinstance(command, commands.CloseConnection): + assert stream_id is not None + if command.connection.state & connection.ConnectionState.CAN_WRITE: + command.connection.state &= ( + ~connection.ConnectionState.CAN_WRITE + ) + yield SendQuicStreamData( + quic_conn, stream_id, b"", end_stream=True + ) + # XXX: Use `command.connection.state & connection.ConnectionState.CAN_READ` instead? + only_close_our_half = ( + isinstance(command, commands.CloseTcpConnection) + and command.half_close + ) + if not only_close_our_half: + if stream_is_client_initiated( + stream_id + ) == to_client or not stream_is_unidirectional(stream_id): + yield StopSendingQuicStream( + quic_conn, stream_id, QuicErrorCode.NO_ERROR + ) + yield from self.close_stream_layer(child_layer, to_client) + + # open server connections by reserving the next stream ID + elif isinstance(command, commands.OpenConnection): + assert not to_client + assert stream_id is None + client_stream_id = child_layer.stream_id(client=True) + assert client_stream_id is not None + stream_id = self.get_next_available_stream_id( + is_client=True, + is_unidirectional=stream_is_unidirectional(client_stream_id), + ) + child_layer.open_server_stream(stream_id) + self.server_stream_ids[stream_id] = child_layer + yield from self.event_to_child( + child_layer, events.OpenConnectionCompleted(command, None) + ) + + else: + raise AssertionError( + f"Unexpected stream connection command: {command!r}" + ) + + # remember blocking and wakeup commands + else: + if command.blocking or isinstance(command, commands.RequestWakeup): + self.command_sources[command] = child_layer + if isinstance(command, commands.OpenConnection): + self.connections[command.connection] = child_layer + yield command + + def get_next_available_stream_id( + self, is_client: bool, is_unidirectional: bool = False + ) -> int: + index = (int(is_unidirectional) << 1) | int(not is_client) + stream_id = self.next_stream_id[index] + self.next_stream_id[index] = stream_id + 4 + return stream_id + + def done(self, _) -> layer.CommandGenerator[None]: # pragma: no cover + yield from () diff --git a/mitmproxy/proxy/layers/quic/_stream_layers.py b/mitmproxy/proxy/layers/quic/_stream_layers.py new file mode 100644 index 0000000000..3f460dc44f --- /dev/null +++ b/mitmproxy/proxy/layers/quic/_stream_layers.py @@ -0,0 +1,638 @@ +""" +This module contains the client and server proxy layers for QUIC streams +which decrypt and encrypt traffic. Decrypted stream data is then forwarded +to either the raw layers, or the HTTP/3 client in ../http/_http3.py. +""" + +from __future__ import annotations + +import time +from collections.abc import Callable +from logging import DEBUG +from logging import ERROR +from logging import WARNING + +from aioquic.buffer import Buffer as QuicBuffer +from aioquic.h3.connection import ErrorCode as H3ErrorCode +from aioquic.quic import events as quic_events +from aioquic.quic.configuration import QuicConfiguration +from aioquic.quic.connection import QuicConnection +from aioquic.quic.connection import QuicConnectionState +from aioquic.quic.connection import QuicErrorCode +from aioquic.quic.packet import encode_quic_version_negotiation +from aioquic.quic.packet import PACKET_TYPE_INITIAL +from aioquic.quic.packet import pull_quic_header +from cryptography import x509 + +from ._client_hello_parser import quic_parse_client_hello_from_datagrams +from ._commands import CloseQuicConnection +from ._commands import QuicStreamCommand +from ._commands import ResetQuicStream +from ._commands import SendQuicStreamData +from ._commands import StopSendingQuicStream +from ._events import QuicConnectionClosed +from ._events import QuicStreamDataReceived +from ._events import QuicStreamReset +from ._events import QuicStreamStopSending +from ._hooks import QuicStartClientHook +from ._hooks import QuicStartServerHook +from ._hooks import QuicTlsData +from ._hooks import QuicTlsSettings +from mitmproxy import certs +from mitmproxy import connection +from mitmproxy import ctx +from mitmproxy.net import tls +from mitmproxy.proxy import commands +from mitmproxy.proxy import context +from mitmproxy.proxy import events +from mitmproxy.proxy import layer +from mitmproxy.proxy import tunnel +from mitmproxy.proxy.layers.tls import TlsClienthelloHook +from mitmproxy.proxy.layers.tls import TlsEstablishedClientHook +from mitmproxy.proxy.layers.tls import TlsEstablishedServerHook +from mitmproxy.proxy.layers.tls import TlsFailedClientHook +from mitmproxy.proxy.layers.tls import TlsFailedServerHook +from mitmproxy.proxy.layers.udp import UDPLayer +from mitmproxy.tls import ClientHelloData + +SUPPORTED_QUIC_VERSIONS_SERVER = QuicConfiguration(is_client=False).supported_versions + + +class QuicLayer(tunnel.TunnelLayer): + quic: QuicConnection | None = None + tls: QuicTlsSettings | None = None + + def __init__( + self, + context: context.Context, + conn: connection.Connection, + time: Callable[[], float] | None, + ) -> None: + super().__init__(context, tunnel_connection=conn, conn=conn) + self.child_layer = layer.NextLayer(self.context, ask_on_start=True) + self._time = time or ctx.master.event_loop.time + self._wakeup_commands: dict[commands.RequestWakeup, float] = dict() + conn.tls = True + + def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: + if isinstance(event, events.Wakeup) and event.command in self._wakeup_commands: + # TunnelLayer has no understanding of wakeups, so we turn this into an empty DataReceived event + # which TunnelLayer recognizes as belonging to our connection. + assert self.quic + scheduled_time = self._wakeup_commands.pop(event.command) + if self.quic._state is not QuicConnectionState.TERMINATED: + # weird quirk: asyncio sometimes returns a bit ahead of time. + now = max(scheduled_time, self._time()) + self.quic.handle_timer(now) + yield from super()._handle_event( + events.DataReceived(self.tunnel_connection, b"") + ) + else: + yield from super()._handle_event(event) + + def event_to_child(self, event: events.Event) -> layer.CommandGenerator[None]: + # the parent will call _handle_command multiple times, we transmit cumulative afterwards + # this will reduce the number of sends, especially if data=b"" and end_stream=True + yield from super().event_to_child(event) + if self.quic: + yield from self.tls_interact() + + def _handle_command( + self, command: commands.Command + ) -> layer.CommandGenerator[None]: + """Turns stream commands into aioquic connection invocations.""" + if isinstance(command, QuicStreamCommand) and command.connection is self.conn: + assert self.quic + if isinstance(command, SendQuicStreamData): + self.quic.send_stream_data( + command.stream_id, command.data, command.end_stream + ) + elif isinstance(command, ResetQuicStream): + stream = self.quic._get_or_create_stream_for_send(command.stream_id) + existing_reset_error_code = stream.sender._reset_error_code + if existing_reset_error_code is None: + self.quic.reset_stream(command.stream_id, command.error_code) + elif self.debug: # pragma: no cover + yield commands.Log( + f"{self.debug}[quic] stream {stream.stream_id} already reset ({existing_reset_error_code=}, {command.error_code=})", + DEBUG, + ) + elif isinstance(command, StopSendingQuicStream): + # the stream might have already been closed, check before stopping + if command.stream_id in self.quic._streams: + self.quic.stop_stream(command.stream_id, command.error_code) + else: + raise AssertionError(f"Unexpected stream command: {command!r}") + else: + yield from super()._handle_command(command) + + def start_tls( + self, original_destination_connection_id: bytes | None + ) -> layer.CommandGenerator[None]: + """Initiates the aioquic connection.""" + + # must only be called if QUIC is uninitialized + assert not self.quic + assert not self.tls + + # query addons to provide the necessary TLS settings + tls_data = QuicTlsData(self.conn, self.context) + if self.conn is self.context.client: + yield QuicStartClientHook(tls_data) + else: + yield QuicStartServerHook(tls_data) + if not tls_data.settings: + yield commands.Log( + f"No QUIC context was provided, failing connection.", ERROR + ) + yield commands.CloseConnection(self.conn) + return + + # build the aioquic connection + configuration = tls_settings_to_configuration( + settings=tls_data.settings, + is_client=self.conn is self.context.server, + server_name=self.conn.sni, + ) + self.quic = QuicConnection( + configuration=configuration, + original_destination_connection_id=original_destination_connection_id, + ) + self.tls = tls_data.settings + + # if we act as client, connect to upstream + if original_destination_connection_id is None: + self.quic.connect(self.conn.peername, now=self._time()) + yield from self.tls_interact() + + def tls_interact(self) -> layer.CommandGenerator[None]: + """Retrieves all pending outgoing packets from aioquic and sends the data.""" + + # send all queued datagrams + assert self.quic + now = self._time() + + for data, addr in self.quic.datagrams_to_send(now=now): + assert addr == self.conn.peername + yield commands.SendData(self.tunnel_connection, data) + + timer = self.quic.get_timer() + if timer is not None: + # smooth wakeups a bit. + smoothed = timer + 0.002 + # request a new wakeup if all pending requests trigger at a later time + if not any( + existing <= smoothed for existing in self._wakeup_commands.values() + ): + command = commands.RequestWakeup(timer - now) + self._wakeup_commands[command] = timer + yield command + + def receive_handshake_data( + self, data: bytes + ) -> layer.CommandGenerator[tuple[bool, str | None]]: + assert self.quic + + # forward incoming data to aioquic + if data: + self.quic.receive_datagram(data, self.conn.peername, now=self._time()) + + # handle pre-handshake events + while event := self.quic.next_event(): + if isinstance(event, quic_events.ConnectionTerminated): + err = event.reason_phrase or error_code_to_str(event.error_code) + return False, err + elif isinstance(event, quic_events.HandshakeCompleted): + # concatenate all peer certificates + all_certs: list[x509.Certificate] = [] + if self.quic.tls._peer_certificate: + all_certs.append(self.quic.tls._peer_certificate) + all_certs.extend(self.quic.tls._peer_certificate_chain) + + # set the connection's TLS properties + self.conn.timestamp_tls_setup = time.time() + if event.alpn_protocol: + self.conn.alpn = event.alpn_protocol.encode("ascii") + self.conn.certificate_list = [certs.Cert(cert) for cert in all_certs] + assert self.quic.tls.key_schedule + self.conn.cipher = self.quic.tls.key_schedule.cipher_suite.name + self.conn.tls_version = "QUICv1" + + # log the result and report the success to addons + if self.debug: + yield commands.Log( + f"{self.debug}[quic] tls established: {self.conn}", DEBUG + ) + if self.conn is self.context.client: + yield TlsEstablishedClientHook( + QuicTlsData(self.conn, self.context, settings=self.tls) + ) + else: + yield TlsEstablishedServerHook( + QuicTlsData(self.conn, self.context, settings=self.tls) + ) + + yield from self.tls_interact() + return True, None + elif isinstance( + event, + ( + quic_events.ConnectionIdIssued, + quic_events.ConnectionIdRetired, + quic_events.PingAcknowledged, + quic_events.ProtocolNegotiated, + ), + ): + pass + else: + raise AssertionError(f"Unexpected event: {event!r}") + + # transmit buffered data and re-arm timer + yield from self.tls_interact() + return False, None + + def on_handshake_error(self, err: str) -> layer.CommandGenerator[None]: + self.conn.error = err + if self.conn is self.context.client: + yield TlsFailedClientHook( + QuicTlsData(self.conn, self.context, settings=self.tls) + ) + else: + yield TlsFailedServerHook( + QuicTlsData(self.conn, self.context, settings=self.tls) + ) + yield from super().on_handshake_error(err) + + def receive_data(self, data: bytes) -> layer.CommandGenerator[None]: + assert self.quic + + # forward incoming data to aioquic + if data: + self.quic.receive_datagram(data, self.conn.peername, now=self._time()) + + # handle post-handshake events + while event := self.quic.next_event(): + if isinstance(event, quic_events.ConnectionTerminated): + if self.debug: + reason = event.reason_phrase or error_code_to_str(event.error_code) + yield commands.Log( + f"{self.debug}[quic] close_notify {self.conn} (reason={reason})", + DEBUG, + ) + # We don't rely on `ConnectionTerminated` to dispatch `QuicConnectionClosed`, because + # after aioquic receives a termination frame, it still waits for the next `handle_timer` + # before returning `ConnectionTerminated` in `next_event`. In the meantime, the underlying + # connection could be closed. Therefore, we instead dispatch on `ConnectionClosed` and simply + # close the connection here. + yield commands.CloseConnection(self.tunnel_connection) + return # we don't handle any further events, nor do/can we transmit data, so exit + elif isinstance(event, quic_events.DatagramFrameReceived): + yield from self.event_to_child( + events.DataReceived(self.conn, event.data) + ) + elif isinstance(event, quic_events.StreamDataReceived): + yield from self.event_to_child( + QuicStreamDataReceived( + self.conn, event.stream_id, event.data, event.end_stream + ) + ) + elif isinstance(event, quic_events.StreamReset): + yield from self.event_to_child( + QuicStreamReset(self.conn, event.stream_id, event.error_code) + ) + elif isinstance(event, quic_events.StopSendingReceived): + yield from self.event_to_child( + QuicStreamStopSending(self.conn, event.stream_id, event.error_code) + ) + elif isinstance( + event, + ( + quic_events.ConnectionIdIssued, + quic_events.ConnectionIdRetired, + quic_events.PingAcknowledged, + quic_events.ProtocolNegotiated, + ), + ): + pass + else: + raise AssertionError(f"Unexpected event: {event!r}") + + # transmit buffered data and re-arm timer + yield from self.tls_interact() + + def receive_close(self) -> layer.CommandGenerator[None]: + assert self.quic + # if `_close_event` is not set, the underlying connection has been closed + # we turn this into a QUIC close event as well + close_event = self.quic._close_event or quic_events.ConnectionTerminated( + QuicErrorCode.NO_ERROR, None, "Connection closed." + ) + yield from self.event_to_child( + QuicConnectionClosed( + self.conn, + close_event.error_code, + close_event.frame_type, + close_event.reason_phrase, + ) + ) + + def send_data(self, data: bytes) -> layer.CommandGenerator[None]: + # non-stream data uses datagram frames + assert self.quic + if data: + self.quic.send_datagram_frame(data) + yield from self.tls_interact() + + def send_close( + self, command: commands.CloseConnection + ) -> layer.CommandGenerator[None]: + # properly close the QUIC connection + if self.quic: + if isinstance(command, CloseQuicConnection): + self.quic.close( + command.error_code, command.frame_type, command.reason_phrase + ) + else: + self.quic.close() + yield from self.tls_interact() + yield from super().send_close(command) + + +class ServerQuicLayer(QuicLayer): + """ + This layer establishes QUIC for a single server connection. + """ + + wait_for_clienthello: bool = False + + def __init__( + self, + context: context.Context, + conn: connection.Server | None = None, + time: Callable[[], float] | None = None, + ): + super().__init__(context, conn or context.server, time) + + def start_handshake(self) -> layer.CommandGenerator[None]: + wait_for_clienthello = not self.command_to_reply_to and isinstance( + self.child_layer, ClientQuicLayer + ) + if wait_for_clienthello: + self.wait_for_clienthello = True + self.tunnel_state = tunnel.TunnelState.CLOSED + else: + yield from self.start_tls(None) + + def event_to_child(self, event: events.Event) -> layer.CommandGenerator[None]: + if self.wait_for_clienthello: + for command in super().event_to_child(event): + if ( + isinstance(command, commands.OpenConnection) + and command.connection == self.conn + ): + self.wait_for_clienthello = False + else: + yield command + else: + yield from super().event_to_child(event) + + def on_handshake_error(self, err: str) -> layer.CommandGenerator[None]: + yield commands.Log(f"Server QUIC handshake failed. {err}", level=WARNING) + yield from super().on_handshake_error(err) + + +class ClientQuicLayer(QuicLayer): + """ + This layer establishes QUIC on a single client connection. + """ + + server_tls_available: bool + """Indicates whether the parent layer is a ServerQuicLayer.""" + handshake_datagram_buf: list[bytes] + + def __init__( + self, context: context.Context, time: Callable[[], float] | None = None + ) -> None: + # same as ClientTLSLayer, we might be nested in some other transport + if context.client.tls: + context.client.alpn = None + context.client.cipher = None + context.client.sni = None + context.client.timestamp_tls_setup = None + context.client.tls_version = None + context.client.certificate_list = [] + context.client.mitmcert = None + context.client.alpn_offers = [] + context.client.cipher_list = [] + + super().__init__(context, context.client, time) + self.server_tls_available = len(self.context.layers) >= 2 and isinstance( + self.context.layers[-2], ServerQuicLayer + ) + self.handshake_datagram_buf = [] + + def start_handshake(self) -> layer.CommandGenerator[None]: + yield from () + + def receive_handshake_data( + self, data: bytes + ) -> layer.CommandGenerator[tuple[bool, str | None]]: + if not self.context.options.http3: + yield commands.Log( + f"Swallowing QUIC handshake because HTTP/3 is disabled.", DEBUG + ) + return False, None + + # if we already had a valid client hello, don't process further packets + if self.tls: + return (yield from super().receive_handshake_data(data)) + + # fail if the received data is not a QUIC packet + buffer = QuicBuffer(data=data) + try: + header = pull_quic_header(buffer) + except TypeError: + return False, f"Cannot parse QUIC header: Malformed head ({data.hex()})" + except ValueError as e: + return False, f"Cannot parse QUIC header: {e} ({data.hex()})" + + # negotiate version, support all versions known to aioquic + if ( + header.version is not None + and header.version not in SUPPORTED_QUIC_VERSIONS_SERVER + ): + yield commands.SendData( + self.tunnel_connection, + encode_quic_version_negotiation( + source_cid=header.destination_cid, + destination_cid=header.source_cid, + supported_versions=SUPPORTED_QUIC_VERSIONS_SERVER, + ), + ) + return False, None + + # ensure it's (likely) a client handshake packet + if len(data) < 1200 or header.packet_type != PACKET_TYPE_INITIAL: + return ( + False, + f"Invalid handshake received, roaming not supported. ({data.hex()})", + ) + + self.handshake_datagram_buf.append(data) + # extract the client hello + try: + client_hello = quic_parse_client_hello_from_datagrams( + self.handshake_datagram_buf + ) + except ValueError as e: + msgs = b"\n".join(self.handshake_datagram_buf) + dbg = f"Cannot parse ClientHello: {str(e)} ({msgs.hex()})" + self.handshake_datagram_buf.clear() + return False, dbg + + if not client_hello: + return False, None + + # copy the client hello information + self.conn.sni = client_hello.sni + self.conn.alpn_offers = client_hello.alpn_protocols + + # check with addons what we shall do + tls_clienthello = ClientHelloData(self.context, client_hello) + yield TlsClienthelloHook(tls_clienthello) + + # replace the QUIC layer with an UDP layer if requested + if tls_clienthello.ignore_connection: + self.conn = self.tunnel_connection = connection.Client( + peername=("ignore-conn", 0), + sockname=("ignore-conn", 0), + transport_protocol="udp", + state=connection.ConnectionState.OPEN, + ) + + # we need to replace the server layer as well, if there is one + parent_layer = self.context.layers[self.context.layers.index(self) - 1] + if isinstance(parent_layer, ServerQuicLayer): + parent_layer.conn = parent_layer.tunnel_connection = connection.Server( + address=None + ) + replacement_layer = UDPLayer(self.context, ignore=True) + parent_layer.handle_event = replacement_layer.handle_event # type: ignore + parent_layer._handle_event = replacement_layer._handle_event # type: ignore + yield from parent_layer.handle_event(events.Start()) + for dgm in self.handshake_datagram_buf: + yield from parent_layer.handle_event( + events.DataReceived(self.context.client, dgm) + ) + self.handshake_datagram_buf.clear() + return True, None + + # start the server QUIC connection if demanded and available + if ( + tls_clienthello.establish_server_tls_first + and not self.context.server.tls_established + ): + err = yield from self.start_server_tls() + if err: + yield commands.Log( + f"Unable to establish QUIC connection with server ({err}). " + f"Trying to establish QUIC with client anyway. " + f"If you plan to redirect requests away from this server, " + f"consider setting `connection_strategy` to `lazy` to suppress early connections." + ) + + # start the client QUIC connection + yield from self.start_tls(header.destination_cid) + # XXX copied from TLS, we assume that `CloseConnection` in `start_tls` takes effect immediately + if not self.conn.connected: + return False, "connection closed early" + + # send the client hello to aioquic + assert self.quic + for dgm in self.handshake_datagram_buf: + self.quic.receive_datagram(dgm, self.conn.peername, now=self._time()) + self.handshake_datagram_buf.clear() + + # handle events emanating from `self.quic` + return (yield from super().receive_handshake_data(b"")) + + def start_server_tls(self) -> layer.CommandGenerator[str | None]: + if not self.server_tls_available: + return f"No server QUIC available." + err = yield commands.OpenConnection(self.context.server) + return err + + def on_handshake_error(self, err: str) -> layer.CommandGenerator[None]: + yield commands.Log(f"Client QUIC handshake failed. {err}", level=WARNING) + yield from super().on_handshake_error(err) + self.event_to_child = self.errored # type: ignore + + def errored(self, event: events.Event) -> layer.CommandGenerator[None]: + if self.debug is not None: + yield commands.Log( + f"{self.debug}[quic] Swallowing {event} as handshake failed.", DEBUG + ) + + +class QuicSecretsLogger: + logger: tls.MasterSecretLogger + + def __init__(self, logger: tls.MasterSecretLogger) -> None: + super().__init__() + self.logger = logger + + def write(self, s: str) -> int: + if s[-1:] == "\n": + s = s[:-1] + data = s.encode("ascii") + self.logger(None, data) # type: ignore + return len(data) + 1 + + def flush(self) -> None: + # done by the logger during write + pass + + +def error_code_to_str(error_code: int) -> str: + """Returns the corresponding name of the given error code or a string containing its numeric value.""" + + try: + return H3ErrorCode(error_code).name + except ValueError: + try: + return QuicErrorCode(error_code).name + except ValueError: + return f"unknown error (0x{error_code:x})" + + +def is_success_error_code(error_code: int) -> bool: + """Returns whether the given error code actually indicates no error.""" + + return error_code in (QuicErrorCode.NO_ERROR, H3ErrorCode.H3_NO_ERROR) + + +def tls_settings_to_configuration( + settings: QuicTlsSettings, + is_client: bool, + server_name: str | None = None, +) -> QuicConfiguration: + """Converts `QuicTlsSettings` to `QuicConfiguration`.""" + + return QuicConfiguration( + alpn_protocols=settings.alpn_protocols, + is_client=is_client, + secrets_log_file=( + QuicSecretsLogger(tls.log_master_secret) # type: ignore + if tls.log_master_secret is not None + else None + ), + server_name=server_name, + cafile=settings.ca_file, + capath=settings.ca_path, + certificate=settings.certificate, + certificate_chain=settings.certificate_chain, + cipher_suites=settings.cipher_suites, + private_key=settings.certificate_private_key, + verify_mode=settings.verify_mode, + max_datagram_frame_size=65536, + ) diff --git a/mitmproxy/proxy/layers/tcp.py b/mitmproxy/proxy/layers/tcp.py index 296adb80b3..f0dfc47a19 100644 --- a/mitmproxy/proxy/layers/tcp.py +++ b/mitmproxy/proxy/layers/tcp.py @@ -1,10 +1,13 @@ from dataclasses import dataclass -from typing import Optional -from mitmproxy import flow, tcp -from mitmproxy.proxy import commands, events, layer +from mitmproxy import flow +from mitmproxy import tcp +from mitmproxy.connection import Connection +from mitmproxy.connection import ConnectionState +from mitmproxy.proxy import commands +from mitmproxy.proxy import events +from mitmproxy.proxy import layer from mitmproxy.proxy.commands import StartHook -from mitmproxy.connection import ConnectionState, Connection from mitmproxy.proxy.context import Context from mitmproxy.proxy.events import MessageInjected from mitmproxy.proxy.utils import expect @@ -60,7 +63,7 @@ class TCPLayer(layer.Layer): Simple TCP layer that just relays messages right now. """ - flow: Optional[tcp.TCPFlow] + flow: tcp.TCPFlow | None def __init__(self, context: Context, ignore: bool = False): super().__init__(context) @@ -89,7 +92,6 @@ def start(self, _) -> layer.CommandGenerator[None]: @expect(events.DataReceived, events.ConnectionClosed, TcpMessageInjected) def relay_messages(self, event: events.Event) -> layer.CommandGenerator[None]: - if isinstance(event, TcpMessageInjected): # we just spoof that we received data here and then process that regularly. event = events.DataReceived( @@ -123,16 +125,16 @@ def relay_messages(self, event: events.Event) -> layer.CommandGenerator[None]: or (self.context.server.state & ConnectionState.CAN_READ) ) if all_done: + self._handle_event = self.done if self.context.server.state is not ConnectionState.CLOSED: yield commands.CloseConnection(self.context.server) if self.context.client.state is not ConnectionState.CLOSED: yield commands.CloseConnection(self.context.client) - self._handle_event = self.done if self.flow: yield TcpEndHook(self.flow) self.flow.live = False else: - yield commands.CloseConnection(send_to, half_close=True) + yield commands.CloseTcpConnection(send_to, half_close=True) else: raise AssertionError(f"Unexpected event: {event}") diff --git a/mitmproxy/proxy/layers/tls.py b/mitmproxy/proxy/layers/tls.py index 05307d6e12..bf253fbda9 100644 --- a/mitmproxy/proxy/layers/tls.py +++ b/mitmproxy/proxy/layers/tls.py @@ -1,33 +1,34 @@ import struct -from logging import DEBUG, ERROR, INFO, WARNING - import time +import typing +from collections.abc import Iterator from dataclasses import dataclass -from typing import Iterator, Optional +from logging import DEBUG +from logging import ERROR +from logging import INFO +from logging import WARNING from OpenSSL import SSL -from mitmproxy import certs, connection -from mitmproxy.proxy import commands, events, layer, tunnel +from mitmproxy import certs +from mitmproxy import connection +from mitmproxy.connection import TlsVersion +from mitmproxy.net.tls import starts_like_dtls_record +from mitmproxy.net.tls import starts_like_tls_record +from mitmproxy.proxy import commands from mitmproxy.proxy import context +from mitmproxy.proxy import events +from mitmproxy.proxy import layer +from mitmproxy.proxy import tunnel from mitmproxy.proxy.commands import StartHook -from mitmproxy.proxy.layers import tcp, udp -from mitmproxy.tls import ClientHello, ClientHelloData, TlsData +from mitmproxy.proxy.layers import tcp +from mitmproxy.proxy.layers import udp +from mitmproxy.tls import ClientHello +from mitmproxy.tls import ClientHelloData +from mitmproxy.tls import TlsData from mitmproxy.utils import human -def is_tls_handshake_record(d: bytes) -> bool: - """ - Returns: - True, if the passed bytes start with the TLS record magic bytes - False, otherwise. - """ - # TLS ClientHello magic, works for SSLv3, TLSv1.0, TLSv1.1, TLSv1.2. - # TLS 1.3 mandates legacy_record_version to be 0x0301. - # http://www.moserware.com/2009/06/first-few-milliseconds-of-https.html#client-hello - return len(d) >= 3 and d[0] == 0x16 and d[1] == 0x03 and 0x0 <= d[2] <= 0x03 - - def handshake_record_contents(data: bytes) -> Iterator[bytes]: """ Returns a generator that yields the bytes contained in each handshake record. @@ -39,7 +40,7 @@ def handshake_record_contents(data: bytes) -> Iterator[bytes]: if len(data) < offset + 5: return record_header = data[offset : offset + 5] - if not is_tls_handshake_record(record_header): + if not starts_like_tls_record(record_header): raise ValueError(f"Expected TLS record, got {record_header!r} instead.") record_size = struct.unpack("!H", record_header[3:])[0] if record_size == 0: @@ -53,7 +54,7 @@ def handshake_record_contents(data: bytes) -> Iterator[bytes]: offset += record_size -def get_client_hello(data: bytes) -> Optional[bytes]: +def get_client_hello(data: bytes) -> bytes | None: """ Read all TLS records that contain the initial ClientHello. Returns the raw handshake packet bytes, without TLS record headers. @@ -68,7 +69,7 @@ def get_client_hello(data: bytes) -> Optional[bytes]: return None -def parse_client_hello(data: bytes) -> Optional[ClientHello]: +def parse_client_hello(data: bytes) -> ClientHello | None: """ Check if the supplied bytes contain a full ClientHello message, and if so, parse it. @@ -90,15 +91,6 @@ def parse_client_hello(data: bytes) -> Optional[ClientHello]: return None -def is_dtls_handshake_record(d: bytes) -> bool: - """ - Returns: - True, if the passed bytes start with the DTLS record magic bytes - False, otherwise. - """ - return len(d) >= 3 and d[0] == 0x16 and d[1] == 0xfe and d[2] == 0xfd - - def dtls_handshake_record_contents(data: bytes) -> Iterator[bytes]: """ Returns a generator that yields the bytes contained in each handshake record. @@ -111,7 +103,7 @@ def dtls_handshake_record_contents(data: bytes) -> Iterator[bytes]: if len(data) < offset + 13: return record_header = data[offset : offset + 13] - if not is_dtls_handshake_record(record_header): + if not starts_like_dtls_record(record_header): raise ValueError(f"Expected DTLS record, got {record_header!r} instead.") # Length fields starts at 11 record_size = struct.unpack("!H", record_header[11:])[0] @@ -126,7 +118,7 @@ def dtls_handshake_record_contents(data: bytes) -> Iterator[bytes]: offset += record_size -def get_dtls_client_hello(data: bytes) -> Optional[bytes]: +def get_dtls_client_hello(data: bytes) -> bytes | None: """ Read all DTLS records that contain the initial ClientHello. Returns the raw handshake packet bytes, without TLS record headers. @@ -136,13 +128,15 @@ def get_dtls_client_hello(data: bytes) -> Optional[bytes]: client_hello += d if len(client_hello) >= 13: # comment about slicing: we skip the epoch and sequence number - client_hello_size = struct.unpack("!I", b"\x00" + client_hello[9:12])[0] + 12 + client_hello_size = ( + struct.unpack("!I", b"\x00" + client_hello[9:12])[0] + 12 + ) if len(client_hello) >= client_hello_size: return client_hello[:client_hello_size] return None -def dtls_parse_client_hello(data: bytes) -> Optional[ClientHello]: +def dtls_parse_client_hello(data: bytes) -> ClientHello | None: """ Check if the supplied bytes contain a full ClientHello message, and if so, parse it. @@ -165,7 +159,9 @@ def dtls_parse_client_hello(data: bytes) -> Optional[ClientHello]: HTTP1_ALPNS = (b"http/1.1", b"http/1.0", b"http/0.9") -HTTP_ALPNS = (b"h2",) + HTTP1_ALPNS +HTTP2_ALPN = b"h2" +HTTP3_ALPN = b"h3" +HTTP_ALPNS = (HTTP3_ALPN, HTTP2_ALPN, *HTTP1_ALPNS) # We need these classes as hooks can only have one argument at the moment. @@ -257,7 +253,9 @@ def __init__(self, context: context.Context, conn: connection.Connection): conn.tls = True def __repr__(self): - return super().__repr__().replace(")", f" {self.conn.sni} {self.conn.alpn})") + return ( + super().__repr__().replace(")", f" {self.conn.sni!r} {self.conn.alpn!r})") + ) @property def is_dtls(self): @@ -265,7 +263,7 @@ def is_dtls(self): @property def proto_name(self): - return 'DTLS' if self.is_dtls else 'TLS' + return "DTLS" if self.is_dtls else "TLS" def start_tls(self) -> layer.CommandGenerator[None]: assert not self.tls @@ -295,7 +293,7 @@ def tls_interact(self) -> layer.CommandGenerator[None]: def receive_handshake_data( self, data: bytes - ) -> layer.CommandGenerator[tuple[bool, Optional[str]]]: + ) -> layer.CommandGenerator[tuple[bool, str | None]]: # bio_write errors for b"", so we need to check first if we actually received something. if data: self.tls.bio_write(data) @@ -318,13 +316,17 @@ def receive_handshake_data( ("SSL routines", "", "certificate verify failed"), # OpenSSL 3+ ]: verify_result = SSL._lib.SSL_get_verify_result(self.tls._ssl) # type: ignore - error = SSL._ffi.string(SSL._lib.X509_verify_cert_error_string(verify_result)).decode() # type: ignore + error = SSL._ffi.string( # type: ignore + SSL._lib.X509_verify_cert_error_string(verify_result) # type: ignore + ).decode() err = f"Certificate verify failed: {error}" elif last_err in [ ("SSL routines", "ssl3_read_bytes", "tlsv1 alert unknown ca"), ("SSL routines", "ssl3_read_bytes", "sslv3 alert bad certificate"), + ("SSL routines", "ssl3_read_bytes", "ssl/tls alert bad certificate"), ("SSL routines", "", "tlsv1 alert unknown ca"), # OpenSSL 3+ ("SSL routines", "", "sslv3 alert bad certificate"), # OpenSSL 3+ + ("SSL routines", "", "ssl/tls alert bad certificate"), # OpenSSL 3.2+ ]: assert isinstance(last_err, tuple) err = last_err[2] @@ -333,6 +335,8 @@ def receive_handshake_data( in [ ("SSL routines", "ssl3_get_record", "wrong version number"), ("SSL routines", "", "wrong version number"), # OpenSSL 3+ + ("SSL routines", "", "packet length too long"), # OpenSSL 3+ + ("SSL routines", "", "record layer failure"), # OpenSSL 3+ ] and data[:4].isascii() ): @@ -360,14 +364,24 @@ def receive_handshake_data( cert = self.tls.get_peer_certificate() if cert: all_certs.insert(0, cert) + self.conn.certificate_list = [] + for cert in all_certs: + try: + # This may fail for weird certs, https://github.com/mitmproxy/mitmproxy/issues/6968. + parsed_cert = certs.Cert.from_pyopenssl(cert) + except ValueError as e: + yield commands.Log( + f"{self.debug}[tls] failed to parse certificate: {e}", WARNING + ) + else: + self.conn.certificate_list.append(parsed_cert) self.conn.timestamp_tls_setup = time.time() self.conn.alpn = self.tls.get_alpn_proto_negotiated() - self.conn.certificate_list = [ - certs.Cert.from_pyopenssl(x) for x in all_certs - ] self.conn.cipher = self.tls.get_cipher_name() - self.conn.tls_version = self.tls.get_protocol_version_name() + self.conn.tls_version = typing.cast( + TlsVersion, self.tls.get_protocol_version_name() + ) if self.debug: yield commands.Log( f"{self.debug}[tls] tls established: {self.conn}", DEBUG @@ -421,9 +435,7 @@ def receive_data(self, data: bytes) -> layer.CommandGenerator[None]: if close: self.conn.state &= ~connection.ConnectionState.CAN_READ if self.debug: - yield commands.Log( - f"{self.debug}[tls] close_notify {self.conn}", DEBUG - ) + yield commands.Log(f"{self.debug}[tls] close_notify {self.conn}", DEBUG) yield from self.event_to_child(events.ConnectionClosed(self.conn)) def receive_close(self) -> layer.CommandGenerator[None]: @@ -440,9 +452,11 @@ def send_data(self, data: bytes) -> layer.CommandGenerator[None]: pass yield from self.tls_interact() - def send_close(self, half_close: bool) -> layer.CommandGenerator[None]: + def send_close( + self, command: commands.CloseConnection + ) -> layer.CommandGenerator[None]: # We should probably shutdown the TLS connection properly here. - yield from super().send_close(half_close) + yield from super().send_close(command) class ServerTLSLayer(TLSLayer): @@ -452,9 +466,7 @@ class ServerTLSLayer(TLSLayer): wait_for_clienthello: bool = False - def __init__( - self, context: context.Context, conn: Optional[connection.Server] = None - ): + def __init__(self, context: context.Context, conn: connection.Server | None = None): super().__init__(context, conn or context.server) def start_handshake(self) -> layer.CommandGenerator[None]: @@ -544,7 +556,7 @@ def start_handshake(self) -> layer.CommandGenerator[None]: def receive_handshake_data( self, data: bytes - ) -> layer.CommandGenerator[tuple[bool, Optional[str]]]: + ) -> layer.CommandGenerator[tuple[bool, str | None]]: if self.client_hello_parsed: return (yield from super().receive_handshake_data(data)) self.recv_buffer.extend(data) @@ -570,12 +582,12 @@ def receive_handshake_data( # we've figured out that we don't want to intercept this connection, so we assign fake connection objects # to all TLS layers. This makes the real connection contents just go through. self.conn = self.tunnel_connection = connection.Client( - ("ignore-conn", 0), ("ignore-conn", 0), time.time() + peername=("ignore-conn", 0), sockname=("ignore-conn", 0) ) parent_layer = self.context.layers[self.context.layers.index(self) - 1] if isinstance(parent_layer, ServerTLSLayer): parent_layer.conn = parent_layer.tunnel_connection = connection.Server( - None + address=None ) if self.is_dtls: self.child_layer = udp.UDPLayer(self.context, ignore=True) @@ -607,7 +619,7 @@ def receive_handshake_data( self.recv_buffer.clear() return ret - def start_server_tls(self) -> layer.CommandGenerator[Optional[str]]: + def start_server_tls(self) -> layer.CommandGenerator[str | None]: """ We often need information from the upstream connection to establish TLS with the client. For example, we need to check if the client does ALPN or not. @@ -659,7 +671,9 @@ def on_handshake_error(self, err: str) -> layer.CommandGenerator[None]: def errored(self, event: events.Event) -> layer.CommandGenerator[None]: if self.debug is not None: - yield commands.Log(f"Swallowing {event} as handshake failed.", DEBUG) + yield commands.Log( + f"{self.debug}[tls] Swallowing {event} as handshake failed.", DEBUG + ) class MockTLSLayer(TLSLayer): @@ -670,4 +684,4 @@ class MockTLSLayer(TLSLayer): """ def __init__(self, ctx: context.Context): - super().__init__(ctx, connection.Server(None)) + super().__init__(ctx, connection.Server(address=None)) diff --git a/mitmproxy/proxy/layers/udp.py b/mitmproxy/proxy/layers/udp.py index 3026a71cea..e7a889c847 100644 --- a/mitmproxy/proxy/layers/udp.py +++ b/mitmproxy/proxy/layers/udp.py @@ -1,10 +1,12 @@ from dataclasses import dataclass -from typing import Optional -from mitmproxy import flow, udp -from mitmproxy.proxy import commands, events, layer -from mitmproxy.proxy.commands import StartHook +from mitmproxy import flow +from mitmproxy import udp from mitmproxy.connection import Connection +from mitmproxy.proxy import commands +from mitmproxy.proxy import events +from mitmproxy.proxy import layer +from mitmproxy.proxy.commands import StartHook from mitmproxy.proxy.context import Context from mitmproxy.proxy.events import MessageInjected from mitmproxy.proxy.utils import expect @@ -60,7 +62,7 @@ class UDPLayer(layer.Layer): Simple UDP layer that just relays messages right now. """ - flow: Optional[udp.UDPFlow] + flow: udp.UDPFlow | None def __init__(self, context: Context, ignore: bool = False): super().__init__(context) @@ -89,7 +91,6 @@ def start(self, _) -> layer.CommandGenerator[None]: @expect(events.DataReceived, events.ConnectionClosed, UdpMessageInjected) def relay_messages(self, event: events.Event) -> layer.CommandGenerator[None]: - if isinstance(event, UdpMessageInjected): # we just spoof that we received data here and then process that regularly. event = events.DataReceived( @@ -118,13 +119,11 @@ def relay_messages(self, event: events.Event) -> layer.CommandGenerator[None]: yield commands.SendData(send_to, event.data) elif isinstance(event, events.ConnectionClosed): - if send_to.connected: - yield commands.CloseConnection(send_to) - else: - self._handle_event = self.done - if self.flow: - yield UdpEndHook(self.flow) - self.flow.live = False + self._handle_event = self.done + yield commands.CloseConnection(send_to) + if self.flow: + yield UdpEndHook(self.flow) + self.flow.live = False else: raise AssertionError(f"Unexpected event: {event}") diff --git a/mitmproxy/proxy/layers/websocket.py b/mitmproxy/proxy/layers/websocket.py index a2c57fee91..85b63b4bdb 100644 --- a/mitmproxy/proxy/layers/websocket.py +++ b/mitmproxy/proxy/layers/websocket.py @@ -1,19 +1,23 @@ import time +from collections.abc import Iterator from dataclasses import dataclass -from typing import Iterator -import wsproto import wsproto.extensions import wsproto.frame_protocol import wsproto.utilities -from mitmproxy import connection, http, websocket -from mitmproxy.proxy import commands, events, layer +from wsproto import ConnectionState +from wsproto.frame_protocol import Opcode + +from mitmproxy import connection +from mitmproxy import http +from mitmproxy import websocket +from mitmproxy.proxy import commands +from mitmproxy.proxy import events +from mitmproxy.proxy import layer from mitmproxy.proxy.commands import StartHook from mitmproxy.proxy.context import Context from mitmproxy.proxy.events import MessageInjected from mitmproxy.proxy.utils import expect -from wsproto import ConnectionState -from wsproto.frame_protocol import Opcode @dataclass @@ -93,7 +97,6 @@ def __init__(self, context: Context, flow: http.HTTPFlow): @expect(events.Start) def start(self, _) -> layer.CommandGenerator[None]: - client_extensions = [] server_extensions = [] diff --git a/mitmproxy/proxy/mode_servers.py b/mitmproxy/proxy/mode_servers.py index ca00d4389c..12320bc323 100644 --- a/mitmproxy/proxy/mode_servers.py +++ b/mitmproxy/proxy/mode_servers.py @@ -9,32 +9,51 @@ await inst.start() # TCP server is running now. """ + from __future__ import annotations import asyncio +import errno import json import logging +import os import socket +import sys import textwrap import typing -from abc import ABCMeta, abstractmethod +from abc import ABCMeta +from abc import abstractmethod from contextlib import contextmanager from pathlib import Path -from typing import ClassVar, Generic, TypeVar, cast, get_args - -import errno -import mitmproxy_wireguard as wg - -from mitmproxy import ctx, flow, platform +from typing import cast +from typing import ClassVar +from typing import Generic +from typing import get_args +from typing import TYPE_CHECKING +from typing import TypeVar + +import mitmproxy_rs +from mitmproxy import ctx +from mitmproxy import flow +from mitmproxy import platform from mitmproxy.connection import Address -from mitmproxy.master import Master -from mitmproxy.net import local_ip, udp -from mitmproxy.net.udp_wireguard import WireGuardDatagramTransport -from mitmproxy.proxy import commands, layers, mode_specs, server +from mitmproxy.net import local_ip +from mitmproxy.proxy import commands +from mitmproxy.proxy import layers +from mitmproxy.proxy import mode_specs +from mitmproxy.proxy import server from mitmproxy.proxy.context import Context from mitmproxy.proxy.layer import Layer from mitmproxy.utils import human +if sys.version_info < (3, 11): + from typing_extensions import Self # pragma: no cover +else: + from typing import Self + +if TYPE_CHECKING: + from mitmproxy.master import Master + logger = logging.getLogger(__name__) @@ -55,33 +74,32 @@ async def handle_hook(self, hook: commands.StartHook) -> None: await data.wait_for_resume() # pragma: no cover -M = TypeVar('M', bound=mode_specs.ProxyMode) +M = TypeVar("M", bound=mode_specs.ProxyMode) class ServerManager(typing.Protocol): - connections: dict[tuple, ProxyConnectionHandler] + # temporary workaround: for UDP, we use the 4-tuple because we don't have a uuid. + connections: dict[tuple | str, ProxyConnectionHandler] @contextmanager - def register_connection(self, connection_id: tuple, handler: ProxyConnectionHandler): - ... # pragma: no cover - - -# Python 3.11: Use typing.Self -Self = TypeVar("Self", bound="ServerInstance") + def register_connection( + self, connection_id: tuple | str, handler: ProxyConnectionHandler + ): ... # pragma: no cover class ServerInstance(Generic[M], metaclass=ABCMeta): __modes: ClassVar[dict[str, type[ServerInstance]]] = {} + last_exception: Exception | None = None + def __init__(self, mode: M, manager: ServerManager): self.mode: M = mode self.manager: ServerManager = manager - self.last_exception: Exception | None = None def __init_subclass__(cls, **kwargs): """Register all subclasses so that make() finds them.""" # extract mode from Generic[Mode]. - mode = get_args(cls.__orig_bases__[0])[0] + mode = get_args(cls.__orig_bases__[0])[0] # type: ignore if not isinstance(mode, TypeVar): assert issubclass(mode, mode_specs.ProxyMode) assert mode.type_name not in ServerInstance.__modes @@ -89,7 +107,7 @@ def __init_subclass__(cls, **kwargs): @classmethod def make( - cls: typing.Type[Self], + cls, mode: mode_specs.ProxyMode | str, manager: ServerManager, ) -> Self: @@ -107,12 +125,41 @@ def make( def is_running(self) -> bool: pass - @abstractmethod async def start(self) -> None: + try: + await self._start() + except Exception as e: + self.last_exception = e + raise + else: + self.last_exception = None + if self.listen_addrs: + addrs = " and ".join({human.format_address(a) for a in self.listen_addrs}) + logger.info(f"{self.mode.description} listening at {addrs}.") + else: + logger.info(f"{self.mode.description} started.") + + async def stop(self) -> None: + listen_addrs = self.listen_addrs + try: + await self._stop() + except Exception as e: + self.last_exception = e + raise + else: + self.last_exception = None + if listen_addrs: + addrs = " and ".join({human.format_address(a) for a in listen_addrs}) + logger.info(f"{self.mode.description} at {addrs} stopped.") + else: + logger.info(f"{self.mode.description} stopped.") + + @abstractmethod + async def _start(self) -> None: pass @abstractmethod - async def stop(self) -> None: + async def _stop(self) -> None: pass @property @@ -134,123 +181,102 @@ def to_json(self) -> dict: "listen_addrs": self.listen_addrs, } - async def handle_tcp_connection( + async def handle_stream( self, - reader: asyncio.StreamReader | wg.TcpStream, - writer: asyncio.StreamWriter | wg.TcpStream, + reader: asyncio.StreamReader | mitmproxy_rs.Stream, + writer: asyncio.StreamWriter | mitmproxy_rs.Stream | None = None, ) -> None: + if writer is None: + assert isinstance(reader, mitmproxy_rs.Stream) + writer = reader handler = ProxyConnectionHandler( ctx.master, reader, writer, ctx.options, self.mode ) handler.layer = self.make_top_layer(handler.layer.context) if isinstance(self.mode, mode_specs.TransparentMode): + assert isinstance(writer, asyncio.StreamWriter) s = cast(socket.socket, writer.get_extra_info("socket")) try: assert platform.original_addr original_dst = platform.original_addr(s) except Exception as e: logger.error(f"Transparent mode failure: {e!r}") + writer.close() return else: handler.layer.context.client.sockname = original_dst handler.layer.context.server.address = original_dst - elif isinstance(self.mode, mode_specs.WireGuardMode): - original_dst = writer.get_extra_info("original_dst") - handler.layer.context.client.sockname = original_dst - handler.layer.context.server.address = original_dst - - connection_id = ( - handler.layer.context.client.transport_protocol, - handler.layer.context.client.peername, - handler.layer.context.client.sockname, - ) - with self.manager.register_connection(connection_id, handler): - await handler.handle_client() - - def handle_udp_datagram( - self, - transport: asyncio.DatagramTransport, - data: bytes, - remote_addr: Address, - local_addr: Address, - ) -> None: - connection_id = ("udp", remote_addr, local_addr) - if connection_id not in self.manager.connections: - reader = udp.DatagramReader() - writer = udp.DatagramWriter(transport, remote_addr, reader) - handler = ProxyConnectionHandler( - ctx.master, reader, writer, ctx.options, self.mode + elif isinstance( + self.mode, + (mode_specs.WireGuardMode, mode_specs.LocalMode, mode_specs.TunMode), + ): # pragma: no cover on platforms without wg-test-client + handler.layer.context.server.address = writer.get_extra_info( + "remote_endpoint", handler.layer.context.client.sockname ) - handler.timeout_watchdog.CONNECTION_TIMEOUT = 20 - handler.layer = self.make_top_layer(handler.layer.context) - handler.layer.context.client.transport_protocol = "udp" - handler.layer.context.server.transport_protocol = "udp" - if isinstance(self.mode, mode_specs.WireGuardMode): - handler.layer.context.server.address = local_addr - - # pre-register here - we may get datagrams before the task is executed. - self.manager.connections[connection_id] = handler - asyncio.create_task(self.handle_udp_connection(connection_id, handler)) - else: - handler = self.manager.connections[connection_id] - reader = cast(udp.DatagramReader, handler.transports[handler.client].reader) - reader.feed_data(data, remote_addr) - async def handle_udp_connection(self, connection_id: tuple, handler: ProxyConnectionHandler) -> None: - with self.manager.register_connection(connection_id, handler): + with self.manager.register_connection(handler.layer.context.client.id, handler): await handler.handle_client() class AsyncioServerInstance(ServerInstance[M], metaclass=ABCMeta): - _server: asyncio.Server | udp.UdpServer | None = None - _listen_addrs: tuple[Address, ...] = tuple() + _servers: list[asyncio.Server | mitmproxy_rs.udp.UdpServer] + + def __init__(self, *args, **kwargs) -> None: + self._servers = [] + super().__init__(*args, **kwargs) @property def is_running(self) -> bool: - return self._server is not None + return bool(self._servers) - async def start(self) -> None: - assert self._server is None + @property + def listen_addrs(self) -> tuple[Address, ...]: + addrs = [] + for s in self._servers: + if isinstance(s, mitmproxy_rs.udp.UdpServer): + addrs.append(s.getsockname()) + else: + try: + addrs.extend(sock.getsockname() for sock in s.sockets) + except OSError: # pragma: no cover + pass # this can fail during shutdown, see https://github.com/mitmproxy/mitmproxy/issues/6529 + return tuple(addrs) + + async def _start(self) -> None: + assert not self._servers host = self.mode.listen_host(ctx.options.listen_host) port = self.mode.listen_port(ctx.options.listen_port) + assert port is not None try: - self._server = await self.listen(host, port) - self._listen_addrs = tuple(s.getsockname() for s in self._server.sockets) + self._servers = await self.listen(host, port) except OSError as e: - self.last_exception = e message = f"{self.mode.description} failed to listen on {host or '*'}:{port} with {e}" if e.errno == errno.EADDRINUSE and self.mode.custom_listen_port is None: - assert self.mode.custom_listen_host is None # since [@ [listen_addr:]listen_port] - message += f"\nTry specifying a different port by using `--mode {self.mode.full_spec}@{port + 1}`." + assert ( + self.mode.custom_listen_host is None + ) # since [@ [listen_addr:]listen_port] + message += f"\nTry specifying a different port by using `--mode {self.mode.full_spec}@{port + 2}`." raise OSError(e.errno, message, e.filename) from e - except Exception as e: - self.last_exception = e - raise - else: - self.last_exception = None - addrs = " and ".join({human.format_address(a) for a in self._listen_addrs}) - logger.info(f"{self.mode.description} listening at {addrs}.") - async def stop(self) -> None: - assert self._server is not None - # we always reset _server and _listen_addrs and ignore failures - server = self._server - listen_addrs = self._listen_addrs - self._server = None - self._listen_addrs = tuple() + async def _stop(self) -> None: + assert self._servers try: - server.close() - await server.wait_closed() - except Exception as e: - self.last_exception = e - raise - else: - self.last_exception = None - addrs = " and ".join({human.format_address(a) for a in listen_addrs}) - logger.info(f"Stopped {self.mode.description} at {addrs}.") + for s in self._servers: + s.close() + # https://github.com/python/cpython/issues/104344 + # await asyncio.gather(*[s.wait_closed() for s in self._servers]) + finally: + # we always reset _server and ignore failures + self._servers = [] + + async def listen( + self, host: str, port: int + ) -> list[asyncio.Server | mitmproxy_rs.udp.UdpServer]: + if self.mode.transport_protocol not in ("tcp", "udp", "both"): + raise AssertionError(self.mode.transport_protocol) - async def listen(self, host: str, port: int) -> asyncio.Server | udp.UdpServer: - if self.mode.transport_protocol == "tcp": + servers: list[asyncio.Server | mitmproxy_rs.udp.UdpServer] = [] + if self.mode.transport_protocol in ("tcp", "both"): # workaround for https://github.com/python/cpython/issues/89856: # We want both IPv4 and IPv6 sockets to bind to the same port. # This may fail (https://github.com/mitmproxy/mitmproxy/pull/5542#issuecomment-1222803291), @@ -259,145 +285,208 @@ async def listen(self, host: str, port: int) -> asyncio.Server | udp.UdpServer: try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(("", 0)) - fixed_port = s.getsockname()[1] + port = s.getsockname()[1] s.close() - return await asyncio.start_server(self.handle_tcp_connection, host, fixed_port) + servers.append( + await asyncio.start_server(self.handle_stream, host, port) + ) except Exception as e: - logger.debug(f"Failed to listen on a single port ({e!r}), falling back to default behavior.") - return await asyncio.start_server(self.handle_tcp_connection, host, port) - elif self.mode.transport_protocol == "udp": - # create_datagram_endpoint only creates one socket, so the workaround above doesn't apply - # NOTE once we do dual servers, we should consider creating sockets manually to ensure - # both TCP and UDP listen to the same IPs and same ports - return await udp.start_server( - self.handle_udp_datagram, - host, - port, - ) - else: - raise AssertionError(self.mode.transport_protocol) + logger.debug( + f"Failed to listen on a single port ({e!r}), falling back to default behavior." + ) + port = 0 + servers.append( + await asyncio.start_server(self.handle_stream, host, port) + ) + else: + servers.append( + await asyncio.start_server(self.handle_stream, host, port) + ) + if self.mode.transport_protocol in ("udp", "both"): + # we start two servers for dual-stack support. + # On Linux, this would also be achievable by toggling IPV6_V6ONLY off, but this here works cross-platform. + if host == "": + ipv4 = await mitmproxy_rs.udp.start_udp_server( + "0.0.0.0", + port, + self.handle_stream, + ) + servers.append(ipv4) + try: + ipv6 = await mitmproxy_rs.udp.start_udp_server( + "[::]", + ipv4.getsockname()[1], + self.handle_stream, + ) + servers.append(ipv6) # pragma: no cover + except Exception: # pragma: no cover + logger.debug("Failed to listen on '::', listening on IPv4 only.") + else: + servers.append( + await mitmproxy_rs.udp.start_udp_server( + host, + port, + self.handle_stream, + ) + ) - @property - def listen_addrs(self) -> tuple[Address, ...]: - return self._listen_addrs + return servers class WireGuardServerInstance(ServerInstance[mode_specs.WireGuardMode]): - _server: wg.Server | None = None - _listen_addrs: tuple[Address, ...] = tuple() + _server: mitmproxy_rs.wireguard.WireGuardServer | None = None server_key: str client_key: str - def make_top_layer(self, context: Context) -> Layer: + def make_top_layer( + self, context: Context + ) -> Layer: # pragma: no cover on platforms without wg-test-client return layers.modes.TransparentProxy(context) @property def is_running(self) -> bool: return self._server is not None - async def start(self) -> None: + @property + def listen_addrs(self) -> tuple[Address, ...]: + if self._server: + return (self._server.getsockname(),) + else: + return tuple() + + async def _start(self) -> None: assert self._server is None host = self.mode.listen_host(ctx.options.listen_host) port = self.mode.listen_port(ctx.options.listen_port) + assert port is not None if self.mode.data: conf_path = Path(self.mode.data).expanduser() else: conf_path = Path(ctx.options.confdir).expanduser() / "wireguard.conf" - try: - if not conf_path.exists(): - conf_path.write_text(json.dumps({ - "server_key": wg.genkey(), - "client_key": wg.genkey(), - }, indent=4)) - - try: - c = json.loads(conf_path.read_text()) - self.server_key = c["server_key"] - self.client_key = c["client_key"] - except Exception as e: - raise ValueError(f"Invalid configuration file ({conf_path}): {e}") from e - # error early on invalid keys - p = wg.pubkey(self.client_key) - _ = wg.pubkey(self.server_key) - - self._server = await wg.start_server( - host, - port, - self.server_key, - [p], - self.wg_handle_tcp_connection, - self.wg_handle_udp_datagram, + if not conf_path.exists(): + conf_path.parent.mkdir(parents=True, exist_ok=True) + conf_path.write_text( + json.dumps( + { + "server_key": mitmproxy_rs.wireguard.genkey(), + "client_key": mitmproxy_rs.wireguard.genkey(), + }, + indent=4, + ) ) - self._listen_addrs = (self._server.getsockname(),) + + try: + c = json.loads(conf_path.read_text()) + self.server_key = c["server_key"] + self.client_key = c["client_key"] except Exception as e: - self.last_exception = e - message = f"{self.mode.description} failed to listen on {host or '*'}:{port} with {e}" - raise OSError(message) from e - else: - self.last_exception = None + raise ValueError(f"Invalid configuration file ({conf_path}): {e}") from e + # error early on invalid keys + p = mitmproxy_rs.wireguard.pubkey(self.client_key) + _ = mitmproxy_rs.wireguard.pubkey(self.server_key) + + self._server = await mitmproxy_rs.wireguard.start_wireguard_server( + host or "0.0.0.0", + port, + self.server_key, + [p], + self.handle_stream, + self.handle_stream, + ) - addrs = " and ".join({human.format_address(a) for a in self.listen_addrs}) conf = self.client_conf() assert conf - logger.info( - f"{self.mode.description} listening at {addrs}.\n" - + "------------------------------------------------------------\n" - + conf - + "\n------------------------------------------------------------" - ) + logger.info("-" * 60 + "\n" + conf + "\n" + "-" * 60) def client_conf(self) -> str | None: if not self._server: return None - host = local_ip.get_local_ip() or local_ip.get_local_ip6() + host = ( + self.mode.listen_host(ctx.options.listen_host) + or local_ip.get_local_ip() + or local_ip.get_local_ip6() + ) port = self.mode.listen_port(ctx.options.listen_port) - return textwrap.dedent(f""" + return textwrap.dedent( + f""" [Interface] PrivateKey = {self.client_key} Address = 10.0.0.1/32 DNS = 10.0.0.53 [Peer] - PublicKey = {wg.pubkey(self.server_key)} + PublicKey = {mitmproxy_rs.wireguard.pubkey(self.server_key)} AllowedIPs = 0.0.0.0/0 Endpoint = {host}:{port} - """).strip() + """ + ).strip() def to_json(self) -> dict: - return { - "wireguard_conf": self.client_conf(), - **super().to_json() - } + return {"wireguard_conf": self.client_conf(), **super().to_json()} - async def stop(self) -> None: + async def _stop(self) -> None: assert self._server is not None - self._server.close() - await self._server.wait_closed() - self._server = None - self.last_exception = None + try: + self._server.close() + await self._server.wait_closed() + finally: + self._server = None + - addrs = " and ".join({human.format_address(a) for a in self.listen_addrs}) - logger.info(f"Stopped {self.mode.description} at {addrs}.") +class LocalRedirectorInstance(ServerInstance[mode_specs.LocalMode]): + _server: ClassVar[mitmproxy_rs.local.LocalRedirector | None] = None + """The local redirector daemon. Will be started once and then reused for all future instances.""" + _instance: ClassVar[LocalRedirectorInstance | None] = None + """The current LocalRedirectorInstance. Will be unset again if an instance is stopped.""" + listen_addrs = () @property - def listen_addrs(self) -> tuple[Address, ...]: - return self._listen_addrs + def is_running(self) -> bool: + return self._instance is not None - async def wg_handle_tcp_connection(self, stream: wg.TcpStream) -> None: - await self.handle_tcp_connection(stream, stream) + def make_top_layer(self, context: Context) -> Layer: + return layers.modes.TransparentProxy(context) - def wg_handle_udp_datagram(self, data: bytes, remote_addr: Address, local_addr: Address) -> None: - assert self._server is not None - transport = WireGuardDatagramTransport(self._server, local_addr, remote_addr) - self.handle_udp_datagram( - transport, - data, - remote_addr, - local_addr - ) + @classmethod + async def redirector_handle_stream( + cls, + stream: mitmproxy_rs.Stream, + ) -> None: + if cls._instance is not None: + await cls._instance.handle_stream(stream) + + async def _start(self) -> None: + if self._instance: + raise RuntimeError("Cannot spawn more than one local redirector.") + + if self.mode.data: + spec = f"{self.mode.data},!{os.getpid()}" + else: + spec = f"!{os.getpid()}" + + cls = self.__class__ + cls._instance = self # assign before awaiting to avoid races + if cls._server is None: + try: + cls._server = await mitmproxy_rs.local.start_local_redirector( + cls.redirector_handle_stream, + cls.redirector_handle_stream, + ) + except Exception: + cls._instance = None + raise + + cls._server.set_intercept(spec) + + async def _stop(self) -> None: + assert self._instance + assert self._server + self.__class__._instance = None + # We're not shutting down the server because we want to avoid additional UAC prompts. + self._server.set_intercept("") class RegularInstance(AsyncioServerInstance[mode_specs.RegularMode]): @@ -428,3 +517,49 @@ def make_top_layer(self, context: Context) -> Layer: class DnsInstance(AsyncioServerInstance[mode_specs.DnsMode]): def make_top_layer(self, context: Context) -> Layer: return layers.DNSLayer(context) + + +class TunInstance(ServerInstance[mode_specs.TunMode]): + _server: mitmproxy_rs.tun.TunInterface | None = None + listen_addrs = () + + def make_top_layer( + self, context: Context + ) -> Layer: # pragma: no cover mocked in tests + return layers.modes.TransparentProxy(context) + + @property + def is_running(self) -> bool: + return self._server is not None + + @property + def tun_name(self) -> str | None: + if self._server: + return self._server.tun_name() + else: + return None + + def to_json(self) -> dict: + return {"tun_name": self.tun_name, **super().to_json()} + + async def _start(self) -> None: + assert self._server is None + self._server = await mitmproxy_rs.tun.create_tun_interface( + self.handle_stream, + self.handle_stream, + tun_name=self.mode.data or None, + ) + logger.info(f"TUN interface created: {self._server.tun_name()}") + + async def _stop(self) -> None: + assert self._server is not None + try: + self._server.close() + await self._server.wait_closed() + finally: + self._server = None + + +# class Http3Instance(AsyncioServerInstance[mode_specs.Http3Mode]): +# def make_top_layer(self, context: Context) -> Layer: +# return layers.modes.HttpProxy(context) diff --git a/mitmproxy/proxy/mode_specs.py b/mitmproxy/proxy/mode_specs.py index 1e34a44069..c1fa60feda 100644 --- a/mitmproxy/proxy/mode_specs.py +++ b/mitmproxy/proxy/mode_specs.py @@ -22,16 +22,25 @@ from __future__ import annotations -from abc import ABCMeta, abstractmethod +import dataclasses +import platform +import re +import sys +from abc import ABCMeta +from abc import abstractmethod from dataclasses import dataclass from functools import cache -from typing import ClassVar, Literal, Type, TypeVar +from typing import ClassVar +from typing import Literal +import mitmproxy_rs from mitmproxy.coretypes.serializable import Serializable from mitmproxy.net import server_spec -# Python 3.11: Use typing.Self -Self = TypeVar("Self", bound="ProxyMode") +if sys.version_info < (3, 11): + from typing_extensions import Self # pragma: no cover +else: + from typing import Self @dataclass(frozen=True) # type: ignore @@ -40,6 +49,7 @@ class ProxyMode(Serializable, metaclass=ABCMeta): Parsed representation of a proxy mode spec. Subclassed for each specific mode, which then does its own data validation. """ + full_spec: str """The full proxy mode spec as entered by the user.""" data: str @@ -49,9 +59,11 @@ class ProxyMode(Serializable, metaclass=ABCMeta): custom_listen_port: int | None """A custom listen port, if specified in the spec.""" - type_name: ClassVar[str] # automatically derived from the class name in __init_subclass__ + type_name: ClassVar[ + str + ] # automatically derived from the class name in __init_subclass__ """The unique name for this proxy mode, e.g. "regular" or "reverse".""" - __types: ClassVar[dict[str, Type[ProxyMode]]] = {} + __types: ClassVar[dict[str, type[ProxyMode]]] = {} def __init_subclass__(cls, **kwargs): cls.type_name = cls.__name__.removesuffix("Mode").lower() @@ -71,7 +83,7 @@ def description(self) -> str: """The mode description that will be used in server logs and UI.""" @property - def default_port(self) -> int: + def default_port(self) -> int | None: """ Default listen port of servers for this mode, see `ProxyMode.listen_port()`. """ @@ -79,12 +91,12 @@ def default_port(self) -> int: @property @abstractmethod - def transport_protocol(self) -> Literal["tcp", "udp"]: + def transport_protocol(self) -> Literal["tcp", "udp", "both"]: """The transport protocol used by this mode's server.""" @classmethod @cache - def parse(cls: Type[Self], spec: str) -> Self: + def parse(cls, spec: str) -> Self: """ Parse a proxy mode specification and return the corresponding `ProxyMode` instance. """ @@ -120,10 +132,7 @@ def parse(cls: Type[Self], spec: str) -> Self: raise ValueError(f"{mode!r} is not a spec for a {cls.type_name} mode") return mode_cls( - full_spec=spec, - data=data, - custom_listen_host=host, - custom_listen_port=port + full_spec=spec, data=data, custom_listen_host=host, custom_listen_port=port ) def listen_host(self, default: str | None = None) -> str: @@ -139,11 +148,12 @@ def listen_host(self, default: str | None = None) -> str: else: return "" - def listen_port(self, default: int | None = None) -> int: + def listen_port(self, default: int | None = None) -> int | None: """ Return the port a server for this mode should listen on. This can be either directly specified in the spec, taken from a user-configured global default (`options.listen_port`), or from `ProxyMode.default_port`. + May be `None` for modes that don't bind to a specific address, e.g. local redirect mode. """ if self.custom_listen_port is not None: return self.custom_listen_port @@ -161,11 +171,12 @@ def get_state(self): def set_state(self, state): if state != self.full_spec: - raise RuntimeError("Proxy modes are frozen.") + raise dataclasses.FrozenInstanceError("Proxy modes are immutable.") -TCP: Literal['tcp', 'udp'] = "tcp" -UDP: Literal['tcp', 'udp'] = "udp" +TCP: Literal["tcp", "udp", "both"] = "tcp" +UDP: Literal["tcp", "udp", "both"] = "udp" +BOTH: Literal["tcp", "udp", "both"] = "both" def _check_empty(data): @@ -175,6 +186,7 @@ def _check_empty(data): class RegularMode(ProxyMode): """A regular HTTP(S) proxy that is interfaced with `HTTP CONNECT` calls (or absolute-form HTTP requests).""" + description = "HTTP(S) proxy" transport_protocol = TCP @@ -184,7 +196,8 @@ def __post_init__(self) -> None: class TransparentMode(ProxyMode): """A transparent proxy, see https://docs.mitmproxy.org/dev/howto-transparent/""" - description = "transparent proxy" + + description = "Transparent Proxy" transport_protocol = TCP def __post_init__(self) -> None: @@ -193,6 +206,7 @@ def __post_init__(self) -> None: class UpstreamMode(ProxyMode): """A regular HTTP(S) proxy, but all connections are forwarded to a second upstream HTTP(S) proxy.""" + description = "HTTP(S) proxy (upstream mode)" transport_protocol = TCP scheme: Literal["http", "https"] @@ -208,20 +222,25 @@ def __post_init__(self) -> None: class ReverseMode(ProxyMode): """A reverse proxy. This acts like a normal server, but redirects all requests to a fixed target.""" + description = "reverse proxy" transport_protocol = TCP - scheme: Literal["http", "https", "tls", "dtls", "tcp", "udp", "dns"] + scheme: Literal[ + "http", "https", "http3", "tls", "dtls", "tcp", "udp", "dns", "quic" + ] address: tuple[str, int] # noinspection PyDataclass def __post_init__(self) -> None: self.scheme, self.address = server_spec.parse(self.data, default_scheme="https") - if self.scheme in ("dns", "dtls", "udp"): + if self.scheme in ("http3", "dtls", "udp", "quic"): self.transport_protocol = UDP + elif self.scheme in ("dns", "https"): + self.transport_protocol = BOTH self.description = f"{self.description} to {self.data}" @property - def default_port(self) -> int: + def default_port(self) -> int | None: if self.scheme == "dns": return 53 return super().default_port @@ -229,6 +248,7 @@ def default_port(self) -> int: class Socks5Mode(ProxyMode): """A SOCKSv5 proxy.""" + description = "SOCKS v5 proxy" default_port = 1080 transport_protocol = TCP @@ -239,19 +259,78 @@ def __post_init__(self) -> None: class DnsMode(ProxyMode): """A DNS server.""" + description = "DNS server" default_port = 53 - transport_protocol = UDP + transport_protocol = BOTH def __post_init__(self) -> None: _check_empty(self.data) +# class Http3Mode(ProxyMode): +# """ +# A regular HTTP3 proxy that is interfaced with absolute-form HTTP requests. +# (This class will be merged into `RegularMode` once the UDP implementation is deemed stable enough.) +# """ +# +# description = "HTTP3 proxy" +# transport_protocol = UDP +# +# def __post_init__(self) -> None: +# _check_empty(self.data) + + class WireGuardMode(ProxyMode): """Proxy Server based on WireGuard""" + description = "WireGuard server" default_port = 51820 transport_protocol = UDP def __post_init__(self) -> None: pass + + +class LocalMode(ProxyMode): + """OS-level transparent proxy.""" + + description = "Local redirector" + transport_protocol = BOTH + default_port = None + + def __post_init__(self) -> None: + # should not raise + mitmproxy_rs.local.LocalRedirector.describe_spec(self.data) + + +class TunMode(ProxyMode): + """A Tun interface.""" + + description = "TUN interface" + default_port = None + transport_protocol = BOTH + + def __post_init__(self) -> None: + invalid_tun_name = self.data and ( + # The Rust side is Linux only for the moment, but eventually we may need this. + platform.system() == "Darwin" and not re.match(r"^utun\d+$", self.data) + ) + if invalid_tun_name: # pragma: no cover + raise ValueError( + f"Invalid tun name: {self.data}. " + f"On macOS, the tun name must be the form utunx where x is a number, such as utun3." + ) + + +class OsProxyMode(ProxyMode): # pragma: no cover + """Deprecated alias for LocalMode""" + + description = "Deprecated alias for LocalMode" + transport_protocol = BOTH + default_port = None + + def __post_init__(self) -> None: + raise ValueError( + "osproxy mode has been renamed to local mode. Thanks for trying our experimental features!" + ) diff --git a/mitmproxy/proxy/server.py b/mitmproxy/proxy/server.py index 1341564ada..1071138d76 100644 --- a/mitmproxy/proxy/server.py +++ b/mitmproxy/proxy/server.py @@ -6,42 +6,56 @@ - Process any commands from layer (such as opening a server connection) - Wait for any IO and send it as events to top layer. """ + import abc import asyncio import collections import logging - import time -import traceback -from collections.abc import Awaitable, Callable, MutableMapping +from collections.abc import Awaitable +from collections.abc import Callable +from collections.abc import MutableMapping from contextlib import contextmanager from dataclasses import dataclass -from typing import Optional, Union +from types import TracebackType +from typing import Literal -import mitmproxy_wireguard as wg from OpenSSL import SSL -from mitmproxy import http, options as moptions, tls +import mitmproxy_rs +from mitmproxy import http +from mitmproxy import options as moptions +from mitmproxy import tls +from mitmproxy.connection import Address +from mitmproxy.connection import Client +from mitmproxy.connection import Connection +from mitmproxy.connection import ConnectionState +from mitmproxy.proxy import commands +from mitmproxy.proxy import events +from mitmproxy.proxy import layer +from mitmproxy.proxy import layers +from mitmproxy.proxy import mode_specs +from mitmproxy.proxy import server_hooks from mitmproxy.proxy.context import Context from mitmproxy.proxy.layers.http import HTTPMode -from mitmproxy.proxy import commands, events, layer, layers, mode_specs, server_hooks -from mitmproxy.connection import Address, Client, Connection, ConnectionState -from mitmproxy.net import udp from mitmproxy.utils import asyncio_utils from mitmproxy.utils import human from mitmproxy.utils.data import pkg_data - logger = logging.getLogger(__name__) +TCP_TIMEOUT = 60 * 10 +UDP_TIMEOUT = 20 + class TimeoutWatchdog: last_activity: float - CONNECTION_TIMEOUT = 10 * 60 + timeout: int can_timeout: asyncio.Event blocker: int - def __init__(self, callback: Callable[[], Awaitable]): + def __init__(self, timeout: int, callback: Callable[[], Awaitable]): + self.timeout = timeout self.callback = callback self.last_activity = time.time() self.can_timeout = asyncio.Event() @@ -55,10 +69,8 @@ async def watch(self): try: while True: await self.can_timeout.wait() - await asyncio.sleep( - self.CONNECTION_TIMEOUT - (time.time() - self.last_activity) - ) - if self.last_activity + self.CONNECTION_TIMEOUT < time.time(): + await asyncio.sleep(self.timeout - (time.time() - self.last_activity)) + if self.last_activity + self.timeout < time.time(): await self.callback() return except asyncio.CancelledError: @@ -79,9 +91,9 @@ def disarm(self): @dataclass class ConnectionIO: - handler: Optional[asyncio.Task] = None - reader: Optional[Union[asyncio.StreamReader, udp.DatagramReader, wg.TcpStream]] = None - writer: Optional[Union[asyncio.StreamWriter, udp.DatagramWriter, wg.TcpStream]] = None + handler: asyncio.Task | None = None + reader: asyncio.StreamReader | mitmproxy_rs.Stream | None = None + writer: asyncio.StreamWriter | mitmproxy_rs.Stream | None = None class ConnectionHandler(metaclass=abc.ABCMeta): @@ -91,18 +103,26 @@ class ConnectionHandler(metaclass=abc.ABCMeta): max_conns: collections.defaultdict[Address, asyncio.Semaphore] layer: "layer.Layer" wakeup_timer: set[asyncio.Task] + hook_tasks: set[asyncio.Task] def __init__(self, context: Context) -> None: self.client = context.client self.transports = {} self.max_conns = collections.defaultdict(lambda: asyncio.Semaphore(5)) self.wakeup_timer = set() + self.hook_tasks = set() # Ask for the first layer right away. # In a reverse proxy scenario, this is necessary as we would otherwise hang # on protocols that start with a server greeting. self.layer = layer.NextLayer(context, ask_on_start=True) - self.timeout_watchdog = TimeoutWatchdog(self.on_timeout) + if self.client.transport_protocol == "tcp": + timeout = TCP_TIMEOUT + else: + timeout = UDP_TIMEOUT + self.timeout_watchdog = TimeoutWatchdog(timeout, self.on_timeout) + + self._server_event_lock = asyncio.Lock() # workaround for https://bugs.python.org/issue40124 / https://bugs.python.org/issue29930 self._drain_lock = asyncio.Lock() @@ -126,16 +146,20 @@ async def handle_client(self) -> None: assert writer writer.close() else: + await self.server_event(events.Start()) handler = asyncio_utils.create_task( self.handle_connection(self.client), name=f"client connection handler", client=self.client.peername, ) self.transports[self.client].handler = handler - self.server_event(events.Start()) await asyncio.wait([handler]) if not handler.cancelled() and (e := handler.exception()): - self.log(f"mitmproxy has crashed!\n{traceback.format_exception(e)}", logging.ERROR) + self.log( + f"connection handler has crashed: {e}", + logging.ERROR, + exc_info=(type(e), e, e.__traceback__), + ) watch.cancel() while self.wakeup_timer: @@ -159,7 +183,7 @@ async def handle_client(self) -> None: async def open_connection(self, command: commands.OpenConnection) -> None: if not command.connection.address: self.log(f"Cannot open connection, no hostname given.") - self.server_event( + await self.server_event( events.OpenConnectionCompleted( command, f"Cannot open connection, no hostname given." ) @@ -174,14 +198,15 @@ async def open_connection(self, command: commands.OpenConnection) -> None: self.log( f"server connection to {human.format_address(command.connection.address)} killed before connect: {err}" ) - self.server_event( + await self.handle_hook(server_hooks.ServerConnectErrorHook(hook_data)) + await self.server_event( events.OpenConnectionCompleted(command, f"Connection killed: {err}") ) return async with self.max_conns[command.connection.address]: - reader: Union[asyncio.StreamReader, udp.DatagramReader] - writer: Union[asyncio.StreamWriter, udp.DatagramWriter] + reader: asyncio.StreamReader | mitmproxy_rs.Stream + writer: asyncio.StreamWriter | mitmproxy_rs.Stream try: command.connection.timestamp_start = time.time() if command.connection.transport_protocol == "tcp": @@ -190,7 +215,7 @@ async def open_connection(self, command: commands.OpenConnection) -> None: local_addr=command.connection.sockname, ) elif command.connection.transport_protocol == "udp": - reader, writer = await udp.open_connection( + reader = writer = await mitmproxy_rs.udp.open_udp_connection( *command.connection.address, local_addr=command.connection.sockname, ) @@ -202,7 +227,8 @@ async def open_connection(self, command: commands.OpenConnection) -> None: err = "connection cancelled" self.log(f"error establishing server connection: {err}") command.connection.error = err - self.server_event(events.OpenConnectionCompleted(command, err)) + await self.handle_hook(server_hooks.ServerConnectErrorHook(hook_data)) + await self.server_event(events.OpenConnectionCompleted(command, err)) if isinstance(e, asyncio.CancelledError): # From https://docs.python.org/3/library/asyncio-exceptions.html#asyncio.CancelledError: # > In almost all situations the exception must be re-raised. @@ -215,8 +241,11 @@ async def open_connection(self, command: commands.OpenConnection) -> None: command.connection.state = ConnectionState.OPEN command.connection.peername = writer.get_extra_info("peername") command.connection.sockname = writer.get_extra_info("sockname") - self.transports[command.connection].reader = reader - self.transports[command.connection].writer = writer + self.transports[command.connection] = ConnectionIO( + handler=asyncio.current_task(), + reader=reader, + writer=writer, + ) assert command.connection.peername if command.connection.address[0] != command.connection.peername[0]: @@ -225,29 +254,23 @@ async def open_connection(self, command: commands.OpenConnection) -> None: addr = human.format_address(command.connection.address) self.log(f"server connect {addr}") await self.handle_hook(server_hooks.ServerConnectedHook(hook_data)) - self.server_event(events.OpenConnectionCompleted(command, None)) - - # during connection opening, this function is the designated handler that can be cancelled. - # once we have a connection, we do want the teardown here to happen in any case, so we - # reassign the handler to .handle_connection and then clean up here once that is done. - new_handler = asyncio_utils.create_task( - self.handle_connection(command.connection), - name=f"server connection handler for {addr}", - client=self.client.peername, - ) - self.transports[command.connection].handler = new_handler - await asyncio.wait([new_handler]) - - self.log(f"server disconnect {addr}") - command.connection.timestamp_end = time.time() - await self.handle_hook(server_hooks.ServerDisconnectedHook(hook_data)) + await self.server_event(events.OpenConnectionCompleted(command, None)) + + try: + await self.handle_connection(command.connection) + finally: + self.log(f"server disconnect {addr}") + command.connection.timestamp_end = time.time() + await self.handle_hook( + server_hooks.ServerDisconnectedHook(hook_data) + ) async def wakeup(self, request: commands.RequestWakeup) -> None: await asyncio.sleep(request.delay) task = asyncio.current_task() assert task is not None self.wakeup_timer.discard(task) - self.server_event(events.Wakeup(request)) + await self.server_event(events.Wakeup(request)) async def handle_connection(self, connection: Connection) -> None: """ @@ -269,7 +292,7 @@ async def handle_connection(self, connection: Connection) -> None: cancelled = e break - self.server_event(events.DataReceived(connection, data)) + await self.server_event(events.DataReceived(connection, data)) try: await self.drain_writers() @@ -277,18 +300,22 @@ async def handle_connection(self, connection: Connection) -> None: cancelled = e break - if cancelled is None: + if cancelled is None and connection.transport_protocol == "tcp": + # TCP connections can be half-closed. connection.state &= ~ConnectionState.CAN_READ else: connection.state = ConnectionState.CLOSED - self.server_event(events.ConnectionClosed(connection)) + await self.server_event(events.ConnectionClosed(connection)) - if cancelled is None and connection.state is ConnectionState.CAN_WRITE: + if connection.state is ConnectionState.CAN_WRITE: # we may still use this connection to *send* stuff, # even though the remote has closed their side of the connection. # to make this work we keep this task running and wait for cancellation. - await asyncio.Event().wait() + try: + await asyncio.Event().wait() + except asyncio.CancelledError as e: + cancelled = e try: writer = self.transports[connection].writer @@ -307,7 +334,7 @@ async def drain_writers(self): write buffers, so if we cannot write fast enough our own read buffers run full and the TCP recv stream is throttled. """ async with self._drain_lock: - for transport in self.transports.values(): + for transport in list(self.transports.values()): if transport.writer is not None: try: await transport.writer.drain() @@ -316,73 +343,99 @@ async def drain_writers(self): transport.handler.cancel(f"Error sending data: {e}") async def on_timeout(self) -> None: - self.log(f"Closing connection due to inactivity: {self.client}") - handler = self.transports[self.client].handler - assert handler - handler.cancel("timeout") + try: + handler = self.transports[self.client].handler + except KeyError: # pragma: no cover + # there is a super short window between connection close and watchdog cancellation + pass + else: + if self.client.transport_protocol == "tcp": + self.log(f"Closing connection due to inactivity: {self.client}") + assert handler + handler.cancel("timeout") async def hook_task(self, hook: commands.StartHook) -> None: await self.handle_hook(hook) if hook.blocking: - self.server_event(events.HookCompleted(hook)) + await self.server_event(events.HookCompleted(hook)) @abc.abstractmethod async def handle_hook(self, hook: commands.StartHook) -> None: pass - def log(self, message: str, level: int = logging.INFO) -> None: + def log( + self, + message: str, + level: int = logging.INFO, + exc_info: Literal[True] + | tuple[type[BaseException], BaseException, TracebackType | None] + | None = None, + ) -> None: logger.log( - level, - message, - extra={"client": self.client.peername} + level, message, extra={"client": self.client.peername}, exc_info=exc_info ) - def server_event(self, event: events.Event) -> None: - self.timeout_watchdog.register_activity() - try: - layer_commands = self.layer.handle_event(event) - for command in layer_commands: - - if isinstance(command, commands.OpenConnection): - assert command.connection not in self.transports - handler = asyncio_utils.create_task( - self.open_connection(command), - name=f"server connection manager {command.connection.address}", - client=self.client.peername, - ) - self.transports[command.connection] = ConnectionIO(handler=handler) - elif isinstance(command, commands.RequestWakeup): - task = asyncio_utils.create_task( - self.wakeup(command), - name=f"wakeup timer ({command.delay:.1f}s)", - client=self.client.peername, - ) - assert task is not None - self.wakeup_timer.add(task) - elif ( - isinstance(command, commands.ConnectionCommand) - and command.connection not in self.transports - ): - pass # The connection has already been closed. - elif isinstance(command, commands.SendData): - writer = self.transports[command.connection].writer - assert writer - if not writer.is_closing(): - writer.write(command.data) - elif isinstance(command, commands.CloseConnection): - self.close_connection(command.connection, command.half_close) - elif isinstance(command, commands.StartHook): - asyncio_utils.create_task( - self.hook_task(command), - name=f"handle_hook({command.name})", - client=self.client.peername, - ) - elif isinstance(command, commands.Log): - self.log(command.message, command.level) - else: - raise RuntimeError(f"Unexpected command: {command}") - except Exception: - self.log(f"mitmproxy has crashed!\n{traceback.format_exc()}", logging.ERROR) + async def server_event(self, event: events.Event) -> None: + # server_event is supposed to be completely sync without any `await` that could pause execution. + # However, create_task with an [eager task factory] will schedule tasks immediately, + # which causes [reentrancy issues]. So we put the entire thing behind a lock. + # + # [eager task factory]: https://docs.python.org/3/library/asyncio-task.html#eager-task-factory + # [reentrancy issues]: https://github.com/mitmproxy/mitmproxy/issues/7027. + async with self._server_event_lock: + # No `await` beyond this point. + + self.timeout_watchdog.register_activity() + try: + layer_commands = self.layer.handle_event(event) + for command in layer_commands: + if isinstance(command, commands.OpenConnection): + assert command.connection not in self.transports + handler = asyncio_utils.create_task( + self.open_connection(command), + name=f"server connection handler {command.connection.address}", + client=self.client.peername, + ) + self.transports[command.connection] = ConnectionIO( + handler=handler + ) + elif isinstance(command, commands.RequestWakeup): + task = asyncio_utils.create_task( + self.wakeup(command), + name=f"wakeup timer ({command.delay:.1f}s)", + client=self.client.peername, + ) + assert task is not None + self.wakeup_timer.add(task) + elif ( + isinstance(command, commands.ConnectionCommand) + and command.connection not in self.transports + ): + pass # The connection has already been closed. + elif isinstance(command, commands.SendData): + writer = self.transports[command.connection].writer + assert writer + if not writer.is_closing(): + writer.write(command.data) + elif isinstance(command, commands.CloseTcpConnection): + self.close_connection(command.connection, command.half_close) + elif isinstance(command, commands.CloseConnection): + self.close_connection(command.connection, False) + elif isinstance(command, commands.StartHook): + t = asyncio_utils.create_task( + self.hook_task(command), + name=f"handle_hook({command.name})", + client=self.client.peername, + ) + # Python 3.11 Use TaskGroup instead. + self.hook_tasks.add(t) + t.add_done_callback(self.hook_tasks.remove) + elif isinstance(command, commands.Log): + self.log(command.message, command.level) + else: + raise RuntimeError(f"Unexpected command: {command}") + except Exception: + self.log(f"mitmproxy has crashed!", logging.ERROR, exc_info=True) def close_connection( self, connection: Connection, half_close: bool = False @@ -413,16 +466,18 @@ def close_connection( class LiveConnectionHandler(ConnectionHandler, metaclass=abc.ABCMeta): def __init__( self, - reader: Union[asyncio.StreamReader, wg.TcpStream], - writer: Union[asyncio.StreamWriter, wg.TcpStream], + reader: asyncio.StreamReader | mitmproxy_rs.Stream, + writer: asyncio.StreamWriter | mitmproxy_rs.Stream, options: moptions.Options, mode: mode_specs.ProxyMode, ) -> None: client = Client( - writer.get_extra_info("peername"), - writer.get_extra_info("sockname"), - time.time(), + transport_protocol=writer.get_extra_info("transport_protocol", "tcp"), + peername=writer.get_extra_info("peername"), + sockname=writer.get_extra_info("sockname"), + timestamp_start=time.time(), proxy_mode=mode, + state=ConnectionState.OPEN, ) context = Context(client, options) super().__init__(context) @@ -436,9 +491,9 @@ class SimpleConnectionHandler(LiveConnectionHandler): # pragma: no cover hook_handlers: dict[str, Callable] - def __init__(self, reader, writer, options, mode, hooks): + def __init__(self, reader, writer, options, mode, hook_handlers): super().__init__(reader, writer, options, mode) - self.hook_handlers = hooks + self.hook_handlers = hook_handlers async def handle_hook(self, hook: commands.StartHook) -> None: if hook.name in self.hook_handlers: @@ -479,9 +534,9 @@ async def handle(reader, writer): ] def next_layer(nl: layer.NextLayer): - l = layer_stack.pop(0)(nl.context) - l.debug = " " * len(nl.context.layers) - nl.layer = l + layr = layer_stack.pop(0)(nl.context) + layr.debug = " " * len(nl.context.layers) + nl.layer = layr def request(flow: http.HTTPFlow): if "cached" in flow.request.path: diff --git a/mitmproxy/proxy/server_hooks.py b/mitmproxy/proxy/server_hooks.py index 22e1e5418b..1da4b60f22 100644 --- a/mitmproxy/proxy/server_hooks.py +++ b/mitmproxy/proxy/server_hooks.py @@ -1,7 +1,7 @@ from dataclasses import dataclass -from mitmproxy import connection from . import commands +from mitmproxy import connection @dataclass @@ -63,3 +63,14 @@ class ServerDisconnectedHook(commands.StartHook): """ data: ServerConnectionHookData + + +@dataclass +class ServerConnectErrorHook(commands.StartHook): + """ + Mitmproxy failed to connect to a server. + + Every server connection will receive either a server_connected or a server_connect_error event, but not both. + """ + + data: ServerConnectionHookData diff --git a/mitmproxy/proxy/tunnel.py b/mitmproxy/proxy/tunnel.py index d14023a742..bdda421543 100644 --- a/mitmproxy/proxy/tunnel.py +++ b/mitmproxy/proxy/tunnel.py @@ -1,9 +1,13 @@ import time -from enum import Enum, auto -from typing import Optional, Union +from enum import auto +from enum import Enum +from typing import Union from mitmproxy import connection -from mitmproxy.proxy import commands, context, events, layer +from mitmproxy.proxy import commands +from mitmproxy.proxy import context +from mitmproxy.proxy import events +from mitmproxy.proxy import layer from mitmproxy.proxy.layer import Layer @@ -26,7 +30,7 @@ class TunnelLayer(layer.Layer): conn: connection.Connection """The 'inner' connection which provides data I/O""" tunnel_state: TunnelState = TunnelState.INACTIVE - command_to_reply_to: Optional[commands.OpenConnection] = None + command_to_reply_to: commands.OpenConnection | None = None _event_queue: list[events.Event] """ If the connection already exists when we receive the start event, @@ -93,7 +97,7 @@ def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: else: yield from self.event_to_child(event) - def _handshake_finished(self, err: Optional[str]): + def _handshake_finished(self, err: str | None) -> layer.CommandGenerator[None]: if err: self.tunnel_state = TunnelState.CLOSED else: @@ -108,6 +112,37 @@ def _handshake_finished(self, err: Optional[str]): yield from self.event_to_child(evt) self._event_queue.clear() + def _handle_command( + self, command: commands.Command + ) -> layer.CommandGenerator[None]: + if ( + isinstance(command, commands.ConnectionCommand) + and command.connection == self.conn + ): + if isinstance(command, commands.SendData): + yield from self.send_data(command.data) + elif isinstance(command, commands.CloseConnection): + if self.conn != self.tunnel_connection: + self.conn.state &= ~connection.ConnectionState.CAN_WRITE + command.connection = self.tunnel_connection + yield from self.send_close(command) + elif isinstance(command, commands.OpenConnection): + # create our own OpenConnection command object that blocks here. + self.command_to_reply_to = command + self.tunnel_state = TunnelState.ESTABLISHING + err = yield commands.OpenConnection(self.tunnel_connection) + if err: + yield from self.event_to_child( + events.OpenConnectionCompleted(command, err) + ) + self.tunnel_state = TunnelState.CLOSED + else: + yield from self.start_handshake() + else: # pragma: no cover + raise AssertionError(f"Unexpected command: {command}") + else: + yield command + def event_to_child(self, event: events.Event) -> layer.CommandGenerator[None]: if ( self.tunnel_state is TunnelState.ESTABLISHING @@ -116,42 +151,14 @@ def event_to_child(self, event: events.Event) -> layer.CommandGenerator[None]: self._event_queue.append(event) return for command in self.child_layer.handle_event(event): - if ( - isinstance(command, commands.ConnectionCommand) - and command.connection == self.conn - ): - if isinstance(command, commands.SendData): - yield from self.send_data(command.data) - elif isinstance(command, commands.CloseConnection): - if self.conn != self.tunnel_connection: - if command.half_close: - self.conn.state &= ~connection.ConnectionState.CAN_WRITE - else: - self.conn.state = connection.ConnectionState.CLOSED - yield from self.send_close(command.half_close) - elif isinstance(command, commands.OpenConnection): - # create our own OpenConnection command object that blocks here. - self.command_to_reply_to = command - self.tunnel_state = TunnelState.ESTABLISHING - err = yield commands.OpenConnection(self.tunnel_connection) - if err: - yield from self.event_to_child( - events.OpenConnectionCompleted(command, err) - ) - self.tunnel_state = TunnelState.CLOSED - else: - yield from self.start_handshake() - else: # pragma: no cover - raise AssertionError(f"Unexpected command: {command}") - else: - yield command + yield from self._handle_command(command) def start_handshake(self) -> layer.CommandGenerator[None]: yield from self._handle_event(events.DataReceived(self.tunnel_connection, b"")) def receive_handshake_data( self, data: bytes - ) -> layer.CommandGenerator[tuple[bool, Optional[str]]]: + ) -> layer.CommandGenerator[tuple[bool, str | None]]: """returns a (done, err) tuple""" yield from () return True, None @@ -169,8 +176,10 @@ def receive_close(self) -> layer.CommandGenerator[None]: def send_data(self, data: bytes) -> layer.CommandGenerator[None]: yield commands.SendData(self.tunnel_connection, data) - def send_close(self, half_close: bool) -> layer.CommandGenerator[None]: - yield commands.CloseConnection(self.tunnel_connection, half_close=half_close) + def send_close( + self, command: commands.CloseConnection + ) -> layer.CommandGenerator[None]: + yield command class LayerStack: diff --git a/mitmproxy/proxy/utils.py b/mitmproxy/proxy/utils.py index d2b2aa23aa..114a8e1555 100644 --- a/mitmproxy/proxy/utils.py +++ b/mitmproxy/proxy/utils.py @@ -1,6 +1,7 @@ """ Utility decorators that help build state machines """ + import functools from mitmproxy.proxy import events @@ -33,3 +34,35 @@ def _check_event_type(self, event: events.Event): return f return decorator + + +class ReceiveBuffer: + """ + A data structure to collect stream contents efficiently in O(n). + """ + + _chunks: list[bytes] + _len: int + + def __init__(self): + self._chunks = [] + self._len = 0 + + def __iadd__(self, other: bytes): + assert isinstance(other, bytes) + self._chunks.append(other) + self._len += len(other) + return self + + def __len__(self): + return self._len + + def __bytes__(self): + return b"".join(self._chunks) + + def __bool__(self): + return self._len > 0 + + def clear(self): + self._chunks.clear() + self._len = 0 diff --git a/mitmproxy/script/concurrent.py b/mitmproxy/script/concurrent.py index 9d9546568d..757548b643 100644 --- a/mitmproxy/script/concurrent.py +++ b/mitmproxy/script/concurrent.py @@ -5,6 +5,7 @@ import asyncio import inspect + from mitmproxy import hooks diff --git a/mitmproxy/stateobject.py b/mitmproxy/stateobject.py deleted file mode 100644 index 4f87a52279..0000000000 --- a/mitmproxy/stateobject.py +++ /dev/null @@ -1,96 +0,0 @@ -import json -from collections import abc -import typing -from mitmproxy.coretypes import serializable -from mitmproxy.utils import typecheck - - -class StateObject(serializable.Serializable): - """ - An object with serializable state. - - State attributes can either be serializable types(str, tuple, bool, ...) - or StateObject instances themselves. - """ - - _stateobject_attributes: typing.ClassVar[abc.MutableMapping[str, typing.Any]] - """ - An attribute-name -> class-or-type dict containing all attributes that - should be serialized. If the attribute is a class, it must implement the - Serializable protocol. - """ - - def get_state(self): - """ - Retrieve object state. - """ - state = {} - for attr, cls in self._stateobject_attributes.items(): - val = getattr(self, attr) - state[attr] = get_state(cls, val) - return state - - def set_state(self, state): - """ - Load object state from data returned by a get_state call. - """ - state = state.copy() - for attr, cls in self._stateobject_attributes.items(): - val = state.pop(attr) - if val is None: - setattr(self, attr, val) - else: - curr = getattr(self, attr, None) - if hasattr(curr, "set_state"): - curr.set_state(val) - else: - setattr(self, attr, make_object(cls, val)) - if state: - raise RuntimeWarning(f"Unexpected State in __setstate__: {state}") - - -def _process(typeinfo: typecheck.Type, val: typing.Any, make: bool) -> typing.Any: - if val is None: - return None - elif make and hasattr(typeinfo, "from_state"): - return typeinfo.from_state(val) - elif not make and hasattr(val, "get_state"): - return val.get_state() - - origin = typing.get_origin(typeinfo) - - if origin is list: - T = typing.get_args(typeinfo)[0] - return [_process(T, x, make) for x in val] - elif origin is tuple: - Ts = typing.get_args(typeinfo) - if len(Ts) != len(val): - raise ValueError(f"Invalid data. Expected {Ts}, got {val}.") - return tuple(_process(T, x, make) for T, x in zip(Ts, val)) - elif origin is dict: - k_cls, v_cls = typing.get_args(typeinfo) - return { - _process(k_cls, k, make): _process(v_cls, v, make) for k, v in val.items() - } - elif typeinfo is typing.Any: - # This requires a bit of explanation. We can't import our IO layer here, - # because it causes a circular import. Rather than restructuring the - # code for this, we use JSON serialization, which has similar primitive - # type restrictions as tnetstring, to check for conformance. - try: - json.dumps(val) - except TypeError: - raise ValueError(f"Data not serializable: {val}") - return val - else: - return typeinfo(val) - - -def make_object(typeinfo: typecheck.Type, val: typing.Any) -> typing.Any: - """Create an object based on the state given in val.""" - return _process(typeinfo, val, True) - - -def get_state(typeinfo: typecheck.Type, val: typing.Any) -> typing.Any: - """Get the state of the object given as val.""" - return _process(typeinfo, val, False) diff --git a/mitmproxy/tcp.py b/mitmproxy/tcp.py index a2512cfb83..eec4c26fc9 100644 --- a/mitmproxy/tcp.py +++ b/mitmproxy/tcp.py @@ -1,6 +1,7 @@ import time -from mitmproxy import connection, flow +from mitmproxy import connection +from mitmproxy import flow from mitmproxy.coretypes import serializable @@ -54,8 +55,15 @@ def __init__( super().__init__(client_conn, server_conn, live) self.messages = [] - _stateobject_attributes = flow.Flow._stateobject_attributes.copy() - _stateobject_attributes["messages"] = list[TCPMessage] + def get_state(self) -> serializable.State: + return { + **super().get_state(), + "messages": [m.get_state() for m in self.messages], + } + + def set_state(self, state: serializable.State) -> None: + self.messages = [TCPMessage.from_state(m) for m in state.pop("messages")] + super().set_state(state) def __repr__(self): return f"" diff --git a/mitmproxy/test/taddons.py b/mitmproxy/test/taddons.py index 24dbaebdaa..82ee2de304 100644 --- a/mitmproxy/test/taddons.py +++ b/mitmproxy/test/taddons.py @@ -2,10 +2,11 @@ import mitmproxy.master import mitmproxy.options -from mitmproxy import hooks from mitmproxy import command from mitmproxy import eventsequence -from mitmproxy.addons import script, core +from mitmproxy import hooks +from mitmproxy.addons import core +from mitmproxy.addons import script class context: diff --git a/mitmproxy/test/tflow.py b/mitmproxy/test/tflow.py index fb2560cea0..84654deb7d 100644 --- a/mitmproxy/test/tflow.py +++ b/mitmproxy/test/tflow.py @@ -1,5 +1,6 @@ import uuid -from typing import Optional, Union + +from wsproto.frame_protocol import Opcode from mitmproxy import connection from mitmproxy import dns @@ -8,10 +9,12 @@ from mitmproxy import tcp from mitmproxy import udp from mitmproxy import websocket +from mitmproxy.connection import ConnectionState from mitmproxy.proxy.mode_specs import ProxyMode -from mitmproxy.test.tutils import tdnsreq, tdnsresp -from mitmproxy.test.tutils import treq, tresp -from wsproto.frame_protocol import Opcode +from mitmproxy.test.tutils import tdnsreq +from mitmproxy.test.tutils import tdnsresp +from mitmproxy.test.tutils import treq +from mitmproxy.test.tutils import tresp def ttcpflow( @@ -118,11 +121,11 @@ def twebsocketflow( def tdnsflow( *, - client_conn: Optional[connection.Client] = None, - server_conn: Optional[connection.Server] = None, - req: Optional[dns.Message] = None, - resp: Union[bool, dns.Message] = False, - err: Union[bool, flow.Error] = False, + client_conn: connection.Client | None = None, + server_conn: connection.Server | None = None, + req: dns.Message | None = None, + resp: bool | dns.Message = False, + err: bool | flow.Error = False, live: bool = True, ) -> dns.DNSFlow: """Create a DNS flow for testing.""" @@ -155,12 +158,12 @@ def tdnsflow( def tflow( *, - client_conn: Optional[connection.Client] = None, - server_conn: Optional[connection.Server] = None, - req: Optional[http.Request] = None, - resp: Union[bool, http.Response] = False, - err: Union[bool, flow.Error] = False, - ws: Union[bool, websocket.WebSocketData] = False, + client_conn: connection.Client | None = None, + server_conn: connection.Server | None = None, + req: http.Request | None = None, + resp: bool | http.Response = False, + err: bool | flow.Error = False, + ws: bool | websocket.WebSocketData = False, live: bool = True, ) -> http.HTTPFlow: """Create a flow for testing.""" @@ -211,58 +214,50 @@ def tdummyflow(client_conn=True, server_conn=True, err=None) -> DummyFlow: def tclient_conn() -> connection.Client: - c = connection.Client.from_state( - dict( - id=str(uuid.uuid4()), - address=("127.0.0.1", 22), - mitmcert=None, - tls_established=True, - timestamp_start=946681200, - timestamp_tls_setup=946681201, - timestamp_end=946681206, - sni="address", - cipher_name="cipher", - alpn=b"http/1.1", - tls_version="TLSv1.2", - tls_extensions=[(0x00, bytes.fromhex("000e00000b6578616d"))], - state=0, - sockname=("", 0), - error=None, - tls=False, - certificate_list=[], - alpn_offers=[], - cipher_list=[], - proxy_mode="regular", - ) + c = connection.Client( + id=str(uuid.uuid4()), + peername=("127.0.0.1", 22), + sockname=("", 0), + mitmcert=None, + timestamp_start=946681200, + timestamp_tls_setup=946681201, + timestamp_end=946681206, + sni="address", + cipher="cipher", + alpn=b"http/1.1", + tls_version="TLSv1.2", + state=ConnectionState.OPEN, + error=None, + tls=False, + certificate_list=[], + alpn_offers=[], + cipher_list=[], + proxy_mode=ProxyMode.parse("regular"), ) return c def tserver_conn() -> connection.Server: - c = connection.Server.from_state( - dict( - id=str(uuid.uuid4()), - address=("address", 22), - source_address=("address", 22), - ip_address=("192.168.0.1", 22), - timestamp_start=946681202, - timestamp_tcp_setup=946681203, - timestamp_tls_setup=946681204, - timestamp_end=946681205, - tls_established=True, - sni="address", - alpn=None, - tls_version="TLSv1.2", - via=None, - state=0, - error=None, - tls=False, - certificate_list=[], - alpn_offers=[], - cipher_name=None, - cipher_list=[], - via2=None, - ) + c = connection.Server( + id=str(uuid.uuid4()), + address=("address", 22), + peername=("192.168.0.1", 22), + sockname=("address", 22), + timestamp_start=946681202, + timestamp_tcp_setup=946681203, + timestamp_tls_setup=946681204, + timestamp_end=946681205, + sni="address", + alpn=None, + tls_version="TLSv1.2", + via=None, + state=ConnectionState.CLOSED, + error=None, + tls=False, + certificate_list=[], + alpn_offers=[], + cipher=None, + cipher_list=[], ) return c diff --git a/mitmproxy/tls.py b/mitmproxy/tls.py index 98fed77a44..de0aa340e6 100644 --- a/mitmproxy/tls.py +++ b/mitmproxy/tls.py @@ -1,12 +1,12 @@ import io from dataclasses import dataclass -from typing import Optional from kaitaistruct import KaitaiStream - from OpenSSL import SSL + from mitmproxy import connection -from mitmproxy.contrib.kaitaistruct import tls_client_hello, dtls_client_hello +from mitmproxy.contrib.kaitaistruct import dtls_client_hello +from mitmproxy.contrib.kaitaistruct import tls_client_hello from mitmproxy.net import check from mitmproxy.proxy import context @@ -18,7 +18,7 @@ class ClientHello: _raw_bytes: bytes - def __init__(self, raw_client_hello: bytes, dtls: bool=False): + def __init__(self, raw_client_hello: bytes, dtls: bool = False): """Create a TLS ClientHello object from raw bytes.""" self._raw_bytes = raw_client_hello if dtls: @@ -67,7 +67,7 @@ def cipher_suites(self) -> list[int]: return self._client_hello.cipher_suites.cipher_suites @property - def sni(self) -> Optional[str]: + def sni(self) -> str | None: """ The [Server Name Indication](https://en.wikipedia.org/wiki/Server_Name_Indication), which indicates which hostname the client wants to connect to. @@ -141,7 +141,7 @@ class TlsData: """The affected connection.""" context: context.Context """The context object for this connection.""" - ssl_conn: Optional[SSL.Connection] = None + ssl_conn: SSL.Connection | None = None """ The associated pyOpenSSL `SSL.Connection` object. This will be set by an addon in the `tls_start_*` event hooks. diff --git a/mitmproxy/tools/cmdline.py b/mitmproxy/tools/cmdline.py index 15353332b5..a1e901753b 100644 --- a/mitmproxy/tools/cmdline.py +++ b/mitmproxy/tools/cmdline.py @@ -49,6 +49,7 @@ def common_options(parser, opts): opts.make_parser(parser, "mode", short="m") opts.make_parser(parser, "anticache") opts.make_parser(parser, "showhost") + opts.make_parser(parser, "show_ignored_hosts") opts.make_parser(parser, "rfile", metavar="PATH", short="r") opts.make_parser(parser, "scripts", metavar="SCRIPT", short="s") opts.make_parser(parser, "stickycookie", metavar="FILTER") @@ -83,7 +84,8 @@ def common_options(parser, opts): group = parser.add_argument_group("Server Replay") opts.make_parser(group, "server_replay", metavar="PATH", short="S") opts.make_parser(group, "server_replay_kill_extra") - opts.make_parser(group, "server_replay_nopop") + opts.make_parser(group, "server_replay_extra") + opts.make_parser(group, "server_replay_reuse") opts.make_parser(group, "server_replay_refresh") # Map Remote diff --git a/mitmproxy/tools/console/__init__.py b/mitmproxy/tools/console/__init__.py index 947258c920..a656793e52 100644 --- a/mitmproxy/tools/console/__init__.py +++ b/mitmproxy/tools/console/__init__.py @@ -1,4 +1,3 @@ from mitmproxy.tools.console import master - __all__ = ["master"] diff --git a/mitmproxy/tools/console/commander/commander.py b/mitmproxy/tools/console/commander/commander.py index 53acbf4bd2..11f2b67926 100644 --- a/mitmproxy/tools/console/commander/commander.py +++ b/mitmproxy/tools/console/commander/commander.py @@ -1,6 +1,6 @@ import abc from collections.abc import Sequence -from typing import NamedTuple, Optional +from typing import NamedTuple import urwid from urwid.text_layout import calc_coords @@ -53,7 +53,7 @@ def __init__(self, master: mitmproxy.master.Master, start: str = "") -> None: self.text = start # Cursor is always within the range [0:len(buffer)]. self._cursor = len(self.text) - self.completion: Optional[CompletionState] = None + self.completion: CompletionState | None = None @property def cursor(self) -> int: diff --git a/mitmproxy/tools/console/commandexecutor.py b/mitmproxy/tools/console/commandexecutor.py index d683a1e4b5..f047f54bca 100644 --- a/mitmproxy/tools/console/commandexecutor.py +++ b/mitmproxy/tools/console/commandexecutor.py @@ -3,7 +3,6 @@ from mitmproxy import exceptions from mitmproxy import flow - from mitmproxy.tools.console import overlay from mitmproxy.tools.console import signals @@ -20,11 +19,11 @@ def __call__(self, cmd: str) -> None: logging.error(str(e)) else: if ret is not None: - if type(ret) == Sequence[flow.Flow]: + if type(ret) == Sequence[flow.Flow]: # noqa: E721 signals.status_message.send( message="Command returned %s flows" % len(ret) ) - elif type(ret) == flow.Flow: + elif type(ret) is flow.Flow: signals.status_message.send(message="Command returned 1 flow") else: self.master.overlay( diff --git a/mitmproxy/tools/console/commands.py b/mitmproxy/tools/console/commands.py index 6b890c2d32..67c1f18993 100644 --- a/mitmproxy/tools/console/commands.py +++ b/mitmproxy/tools/console/commands.py @@ -1,6 +1,7 @@ -import urwid import textwrap +import urwid + from mitmproxy import command from mitmproxy.tools.console import layoutwidget from mitmproxy.tools.console import signals @@ -14,8 +15,7 @@ class CommandItem(urwid.WidgetWrap): def __init__(self, walker, cmd: command.Command, focused: bool): self.walker, self.cmd, self.focused = walker, cmd, focused - super().__init__(None) - self._w = self.get_widget() + super().__init__(self.get_widget()) def get_widget(self): parts = [("focus", ">> " if self.focused else " "), ("title", self.cmd.name)] @@ -111,20 +111,22 @@ def __init__(self, master): def set_active(self, val): h = urwid.Text("Command Help") style = "heading" if val else "heading_inactive" - self.header = urwid.AttrWrap(h, style) + self.header = urwid.AttrMap(h, style) def widget(self, txt): cols, _ = self.master.ui.get_cols_rows() return urwid.ListBox([urwid.Text(i) for i in textwrap.wrap(txt, cols)]) def sig_mod(self, txt): - self.set_body(self.widget(txt)) + self.body = self.widget(txt) class Commands(urwid.Pile, layoutwidget.LayoutWidget): title = "Command Reference" keyctx = "commands" + focus_position: int + def __init__(self, master): oh = CommandHelp(master) super().__init__( diff --git a/mitmproxy/tools/console/common.py b/mitmproxy/tools/console/common.py index 61c2ef0801..28f4714a50 100644 --- a/mitmproxy/tools/console/common.py +++ b/mitmproxy/tools/console/common.py @@ -1,22 +1,21 @@ import enum -import platform import math +import platform from collections.abc import Iterable from functools import lru_cache -from typing import Optional, Union - -from publicsuffix2 import get_sld, get_tld -import urwid import urwid.util +from publicsuffix2 import get_sld +from publicsuffix2 import get_tld +from mitmproxy import dns from mitmproxy import flow +from mitmproxy.dns import DNSFlow from mitmproxy.http import HTTPFlow -from mitmproxy.utils import human, emoji from mitmproxy.tcp import TCPFlow from mitmproxy.udp import UDPFlow -from mitmproxy import dns -from mitmproxy.dns import DNSFlow +from mitmproxy.utils import emoji +from mitmproxy.utils import human # Detect Windows Subsystem for Linux and Windows IS_WINDOWS_OR_WSL = ( @@ -32,22 +31,22 @@ def is_keypress(k): return True -def highlight_key(str, key, textattr="text", keyattr="key"): - l = [] - parts = str.split(key, 1) +def highlight_key(text, key, textattr="text", keyattr="key"): + lst = [] + parts = text.split(key, 1) if parts[0]: - l.append((textattr, parts[0])) - l.append((keyattr, key)) + lst.append((textattr, parts[0])) + lst.append((keyattr, key)) if parts[1]: - l.append((textattr, parts[1])) - return l + lst.append((textattr, parts[1])) + return lst KEY_MAX = 30 def format_keyvals( - entries: Iterable[tuple[str, Union[None, str, urwid.Widget]]], + entries: Iterable[tuple[str, None | str | urwid.Widget]], key_format: str = "key", value_format: str = "text", indent: int = 0, @@ -96,8 +95,8 @@ def fcol(s: str, attr: str) -> tuple[str, int, urwid.Text]: SYMBOL_REPLAY = "\u21ba" SYMBOL_RETURN = "\u2190" SYMBOL_MARK = "\u25cf" - SYMBOL_UP = "\u21E7" - SYMBOL_DOWN = "\u21E9" + SYMBOL_UP = "\u21e7" + SYMBOL_DOWN = "\u21e9" SYMBOL_ELLIPSIS = "\u2026" SYMBOL_FROM_CLIENT = "\u21d2" SYMBOL_TO_CLIENT = "\u21d0" @@ -119,6 +118,7 @@ def fcol(s: str, attr: str) -> tuple[str, int, urwid.Text]: "tcp": "scheme_tcp", "udp": "scheme_udp", "dns": "scheme_dns", + "quic": "scheme_quic", } HTTP_REQUEST_METHOD_STYLES = { "GET": "method_get", @@ -188,7 +188,7 @@ def render(self, size, focus=False): text = text[::-1] attr = attr[::-1] - text_len = urwid.util.calc_width(text, 0, len(text)) + text_len = urwid.calc_width(text, 0, len(text)) if size is not None and len(size) > 0: width = size[0] else: @@ -242,11 +242,11 @@ def rle_append_beginning_modify(rle, a_r): rle[0:0] = [(a, r)] -def colorize_host(host): +def colorize_host(host: str): tld = get_tld(host) sld = get_sld(host) - attr = [] + attr: list = [] tld_size = len(tld) sld_size = len(sld) - tld_size @@ -268,14 +268,14 @@ def colorize_host(host): return attr -def colorize_req(s): +def colorize_req(s: str): path = s.split("?", 2)[0] i_query = len(path) i_last_slash = path.rfind("/") i_ext = path[i_last_slash + 1 :].rfind(".") i_ext = i_last_slash + i_ext if i_ext >= 0 else len(s) in_val = False - attr = [] + attr: list = [] for i in range(len(s)): c = s[i] if ( @@ -358,7 +358,7 @@ def format_size(num_bytes: int) -> tuple[str, str]: def format_left_indicators(*, focused: bool, intercepted: bool, timestamp: float): - indicators: list[Union[str, tuple[str, str]]] = [] + indicators: list[str | tuple[str, str]] = [] if focused: indicators.append(("focus", ">>")) else: @@ -376,7 +376,7 @@ def format_right_indicators( replay: bool, marked: str, ): - indicators: list[Union[str, tuple[str, str]]] = [] + indicators: list[str | tuple[str, str]] = [] if replay: indicators.append(("replay", SYMBOL_REPLAY)) else: @@ -404,12 +404,12 @@ def format_http_flow_list( request_timestamp: float, request_is_push_promise: bool, intercepted: bool, - response_code: Optional[int], - response_reason: Optional[str], - response_content_length: Optional[int], - response_content_type: Optional[str], - duration: Optional[float], - error_message: Optional[str], + response_code: int | None, + response_reason: str | None, + response_content_length: int | None, + response_content_type: str | None, + duration: float | None, + error_message: str | None, ) -> urwid.Widget: req = [] @@ -492,7 +492,7 @@ def format_http_flow_table( render_mode: RenderMode, focused: bool, marked: str, - is_replay: Optional[str], + is_replay: str | None, request_method: str, request_scheme: str, request_host: str, @@ -502,12 +502,12 @@ def format_http_flow_table( request_timestamp: float, request_is_push_promise: bool, intercepted: bool, - response_code: Optional[int], - response_reason: Optional[str], - response_content_length: Optional[int], - response_content_type: Optional[str], - duration: Optional[float], - error_message: Optional[str], + response_code: int | None, + response_reason: str | None, + response_content_length: int | None, + response_content_type: str | None, + duration: float | None, + error_message: str | None, ) -> urwid.Widget: items = [ format_left_indicators( @@ -548,7 +548,6 @@ def format_http_flow_table( response_style = "" if response_code: - status = str(response_code) status_style = response_style or HTTP_RESPONSE_CODE_STYLE.get( response_code // 100, "code_other" @@ -616,8 +615,8 @@ def format_message_flow( client_address, server_address, total_size: int, - duration: Optional[float], - error_message: Optional[str], + duration: float | None, + error_message: str | None, ): conn = f"{human.format_address(client_address)} <-> {human.format_address(server_address)}" @@ -668,16 +667,16 @@ def format_dns_flow( focused: bool, intercepted: bool, marked: str, - is_replay: Optional[str], + is_replay: str | None, op_code: str, request_timestamp: float, domain: str, type: str, - response_code: Optional[str], + response_code: str | None, response_code_http_equiv: int, - answer: Optional[str], + answer: str | None, error_message: str, - duration: Optional[float], + duration: float | None, ): items = [] @@ -748,8 +747,8 @@ def format_flow( relevant for display and call the render with only that. This assures that rows are updated if the flow is changed. """ - duration: Optional[float] - error_message: Optional[str] + duration: float | None + error_message: str | None if f.error: error_message = f.error.msg else: @@ -763,12 +762,16 @@ def format_flow( duration = f.messages[-1].timestamp - f.client_conn.timestamp_start else: duration = None + if f.client_conn.tls_version == "QUICv1": + protocol = "quic" + else: + protocol = f.type return format_message_flow( render_mode=render_mode, focused=focused, timestamp_start=f.client_conn.timestamp_start, marked=f.marked, - protocol=f.type, + protocol=protocol, client_address=f.client_conn.peername, server_address=f.server_conn.address, total_size=total_size, @@ -778,7 +781,7 @@ def format_flow( elif isinstance(f, DNSFlow): if f.response: duration = f.response.timestamp - f.request.timestamp - response_code_str: Optional[str] = dns.response_codes.to_str( + response_code_str: str | None = dns.response_codes.to_str( f.response.response_code ) response_code_http_equiv = dns.response_codes.http_equiv_status_code( @@ -810,14 +813,14 @@ def format_flow( ) elif isinstance(f, HTTPFlow): intercepted = f.intercepted - response_content_length: Optional[int] + response_content_length: int | None if f.response: if f.response.raw_content is not None: response_content_length = len(f.response.raw_content) else: response_content_length = None - response_code: Optional[int] = f.response.status_code - response_reason: Optional[str] = f.response.reason + response_code: int | None = f.response.status_code + response_reason: str | None = f.response.reason response_content_type = f.response.headers.get("content-type") if f.response.timestamp_end: duration = max( diff --git a/mitmproxy/tools/console/consoleaddons.py b/mitmproxy/tools/console/consoleaddons.py index f88603d0a2..66763a2206 100644 --- a/mitmproxy/tools/console/consoleaddons.py +++ b/mitmproxy/tools/console/consoleaddons.py @@ -3,7 +3,8 @@ from collections.abc import Sequence import mitmproxy.types -from mitmproxy import command, command_lexer +from mitmproxy import command +from mitmproxy import command_lexer from mitmproxy import contentviews from mitmproxy import ctx from mitmproxy import dns @@ -374,7 +375,9 @@ def edit_focus_options(self) -> Sequence[str]: flow = self.master.view.focus.flow focus_options = [] - if isinstance(flow, tcp.TCPFlow): + if flow is None: + raise exceptions.CommandError("No flow selected.") + elif isinstance(flow, tcp.TCPFlow): focus_options = ["tcp-message"] elif isinstance(flow, udp.UDPFlow): focus_options = ["udp-message"] @@ -395,6 +398,8 @@ def edit_focus_options(self) -> Sequence[str]: "set-cookies", "url", ] + if flow.websocket: + focus_options.append("websocket-message") elif isinstance(flow, dns.DNSFlow): raise exceptions.CommandError( "Cannot edit DNS flows yet, please submit a patch." @@ -466,6 +471,10 @@ def edit_focus(self, flow_part: str) -> None: message = flow.messages[-1] c = self.master.spawn_editor(message.content or b"") message.content = c.rstrip(b"\n") + elif flow_part == "websocket-message": + message = flow.websocket.messages[-1] + c = self.master.spawn_editor(message.content or b"") + message.content = c.rstrip(b"\n") def _grideditor(self): gewidget = self.master.window.current("grideditor") diff --git a/mitmproxy/tools/console/defaultkeys.py b/mitmproxy/tools/console/defaultkeys.py index a316a90c91..2294538c35 100644 --- a/mitmproxy/tools/console/defaultkeys.py +++ b/mitmproxy/tools/console/defaultkeys.py @@ -17,6 +17,7 @@ def map(km: Keymap) -> None: km.add("E", "console.view.eventlog", ["commonkey", "global"], "View event log") km.add("Q", "console.exit", ["global"], "Exit immediately") km.add("q", "console.view.pop", ["commonkey", "global"], "Exit the current view") + km.add("esc", "console.view.pop", ["commonkey", "global"], "Exit the current view") km.add("-", "console.layout.cycle", ["global"], "Cycle to next layout") km.add("ctrl right", "console.panes.next", ["global"], "Focus next layout pane") km.add("ctrl left", "console.panes.prev", ["global"], "Focus previous layout pane") @@ -73,7 +74,7 @@ def map(km: Keymap) -> None: "D", "view.flows.duplicate @focus", ["flowlist", "flowview"], "Duplicate flow" ) km.add( - "e", + "x", """ console.choose.cmd Format export.formats console.command export.file {choice} @focus @@ -82,7 +83,12 @@ def map(km: Keymap) -> None: "Export this flow to file", ) km.add("f", "console.command.set view_filter", ["flowlist"], "Set view filter") - km.add("F", "set console_focus_follow toggle", ["flowlist"], "Set focus follow") + km.add( + "F", + "set console_focus_follow toggle", + ["flowlist", "flowview"], + "Set focus follow", + ) km.add( "ctrl l", "console.command cut.clip ", @@ -150,7 +156,7 @@ def map(km: Keymap) -> None: console.choose.cmd Part console.edit.focus.options console.edit.focus {choice} """, - ["flowview"], + ["flowlist", "flowview"], "Edit a flow component", ) km.add( diff --git a/mitmproxy/tools/console/eventlog.py b/mitmproxy/tools/console/eventlog.py index da53539e11..ab6a03c658 100644 --- a/mitmproxy/tools/console/eventlog.py +++ b/mitmproxy/tools/console/eventlog.py @@ -1,8 +1,9 @@ import collections import urwid -from mitmproxy.tools.console import layoutwidget + from mitmproxy import log +from mitmproxy.tools.console import layoutwidget class LogBufferWalker(urwid.SimpleListWalker): diff --git a/mitmproxy/tools/console/flowdetailview.py b/mitmproxy/tools/console/flowdetailview.py index 56161d50ce..e3a28b91a6 100644 --- a/mitmproxy/tools/console/flowdetailview.py +++ b/mitmproxy/tools/console/flowdetailview.py @@ -1,11 +1,11 @@ -from typing import Optional - import urwid import mitmproxy.flow from mitmproxy import http -from mitmproxy.tools.console import common, searchable -from mitmproxy.utils import human, strutils +from mitmproxy.tools.console import common +from mitmproxy.tools.console import searchable +from mitmproxy.utils import human +from mitmproxy.utils import strutils def maybe_timestamp(base, attr): @@ -25,8 +25,8 @@ def flowdetails(state, flow: mitmproxy.flow.Flow): sc = flow.server_conn cc = flow.client_conn - req: Optional[http.Request] - resp: Optional[http.Response] + req: http.Request | None + resp: http.Response | None if isinstance(flow, http.HTTPFlow): req = flow.request resp = flow.response @@ -80,7 +80,7 @@ def flowdetails(state, flow: mitmproxy.flow.Flow): ] if c.altnames: - parts.append(("Alt names", ", ".join(c.altnames))) + parts.append(("Alt names", ", ".join(str(x.value) for x in c.altnames))) text.extend(common.format_keyvals(parts, indent=4)) if cc is not None: diff --git a/mitmproxy/tools/console/flowlist.py b/mitmproxy/tools/console/flowlist.py index f0016577f1..e9723e24f4 100644 --- a/mitmproxy/tools/console/flowlist.py +++ b/mitmproxy/tools/console/flowlist.py @@ -1,5 +1,4 @@ from functools import lru_cache -from typing import Optional import urwid @@ -70,7 +69,7 @@ def set_focus(self, index): self.master.view.focus.index = index @lru_cache(maxsize=None) - def _get(self, pos: int) -> tuple[Optional[FlowItem], Optional[int]]: + def _get(self, pos: int) -> tuple[FlowItem | None, int | None]: if not self.master.view.inbounds(pos): return None, None return FlowItem(self.master, self.master.view[pos]), pos diff --git a/mitmproxy/tools/console/flowview.py b/mitmproxy/tools/console/flowview.py index 20e969c395..a57ead7402 100644 --- a/mitmproxy/tools/console/flowview.py +++ b/mitmproxy/tools/console/flowview.py @@ -1,9 +1,7 @@ import logging - import math import sys from functools import lru_cache -from typing import Optional import urwid @@ -119,7 +117,14 @@ def tab_http_request(self): def tab_http_response(self): flow = self.flow assert isinstance(flow, http.HTTPFlow) - if self.flow.intercepted and flow.response: + + # there is no good way to detect what part of the flow is intercepted, + # so we apply some heuristics to see if it's the HTTP response. + websocket_started = flow.websocket and len(flow.websocket.messages) != 0 + response_is_intercepted = ( + self.flow.intercepted and flow.response and not websocket_started + ) + if response_is_intercepted: return "Response intercepted" else: return "Response" @@ -147,7 +152,14 @@ def tab_udp_stream(self): return "UDP Stream" def tab_websocket_messages(self): - return "WebSocket Messages" + flow = self.flow + assert isinstance(flow, http.HTTPFlow) + assert flow.websocket + + if self.flow.intercepted and len(flow.websocket.messages) != 0: + return "WebSocket Messages intercepted" + else: + return "WebSocket Messages" def tab_details(self): return "Detail" @@ -189,7 +201,7 @@ def _contentview_status_bar(self, description: str, viewmode: str): align="right", ), ] - contentview_status_bar = urwid.AttrWrap(urwid.Columns(cols), "heading") + contentview_status_bar = urwid.AttrMap(urwid.Columns(cols), "heading") return contentview_status_bar FROM_CLIENT_MARKER = ("from_client", f"{common.SYMBOL_FROM_CLIENT} ") @@ -256,21 +268,17 @@ def view_message_stream(self) -> urwid.Widget: viewmode = self.master.commands.call("console.flowview.mode") widget_lines = [] - - from_client = flow.messages[0].from_client for m in flow.messages: _, lines, _ = contentviews.get_message_content_view(viewmode, m, flow) for line in lines: - if from_client: + if m.from_client: line.insert(0, self.FROM_CLIENT_MARKER) else: line.insert(0, self.TO_CLIENT_MARKER) widget_lines.append(urwid.Text(line)) - from_client = not from_client - if flow.intercepted: markup = widget_lines[-1].get_text()[0] widget_lines[-1].set_text(("intercept", markup)) @@ -330,7 +338,7 @@ def _get_content_view(self, viewmode, max_lines, _): text_objects = [] for line in lines: txt = [] - for (style, text) in line: + for style, text in line: if total_chars + len(text) > max_chars: text = text[: max_chars - total_chars] txt.append((style, text)) @@ -404,7 +412,7 @@ def conn_text(self, conn): align="right", ), ] - title = urwid.AttrWrap(urwid.Columns(cols), "heading") + title = urwid.AttrMap(urwid.Columns(cols), "heading") txt.append(title) txt.extend(body) @@ -422,7 +430,7 @@ def conn_text(self, conn): return searchable.Searchable(txt) def dns_message_text( - self, type: str, message: Optional[dns.Message] + self, type: str, message: dns.Message | None ) -> searchable.Searchable: # Keep in sync with web/src/js/components/FlowView/DnsMessages.tsx if message: diff --git a/mitmproxy/tools/console/grideditor/__init__.py b/mitmproxy/tools/console/grideditor/__init__.py index c13ea70e37..6bcae5b94b 100644 --- a/mitmproxy/tools/console/grideditor/__init__.py +++ b/mitmproxy/tools/console/grideditor/__init__.py @@ -1,17 +1,15 @@ from . import base -from .editors import ( - CookieAttributeEditor, - CookieEditor, - DataViewer, - OptionsEditor, - PathEditor, - QueryEditor, - RequestHeaderEditor, - RequestMultipartEditor, - RequestUrlEncodedEditor, - ResponseHeaderEditor, - SetCookieEditor, -) +from .editors import CookieAttributeEditor +from .editors import CookieEditor +from .editors import DataViewer +from .editors import OptionsEditor +from .editors import PathEditor +from .editors import QueryEditor +from .editors import RequestHeaderEditor +from .editors import RequestMultipartEditor +from .editors import RequestUrlEncodedEditor +from .editors import ResponseHeaderEditor +from .editors import SetCookieEditor __all__ = [ "base", diff --git a/mitmproxy/tools/console/grideditor/base.py b/mitmproxy/tools/console/grideditor/base.py index 8c20a23fb0..f03be63279 100644 --- a/mitmproxy/tools/console/grideditor/base.py +++ b/mitmproxy/tools/console/grideditor/base.py @@ -1,19 +1,34 @@ import abc import copy import os -from collections.abc import Callable, Container, Iterable, Sequence -from typing import Any, AnyStr, Optional +from collections.abc import Callable +from collections.abc import Container +from collections.abc import Iterable +from collections.abc import MutableSequence +from collections.abc import Sequence +from typing import Any +from typing import ClassVar +from typing import Literal +from typing import overload import urwid -from mitmproxy.utils import strutils +import mitmproxy.tools.console.master from mitmproxy import exceptions -from mitmproxy.tools.console import signals from mitmproxy.tools.console import layoutwidget -import mitmproxy.tools.console.master +from mitmproxy.tools.console import signals +from mitmproxy.utils import strutils -def read_file(filename: str, escaped: bool) -> AnyStr: +@overload +def read_file(filename: str, escaped: Literal[True]) -> bytes: ... + + +@overload +def read_file(filename: str, escaped: Literal[False]) -> str: ... + + +def read_file(filename: str, escaped: bool) -> bytes | str: filename = os.path.expanduser(filename) try: with open(filename, "r" if escaped else "rb") as f: @@ -58,21 +73,21 @@ def Edit(self, data) -> Cell: def blank(self) -> Any: pass - def keypress(self, key: str, editor: "GridEditor") -> Optional[str]: + def keypress(self, key: str, editor: "GridEditor") -> str | None: return key class GridRow(urwid.WidgetWrap): def __init__( self, - focused: Optional[int], + focused: int | None, editing: bool, editor: "GridEditor", values: tuple[Iterable[bytes], Container[int]], ) -> None: self.focused = focused self.editor = editor - self.edit_col: Optional[Cell] = None + self.edit_col: Cell | None = None errors = values[1] self.fields: Sequence[Any] = [] @@ -84,11 +99,11 @@ def __init__( w = self.editor.columns[i].Display(v) if focused == i: if i in errors: - w = urwid.AttrWrap(w, "focusfield_error") + w = urwid.AttrMap(w, "focusfield_error") else: - w = urwid.AttrWrap(w, "focusfield") + w = urwid.AttrMap(w, "focusfield") elif i in errors: - w = urwid.AttrWrap(w, "field_error") + w = urwid.AttrMap(w, "field_error") self.fields.append(w) fspecs = self.fields[:] @@ -96,7 +111,7 @@ def __init__( fspecs[0] = ("fixed", self.editor.first_width + 2, fspecs[0]) w = urwid.Columns(fspecs, dividechars=2) if focused is not None: - w.set_focus_column(focused) + w.focus_position = focused super().__init__(w) def keypress(self, s, k): @@ -117,11 +132,11 @@ class GridWalker(urwid.ListWalker): """ def __init__(self, lst: Iterable[list], editor: "GridEditor") -> None: - self.lst: Sequence[tuple[Any, set]] = [(i, set()) for i in lst] + self.lst: MutableSequence[tuple[Any, set]] = [(i, set()) for i in lst] self.editor = editor self.focus = 0 self.focus_col = 0 - self.edit_row: Optional[GridRow] = None + self.edit_row: GridRow | None = None def _modified(self): self.editor.show_empty_msg() @@ -150,7 +165,7 @@ def set_value(self, val, focus, focus_col, errors=None): errors = set() row = list(self.lst[focus][0]) row[focus_col] = val - self.lst[focus] = [tuple(row), errors] + self.lst[focus] = [tuple(row), errors] # type: ignore self._modified() def delete_focus(self): @@ -180,7 +195,7 @@ def start_edit(self): self._modified() def stop_edit(self): - if self.edit_row: + if self.edit_row and self.edit_row.edit_col: try: val = self.edit_row.edit_col.get_data() except ValueError: @@ -242,7 +257,7 @@ def __init__(self, lw): class BaseGridEditor(urwid.WidgetWrap): title: str = "" - keyctx = "grideditor" + keyctx: ClassVar[str] = "grideditor" def __init__( self, @@ -252,7 +267,7 @@ def __init__( value: Any, callback: Callable[..., None], *cb_args, - **cb_kwargs + **cb_kwargs, ) -> None: value = self.data_in(copy.deepcopy(value)) self.master = master @@ -280,7 +295,7 @@ def __init__( else: headings.append(c) h = urwid.Columns(headings, dividechars=2) - h = urwid.AttrWrap(h, "heading") + h = urwid.AttrMap(h, "heading") self.walker = GridWalker(self.value, self) self.lb = GridListBox(self.walker) @@ -298,16 +313,14 @@ def layout_popping(self): def show_empty_msg(self): if self.walker.lst: - self._w.set_footer(None) + self._w.footer = None else: - self._w.set_footer( - urwid.Text( - [ - ("highlight", "No values - you should add some. Press "), - ("key", "?"), - ("highlight", " for help."), - ] - ) + self._w.footer = urwid.Text( + [ + ("highlight", "No values - you should add some. Press "), + ("key", "?"), + ("highlight", " for help."), + ] ) def set_subeditor_value(self, val, focus, focus_col): @@ -353,7 +366,7 @@ def data_in(self, data: Any) -> Iterable[list]: """ return data - def is_error(self, col: int, val: Any) -> Optional[str]: + def is_error(self, col: int, val: Any) -> str | None: """ Return None, or a string error message. """ @@ -388,7 +401,7 @@ def cmd_spawn_editor(self): class GridEditor(BaseGridEditor): title = "" columns: Sequence[Column] = () - keyctx = "grideditor" + keyctx: ClassVar[str] = "grideditor" def __init__( self, @@ -396,7 +409,7 @@ def __init__( value: Any, callback: Callable[..., None], *cb_args, - **cb_kwargs + **cb_kwargs, ) -> None: super().__init__( master, self.title, self.columns, value, callback, *cb_args, **cb_kwargs @@ -408,7 +421,7 @@ class FocusEditor(urwid.WidgetWrap, layoutwidget.LayoutWidget): A specialised GridEditor that edits the current focused flow. """ - keyctx = "grideditor" + keyctx: ClassVar[str] = "grideditor" def __init__(self, master): self.master = master diff --git a/mitmproxy/tools/console/grideditor/col_bytes.py b/mitmproxy/tools/console/grideditor/col_bytes.py index f291474170..0ac4a7ed1c 100644 --- a/mitmproxy/tools/console/grideditor/col_bytes.py +++ b/mitmproxy/tools/console/grideditor/col_bytes.py @@ -1,4 +1,5 @@ import urwid + from mitmproxy.tools.console import signals from mitmproxy.tools.console.grideditor import base from mitmproxy.utils import strutils @@ -36,11 +37,11 @@ class Edit(base.Cell): def __init__(self, data: bytes) -> None: d = strutils.bytes_to_escaped_str(data) w = urwid.Edit(edit_text=d, wrap="any", multiline=True) - w = urwid.AttrWrap(w, "editfield") + w = urwid.AttrMap(w, "editfield") super().__init__(w) def get_data(self) -> bytes: - txt = self._w.get_text()[0].strip() + txt = self._w.base_widget.get_text()[0].strip() try: return strutils.escaped_str_to_bytes(txt) except ValueError: diff --git a/mitmproxy/tools/console/grideditor/col_subgrid.py b/mitmproxy/tools/console/grideditor/col_subgrid.py index be4b4271b5..17887b1af2 100644 --- a/mitmproxy/tools/console/grideditor/col_subgrid.py +++ b/mitmproxy/tools/console/grideditor/col_subgrid.py @@ -1,7 +1,8 @@ import urwid -from mitmproxy.tools.console.grideditor import base -from mitmproxy.tools.console import signals + from mitmproxy.net.http import cookies +from mitmproxy.tools.console import signals +from mitmproxy.tools.console.grideditor import base class Column(base.Column): @@ -20,9 +21,7 @@ def blank(self): def keypress(self, key: str, editor): if key in "rRe": - signals.status_message.send( - message="Press enter to edit this field." - ) + signals.status_message.send(message="Press enter to edit this field.") return elif key == "m_select": self.subeditor.grideditor = editor diff --git a/mitmproxy/tools/console/grideditor/col_text.py b/mitmproxy/tools/console/grideditor/col_text.py index ec49ce2a05..d5ad1cba03 100644 --- a/mitmproxy/tools/console/grideditor/col_text.py +++ b/mitmproxy/tools/console/grideditor/col_text.py @@ -28,10 +28,10 @@ def blank(self): class EncodingMixin: def __init__(self, data, encoding_args): self.encoding_args = encoding_args - super().__init__(data.__str__().encode(*self.encoding_args)) + super().__init__(str(data).encode(*self.encoding_args)) # type: ignore def get_data(self): - data = super().get_data() + data = super().get_data() # type: ignore try: return data.decode(*self.encoding_args) except ValueError: diff --git a/mitmproxy/tools/console/grideditor/col_viewany.py b/mitmproxy/tools/console/grideditor/col_viewany.py index 2801587c04..50ffe2a7d3 100644 --- a/mitmproxy/tools/console/grideditor/col_viewany.py +++ b/mitmproxy/tools/console/grideditor/col_viewany.py @@ -1,9 +1,11 @@ """ A display-only column that displays any data type. """ + from typing import Any import urwid + from mitmproxy.tools.console.grideditor import base from mitmproxy.utils import strutils diff --git a/mitmproxy/tools/console/grideditor/editors.py b/mitmproxy/tools/console/grideditor/editors.py index 4e2677b657..4e5eb401df 100644 --- a/mitmproxy/tools/console/grideditor/editors.py +++ b/mitmproxy/tools/console/grideditor/editors.py @@ -1,4 +1,4 @@ -from typing import Any, Union +from typing import Any import urwid @@ -50,10 +50,9 @@ def set_data(self, vals, flow): class RequestMultipartEditor(base.FocusEditor): title = "Edit Multipart Form" - columns = [col_text.Column("Key"), col_text.Column("Value")] + columns = [col_bytes.Column("Key"), col_bytes.Column("Value")] def get_data(self, flow): - return flow.request.multipart_form.items(multi=True) def set_data(self, vals, flow): @@ -65,7 +64,6 @@ class RequestUrlEncodedEditor(base.FocusEditor): columns = [col_text.Column("Key"), col_text.Column("Value")] def get_data(self, flow): - return flow.request.urlencoded_form.items(multi=True) def set_data(self, vals, flow): @@ -192,11 +190,7 @@ class DataViewer(base.GridEditor, layoutwidget.LayoutWidget): def __init__( self, master, - vals: Union[ - list[list[Any]], - list[Any], - Any, - ], + vals: (list[list[Any]] | list[Any] | Any), ) -> None: if vals is not None: # Whatever vals is, make it a list of rows containing lists of column values. diff --git a/mitmproxy/tools/console/keybindings.py b/mitmproxy/tools/console/keybindings.py index eb2db10aa8..a9d7237657 100644 --- a/mitmproxy/tools/console/keybindings.py +++ b/mitmproxy/tools/console/keybindings.py @@ -1,6 +1,7 @@ -import urwid import textwrap +import urwid + from mitmproxy.tools.console import layoutwidget from mitmproxy.tools.console import signals from mitmproxy.utils import signals as utils_signals @@ -8,14 +9,10 @@ HELP_HEIGHT = 5 -keybinding_focus_change = utils_signals.SyncSignal(lambda text: None) - - class KeyItem(urwid.WidgetWrap): def __init__(self, walker, binding, focused): self.walker, self.binding, self.focused = walker, binding, focused - super().__init__(None) - self._w = self.get_widget() + super().__init__(self.get_widget()) def get_widget(self): cmd = textwrap.dedent(self.binding.command).strip() @@ -38,7 +35,8 @@ def keypress(self, size, key): class KeyListWalker(urwid.ListWalker): - def __init__(self, master): + def __init__(self, master, keybinding_focus_change): + self.keybinding_focus_change = keybinding_focus_change self.master = master self.index = 0 @@ -66,7 +64,7 @@ def set_focus(self, index): binding = self.bindings[index] self.index = index self.focus_obj = self._get(self.index) - keybinding_focus_change.send(binding.help or "") + self.keybinding_focus_change.send(binding.help or "") self._modified() def get_next(self, pos): @@ -89,9 +87,9 @@ def positions(self, reverse=False): class KeyList(urwid.ListBox): - def __init__(self, master): + def __init__(self, master, keybinding_focus_change): self.master = master - self.walker = KeyListWalker(master) + self.walker = KeyListWalker(master, keybinding_focus_change) super().__init__(self.walker) def keypress(self, size, key): @@ -108,7 +106,7 @@ def keypress(self, size, key): class KeyHelp(urwid.Frame): - def __init__(self, master): + def __init__(self, master, keybinding_focus_change): self.master = master super().__init__(self.widget("")) self.set_active(False) @@ -117,25 +115,28 @@ def __init__(self, master): def set_active(self, val): h = urwid.Text("Key Binding Help") style = "heading" if val else "heading_inactive" - self.header = urwid.AttrWrap(h, style) + self.header = urwid.AttrMap(h, style) def widget(self, txt): cols, _ = self.master.ui.get_cols_rows() return urwid.ListBox([urwid.Text(i) for i in textwrap.wrap(txt, cols)]) def sig_mod(self, txt): - self.set_body(self.widget(txt)) + self.body = self.widget(txt) class KeyBindings(urwid.Pile, layoutwidget.LayoutWidget): title = "Key Bindings" keyctx = "keybindings" + focus_position: int def __init__(self, master): - oh = KeyHelp(master) + keybinding_focus_change = utils_signals.SyncSignal(lambda text: None) + + oh = KeyHelp(master, keybinding_focus_change) super().__init__( [ - KeyList(master), + KeyList(master, keybinding_focus_change), (HELP_HEIGHT, oh), ] ) @@ -144,13 +145,13 @@ def __init__(self, master): def get_focused_binding(self): if self.focus_position != 0: return None - f = self.widget_list[0] + f = self.contents[0][0] return f.walker.get_focus()[0].binding def keypress(self, size, key): if key == "m_next": self.focus_position = (self.focus_position + 1) % len(self.widget_list) - self.widget_list[1].set_active(self.focus_position == 1) + self.contents[1][0].set_active(self.focus_position == 1) key = None # This is essentially a copypasta from urwid.Pile's keypress handler. @@ -158,6 +159,5 @@ def keypress(self, size, key): item_rows = None if len(size) == 2: item_rows = self.get_item_rows(size, focus=True) - i = self.widget_list.index(self.focus_item) - tsize = self.get_item_size(size, i, True, item_rows) - return self.focus_item.keypress(tsize, key) + tsize = self.get_item_size(size, self.focus_position, True, item_rows) + return self.focus.keypress(tsize, key) diff --git a/mitmproxy/tools/console/keymap.py b/mitmproxy/tools/console/keymap.py index c9539dd750..514066a9bc 100644 --- a/mitmproxy/tools/console/keymap.py +++ b/mitmproxy/tools/console/keymap.py @@ -1,18 +1,17 @@ import logging import os +from collections import defaultdict from collections.abc import Sequence from functools import cache -from typing import Optional -import ruamel.yaml import ruamel.yaml.error +import mitmproxy.types from mitmproxy import command -from mitmproxy.tools.console import commandexecutor -from mitmproxy.tools.console import signals from mitmproxy import ctx from mitmproxy import exceptions -import mitmproxy.types +from mitmproxy.tools.console import commandexecutor +from mitmproxy.tools.console import signals class KeyBindingError(Exception): @@ -62,7 +61,9 @@ def keyspec(self): return self.key.replace("space", " ") def key_short(self) -> str: - return self.key.replace("enter", "⏎").replace("right", "→").replace("space", "␣") + return ( + self.key.replace("enter", "⏎").replace("right", "→").replace("space", "␣") + ) def sortkey(self): return self.key + ",".join(self.contexts) @@ -71,9 +72,7 @@ def sortkey(self): class Keymap: def __init__(self, master): self.executor = commandexecutor.CommandExecutor(master) - self.keys = {} - for c in Contexts: - self.keys[c] = {} + self.keys: dict[str, dict[str, Binding]] = defaultdict(dict) self.bindings = [] def _check_contexts(self, contexts): @@ -135,13 +134,13 @@ def unbind(self, binding: Binding) -> None: self.bindings = [b for b in self.bindings if b != binding] self._on_change() - def get(self, context: str, key: str) -> Optional[Binding]: + def get(self, context: str, key: str) -> Binding | None: if context in self.keys: return self.keys[context].get(key, None) return None @cache - def binding_for_help(self, help: str) -> Optional[Binding]: + def binding_for_help(self, help: str) -> Binding | None: for b in self.bindings: if b.help == help: return b @@ -155,23 +154,25 @@ def list(self, context: str) -> Sequence[Binding]: multi.sort(key=lambda x: x.sortkey()) return single + multi - def handle(self, context: str, key: str) -> Optional[str]: + def handle(self, context: str, key: str) -> str | None: """ Returns the key if it has not been handled, or None. """ b = self.get(context, key) or self.get("global", key) if b: - return self.executor(b.command) + self.executor(b.command) + return None return key - def handle_only(self, context: str, key: str) -> Optional[str]: + def handle_only(self, context: str, key: str) -> str | None: """ Like handle, but ignores global bindings. Returns the key if it has not been handled, or None. """ b = self.get(context, key) if b: - return self.executor(b.command) + self.executor(b.command) + return None return key @@ -187,10 +188,13 @@ def handle_only(self, context: str, key: str) -> Optional[str]: class KeymapConfig: defaultFile = "keys.yaml" + def __init__(self, master): + self.master = master + @command.command("console.keymap.load") def keymap_load_path(self, path: mitmproxy.types.Path) -> None: try: - self.load_path(ctx.master.keymap, path) # type: ignore + self.load_path(self.master.keymap, path) # type: ignore except (OSError, KeyBindingError) as e: raise exceptions.CommandError("Could not load key bindings - %s" % e) from e @@ -198,7 +202,7 @@ def running(self): p = os.path.join(os.path.expanduser(ctx.options.confdir), self.defaultFile) if os.path.exists(p): try: - self.load_path(ctx.master.keymap, p) + self.load_path(self.master.keymap, p) except KeyBindingError as e: logging.error(e) diff --git a/mitmproxy/tools/console/layoutwidget.py b/mitmproxy/tools/console/layoutwidget.py index dd6e910021..5443c4f0db 100644 --- a/mitmproxy/tools/console/layoutwidget.py +++ b/mitmproxy/tools/console/layoutwidget.py @@ -1,3 +1,6 @@ +from typing import ClassVar + + class LayoutWidget: """ All top-level layout widgets and all widgets that may be set in an @@ -6,7 +9,7 @@ class LayoutWidget: # Title is only required for windows, not overlay components title = "" - keyctx = "" + keyctx: ClassVar[str] = "" def key_responder(self): """ diff --git a/mitmproxy/tools/console/master.py b/mitmproxy/tools/console/master.py index a5f4d0c474..a0a1d73d1b 100644 --- a/mitmproxy/tools/console/master.py +++ b/mitmproxy/tools/console/master.py @@ -1,6 +1,6 @@ import asyncio +import contextlib import mimetypes -import os import os.path import shlex import shutil @@ -8,35 +8,34 @@ import subprocess import sys import tempfile -import contextlib import threading from typing import TypeVar -from tornado.platform.asyncio import AddThreadSelectorEventLoop - import urwid +from tornado.platform.asyncio import AddThreadSelectorEventLoop from mitmproxy import addons -from mitmproxy import master from mitmproxy import log -from mitmproxy.addons import errorcheck, intercept +from mitmproxy import master +from mitmproxy import options +from mitmproxy.addons import errorcheck from mitmproxy.addons import eventstore +from mitmproxy.addons import intercept from mitmproxy.addons import readfile from mitmproxy.addons import view -from mitmproxy.contrib.tornado import patch_tornado from mitmproxy.tools.console import consoleaddons from mitmproxy.tools.console import defaultkeys from mitmproxy.tools.console import keymap from mitmproxy.tools.console import palettes from mitmproxy.tools.console import signals from mitmproxy.tools.console import window - +from mitmproxy.utils import strutils T = TypeVar("T", str, bytes) class ConsoleMaster(master.Master): - def __init__(self, opts): + def __init__(self, opts: options.Options) -> None: super().__init__(opts) self.view: view.View = view.View() @@ -48,8 +47,6 @@ def __init__(self, opts): defaultkeys.map(self.keymap) self.options.errored.connect(self.options_error) - self.view_stack = [] - self.addons.add(*addons.default_addons()) self.addons.add( intercept.Intercept(), @@ -57,11 +54,11 @@ def __init__(self, opts): self.events, readfile.ReadFile(), consoleaddons.ConsoleAddon(self), - keymap.KeymapConfig(), - errorcheck.ErrorCheck(log_to_stderr=True), + keymap.KeymapConfig(self), + errorcheck.ErrorCheck(repeat_errors_on_stderr=True), ) - self.window = None + self.window: window.Window | None = None def __setattr__(self, name, value): super().__setattr__(name, value) @@ -124,18 +121,29 @@ def get_editor(self) -> str: else: return "vi" + def get_hex_editor(self) -> str: + editors = ["ghex", "bless", "hexedit", "hxd", "hexer", "hexcurse"] + for editor in editors: + if shutil.which(editor): + return editor + return self.get_editor() + def spawn_editor(self, data: T) -> T: - text = not isinstance(data, bytes) + text = isinstance(data, str) fd, name = tempfile.mkstemp("", "mitmproxy", text=text) + with_hexeditor = isinstance(data, bytes) and strutils.is_mostly_bin(data) with open(fd, "w" if text else "wb") as f: f.write(data) - c = self.get_editor() + if with_hexeditor: + c = self.get_hex_editor() + else: + c = self.get_editor() cmd = shlex.split(c) cmd.append(name) with self.uistopped(): try: subprocess.call(cmd) - except: + except Exception: signals.status_message.send(message="Can't start editor: %s" % c) else: with open(name, "r" if text else "rb") as f: @@ -170,7 +178,7 @@ def spawn_external_viewer(self, data, contenttype): with self.uistopped(): try: subprocess.call(cmd, shell=False) - except: + except Exception: signals.status_message.send( message="Can't start external viewer: %s" % " ".join(c) ) @@ -219,7 +227,6 @@ async def running(self) -> None: loop = asyncio.get_running_loop() if isinstance(loop, getattr(asyncio, "ProactorEventLoop", tuple())): - patch_tornado() # fix for https://bugs.python.org/issue37373 loop = AddThreadSelectorEventLoop(loop) # type: ignore self.loop = urwid.MainLoop( @@ -241,9 +248,11 @@ async def done(self): await super().done() def overlay(self, widget, **kwargs): + assert self.window self.window.set_overlay(widget, **kwargs) def switch_view(self, name): + assert self.window self.window.push(name) def quit(self, a): diff --git a/mitmproxy/tools/console/options.py b/mitmproxy/tools/console/options.py index fbfe3e7465..1e52cca258 100644 --- a/mitmproxy/tools/console/options.py +++ b/mitmproxy/tools/console/options.py @@ -1,16 +1,18 @@ from __future__ import annotations -from collections.abc import Sequence -import urwid -import textwrap import pprint +import textwrap +import typing +from collections.abc import Sequence from typing import Optional +import urwid + from mitmproxy import exceptions from mitmproxy import optmanager from mitmproxy.tools.console import layoutwidget -from mitmproxy.tools.console import signals from mitmproxy.tools.console import overlay +from mitmproxy.tools.console import signals HELP_HEIGHT = 5 @@ -32,12 +34,11 @@ def __init__(self, walker, opt, focused, namewidth, editing): self.walker, self.opt, self.focused = walker, opt, focused self.namewidth = namewidth self.editing = editing - super().__init__(None) - self._w = self.get_widget() + super().__init__(self.get_widget()) def get_widget(self): val = self.opt.current() - if self.opt.typespec == bool: + if self.opt.typespec is bool: displayval = "true" if val else "false" elif not val: displayval = "" @@ -189,7 +190,7 @@ def keypress(self, size, key): self.walker._modified() elif key == "m_select": foc, idx = self.get_focus() - if foc.opt.typespec == bool: + if foc.opt.typespec is bool: self.master.options.toggler(foc.opt.name)() # Bust the focus widget cache self.set_focus(self.walker.index) @@ -206,7 +207,7 @@ def keypress(self, size, key): self.master.options.setter(foc.opt.name), ) ) - elif foc.opt.typespec == Sequence[str]: + elif foc.opt.typespec in (Sequence[str], typing.Sequence[str]): self.master.overlay( overlay.OptionsOverlay( self.master, @@ -230,20 +231,22 @@ def __init__(self, master): def set_active(self, val): h = urwid.Text("Option Help") style = "heading" if val else "heading_inactive" - self.header = urwid.AttrWrap(h, style) + self.header = urwid.AttrMap(h, style) def widget(self, txt): cols, _ = self.master.ui.get_cols_rows() return urwid.ListBox([urwid.Text(i) for i in textwrap.wrap(txt, cols)]) def update_help_text(self, txt: str) -> None: - self.set_body(self.widget(txt)) + self.body = self.widget(txt) class Options(urwid.Pile, layoutwidget.LayoutWidget): title = "Options" keyctx = "options" + focus_position: int + def __init__(self, master): oh = OptionHelp(master) self.optionslist = OptionsList(master, oh) @@ -270,6 +273,5 @@ def keypress(self, size, key): item_rows = None if len(size) == 2: item_rows = self.get_item_rows(size, focus=True) - i = self.widget_list.index(self.focus_item) - tsize = self.get_item_size(size, i, True, item_rows) - return self.focus_item.keypress(tsize, key) + tsize = self.get_item_size(size, self.focus_position, True, item_rows) + return self.focus.keypress(tsize, key) diff --git a/mitmproxy/tools/console/overlay.py b/mitmproxy/tools/console/overlay.py index cd32161918..fad1dd4449 100644 --- a/mitmproxy/tools/console/overlay.py +++ b/mitmproxy/tools/console/overlay.py @@ -2,10 +2,10 @@ import urwid -from mitmproxy.tools.console import signals from mitmproxy.tools.console import grideditor -from mitmproxy.tools.console import layoutwidget from mitmproxy.tools.console import keymap +from mitmproxy.tools.console import layoutwidget +from mitmproxy.tools.console import signals class SimpleOverlay(urwid.Overlay, layoutwidget.LayoutWidget): @@ -45,7 +45,7 @@ def __init__(self, txt, focus, current, shortcut): else: s = "option_selected" if focus else "text" super().__init__( - urwid.AttrWrap( + urwid.AttrMap( urwid.Padding(urwid.Text(txt)), s, ) @@ -107,7 +107,7 @@ def __init__(self, master, title, choices, current, callback): self.walker = ChooserListWalker(choices, current) super().__init__( - urwid.AttrWrap( + urwid.AttrMap( urwid.LineBox( urwid.BoxAdapter(urwid.ListBox(self.walker), len(choices)), title=title, @@ -152,7 +152,7 @@ def __init__(self, master, name, vals, vspace): cols, rows = master.ui.get_cols_rows() self.ge = grideditor.OptionsEditor(master, name, vals) super().__init__( - urwid.AttrWrap( + urwid.AttrMap( urwid.LineBox(urwid.BoxAdapter(self.ge, rows - vspace), title=name), "background", ) @@ -176,7 +176,7 @@ def __init__(self, master, vals): cols, rows = master.ui.get_cols_rows() self.ge = grideditor.DataViewer(master, vals) super().__init__( - urwid.AttrWrap( + urwid.AttrMap( urwid.LineBox(urwid.BoxAdapter(self.ge, rows - 5), title="Data viewer"), "background", ) diff --git a/mitmproxy/tools/console/palettes.py b/mitmproxy/tools/console/palettes.py index 4e86258e21..087b4a3061 100644 --- a/mitmproxy/tools/console/palettes.py +++ b/mitmproxy/tools/console/palettes.py @@ -3,8 +3,10 @@ # # http://urwid.org/manual/displayattributes.html # -from collections.abc import Mapping, Sequence -from typing import Optional +from __future__ import annotations + +from collections.abc import Mapping +from collections.abc import Sequence class Palette: @@ -39,6 +41,7 @@ class Palette: "scheme_tcp", "scheme_udp", "scheme_dns", + "scheme_quic", "scheme_other", "url_punctuation", "url_domain", @@ -88,10 +91,11 @@ class Palette: "commander_hint", ] _fields.extend(["gradient_%02d" % i for i in range(100)]) - high: Optional[Mapping[str, Sequence[str]]] = None + high: Mapping[str, Sequence[str]] | None = None + low: Mapping[str, Sequence[str]] - def palette(self, transparent): - l = [] + def palette(self, transparent: bool): + lst: list[Sequence[str | None]] = [] highback, lowback = None, None if not transparent: if self.high and self.high.get("background"): @@ -100,24 +104,24 @@ def palette(self, transparent): for i in self._fields: if transparent and i == "background": - l.append(["background", "default", "default"]) + lst.append(["background", "default", "default"]) else: - v = [i] + v: list[str | None] = [i] low = list(self.low[i]) if lowback and low[1] == "default": low[1] = lowback v.extend(low) if self.high and i in self.high: v.append(None) - high = list(self.high[i]) + high: list[str | None] = list(self.high[i]) if highback and high[1] == "default": high[1] = highback v.extend(high) elif highback and self.low[i][1] == "default": high = [None, low[0], highback] v.extend(high) - l.append(tuple(v)) - return l + lst.append(tuple(v)) + return lst def gen_gradient(palette, cols): @@ -142,7 +146,6 @@ def gen_rgb_gradient(palette, cols): class LowDark(Palette): - """ Low-color dark background """ @@ -178,6 +181,7 @@ class LowDark(Palette): scheme_tcp=("dark magenta", "default"), scheme_udp=("dark magenta", "default"), scheme_dns=("dark blue", "default"), + scheme_quic=("brown", "default"), scheme_other=("dark magenta", "default"), url_punctuation=("light gray", "default"), url_domain=("white", "default"), @@ -242,7 +246,6 @@ class Dark(LowDark): class LowLight(Palette): - """ Low-color light background """ @@ -278,6 +281,7 @@ class LowLight(Palette): scheme_tcp=("light magenta", "default"), scheme_udp=("light magenta", "default"), scheme_dns=("light blue", "default"), + scheme_quic=("brown", "default"), scheme_other=("light magenta", "default"), url_punctuation=("dark gray", "default"), url_domain=("dark gray", "default"), @@ -399,6 +403,7 @@ class SolarizedLight(LowLight): scheme_tcp=("light magenta", "default"), scheme_udp=("light magenta", "default"), scheme_dns=("light blue", "default"), + scheme_quic=(sol_orange, "default"), scheme_other=("light magenta", "default"), url_punctuation=("dark gray", "default"), url_domain=("dark gray", "default"), diff --git a/mitmproxy/tools/console/quickhelp.py b/mitmproxy/tools/console/quickhelp.py index 58878c3d4b..663a03cfad 100644 --- a/mitmproxy/tools/console/quickhelp.py +++ b/mitmproxy/tools/console/quickhelp.py @@ -1,8 +1,9 @@ """ This module is reponsible for drawing the quick key help at the bottom of mitmproxy. """ + from dataclasses import dataclass -from typing import Optional, Union +from typing import Union import urwid @@ -21,6 +22,7 @@ @dataclass class BasicKeyHelp: """Quick help for urwid-builtin keybindings (i.e. those keys that do not appear in the keymap)""" + key: str @@ -49,7 +51,7 @@ def make_rows(self, keymap: Keymap) -> tuple[urwid.Columns, urwid.Columns]: def make( widget: type[urwid.Widget], - focused_flow: Optional[flow.Flow], + focused_flow: flow.Flow | None, is_root_widget: bool, ) -> QuickHelp: top_label = "" @@ -67,10 +69,12 @@ def make( "Export": "Export this flow to file", "Delete": "Delete flow from view", } - if focused_flow.marked: - top_items["Unmark"] = "Toggle mark on this flow" - else: - top_items["Mark"] = "Toggle mark on this flow" + if widget == FlowListBox: + if focused_flow.marked: + top_items["Unmark"] = "Toggle mark on this flow" + else: + top_items["Mark"] = "Toggle mark on this flow" + top_items["Edit"] = "Edit a flow component" if focused_flow.intercepted: top_items["Resume"] = "Resume this intercepted flow" if focused_flow.modified(): @@ -166,7 +170,7 @@ def _make_row(label: str, items: HelpItems, keymap: Keymap) -> urwid.Columns: cols = [ (len(label), urwid.Text(label)), ] - for (short, long) in items.items(): + for short, long in items.items(): if isinstance(long, BasicKeyHelp): key_short = long.key else: @@ -180,7 +184,7 @@ def _make_row(label: str, items: HelpItems, keymap: Keymap) -> urwid.Columns: " ", short, ], - wrap="clip" + wrap="clip", ) cols.append((14, txt)) diff --git a/mitmproxy/tools/console/signals.py b/mitmproxy/tools/console/signals.py index 024fad5007..63d5f59a00 100644 --- a/mitmproxy/tools/console/signals.py +++ b/mitmproxy/tools/console/signals.py @@ -10,40 +10,39 @@ # Show a status message in the action bar # Instead of using this signal directly, consider emitting a log event. -def _status_message(message: StatusMessage, expire: int = 5) -> None: - ... +def _status_message(message: StatusMessage, expire: int = 5) -> None: ... status_message = signals.SyncSignal(_status_message) # Prompt for input -def _status_prompt(prompt: str, text: str | None, callback: Callable[[str], None]) -> None: - ... +def _status_prompt( + prompt: str, text: str | None, callback: Callable[[str], None] +) -> None: ... status_prompt = signals.SyncSignal(_status_prompt) # Prompt for a single keystroke -def _status_prompt_onekey(prompt: str, keys: list[tuple[str, str]], callback: Callable[[str], None]) -> None: - ... +def _status_prompt_onekey( + prompt: str, keys: list[tuple[str, str]], callback: Callable[[str], None] +) -> None: ... status_prompt_onekey = signals.SyncSignal(_status_prompt_onekey) # Prompt for a command -def _status_prompt_command(partial: str = "", cursor: int | None = None) -> None: - ... +def _status_prompt_command(partial: str = "", cursor: int | None = None) -> None: ... status_prompt_command = signals.SyncSignal(_status_prompt_command) # Call a callback in N seconds -def _call_in(seconds: float, callback: Callable[[], None]) -> None: - ... +def _call_in(seconds: float, callback: Callable[[], None]) -> None: ... call_in = signals.SyncSignal(_call_in) diff --git a/mitmproxy/tools/console/statusbar.py b/mitmproxy/tools/console/statusbar.py index a8ca015909..705427647b 100644 --- a/mitmproxy/tools/console/statusbar.py +++ b/mitmproxy/tools/console/statusbar.py @@ -1,20 +1,24 @@ from __future__ import annotations + from collections.abc import Callable from functools import lru_cache -from typing import Optional import urwid import mitmproxy.tools.console.master -from mitmproxy.tools.console import commandexecutor, flowlist, quickhelp +from mitmproxy.tools.console import commandexecutor from mitmproxy.tools.console import common +from mitmproxy.tools.console import flowlist +from mitmproxy.tools.console import quickhelp from mitmproxy.tools.console import signals from mitmproxy.tools.console.commander import commander from mitmproxy.utils import human @lru_cache -def shorten_message(msg: tuple[str, str] | str, max_width: int) -> list[tuple[str, str]]: +def shorten_message( + msg: tuple[str, str] | str, max_width: int +) -> list[tuple[str, str]]: """ Shorten message so that it fits into a single line in the statusbar. """ @@ -69,7 +73,9 @@ def sig_update(self, flow=None) -> None: if not self.prompting and flow is None or flow == self.master.view.focus.flow: self.show_quickhelp() - def sig_message(self, message: tuple[str, str] | str, expire: int | None = 1) -> None: + def sig_message( + self, message: tuple[str, str] | str, expire: int | None = 1 + ) -> None: if self.prompting: return cols, _ = self.master.ui.get_cols_rows() @@ -84,15 +90,15 @@ def cb(): signals.call_in.send(seconds=expire, callback=cb) - def sig_prompt(self, prompt: str, text: str | None, callback: Callable[[str], None]) -> None: + def sig_prompt( + self, prompt: str, text: str | None, callback: Callable[[str], None] + ) -> None: signals.focus.send(section="footer") self.top._w = urwid.Edit(f"{prompt.strip()}: ", text or "") self.bottom._w = urwid.Text("") self.prompting = callback - def sig_prompt_command( - self, partial: str = "", cursor: Optional[int] = None - ) -> None: + def sig_prompt_command(self, partial: str = "", cursor: int | None = None) -> None: signals.focus.send(section="footer") self.top._w = commander.CommandEdit( self.master, @@ -109,7 +115,9 @@ def execute_command(self, txt: str) -> None: execute = commandexecutor.CommandExecutor(self.master) execute(txt) - def sig_prompt_onekey(self, prompt: str, keys: list[tuple[str, str]], callback: Callable[[str], None]) -> None: + def sig_prompt_onekey( + self, prompt: str, keys: list[tuple[str, str]], callback: Callable[[str], None] + ) -> None: """ Keys are a set of (word, key) tuples. The appropriate key in the word is highlighted. @@ -150,11 +158,11 @@ def keypress(self, size, k): return k def show_quickhelp(self) -> None: - try: - s = self.master.window.focus_stack() + if w := self.master.window: + s = w.focus_stack() focused_widget = type(s.top_widget()) is_top_widget = len(s.stack) == 1 - except AttributeError: # on startup + else: # on startup focused_widget = flowlist.FlowListBox is_top_widget = True focused_flow = self.master.view.focus.flow @@ -196,7 +204,7 @@ def refresh(self) -> None: self.redraw() signals.call_in.send(seconds=self.REFRESHTIME, callback=self.refresh) - def sig_update(self, flow=None, updated=None): + def sig_update(self, *args, **kwargs) -> None: self.redraw() def keypress(self, *args, **kwargs): @@ -270,8 +278,6 @@ def get_status(self) -> list[tuple[str, str] | str]: opts.append("showhost") if not self.master.options.server_replay_refresh: opts.append("norefresh") - if self.master.options.server_replay_kill_extra: - opts.append("killextra") if not self.master.options.upstream_cert: opts.append("no-upstream-cert") if self.master.options.console_focus_follow: @@ -297,7 +303,7 @@ def get_status(self) -> list[tuple[str, str] | str]: def redraw(self) -> None: fc = self.master.commands.execute("view.properties.length") - if self.master.view.focus.flow is None: + if self.master.view.focus.index is None: offset = 0 else: offset = self.master.view.focus.index + 1 @@ -315,16 +321,18 @@ def redraw(self) -> None: ("heading", f"{arrow} {marked} [{offset}/{fc}]".ljust(11)), ] - listen_addrs: list[str] = list(dict.fromkeys( - human.format_address(a) - for a in self.master.addons.get("proxyserver").listen_addrs() - )) + listen_addrs: list[str] = list( + dict.fromkeys( + human.format_address(a) + for a in self.master.addons.get("proxyserver").listen_addrs() + ) + ) if listen_addrs: boundaddr = f"[{', '.join(listen_addrs)}]" else: boundaddr = "" t.extend(self.get_status()) - status = urwid.AttrWrap( + status = urwid.AttrMap( urwid.Columns( [ urwid.Text(t), diff --git a/mitmproxy/tools/console/tabs.py b/mitmproxy/tools/console/tabs.py index a3ba09a109..adf9c481f9 100644 --- a/mitmproxy/tools/console/tabs.py +++ b/mitmproxy/tools/console/tabs.py @@ -8,7 +8,7 @@ def __init__(self, offset, content, attr, onclick): """ p = urwid.Text(content, align="center") p = urwid.Padding(p, align="center", width=("relative", 100)) - p = urwid.AttrWrap(p, attr) + p = urwid.AttrMap(p, attr) urwid.WidgetWrap.__init__(self, p) self.offset = offset self.onclick = onclick @@ -21,11 +21,10 @@ def mouse_event(self, size, event, button, col, row, focus): class Tabs(urwid.WidgetWrap): def __init__(self, tabs, tab_offset=0): - super().__init__("") + super().__init__(urwid.Pile([])) self.tab_offset = tab_offset self.tabs = tabs self.show() - self._w = urwid.Pile([]) def change_tab(self, offset): self.tab_offset = offset @@ -56,4 +55,4 @@ def show(self): self._w = urwid.Frame( body=self.tabs[self.tab_offset % len(self.tabs)][1](), header=headers ) - self._w.set_focus("body") + self._w.focus_position = "body" diff --git a/mitmproxy/tools/console/window.py b/mitmproxy/tools/console/window.py index 5c2a2392ee..8c3a6de1fc 100644 --- a/mitmproxy/tools/console/window.py +++ b/mitmproxy/tools/console/window.py @@ -1,7 +1,7 @@ -import os import re import urwid + from mitmproxy import flow from mitmproxy.tools.console import commands from mitmproxy.tools.console import common @@ -16,11 +16,6 @@ from mitmproxy.tools.console import signals from mitmproxy.tools.console import statusbar -if os.name == "nt": - from mitmproxy.contrib.urwid import raw_display -else: - from urwid import raw_display # type: ignore - class StackWidget(urwid.Frame): def __init__(self, window, widget, title, focus): @@ -28,7 +23,7 @@ def __init__(self, window, widget, title, focus): self.window = window if title: - header = urwid.AttrWrap( + header = urwid.AttrMap( urwid.Text(title), "heading" if focus else "heading_inactive" ) else: @@ -134,7 +129,7 @@ class Window(urwid.Frame): def __init__(self, master): self.statusbar = statusbar.StatusBar(master) super().__init__( - None, header=None, footer=urwid.AttrWrap(self.statusbar, "background") + None, header=None, footer=urwid.AttrMap(self.statusbar, "background") ) self.master = master self.master.view.sig_view_refresh.connect(self.view_changed) @@ -148,7 +143,9 @@ def __init__(self, master): signals.flow_change.connect(self.flow_changed) signals.pop_view_state.connect(self.pop) - self.master.options.subscribe(self.configure, ["console_layout", "console_layout_headers"]) + self.master.options.subscribe( + self.configure, ["console_layout", "console_layout_headers"] + ) self.pane = 0 self.stacks = [WindowStack(master, "flowlist"), WindowStack(master, "eventlog")] @@ -188,7 +185,7 @@ def wrapped(idx): focus_column=self.pane, ) - self.body = urwid.AttrWrap(w, "background") + self.body = urwid.AttrMap(w, "background") signals.window_refresh.send() def flow_changed(self, flow: flow.Flow) -> None: @@ -306,7 +303,7 @@ def keypress(self, size, k): return self.master.keymap.handle(self.focus_stack().top_widget().keyctx, k) -class Screen(raw_display.Screen): +class Screen(urwid.raw_display.Screen): def write(self, data): if common.IS_WINDOWS_OR_WSL: # replace urwid's SI/SO, which produce artifacts under WSL. diff --git a/mitmproxy/tools/dump.py b/mitmproxy/tools/dump.py index 527a93e8b6..2f815b46be 100644 --- a/mitmproxy/tools/dump.py +++ b/mitmproxy/tools/dump.py @@ -1,19 +1,21 @@ from mitmproxy import addons from mitmproxy import master from mitmproxy import options -from mitmproxy.addons import dumper, errorcheck, keepserving, readfile, termlog +from mitmproxy.addons import dumper +from mitmproxy.addons import errorcheck +from mitmproxy.addons import keepserving +from mitmproxy.addons import readfile class DumpMaster(master.Master): def __init__( self, options: options.Options, + loop=None, with_termlog=True, with_dumper=True, ) -> None: - super().__init__(options) - if with_termlog: - self.addons.add(termlog.TermLog()) + super().__init__(options, event_loop=loop, with_termlog=with_termlog) self.addons.add(*addons.default_addons()) if with_dumper: self.addons.add(dumper.Dumper()) diff --git a/mitmproxy/tools/main.py b/mitmproxy/tools/main.py index c124861893..232af9c1da 100644 --- a/mitmproxy/tools/main.py +++ b/mitmproxy/tools/main.py @@ -1,17 +1,23 @@ +from __future__ import annotations + import argparse import asyncio import logging import os import signal import sys -from collections.abc import Callable, Sequence -from typing import Any, Optional, TypeVar +from collections.abc import Callable +from collections.abc import Sequence +from typing import Any +from typing import TypeVar -from mitmproxy import exceptions, master +from mitmproxy import exceptions +from mitmproxy import master from mitmproxy import options from mitmproxy import optmanager from mitmproxy.tools import cmdline -from mitmproxy.utils import debug, arg_check +from mitmproxy.utils import arg_check +from mitmproxy.utils import debug def process_options(parser, opts, args): @@ -28,9 +34,7 @@ def process_options(parser, opts, args): args.flow_detail = 2 adict = { - key: val - for key, val in vars(args).items() - if key in opts and val is not None + key: val for key, val in vars(args).items() if key in opts and val is not None } opts.update(**adict) @@ -42,7 +46,7 @@ def run( master_cls: type[T], make_parser: Callable[[options.Options], argparse.ArgumentParser], arguments: Sequence[str], - extra: Callable[[Any], dict] = None, + extra: Callable[[Any], dict] | None = None, ) -> T: # pragma: no cover """ extra: Extra argument processing callable which returns a dict of @@ -54,6 +58,9 @@ async def main() -> T: logging.getLogger("tornado").setLevel(logging.WARNING) logging.getLogger("asyncio").setLevel(logging.WARNING) logging.getLogger("hpack").setLevel(logging.WARNING) + logging.getLogger("quic").setLevel( + logging.WARNING + ) # aioquic uses a different prefix... debug.register_info_dumpers() opts = options.Options() @@ -109,10 +116,20 @@ def _sigint(*_): def _sigterm(*_): loop.call_soon_threadsafe(master.shutdown) - # We can't use loop.add_signal_handler because that's not available on Windows' Proactorloop, - # but signal.signal just works fine for our purposes. - signal.signal(signal.SIGINT, _sigint) - signal.signal(signal.SIGTERM, _sigterm) + try: + # Prefer loop.add_signal_handler where it is available + # https://github.com/mitmproxy/mitmproxy/issues/7128 + loop.add_signal_handler(signal.SIGINT, _sigint) + loop.add_signal_handler(signal.SIGTERM, _sigterm) + except NotImplementedError: + # Fall back to `signal.signal` for platforms where that is not available (Windows' Proactorloop) + signal.signal(signal.SIGINT, _sigint) + signal.signal(signal.SIGTERM, _sigterm) + + # to fix the issue mentioned https://github.com/mitmproxy/mitmproxy/issues/6744 + # by setting SIGPIPE to SIG_IGN, the process will not terminate and continue to run + if hasattr(signal, "SIGPIPE"): + signal.signal(signal.SIGPIPE, signal.SIG_IGN) await master.run() return master @@ -120,14 +137,14 @@ def _sigterm(*_): return asyncio.run(main()) -def mitmproxy(args=None) -> Optional[int]: # pragma: no cover +def mitmproxy(args=None) -> int | None: # pragma: no cover from mitmproxy.tools import console run(console.master.ConsoleMaster, cmdline.mitmproxy, args) return None -def mitmdump(args=None) -> Optional[int]: # pragma: no cover +def mitmdump(args=None) -> int | None: # pragma: no cover from mitmproxy.tools import dump def extra(args): @@ -144,7 +161,7 @@ def extra(args): return None -def mitmweb(args=None) -> Optional[int]: # pragma: no cover +def mitmweb(args=None) -> int | None: # pragma: no cover from mitmproxy.tools import web run(web.master.WebMaster, cmdline.mitmweb, args) diff --git a/mitmproxy/tools/web/app.py b/mitmproxy/tools/web/app.py index c085934057..5352f8a799 100644 --- a/mitmproxy/tools/web/app.py +++ b/mitmproxy/tools/web/app.py @@ -1,14 +1,17 @@ from __future__ import annotations + import asyncio import hashlib import json import logging import os.path import re +import sys +from collections.abc import Callable from collections.abc import Sequence from io import BytesIO from itertools import islice -from typing import ClassVar, Optional, Union +from typing import ClassVar import tornado.escape import tornado.web @@ -16,7 +19,10 @@ import mitmproxy.flow import mitmproxy.tools.web.master -from mitmproxy import certs, command, contentviews +import mitmproxy_rs +from mitmproxy import certs +from mitmproxy import command +from mitmproxy import contentviews from mitmproxy import flowfilter from mitmproxy import http from mitmproxy import io @@ -25,14 +31,22 @@ from mitmproxy import version from mitmproxy.dns import DNSFlow from mitmproxy.http import HTTPFlow -from mitmproxy.tcp import TCPFlow, TCPMessage -from mitmproxy.udp import UDPFlow, UDPMessage +from mitmproxy.tcp import TCPFlow +from mitmproxy.tcp import TCPMessage +from mitmproxy.udp import UDPFlow +from mitmproxy.udp import UDPMessage from mitmproxy.utils.emoji import emoji from mitmproxy.utils.strutils import always_str from mitmproxy.websocket import WebSocketMessage +TRANSPARENT_PNG = ( + b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08" + b"\x04\x00\x00\x00\xb5\x1c\x0c\x02\x00\x00\x00\x0bIDATx\xdac\xfc\xff\x07" + b"\x00\x02\x00\x01\xfc\xa8Q\rh\x00\x00\x00\x00IEND\xaeB`\x82" +) + -def cert_to_json(certs: Sequence[certs.Cert]) -> Optional[dict]: +def cert_to_json(certs: Sequence[certs.Cert]) -> dict | None: if not certs: return None cert = certs[0] @@ -44,7 +58,7 @@ def cert_to_json(certs: Sequence[certs.Cert]) -> Optional[dict]: "serial": str(cert.serial), "subject": cert.subject, "issuer": cert.issuer, - "altnames": cert.altnames, + "altnames": [str(x.value) for x in cert.altnames], } @@ -103,8 +117,8 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict: f["error"] = flow.error.get_state() if isinstance(flow, HTTPFlow): - content_length: Optional[int] - content_hash: Optional[str] + content_length: int | None + content_hash: str | None if flow.request.raw_content is not None: content_length = len(flow.request.raw_content) @@ -191,9 +205,9 @@ class APIError(tornado.web.HTTPError): class RequestHandler(tornado.web.RequestHandler): - application: "Application" + application: Application - def write(self, chunk: Union[str, bytes, dict, list]): + def write(self, chunk: str | bytes | dict | list): # Writing arrays on the top level is ok nowadays. # http://flask.pocoo.org/docs/0.11/security/#json-security if isinstance(chunk, list): @@ -237,11 +251,11 @@ def filecontents(self): return self.request.body @property - def view(self) -> "mitmproxy.addons.view.View": + def view(self) -> mitmproxy.addons.view.View: return self.application.master.view @property - def master(self) -> "mitmproxy.tools.web.master.WebMaster": + def master(self) -> mitmproxy.tools.web.master.WebMaster: return self.application.master @property @@ -276,8 +290,9 @@ def get(self): class WebSocketEventBroadcaster(tornado.websocket.WebSocketHandler): # raise an error if inherited class doesn't specify its own instance. connections: ClassVar[set[WebSocketEventBroadcaster]] + _send_tasks: ClassVar[set[asyncio.Task]] = set() - def open(self): + def open(self, *args, **kwargs): self.connections.add(self) def on_close(self): @@ -291,7 +306,9 @@ async def wrapper(): except tornado.websocket.WebSocketClosedError: cls.connections.discard(conn) - asyncio.ensure_future(wrapper()) + t = asyncio.create_task(wrapper()) + cls._send_tasks.add(t) + t.add_done_callback(cls._send_tasks.remove) @classmethod def broadcast(cls, **kwargs): @@ -299,7 +316,7 @@ def broadcast(cls, **kwargs): "utf8", "surrogateescape" ) - for conn in cls.connections: + for conn in cls.connections.copy(): cls.send(conn, message) @@ -313,16 +330,22 @@ def get(self): class DumpFlows(RequestHandler): - def get(self): + def get(self) -> None: self.set_header("Content-Disposition", "attachment; filename=flows") self.set_header("Content-Type", "application/octet-stream") + match: Callable[[mitmproxy.flow.Flow], bool] try: match = flowfilter.parse(self.request.arguments["filter"][0].decode()) except ValueError: # thrown py flowfilter.parse if filter is invalid raise APIError(400, f"Invalid filter argument / regex") - except (KeyError, IndexError): # Key+Index: ["filter"][0] can fail, if it's not set - match = bool # returns always true + except ( + KeyError, + IndexError, + ): # Key+Index: ["filter"][0] can fail, if it's not set + + def match(_) -> bool: + return True with BytesIO() as bio: fw = io.FlowWriter(bio) @@ -331,11 +354,11 @@ def get(self): fw.add(f) self.write(bio.getvalue()) - def post(self): + async def post(self): self.view.clear() bio = BytesIO(self.filecontents) - for i in io.FlowReader(bio).stream(): - asyncio.ensure_future(self.master.load_flow(i)) + for f in io.FlowReader(bio).stream(): + await self.master.load_flow(f) bio.close() @@ -381,7 +404,7 @@ def delete(self, flow_id): self.flow.kill() self.view.remove([self.flow]) - def put(self, flow_id): + def put(self, flow_id) -> None: flow: mitmproxy.flow.Flow = self.flow flow.backup() try: @@ -433,6 +456,8 @@ def put(self, flow_id): raise APIError(400, f"Unknown update response.{k}: {v}") elif a == "marked": flow.marked = b + elif a == "comment": + flow.comment = b else: raise APIError(400, f"Unknown update {a}: {b}") except APIError: @@ -469,13 +494,13 @@ def post(self, flow_id, message): def get(self, flow_id, message): message = getattr(self.flow, message) + assert isinstance(self.flow, HTTPFlow) original_cd = message.headers.get("Content-Disposition", None) filename = None if original_cd: - filename = re.search(r'filename=([-\w" .()]+)', original_cd) - if filename: - filename = filename.group(1) + if m := re.search(r'filename=([-\w" .()]+)', original_cd): + filename = m.group(1) if not filename: filename = self.flow.request.path.split("?")[0].split("/")[-1] @@ -492,9 +517,9 @@ class FlowContentView(RequestHandler): def message_to_json( self, viewname: str, - message: Union[http.Message, TCPMessage, UDPMessage, WebSocketMessage], - flow: Union[HTTPFlow, TCPFlow, UDPFlow], - max_lines: Optional[int] = None, + message: http.Message | TCPMessage | UDPMessage | WebSocketMessage, + flow: HTTPFlow | TCPFlow | UDPFlow, + max_lines: int | None = None, ): description, lines, error = contentviews.get_message_content_view( viewname, message, flow @@ -509,7 +534,7 @@ def message_to_json( description=description, ) - def get(self, flow_id, message, content_view): + def get(self, flow_id, message, content_view) -> None: flow = self.flow assert isinstance(flow, (HTTPFlow, TCPFlow, UDPFlow)) @@ -519,6 +544,7 @@ def get(self, flow_id, message, content_view): max_lines = None if message == "messages": + messages: list[TCPMessage] | list[UDPMessage] | list[WebSocketMessage] if isinstance(flow, HTTPFlow) and flow.websocket: messages = flow.websocket.messages elif isinstance(flow, (TCPFlow, UDPFlow)): @@ -544,7 +570,7 @@ def get(self, flow_id, message, content_view): class Commands(RequestHandler): def get(self) -> None: commands = {} - for (name, cmd) in self.master.commands.commands.items(): + for name, cmd in self.master.commands.commands.items(): commands[name] = { "help": cmd.help, "parameters": [ @@ -614,31 +640,75 @@ def get(self): raise tornado.web.HTTPError( 403, reason="To protect against DNS rebinding, mitmweb can only be accessed by IP at the moment. " - "(https://github.com/mitmproxy/mitmproxy/issues/3234)", + "(https://github.com/mitmproxy/mitmproxy/issues/3234)", ) class State(RequestHandler): - def get(self): - self.write({ + # Separate method for testability. + @staticmethod + def get_json(master: mitmproxy.tools.web.master.WebMaster): + return { "version": version.VERSION, "contentViews": [v.name for v in contentviews.views if v.name != "Query"], - "servers": [s.to_json() for s in self.master.proxyserver.servers] - }) + "servers": { + s.mode.full_spec: s.to_json() for s in master.proxyserver.servers + }, + "platform": sys.platform, + } + + def get(self): + self.write(State.get_json(self.master)) + + +class ProcessList(RequestHandler): + @staticmethod + def get_json(): + processes = mitmproxy_rs.process_info.active_executables() + return [ + { + "is_visible": process.is_visible, + "executable": process.executable, + "is_system": process.is_system, + "display_name": process.display_name, + } + for process in processes + ] + + def get(self): + self.write(ProcessList.get_json()) + + +class ProcessImage(RequestHandler): + def get(self): + path = self.get_query_argument("path", None) + + if not path: + raise APIError(400, "Missing 'path' parameter.") + + try: + icon_bytes = mitmproxy_rs.process_info.executable_icon(path) + except Exception: + icon_bytes = TRANSPARENT_PNG + + self.set_header("Content-Type", "image/png") + self.set_header("X-Content-Type-Options", "nosniff") + self.set_header("Cache-Control", "max-age=604800") + self.write(icon_bytes) class GZipContentAndFlowFiles(tornado.web.GZipContentEncoding): CONTENT_TYPES = { "application/octet-stream", - *tornado.web.GZipContentEncoding.CONTENT_TYPES + *tornado.web.GZipContentEncoding.CONTENT_TYPES, } class Application(tornado.web.Application): - master: "mitmproxy.tools.web.master.WebMaster" + master: mitmproxy.tools.web.master.WebMaster def __init__( - self, master: "mitmproxy.tools.web.master.WebMaster", debug: bool + self, master: mitmproxy.tools.web.master.WebMaster, debug: bool ) -> None: self.master = master super().__init__( @@ -686,5 +756,7 @@ def __init__( (r"/options(?:\.json)?", Options), (r"/options/save", SaveOptions), (r"/state(?:\.json)?", State), + (r"/processes", ProcessList), + (r"/executable-icon", ProcessImage), ], ) diff --git a/mitmproxy/tools/web/master.py b/mitmproxy/tools/web/master.py index 0a67108c0a..5ec3ac6025 100644 --- a/mitmproxy/tools/web/master.py +++ b/mitmproxy/tools/web/master.py @@ -1,6 +1,6 @@ +import errno import logging -import errno import tornado.httpserver import tornado.ioloop @@ -8,22 +8,24 @@ from mitmproxy import flow from mitmproxy import log from mitmproxy import master +from mitmproxy import options from mitmproxy import optmanager -from mitmproxy.addons import errorcheck, eventstore +from mitmproxy.addons import errorcheck +from mitmproxy.addons import eventstore from mitmproxy.addons import intercept from mitmproxy.addons import readfile -from mitmproxy.addons import termlog from mitmproxy.addons import view from mitmproxy.addons.proxyserver import Proxyserver -from mitmproxy.contrib.tornado import patch_tornado -from mitmproxy.tools.web import app, webaddons, static_viewer +from mitmproxy.tools.web import app +from mitmproxy.tools.web import static_viewer +from mitmproxy.tools.web import webaddons logger = logging.getLogger(__name__) class WebMaster(master.Master): - def __init__(self, options, with_termlog=True): - super().__init__(options) + def __init__(self, opts: options.Options, with_termlog: bool = True): + super().__init__(opts, with_termlog=with_termlog) self.view = view.View() self.view.sig_view_add.connect(self._sig_view_add) self.view.sig_view_remove.connect(self._sig_view_remove) @@ -36,13 +38,11 @@ def __init__(self, options, with_termlog=True): self.options.changed.connect(self._sig_options_update) - if with_termlog: - self.addons.add(termlog.TermLog()) self.addons.add(*addons.default_addons()) self.addons.add( webaddons.WebAddon(), intercept.Intercept(), - readfile.ReadFile(), + readfile.ReadFileStdin(), static_viewer.StaticViewer(), self.view, self.events, @@ -86,22 +86,27 @@ def _sig_servers_changed(self) -> None: app.ClientConnection.broadcast( resource="state", cmd="update", - data={"servers": [s.to_json() for s in self.proxyserver.servers]} + payload={ + "servers": { + s.mode.full_spec: s.to_json() for s in self.proxyserver.servers + } + }, ) async def running(self): - patch_tornado() # Register tornado with the current event loop tornado.ioloop.IOLoop.current() # Add our web app. - http_server = tornado.httpserver.HTTPServer(self.app) + http_server = tornado.httpserver.HTTPServer( + self.app, max_buffer_size=2**32 + ) # 4GB try: http_server.listen(self.options.web_port, self.options.web_host) except OSError as e: message = f"Web server failed to listen on {self.options.web_host or '*'}:{self.options.web_port} with {e}" if e.errno == errno.EADDRINUSE: - message += f"\nTry specifying a different port by using `--set web_port={self.options.web_port + 1}`." + message += f"\nTry specifying a different port by using `--set web_port={self.options.web_port + 2}`." raise OSError(e.errno, message, e.filename) from e logger.info( diff --git a/mitmproxy/tools/web/static/app.css b/mitmproxy/tools/web/static/app.css index 6cb8d58282..482f23252d 100644 --- a/mitmproxy/tools/web/static/app.css +++ b/mitmproxy/tools/web/static/app.css @@ -1,2 +1,2 @@ -html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}.resource-icon{width:32px;height:32px}.resource-icon-css{background-image:url(images/chrome-devtools/resourceCSSIcon.png)}.resource-icon-document{background-image:url(images/chrome-devtools/resourceDocumentIcon.png)}.resource-icon-js{background-image:url(images/chrome-devtools/resourceJSIcon.png)}.resource-icon-plain{background-image:url(images/chrome-devtools/resourcePlainIcon.png)}.resource-icon-executable{background-image:url(images/resourceExecutableIcon.png)}.resource-icon-flash{background-image:url(images/resourceFlashIcon.png)}.resource-icon-image{background-image:url(images/resourceImageIcon.png)}.resource-icon-java{background-image:url(images/resourceJavaIcon.png)}.resource-icon-not-modified{background-image:url(images/resourceNotModifiedIcon.png)}.resource-icon-redirect{background-image:url(images/resourceRedirectIcon.png)}.resource-icon-websocket{background-image:url(images/resourceWebSocketIcon.png)}.resource-icon-tcp{background-image:url(images/resourceTcpIcon.png)}.resource-icon-udp{background-image:url(images/resourceUdpIcon.png)}.resource-icon-dns{background-image:url(images/resourceDnsIcon.png)}#container,#mitmproxy,body,html{height:100%;margin:0;overflow:hidden}#container{display:flex;flex-direction:column;outline:0}#container>.eventlog,#container>footer,#container>header{flex:0 0 auto}.main-view{flex:1 1 auto;height:0;display:flex;flex-direction:row}.main-view.vertical{flex-direction:column}.main-view .flow-detail,.main-view .flow-table{flex:1 1 auto}.splitter{flex:0 0 1px;background-color:#aaa;position:relative}.splitter>div{position:absolute}.splitter.splitter-x{cursor:col-resize}.splitter.splitter-x>div{margin-left:-1px;width:4px;height:100%}.splitter.splitter-y{cursor:row-resize}.splitter.splitter-y>div{margin-top:-1px;height:4px;width:100%}.nav-tabs{border-bottom:solid #a6a6a6 1px}.nav-tabs>a{display:inline-block;border:solid transparent 1px;text-decoration:none}.nav-tabs>a.active{background-color:#fff;border-color:#a6a6a6;border-bottom-color:#fff}.nav-tabs>a.special{color:#fff;background-color:#396cad;border-bottom-color:#396cad}.nav-tabs>a.special:hover{background-color:#5386c6}.nav-tabs-lg>a{padding:3px 14px;margin:0 2px -1px}.nav-tabs-sm>a{padding:0 7px;margin:2px 2px -1px}header{padding-top:6px;background-color:#fff}header>div{display:block;margin:0;padding:0;border-bottom:solid #a6a6a6 1px;height:95px;overflow:visible}.menu-group{margin:0 5px 0 6px;display:inline-block;height:95px}.menu-content{height:79px;display:flow-root}.menu-content>a{display:inline-block}.menu-content>.btn,.menu-content>a>.btn{height:79px;text-align:center;margin:0 1px;padding:12px 5px;border:none;border-radius:0}.menu-content>.btn i,.menu-content>a>.btn i{font-size:20px;display:block;margin:0 auto 5px}.menu-content>.btn.btn-sm{height:26.33333333px;padding:0 5px}.menu-content>.btn.btn-sm i{display:inline-block;font-size:14px;margin:0}.menu-entry{text-align:left;height:26.33333333px;line-height:1;padding:.5rem 1rem}.menu-entry label{font-size:1.2rem;font-weight:400;margin:0}.menu-entry input[type=checkbox]{margin:0 2px;vertical-align:middle}.menu-legend{color:#777;height:16px;text-align:center;font-size:12px;padding:0 5px}.menu-group+.menu-group:before{margin-left:-6px;content:" ";border-left:solid 1px #e6e6e6;margin-top:10px;height:75px;position:absolute}.main-menu{display:flex}.main-menu .menu-group{width:50%}.main-menu .btn-sm{margin-top:6px}.filter-input{margin:4px 0}.filter-input .popover{top:27px;left:43px;display:block;max-width:none;opacity:.9}@media (max-width:767px){.filter-input .popover{top:16px;left:29px;right:2px}}.filter-input .popover .popover-content{max-height:500px;overflow-y:auto}.filter-input .popover .popover-content tr{cursor:pointer}.filter-input .popover .popover-content tr:hover{background-color:hsla(209,52%,84%,.5)!important}.connection-indicator{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em;float:right;margin:5px;opacity:1;transition:all 1s linear}a.connection-indicator:focus,a.connection-indicator:hover{color:#fff;text-decoration:none;cursor:pointer}.connection-indicator:empty{display:none}.btn .connection-indicator{position:relative;top:-1px}.connection-indicator.fetching,.connection-indicator.init{background-color:#5bc0de}.connection-indicator.established{background-color:#5cb85c;opacity:0}.connection-indicator.error{background-color:#d9534f;transition:all .2s linear}.connection-indicator.offline{background-color:#f0ad4e;opacity:1}.flow-table{width:100%;overflow-y:scroll;overflow-x:hidden}.flow-table table{width:100%;table-layout:fixed}.flow-table thead tr{background-color:#f2f2f2;border-bottom:solid #bebebe 1px;line-height:23px}.flow-table th{font-weight:400;position:relative!important;padding-left:1px;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.flow-table th.sort-asc,.flow-table th.sort-desc{background-color:#fafafa}.flow-table th.sort-asc:after,.flow-table th.sort-desc:after{font:normal normal normal 14px/1 FontAwesome;position:absolute;right:3px;top:3px;padding:2px;background-color:rgba(250,250,250,.8)}.flow-table th.sort-asc:after{content:"\f0de"}.flow-table th.sort-desc:after{content:"\f0dd"}.flow-table tr{cursor:pointer;background-color:#fff}.flow-table tr:nth-child(even){background-color:#f2f2f2}.flow-table tr.selected{background-color:#e0ebf5!important}.flow-table tr.selected.highlighted{background-color:#7bbefc!important}.flow-table tr.highlighted{background-color:#ffeb99}.flow-table tr.highlighted:nth-child(even){background-color:#ffe57f}.flow-table td{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.flow-table tr.intercepted:not(.has-response) .col-method,.flow-table tr.intercepted:not(.has-response) .col-path{color:#ff7f00}.flow-table tr.intercepted.has-response .col-size,.flow-table tr.intercepted.has-response .col-status,.flow-table tr.intercepted.has-response .col-time{color:#ff7f00}.flow-table .fa{line-height:inherit}.flow-table .col-tls{width:10px}.flow-table .col-tls-https{background-color:rgba(0,185,0,.5)}.flow-table .col-icon{width:32px}.flow-table .col-path .fa{margin-left:0;font-size:16px}.flow-table .col-path .fa-repeat{color:green}.flow-table .col-path .fa-pause{color:#ff7f00}.flow-table .col-path .fa-exclamation,.flow-table .col-path .fa-times{color:#8b0000}.flow-table .col-method{width:60px}.flow-table .col-status{width:50px}.flow-table .col-size{width:70px}.flow-table .col-time{width:50px}.flow-table .col-timestamp{width:170px}.flow-table td.col-size,.flow-table td.col-time,.flow-table td.col-timestamp{text-align:right}.flow-table .col-quickactions{width:0;direction:rtl;overflow:hidden;background-color:inherit;font-size:20px}.flow-table .col-quickactions *{direction:ltr}.flow-table .col-quickactions.hover,.flow-table tr:hover .col-quickactions{overflow:visible}.flow-table .col-quickactions>div{height:32px;background-color:inherit;display:inline-flex;align-items:center}.flow-table .col-quickactions>div>a{margin-right:2px;height:32px;width:32px;border-radius:16px;text-align:center}.flow-table .col-quickactions>div>a:hover{background-color:rgba(0,0,0,.05)}.flow-table .col-quickactions .fa-play{transform:translate(1px,2px)}.flow-table .col-quickactions .fa-repeat{transform:translate(0,2px)}.flow-detail{width:100%;overflow:hidden;display:flex;flex-direction:column}.flow-detail nav{background-color:#f2f2f2}.flow-detail section{overflow-y:scroll;flex:1;padding:5px 12px 10px}.flow-detail section>footer{box-shadow:0 0 3px gray;padding:2px;margin:0;height:23px}.flow-detail .first-line{font-family:Menlo,Monaco,Consolas,"Courier New",monospace;background-color:#428bca;color:#fff;margin:0 -8px 2px;padding:4px 8px;border-radius:5px;word-break:break-all;max-height:100px;overflow-y:auto}.flow-detail .contentview{margin:0 -12px;padding:0 12px}.flow-detail .contentview .controls{display:flex;align-items:center}.flow-detail .contentview .controls h5{flex:1;font-size:12px;font-weight:700;margin:10px 0}.flow-detail .contentview pre button:not(:only-child){margin-top:6px}.flow-detail hr{margin:0}.inline-input{display:inline;margin:0 -3px;padding:0 3px;border:solid transparent 1px}.inline-input:hover{box-shadow:0 0 0 1px rgba(0,0,0,.0125),0 2px 4px rgba(0,0,0,.05),0 2px 6px rgba(0,0,0,.025);background-color:rgba(255,255,255,.1)}.inline-input[placeholder]:empty:not(:focus-visible):before{content:attr(placeholder);color:#d3d3d3;font-style:italic}.inline-input[contenteditable]{outline-width:0;box-shadow:0 0 0 1px rgba(0,0,0,.05),0 2px 4px rgba(0,0,0,.2),0 2px 6px rgba(0,0,0,.1);background-color:rgba(255,255,255,.2)}.inline-input[contenteditable].has-warning{color:#ffb8b8}.certificate-table,.connection-table,.timing-table{width:100%;table-layout:fixed;word-break:break-all}.certificate-table td:nth-child(2),.connection-table td:nth-child(2),.timing-table td:nth-child(2){font-family:Menlo,Monaco,Consolas,"Courier New",monospace;width:70%}.certificate-table tr:not(:first-child),.connection-table tr:not(:first-child),.timing-table tr:not(:first-child){border-top:1px solid #f7f7f7}.certificate-table td,.connection-table td,.timing-table td{vertical-align:top}.connection-table td:first-child{padding-right:1em}.headers,.trailers{position:relative;min-height:2ex;overflow-wrap:break-word}.headers .kv-row,.trailers .kv-row{margin-bottom:.3em;max-height:12.4ex;overflow-y:auto}.headers .kv-key,.trailers .kv-key{font-weight:700}.headers .kv-value,.trailers .kv-value{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}.headers .inline-input,.trailers .inline-input{background-color:#fff}.headers .kv-add-row,.trailers .kv-add-row{opacity:0;color:#666;position:absolute;bottom:4px;right:4px;transition:all .1s ease-in-out}.headers:hover .kv-add-row,.trailers:hover .kv-add-row{opacity:1}.connection-table td,.timing-table td{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}dl.cert-attributes{display:flex;flex-flow:row;flex-wrap:wrap;margin-bottom:0}dl.cert-attributes dd,dl.cert-attributes dt{text-overflow:ellipsis;overflow:hidden}dl.cert-attributes dt{flex:0 0 2em}dl.cert-attributes dd{flex:0 0 calc(100% - 2em)}.dns-request table td,.dns-request table th,.dns-response table td,.dns-response table th{padding-right:1rem}.flowview-image{text-align:center;padding:10px 0}.flowview-image img{max-width:100%;max-height:100%}.edit-flow-container{position:fixed;right:20px}.edit-flow{cursor:pointer;position:absolute;right:0;top:5px;height:40px;width:40px;border-radius:20px;z-index:10000;background-color:rgba(255,255,255,.7);border:solid 2px rgba(248,145,59,.7);text-align:center;font-size:22px;line-height:37px;transition:all .1s ease-in-out}.edit-flow:hover{background-color:rgba(239,108,0,.7);color:rgba(0,0,0,.8);border:solid 2px transparent}.eventlog{height:200px;flex:0 0 auto;display:flex;flex-direction:column}.eventlog>div{background-color:#f2f2f2;padding:0 5px;flex:0 0 auto;border-top:1px solid #aaa;cursor:row-resize}.eventlog>pre{flex:1 1 auto;margin:0;border-radius:0;overflow-x:auto;overflow-y:scroll;background-color:#fcfcfc}.eventlog .fa-close{cursor:pointer;float:right;color:grey;padding:3px 0;padding-left:10px}.eventlog .fa-close:hover{color:#000}.eventlog .btn-toggle{margin-top:-2px;margin-left:3px;padding:2px 2px;font-size:10px;line-height:10px;border-radius:2px}.eventlog .label{cursor:pointer;vertical-align:middle;display:inline-block;margin-top:-2px;margin-left:3px}footer{box-shadow:0 -1px 3px #d3d3d3;padding:0 0 4px 3px}footer .label{margin-right:3px}.CodeMirror{border:1px solid #ccc;height:auto!important}.CodeMirror{font-family:monospace;height:300px;color:#000;direction:ltr}.CodeMirror-lines{padding:4px 0}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-fat-cursor-mark{background-color:rgba(20,255,20,.5);-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite}.cm-animate-fat-cursor{width:auto;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;left:0;right:0;top:-50px;bottom:0;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-type,.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta{color:#555}.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-s-default .cm-error{color:red}.cm-invalidchar{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0b0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#a22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-50px;margin-right:-50px;padding-bottom:50px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:50px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none;outline:0}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-50px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;font-variant-ligatures:contextual}.CodeMirror-wrap pre.CodeMirror-line,.CodeMirror-wrap pre.CodeMirror-line-like{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;padding:.1px}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-focused div.CodeMirror-cursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background-color:#ffa;background-color:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0}.contentview .header{font-weight:700}.contentview .highlight{font-weight:700}.contentview .offset{color:#00f}.contentview .codeeditor{margin-bottom:12px}.contentview .Token_Name_Tag{color:#006400}.contentview .Token_Literal_String{color:#b22222}.contentview .Token_Literal_Number{color:purple}.contentview .Token_Keyword_Constant{color:#00f}.modal-visible{display:block}.modal-dialog{overflow-y:initial!important}.modal-body{max-height:calc(100vh - 200px);overflow-y:auto}.dropdown-menu{margin:0!important}.dropdown-menu>li>a{padding:3px 10px}.command-title{background-color:#f2f2f2;border:1px solid #aaa}.command-result{display:block;margin:0;background-color:#fcfcfc;height:100px;max-height:100px;overflow:auto}.command-suggestion{background-color:#9c9c9c}.argument-suggestion{background-color:hsla(209,52%,84%,.5)!important}.command>.popover{display:block;position:relative;max-width:none}.available-commands{overflow:auto}.wireguard-config{margin:1rem 0;display:flex;flex-wrap:wrap;column-gap:2rem;align-items:center}.wireguard-config>*{margin:0} +html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}.resource-icon{width:32px;height:32px}.resource-icon-css{background-image:url(images/chrome-devtools/resourceCSSIcon.png)}.resource-icon-document{background-image:url(images/chrome-devtools/resourceDocumentIcon.png)}.resource-icon-js{background-image:url(images/chrome-devtools/resourceJSIcon.png)}.resource-icon-plain{background-image:url(images/chrome-devtools/resourcePlainIcon.png)}.resource-icon-executable{background-image:url(images/resourceExecutableIcon.png)}.resource-icon-flash{background-image:url(images/resourceFlashIcon.png)}.resource-icon-image{background-image:url(images/resourceImageIcon.png)}.resource-icon-java{background-image:url(images/resourceJavaIcon.png)}.resource-icon-not-modified{background-image:url(images/resourceNotModifiedIcon.png)}.resource-icon-redirect{background-image:url(images/resourceRedirectIcon.png)}.resource-icon-websocket{background-image:url(images/resourceWebSocketIcon.png)}.resource-icon-tcp{background-image:url(images/resourceTcpIcon.png)}.resource-icon-udp{background-image:url(images/resourceUdpIcon.png)}.resource-icon-dns{background-image:url(images/resourceDnsIcon.png)}.resource-icon-quic{background-image:url(images/resourceQuicIcon.png)}#container,#mitmproxy,body,html{height:100%;margin:0;overflow:hidden}#container{display:flex;flex-direction:column;outline:0}#container>.eventlog,#container>footer,#container>header{flex:0 0 auto}.main-view{flex:1 1 auto;height:0;display:flex;flex-direction:row}.main-view.vertical{flex-direction:column}.main-view .flow-detail,.main-view .flow-table{flex:1 1 auto}.splitter{flex:0 0 1px;background-color:#aaa;position:relative}.splitter>div{position:absolute}.splitter.splitter-x{cursor:col-resize}.splitter.splitter-x>div{margin-left:-1px;width:4px;height:100%}.splitter.splitter-y{cursor:row-resize}.splitter.splitter-y>div{margin-top:-1px;height:4px;width:100%}.nav-tabs{border-bottom:solid #a6a6a6 1px}.nav-tabs>a{display:inline-block;border:solid transparent 1px;text-decoration:none}.nav-tabs>a.active{background-color:#fff;border-color:#a6a6a6;border-bottom-color:#fff}.nav-tabs>a.special{color:#fff;background-color:#396cad;border-bottom-color:#396cad}.nav-tabs>a.special:hover{background-color:#5386c6}.nav-tabs-lg>a{padding:3px 14px;margin:0 2px -1px}.nav-tabs-sm>a{padding:0 7px;margin:2px 2px -1px}header{padding-top:6px;background-color:#fff}header>div:not(:empty){display:block;margin:0;padding:0;border-bottom:solid #a6a6a6 1px;height:95px;overflow:visible}.menu-group{margin:0 5px 0 6px;display:inline-block;height:95px}.menu-content{height:79px;display:flow-root}.menu-content>a{display:inline-block}.menu-content>.btn,.menu-content>a>.btn{height:79px;text-align:center;margin:0 1px;padding:12px 5px;border:none;border-radius:0}.menu-content>.btn i,.menu-content>a>.btn i{font-size:20px;display:block;margin:0 auto 5px}.menu-content>.btn.btn-sm{height:26.33333333px;padding:0 5px}.menu-content>.btn.btn-sm i{display:inline-block;font-size:14px;margin:0}.menu-entry{text-align:left;height:26.33333333px;line-height:1;padding:.5rem 1rem}.menu-entry label{font-size:1.2rem;font-weight:400;margin:0}.menu-entry input[type=checkbox]{margin:0 2px;vertical-align:middle}.menu-legend{color:#777;height:16px;text-align:center;font-size:12px;padding:0 5px}.menu-group+.menu-group:before{margin-left:-6px;content:" ";border-left:solid 1px #e6e6e6;margin-top:10px;height:75px;position:absolute}.main-menu{display:flex}.main-menu .menu-group{width:50%}.main-menu .btn-sm{margin-top:6px}.filter-input{margin:4px 0}.filter-input .popover{top:27px;left:43px;display:block;max-width:none;opacity:.9}@media (max-width:767px){.filter-input .popover{top:16px;left:29px;right:2px}}.filter-input .popover .popover-content{max-height:500px;overflow-y:auto}.filter-input .popover .popover-content tr{cursor:pointer}.filter-input .popover .popover-content tr:hover{background-color:hsla(209,52%,84%,.5)!important}.connection-indicator{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em;float:right;margin:5px;opacity:1;transition:all 1s linear}a.connection-indicator:focus,a.connection-indicator:hover{color:#fff;text-decoration:none;cursor:pointer}.connection-indicator:empty{display:none}.btn .connection-indicator{position:relative;top:-1px}.connection-indicator.fetching,.connection-indicator.init{background-color:#5bc0de}.connection-indicator.established{background-color:#5cb85c;opacity:0}.connection-indicator.error{background-color:#d9534f;transition:all .2s linear}.connection-indicator.offline{background-color:#f0ad4e;opacity:1}.flow-table{width:100%;overflow-y:scroll;overflow-x:hidden}.flow-table table{width:100%;table-layout:fixed}.flow-table thead tr{background-color:#f2f2f2;border-bottom:solid #bebebe 1px;line-height:23px}.flow-table th{font-weight:400;position:relative!important;padding-left:1px;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.flow-table th.sort-asc,.flow-table th.sort-desc{background-color:#fafafa}.flow-table th.sort-asc:after,.flow-table th.sort-desc:after{font:normal normal normal 14px/1 FontAwesome;position:absolute;right:3px;top:3px;padding:2px;background-color:rgba(250,250,250,.8)}.flow-table th.sort-asc:after{content:"\f0de"}.flow-table th.sort-desc:after{content:"\f0dd"}.flow-table tr{cursor:pointer;background-color:#fff}.flow-table tr:nth-child(even){background-color:#f2f2f2}.flow-table tr.selected{background-color:#e0ebf5!important}.flow-table tr.selected.highlighted{background-color:#7bbefc!important}.flow-table tr.highlighted{background-color:#ffeb99}.flow-table tr.highlighted:nth-child(even){background-color:#ffe57f}.flow-table td{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.flow-table tr.intercepted:not(.has-response) .col-method,.flow-table tr.intercepted:not(.has-response) .col-path{color:#ff7f00}.flow-table tr.intercepted.has-response .col-size,.flow-table tr.intercepted.has-response .col-status,.flow-table tr.intercepted.has-response .col-time{color:#ff7f00}.flow-table .fa{line-height:inherit}.flow-table .col-tls{width:10px}.flow-table .col-tls-https{background-color:rgba(0,185,0,.5)}.flow-table .col-index{text-align:right;width:4ch}.flow-table .col-icon{width:32px}.flow-table .col-path .fa{margin-left:0;font-size:16px}.flow-table .col-path .fa-repeat{color:green}.flow-table .col-path .fa-pause{color:#ff7f00}.flow-table .col-path .fa-exclamation,.flow-table .col-path .fa-times{color:#8b0000}.flow-table .col-method{width:60px}.flow-table .col-version{width:80px}.flow-table .col-status{width:50px}.flow-table .col-size{width:70px}.flow-table .col-time{width:50px}.flow-table .col-timestamp{width:170px}.flow-table .col-comment{width:150px;padding-left:5px}.flow-table td.col-comment,.flow-table td.col-size,.flow-table td.col-time,.flow-table td.col-timestamp{text-align:right}.flow-table .col-quickactions{width:0;direction:rtl;overflow:hidden;background-color:inherit;font-size:20px}.flow-table .col-quickactions *{direction:ltr}.flow-table tr:hover .col-quickactions{overflow:visible}.flow-table .col-quickactions>div{height:32px;background-color:inherit;display:inline-flex;align-items:center}.flow-table .col-quickactions>div>a{margin-right:2px;height:32px;width:32px;border-radius:16px;text-align:center}.flow-table .col-quickactions>div>a:hover{background-color:rgba(0,0,0,.05)}.flow-table .col-quickactions .fa-play{transform:translate(1px,2px)}.flow-table .col-quickactions .fa-repeat{transform:translate(0,2px)}.flow-detail{width:100%;overflow:hidden;display:flex;flex-direction:column}.flow-detail nav{background-color:#f2f2f2}.flow-detail section{overflow-y:scroll;flex:1;padding:5px 12px 10px}.flow-detail section>footer{box-shadow:0 0 3px gray;padding:2px;margin:0;height:23px}.flow-detail .close-button{margin-top:2px;padding-left:10px;padding-right:7px;color:grey;font-size:15px;border:none}.flow-detail .close-button:hover{color:#000}.flow-detail .first-line{font-family:Menlo,Monaco,Consolas,"Courier New",monospace;background-color:#428bca;color:#fff;margin:0 -8px 2px;padding:4px 8px;border-radius:5px;word-break:break-all;max-height:100px;overflow-y:auto}.flow-detail .contentview{margin:0 -12px;padding:0 12px}.flow-detail .contentview .controls{display:flex;align-items:center}.flow-detail .contentview .controls h5{flex:1;font-size:12px;font-weight:700;margin:10px 0}.flow-detail .contentview pre button:not(:only-child){margin-top:6px}.flow-detail hr{margin:0}.inline-input{display:inline;margin:0 -3px;padding:0 3px;border:solid transparent 1px}.inline-input:hover{box-shadow:0 0 0 1px rgba(0,0,0,.0125),0 2px 4px rgba(0,0,0,.05),0 2px 6px rgba(0,0,0,.025);background-color:rgba(255,255,255,.1)}.inline-input[placeholder]:empty:not(:focus-visible):before{content:attr(placeholder);color:#d3d3d3;font-style:italic}.inline-input[contenteditable]{outline-width:0;box-shadow:0 0 0 1px rgba(0,0,0,.05),0 2px 4px rgba(0,0,0,.2),0 2px 6px rgba(0,0,0,.1);background-color:rgba(255,255,255,.2)}.inline-input[contenteditable].has-warning{color:#ffb8b8}.certificate-table,.connection-table,.timing-table{width:100%;table-layout:fixed;word-break:break-all}.certificate-table td:nth-child(2),.connection-table td:nth-child(2),.timing-table td:nth-child(2){font-family:Menlo,Monaco,Consolas,"Courier New",monospace;width:70%}.certificate-table tr:not(:first-child),.connection-table tr:not(:first-child),.timing-table tr:not(:first-child){border-top:1px solid #f7f7f7}.certificate-table td,.connection-table td,.timing-table td{vertical-align:top}.connection-table td:first-child{padding-right:1em}.headers,.trailers{position:relative;min-height:2ex;overflow-wrap:break-word}.headers .kv-row,.trailers .kv-row{margin-bottom:.3em;max-height:12.4ex;overflow-y:auto}.headers .kv-key,.trailers .kv-key{font-weight:700}.headers .kv-value,.trailers .kv-value{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}.headers .inline-input,.trailers .inline-input{background-color:#fff}.headers .kv-add-row,.trailers .kv-add-row{opacity:0;color:#666;position:absolute;bottom:4px;right:4px;transition:all .1s ease-in-out}.headers:hover .kv-add-row,.trailers:hover .kv-add-row{opacity:1}.connection-table td,.timing-table td{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}dl.cert-attributes{display:flex;flex-flow:row;flex-wrap:wrap;margin-bottom:0}dl.cert-attributes dd,dl.cert-attributes dt{text-overflow:ellipsis;overflow:hidden}dl.cert-attributes dt{flex:0 0 2em}dl.cert-attributes dd{flex:0 0 calc(100% - 2em)}.dns-request table td,.dns-request table th,.dns-response table td,.dns-response table th{padding-right:1rem}.flowview-image{text-align:center;padding:10px 0}.flowview-image img{max-width:100%;max-height:100%}.edit-flow-container{position:fixed;right:20px}.edit-flow{cursor:pointer;position:absolute;right:0;top:5px;height:40px;width:40px;border-radius:20px;z-index:10000;background-color:rgba(255,255,255,.7);border:solid 2px rgba(248,145,59,.7);text-align:center;font-size:22px;line-height:37px;transition:all .1s ease-in-out}.edit-flow:hover{background-color:rgba(239,108,0,.7);color:rgba(0,0,0,.8);border:solid 2px transparent}.eventlog{height:200px;flex:0 0 auto;display:flex;flex-direction:column}.eventlog>div{background-color:#f2f2f2;padding:0 5px;flex:0 0 auto;border-top:1px solid #aaa;cursor:row-resize}.eventlog>pre{flex:1 1 auto;margin:0;border-radius:0;overflow-x:auto;overflow-y:scroll;background-color:#fcfcfc}.eventlog .fa-close{cursor:pointer;float:right;color:grey;padding:3px 0;padding-left:10px}.eventlog .fa-close:hover{color:#000}.eventlog .btn-toggle{margin-top:-2px;margin-left:3px;padding:2px 2px;font-size:10px;line-height:10px;border-radius:2px}.eventlog .label{cursor:pointer;vertical-align:middle;display:inline-block;margin-top:-2px;margin-left:3px}footer{box-shadow:0 -1px 3px #d3d3d3;padding:0 0 4px 3px}footer .label{margin-right:3px}.CodeMirror{border:1px solid #ccc;height:auto!important}.CodeMirror{font-family:monospace;height:300px;color:#000;direction:ltr}.CodeMirror-lines{padding:4px 0}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-fat-cursor .CodeMirror-line::selection,.cm-fat-cursor .CodeMirror-line>span::selection,.cm-fat-cursor .CodeMirror-line>span>span::selection{background:0 0}.cm-fat-cursor .CodeMirror-line::-moz-selection,.cm-fat-cursor .CodeMirror-line>span::-moz-selection,.cm-fat-cursor .CodeMirror-line>span>span::-moz-selection{background:0 0}.cm-fat-cursor{caret-color:transparent}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;left:0;right:0;top:-50px;bottom:0;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-type,.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta{color:#555}.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-s-default .cm-error{color:red}.cm-invalidchar{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0b0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#a22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-50px;margin-right:-50px;padding-bottom:50px;height:100%;outline:0;position:relative;z-index:0}.CodeMirror-sizer{position:relative;border-right:50px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none;outline:0}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-50px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;font-variant-ligatures:contextual}.CodeMirror-wrap pre.CodeMirror-line,.CodeMirror-wrap pre.CodeMirror-line-like{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;padding:.1px}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-focused div.CodeMirror-cursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background-color:#ffa;background-color:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0}.contentview .header{font-weight:700}.contentview .highlight{font-weight:700}.contentview .offset{color:#00f}.contentview .codeeditor{margin-bottom:12px}.contentview .Token_Name_Tag{color:#006400}.contentview .Token_Literal_String{color:#b22222}.contentview .Token_Literal_Number{color:purple}.contentview .Token_Keyword_Constant{color:#00f}.modal-visible{display:block}.modal-dialog{overflow-y:initial!important}.modal-body{max-height:calc(100vh - 200px);overflow-y:auto}.dropdown-menu{margin:0!important;max-height:250px;overflow-y:scroll}.dropdown-menu>li>a{padding:3px 10px}.command-title{background-color:#f2f2f2;border:1px solid #aaa}.command-result{display:block;margin:0;background-color:#fcfcfc;height:100px;max-height:100px;overflow:auto}.command-suggestion{background-color:#9c9c9c}.argument-suggestion{background-color:hsla(209,52%,84%,.5)!important}.command>.popover{display:block;position:relative;max-width:none}.available-commands{overflow:auto}.wireguard-config{margin:1rem 0;display:flex;flex-wrap:wrap;column-gap:2rem;align-items:center}.wireguard-config>*{margin:0}.modes{padding:1em 2em;overflow-y:auto;height:100%;width:100vw}.modes h3{margin-bottom:10px}.modes .modes-category{padding-left:10px}.modes .green-left-border{border-left:10px solid #77c77a}.modes .gray-left-border{border-left:10px solid #b2b2b2}.modes .modes-container{display:flex;flex-direction:column;gap:25px}.modes .mode-title{font-size:16px;font-weight:600}.modes .mode-description{color:#b2b2b2;margin-top:-10px}.modes .mode-entry{text-align:left;display:flex;align-items:center;font-size:1.5rem;font-weight:400;margin:0}.modes .mode-entry input[type=checkbox]{margin:0 10px;vertical-align:middle;width:1.8rem;height:1.8rem}.modes .mode-entry p{margin:0}.modes .mode-status{margin-left:10px;margin-top:5px;font-weight:600}.modes .mode-local i{font-size:1.8rem;cursor:pointer}.modes .mode-local .processes-container{display:flex;flex-direction:row;align-items:center;gap:5px;margin-left:5px}.modes .mode-local .selected-processes{display:flex;flex-wrap:wrap;gap:5px;overflow:hidden}.modes .mode-local .selected-process{display:flex;flex-direction:row;gap:5px;background-color:#e0e0e0;padding:5px;border-radius:4px;height:30px;font-size:12px;font-weight:500;align-content:center;align-items:center}.modes .mode-local .selected-process>i{font-size:15px}.modes .mode-local .dropdown-container{display:flex;flex-direction:row;align-items:center}.modes .mode-local .local-dropdown{position:relative;width:200px}.modes .mode-local .local-dropdown .dropdown-header{padding:5px;border:1px solid #ccc;border-radius:4px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;margin-right:10px;height:30px}.modes .mode-local .local-dropdown input{border:none;flex:1;outline:0;color:#000}.modes .mode-local .local-dropdown .dropdown-list{width:100%;max-height:300px;overflow-y:auto;z-index:1;list-style:none;padding:0;margin-bottom:0}.modes .mode-local .local-dropdown .dropdown-item{display:flex;flex-direction:row;align-items:center;padding:8px 10px;margin:4px 0;border-radius:4px;cursor:pointer;transition:background-color .2s ease}.modes .mode-local .local-dropdown .dropdown-item.selected{background-color:#e0e0e0}.modes .mode-local .local-dropdown .dropdown-item:hover{background-color:#f0f0f0}.modes .mode-local .local-dropdown .process-details{display:flex;flex-direction:row;align-items:center}.modes .mode-local .local-dropdown .process-icon{margin-right:5px;margin-left:5px;color:#5f6368;width:25px;height:25px}.modes .mode-local .local-dropdown .process-name{font-weight:500;color:#333}.modes .mode-local .local-popover button{font-size:1rem}.modes .mode-local .local-popover i{font-size:1.5rem}@supports (anchor-name:--test){.modes .mode-local .local-popover>div{margin:0;position:absolute;left:0;top:calc(anchor(bottom) + 10px);justify-self:anchor-center;align-self:auto}}.modes .mode-local .local-popover>div{gap:.5rem;align-items:center}.modes .mode-local .local-popover>div:popover-open{display:block}.modes .mode-input{border:1px solid #ccc;margin-left:10px;border-radius:4px;min-width:120px;height:25px}.modes .mode-reverse-input{border:1px solid #ccc;margin-left:10px;margin-right:10px;border-radius:4px;min-width:70px;height:25px}.modes .mode-upstream-input{border:1px solid #ccc;margin-left:10px;margin-right:5px;border-radius:4px;min-width:70px;height:25px}.modes .mode-reverse-dropdown{margin:0 5px;border:1px solid #ccc;border-radius:4px;height:25px}.modes .mode-reverse-servers{display:flex;flex-direction:column;gap:10px}.modes .mode-reverse-add-server{margin-left:10px;display:flex;flex-direction:row;cursor:pointer;font-weight:500;opacity:.5;transition:all 50ms ease-in-out}.modes .mode-reverse-add-server:hover{opacity:1}.modes .mode-reverse-add-server i{font-size:2rem;margin-right:5px}.modes .mode-popover>button{background-color:transparent;border:none;cursor:pointer;font-size:2rem}@supports (anchor-name:--test){.modes .mode-popover>div{margin:0;position:absolute;left:calc(anchor(right) + 5px);align-self:anchor-center}}.modes .mode-popover>div{border:1px solid #ccc;border-radius:4px;padding:10px 15px;background-color:#f9f9f9;box-shadow:0 4px 8px rgba(0,0,0,.1);grid-template-columns:9em 1fr;grid-auto-rows:min-content;gap:.5rem;align-items:center}.modes .mode-popover>div:popover-open{display:grid}.modes .mode-popover>div>h4{text-align:center;grid-column:1/-1}.modes .mode-popover>div>.mode-input{text-align:right;background-color:#fff;margin:0}.modes .missing-mode-container{color:#b2b2b2}.modes .missing-mode-container .title-icon-container{display:flex;flex-direction:row;align-items:center;gap:5px}.modes .missing-mode-container .title-icon-container i{font-size:1.8rem} /*# sourceMappingURL=app.css.map */ diff --git a/mitmproxy/tools/web/static/app.js b/mitmproxy/tools/web/static/app.js index 228782695f..d97e25bb0d 100644 --- a/mitmproxy/tools/web/static/app.js +++ b/mitmproxy/tools/web/static/app.js @@ -1,123 +1,137 @@ -(()=>{var fD=Object.create;var qc=Object.defineProperty,cD=Object.defineProperties,pD=Object.getOwnPropertyDescriptor,dD=Object.getOwnPropertyDescriptors,hD=Object.getOwnPropertyNames,Ag=Object.getOwnPropertySymbols,mD=Object.getPrototypeOf,Cw=Object.prototype.hasOwnProperty,Fb=Object.prototype.propertyIsEnumerable;var bw=(e,t,n)=>t in e?qc(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,ke=(e,t)=>{for(var n in t||(t={}))Cw.call(t,n)&&bw(e,n,t[n]);if(Ag)for(var n of Ag(t))Fb.call(t,n)&&bw(e,n,t[n]);return e},Pt=(e,t)=>cD(e,dD(t)),Bb=e=>qc(e,"__esModule",{value:!0}),o=(e,t)=>qc(e,"name",{value:t,configurable:!0});var Ws=(e,t)=>{var n={};for(var l in e)Cw.call(e,l)&&t.indexOf(l)<0&&(n[l]=e[l]);if(e!=null&&Ag)for(var l of Ag(e))t.indexOf(l)<0&&Fb.call(e,l)&&(n[l]=e[l]);return n};var Ue=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),Hb=(e,t)=>{Bb(e);for(var n in t)qc(e,n,{get:t[n],enumerable:!0})},gD=(e,t,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let l of hD(t))!Cw.call(e,l)&&l!=="default"&&qc(e,l,{get:()=>t[l],enumerable:!(n=pD(t,l))||n.enumerable});return e},fe=e=>gD(Bb(qc(e!=null?fD(mD(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var Vc=(e,t,n)=>(bw(e,typeof t!="symbol"?t+"":t,n),n);var Ia=(e,t,n)=>new Promise((l,d)=>{var h=C=>{try{v(n.next(C))}catch(k){d(k)}},c=C=>{try{v(n.throw(C))}catch(k){d(k)}},v=C=>C.done?l(C.value):Promise.resolve(C.value).then(h,c);v((n=n.apply(e,t)).next())});var Ew=Ue((f4,Ub)=>{"use strict";var Wb=Object.getOwnPropertySymbols,vD=Object.prototype.hasOwnProperty,yD=Object.prototype.propertyIsEnumerable;function wD(e){if(e==null)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}o(wD,"toObject");function xD(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de",Object.getOwnPropertyNames(e)[0]==="5")return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;var l=Object.getOwnPropertyNames(t).map(function(h){return t[h]});if(l.join("")!=="0123456789")return!1;var d={};return"abcdefghijklmnopqrst".split("").forEach(function(h){d[h]=h}),Object.keys(Object.assign({},d)).join("")==="abcdefghijklmnopqrst"}catch(h){return!1}}o(xD,"shouldUseNative");Ub.exports=xD()?Object.assign:function(e,t){for(var n,l=wD(e),d,h=1;h{"use strict";var _w=Ew(),Kc=60103,zb=60106;Et.Fragment=60107;Et.StrictMode=60108;Et.Profiler=60114;var $b=60109,jb=60110,qb=60112;Et.Suspense=60113;var Vb=60115,Kb=60116;typeof Symbol=="function"&&Symbol.for&&(Co=Symbol.for,Kc=Co("react.element"),zb=Co("react.portal"),Et.Fragment=Co("react.fragment"),Et.StrictMode=Co("react.strict_mode"),Et.Profiler=Co("react.profiler"),$b=Co("react.provider"),jb=Co("react.context"),qb=Co("react.forward_ref"),Et.Suspense=Co("react.suspense"),Vb=Co("react.memo"),Kb=Co("react.lazy"));var Co,Gb=typeof Symbol=="function"&&Symbol.iterator;function SD(e){return e===null||typeof e!="object"?null:(e=Gb&&e[Gb]||e["@@iterator"],typeof e=="function"?e:null)}o(SD,"y");function Yd(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n{"use strict";iE.exports=nE()});var fE=Ue(Rt=>{"use strict";var Yc,Xd,Ig,Ow;typeof performance=="object"&&typeof performance.now=="function"?(oE=performance,Rt.unstable_now=function(){return oE.now()}):(Mw=Date,sE=Mw.now(),Rt.unstable_now=function(){return Mw.now()-sE});var oE,Mw,sE;typeof window=="undefined"||typeof MessageChannel!="function"?(Xc=null,Aw=null,Dw=o(function(){if(Xc!==null)try{var e=Rt.unstable_now();Xc(!0,e),Xc=null}catch(t){throw setTimeout(Dw,0),t}},"w"),Yc=o(function(e){Xc!==null?setTimeout(Yc,0,e):(Xc=e,setTimeout(Dw,0))},"f"),Xd=o(function(e,t){Aw=setTimeout(e,t)},"g"),Ig=o(function(){clearTimeout(Aw)},"h"),Rt.unstable_shouldYield=function(){return!1},Ow=Rt.unstable_forceFrameRate=function(){}):(lE=window.setTimeout,aE=window.clearTimeout,typeof console!="undefined"&&(uE=window.cancelAnimationFrame,typeof window.requestAnimationFrame!="function"&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills"),typeof uE!="function"&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills")),Qd=!1,Zd=null,Fg=-1,Rw=5,Iw=0,Rt.unstable_shouldYield=function(){return Rt.unstable_now()>=Iw},Ow=o(function(){},"k"),Rt.unstable_forceFrameRate=function(e){0>e||125>>1,d=e[l];if(d!==void 0&&0Wg(c,n))C!==void 0&&0>Wg(C,c)?(e[l]=C,e[v]=n,l=v):(e[l]=c,e[h]=n,l=h);else if(C!==void 0&&0>Wg(C,n))e[l]=C,e[v]=n,l=v;else break e}}return t}return null}o(Hg,"K");function Wg(e,t){var n=e.sortIndex-t.sortIndex;return n!==0?n:e.id-t.id}o(Wg,"I");var Us=[],Fa=[],TD=1,bo=null,Yn=3,Ug=!1,df=!1,Jd=!1;function Hw(e){for(var t=os(Fa);t!==null;){if(t.callback===null)Hg(Fa);else if(t.startTime<=e)Hg(Fa),t.sortIndex=t.expirationTime,Bw(Us,t);else break;t=os(Fa)}}o(Hw,"T");function Ww(e){if(Jd=!1,Hw(e),!df)if(os(Us)!==null)df=!0,Yc(Uw);else{var t=os(Fa);t!==null&&Xd(Ww,t.startTime-e)}}o(Ww,"U");function Uw(e,t){df=!1,Jd&&(Jd=!1,Ig()),Ug=!0;var n=Yn;try{for(Hw(t),bo=os(Us);bo!==null&&(!(bo.expirationTime>t)||e&&!Rt.unstable_shouldYield());){var l=bo.callback;if(typeof l=="function"){bo.callback=null,Yn=bo.priorityLevel;var d=l(bo.expirationTime<=t);t=Rt.unstable_now(),typeof d=="function"?bo.callback=d:bo===os(Us)&&Hg(Us),Hw(t)}else Hg(Us);bo=os(Us)}if(bo!==null)var h=!0;else{var c=os(Fa);c!==null&&Xd(Ww,c.startTime-t),h=!1}return h}finally{bo=null,Yn=n,Ug=!1}}o(Uw,"V");var kD=Ow;Rt.unstable_IdlePriority=5;Rt.unstable_ImmediatePriority=1;Rt.unstable_LowPriority=4;Rt.unstable_NormalPriority=3;Rt.unstable_Profiling=null;Rt.unstable_UserBlockingPriority=2;Rt.unstable_cancelCallback=function(e){e.callback=null};Rt.unstable_continueExecution=function(){df||Ug||(df=!0,Yc(Uw))};Rt.unstable_getCurrentPriorityLevel=function(){return Yn};Rt.unstable_getFirstCallbackNode=function(){return os(Us)};Rt.unstable_next=function(e){switch(Yn){case 1:case 2:case 3:var t=3;break;default:t=Yn}var n=Yn;Yn=t;try{return e()}finally{Yn=n}};Rt.unstable_pauseExecution=function(){};Rt.unstable_requestPaint=kD;Rt.unstable_runWithPriority=function(e,t){switch(e){case 1:case 2:case 3:case 4:case 5:break;default:e=3}var n=Yn;Yn=e;try{return t()}finally{Yn=n}};Rt.unstable_scheduleCallback=function(e,t,n){var l=Rt.unstable_now();switch(typeof n=="object"&&n!==null?(n=n.delay,n=typeof n=="number"&&0l?(e.sortIndex=n,Bw(Fa,e),os(Us)===null&&e===os(Fa)&&(Jd?Ig():Jd=!0,Xd(Ww,n-l))):(e.sortIndex=d,Bw(Us,e),df||Ug||(df=!0,Yc(Uw))),e};Rt.unstable_wrapCallback=function(e){var t=Yn;return function(){var n=Yn;Yn=t;try{return e.apply(this,arguments)}finally{Yn=n}}}});var pE=Ue((h4,cE)=>{"use strict";cE.exports=fE()});var ZT=Ue(Lo=>{"use strict";var zg=Oe(),hr=Ew(),Tn=pE();function we(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;nt}return!1}o(OD,"na");function vi(e,t,n,l,d,h,c){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=l,this.attributeNamespace=d,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=h,this.removeEmptyString=c}o(vi,"B");var Rn={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){Rn[e]=new vi(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];Rn[t]=new vi(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){Rn[e]=new vi(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){Rn[e]=new vi(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){Rn[e]=new vi(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){Rn[e]=new vi(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){Rn[e]=new vi(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){Rn[e]=new vi(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){Rn[e]=new vi(e,5,!1,e.toLowerCase(),null,!1,!1)});var zw=/[\-:]([a-z])/g;function $w(e){return e[1].toUpperCase()}o($w,"pa");"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(zw,$w);Rn[t]=new vi(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(zw,$w);Rn[t]=new vi(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(zw,$w);Rn[t]=new vi(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){Rn[e]=new vi(e,1,!1,e.toLowerCase(),null,!1,!1)});Rn.xlinkHref=new vi("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){Rn[e]=new vi(e,1,!1,e.toLowerCase(),null,!0,!0)});function jw(e,t,n,l){var d=Rn.hasOwnProperty(t)?Rn[t]:null,h=d!==null?d.type===0:l?!1:!(!(2v||d[c]!==h[v])return` -`+d[c].replace(" at new "," at ");while(1<=c&&0<=v);break}}}finally{e1=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?oh(e):""}o(Vg,"Pa");function MD(e){switch(e.tag){case 5:return oh(e.type);case 16:return oh("Lazy");case 13:return oh("Suspense");case 19:return oh("SuspenseList");case 0:case 2:case 15:return e=Vg(e.type,!1),e;case 11:return e=Vg(e.type.render,!1),e;case 22:return e=Vg(e.type._render,!1),e;case 1:return e=Vg(e.type,!0),e;default:return""}}o(MD,"Qa");function Zc(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Ba:return"Fragment";case gf:return"Portal";case rh:return"Profiler";case qw:return"StrictMode";case nh:return"Suspense";case jg:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Kw:return(e.displayName||"Context")+".Consumer";case Vw:return(e._context.displayName||"Context")+".Provider";case $g:var t=e.render;return t=t.displayName||t.name||"",e.displayName||(t!==""?"ForwardRef("+t+")":"ForwardRef");case qg:return Zc(e.type);case Yw:return Zc(e._render);case Gw:t=e._payload,e=e._init;try{return Zc(e(t))}catch(n){}}return null}o(Zc,"Ra");function Ha(e){switch(typeof e){case"boolean":case"number":case"object":case"string":case"undefined":return e;default:return""}}o(Ha,"Sa");function wE(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}o(wE,"Ta");function AD(e){var t=wE(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),l=""+e[t];if(!e.hasOwnProperty(t)&&typeof n!="undefined"&&typeof n.get=="function"&&typeof n.set=="function"){var d=n.get,h=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return d.call(this)},set:function(c){l=""+c,h.call(this,c)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return l},setValue:function(c){l=""+c},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}o(AD,"Ua");function Kg(e){e._valueTracker||(e._valueTracker=AD(e))}o(Kg,"Va");function xE(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),l="";return e&&(l=wE(e)?e.checked?"true":"false":e.value),e=l,e!==n?(t.setValue(e),!0):!1}o(xE,"Wa");function Gg(e){if(e=e||(typeof document!="undefined"?document:void 0),typeof e=="undefined")return null;try{return e.activeElement||e.body}catch(t){return e.body}}o(Gg,"Xa");function t1(e,t){var n=t.checked;return hr({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}o(t1,"Ya");function SE(e,t){var n=t.defaultValue==null?"":t.defaultValue,l=t.checked!=null?t.checked:t.defaultChecked;n=Ha(t.value!=null?t.value:n),e._wrapperState={initialChecked:l,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}o(SE,"Za");function CE(e,t){t=t.checked,t!=null&&jw(e,"checked",t,!1)}o(CE,"$a");function r1(e,t){CE(e,t);var n=Ha(t.value),l=t.type;if(n!=null)l==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(l==="submit"||l==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?n1(e,t.type,n):t.hasOwnProperty("defaultValue")&&n1(e,t.type,Ha(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}o(r1,"ab");function bE(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var l=t.type;if(!(l!=="submit"&&l!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}o(bE,"cb");function n1(e,t,n){(t!=="number"||Gg(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}o(n1,"bb");function DD(e){var t="";return zg.Children.forEach(e,function(n){n!=null&&(t+=n)}),t}o(DD,"db");function i1(e,t){return e=hr({children:void 0},t),(t=DD(t.children))&&(e.children=t),e}o(i1,"eb");function Jc(e,t,n,l){if(e=e.options,t){t={};for(var d=0;d=n.length))throw Error(we(93));n=n[0]}t=n}t==null&&(t=""),n=t}e._wrapperState={initialValue:Ha(n)}}o(EE,"hb");function _E(e,t){var n=Ha(t.value),l=Ha(t.defaultValue);n!=null&&(n=""+n,n!==e.value&&(e.value=n),t.defaultValue==null&&e.defaultValue!==n&&(e.defaultValue=n)),l!=null&&(e.defaultValue=""+l)}o(_E,"ib");function TE(e){var t=e.textContent;t===e._wrapperState.initialValue&&t!==""&&t!==null&&(e.value=t)}o(TE,"jb");var s1={html:"http://www.w3.org/1999/xhtml",mathml:"http://www.w3.org/1998/Math/MathML",svg:"http://www.w3.org/2000/svg"};function kE(e){switch(e){case"svg":return"http://www.w3.org/2000/svg";case"math":return"http://www.w3.org/1998/Math/MathML";default:return"http://www.w3.org/1999/xhtml"}}o(kE,"lb");function l1(e,t){return e==null||e==="http://www.w3.org/1999/xhtml"?kE(t):e==="http://www.w3.org/2000/svg"&&t==="foreignObject"?"http://www.w3.org/1999/xhtml":e}o(l1,"mb");var Yg,NE=function(e){return typeof MSApp!="undefined"&&MSApp.execUnsafeLocalFunction?function(t,n,l,d){MSApp.execUnsafeLocalFunction(function(){return e(t,n,l,d)})}:e}(function(e,t){if(e.namespaceURI!==s1.svg||"innerHTML"in e)e.innerHTML=t;else{for(Yg=Yg||document.createElement("div"),Yg.innerHTML=""+t.valueOf().toString()+"",t=Yg.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function sh(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}o(sh,"pb");var lh={animationIterationCount:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},RD=["Webkit","ms","Moz","O"];Object.keys(lh).forEach(function(e){RD.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),lh[t]=lh[e]})});function LE(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||lh.hasOwnProperty(e)&&lh[e]?(""+t).trim():t+"px"}o(LE,"sb");function PE(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var l=n.indexOf("--")===0,d=LE(n,t[n],l);n==="float"&&(n="cssFloat"),l?e.setProperty(n,d):e[n]=d}}o(PE,"tb");var ID=hr({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function a1(e,t){if(t){if(ID[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(we(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(we(60));if(!(typeof t.dangerouslySetInnerHTML=="object"&&"__html"in t.dangerouslySetInnerHTML))throw Error(we(61))}if(t.style!=null&&typeof t.style!="object")throw Error(we(62))}}o(a1,"vb");function u1(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}o(u1,"wb");function f1(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}o(f1,"xb");var c1=null,ep=null,tp=null;function OE(e){if(e=_h(e)){if(typeof c1!="function")throw Error(we(280));var t=e.stateNode;t&&(t=gv(t),c1(e.stateNode,e.type,t))}}o(OE,"Bb");function ME(e){ep?tp?tp.push(e):tp=[e]:ep=e}o(ME,"Eb");function AE(){if(ep){var e=ep,t=tp;if(tp=ep=null,OE(e),t)for(e=0;el?0:1<n;n++)t.push(e);return t}o(E1,"Zc");function rv(e,t,n){e.pendingLanes|=t;var l=t-1;e.suspendedLanes&=l,e.pingedLanes&=l,e=e.eventTimes,t=31-$a(t),e[t]=n}o(rv,"$c");var $a=Math.clz32?Math.clz32:ZD,XD=Math.log,QD=Math.LN2;function ZD(e){return e===0?32:31-(XD(e)/QD|0)|0}o(ZD,"ad");var JD=Tn.unstable_UserBlockingPriority,eR=Tn.unstable_runWithPriority,nv=!0;function tR(e,t,n,l){vf||d1();var d=_1,h=vf;vf=!0;try{DE(d,e,t,n,l)}finally{(vf=h)||m1()}}o(tR,"gd");function rR(e,t,n,l){eR(JD,_1.bind(null,e,t,n,l))}o(rR,"id");function _1(e,t,n,l){if(nv){var d;if((d=(t&4)==0)&&0=yh),s_=String.fromCharCode(32),l_=!1;function a_(e,t){switch(e){case"keyup":return _R.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}o(a_,"ge");function u_(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}o(u_,"he");var lp=!1;function kR(e,t){switch(e){case"compositionend":return u_(t);case"keypress":return t.which!==32?null:(l_=!0,s_);case"textInput":return e=t.data,e===s_&&l_?null:e;default:return null}}o(kR,"je");function NR(e,t){if(lp)return e==="compositionend"||!A1&&a_(e,t)?(e=e_(),iv=k1=ja=null,lp=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=l}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=m_(n)}}o(g_,"Le");function v_(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?v_(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}o(v_,"Me");function y_(){for(var e=window,t=Gg();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch(l){n=!1}if(n)e=t.contentWindow;else break;t=Gg(e.document)}return t}o(y_,"Ne");function R1(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}o(R1,"Oe");var BR=Ll&&"documentMode"in document&&11>=document.documentMode,ap=null,I1=null,Ch=null,F1=!1;function w_(e,t,n){var l=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;F1||ap==null||ap!==Gg(l)||(l=ap,"selectionStart"in l&&R1(l)?l={start:l.selectionStart,end:l.selectionEnd}:(l=(l.ownerDocument&&l.ownerDocument.defaultView||window).getSelection(),l={anchorNode:l.anchorNode,anchorOffset:l.anchorOffset,focusNode:l.focusNode,focusOffset:l.focusOffset}),Ch&&Sh(Ch,l)||(Ch=l,l=pv(I1,"onSelect"),0dp||(e.current=j1[dp],j1[dp]=null,dp--)}o(fr,"H");function _r(e,t){dp++,j1[dp]=e.current,e.current=t}o(_r,"I");var Ka={},Xn=Va(Ka),Ri=Va(!1),xf=Ka;function hp(e,t){var n=e.type.contextTypes;if(!n)return Ka;var l=e.stateNode;if(l&&l.__reactInternalMemoizedUnmaskedChildContext===t)return l.__reactInternalMemoizedMaskedChildContext;var d={},h;for(h in n)d[h]=t[h];return l&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=d),d}o(hp,"Ef");function Ii(e){return e=e.childContextTypes,e!=null}o(Ii,"Ff");function vv(){fr(Ri),fr(Xn)}o(vv,"Gf");function D_(e,t,n){if(Xn.current!==Ka)throw Error(we(168));_r(Xn,t),_r(Ri,n)}o(D_,"Hf");function R_(e,t,n){var l=e.stateNode;if(e=t.childContextTypes,typeof l.getChildContext!="function")return n;l=l.getChildContext();for(var d in l)if(!(d in e))throw Error(we(108,Zc(t)||"Unknown",d));return hr({},n,l)}o(R_,"If");function yv(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Ka,xf=Xn.current,_r(Xn,e),_r(Ri,Ri.current),!0}o(yv,"Jf");function I_(e,t,n){var l=e.stateNode;if(!l)throw Error(we(169));n?(e=R_(e,t,xf),l.__reactInternalMemoizedMergedChildContext=e,fr(Ri),fr(Xn),_r(Xn,e)):fr(Ri),_r(Ri,n)}o(I_,"Kf");var q1=null,Sf=null,UR=Tn.unstable_runWithPriority,V1=Tn.unstable_scheduleCallback,K1=Tn.unstable_cancelCallback,zR=Tn.unstable_shouldYield,F_=Tn.unstable_requestPaint,G1=Tn.unstable_now,$R=Tn.unstable_getCurrentPriorityLevel,wv=Tn.unstable_ImmediatePriority,B_=Tn.unstable_UserBlockingPriority,H_=Tn.unstable_NormalPriority,W_=Tn.unstable_LowPriority,U_=Tn.unstable_IdlePriority,Y1={},jR=F_!==void 0?F_:function(){},Pl=null,xv=null,X1=!1,z_=G1(),Qn=1e4>z_?G1:function(){return G1()-z_};function mp(){switch($R()){case wv:return 99;case B_:return 98;case H_:return 97;case W_:return 96;case U_:return 95;default:throw Error(we(332))}}o(mp,"eg");function $_(e){switch(e){case 99:return wv;case 98:return B_;case 97:return H_;case 96:return W_;case 95:return U_;default:throw Error(we(332))}}o($_,"fg");function Cf(e,t){return e=$_(e),UR(e,t)}o(Cf,"gg");function Th(e,t,n){return e=$_(e),V1(e,t,n)}o(Th,"hg");function $s(){if(xv!==null){var e=xv;xv=null,K1(e)}j_()}o($s,"ig");function j_(){if(!X1&&Pl!==null){X1=!0;var e=0;try{var t=Pl;Cf(99,function(){for(;epe?(me=ne,ne=null):me=ne.sibling;var xe=B(R,ne,I[pe],G);if(xe===null){ne===null&&(ne=me);break}e&&ne&&xe.alternate===null&&t(R,ne),A=h(xe,A,pe),se===null?K=xe:se.sibling=xe,se=xe,ne=me}if(pe===I.length)return n(R,ne),K;if(ne===null){for(;pepe?(me=ne,ne=null):me=ne.sibling;var Ve=B(R,ne,xe.value,G);if(Ve===null){ne===null&&(ne=me);break}e&&ne&&Ve.alternate===null&&t(R,ne),A=h(Ve,A,pe),se===null?K=Ve:se.sibling=Ve,se=Ve,ne=me}if(xe.done)return n(R,ne),K;if(ne===null){for(;!xe.done;pe++,xe=I.next())xe=j(R,xe.value,G),xe!==null&&(A=h(xe,A,pe),se===null?K=xe:se.sibling=xe,se=xe);return K}for(ne=l(R,ne);!xe.done;pe++,xe=I.next())xe=X(ne,R,pe,xe.value,G),xe!==null&&(e&&xe.alternate!==null&&ne.delete(xe.key===null?pe:xe.key),A=h(xe,A,pe),se===null?K=xe:se.sibling=xe,se=xe);return e&&ne.forEach(function(tt){return t(R,tt)}),K}return o(Z,"w"),function(R,A,I,G){var K=typeof I=="object"&&I!==null&&I.type===Ba&&I.key===null;K&&(I=I.props.children);var se=typeof I=="object"&&I!==null;if(se)switch(I.$$typeof){case th:e:{for(se=I.key,K=A;K!==null;){if(K.key===se){switch(K.tag){case 7:if(I.type===Ba){n(R,K.sibling),A=d(K,I.props.children),A.return=R,R=A;break e}break;default:if(K.elementType===I.type){n(R,K.sibling),A=d(K,I.props),A.ref=Nh(R,K,I),A.return=R,R=A;break e}}n(R,K);break}else t(R,K);K=K.sibling}I.type===Ba?(A=_p(I.props.children,R.mode,G,I.key),A.return=R,R=A):(G=qv(I.type,I.key,I.props,null,R.mode,G),G.ref=Nh(R,A,I),G.return=R,R=G)}return c(R);case gf:e:{for(K=I.key;A!==null;){if(A.key===K)if(A.tag===4&&A.stateNode.containerInfo===I.containerInfo&&A.stateNode.implementation===I.implementation){n(R,A.sibling),A=d(A,I.children||[]),A.return=R,R=A;break e}else{n(R,A);break}else t(R,A);A=A.sibling}A=Fx(I,R.mode,G),A.return=R,R=A}return c(R)}if(typeof I=="string"||typeof I=="number")return I=""+I,A!==null&&A.tag===6?(n(R,A.sibling),A=d(A,I),A.return=R,R=A):(n(R,A),A=Ix(I,R.mode,G),A.return=R,R=A),c(R);if(Tv(I))return J(R,A,I,G);if(ih(I))return Z(R,A,I,G);if(se&&kv(R,I),typeof I=="undefined"&&!K)switch(R.tag){case 1:case 22:case 0:case 11:case 15:throw Error(we(152,Zc(R.type)||"Component"))}return n(R,A)}}o(J_,"Sg");var Nv=J_(!0),eT=J_(!1),Lh={},js=Va(Lh),Ph=Va(Lh),Oh=Va(Lh);function bf(e){if(e===Lh)throw Error(we(174));return e}o(bf,"dh");function tx(e,t){switch(_r(Oh,t),_r(Ph,e),_r(js,Lh),e=t.nodeType,e){case 9:case 11:t=(t=t.documentElement)?t.namespaceURI:l1(null,"");break;default:e=e===8?t.parentNode:t,t=e.namespaceURI||null,e=e.tagName,t=l1(t,e)}fr(js),_r(js,t)}o(tx,"eh");function yp(){fr(js),fr(Ph),fr(Oh)}o(yp,"fh");function tT(e){bf(Oh.current);var t=bf(js.current),n=l1(t,e.type);t!==n&&(_r(Ph,e),_r(js,n))}o(tT,"gh");function rx(e){Ph.current===e&&(fr(js),fr(Ph))}o(rx,"hh");var Tr=Va(0);function Lv(e){for(var t=e;t!==null;){if(t.tag===13){var n=t.memoizedState;if(n!==null&&(n=n.dehydrated,n===null||n.data==="$?"||n.data==="$!"))return t}else if(t.tag===19&&t.memoizedProps.revealOrder!==void 0){if((t.flags&64)!=0)return t}else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}o(Lv,"ih");var Ol=null,Qa=null,qs=!1;function rT(e,t){var n=No(5,null,null,0);n.elementType="DELETED",n.type="DELETED",n.stateNode=t,n.return=e,n.flags=8,e.lastEffect!==null?(e.lastEffect.nextEffect=n,e.lastEffect=n):e.firstEffect=e.lastEffect=n}o(rT,"mh");function nT(e,t){switch(e.tag){case 5:var n=e.type;return t=t.nodeType!==1||n.toLowerCase()!==t.nodeName.toLowerCase()?null:t,t!==null?(e.stateNode=t,!0):!1;case 6:return t=e.pendingProps===""||t.nodeType!==3?null:t,t!==null?(e.stateNode=t,!0):!1;case 13:return!1;default:return!1}}o(nT,"oh");function nx(e){if(qs){var t=Qa;if(t){var n=t;if(!nT(e,t)){if(t=fp(n.nextSibling),!t||!nT(e,t)){e.flags=e.flags&-1025|2,qs=!1,Ol=e;return}rT(Ol,n)}Ol=e,Qa=fp(t.firstChild)}else e.flags=e.flags&-1025|2,qs=!1,Ol=e}}o(nx,"ph");function iT(e){for(e=e.return;e!==null&&e.tag!==5&&e.tag!==3&&e.tag!==13;)e=e.return;Ol=e}o(iT,"qh");function Pv(e){if(e!==Ol)return!1;if(!qs)return iT(e),qs=!0,!1;var t=e.type;if(e.tag!==5||t!=="head"&&t!=="body"&&!U1(t,e.memoizedProps))for(t=Qa;t;)rT(e,t),t=fp(t.nextSibling);if(iT(e),e.tag===13){if(e=e.memoizedState,e=e!==null?e.dehydrated:null,!e)throw Error(we(317));e:{for(e=e.nextSibling,t=0;e;){if(e.nodeType===8){var n=e.data;if(n==="/$"){if(t===0){Qa=fp(e.nextSibling);break e}t--}else n!=="$"&&n!=="$!"&&n!=="$?"||t++}e=e.nextSibling}Qa=null}}else Qa=Ol?fp(e.stateNode.nextSibling):null;return!0}o(Pv,"rh");function ix(){Qa=Ol=null,qs=!1}o(ix,"sh");var wp=[];function ox(){for(var e=0;eh))throw Error(we(301));h+=1,In=Zn=null,t.updateQueue=null,Mh.current=YR,e=n(l,d)}while(Dh)}if(Mh.current=Rv,t=Zn!==null&&Zn.next!==null,Ah=0,In=Zn=Fr=null,Ov=!1,t)throw Error(we(300));return e}o(lx,"Ch");function Ef(){var e={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};return In===null?Fr.memoizedState=In=e:In=In.next=e,In}o(Ef,"Hh");function _f(){if(Zn===null){var e=Fr.alternate;e=e!==null?e.memoizedState:null}else e=Zn.next;var t=In===null?Fr.memoizedState:In.next;if(t!==null)In=t,Zn=e;else{if(e===null)throw Error(we(310));Zn=e,e={memoizedState:Zn.memoizedState,baseState:Zn.baseState,baseQueue:Zn.baseQueue,queue:Zn.queue,next:null},In===null?Fr.memoizedState=In=e:In=In.next=e}return In}o(_f,"Ih");function Vs(e,t){return typeof t=="function"?t(e):t}o(Vs,"Jh");function Rh(e){var t=_f(),n=t.queue;if(n===null)throw Error(we(311));n.lastRenderedReducer=e;var l=Zn,d=l.baseQueue,h=n.pending;if(h!==null){if(d!==null){var c=d.next;d.next=h.next,h.next=c}l.baseQueue=d=h,n.pending=null}if(d!==null){d=d.next,l=l.baseState;var v=c=h=null,C=d;do{var k=C.lane;if((Ah&k)===k)v!==null&&(v=v.next={lane:0,action:C.action,eagerReducer:C.eagerReducer,eagerState:C.eagerState,next:null}),l=C.eagerReducer===e?C.eagerState:e(l,C.action);else{var O={lane:k,action:C.action,eagerReducer:C.eagerReducer,eagerState:C.eagerState,next:null};v===null?(c=v=O,h=l):v=v.next=O,Fr.lanes|=k,Hh|=k}C=C.next}while(C!==null&&C!==d);v===null?h=l:v.next=c,Eo(l,t.memoizedState)||(ls=!0),t.memoizedState=l,t.baseState=h,t.baseQueue=v,n.lastRenderedState=l}return[t.memoizedState,n.dispatch]}o(Rh,"Kh");function Ih(e){var t=_f(),n=t.queue;if(n===null)throw Error(we(311));n.lastRenderedReducer=e;var l=n.dispatch,d=n.pending,h=t.memoizedState;if(d!==null){n.pending=null;var c=d=d.next;do h=e(h,c.action),c=c.next;while(c!==d);Eo(h,t.memoizedState)||(ls=!0),t.memoizedState=h,t.baseQueue===null&&(t.baseState=h),n.lastRenderedState=h}return[h,l]}o(Ih,"Lh");function oT(e,t,n){var l=t._getVersion;l=l(t._source);var d=t._workInProgressVersionPrimary;if(d!==null?e=d===l:(e=e.mutableReadLanes,(e=(Ah&e)===e)&&(t._workInProgressVersionPrimary=l,wp.push(t))),e)return n(t._source);throw wp.push(t),Error(we(350))}o(oT,"Mh");function sT(e,t,n,l){var d=yi;if(d===null)throw Error(we(349));var h=t._getVersion,c=h(t._source),v=Mh.current,C=v.useState(function(){return oT(d,t,n)}),k=C[1],O=C[0];C=In;var j=e.memoizedState,B=j.refs,X=B.getSnapshot,J=j.source;j=j.subscribe;var Z=Fr;return e.memoizedState={refs:B,source:t,subscribe:l},v.useEffect(function(){B.getSnapshot=n,B.setSnapshot=k;var R=h(t._source);if(!Eo(c,R)){R=n(t._source),Eo(O,R)||(k(R),R=Ja(Z),d.mutableReadLanes|=R&d.pendingLanes),R=d.mutableReadLanes,d.entangledLanes|=R;for(var A=d.entanglements,I=R;0n?98:n,function(){e(!0)}),Cf(97<\/script>",e=e.removeChild(e.firstChild)):typeof l.is=="string"?e=c.createElement(n,{is:l.is}):(e=c.createElement(n),n==="select"&&(c=e,l.multiple?c.multiple=!0:l.size&&(c.size=l.size))):e=c.createElementNS(e,n),e[qa]=t,e[mv]=l,kT(e,t,!1,!1),t.stateNode=e,c=u1(n,l),n){case"dialog":ur("cancel",e),ur("close",e),d=l;break;case"iframe":case"object":case"embed":ur("load",e),d=l;break;case"video":case"audio":for(d=0;dkx&&(t.flags|=64,h=!0,Bh(l,!1),t.lanes=33554432)}else{if(!h)if(e=Lv(c),e!==null){if(t.flags|=64,h=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Bh(l,!0),l.tail===null&&l.tailMode==="hidden"&&!c.alternate&&!qs)return t=t.lastEffect=l.lastEffect,t!==null&&(t.nextEffect=null),null}else 2*Qn()-l.renderingStartTime>kx&&n!==1073741824&&(t.flags|=64,h=!0,Bh(l,!1),t.lanes=33554432);l.isBackwards?(c.sibling=t.child,t.child=c):(n=l.last,n!==null?n.sibling=c:t.child=c,l.last=c)}return l.tail!==null?(n=l.tail,l.rendering=n,l.tail=n.sibling,l.lastEffect=t.lastEffect,l.renderingStartTime=Qn(),n.sibling=null,t=Tr.current,_r(Tr,h?t&1|2:t&1),n):null;case 23:case 24:return Ax(),e!==null&&e.memoizedState!==null!=(t.memoizedState!==null)&&l.mode!=="unstable-defer-without-hiding"&&(t.flags|=4),null}throw Error(we(156,t.tag))}o(QR,"Gi");function ZR(e){switch(e.tag){case 1:Ii(e.type)&&vv();var t=e.flags;return t&4096?(e.flags=t&-4097|64,e):null;case 3:if(yp(),fr(Ri),fr(Xn),ox(),t=e.flags,(t&64)!=0)throw Error(we(285));return e.flags=t&-4097|64,e;case 5:return rx(e),null;case 13:return fr(Tr),t=e.flags,t&4096?(e.flags=t&-4097|64,e):null;case 19:return fr(Tr),null;case 4:return yp(),null;case 10:return Z1(e),null;case 23:case 24:return Ax(),null;default:return null}}o(ZR,"Li");function vx(e,t){try{var n="",l=t;do n+=MD(l),l=l.return;while(l);var d=n}catch(h){d=` +(()=>{var MI=Object.create;var fv=Object.defineProperty;var OI=Object.getOwnPropertyDescriptor;var RI=Object.getOwnPropertyNames;var DI=Object.getPrototypeOf,II=Object.prototype.hasOwnProperty;var i=(e,t)=>fv(e,"name",{value:t,configurable:!0});var rt=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),_b=(e,t)=>{for(var n in t)fv(e,n,{get:t[n],enumerable:!0})},FI=(e,t,n,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let l of RI(t))!II.call(e,l)&&l!==n&&fv(e,l,{get:()=>t[l],enumerable:!(s=OI(t,l))||s.enumerable});return e};var de=(e,t,n)=>(n=e!=null?MI(DI(e)):{},FI(t||!e||!e.__esModule?fv(n,"default",{value:e,enumerable:!0}):n,e));var Db=rt(mt=>{"use strict";var Mh=Symbol.for("react.element"),BI=Symbol.for("react.portal"),HI=Symbol.for("react.fragment"),zI=Symbol.for("react.strict_mode"),UI=Symbol.for("react.profiler"),WI=Symbol.for("react.provider"),$I=Symbol.for("react.context"),VI=Symbol.for("react.forward_ref"),qI=Symbol.for("react.suspense"),jI=Symbol.for("react.memo"),KI=Symbol.for("react.lazy"),Eb=Symbol.iterator;function GI(e){return e===null||typeof e!="object"?null:(e=Eb&&e[Eb]||e["@@iterator"],typeof e=="function"?e:null)}i(GI,"A");var kb={isMounted:i(function(){return!1},"isMounted"),enqueueForceUpdate:i(function(){},"enqueueForceUpdate"),enqueueReplaceState:i(function(){},"enqueueReplaceState"),enqueueSetState:i(function(){},"enqueueSetState")},Nb=Object.assign,Pb={};function Ad(e,t,n){this.props=e,this.context=t,this.refs=Pb,this.updater=n||kb}i(Ad,"E");Ad.prototype.isReactComponent={};Ad.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};Ad.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function Ab(){}i(Ab,"F");Ab.prototype=Ad.prototype;function n1(e,t,n){this.props=e,this.context=t,this.refs=Pb,this.updater=n||kb}i(n1,"G");var i1=n1.prototype=new Ab;i1.constructor=n1;Nb(i1,Ad.prototype);i1.isPureReactComponent=!0;var bb=Array.isArray,Lb=Object.prototype.hasOwnProperty,o1={current:null},Mb={key:!0,ref:!0,__self:!0,__source:!0};function Ob(e,t,n){var s,l={},h=null,d=null;if(t!=null)for(s in t.ref!==void 0&&(d=t.ref),t.key!==void 0&&(h=""+t.key),t)Lb.call(t,s)&&!Mb.hasOwnProperty(s)&&(l[s]=t[s]);var v=arguments.length-2;if(v===1)l.children=n;else if(1{"use strict";Ib.exports=Db()});var jb=rt(tr=>{"use strict";function c1(e,t){var n=e.length;e.push(t);e:for(;0>>1,l=e[s];if(0>>1;smv(v,n))Smv(b,v)?(e[s]=b,e[S]=n,s=S):(e[s]=v,e[d]=n,s=d);else if(Smv(b,n))e[s]=b,e[S]=n,s=S;else break e}}return t}i(vv,"k");function mv(e,t){var n=e.sortIndex-t.sortIndex;return n!==0?n:e.id-t.id}i(mv,"g");typeof performance=="object"&&typeof performance.now=="function"?(Fb=performance,tr.unstable_now=function(){return Fb.now()}):(a1=Date,Bb=a1.now(),tr.unstable_now=function(){return a1.now()-Bb});var Fb,a1,Bb,va=[],xu=[],ZI=1,rs=null,ti=3,yv=!1,Zc=!1,Rh=!1,Ub=typeof setTimeout=="function"?setTimeout:null,Wb=typeof clearTimeout=="function"?clearTimeout:null,Hb=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function f1(e){for(var t=As(xu);t!==null;){if(t.callback===null)vv(xu);else if(t.startTime<=e)vv(xu),t.sortIndex=t.expirationTime,c1(va,t);else break;t=As(xu)}}i(f1,"G");function d1(e){if(Rh=!1,f1(e),!Zc)if(As(va)!==null)Zc=!0,h1(p1);else{var t=As(xu);t!==null&&m1(d1,t.startTime-e)}}i(d1,"H");function p1(e,t){Zc=!1,Rh&&(Rh=!1,Wb(Dh),Dh=-1),yv=!0;var n=ti;try{for(f1(t),rs=As(va);rs!==null&&(!(rs.expirationTime>t)||e&&!qb());){var s=rs.callback;if(typeof s=="function"){rs.callback=null,ti=rs.priorityLevel;var l=s(rs.expirationTime<=t);t=tr.unstable_now(),typeof l=="function"?rs.callback=l:rs===As(va)&&vv(va),f1(t)}else vv(va);rs=As(va)}if(rs!==null)var h=!0;else{var d=As(xu);d!==null&&m1(d1,d.startTime-t),h=!1}return h}finally{rs=null,ti=n,yv=!1}}i(p1,"J");var wv=!1,gv=null,Dh=-1,$b=5,Vb=-1;function qb(){return!(tr.unstable_now()-Vb<$b)}i(qb,"M");function l1(){if(gv!==null){var e=tr.unstable_now();Vb=e;var t=!0;try{t=gv(!0,e)}finally{t?Oh():(wv=!1,gv=null)}}else wv=!1}i(l1,"R");var Oh;typeof Hb=="function"?Oh=i(function(){Hb(l1)},"S"):typeof MessageChannel<"u"?(u1=new MessageChannel,zb=u1.port2,u1.port1.onmessage=l1,Oh=i(function(){zb.postMessage(null)},"S")):Oh=i(function(){Ub(l1,0)},"S");var u1,zb;function h1(e){gv=e,wv||(wv=!0,Oh())}i(h1,"I");function m1(e,t){Dh=Ub(function(){e(tr.unstable_now())},t)}i(m1,"K");tr.unstable_IdlePriority=5;tr.unstable_ImmediatePriority=1;tr.unstable_LowPriority=4;tr.unstable_NormalPriority=3;tr.unstable_Profiling=null;tr.unstable_UserBlockingPriority=2;tr.unstable_cancelCallback=function(e){e.callback=null};tr.unstable_continueExecution=function(){Zc||yv||(Zc=!0,h1(p1))};tr.unstable_forceFrameRate=function(e){0>e||125s?(e.sortIndex=n,c1(xu,e),As(va)===null&&e===As(xu)&&(Rh?(Wb(Dh),Dh=-1):Rh=!0,m1(d1,n-s))):(e.sortIndex=l,c1(va,e),Zc||yv||(Zc=!0,h1(p1))),e};tr.unstable_shouldYield=qb;tr.unstable_wrapCallback=function(e){var t=ti;return function(){var n=ti;ti=t;try{return e.apply(this,arguments)}finally{ti=n}}}});var Gb=rt((IW,Kb)=>{"use strict";Kb.exports=jb()});var JN=rt(Lo=>{"use strict";var eF=be(),Po=Gb();function ge(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),B1=Object.prototype.hasOwnProperty,tF=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Yb={},Xb={};function rF(e){return B1.call(Xb,e)?!0:B1.call(Yb,e)?!1:tF.test(e)?Xb[e]=!0:(Yb[e]=!0,!1)}i(rF,"oa");function nF(e,t,n,s){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return s?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}i(nF,"pa");function iF(e,t,n,s){if(t===null||typeof t>"u"||nF(e,t,n,s))return!0;if(s)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}i(iF,"qa");function Ti(e,t,n,s,l,h,d){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=s,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=h,this.removeEmptyString=d}i(Ti,"v");var jn={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){jn[e]=new Ti(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];jn[t]=new Ti(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){jn[e]=new Ti(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){jn[e]=new Ti(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){jn[e]=new Ti(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){jn[e]=new Ti(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){jn[e]=new Ti(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){jn[e]=new Ti(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){jn[e]=new Ti(e,5,!1,e.toLowerCase(),null,!1,!1)});var AS=/[\-:]([a-z])/g;function LS(e){return e[1].toUpperCase()}i(LS,"sa");"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(AS,LS);jn[t]=new Ti(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(AS,LS);jn[t]=new Ti(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(AS,LS);jn[t]=new Ti(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){jn[e]=new Ti(e,1,!1,e.toLowerCase(),null,!1,!1)});jn.xlinkHref=new Ti("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){jn[e]=new Ti(e,1,!1,e.toLowerCase(),null,!0,!0)});function MS(e,t,n,s){var l=jn.hasOwnProperty(t)?jn[t]:null;(l!==null?l.type!==0:s||!(2v||l[d]!==h[v]){var S=` +`+l[d].replace(" at new "," at ");return e.displayName&&S.includes("")&&(S=S.replace("",e.displayName)),S}while(1<=d&&0<=v);break}}}finally{v1=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Vh(e):""}i(y1,"Oa");function oF(e){switch(e.tag){case 5:return Vh(e.type);case 16:return Vh("Lazy");case 13:return Vh("Suspense");case 19:return Vh("SuspenseList");case 0:case 2:case 15:return e=y1(e.type,!1),e;case 11:return e=y1(e.type.render,!1),e;case 1:return e=y1(e.type,!0),e;default:return""}}i(oF,"Pa");function W1(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Rd:return"Fragment";case Od:return"Portal";case H1:return"Profiler";case OS:return"StrictMode";case z1:return"Suspense";case U1:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case nk:return(e.displayName||"Context")+".Consumer";case rk:return(e._context.displayName||"Context")+".Provider";case RS:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case DS:return t=e.displayName||null,t!==null?t:W1(e.type)||"Memo";case _u:t=e._payload,e=e._init;try{return W1(e(t))}catch{}}return null}i(W1,"Qa");function sF(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return W1(t);case 8:return t===OS?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}i(sF,"Ra");function Fu(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}i(Fu,"Sa");function ok(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}i(ok,"Ta");function aF(e){var t=ok(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),s=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,h=n.set;return Object.defineProperty(e,t,{configurable:!0,get:i(function(){return l.call(this)},"get"),set:i(function(d){s=""+d,h.call(this,d)},"set")}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:i(function(){return s},"getValue"),setValue:i(function(d){s=""+d},"setValue"),stopTracking:i(function(){e._valueTracker=null,delete e[t]},"stopTracking")}}}i(aF,"Ua");function xv(e){e._valueTracker||(e._valueTracker=aF(e))}i(xv,"Va");function sk(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),s="";return e&&(s=ok(e)?e.checked?"true":"false":e.value),e=s,e!==n?(t.setValue(e),!0):!1}i(sk,"Wa");function Yv(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}i(Yv,"Xa");function $1(e,t){var n=t.checked;return Tr({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}i($1,"Ya");function Jb(e,t){var n=t.defaultValue==null?"":t.defaultValue,s=t.checked!=null?t.checked:t.defaultChecked;n=Fu(t.value!=null?t.value:n),e._wrapperState={initialChecked:s,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}i(Jb,"Za");function ak(e,t){t=t.checked,t!=null&&MS(e,"checked",t,!1)}i(ak,"ab");function V1(e,t){ak(e,t);var n=Fu(t.value),s=t.type;if(n!=null)s==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(s==="submit"||s==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?q1(e,t.type,n):t.hasOwnProperty("defaultValue")&&q1(e,t.type,Fu(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}i(V1,"bb");function Zb(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var s=t.type;if(!(s!=="submit"&&s!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}i(Zb,"db");function q1(e,t,n){(t!=="number"||Yv(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}i(q1,"cb");var qh=Array.isArray;function qd(e,t,n,s){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=Cv.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function im(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}i(im,"ob");var Gh={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},lF=["Webkit","ms","Moz","O"];Object.keys(Gh).forEach(function(e){lF.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Gh[t]=Gh[e]})});function fk(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Gh.hasOwnProperty(e)&&Gh[e]?(""+t).trim():t+"px"}i(fk,"rb");function dk(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var s=n.indexOf("--")===0,l=fk(n,t[n],s);n==="float"&&(n="cssFloat"),s?e.setProperty(n,l):e[n]=l}}i(dk,"sb");var uF=Tr({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function G1(e,t){if(t){if(uF[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(ge(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(ge(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(ge(61))}if(t.style!=null&&typeof t.style!="object")throw Error(ge(62))}}i(G1,"ub");function Y1(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}i(Y1,"vb");var X1=null;function IS(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}i(IS,"xb");var Q1=null,jd=null,Kd=null;function rT(e){if(e=Cm(e)){if(typeof Q1!="function")throw Error(ge(280));var t=e.stateNode;t&&(t=_y(t),Q1(e.stateNode,e.type,t))}}i(rT,"Bb");function pk(e){jd?Kd?Kd.push(e):Kd=[e]:jd=e}i(pk,"Eb");function hk(){if(jd){var e=jd,t=Kd;if(Kd=jd=null,rT(e),t)for(e=0;e>>=0,e===0?32:31-(SF(e)/xF|0)|0}i(CF,"nc");var _v=64,Ev=4194304;function jh(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}i(jh,"tc");function Zv(e,t){var n=e.pendingLanes;if(n===0)return 0;var s=0,l=e.suspendedLanes,h=e.pingedLanes,d=n&268435455;if(d!==0){var v=d&~l;v!==0?s=jh(v):(h&=d,h!==0&&(s=jh(h)))}else d=n&~l,d!==0?s=jh(d):h!==0&&(s=jh(h));if(s===0)return 0;if(t!==0&&t!==s&&!(t&l)&&(l=s&-s,h=t&-t,l>=h||l===16&&(h&4194240)!==0))return t;if(s&4&&(s|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=s;0n;n++)t.push(e);return t}i(S1,"zc");function Sm(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Ds(t),e[t]=n}i(Sm,"Ac");function bF(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var s=e.eventTimes;for(e=e.expirationTimes;0=Xh),fT=" ",dT=!1;function Rk(e,t){switch(e){case"keyup":return ZF.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}i(Rk,"ge");function Dk(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}i(Dk,"he");var Dd=!1;function t3(e,t){switch(e){case"compositionend":return Dk(t);case"keypress":return t.which!==32?null:(dT=!0,fT);case"textInput":return e=t.data,e===fT&&dT?null:e;default:return null}}i(t3,"je");function r3(e,t){if(Dd)return e==="compositionend"||!VS&&Rk(e,t)?(e=Mk(),zv=US=ku=null,Dd=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=s}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=mT(n)}}i(gT,"Ke");function Hk(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Hk(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}i(Hk,"Le");function zk(){for(var e=window,t=Yv();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Yv(e.document)}return t}i(zk,"Me");function qS(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}i(qS,"Ne");function f3(e){var t=zk(),n=e.focusedElem,s=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Hk(n.ownerDocument.documentElement,n)){if(s!==null&&qS(n)){if(t=s.start,e=s.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,h=Math.min(s.start,l);s=s.end===void 0?h:Math.min(s.end,l),!e.extend&&h>s&&(l=s,s=h,h=l),l=gT(n,h);var d=gT(n,s);l&&d&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==d.node||e.focusOffset!==d.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),h>s?(e.addRange(t),e.extend(d.node,d.offset)):(t.setEnd(d.node,d.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Id=null,nS=null,Jh=null,iS=!1;function vT(e,t,n){var s=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;iS||Id==null||Id!==Yv(s)||(s=Id,"selectionStart"in s&&qS(s)?s={start:s.selectionStart,end:s.selectionEnd}:(s=(s.ownerDocument&&s.ownerDocument.defaultView||window).getSelection(),s={anchorNode:s.anchorNode,anchorOffset:s.anchorOffset,focusNode:s.focusNode,focusOffset:s.focusOffset}),Jh&&cm(Jh,s)||(Jh=s,s=ry(nS,"onSelect"),0Hd||(e.current=cS[Hd],cS[Hd]=null,Hd--)}i(cr,"E");function rr(e,t){Hd++,cS[Hd]=e.current,e.current=t}i(rr,"G");var Bu={},oi=zu(Bu),Ki=zu(!1),lf=Bu;function Jd(e,t){var n=e.type.contextTypes;if(!n)return Bu;var s=e.stateNode;if(s&&s.__reactInternalMemoizedUnmaskedChildContext===t)return s.__reactInternalMemoizedMaskedChildContext;var l={},h;for(h in n)l[h]=t[h];return s&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}i(Jd,"Yf");function Gi(e){return e=e.childContextTypes,e!=null}i(Gi,"Zf");function iy(){cr(Ki),cr(oi)}i(iy,"$f");function TT(e,t,n){if(oi.current!==Bu)throw Error(ge(168));rr(oi,t),rr(Ki,n)}i(TT,"ag");function Yk(e,t,n){var s=e.stateNode;if(t=t.childContextTypes,typeof s.getChildContext!="function")return n;s=s.getChildContext();for(var l in s)if(!(l in t))throw Error(ge(108,sF(e)||"Unknown",l));return Tr({},n,s)}i(Yk,"bg");function oy(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Bu,lf=oi.current,rr(oi,e),rr(Ki,Ki.current),!0}i(oy,"cg");function kT(e,t,n){var s=e.stateNode;if(!s)throw Error(ge(169));n?(e=Yk(e,t,lf),s.__reactInternalMemoizedMergedChildContext=e,cr(Ki),cr(oi),rr(oi,e)):cr(Ki),rr(Ki,n)}i(kT,"dg");var al=null,Ey=!1,P1=!1;function Xk(e){al===null?al=[e]:al.push(e)}i(Xk,"hg");function x3(e){Ey=!0,Xk(e)}i(x3,"ig");function Uu(){if(!P1&&al!==null){P1=!0;var e=0,t=Vt;try{var n=al;for(Vt=1;e>=d,l-=d,ll=1<<32-Ds(t)+l|n<he?(Re=oe,oe=null):Re=oe.sibling;var Ee=I(F,oe,D[he],q);if(Ee===null){oe===null&&(oe=Re);break}e&&oe&&Ee.alternate===null&&t(F,oe),L=h(Ee,L,he),Z===null?te=Ee:Z.sibling=Ee,Z=Ee,oe=Re}if(he===D.length)return n(F,oe),hr&&ef(F,he),te;if(oe===null){for(;hehe?(Re=oe,oe=null):Re=oe.sibling;var Ye=I(F,oe,Ee.value,q);if(Ye===null){oe===null&&(oe=Re);break}e&&oe&&Ye.alternate===null&&t(F,oe),L=h(Ye,L,he),Z===null?te=Ye:Z.sibling=Ye,Z=Ye,oe=Re}if(Ee.done)return n(F,oe),hr&&ef(F,he),te;if(oe===null){for(;!Ee.done;he++,Ee=D.next())Ee=R(F,Ee.value,q),Ee!==null&&(L=h(Ee,L,he),Z===null?te=Ee:Z.sibling=Ee,Z=Ee);return hr&&ef(F,he),te}for(oe=s(F,oe);!Ee.done;he++,Ee=D.next())Ee=z(oe,F,he,Ee.value,q),Ee!==null&&(e&&Ee.alternate!==null&&oe.delete(Ee.key===null?he:Ee.key),L=h(Ee,L,he),Z===null?te=Ee:Z.sibling=Ee,Z=Ee);return e&&oe.forEach(function(tt){return t(F,tt)}),hr&&ef(F,he),te}i(Y,"t");function ne(F,L,D,q){if(typeof D=="object"&&D!==null&&D.type===Rd&&D.key===null&&(D=D.props.children),typeof D=="object"&&D!==null){switch(D.$$typeof){case Sv:e:{for(var te=D.key,Z=L;Z!==null;){if(Z.key===te){if(te=D.type,te===Rd){if(Z.tag===7){n(F,Z.sibling),L=l(Z,D.props.children),L.return=F,F=L;break e}}else if(Z.elementType===te||typeof te=="object"&&te!==null&&te.$$typeof===_u&&AT(te)===Z.type){n(F,Z.sibling),L=l(Z,D.props),L.ref=zh(F,Z,D),L.return=F,F=L;break e}n(F,Z);break}else t(F,Z);Z=Z.sibling}D.type===Rd?(L=af(D.props.children,F.mode,q,D.key),L.return=F,F=L):(q=Gv(D.type,D.key,D.props,null,F.mode,q),q.ref=zh(F,L,D),q.return=F,F=q)}return d(F);case Od:e:{for(Z=D.key;L!==null;){if(L.key===Z)if(L.tag===4&&L.stateNode.containerInfo===D.containerInfo&&L.stateNode.implementation===D.implementation){n(F,L.sibling),L=l(L,D.children||[]),L.return=F,F=L;break e}else{n(F,L);break}else t(F,L);L=L.sibling}L=F1(D,F.mode,q),L.return=F,F=L}return d(F);case _u:return Z=D._init,ne(F,L,Z(D._payload),q)}if(qh(D))return j(F,L,D,q);if(Ih(D))return Y(F,L,D,q);Dv(F,D)}return typeof D=="string"&&D!==""||typeof D=="number"?(D=""+D,L!==null&&L.tag===6?(n(F,L.sibling),L=l(L,D),L.return=F,F=L):(n(F,L),L=I1(D,F.mode,q),L.return=F,F=L),d(F)):n(F,L)}return i(ne,"J"),ne}i(eN,"Og");var ep=eN(!0),tN=eN(!1),ly=zu(null),uy=null,Wd=null,YS=null;function XS(){YS=Wd=uy=null}i(XS,"$g");function QS(e){var t=ly.current;cr(ly),e._currentValue=t}i(QS,"ah");function pS(e,t,n){for(;e!==null;){var s=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,s!==null&&(s.childLanes|=t)):s!==null&&(s.childLanes&t)!==t&&(s.childLanes|=t),e===n)break;e=e.return}}i(pS,"bh");function Yd(e,t){uy=e,YS=Wd=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(ji=!0),e.firstContext=null)}i(Yd,"ch");function as(e){var t=e._currentValue;if(YS!==e)if(e={context:e,memoizedValue:t,next:null},Wd===null){if(uy===null)throw Error(ge(308));Wd=e,uy.dependencies={lanes:0,firstContext:e}}else Wd=Wd.next=e;return t}i(as,"eh");var nf=null;function JS(e){nf===null?nf=[e]:nf.push(e)}i(JS,"gh");function rN(e,t,n,s){var l=t.interleaved;return l===null?(n.next=n,JS(t)):(n.next=l.next,l.next=n),t.interleaved=n,pl(e,s)}i(rN,"hh");function pl(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}i(pl,"ih");var Eu=!1;function ZS(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}i(ZS,"kh");function nN(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}i(nN,"lh");function cl(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}i(cl,"mh");function Ou(e,t,n){var s=e.updateQueue;if(s===null)return null;if(s=s.shared,Nt&2){var l=s.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),s.pending=t,pl(e,n)}return l=s.interleaved,l===null?(t.next=t,JS(s)):(t.next=l.next,l.next=t),s.interleaved=t,pl(e,n)}i(Ou,"nh");function Wv(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var s=t.lanes;s&=e.pendingLanes,n|=s,t.lanes=n,BS(e,n)}}i(Wv,"oh");function LT(e,t){var n=e.updateQueue,s=e.alternate;if(s!==null&&(s=s.updateQueue,n===s)){var l=null,h=null;if(n=n.firstBaseUpdate,n!==null){do{var d={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};h===null?l=h=d:h=h.next=d,n=n.next}while(n!==null);h===null?l=h=t:h=h.next=t}else l=h=t;n={baseState:s.baseState,firstBaseUpdate:l,lastBaseUpdate:h,shared:s.shared,effects:s.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}i(LT,"ph");function cy(e,t,n,s){var l=e.updateQueue;Eu=!1;var h=l.firstBaseUpdate,d=l.lastBaseUpdate,v=l.shared.pending;if(v!==null){l.shared.pending=null;var S=v,b=S.next;S.next=null,d===null?h=b:d.next=b,d=S;var k=e.alternate;k!==null&&(k=k.updateQueue,v=k.lastBaseUpdate,v!==d&&(v===null?k.firstBaseUpdate=b:v.next=b,k.lastBaseUpdate=S))}if(h!==null){var R=l.baseState;d=0,k=b=S=null,v=h;do{var I=v.lane,z=v.eventTime;if((s&I)===I){k!==null&&(k=k.next={eventTime:z,lane:0,tag:v.tag,payload:v.payload,callback:v.callback,next:null});e:{var j=e,Y=v;switch(I=t,z=n,Y.tag){case 1:if(j=Y.payload,typeof j=="function"){R=j.call(z,R,I);break e}R=j;break e;case 3:j.flags=j.flags&-65537|128;case 0:if(j=Y.payload,I=typeof j=="function"?j.call(z,R,I):j,I==null)break e;R=Tr({},R,I);break e;case 2:Eu=!0}}v.callback!==null&&v.lane!==0&&(e.flags|=64,I=l.effects,I===null?l.effects=[v]:I.push(v))}else z={eventTime:z,lane:I,tag:v.tag,payload:v.payload,callback:v.callback,next:null},k===null?(b=k=z,S=R):k=k.next=z,d|=I;if(v=v.next,v===null){if(v=l.shared.pending,v===null)break;I=v,v=I.next,I.next=null,l.lastBaseUpdate=I,l.shared.pending=null}}while(!0);if(k===null&&(S=R),l.baseState=S,l.firstBaseUpdate=b,l.lastBaseUpdate=k,t=l.shared.interleaved,t!==null){l=t;do d|=l.lane,l=l.next;while(l!==t)}else h===null&&(l.shared.lanes=0);ff|=d,e.lanes=d,e.memoizedState=R}}i(cy,"qh");function MT(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var s=L1.transition;L1.transition={};try{e(!1),t()}finally{Vt=n,L1.transition=s}}i(E3,"vi");function SN(){return ls().memoizedState}i(SN,"wi");function b3(e,t,n){var s=Du(e);if(n={lane:s,action:n,hasEagerState:!1,eagerState:null,next:null},xN(e))CN(t,n);else if(n=rN(e,t,n,s),n!==null){var l=bi();Is(n,e,s,l),_N(n,t,s)}}i(b3,"xi");function T3(e,t,n){var s=Du(e),l={lane:s,action:n,hasEagerState:!1,eagerState:null,next:null};if(xN(e))CN(t,l);else{var h=e.alternate;if(e.lanes===0&&(h===null||h.lanes===0)&&(h=t.lastRenderedReducer,h!==null))try{var d=t.lastRenderedState,v=h(d,n);if(l.hasEagerState=!0,l.eagerState=v,Fs(v,d)){var S=t.interleaved;S===null?(l.next=l,JS(t)):(l.next=S.next,S.next=l),t.interleaved=l;return}}catch{}finally{}n=rN(e,t,l,s),n!==null&&(l=bi(),Is(n,e,s,l),_N(n,t,s))}}i(T3,"ii");function xN(e){var t=e.alternate;return e===br||t!==null&&t===br}i(xN,"zi");function CN(e,t){Zh=dy=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}i(CN,"Ai");function _N(e,t,n){if(n&4194240){var s=t.lanes;s&=e.pendingLanes,n|=s,t.lanes=n,BS(e,n)}}i(_N,"Bi");var py={readContext:as,useCallback:ri,useContext:ri,useEffect:ri,useImperativeHandle:ri,useInsertionEffect:ri,useLayoutEffect:ri,useMemo:ri,useReducer:ri,useRef:ri,useState:ri,useDebugValue:ri,useDeferredValue:ri,useTransition:ri,useMutableSource:ri,useSyncExternalStore:ri,useId:ri,unstable_isNewReconciler:!1},k3={readContext:as,useCallback:i(function(e,t){return wa().memoizedState=[e,t===void 0?null:t],e},"useCallback"),useContext:as,useEffect:RT,useImperativeHandle:i(function(e,t,n){return n=n!=null?n.concat([e]):null,Vv(4194308,4,mN.bind(null,t,e),n)},"useImperativeHandle"),useLayoutEffect:i(function(e,t){return Vv(4194308,4,e,t)},"useLayoutEffect"),useInsertionEffect:i(function(e,t){return Vv(4,2,e,t)},"useInsertionEffect"),useMemo:i(function(e,t){var n=wa();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},"useMemo"),useReducer:i(function(e,t,n){var s=wa();return t=n!==void 0?n(t):t,s.memoizedState=s.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},s.queue=e,e=e.dispatch=b3.bind(null,br,e),[s.memoizedState,e]},"useReducer"),useRef:i(function(e){var t=wa();return e={current:e},t.memoizedState=e},"useRef"),useState:OT,useDebugValue:ax,useDeferredValue:i(function(e){return wa().memoizedState=e},"useDeferredValue"),useTransition:i(function(){var e=OT(!1),t=e[0];return e=E3.bind(null,e[1]),wa().memoizedState=e,[t,e]},"useTransition"),useMutableSource:i(function(){},"useMutableSource"),useSyncExternalStore:i(function(e,t,n){var s=br,l=wa();if(hr){if(n===void 0)throw Error(ge(407));n=n()}else{if(n=t(),On===null)throw Error(ge(349));cf&30||aN(s,t,n)}l.memoizedState=n;var h={value:n,getSnapshot:t};return l.queue=h,RT(uN.bind(null,s,h,e),[e]),s.flags|=2048,ym(9,lN.bind(null,s,h,n,t),void 0,null),n},"useSyncExternalStore"),useId:i(function(){var e=wa(),t=On.identifierPrefix;if(hr){var n=ul,s=ll;n=(s&~(1<<32-Ds(s)-1)).toString(32)+n,t=":"+t+"R"+n,n=gm++,0d&&(d=c),n&=~h}if(n=d,n=Qn()-n,n=(120>n?120:480>n?480:1080>n?1080:1920>n?1920:3e3>n?3e3:4320>n?4320:1960*iI(n/1960))-n,10 component higher in the tree to provide a loading indicator or placeholder to display.`)}Fn!==5&&(Fn=2),C=vx(C,v),B=c;do{switch(B.tag){case 3:h=C,B.flags|=4096,t&=-t,B.lanes|=t;var se=PT(B,h,t);K_(B,se);break e;case 1:h=C;var ne=B.type,pe=B.stateNode;if((B.flags&64)==0&&(typeof ne.getDerivedStateFromError=="function"||pe!==null&&typeof pe.componentDidCatch=="function"&&(Ks===null||!Ks.has(pe)))){B.flags|=4096,t&=-t,B.lanes|=t;var me=OT(B,h,t);K_(B,me);break e}}B=B.return}while(B!==null)}VT(n)}catch(xe){t=xe,sn===n&&n!==null&&(sn=n=n.return);continue}break}while(1)}o($T,"Sj");function jT(){var e=Fv.current;return Fv.current=Rv,e===null?Rv:e}o(jT,"Pj");function jh(e,t){var n=Ze;Ze|=16;var l=jT();yi===e&&Jn===t||Ep(e,t);do try{sI();break}catch(d){$T(e,d)}while(1);if(Q1(),Ze=n,Fv.current=l,sn!==null)throw Error(we(261));return yi=null,Jn=0,Fn}o(jh,"Tj");function sI(){for(;sn!==null;)qT(sn)}o(sI,"ak");function lI(){for(;sn!==null&&!zR();)qT(sn)}o(lI,"Rj");function qT(e){var t=YT(e.alternate,e,Tf);e.memoizedProps=e.pendingProps,t===null?VT(e):sn=t,Cx.current=null}o(qT,"bk");function VT(e){var t=e;do{var n=t.alternate;if(e=t.return,(t.flags&2048)==0){if(n=QR(n,t,Tf),n!==null){sn=n;return}if(n=t,n.tag!==24&&n.tag!==23||n.memoizedState===null||(Tf&1073741824)!=0||(n.mode&4)==0){for(var l=0,d=n.child;d!==null;)l|=d.lanes|d.childLanes,d=d.sibling;n.childLanes=l}e!==null&&(e.flags&2048)==0&&(e.firstEffect===null&&(e.firstEffect=t.firstEffect),t.lastEffect!==null&&(e.lastEffect!==null&&(e.lastEffect.nextEffect=t.firstEffect),e.lastEffect=t.lastEffect),1c&&(v=c,c=se,se=v),v=g_(I,se),h=g_(I,c),v&&h&&(K.rangeCount!==1||K.anchorNode!==v.node||K.anchorOffset!==v.offset||K.focusNode!==h.node||K.focusOffset!==h.offset)&&(G=G.createRange(),G.setStart(v.node,v.offset),K.removeAllRanges(),se>c?(K.addRange(G),K.extend(h.node,h.offset)):(G.setEnd(h.node,h.offset),K.addRange(G)))))),G=[],K=I;K=K.parentNode;)K.nodeType===1&&G.push({element:K,left:K.scrollLeft,top:K.scrollTop});for(typeof I.focus=="function"&&I.focus(),I=0;IQn()-Tx?Ep(e,0):Ex|=n),ko(e,t)}o(pI,"Yj");function dI(e,t){var n=e.stateNode;n!==null&&n.delete(t),t=0,t===0&&(t=e.mode,(t&2)==0?t=1:(t&4)==0?t=mp()===99?1:2:(Dl===0&&(Dl=xp),t=op(62914560&~Dl),t===0&&(t=4194304))),n=Ji(),e=$v(e,t),e!==null&&(rv(e,t,n),ko(e,n))}o(dI,"lj");var YT;YT=o(function(e,t,n){var l=t.lanes;if(e!==null)if(e.memoizedProps!==t.pendingProps||Ri.current)ls=!0;else if((n&l)!=0)ls=(e.flags&16384)!=0;else{switch(ls=!1,t.tag){case 3:xT(t),ix();break;case 5:tT(t);break;case 1:Ii(t.type)&&yv(t);break;case 4:tx(t,t.stateNode.containerInfo);break;case 10:l=t.memoizedProps.value;var d=t.type._context;_r(Sv,d._currentValue),d._currentValue=l;break;case 13:if(t.memoizedState!==null)return(n&t.child.childLanes)!=0?ST(e,t,n):(_r(Tr,Tr.current&1),t=Ml(e,t,n),t!==null?t.sibling:null);_r(Tr,Tr.current&1);break;case 19:if(l=(n&t.childLanes)!=0,(e.flags&64)!=0){if(l)return TT(e,t,n);t.flags|=64}if(d=t.memoizedState,d!==null&&(d.rendering=null,d.tail=null,d.lastEffect=null),_r(Tr,Tr.current),l)break;return null;case 23:case 24:return t.lanes=0,px(e,t,n)}return Ml(e,t,n)}else ls=!1;switch(t.lanes=0,t.tag){case 2:if(l=t.type,e!==null&&(e.alternate=null,t.alternate=null,t.flags|=2),e=t.pendingProps,d=hp(t,Xn.current),vp(t,n),d=lx(null,t,l,e,d,n),t.flags|=1,typeof d=="object"&&d!==null&&typeof d.render=="function"&&d.$$typeof===void 0){if(t.tag=1,t.memoizedState=null,t.updateQueue=null,Ii(l)){var h=!0;yv(t)}else h=!1;t.memoizedState=d.state!==null&&d.state!==void 0?d.state:null,J1(t);var c=l.getDerivedStateFromProps;typeof c=="function"&&Ev(t,l,c,e),d.updater=_v,t.stateNode=d,d._reactInternals=t,ex(t,l,e,n),t=hx(null,t,l,!0,h,n)}else t.tag=0,Bi(null,t,d,n),t=t.child;return t;case 16:d=t.elementType;e:{switch(e!==null&&(e.alternate=null,t.alternate=null,t.flags|=2),e=t.pendingProps,h=d._init,d=h(d._payload),t.type=d,h=t.tag=mI(d),e=ss(d,e),h){case 0:t=dx(null,t,d,e,n);break e;case 1:t=wT(null,t,d,e,n);break e;case 11:t=mT(null,t,d,e,n);break e;case 14:t=gT(null,t,d,ss(d.type,e),l,n);break e}throw Error(we(306,d,""))}return t;case 0:return l=t.type,d=t.pendingProps,d=t.elementType===l?d:ss(l,d),dx(e,t,l,d,n);case 1:return l=t.type,d=t.pendingProps,d=t.elementType===l?d:ss(l,d),wT(e,t,l,d,n);case 3:if(xT(t),l=t.updateQueue,e===null||l===null)throw Error(we(282));if(l=t.pendingProps,d=t.memoizedState,d=d!==null?d.element:null,V_(e,t),kh(t,l,null,n),l=t.memoizedState.element,l===d)ix(),t=Ml(e,t,n);else{if(d=t.stateNode,(h=d.hydrate)&&(Qa=fp(t.stateNode.containerInfo.firstChild),Ol=t,h=qs=!0),h){if(e=d.mutableSourceEagerHydrationData,e!=null)for(d=0;d{"use strict";function JT(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__=="undefined"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(JT)}catch(e){console.error(e)}}o(JT,"checkDCE");JT(),ek.exports=ZT()});var rk=Ue((v4,tk)=>{"use strict";var CI="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED";tk.exports=CI});var sk=Ue((y4,ok)=>{"use strict";var bI=rk();function nk(){}o(nk,"emptyFunction");function ik(){}o(ik,"emptyFunctionWithReset");ik.resetWarningCache=nk;ok.exports=function(){function e(l,d,h,c,v,C){if(C!==bI){var k=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw k.name="Invariant Violation",k}}o(e,"shim"),e.isRequired=e;function t(){return e}o(t,"getShim");var n={array:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:ik,resetWarningCache:nk};return n.PropTypes=n,n}});var ak=Ue((S4,lk)=>{lk.exports=sk()();var w4,x4});var mk=Ue(Ht=>{"use strict";var kn=typeof Symbol=="function"&&Symbol.for,zx=kn?Symbol.for("react.element"):60103,$x=kn?Symbol.for("react.portal"):60106,Yv=kn?Symbol.for("react.fragment"):60107,Xv=kn?Symbol.for("react.strict_mode"):60108,Qv=kn?Symbol.for("react.profiler"):60114,Zv=kn?Symbol.for("react.provider"):60109,Jv=kn?Symbol.for("react.context"):60110,jx=kn?Symbol.for("react.async_mode"):60111,e0=kn?Symbol.for("react.concurrent_mode"):60111,t0=kn?Symbol.for("react.forward_ref"):60112,r0=kn?Symbol.for("react.suspense"):60113,kI=kn?Symbol.for("react.suspense_list"):60120,n0=kn?Symbol.for("react.memo"):60115,i0=kn?Symbol.for("react.lazy"):60116,NI=kn?Symbol.for("react.block"):60121,LI=kn?Symbol.for("react.fundamental"):60117,PI=kn?Symbol.for("react.responder"):60118,OI=kn?Symbol.for("react.scope"):60119;function eo(e){if(typeof e=="object"&&e!==null){var t=e.$$typeof;switch(t){case zx:switch(e=e.type,e){case jx:case e0:case Yv:case Qv:case Xv:case r0:return e;default:switch(e=e&&e.$$typeof,e){case Jv:case t0:case i0:case n0:case Zv:return e;default:return t}}case $x:return t}}}o(eo,"z");function hk(e){return eo(e)===e0}o(hk,"A");Ht.AsyncMode=jx;Ht.ConcurrentMode=e0;Ht.ContextConsumer=Jv;Ht.ContextProvider=Zv;Ht.Element=zx;Ht.ForwardRef=t0;Ht.Fragment=Yv;Ht.Lazy=i0;Ht.Memo=n0;Ht.Portal=$x;Ht.Profiler=Qv;Ht.StrictMode=Xv;Ht.Suspense=r0;Ht.isAsyncMode=function(e){return hk(e)||eo(e)===jx};Ht.isConcurrentMode=hk;Ht.isContextConsumer=function(e){return eo(e)===Jv};Ht.isContextProvider=function(e){return eo(e)===Zv};Ht.isElement=function(e){return typeof e=="object"&&e!==null&&e.$$typeof===zx};Ht.isForwardRef=function(e){return eo(e)===t0};Ht.isFragment=function(e){return eo(e)===Yv};Ht.isLazy=function(e){return eo(e)===i0};Ht.isMemo=function(e){return eo(e)===n0};Ht.isPortal=function(e){return eo(e)===$x};Ht.isProfiler=function(e){return eo(e)===Qv};Ht.isStrictMode=function(e){return eo(e)===Xv};Ht.isSuspense=function(e){return eo(e)===r0};Ht.isValidElementType=function(e){return typeof e=="string"||typeof e=="function"||e===Yv||e===e0||e===Qv||e===Xv||e===r0||e===kI||typeof e=="object"&&e!==null&&(e.$$typeof===i0||e.$$typeof===n0||e.$$typeof===Zv||e.$$typeof===Jv||e.$$typeof===t0||e.$$typeof===LI||e.$$typeof===PI||e.$$typeof===OI||e.$$typeof===NI)};Ht.typeOf=eo});var vk=Ue((I4,gk)=>{"use strict";gk.exports=mk()});var Ek=Ue((F4,bk)=>{"use strict";var qx=vk(),MI={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},AI={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},DI={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},yk={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},Vx={};Vx[qx.ForwardRef]=DI;Vx[qx.Memo]=yk;function wk(e){return qx.isMemo(e)?yk:Vx[e.$$typeof]||MI}o(wk,"getStatics");var RI=Object.defineProperty,II=Object.getOwnPropertyNames,xk=Object.getOwnPropertySymbols,FI=Object.getOwnPropertyDescriptor,BI=Object.getPrototypeOf,Sk=Object.prototype;function Ck(e,t,n){if(typeof t!="string"){if(Sk){var l=BI(t);l&&l!==Sk&&Ck(e,l,n)}var d=II(t);xk&&(d=d.concat(xk(t)));for(var h=wk(e),c=wk(t),v=0;v{"use strict";var Nn=typeof Symbol=="function"&&Symbol.for,Kx=Nn?Symbol.for("react.element"):60103,Gx=Nn?Symbol.for("react.portal"):60106,o0=Nn?Symbol.for("react.fragment"):60107,s0=Nn?Symbol.for("react.strict_mode"):60108,l0=Nn?Symbol.for("react.profiler"):60114,a0=Nn?Symbol.for("react.provider"):60109,u0=Nn?Symbol.for("react.context"):60110,Yx=Nn?Symbol.for("react.async_mode"):60111,f0=Nn?Symbol.for("react.concurrent_mode"):60111,c0=Nn?Symbol.for("react.forward_ref"):60112,p0=Nn?Symbol.for("react.suspense"):60113,HI=Nn?Symbol.for("react.suspense_list"):60120,d0=Nn?Symbol.for("react.memo"):60115,h0=Nn?Symbol.for("react.lazy"):60116,WI=Nn?Symbol.for("react.block"):60121,UI=Nn?Symbol.for("react.fundamental"):60117,zI=Nn?Symbol.for("react.responder"):60118,$I=Nn?Symbol.for("react.scope"):60119;function to(e){if(typeof e=="object"&&e!==null){var t=e.$$typeof;switch(t){case Kx:switch(e=e.type,e){case Yx:case f0:case o0:case l0:case s0:case p0:return e;default:switch(e=e&&e.$$typeof,e){case u0:case c0:case h0:case d0:case a0:return e;default:return t}}case Gx:return t}}}o(to,"z");function _k(e){return to(e)===f0}o(_k,"A");Wt.AsyncMode=Yx;Wt.ConcurrentMode=f0;Wt.ContextConsumer=u0;Wt.ContextProvider=a0;Wt.Element=Kx;Wt.ForwardRef=c0;Wt.Fragment=o0;Wt.Lazy=h0;Wt.Memo=d0;Wt.Portal=Gx;Wt.Profiler=l0;Wt.StrictMode=s0;Wt.Suspense=p0;Wt.isAsyncMode=function(e){return _k(e)||to(e)===Yx};Wt.isConcurrentMode=_k;Wt.isContextConsumer=function(e){return to(e)===u0};Wt.isContextProvider=function(e){return to(e)===a0};Wt.isElement=function(e){return typeof e=="object"&&e!==null&&e.$$typeof===Kx};Wt.isForwardRef=function(e){return to(e)===c0};Wt.isFragment=function(e){return to(e)===o0};Wt.isLazy=function(e){return to(e)===h0};Wt.isMemo=function(e){return to(e)===d0};Wt.isPortal=function(e){return to(e)===Gx};Wt.isProfiler=function(e){return to(e)===l0};Wt.isStrictMode=function(e){return to(e)===s0};Wt.isSuspense=function(e){return to(e)===p0};Wt.isValidElementType=function(e){return typeof e=="string"||typeof e=="function"||e===o0||e===f0||e===l0||e===s0||e===p0||e===HI||typeof e=="object"&&e!==null&&(e.$$typeof===h0||e.$$typeof===d0||e.$$typeof===a0||e.$$typeof===u0||e.$$typeof===c0||e.$$typeof===UI||e.$$typeof===zI||e.$$typeof===$I||e.$$typeof===WI)};Wt.typeOf=to});var Nk=Ue((H4,kk)=>{"use strict";kk.exports=Tk()});var Qh=Ue((Np,Xh)=>{(function(){var e,t="4.17.21",n=200,l="Unsupported core-js use. Try https://npms.io/search?q=ponyfill.",d="Expected a function",h="Invalid `variable` option passed into `_.template`",c="__lodash_hash_undefined__",v=500,C="__lodash_placeholder__",k=1,O=2,j=4,B=1,X=2,J=1,Z=2,R=4,A=8,I=16,G=32,K=64,se=128,ne=256,pe=512,me=30,xe="...",Ve=800,tt=16,_e=1,St=2,We=3,Ke=1/0,Ge=9007199254740991,Xe=17976931348623157e292,nr=0/0,ct=4294967295,Hr=ct-1,Qt=ct>>>1,_t=[["ary",se],["bind",J],["bindKey",Z],["curry",A],["curryRight",I],["flip",pe],["partial",G],["partialRight",K],["rearg",ne]],Ct="[object Arguments]",ut="[object Array]",Lr="[object AsyncFunction]",zt="[object Boolean]",$t="[object Date]",ie="[object DOMException]",rt="[object Error]",Pr="[object Function]",Gt="[object GeneratorFunction]",Yt="[object Map]",Se="[object Number]",Or="[object Null]",fn="[object Object]",Un="[object Promise]",si="[object Proxy]",cn="[object RegExp]",Zt="[object Set]",gr="[object String]",pt="[object Symbol]",Ho="[object Undefined]",Cr="[object WeakMap]",Ui="[object WeakSet]",pn="[object ArrayBuffer]",zn="[object DataView]",Si="[object Float32Array]",Ci="[object Float64Array]",$n="[object Int8Array]",Mn="[object Int16Array]",Js="[object Int32Array]",H="[object Uint8Array]",ee="[object Uint8ClampedArray]",he="[object Uint16Array]",Te="[object Uint32Array]",ir=/\b__p \+= '';/g,Ul=/\b(__p \+=) '' \+/g,Ft=/(__e\(.*?\)|\b__t\)) \+\n'';/g,Wr=/&(?:amp|lt|gt|quot|#39);/g,or=/[&<>"']/g,li=RegExp(Wr.source),ds=RegExp(or.source),lo=/<%-([\s\S]+?)%>/g,bi=/<%([\s\S]+?)%>/g,el=/<%=([\s\S]+?)%>/g,hs=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,dn=/^\w*$/,id=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,tl=/[\\^$.*+?()[\]{}|]/g,Qf=RegExp(tl.source),rl=/^\s+/,od=/\s/,Zf=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,wu=/\{\n\/\* \[wrapped with (.+)\] \*/,sd=/,? & /,zl=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,ms=/[()=,{}\[\]\/\s]/,ld=/\\(\\)?/g,Jf=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,nl=/\w*$/,xu=/^[-+]0x[0-9a-f]+$/i,Wo=/^0b[01]+$/i,ad=/^\[object .+?Constructor\]$/,Uo=/^0o[0-7]+$/i,$l=/^(?:0|[1-9]\d*)$/,ec=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,jt=/($^)/,Me=/['\n\r\u2028\u2029\\]/g,Ei="\\ud800-\\udfff",Su="\\u0300-\\u036f",ai="\\ufe20-\\ufe2f",vt="\\u20d0-\\u20ff",ao=Su+ai+vt,zo="\\u2700-\\u27bf",jl="a-z\\xdf-\\xf6\\xf8-\\xff",ue="\\xac\\xb1\\xd7\\xf7",ze="\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf",Cu="\\u2000-\\u206f",bu=" \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",gs="A-Z\\xc0-\\xd6\\xd8-\\xde",il="\\ufe0e\\ufe0f",Eu=ue+ze+Cu+bu,He="['\u2019]",ud="["+Ei+"]",ql="["+Eu+"]",uo="["+ao+"]",ui="\\d+",tc="["+zo+"]",_u="["+jl+"]",$o="[^"+Ei+Eu+ui+zo+jl+gs+"]",vs="\\ud83c[\\udffb-\\udfff]",Tu="(?:"+uo+"|"+vs+")",ol="[^"+Ei+"]",Vl="(?:\\ud83c[\\udde6-\\uddff]){2}",Kl="[\\ud800-\\udbff][\\udc00-\\udfff]",fo="["+gs+"]",Gl="\\u200d",Yl="(?:"+_u+"|"+$o+")",rc="(?:"+fo+"|"+$o+")",Xl="(?:"+He+"(?:d|ll|m|re|s|t|ve))?",_i="(?:"+He+"(?:D|LL|M|RE|S|T|VE))?",nc=Tu+"?",Ql="["+il+"]?",co="(?:"+Gl+"(?:"+[ol,Vl,Kl].join("|")+")"+Ql+nc+")*",ys="\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",ic="\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])",oc=Ql+nc+co,fd="(?:"+[tc,Vl,Kl].join("|")+")"+oc,cd="(?:"+[ol+uo+"?",uo,Vl,Kl,ud].join("|")+")",ku=RegExp(He,"g"),sc=RegExp(uo,"g"),Nu=RegExp(vs+"(?="+vs+")|"+cd+oc,"g"),lc=RegExp([fo+"?"+_u+"+"+Xl+"(?="+[ql,fo,"$"].join("|")+")",rc+"+"+_i+"(?="+[ql,fo+Yl,"$"].join("|")+")",fo+"?"+Yl+"+"+Xl,fo+"+"+_i,ic,ys,ui,fd].join("|"),"g"),ac=RegExp("["+Gl+Ei+ao+il+"]"),Zl=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,Jl=["Array","Buffer","DataView","Date","Error","Float32Array","Float64Array","Function","Int8Array","Int16Array","Int32Array","Map","Math","Object","Promise","RegExp","Set","String","Symbol","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","WeakMap","_","clearTimeout","isFinite","parseInt","setTimeout"],Lu=-1,Ot={};Ot[Si]=Ot[Ci]=Ot[$n]=Ot[Mn]=Ot[Js]=Ot[H]=Ot[ee]=Ot[he]=Ot[Te]=!0,Ot[Ct]=Ot[ut]=Ot[pn]=Ot[zt]=Ot[zn]=Ot[$t]=Ot[rt]=Ot[Pr]=Ot[Yt]=Ot[Se]=Ot[fn]=Ot[cn]=Ot[Zt]=Ot[gr]=Ot[Cr]=!1;var Nt={};Nt[Ct]=Nt[ut]=Nt[pn]=Nt[zn]=Nt[zt]=Nt[$t]=Nt[Si]=Nt[Ci]=Nt[$n]=Nt[Mn]=Nt[Js]=Nt[Yt]=Nt[Se]=Nt[fn]=Nt[cn]=Nt[Zt]=Nt[gr]=Nt[pt]=Nt[H]=Nt[ee]=Nt[he]=Nt[Te]=!0,Nt[rt]=Nt[Pr]=Nt[Cr]=!1;var P={\u00C0:"A",\u00C1:"A",\u00C2:"A",\u00C3:"A",\u00C4:"A",\u00C5:"A",\u00E0:"a",\u00E1:"a",\u00E2:"a",\u00E3:"a",\u00E4:"a",\u00E5:"a",\u00C7:"C",\u00E7:"c",\u00D0:"D",\u00F0:"d",\u00C8:"E",\u00C9:"E",\u00CA:"E",\u00CB:"E",\u00E8:"e",\u00E9:"e",\u00EA:"e",\u00EB:"e",\u00CC:"I",\u00CD:"I",\u00CE:"I",\u00CF:"I",\u00EC:"i",\u00ED:"i",\u00EE:"i",\u00EF:"i",\u00D1:"N",\u00F1:"n",\u00D2:"O",\u00D3:"O",\u00D4:"O",\u00D5:"O",\u00D6:"O",\u00D8:"O",\u00F2:"o",\u00F3:"o",\u00F4:"o",\u00F5:"o",\u00F6:"o",\u00F8:"o",\u00D9:"U",\u00DA:"U",\u00DB:"U",\u00DC:"U",\u00F9:"u",\u00FA:"u",\u00FB:"u",\u00FC:"u",\u00DD:"Y",\u00FD:"y",\u00FF:"y",\u00C6:"Ae",\u00E6:"ae",\u00DE:"Th",\u00FE:"th",\u00DF:"ss",\u0100:"A",\u0102:"A",\u0104:"A",\u0101:"a",\u0103:"a",\u0105:"a",\u0106:"C",\u0108:"C",\u010A:"C",\u010C:"C",\u0107:"c",\u0109:"c",\u010B:"c",\u010D:"c",\u010E:"D",\u0110:"D",\u010F:"d",\u0111:"d",\u0112:"E",\u0114:"E",\u0116:"E",\u0118:"E",\u011A:"E",\u0113:"e",\u0115:"e",\u0117:"e",\u0119:"e",\u011B:"e",\u011C:"G",\u011E:"G",\u0120:"G",\u0122:"G",\u011D:"g",\u011F:"g",\u0121:"g",\u0123:"g",\u0124:"H",\u0126:"H",\u0125:"h",\u0127:"h",\u0128:"I",\u012A:"I",\u012C:"I",\u012E:"I",\u0130:"I",\u0129:"i",\u012B:"i",\u012D:"i",\u012F:"i",\u0131:"i",\u0134:"J",\u0135:"j",\u0136:"K",\u0137:"k",\u0138:"k",\u0139:"L",\u013B:"L",\u013D:"L",\u013F:"L",\u0141:"L",\u013A:"l",\u013C:"l",\u013E:"l",\u0140:"l",\u0142:"l",\u0143:"N",\u0145:"N",\u0147:"N",\u014A:"N",\u0144:"n",\u0146:"n",\u0148:"n",\u014B:"n",\u014C:"O",\u014E:"O",\u0150:"O",\u014D:"o",\u014F:"o",\u0151:"o",\u0154:"R",\u0156:"R",\u0158:"R",\u0155:"r",\u0157:"r",\u0159:"r",\u015A:"S",\u015C:"S",\u015E:"S",\u0160:"S",\u015B:"s",\u015D:"s",\u015F:"s",\u0161:"s",\u0162:"T",\u0164:"T",\u0166:"T",\u0163:"t",\u0165:"t",\u0167:"t",\u0168:"U",\u016A:"U",\u016C:"U",\u016E:"U",\u0170:"U",\u0172:"U",\u0169:"u",\u016B:"u",\u016D:"u",\u016F:"u",\u0171:"u",\u0173:"u",\u0174:"W",\u0175:"w",\u0176:"Y",\u0177:"y",\u0178:"Y",\u0179:"Z",\u017B:"Z",\u017D:"Z",\u017A:"z",\u017C:"z",\u017E:"z",\u0132:"IJ",\u0133:"ij",\u0152:"Oe",\u0153:"oe",\u0149:"'n",\u017F:"s"},Re={"&":"&","<":"<",">":">",'"':""","'":"'"},sl={"&":"&","<":"<",">":">",""":'"',"'":"'"},vr={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},Pu=parseFloat,ye=parseInt,jo=typeof global=="object"&&global&&global.Object===Object&&global,pd=typeof self=="object"&&self&&self.Object===Object&&self,qt=jo||pd||Function("return this")(),ea=typeof Np=="object"&&Np&&!Np.nodeType&&Np,hn=ea&&typeof Xh=="object"&&Xh&&!Xh.nodeType&&Xh,ws=hn&&hn.exports===ea,zi=ws&&jo.process,Ce=function(){try{var q=hn&&hn.require&&hn.require("util").types;return q||zi&&zi.binding&&zi.binding("util")}catch(re){}}(),ta=Ce&&Ce.isArrayBuffer,Ou=Ce&&Ce.isDate,nt=Ce&&Ce.isMap,uc=Ce&&Ce.isRegExp,fi=Ce&&Ce.isSet,ll=Ce&&Ce.isTypedArray;function Ur(q,re,Q){switch(Q.length){case 0:return q.call(re);case 1:return q.call(re,Q[0]);case 2:return q.call(re,Q[0],Q[1]);case 3:return q.call(re,Q[0],Q[1],Q[2])}return q.apply(re,Q)}o(Ur,"apply");function ra(q,re,Q,Pe){for(var Qe=-1,bt=q==null?0:q.length;++Qe-1}o(xs,"arrayIncludes");function qo(q,re,Q){for(var Pe=-1,Qe=q==null?0:q.length;++Pe-1;);return Q}o(qi,"charsStartIndex");function al(q,re){for(var Q=q.length;Q--&&Go(re,q[Q],0)>-1;);return Q}o(al,"charsEndIndex");function cc(q,re){for(var Q=q.length,Pe=0;Q--;)q[Q]===re&&++Pe;return Pe}o(cc,"countHolders");var Wu=oa(P),gd=oa(Re);function Uu(q){return"\\"+vr[q]}o(Uu,"escapeStringChar");function la(q,re){return q==null?e:q[re]}o(la,"getValue");function qn(q){return ac.test(q)}o(qn,"hasUnicode");function Ti(q){return Zl.test(q)}o(Ti,"hasUnicodeWord");function pc(q){for(var re,Q=[];!(re=q.next()).done;)Q.push(re.value);return Q}o(pc,"iteratorToArray");function aa(q){var re=-1,Q=Array(q.size);return q.forEach(function(Pe,Qe){Q[++re]=[Qe,Pe]}),Q}o(aa,"mapToArray");function dc(q,re){return function(Q){return q(re(Q))}}o(dc,"overArg");function Vi(q,re){for(var Q=-1,Pe=q.length,Qe=0,bt=[];++Q-1}o(Gy,"listCacheHas");function yc(s,f){var m=this.__data__,x=gl(m,s);return x<0?(++this.size,m.push([s,f])):m[x][1]=f,this}o(yc,"listCacheSet"),vo.prototype.clear=Cd,vo.prototype.delete=$m,vo.prototype.get=Yu,vo.prototype.has=Gy,vo.prototype.set=yc;function xr(s){var f=-1,m=s==null?0:s.length;for(this.clear();++f=f?s:f)),s}o(vl,"baseClamp");function Dn(s,f,m,x,_,L){var F,z=f&k,Y=f&O,le=f&j;if(m&&(F=_?m(s,x,_,L):m(s)),F!==e)return F;if(!Sr(s))return s;var ae=ot(s);if(ae){if(F=u(s),!z)return qr(s,F)}else{var ce=xn(s),Le=ce==Pr||ce==Gt;if(Ra(s))return Hd(s,z);if(ce==fn||ce==Ct||Le&&!_){if(F=Y||Le?{}:a(s),!z)return Y?dg(s,Qy(F,s)):ow(s,kd(F,s))}else{if(!Nt[ce])return _?s:{};F=p(s,ce,z)}}L||(L=new Ni);var Fe=L.get(s);if(Fe)return Fe;L.set(s,F),Cb(s)?s.forEach(function(je){F.add(Dn(je,f,m,je,s,L))}):xb(s)&&s.forEach(function(je,ht){F.set(ht,Dn(je,f,m,ht,s,L))});var $e=le?Y?Hc:ff:Y?Ai:_n,ft=ae?e:$e(s);return jn(ft||s,function(je,ht){ft&&(ht=je,je=s[ht]),Qu(F,ht,Dn(je,f,m,ht,s,L))}),F}o(Dn,"baseClone");function Gm(s){var f=_n(s);return function(m){return Ym(m,s,f)}}o(Gm,"baseConforms");function Ym(s,f,m){var x=m.length;if(s==null)return!x;for(s=mt(s);x--;){var _=m[x],L=f[_],F=s[_];if(F===e&&!(_ in s)||!L(F))return!1}return!0}o(Ym,"baseConformsTo");function Xm(s,f,m){if(typeof s!="function")throw new Kn(d);return At(function(){s.apply(e,m)},f)}o(Xm,"baseDelay");function va(s,f,m,x){var _=-1,L=xs,F=!0,z=s.length,Y=[],le=f.length;if(!z)return Y;m&&(f=yt(f,zr(m))),x?(L=qo,F=!1):f.length>=n&&(L=mn,F=!1,f=new tn(f));e:for(;++__?0:_+m),x=x===e||x>_?_:lt(x),x<0&&(x+=_),x=m>x?0:Eb(x);m0&&m(z)?f>1?rn(z,f-1,m,x,_):$i(_,z):x||(_[_.length]=z)}return _}o(rn,"baseFlatten");var bc=mg(),$r=mg(!0);function hi(s,f){return s&&bc(s,f,_n)}o(hi,"baseForOwn");function Ec(s,f){return s&&$r(s,f,_n)}o(Ec,"baseForOwnRight");function Ju(s,f){return Vt(f,function(m){return _l(s[m])})}o(Ju,"baseFunctions");function Ds(s,f){f=Gi(f,s);for(var m=0,x=f.length;s!=null&&mf}o(_c,"baseGt");function Qm(s,f){return s!=null&&et.call(s,f)}o(Qm,"baseHas");function Zm(s,f){return s!=null&&f in mt(s)}o(Zm,"baseHasIn");function ya(s,f,m){return s>=Zr(f,m)&&s=120&&ae.length>=120)?new tn(F&&ae):e}ae=s[0];var ce=-1,Le=z[0];e:for(;++ce<_&&le.length-1;)z!==s&&pa.call(z,Y,1),pa.call(s,Y,1);return s}o(Md,"basePullAll");function Ad(s,f){for(var m=s?f.length:0,x=m-1;m--;){var _=f[m];if(m==x||_!==L){var L=_;S(_)?pa.call(s,_,1):of(s,_)}}return s}o(Ad,"basePullAt");function Pc(s,f){return s+ha(Ns()*(f-s+1))}o(Pc,"baseRandom");function lg(s,f,m,x){for(var _=-1,L=sr(da((f-s)/(m||1)),0),F=Q(L);L--;)F[x?L:++_]=s,s+=m;return F}o(lg,"baseRange");function Dd(s,f){var m="";if(!s||f<1||f>Ge)return m;do f%2&&(m+=s),f=ha(f/2),f&&(s+=s);while(f);return m}o(Dd,"baseRepeat");function st(s,f){return Sn(Ie(s,f,Di),s+"")}o(st,"baseRest");function tw(s){return wc(jc(s))}o(tw,"baseSample");function Fs(s,f){var m=jc(s);return Vr(m,vl(f,0,m.length))}o(Fs,"baseSampleSize");function Zo(s,f,m,x){if(!Sr(s))return s;f=Gi(f,s);for(var _=-1,L=f.length,F=L-1,z=s;z!=null&&++__?0:_+f),m=m>_?_:m,m<0&&(m+=_),_=f>m?0:m-f>>>0,f>>>=0;for(var L=Q(_);++x<_;)L[x]=s[x+f];return L}o(Li,"baseSlice");function rw(s,f){var m;return di(s,function(x,_,L){return m=f(x,_,L),!m}),!!m}o(rw,"baseSome");function es(s,f,m){var x=0,_=s==null?x:s.length;if(typeof f=="number"&&f===f&&_<=Qt){for(;x<_;){var L=x+_>>>1,F=s[L];F!==null&&!Yi(F)&&(m?F<=f:F=n){var le=f?null:uf(s);if(le)return w(le);F=!1,_=mn,Y=new tn}else Y=f?[]:z;e:for(;++x=x?s:Li(s,f,m)}o(Bs,"castSlice");var Ta=Fy||function(s){return qt.clearTimeout(s)};function Hd(s,f){if(f)return s.slice();var m=s.length,x=Hm?Hm(m):new s.constructor(m);return s.copy(x),x}o(Hd,"cloneBuffer");function Dc(s){var f=new s.constructor(s.byteLength);return new cl(f).set(new cl(s)),f}o(Dc,"cloneArrayBuffer");function iw(s,f){var m=f?Dc(s.buffer):s.buffer;return new s.constructor(m,s.byteOffset,s.byteLength)}o(iw,"cloneDataView");function Wd(s){var f=new s.constructor(s.source,nl.exec(s));return f.lastIndex=s.lastIndex,f}o(Wd,"cloneRegExp");function ug(s){return lr?mt(lr.call(s)):{}}o(ug,"cloneSymbol");function fg(s,f){var m=f?Dc(s.buffer):s.buffer;return new s.constructor(m,s.byteOffset,s.length)}o(fg,"cloneTypedArray");function Ud(s,f){if(s!==f){var m=s!==e,x=s===null,_=s===s,L=Yi(s),F=f!==e,z=f===null,Y=f===f,le=Yi(f);if(!z&&!le&&!L&&s>f||L&&F&&Y&&!z&&!le||x&&F&&Y||!m&&Y||!_)return 1;if(!x&&!L&&!le&&s=z)return Y;var le=m[x];return Y*(le=="desc"?-1:1)}}return s.index-f.index}o(cg,"compareMultiple");function pg(s,f,m,x){for(var _=-1,L=s.length,F=m.length,z=-1,Y=f.length,le=sr(L-F,0),ae=Q(Y+le),ce=!x;++z1?m[_-1]:e,F=_>2?m[2]:e;for(L=s.length>3&&typeof L=="function"?(_--,L):e,F&&b(m[0],m[1],F)&&(L=_<3?e:L,_=1),f=mt(f);++x<_;){var z=m[x];z&&s(f,z,x,L)}return f})}o(ka,"createAssigner");function hg(s,f){return function(m,x){if(m==null)return m;if(!Mi(m))return s(m,x);for(var _=m.length,L=f?_:-1,F=mt(m);(f?L--:++L<_)&&x(F[L],L,F)!==!1;);return m}}o(hg,"createBaseEach");function mg(s){return function(f,m,x){for(var _=-1,L=mt(f),F=x(f),z=F.length;z--;){var Y=F[s?z:++_];if(m(L[Y],Y,L)===!1)break}return f}}o(mg,"createBaseFor");function gg(s,f,m){var x=f&J,_=La(s);function L(){var F=this&&this!==qt&&this instanceof L?_:s;return F.apply(x?m:this,arguments)}return o(L,"wrapper"),L}o(gg,"createBind");function vg(s){return function(f){f=Dt(f);var m=qn(f)?Kt(f):e,x=m?m[0]:f.charAt(0),_=m?Bs(m,1).join(""):f.slice(1);return x[s]()+_}}o(vg,"createCaseFirst");function Na(s){return function(f){return Au(Ab(Mb(f).replace(ku,"")),s,"")}}o(Na,"createCompounder");function La(s){return function(){var f=arguments;switch(f.length){case 0:return new s;case 1:return new s(f[0]);case 2:return new s(f[0],f[1]);case 3:return new s(f[0],f[1],f[2]);case 4:return new s(f[0],f[1],f[2],f[3]);case 5:return new s(f[0],f[1],f[2],f[3],f[4]);case 6:return new s(f[0],f[1],f[2],f[3],f[4],f[5]);case 7:return new s(f[0],f[1],f[2],f[3],f[4],f[5],f[6])}var m=go(s.prototype),x=s.apply(m,f);return Sr(x)?x:m}}o(La,"createCtor");function zd(s,f,m){var x=La(s);function _(){for(var L=arguments.length,F=Q(L),z=L,Y=Oa(_);z--;)F[z]=arguments[z];var le=L<3&&F[0]!==Y&&F[L-1]!==Y?[]:Vi(F,Y);if(L-=le.length,L-1?_[L?f[F]:F]:e}}o($d,"createFind");function yg(s){return ts(function(f){var m=f.length,x=m,_=An.prototype.thru;for(s&&f.reverse();x--;){var L=f[x];if(typeof L!="function")throw new Kn(d);if(_&&!F&&cf(L)=="wrapper")var F=new An([],!0)}for(x=F?x:m;++x1&>.reverse(),ae&&Yz))return!1;var le=L.get(s),ae=L.get(f);if(le&&ae)return le==f&&ae==s;var ce=-1,Le=!0,Fe=m&X?new tn:e;for(L.set(s,f),L.set(f,s);++ce1?"& ":"")+f[x],f=f.join(m>2?", ":" "),s.replace(Zf,`{ +`+h.stack}return{value:e,source:t,stack:l,digest:null}}i(rp,"Ji");function R1(e,t,n){return{value:e,source:null,stack:n??null,digest:t??null}}i(R1,"Ki");function gS(e,t){try{console.error(t.value)}catch(n){setTimeout(function(){throw n})}}i(gS,"Li");var A3=typeof WeakMap=="function"?WeakMap:Map;function bN(e,t,n){n=cl(-1,n),n.tag=3,n.payload={element:null};var s=t.value;return n.callback=function(){my||(my=!0,TS=s),gS(e,t)},n}i(bN,"Ni");function TN(e,t,n){n=cl(-1,n),n.tag=3;var s=e.type.getDerivedStateFromError;if(typeof s=="function"){var l=t.value;n.payload=function(){return s(l)},n.callback=function(){gS(e,t)}}var h=e.stateNode;return h!==null&&typeof h.componentDidCatch=="function"&&(n.callback=function(){gS(e,t),typeof s!="function"&&(Ru===null?Ru=new Set([this]):Ru.add(this));var d=t.stack;this.componentDidCatch(t.value,{componentStack:d!==null?d:""})}),n}i(TN,"Qi");function FT(e,t,n){var s=e.pingCache;if(s===null){s=e.pingCache=new A3;var l=new Set;s.set(t,l)}else l=s.get(t),l===void 0&&(l=new Set,s.set(t,l));l.has(n)||(l.add(n),e=V3.bind(null,e,t,n),t.then(e,e))}i(FT,"Si");function BT(e){do{var t;if((t=e.tag===13)&&(t=e.memoizedState,t=t!==null?t.dehydrated!==null:!0),t)return e;e=e.return}while(e!==null);return null}i(BT,"Ui");function HT(e,t,n,s,l){return e.mode&1?(e.flags|=65536,e.lanes=l,e):(e===t?e.flags|=65536:(e.flags|=128,n.flags|=131072,n.flags&=-52805,n.tag===1&&(n.alternate===null?n.tag=17:(t=cl(-1,1),t.tag=2,Ou(n,t,1))),n.lanes|=1),e)}i(HT,"Vi");var L3=ml.ReactCurrentOwner,ji=!1;function Ei(e,t,n,s){t.child=e===null?tN(t,null,n,s):ep(t,e.child,n,s)}i(Ei,"Xi");function zT(e,t,n,s,l){n=n.render;var h=t.ref;return Yd(t,l),s=ix(e,t,n,s,h,l),n=ox(),e!==null&&!ji?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,hl(e,t,l)):(hr&&n&&jS(t),t.flags|=1,Ei(e,t,s,l),t.child)}i(zT,"Yi");function UT(e,t,n,s,l){if(e===null){var h=n.type;return typeof h=="function"&&!mx(h)&&h.defaultProps===void 0&&n.compare===null&&n.defaultProps===void 0?(t.tag=15,t.type=h,kN(e,t,h,s,l)):(e=Gv(n.type,null,s,t,t.mode,l),e.ref=t.ref,e.return=t,t.child=e)}if(h=e.child,!(e.lanes&l)){var d=h.memoizedProps;if(n=n.compare,n=n!==null?n:cm,n(d,s)&&e.ref===t.ref)return hl(e,t,l)}return t.flags|=1,e=Iu(h,s),e.ref=t.ref,e.return=t,t.child=e}i(UT,"$i");function kN(e,t,n,s,l){if(e!==null){var h=e.memoizedProps;if(cm(h,s)&&e.ref===t.ref)if(ji=!1,t.pendingProps=s=h,(e.lanes&l)!==0)e.flags&131072&&(ji=!0);else return t.lanes=e.lanes,hl(e,t,l)}return vS(e,t,n,s,l)}i(kN,"bj");function NN(e,t,n){var s=t.pendingProps,l=s.children,h=e!==null?e.memoizedState:null;if(s.mode==="hidden")if(!(t.mode&1))t.memoizedState={baseLanes:0,cachePool:null,transitions:null},rr(Vd,To),To|=n;else{if(!(n&1073741824))return e=h!==null?h.baseLanes|n:n,t.lanes=t.childLanes=1073741824,t.memoizedState={baseLanes:e,cachePool:null,transitions:null},t.updateQueue=null,rr(Vd,To),To|=e,null;t.memoizedState={baseLanes:0,cachePool:null,transitions:null},s=h!==null?h.baseLanes:n,rr(Vd,To),To|=s}else h!==null?(s=h.baseLanes|n,t.memoizedState=null):s=n,rr(Vd,To),To|=s;return Ei(e,t,l,n),t.child}i(NN,"dj");function PN(e,t){var n=t.ref;(e===null&&n!==null||e!==null&&e.ref!==n)&&(t.flags|=512,t.flags|=2097152)}i(PN,"gj");function vS(e,t,n,s,l){var h=Gi(n)?lf:oi.current;return h=Jd(t,h),Yd(t,l),n=ix(e,t,n,s,h,l),s=ox(),e!==null&&!ji?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,hl(e,t,l)):(hr&&s&&jS(t),t.flags|=1,Ei(e,t,n,l),t.child)}i(vS,"cj");function WT(e,t,n,s,l){if(Gi(n)){var h=!0;oy(t)}else h=!1;if(Yd(t,l),t.stateNode===null)qv(e,t),EN(t,n,s),mS(t,n,s,l),s=!0;else if(e===null){var d=t.stateNode,v=t.memoizedProps;d.props=v;var S=d.context,b=n.contextType;typeof b=="object"&&b!==null?b=as(b):(b=Gi(n)?lf:oi.current,b=Jd(t,b));var k=n.getDerivedStateFromProps,R=typeof k=="function"||typeof d.getSnapshotBeforeUpdate=="function";R||typeof d.UNSAFE_componentWillReceiveProps!="function"&&typeof d.componentWillReceiveProps!="function"||(v!==s||S!==b)&&IT(t,d,s,b),Eu=!1;var I=t.memoizedState;d.state=I,cy(t,s,d,l),S=t.memoizedState,v!==s||I!==S||Ki.current||Eu?(typeof k=="function"&&(hS(t,n,k,s),S=t.memoizedState),(v=Eu||DT(t,n,v,s,I,S,b))?(R||typeof d.UNSAFE_componentWillMount!="function"&&typeof d.componentWillMount!="function"||(typeof d.componentWillMount=="function"&&d.componentWillMount(),typeof d.UNSAFE_componentWillMount=="function"&&d.UNSAFE_componentWillMount()),typeof d.componentDidMount=="function"&&(t.flags|=4194308)):(typeof d.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=s,t.memoizedState=S),d.props=s,d.state=S,d.context=b,s=v):(typeof d.componentDidMount=="function"&&(t.flags|=4194308),s=!1)}else{d=t.stateNode,nN(e,t),v=t.memoizedProps,b=t.type===t.elementType?v:Ms(t.type,v),d.props=b,R=t.pendingProps,I=d.context,S=n.contextType,typeof S=="object"&&S!==null?S=as(S):(S=Gi(n)?lf:oi.current,S=Jd(t,S));var z=n.getDerivedStateFromProps;(k=typeof z=="function"||typeof d.getSnapshotBeforeUpdate=="function")||typeof d.UNSAFE_componentWillReceiveProps!="function"&&typeof d.componentWillReceiveProps!="function"||(v!==R||I!==S)&&IT(t,d,s,S),Eu=!1,I=t.memoizedState,d.state=I,cy(t,s,d,l);var j=t.memoizedState;v!==R||I!==j||Ki.current||Eu?(typeof z=="function"&&(hS(t,n,z,s),j=t.memoizedState),(b=Eu||DT(t,n,b,s,I,j,S)||!1)?(k||typeof d.UNSAFE_componentWillUpdate!="function"&&typeof d.componentWillUpdate!="function"||(typeof d.componentWillUpdate=="function"&&d.componentWillUpdate(s,j,S),typeof d.UNSAFE_componentWillUpdate=="function"&&d.UNSAFE_componentWillUpdate(s,j,S)),typeof d.componentDidUpdate=="function"&&(t.flags|=4),typeof d.getSnapshotBeforeUpdate=="function"&&(t.flags|=1024)):(typeof d.componentDidUpdate!="function"||v===e.memoizedProps&&I===e.memoizedState||(t.flags|=4),typeof d.getSnapshotBeforeUpdate!="function"||v===e.memoizedProps&&I===e.memoizedState||(t.flags|=1024),t.memoizedProps=s,t.memoizedState=j),d.props=s,d.state=j,d.context=S,s=b):(typeof d.componentDidUpdate!="function"||v===e.memoizedProps&&I===e.memoizedState||(t.flags|=4),typeof d.getSnapshotBeforeUpdate!="function"||v===e.memoizedProps&&I===e.memoizedState||(t.flags|=1024),s=!1)}return yS(e,t,n,s,h,l)}i(WT,"hj");function yS(e,t,n,s,l,h){PN(e,t);var d=(t.flags&128)!==0;if(!s&&!d)return l&&kT(t,n,!1),hl(e,t,h);s=t.stateNode,L3.current=t;var v=d&&typeof n.getDerivedStateFromError!="function"?null:s.render();return t.flags|=1,e!==null&&d?(t.child=ep(t,e.child,null,h),t.child=ep(t,null,v,h)):Ei(e,t,v,h),t.memoizedState=s.state,l&&kT(t,n,!0),t.child}i(yS,"jj");function AN(e){var t=e.stateNode;t.pendingContext?TT(e,t.pendingContext,t.pendingContext!==t.context):t.context&&TT(e,t.context,!1),ex(e,t.containerInfo)}i(AN,"kj");function $T(e,t,n,s,l){return Zd(),GS(l),t.flags|=256,Ei(e,t,n,s),t.child}i($T,"lj");var wS={dehydrated:null,treeContext:null,retryLane:0};function SS(e){return{baseLanes:e,cachePool:null,transitions:null}}i(SS,"nj");function LN(e,t,n){var s=t.pendingProps,l=Er.current,h=!1,d=(t.flags&128)!==0,v;if((v=d)||(v=e!==null&&e.memoizedState===null?!1:(l&2)!==0),v?(h=!0,t.flags&=-129):(e===null||e.memoizedState!==null)&&(l|=1),rr(Er,l&1),e===null)return dS(t),e=t.memoizedState,e!==null&&(e=e.dehydrated,e!==null)?(t.mode&1?e.data==="$!"?t.lanes=8:t.lanes=1073741824:t.lanes=1,null):(d=s.children,e=s.fallback,h?(s=t.mode,h=t.child,d={mode:"hidden",children:d},!(s&1)&&h!==null?(h.childLanes=0,h.pendingProps=d):h=Py(d,s,0,null),e=af(e,s,n,null),h.return=t,e.return=t,h.sibling=e,t.child=h,t.child.memoizedState=SS(n),t.memoizedState=wS,e):lx(t,d));if(l=e.memoizedState,l!==null&&(v=l.dehydrated,v!==null))return M3(e,t,d,s,v,l,n);if(h){h=s.fallback,d=t.mode,l=e.child,v=l.sibling;var S={mode:"hidden",children:s.children};return!(d&1)&&t.child!==l?(s=t.child,s.childLanes=0,s.pendingProps=S,t.deletions=null):(s=Iu(l,S),s.subtreeFlags=l.subtreeFlags&14680064),v!==null?h=Iu(v,h):(h=af(h,d,n,null),h.flags|=2),h.return=t,s.return=t,s.sibling=h,t.child=s,s=h,h=t.child,d=e.child.memoizedState,d=d===null?SS(n):{baseLanes:d.baseLanes|n,cachePool:null,transitions:d.transitions},h.memoizedState=d,h.childLanes=e.childLanes&~n,t.memoizedState=wS,s}return h=e.child,e=h.sibling,s=Iu(h,{mode:"visible",children:s.children}),!(t.mode&1)&&(s.lanes=n),s.return=t,s.sibling=null,e!==null&&(n=t.deletions,n===null?(t.deletions=[e],t.flags|=16):n.push(e)),t.child=s,t.memoizedState=null,s}i(LN,"oj");function lx(e,t){return t=Py({mode:"visible",children:t},e.mode,0,null),t.return=e,e.child=t}i(lx,"qj");function Iv(e,t,n,s){return s!==null&&GS(s),ep(t,e.child,null,n),e=lx(t,t.pendingProps.children),e.flags|=2,t.memoizedState=null,e}i(Iv,"sj");function M3(e,t,n,s,l,h,d){if(n)return t.flags&256?(t.flags&=-257,s=R1(Error(ge(422))),Iv(e,t,d,s)):t.memoizedState!==null?(t.child=e.child,t.flags|=128,null):(h=s.fallback,l=t.mode,s=Py({mode:"visible",children:s.children},l,0,null),h=af(h,l,d,null),h.flags|=2,s.return=t,h.return=t,s.sibling=h,t.child=s,t.mode&1&&ep(t,e.child,null,d),t.child.memoizedState=SS(d),t.memoizedState=wS,h);if(!(t.mode&1))return Iv(e,t,d,null);if(l.data==="$!"){if(s=l.nextSibling&&l.nextSibling.dataset,s)var v=s.dgst;return s=v,h=Error(ge(419)),s=R1(h,s,void 0),Iv(e,t,d,s)}if(v=(d&e.childLanes)!==0,ji||v){if(s=On,s!==null){switch(d&-d){case 4:l=2;break;case 16:l=8;break;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:l=32;break;case 536870912:l=268435456;break;default:l=0}l=l&(s.suspendedLanes|d)?0:l,l!==0&&l!==h.retryLane&&(h.retryLane=l,pl(e,l),Is(s,e,l,-1))}return hx(),s=R1(Error(ge(421))),Iv(e,t,d,s)}return l.data==="$?"?(t.flags|=128,t.child=e.child,t=q3.bind(null,e),l._reactRetry=t,null):(e=h.treeContext,ko=Mu(l.nextSibling),No=t,hr=!0,Rs=null,e!==null&&(ns[is++]=ll,ns[is++]=ul,ns[is++]=uf,ll=e.id,ul=e.overflow,uf=t),t=lx(t,s.children),t.flags|=4096,t)}i(M3,"rj");function VT(e,t,n){e.lanes|=t;var s=e.alternate;s!==null&&(s.lanes|=t),pS(e.return,t,n)}i(VT,"vj");function D1(e,t,n,s,l){var h=e.memoizedState;h===null?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:s,tail:n,tailMode:l}:(h.isBackwards=t,h.rendering=null,h.renderingStartTime=0,h.last=s,h.tail=n,h.tailMode=l)}i(D1,"wj");function MN(e,t,n){var s=t.pendingProps,l=s.revealOrder,h=s.tail;if(Ei(e,t,s.children,n),s=Er.current,s&2)s=s&1|2,t.flags|=128;else{if(e!==null&&e.flags&128)e:for(e=t.child;e!==null;){if(e.tag===13)e.memoizedState!==null&&VT(e,n,t);else if(e.tag===19)VT(e,n,t);else if(e.child!==null){e.child.return=e,e=e.child;continue}if(e===t)break e;for(;e.sibling===null;){if(e.return===null||e.return===t)break e;e=e.return}e.sibling.return=e.return,e=e.sibling}s&=1}if(rr(Er,s),!(t.mode&1))t.memoizedState=null;else switch(l){case"forwards":for(n=t.child,l=null;n!==null;)e=n.alternate,e!==null&&fy(e)===null&&(l=n),n=n.sibling;n=l,n===null?(l=t.child,t.child=null):(l=n.sibling,n.sibling=null),D1(t,!1,l,n,h);break;case"backwards":for(n=null,l=t.child,t.child=null;l!==null;){if(e=l.alternate,e!==null&&fy(e)===null){t.child=l;break}e=l.sibling,l.sibling=n,n=l,l=e}D1(t,!0,n,null,h);break;case"together":D1(t,!1,null,null,void 0);break;default:t.memoizedState=null}return t.child}i(MN,"xj");function qv(e,t){!(t.mode&1)&&e!==null&&(e.alternate=null,t.alternate=null,t.flags|=2)}i(qv,"ij");function hl(e,t,n){if(e!==null&&(t.dependencies=e.dependencies),ff|=t.lanes,!(n&t.childLanes))return null;if(e!==null&&t.child!==e.child)throw Error(ge(153));if(t.child!==null){for(e=t.child,n=Iu(e,e.pendingProps),t.child=n,n.return=t;e.sibling!==null;)e=e.sibling,n=n.sibling=Iu(e,e.pendingProps),n.return=t;n.sibling=null}return t.child}i(hl,"Zi");function O3(e,t,n){switch(t.tag){case 3:AN(t),Zd();break;case 5:iN(t);break;case 1:Gi(t.type)&&oy(t);break;case 4:ex(t,t.stateNode.containerInfo);break;case 10:var s=t.type._context,l=t.memoizedProps.value;rr(ly,s._currentValue),s._currentValue=l;break;case 13:if(s=t.memoizedState,s!==null)return s.dehydrated!==null?(rr(Er,Er.current&1),t.flags|=128,null):n&t.child.childLanes?LN(e,t,n):(rr(Er,Er.current&1),e=hl(e,t,n),e!==null?e.sibling:null);rr(Er,Er.current&1);break;case 19:if(s=(n&t.childLanes)!==0,e.flags&128){if(s)return MN(e,t,n);t.flags|=128}if(l=t.memoizedState,l!==null&&(l.rendering=null,l.tail=null,l.lastEffect=null),rr(Er,Er.current),s)break;return null;case 22:case 23:return t.lanes=0,NN(e,t,n)}return hl(e,t,n)}i(O3,"yj");var ON,xS,RN,DN;ON=i(function(e,t){for(var n=t.child;n!==null;){if(n.tag===5||n.tag===6)e.appendChild(n.stateNode);else if(n.tag!==4&&n.child!==null){n.child.return=n,n=n.child;continue}if(n===t)break;for(;n.sibling===null;){if(n.return===null||n.return===t)return;n=n.return}n.sibling.return=n.return,n=n.sibling}},"zj");xS=i(function(){},"Aj");RN=i(function(e,t,n,s){var l=e.memoizedProps;if(l!==s){e=t.stateNode,of(Ca.current);var h=null;switch(n){case"input":l=$1(e,l),s=$1(e,s),h=[];break;case"select":l=Tr({},l,{value:void 0}),s=Tr({},s,{value:void 0}),h=[];break;case"textarea":l=j1(e,l),s=j1(e,s),h=[];break;default:typeof l.onClick!="function"&&typeof s.onClick=="function"&&(e.onclick=ny)}G1(n,s);var d;n=null;for(b in l)if(!s.hasOwnProperty(b)&&l.hasOwnProperty(b)&&l[b]!=null)if(b==="style"){var v=l[b];for(d in v)v.hasOwnProperty(d)&&(n||(n={}),n[d]="")}else b!=="dangerouslySetInnerHTML"&&b!=="children"&&b!=="suppressContentEditableWarning"&&b!=="suppressHydrationWarning"&&b!=="autoFocus"&&(nm.hasOwnProperty(b)?h||(h=[]):(h=h||[]).push(b,null));for(b in s){var S=s[b];if(v=l?.[b],s.hasOwnProperty(b)&&S!==v&&(S!=null||v!=null))if(b==="style")if(v){for(d in v)!v.hasOwnProperty(d)||S&&S.hasOwnProperty(d)||(n||(n={}),n[d]="");for(d in S)S.hasOwnProperty(d)&&v[d]!==S[d]&&(n||(n={}),n[d]=S[d])}else n||(h||(h=[]),h.push(b,n)),n=S;else b==="dangerouslySetInnerHTML"?(S=S?S.__html:void 0,v=v?v.__html:void 0,S!=null&&v!==S&&(h=h||[]).push(b,S)):b==="children"?typeof S!="string"&&typeof S!="number"||(h=h||[]).push(b,""+S):b!=="suppressContentEditableWarning"&&b!=="suppressHydrationWarning"&&(nm.hasOwnProperty(b)?(S!=null&&b==="onScroll"&&ur("scroll",e),h||v===S||(h=[])):(h=h||[]).push(b,S))}n&&(h=h||[]).push("style",n);var b=h;(t.updateQueue=b)&&(t.flags|=4)}},"Bj");DN=i(function(e,t,n,s){n!==s&&(t.flags|=4)},"Cj");function Uh(e,t){if(!hr)switch(e.tailMode){case"hidden":t=e.tail;for(var n=null;t!==null;)t.alternate!==null&&(n=t),t=t.sibling;n===null?e.tail=null:n.sibling=null;break;case"collapsed":n=e.tail;for(var s=null;n!==null;)n.alternate!==null&&(s=n),n=n.sibling;s===null?t||e.tail===null?e.tail=null:e.tail.sibling=null:s.sibling=null}}i(Uh,"Dj");function ni(e){var t=e.alternate!==null&&e.alternate.child===e.child,n=0,s=0;if(t)for(var l=e.child;l!==null;)n|=l.lanes|l.childLanes,s|=l.subtreeFlags&14680064,s|=l.flags&14680064,l.return=e,l=l.sibling;else for(l=e.child;l!==null;)n|=l.lanes|l.childLanes,s|=l.subtreeFlags,s|=l.flags,l.return=e,l=l.sibling;return e.subtreeFlags|=s,e.childLanes=n,t}i(ni,"S");function R3(e,t,n){var s=t.pendingProps;switch(KS(t),t.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return ni(t),null;case 1:return Gi(t.type)&&iy(),ni(t),null;case 3:return s=t.stateNode,tp(),cr(Ki),cr(oi),rx(),s.pendingContext&&(s.context=s.pendingContext,s.pendingContext=null),(e===null||e.child===null)&&(Rv(t)?t.flags|=4:e===null||e.memoizedState.isDehydrated&&!(t.flags&256)||(t.flags|=1024,Rs!==null&&(PS(Rs),Rs=null))),xS(e,t),ni(t),null;case 5:tx(t);var l=of(mm.current);if(n=t.type,e!==null&&t.stateNode!=null)RN(e,t,n,s,l),e.ref!==t.ref&&(t.flags|=512,t.flags|=2097152);else{if(!s){if(t.stateNode===null)throw Error(ge(166));return ni(t),null}if(e=of(Ca.current),Rv(t)){s=t.stateNode,n=t.type;var h=t.memoizedProps;switch(s[Sa]=t,s[pm]=h,e=(t.mode&1)!==0,n){case"dialog":ur("cancel",s),ur("close",s);break;case"iframe":case"object":case"embed":ur("load",s);break;case"video":case"audio":for(l=0;l<\/script>",e=e.removeChild(e.firstChild)):typeof s.is=="string"?e=d.createElement(n,{is:s.is}):(e=d.createElement(n),n==="select"&&(d=e,s.multiple?d.multiple=!0:s.size&&(d.size=s.size))):e=d.createElementNS(e,n),e[Sa]=t,e[pm]=s,ON(e,t,!1,!1),t.stateNode=e;e:{switch(d=Y1(n,s),n){case"dialog":ur("cancel",e),ur("close",e),l=s;break;case"iframe":case"object":case"embed":ur("load",e),l=s;break;case"video":case"audio":for(l=0;lnp&&(t.flags|=128,s=!0,Uh(h,!1),t.lanes=4194304)}else{if(!s)if(e=fy(d),e!==null){if(t.flags|=128,s=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Uh(h,!0),h.tail===null&&h.tailMode==="hidden"&&!d.alternate&&!hr)return ni(t),null}else 2*Vr()-h.renderingStartTime>np&&n!==1073741824&&(t.flags|=128,s=!0,Uh(h,!1),t.lanes=4194304);h.isBackwards?(d.sibling=t.child,t.child=d):(n=h.last,n!==null?n.sibling=d:t.child=d,h.last=d)}return h.tail!==null?(t=h.tail,h.rendering=t,h.tail=t.sibling,h.renderingStartTime=Vr(),t.sibling=null,n=Er.current,rr(Er,s?n&1|2:n&1),t):(ni(t),null);case 22:case 23:return px(),s=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==s&&(t.flags|=8192),s&&t.mode&1?To&1073741824&&(ni(t),t.subtreeFlags&6&&(t.flags|=8192)):ni(t),null;case 24:return null;case 25:return null}throw Error(ge(156,t.tag))}i(R3,"Ej");function D3(e,t){switch(KS(t),t.tag){case 1:return Gi(t.type)&&iy(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return tp(),cr(Ki),cr(oi),rx(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return tx(t),null;case 13:if(cr(Er),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(ge(340));Zd()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return cr(Er),null;case 4:return tp(),null;case 10:return QS(t.type._context),null;case 22:case 23:return px(),null;case 24:return null;default:return null}}i(D3,"Ij");var Fv=!1,ii=!1,I3=typeof WeakSet=="function"?WeakSet:Set,Le=null;function $d(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(s){Mr(e,t,s)}else n.current=null}i($d,"Lj");function CS(e,t,n){try{n()}catch(s){Mr(e,t,s)}}i(CS,"Mj");var qT=!1;function F3(e,t){if(oS=ey,e=zk(),qS(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var s=n.getSelection&&n.getSelection();if(s&&s.rangeCount!==0){n=s.anchorNode;var l=s.anchorOffset,h=s.focusNode;s=s.focusOffset;try{n.nodeType,h.nodeType}catch{n=null;break e}var d=0,v=-1,S=-1,b=0,k=0,R=e,I=null;t:for(;;){for(var z;R!==n||l!==0&&R.nodeType!==3||(v=d+l),R!==h||s!==0&&R.nodeType!==3||(S=d+s),R.nodeType===3&&(d+=R.nodeValue.length),(z=R.firstChild)!==null;)I=R,R=z;for(;;){if(R===e)break t;if(I===n&&++b===l&&(v=d),I===h&&++k===s&&(S=d),(z=R.nextSibling)!==null)break;R=I,I=R.parentNode}R=z}n=v===-1||S===-1?null:{start:v,end:S}}else n=null}n=n||{start:0,end:0}}else n=null;for(sS={focusedElem:e,selectionRange:n},ey=!1,Le=t;Le!==null;)if(t=Le,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,Le=e;else for(;Le!==null;){t=Le;try{var j=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(j!==null){var Y=j.memoizedProps,ne=j.memoizedState,F=t.stateNode,L=F.getSnapshotBeforeUpdate(t.elementType===t.type?Y:Ms(t.type,Y),ne);F.__reactInternalSnapshotBeforeUpdate=L}break;case 3:var D=t.stateNode.containerInfo;D.nodeType===1?D.textContent="":D.nodeType===9&&D.documentElement&&D.removeChild(D.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(ge(163))}}catch(q){Mr(t,t.return,q)}if(e=t.sibling,e!==null){e.return=t.return,Le=e;break}Le=t.return}return j=qT,qT=!1,j}i(F3,"Oj");function em(e,t,n){var s=t.updateQueue;if(s=s!==null?s.lastEffect:null,s!==null){var l=s=s.next;do{if((l.tag&e)===e){var h=l.destroy;l.destroy=void 0,h!==void 0&&CS(t,n,h)}l=l.next}while(l!==s)}}i(em,"Pj");function ky(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var s=n.create;n.destroy=s()}n=n.next}while(n!==t)}}i(ky,"Qj");function _S(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}i(_S,"Rj");function IN(e){var t=e.alternate;t!==null&&(e.alternate=null,IN(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Sa],delete t[pm],delete t[uS],delete t[w3],delete t[S3])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}i(IN,"Sj");function FN(e){return e.tag===5||e.tag===3||e.tag===4}i(FN,"Tj");function jT(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||FN(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}i(jT,"Uj");function ES(e,t,n){var s=e.tag;if(s===5||s===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=ny));else if(s!==4&&(e=e.child,e!==null))for(ES(e,t,n),e=e.sibling;e!==null;)ES(e,t,n),e=e.sibling}i(ES,"Vj");function bS(e,t,n){var s=e.tag;if(s===5||s===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(s!==4&&(e=e.child,e!==null))for(bS(e,t,n),e=e.sibling;e!==null;)bS(e,t,n),e=e.sibling}i(bS,"Wj");var Vn=null,Os=!1;function Cu(e,t,n){for(n=n.child;n!==null;)BN(e,t,n),n=n.sibling}i(Cu,"Yj");function BN(e,t,n){if(xa&&typeof xa.onCommitFiberUnmount=="function")try{xa.onCommitFiberUnmount(wy,n)}catch{}switch(n.tag){case 5:ii||$d(n,t);case 6:var s=Vn,l=Os;Vn=null,Cu(e,t,n),Vn=s,Os=l,Vn!==null&&(Os?(e=Vn,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):Vn.removeChild(n.stateNode));break;case 18:Vn!==null&&(Os?(e=Vn,n=n.stateNode,e.nodeType===8?N1(e.parentNode,n):e.nodeType===1&&N1(e,n),lm(e)):N1(Vn,n.stateNode));break;case 4:s=Vn,l=Os,Vn=n.stateNode.containerInfo,Os=!0,Cu(e,t,n),Vn=s,Os=l;break;case 0:case 11:case 14:case 15:if(!ii&&(s=n.updateQueue,s!==null&&(s=s.lastEffect,s!==null))){l=s=s.next;do{var h=l,d=h.destroy;h=h.tag,d!==void 0&&(h&2||h&4)&&CS(n,t,d),l=l.next}while(l!==s)}Cu(e,t,n);break;case 1:if(!ii&&($d(n,t),s=n.stateNode,typeof s.componentWillUnmount=="function"))try{s.props=n.memoizedProps,s.state=n.memoizedState,s.componentWillUnmount()}catch(v){Mr(n,t,v)}Cu(e,t,n);break;case 21:Cu(e,t,n);break;case 22:n.mode&1?(ii=(s=ii)||n.memoizedState!==null,Cu(e,t,n),ii=s):Cu(e,t,n);break;default:Cu(e,t,n)}}i(BN,"Zj");function KT(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new I3),t.forEach(function(s){var l=j3.bind(null,e,s);n.has(s)||(n.add(s),s.then(l,l))})}}i(KT,"ak");function Ls(e,t){var n=t.deletions;if(n!==null)for(var s=0;sl&&(l=d),s&=~h}if(s=l,s=Vr()-s,s=(120>s?120:480>s?480:1080>s?1080:1920>s?1920:3e3>s?3e3:4320>s?4320:1960*H3(s/1960))-s,10e?16:e,Nu===null)var s=!1;else{if(e=Nu,Nu=null,gy=0,Nt&6)throw Error(ge(331));var l=Nt;for(Nt|=4,Le=e.current;Le!==null;){var h=Le,d=h.child;if(Le.flags&16){var v=h.deletions;if(v!==null){for(var S=0;SVr()-fx?sf(e,0):cx|=n),Yi(e,t)}i(V3,"Ti");function jN(e,t){t===0&&(e.mode&1?(t=Ev,Ev<<=1,!(Ev&130023424)&&(Ev=4194304)):t=1);var n=bi();e=pl(e,t),e!==null&&(Sm(e,t,n),Yi(e,n))}i(jN,"Yk");function q3(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),jN(e,n)}i(q3,"uj");function j3(e,t){var n=0;switch(e.tag){case 13:var s=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:s=e.stateNode;break;default:throw Error(ge(314))}s!==null&&s.delete(t),jN(e,n)}i(j3,"bk");var KN;KN=i(function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Ki.current)ji=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return ji=!1,O3(e,t,n);ji=!!(e.flags&131072)}else ji=!1,hr&&t.flags&1048576&&Qk(t,ay,t.index);switch(t.lanes=0,t.tag){case 2:var s=t.type;qv(e,t),e=t.pendingProps;var l=Jd(t,oi.current);Yd(t,n),l=ix(null,t,s,e,l,n);var h=ox();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Gi(s)?(h=!0,oy(t)):h=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,ZS(t),l.updater=Ty,t.stateNode=l,l._reactInternals=t,mS(t,s,e,n),t=yS(null,t,s,!0,h,n)):(t.tag=0,hr&&h&&jS(t),Ei(null,t,l,n),t=t.child),t;case 16:s=t.elementType;e:{switch(qv(e,t),e=t.pendingProps,l=s._init,s=l(s._payload),t.type=s,l=t.tag=G3(s),e=Ms(s,e),l){case 0:t=vS(null,t,s,e,n);break e;case 1:t=WT(null,t,s,e,n);break e;case 11:t=zT(null,t,s,e,n);break e;case 14:t=UT(null,t,s,Ms(s.type,e),n);break e}throw Error(ge(306,s,""))}return t;case 0:return s=t.type,l=t.pendingProps,l=t.elementType===s?l:Ms(s,l),vS(e,t,s,l,n);case 1:return s=t.type,l=t.pendingProps,l=t.elementType===s?l:Ms(s,l),WT(e,t,s,l,n);case 3:e:{if(AN(t),e===null)throw Error(ge(387));s=t.pendingProps,h=t.memoizedState,l=h.element,nN(e,t),cy(t,s,null,n);var d=t.memoizedState;if(s=d.element,h.isDehydrated)if(h={element:s,isDehydrated:!1,cache:d.cache,pendingSuspenseBoundaries:d.pendingSuspenseBoundaries,transitions:d.transitions},t.updateQueue.baseState=h,t.memoizedState=h,t.flags&256){l=rp(Error(ge(423)),t),t=$T(e,t,s,n,l);break e}else if(s!==l){l=rp(Error(ge(424)),t),t=$T(e,t,s,n,l);break e}else for(ko=Mu(t.stateNode.containerInfo.firstChild),No=t,hr=!0,Rs=null,n=tN(t,null,s,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Zd(),s===l){t=hl(e,t,n);break e}Ei(e,t,s,n)}t=t.child}return t;case 5:return iN(t),e===null&&dS(t),s=t.type,l=t.pendingProps,h=e!==null?e.memoizedProps:null,d=l.children,aS(s,l)?d=null:h!==null&&aS(s,h)&&(t.flags|=32),PN(e,t),Ei(e,t,d,n),t.child;case 6:return e===null&&dS(t),null;case 13:return LN(e,t,n);case 4:return ex(t,t.stateNode.containerInfo),s=t.pendingProps,e===null?t.child=ep(t,null,s,n):Ei(e,t,s,n),t.child;case 11:return s=t.type,l=t.pendingProps,l=t.elementType===s?l:Ms(s,l),zT(e,t,s,l,n);case 7:return Ei(e,t,t.pendingProps,n),t.child;case 8:return Ei(e,t,t.pendingProps.children,n),t.child;case 12:return Ei(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(s=t.type._context,l=t.pendingProps,h=t.memoizedProps,d=l.value,rr(ly,s._currentValue),s._currentValue=d,h!==null)if(Fs(h.value,d)){if(h.children===l.children&&!Ki.current){t=hl(e,t,n);break e}}else for(h=t.child,h!==null&&(h.return=t);h!==null;){var v=h.dependencies;if(v!==null){d=h.child;for(var S=v.firstContext;S!==null;){if(S.context===s){if(h.tag===1){S=cl(-1,n&-n),S.tag=2;var b=h.updateQueue;if(b!==null){b=b.shared;var k=b.pending;k===null?S.next=S:(S.next=k.next,k.next=S),b.pending=S}}h.lanes|=n,S=h.alternate,S!==null&&(S.lanes|=n),pS(h.return,n,t),v.lanes|=n;break}S=S.next}}else if(h.tag===10)d=h.type===t.type?null:h.child;else if(h.tag===18){if(d=h.return,d===null)throw Error(ge(341));d.lanes|=n,v=d.alternate,v!==null&&(v.lanes|=n),pS(d,n,t),d=h.sibling}else d=h.child;if(d!==null)d.return=h;else for(d=h;d!==null;){if(d===t){d=null;break}if(h=d.sibling,h!==null){h.return=d.return,d=h;break}d=d.return}h=d}Ei(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,s=t.pendingProps.children,Yd(t,n),l=as(l),s=s(l),t.flags|=1,Ei(e,t,s,n),t.child;case 14:return s=t.type,l=Ms(s,t.pendingProps),l=Ms(s.type,l),UT(e,t,s,l,n);case 15:return kN(e,t,t.type,t.pendingProps,n);case 17:return s=t.type,l=t.pendingProps,l=t.elementType===s?l:Ms(s,l),qv(e,t),t.tag=1,Gi(s)?(e=!0,oy(t)):e=!1,Yd(t,n),EN(t,s,l),mS(t,s,l,n),yS(null,t,s,!0,e,n);case 19:return MN(e,t,n);case 22:return NN(e,t,n)}throw Error(ge(156,t.tag))},"Vk");function GN(e,t){return xk(e,t)}i(GN,"Fk");function K3(e,t,n,s){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=s,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}i(K3,"$k");function os(e,t,n,s){return new K3(e,t,n,s)}i(os,"Bg");function mx(e){return e=e.prototype,!(!e||!e.isReactComponent)}i(mx,"aj");function G3(e){if(typeof e=="function")return mx(e)?1:0;if(e!=null){if(e=e.$$typeof,e===RS)return 11;if(e===DS)return 14}return 2}i(G3,"Zk");function Iu(e,t){var n=e.alternate;return n===null?(n=os(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}i(Iu,"Pg");function Gv(e,t,n,s,l,h){var d=2;if(s=e,typeof e=="function")mx(e)&&(d=1);else if(typeof e=="string")d=5;else e:switch(e){case Rd:return af(n.children,l,h,t);case OS:d=8,l|=8;break;case H1:return e=os(12,n,t,l|2),e.elementType=H1,e.lanes=h,e;case z1:return e=os(13,n,t,l),e.elementType=z1,e.lanes=h,e;case U1:return e=os(19,n,t,l),e.elementType=U1,e.lanes=h,e;case ik:return Py(n,l,h,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case rk:d=10;break e;case nk:d=9;break e;case RS:d=11;break e;case DS:d=14;break e;case _u:d=16,s=null;break e}throw Error(ge(130,e==null?e:typeof e,""))}return t=os(d,n,t,l),t.elementType=e,t.type=s,t.lanes=h,t}i(Gv,"Rg");function af(e,t,n,s){return e=os(7,e,s,t),e.lanes=n,e}i(af,"Tg");function Py(e,t,n,s){return e=os(22,e,s,t),e.elementType=ik,e.lanes=n,e.stateNode={isHidden:!1},e}i(Py,"pj");function I1(e,t,n){return e=os(6,e,null,t),e.lanes=n,e}i(I1,"Qg");function F1(e,t,n){return t=os(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}i(F1,"Sg");function Y3(e,t,n,s,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=S1(0),this.expirationTimes=S1(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=S1(0),this.identifierPrefix=s,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}i(Y3,"al");function gx(e,t,n,s,l,h,d,v,S){return e=new Y3(e,t,n,v,S),t===1?(t=1,h===!0&&(t|=8)):t=0,h=os(3,null,null,t),e.current=h,h.stateNode=e,h.memoizedState={element:s,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},ZS(h),e}i(gx,"bl");function X3(e,t,n){var s=3{"use strict";function ZN(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(ZN)}catch(e){console.error(e)}}i(ZN,"checkDCE");ZN(),eP.exports=JN()});var rP=rt(xx=>{"use strict";var tP=Sx();xx.createRoot=tP.createRoot,xx.hydrateRoot=tP.hydrateRoot;var UW});var iP=rt(nP=>{"use strict";var Em=be();function t4(e,t){return e===t&&(e!==0||1/e===1/t)||e!==e&&t!==t}i(t4,"n");var r4=typeof Object.is=="function"?Object.is:t4,n4=Em.useSyncExternalStore,i4=Em.useRef,o4=Em.useEffect,s4=Em.useMemo,a4=Em.useDebugValue;nP.useSyncExternalStoreWithSelector=function(e,t,n,s,l){var h=i4(null);if(h.current===null){var d={hasValue:!1,value:null};h.current=d}else d=h.current;h=s4(function(){function S(z){if(!b){if(b=!0,k=z,z=s(z),l!==void 0&&d.hasValue){var j=d.value;if(l(j,z))return R=j}return R=z}if(j=R,r4(k,z))return j;var Y=s(z);return l!==void 0&&l(j,Y)?j:(k=z,R=Y)}i(S,"a");var b=!1,k,R,I=n===void 0?null:n;return[function(){return S(t())},I===null?void 0:function(){return S(I())}]},[t,n,s,l]);var v=n4(e,h[0],h[1]);return o4(function(){d.hasValue=!0,d.value=v},[v]),a4(v),v}});var sP=rt((qW,oP)=>{"use strict";oP.exports=iP()});var Tm=rt((sp,bm)=>{(function(){var e,t="4.17.21",n=200,s="Unsupported core-js use. Try https://npms.io/search?q=ponyfill.",l="Expected a function",h="Invalid `variable` option passed into `_.template`",d="__lodash_hash_undefined__",v=500,S="__lodash_placeholder__",b=1,k=2,R=4,I=1,z=2,j=1,Y=2,ne=4,F=8,L=16,D=32,q=64,te=128,Z=256,oe=512,he=30,Re="...",Ee=800,Ye=16,tt=1,xe=2,Xe=3,je=1/0,Qe=9007199254740991,ot=17976931348623157e292,It=NaN,Pt=4294967295,fn=Pt-1,dn=Pt>>>1,gr=[["ary",te],["bind",j],["bindKey",Y],["curry",F],["curryRight",L],["flip",oe],["partial",D],["partialRight",q],["rearg",Z]],Wt="[object Arguments]",vr="[object Array]",yr="[object AsyncFunction]",Gt="[object Boolean]",Ft="[object Date]",se="[object DOMException]",Ue="[object Error]",Gr="[object Function]",Zt="[object GeneratorFunction]",st="[object Map]",Fe="[object Number]",Fn="[object Null]",bn="[object Object]",no="[object Promise]",Ho="[object Proxy]",lt="[object RegExp]",wr="[object Set]",fr="[object String]",pt="[object Symbol]",io="[object Undefined]",Bn="[object WeakMap]",Mi="[object WeakSet]",Yr="[object ArrayBuffer]",Xr="[object DataView]",oo="[object Float32Array]",hi="[object Float64Array]",Hn="[object Int8Array]",so="[object Int16Array]",bl="[object Int32Array]",X="[object Uint8Array]",ee="[object Uint8ClampedArray]",Ce="[object Uint16Array]",Me="[object Uint32Array]",Tl=/\b__p \+= '';/g,ze=/\b(__p \+=) '' \+/g,Qr=/(__e\(.*?\)|\b__t\)) \+\n'';/g,Yt=/&(?:amp|lt|gt|quot|#39);/g,Mt=/[&<>"']/g,At=RegExp(Yt.source),Na=RegExp(Mt.source),pn=/<%-([\s\S]+?)%>/g,zn=/<%([\s\S]+?)%>/g,kr=/<%=([\s\S]+?)%>/g,gs=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,Tn=/^\w*$/,Pa=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,Aa=/[\\^$.*+?()[\]{}|]/g,nc=RegExp(Aa.source),ic=/^\s+/,oc=/\s/,$f=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,sc=/\{\n\/\* \[wrapped with (.+)\] \*/,Bp=/,? & /,kl=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,Hp=/[()=,{}\[\]\/\s]/,zp=/\\(\\)?/g,Nl=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,Vf=/\w*$/,ac=/^[-+]0x[0-9a-f]+$/i,vs=/^0b[01]+$/i,Up=/^\[object .+?Constructor\]$/,Wp=/^0o[0-7]+$/i,La=/^(?:0|[1-9]\d*)$/,lc=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,ao=/($^)/,qf=/['\n\r\u2028\u2029\\]/g,Oi="\\ud800-\\udfff",uc="\\u0300-\\u036f",cc="\\ufe20-\\ufe2f",or="\\u20d0-\\u20ff",Ae=uc+cc+or,lo="\\u2700-\\u27bf",Pl="a-z\\xdf-\\xf6\\xf8-\\xff",Ri="\\xac\\xb1\\xd7\\xf7",vt="\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf",ys="\\u2000-\\u206f",Ma=" \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",Oa="A-Z\\xc0-\\xd6\\xd8-\\xde",ce="\\ufe0e\\ufe0f",Ve=Ri+vt+ys+Ma,qs="['\u2019]",fc="["+Oi+"]",js="["+Ve+"]",zo="["+Ae+"]",dc="\\d+",qe="["+lo+"]",jf="["+Pl+"]",Al="[^"+Oi+Ve+dc+lo+Pl+Oa+"]",ws="\\ud83c[\\udffb-\\udfff]",mi="(?:"+zo+"|"+ws+")",Ll="[^"+Oi+"]",Ml="(?:\\ud83c[\\udde6-\\uddff]){2}",Ss="[\\ud800-\\udbff][\\udc00-\\udfff]",uo="["+Oa+"]",Ol="\\u200d",Ks="(?:"+jf+"|"+Al+")",pc="(?:"+uo+"|"+Al+")",Rl="(?:"+qs+"(?:d|ll|m|re|s|t|ve))?",hc="(?:"+qs+"(?:D|LL|M|RE|S|T|VE))?",mc=mi+"?",Dl="["+ce+"]?",Ra="(?:"+Ol+"(?:"+[Ll,Ml,Ss].join("|")+")"+Dl+mc+")*",gc="\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",Di="\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])",Kf=Dl+mc+Ra,$p="(?:"+[qe,Ml,Ss].join("|")+")"+Kf,Il="(?:"+[Ll+zo+"?",zo,Ml,Ss,fc].join("|")+")",Da=RegExp(qs,"g"),Vp=RegExp(zo,"g"),vc=RegExp(ws+"(?="+ws+")|"+Il+Kf,"g"),qp=RegExp([uo+"?"+jf+"+"+Rl+"(?="+[js,uo,"$"].join("|")+")",pc+"+"+hc+"(?="+[js,uo+Ks,"$"].join("|")+")",uo+"?"+Ks+"+"+Rl,uo+"+"+hc,Di,gc,dc,$p].join("|"),"g"),jp=RegExp("["+Ol+Oi+Ae+ce+"]"),yc=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,Gf=["Array","Buffer","DataView","Date","Error","Float32Array","Float64Array","Function","Int8Array","Int16Array","Int32Array","Map","Math","Object","Promise","RegExp","Set","String","Symbol","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","WeakMap","_","clearTimeout","isFinite","parseInt","setTimeout"],Kp=-1,Bt={};Bt[oo]=Bt[hi]=Bt[Hn]=Bt[so]=Bt[bl]=Bt[X]=Bt[ee]=Bt[Ce]=Bt[Me]=!0,Bt[Wt]=Bt[vr]=Bt[Yr]=Bt[Gt]=Bt[Xr]=Bt[Ft]=Bt[Ue]=Bt[Gr]=Bt[st]=Bt[Fe]=Bt[bn]=Bt[lt]=Bt[wr]=Bt[fr]=Bt[Bn]=!1;var Ot={};Ot[Wt]=Ot[vr]=Ot[Yr]=Ot[Xr]=Ot[Gt]=Ot[Ft]=Ot[oo]=Ot[hi]=Ot[Hn]=Ot[so]=Ot[bl]=Ot[st]=Ot[Fe]=Ot[bn]=Ot[lt]=Ot[wr]=Ot[fr]=Ot[pt]=Ot[X]=Ot[ee]=Ot[Ce]=Ot[Me]=!0,Ot[Ue]=Ot[Gr]=Ot[Bn]=!1;var M={\u00C0:"A",\u00C1:"A",\u00C2:"A",\u00C3:"A",\u00C4:"A",\u00C5:"A",\u00E0:"a",\u00E1:"a",\u00E2:"a",\u00E3:"a",\u00E4:"a",\u00E5:"a",\u00C7:"C",\u00E7:"c",\u00D0:"D",\u00F0:"d",\u00C8:"E",\u00C9:"E",\u00CA:"E",\u00CB:"E",\u00E8:"e",\u00E9:"e",\u00EA:"e",\u00EB:"e",\u00CC:"I",\u00CD:"I",\u00CE:"I",\u00CF:"I",\u00EC:"i",\u00ED:"i",\u00EE:"i",\u00EF:"i",\u00D1:"N",\u00F1:"n",\u00D2:"O",\u00D3:"O",\u00D4:"O",\u00D5:"O",\u00D6:"O",\u00D8:"O",\u00F2:"o",\u00F3:"o",\u00F4:"o",\u00F5:"o",\u00F6:"o",\u00F8:"o",\u00D9:"U",\u00DA:"U",\u00DB:"U",\u00DC:"U",\u00F9:"u",\u00FA:"u",\u00FB:"u",\u00FC:"u",\u00DD:"Y",\u00FD:"y",\u00FF:"y",\u00C6:"Ae",\u00E6:"ae",\u00DE:"Th",\u00FE:"th",\u00DF:"ss",\u0100:"A",\u0102:"A",\u0104:"A",\u0101:"a",\u0103:"a",\u0105:"a",\u0106:"C",\u0108:"C",\u010A:"C",\u010C:"C",\u0107:"c",\u0109:"c",\u010B:"c",\u010D:"c",\u010E:"D",\u0110:"D",\u010F:"d",\u0111:"d",\u0112:"E",\u0114:"E",\u0116:"E",\u0118:"E",\u011A:"E",\u0113:"e",\u0115:"e",\u0117:"e",\u0119:"e",\u011B:"e",\u011C:"G",\u011E:"G",\u0120:"G",\u0122:"G",\u011D:"g",\u011F:"g",\u0121:"g",\u0123:"g",\u0124:"H",\u0126:"H",\u0125:"h",\u0127:"h",\u0128:"I",\u012A:"I",\u012C:"I",\u012E:"I",\u0130:"I",\u0129:"i",\u012B:"i",\u012D:"i",\u012F:"i",\u0131:"i",\u0134:"J",\u0135:"j",\u0136:"K",\u0137:"k",\u0138:"k",\u0139:"L",\u013B:"L",\u013D:"L",\u013F:"L",\u0141:"L",\u013A:"l",\u013C:"l",\u013E:"l",\u0140:"l",\u0142:"l",\u0143:"N",\u0145:"N",\u0147:"N",\u014A:"N",\u0144:"n",\u0146:"n",\u0148:"n",\u014B:"n",\u014C:"O",\u014E:"O",\u0150:"O",\u014D:"o",\u014F:"o",\u0151:"o",\u0154:"R",\u0156:"R",\u0158:"R",\u0155:"r",\u0157:"r",\u0159:"r",\u015A:"S",\u015C:"S",\u015E:"S",\u0160:"S",\u015B:"s",\u015D:"s",\u015F:"s",\u0161:"s",\u0162:"T",\u0164:"T",\u0166:"T",\u0163:"t",\u0165:"t",\u0167:"t",\u0168:"U",\u016A:"U",\u016C:"U",\u016E:"U",\u0170:"U",\u0172:"U",\u0169:"u",\u016B:"u",\u016D:"u",\u016F:"u",\u0171:"u",\u0173:"u",\u0174:"W",\u0175:"w",\u0176:"Y",\u0177:"y",\u0178:"Y",\u0179:"Z",\u017B:"Z",\u017D:"Z",\u017A:"z",\u017C:"z",\u017E:"z",\u0132:"IJ",\u0133:"ij",\u0152:"Oe",\u0153:"oe",\u0149:"'n",\u017F:"s"},De={"&":"&","<":"<",">":">",'"':""","'":"'"},Gs={"&":"&","<":"<",">":">",""":'"',"'":"'"},gi={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},Fl=parseFloat,ve=parseInt,Ia=typeof global=="object"&&global&&global.Object===Object&&global,Gp=typeof self=="object"&&self&&self.Object===Object&&self,Rt=Ia||Gp||Function("return this")(),Yf=typeof sp=="object"&&sp&&!sp.nodeType&&sp,Ys=Yf&&typeof bm=="object"&&bm&&!bm.nodeType&&bm,Fa=Ys&&Ys.exports===Yf,Uo=Fa&&Ia.process,Se=function(){try{var G=Ys&&Ys.require&&Ys.require("util").types;return G||Uo&&Uo.binding&&Uo.binding("util")}catch{}}(),Xs=Se&&Se.isArrayBuffer,vi=Se&&Se.isDate,Ze=Se&&Se.isMap,Bl=Se&&Se.isRegExp,hn=Se&&Se.isSet,Hl=Se&&Se.isTypedArray;function Jr(G,ie,J){switch(J.length){case 0:return G.call(ie);case 1:return G.call(ie,J[0]);case 2:return G.call(ie,J[0],J[1]);case 3:return G.call(ie,J[0],J[1],J[2])}return G.apply(ie,J)}i(Jr,"apply");function zl(G,ie,J,Pe){for(var Je=-1,yt=G==null?0:G.length;++Je-1}i(Ba,"arrayIncludes");function Ul(G,ie,J){for(var Pe=-1,Je=G==null?0:G.length;++Pe-1;);return J}i(yi,"charsStartIndex");function ea(G,ie){for(var J=G.length;J--&&Kt(ie,G[J],0)>-1;);return J}i(ea,"charsEndIndex");function ql(G,ie){for(var J=G.length,Pe=0;J--;)G[J]===ie&&++Pe;return Pe}i(ql,"countHolders");var Tc=Ha(M),Fi=Ha(De);function $o(G){return"\\"+gi[G]}i($o,"escapeStringChar");function jl(G,ie){return G==null?e:G[ie]}i(jl,"getValue");function xs(G){return jp.test(G)}i(xs,"hasUnicode");function td(G){return yc.test(G)}i(td,"hasUnicodeWord");function Qp(G){for(var ie,J=[];!(ie=G.next()).done;)J.push(ie.value);return J}i(Qp,"iteratorToArray");function Kl(G){var ie=-1,J=Array(G.size);return G.forEach(function(Pe,Je){J[++ie]=[Je,Pe]}),J}i(Kl,"mapToArray");function Gl(G,ie){return function(J){return G(ie(J))}}i(Gl,"overArg");function kn(G,ie){for(var J=-1,Pe=G.length,Je=0,yt=[];++J-1}i(xg,"listCacheHas");function Cg(a,f){var m=this.__data__,x=tu(m,a);return x<0?(++this.size,m.push([a,f])):m[x][1]=f,this}i(Cg,"listCacheSet"),vo.prototype.clear=Cw,vo.prototype.delete=sh,vo.prototype.get=ah,vo.prototype.has=xg,vo.prototype.set=Cg;function yo(a){var f=-1,m=a==null?0:a.length;for(this.clear();++f=f?a:f)),a}i(Ts,"baseClamp");function wi(a,f,m,x,T,A){var H,$=f&b,Q=f&k,le=f&R;if(m&&(H=T?m(a,x,T,A):m(a)),H!==e)return H;if(!_r(a))return a;var ue=at(a);if(ue){if(H=zw(a),!$)return ei(a,H)}else{var fe=zr(a),Ne=fe==Gr||fe==Zt;if(Su(a))return xh(a,$);if(fe==bn||fe==Wt||Ne&&!T){if(H=Q||Ne?{}:rv(a),!$)return Q?Dw(a,Tg(H,a)):jg(a,ch(H,a))}else{if(!Ot[fe])return T?a:{};H=kd(a,fe,$)}}A||(A=new sr);var He=A.get(a);if(He)return He;A.set(a,H),lb(a)?a.forEach(function(Ge){H.add(wi(Ge,f,m,Ge,a,A))}):sb(a)&&a.forEach(function(Ge,ht){H.set(ht,wi(Ge,f,m,ht,a,A))});var Ke=le?Q?Td:bd:Q?$i:Ln,dt=ue?e:Ke(a);return Zn(dt||a,function(Ge,ht){dt&&(ht=Ge,Ge=a[ht]),eu(H,ht,wi(Ge,f,m,ht,a,A))}),H}i(wi,"baseClone");function Tw(a){var f=Ln(a);return function(m){return fh(m,a,f)}}i(Tw,"baseConforms");function fh(a,f,m){var x=m.length;if(a==null)return!x;for(a=Ht(a);x--;){var T=m[x],A=f[T],H=a[T];if(H===e&&!(T in a)||!A(H))return!1}return!0}i(fh,"baseConformsTo");function kg(a,f,m){if(typeof a!="function")throw new Bi(l);return Ie(function(){a.apply(e,m)},f)}i(kg,"baseDelay");function Dc(a,f,m,x){var T=-1,A=Ba,H=!0,$=a.length,Q=[],le=f.length;if(!$)return Q;m&&(f=$t(f,mn(m))),x?(A=Ul,H=!1):f.length>=n&&(A=co,H=!1,f=new ia(f));e:for(;++T<$;){var ue=a[T],fe=m==null?ue:m(ue);if(ue=x||ue!==0?ue:0,H&&fe===fe){for(var Ne=le;Ne--;)if(f[Ne]===fe)continue e;Q.push(ue)}else A(f,fe,x)||Q.push(ue)}return Q}i(Dc,"baseDifference");var So=hu(zi),dh=hu(pd,!0);function kw(a,f){var m=!0;return So(a,function(x,T,A){return m=!!f(x,T,A),m}),m}i(kw,"baseEvery");function dd(a,f,m){for(var x=-1,T=a.length;++xT?0:T+m),x=x===e||x>T?T:ct(x),x<0&&(x+=T),x=m>x?0:cb(x);m0&&m($)?f>1?Br($,f-1,m,x,T):Ii(T,$):x||(T[T.length]=$)}return T}i(Br,"baseFlatten");var nu=Kg(),ph=Kg(!0);function zi(a,f){return a&&nu(a,f,Ln)}i(zi,"baseForOwn");function pd(a,f){return a&&ph(a,f,Ln)}i(pd,"baseForOwnRight");function hd(a,f){return jt(f,function(m){return nl(a[m])})}i(hd,"baseFunctions");function la(a,f){f=fa(f,a);for(var m=0,x=f.length;a!=null&&mf}i(md,"baseGt");function Pg(a,f){return a!=null&&kt.call(a,f)}i(Pg,"baseHas");function Ag(a,f){return a!=null&&f in Ht(a)}i(Ag,"baseHasIn");function iu(a,f,m){return a>=Pr(f,m)&&a=120&&ue.length>=120)?new ia(H&&ue):e}ue=a[0];var fe=-1,Ne=$[0];e:for(;++fe-1;)$!==a&&Wa.call($,Q,1),Wa.call(a,Q,1);return a}i(yh,"basePullAll");function Nn(a,f){for(var m=a?f.length:0,x=m-1;m--;){var T=f[m];if(m==x||T!==A){var A=T;c(T)?Wa.call(a,T,1):vd(a,T)}}return a}i(Nn,"basePullAt");function gd(a,f){return a+Va(ih()*(f-a+1))}i(gd,"baseRandom");function Pw(a,f,m,x){for(var T=-1,A=er(Pc((f-a)/(m||1)),0),H=J(A);A--;)H[x?A:++T]=a,a+=m;return H}i(Pw,"baseRange");function wh(a,f){var m="";if(!a||f<1||f>Qe)return m;do f%2&&(m+=a),f=Va(f/2),f&&(a+=a);while(f);return m}i(wh,"baseRepeat");function ut(a,f){return Oe(pe(a,f,Vi),a+"")}i(ut,"baseRest");function Ug(a){return lh(Pd(a))}i(Ug,"baseSample");function Wg(a,f){var m=Pd(a);return pr(m,Ts(f,0,m.length))}i(Wg,"baseSampleSize");function fu(a,f,m,x){if(!_r(a))return a;f=fa(f,a);for(var T=-1,A=f.length,H=A-1,$=a;$!=null&&++TT?0:T+f),m=m>T?T:m,m<0&&(m+=T),T=f>m?0:m-f>>>0,f>>>=0;for(var A=J(T);++x>>1,H=a[A];H!==null&&!_o(H)&&(m?H<=f:H=n){var le=f?null:Fw(a);if(le)return w(le);H=!1,T=co,Q=new ia}else Q=f?[]:$;e:for(;++x=x?a:Hr(a,f,m)}i(ks,"castSlice");var qg=eh||function(a){return Rt.clearTimeout(a)};function xh(a,f){if(f)return a.slice();var m=a.length,x=Jp?Jp(m):new a.constructor(m);return a.copy(x),x}i(xh,"cloneBuffer");function du(a){var f=new a.constructor(a.byteLength);return new id(f).set(new id(a)),f}i(du,"cloneArrayBuffer");function Mw(a,f){var m=f?du(a.buffer):a.buffer;return new a.constructor(m,a.byteOffset,a.byteLength)}i(Mw,"cloneDataView");function Ow(a){var f=new a.constructor(a.source,Vf.exec(a));return f.lastIndex=a.lastIndex,f}i(Ow,"cloneRegExp");function Vc(a){return Mc?Ht(Mc.call(a)):{}}i(Vc,"cloneSymbol");function Ch(a,f){var m=f?du(a.buffer):a.buffer;return new a.constructor(m,a.byteOffset,a.length)}i(Ch,"cloneTypedArray");function _h(a,f){if(a!==f){var m=a!==e,x=a===null,T=a===a,A=_o(a),H=f!==e,$=f===null,Q=f===f,le=_o(f);if(!$&&!le&&!A&&a>f||A&&H&&Q&&!$&&!le||x&&H&&Q||!m&&Q||!T)return 1;if(!x&&!A&&!le&&a=$)return Q;var le=m[x];return Q*(le=="desc"?-1:1)}}return a.index-f.index}i(Rw,"compareMultiple");function wd(a,f,m,x){for(var T=-1,A=a.length,H=m.length,$=-1,Q=f.length,le=er(A-H,0),ue=J(Q+le),fe=!x;++$1?m[T-1]:e,H=T>2?m[2]:e;for(A=a.length>3&&typeof A=="function"?(T--,A):e,H&&u(m[0],m[1],H)&&(A=T<3?e:A,T=1),f=Ht(f);++x-1?T[A?f[H]:H]:e}}i(bh,"createFind");function Xg(a){return Qo(function(f){var m=f.length,x=m,T=Lt.prototype.thru;for(a&&f.reverse();x--;){var A=f[x];if(typeof A!="function")throw new Bi(l);if(T&&!H&&Xc(A)=="wrapper")var H=new Lt([],!0)}for(x=H?x:m;++x1&&St.reverse(),ue&&Q$))return!1;var le=A.get(a),ue=A.get(f);if(le&&ue)return le==f&&ue==a;var fe=-1,Ne=!0,He=m&z?new ia:e;for(A.set(a,f),A.set(f,a);++fe<$;){var Ke=a[fe],dt=f[fe];if(x)var Ge=H?x(dt,Ke,fe,f,a,A):x(Ke,dt,fe,a,f,A);if(Ge!==e){if(Ge)continue;Ne=!1;break}if(He){if(!Qs(f,function(ht,St){if(!co(He,St)&&(Ke===ht||T(Ke,ht,m,x,A)))return He.push(St)})){Ne=!1;break}}else if(!(Ke===dt||T(Ke,dt,m,x,A))){Ne=!1;break}}return A.delete(a),A.delete(f),Ne}i(Ui,"equalArrays");function Ed(a,f,m,x,T,A,H){switch(m){case Xr:if(a.byteLength!=f.byteLength||a.byteOffset!=f.byteOffset)return!1;a=a.buffer,f=f.buffer;case Yr:return!(a.byteLength!=f.byteLength||!A(new id(a),new id(f)));case Gt:case Ft:case Fe:return Ns(+a,+f);case Ue:return a.name==f.name&&a.message==f.message;case lt:case fr:return a==f+"";case st:var $=Kl;case wr:var Q=x&I;if($||($=w),a.size!=f.size&&!Q)return!1;var le=H.get(a);if(le)return le==f;x|=z,H.set(a,f);var ue=Ui($(a),$(f),x,T,A,H);return H.delete(a),ue;case pt:if(Mc)return Mc.call(a)==Mc.call(f)}return!1}i(Ed,"equalByTag");function Nh(a,f,m,x,T,A){var H=m&I,$=bd(a),Q=$.length,le=bd(f),ue=le.length;if(Q!=ue&&!H)return!1;for(var fe=Q;fe--;){var Ne=$[fe];if(!(H?Ne in f:kt.call(f,Ne)))return!1}var He=A.get(a),Ke=A.get(f);if(He&&Ke)return He==f&&Ke==a;var dt=!0;A.set(a,f),A.set(f,a);for(var Ge=H;++fe1?"& ":"")+f[x],f=f.join(m>2?", ":" "),a.replace($f,`{ /* [wrapped with `+f+`] */ -`)}o(g,"insertWrapDetails");function y(s){return ot(s)||pf(s)||!!(pl&&s&&s[pl])}o(y,"isFlattenable");function S(s,f){var m=typeof s;return f=f??Ge,!!f&&(m=="number"||m!="symbol"&&$l.test(s))&&s>-1&&s%1==0&&s0){if(++f>=Ve)return arguments[0]}else f=0;return s.apply(e,arguments)}}o(dr,"shortOut");function Vr(s,f){var m=-1,x=s.length,_=x-1;for(f=f===e?x:f;++m1?s[f-1]:e;return m=typeof m=="function"?(s.pop(),m):e,ab(s,m)});function ub(s){var f=N(s);return f.__chain__=!0,f}o(ub,"chain");function JO(s,f){return f(s),s}o(JO,"tap");function _g(s,f){return f(s)}o(_g,"thru");var eM=ts(function(s){var f=s.length,m=f?s[0]:0,x=this.__wrapped__,_=o(function(L){return Nd(L,s)},"interceptor");return f>1||this.__actions__.length||!(x instanceof dt)||!S(m)?this.thru(_):(x=x.slice(m,+m+(f?1:0)),x.__actions__.push({func:_g,args:[_],thisArg:e}),new An(x,this.__chain__).thru(function(L){return f&&!L.length&&L.push(e),L}))});function tM(){return ub(this)}o(tM,"wrapperChain");function rM(){return new An(this.value(),this.__chain__)}o(rM,"wrapperCommit");function nM(){this.__values__===e&&(this.__values__=bb(this.value()));var s=this.__index__>=this.__values__.length,f=s?e:this.__values__[this.__index__++];return{done:s,value:f}}o(nM,"wrapperNext");function iM(){return this}o(iM,"wrapperToIterator");function oM(s){for(var f,m=this;m instanceof vc;){var x=bn(m);x.__index__=0,x.__values__=e,f?_.__wrapped__=x:f=x;var _=x;m=m.__wrapped__}return _.__wrapped__=s,f}o(oM,"wrapperPlant");function sM(){var s=this.__wrapped__;if(s instanceof dt){var f=s;return this.__actions__.length&&(f=new dt(this)),f=f.reverse(),f.__actions__.push({func:_g,args:[lw],thisArg:e}),new An(f,this.__chain__)}return this.thru(lw)}o(sM,"wrapperReverse");function lM(){return ag(this.__wrapped__,this.__actions__)}o(lM,"wrapperValue");var aM=Ic(function(s,f,m){et.call(s,m)?++s[m]:yo(s,m,1)});function uM(s,f,m){var x=ot(s)?Mu:Cc;return m&&b(s,f,m)&&(f=e),x(s,Be(f,3))}o(uM,"every");function fM(s,f){var m=ot(s)?Vt:Pd;return m(s,Be(f,3))}o(fM,"filter");var cM=$d(zc),pM=$d(ib);function dM(s,f){return rn(Tg(s,f),1)}o(dM,"flatMap");function hM(s,f){return rn(Tg(s,f),Ke)}o(hM,"flatMapDeep");function mM(s,f,m){return m=m===e?1:lt(m),rn(Tg(s,f),m)}o(mM,"flatMapDepth");function fb(s,f){var m=ot(s)?jn:di;return m(s,Be(f,3))}o(fb,"forEach");function cb(s,f){var m=ot(s)?dd:Sc;return m(s,Be(f,3))}o(cb,"forEachRight");var gM=Ic(function(s,f,m){et.call(s,m)?s[m].push(f):yo(s,m,[f])});function vM(s,f,m,x){s=Mi(s)?s:jc(s),m=m&&!x?lt(m):0;var _=s.length;return m<0&&(m=sr(_+m,0)),Og(s)?m<=_&&s.indexOf(f,m)>-1:!!_&&Go(s,f,m)>-1}o(vM,"includes");var yM=st(function(s,f,m){var x=-1,_=typeof f=="function",L=Mi(s)?Q(s.length):[];return di(s,function(F){L[++x]=_?Ur(f,F,m):wa(F,f,m)}),L}),wM=Ic(function(s,f,m){yo(s,m,f)});function Tg(s,f){var m=ot(s)?yt:Ea;return m(s,Be(f,3))}o(Tg,"map");function xM(s,f,m,x){return s==null?[]:(ot(f)||(f=f==null?[]:[f]),m=x?e:m,ot(m)||(m=m==null?[]:[m]),yn(s,f,m))}o(xM,"orderBy");var SM=Ic(function(s,f,m){s[m?0:1].push(f)},function(){return[[],[]]});function CM(s,f,m){var x=ot(s)?Au:Fu,_=arguments.length<3;return x(s,Be(f,4),m,_,di)}o(CM,"reduce");function bM(s,f,m){var x=ot(s)?hd:Fu,_=arguments.length<3;return x(s,Be(f,4),m,_,Sc)}o(bM,"reduceRight");function EM(s,f){var m=ot(s)?Vt:Pd;return m(s,Lg(Be(f,3)))}o(EM,"reject");function _M(s){var f=ot(s)?wc:tw;return f(s)}o(_M,"sample");function TM(s,f,m){(m?b(s,f,m):f===e)?f=1:f=lt(f);var x=ot(s)?As:Fs;return x(s,f)}o(TM,"sampleSize");function kM(s){var f=ot(s)?Km:Jo;return f(s)}o(kM,"shuffle");function NM(s){if(s==null)return 0;if(Mi(s))return Og(s)?wr(s):s.length;var f=xn(s);return f==Yt||f==Zt?s.size:Nc(s).length}o(NM,"size");function LM(s,f,m){var x=ot(s)?Vo:rw;return m&&b(s,f,m)&&(f=e),x(s,Be(f,3))}o(LM,"some");var PM=st(function(s,f){if(s==null)return[];var m=f.length;return m>1&&b(s,f[0],f[1])?f=[]:m>2&&b(f[0],f[1],f[2])&&(f=[f[0]]),yn(s,rn(f,1),[])}),kg=By||function(){return qt.Date.now()};function OM(s,f){if(typeof f!="function")throw new Kn(d);return s=lt(s),function(){if(--s<1)return f.apply(this,arguments)}}o(OM,"after");function pb(s,f,m){return f=m?e:f,f=s&&f==null?s.length:f,Oi(s,se,e,e,e,e,f)}o(pb,"ary");function db(s,f){var m;if(typeof f!="function")throw new Kn(d);return s=lt(s),function(){return--s>0&&(m=f.apply(this,arguments)),s<=1&&(f=e),m}}o(db,"before");var uw=st(function(s,f,m){var x=J;if(m.length){var _=Vi(m,Oa(uw));x|=G}return Oi(s,x,f,m,_)}),hb=st(function(s,f,m){var x=J|Z;if(m.length){var _=Vi(m,Oa(hb));x|=G}return Oi(f,x,s,m,_)});function mb(s,f,m){f=m?e:f;var x=Oi(s,A,e,e,e,e,e,f);return x.placeholder=mb.placeholder,x}o(mb,"curry");function gb(s,f,m){f=m?e:f;var x=Oi(s,I,e,e,e,e,e,f);return x.placeholder=gb.placeholder,x}o(gb,"curryRight");function vb(s,f,m){var x,_,L,F,z,Y,le=0,ae=!1,ce=!1,Le=!0;if(typeof s!="function")throw new Kn(d);f=So(f)||0,Sr(m)&&(ae=!!m.leading,ce="maxWait"in m,L=ce?sr(So(m.maxWait)||0,f):L,Le="trailing"in m?!!m.trailing:Le);function Fe(Ir){var is=x,kl=_;return x=_=e,le=Ir,F=s.apply(kl,is),F}o(Fe,"invokeFunc");function $e(Ir){return le=Ir,z=At(ht,f),ae?Fe(Ir):F}o($e,"leadingEdge");function ft(Ir){var is=Ir-Y,kl=Ir-le,Ib=f-is;return ce?Zr(Ib,L-kl):Ib}o(ft,"remainingWait");function je(Ir){var is=Ir-Y,kl=Ir-le;return Y===e||is>=f||is<0||ce&&kl>=L}o(je,"shouldInvoke");function ht(){var Ir=kg();if(je(Ir))return gt(Ir);z=At(ht,ft(Ir))}o(ht,"timerExpired");function gt(Ir){return z=e,Le&&x?Fe(Ir):(x=_=e,F)}o(gt,"trailingEdge");function Xi(){z!==e&&Ta(z),le=0,x=Y=_=z=e}o(Xi,"cancel");function gi(){return z===e?F:gt(kg())}o(gi,"flush");function Qi(){var Ir=kg(),is=je(Ir);if(x=arguments,_=this,Y=Ir,is){if(z===e)return $e(Y);if(ce)return Ta(z),z=At(ht,f),Fe(Y)}return z===e&&(z=At(ht,f)),F}return o(Qi,"debounced"),Qi.cancel=Xi,Qi.flush=gi,Qi}o(vb,"debounce");var MM=st(function(s,f){return Xm(s,1,f)}),AM=st(function(s,f,m){return Xm(s,So(f)||0,m)});function DM(s){return Oi(s,pe)}o(DM,"flip");function Ng(s,f){if(typeof s!="function"||f!=null&&typeof f!="function")throw new Kn(d);var m=o(function(){var x=arguments,_=f?f.apply(this,x):x[0],L=m.cache;if(L.has(_))return L.get(_);var F=s.apply(this,x);return m.cache=L.set(_,F)||L,F},"memoized");return m.cache=new(Ng.Cache||xr),m}o(Ng,"memoize"),Ng.Cache=xr;function Lg(s){if(typeof s!="function")throw new Kn(d);return function(){var f=arguments;switch(f.length){case 0:return!s.call(this);case 1:return!s.call(this,f[0]);case 2:return!s.call(this,f[0],f[1]);case 3:return!s.call(this,f[0],f[1],f[2])}return!s.apply(this,f)}}o(Lg,"negate");function RM(s){return db(2,s)}o(RM,"once");var IM=nw(function(s,f){f=f.length==1&&ot(f[0])?yt(f[0],zr(Be())):yt(rn(f,1),zr(Be()));var m=f.length;return st(function(x){for(var _=-1,L=Zr(x.length,m);++_=f}),pf=xa(function(){return arguments}())?xa:function(s){return Er(s)&&et.call(s,"callee")&&!hc.call(s,"callee")},ot=Q.isArray,QM=ta?zr(ta):Zy;function Mi(s){return s!=null&&Pg(s.length)&&!_l(s)}o(Mi,"isArrayLike");function Rr(s){return Er(s)&&Mi(s)}o(Rr,"isArrayLikeObject");function ZM(s){return s===!0||s===!1||Er(s)&&jr(s)==zt}o(ZM,"isBoolean");var Ra=qu||Sw,JM=Ou?zr(Ou):Sa;function eA(s){return Er(s)&&s.nodeType===1&&!Gd(s)}o(eA,"isElement");function tA(s){if(s==null)return!0;if(Mi(s)&&(ot(s)||typeof s=="string"||typeof s.splice=="function"||Ra(s)||$c(s)||pf(s)))return!s.length;var f=xn(s);if(f==Yt||f==Zt)return!s.size;if(te(s))return!Nc(s).length;for(var m in s)if(et.call(s,m))return!1;return!0}o(tA,"isEmpty");function rA(s,f){return Ca(s,f)}o(rA,"isEqual");function nA(s,f,m){m=typeof m=="function"?m:e;var x=m?m(s,f):e;return x===e?Ca(s,f,e,m):!!x}o(nA,"isEqualWith");function cw(s){if(!Er(s))return!1;var f=jr(s);return f==rt||f==ie||typeof s.message=="string"&&typeof s.name=="string"&&!Gd(s)}o(cw,"isError");function iA(s){return typeof s=="number"&&Wm(s)}o(iA,"isFinite");function _l(s){if(!Sr(s))return!1;var f=jr(s);return f==Pr||f==Gt||f==Lr||f==si}o(_l,"isFunction");function wb(s){return typeof s=="number"&&s==lt(s)}o(wb,"isInteger");function Pg(s){return typeof s=="number"&&s>-1&&s%1==0&&s<=Ge}o(Pg,"isLength");function Sr(s){var f=typeof s;return s!=null&&(f=="object"||f=="function")}o(Sr,"isObject");function Er(s){return s!=null&&typeof s=="object"}o(Er,"isObjectLike");var xb=nt?zr(nt):eg;function oA(s,f){return s===f||wl(s,f,Ma(f))}o(oA,"isMatch");function sA(s,f,m){return m=typeof m=="function"?m:e,wl(s,f,Ma(f),m)}o(sA,"isMatchWith");function lA(s){return Sb(s)&&s!=+s}o(lA,"isNaN");function aA(s){if($(s))throw new Qe(l);return ba(s)}o(aA,"isNative");function uA(s){return s===null}o(uA,"isNull");function fA(s){return s==null}o(fA,"isNil");function Sb(s){return typeof s=="number"||Er(s)&&jr(s)==Se}o(Sb,"isNumber");function Gd(s){if(!Er(s)||jr(s)!=fn)return!1;var f=ca(s);if(f===null)return!0;var m=et.call(f,"constructor")&&f.constructor;return typeof m=="function"&&m instanceof m&&ho.call(m)==Iy}o(Gd,"isPlainObject");var pw=uc?zr(uc):ef;function cA(s){return wb(s)&&s>=-Ge&&s<=Ge}o(cA,"isSafeInteger");var Cb=fi?zr(fi):tf;function Og(s){return typeof s=="string"||!ot(s)&&Er(s)&&jr(s)==gr}o(Og,"isString");function Yi(s){return typeof s=="symbol"||Er(s)&&jr(s)==pt}o(Yi,"isSymbol");var $c=ll?zr(ll):tg;function pA(s){return s===e}o(pA,"isUndefined");function dA(s){return Er(s)&&xn(s)==Cr}o(dA,"isWeakMap");function hA(s){return Er(s)&&jr(s)==Ui}o(hA,"isWeakSet");var mA=Mt(Is),gA=Mt(function(s,f){return s<=f});function bb(s){if(!s)return[];if(Mi(s))return Og(s)?Kt(s):qr(s);if(_s&&s[_s])return pc(s[_s]());var f=xn(s),m=f==Yt?aa:f==Zt?w:jc;return m(s)}o(bb,"toArray");function Tl(s){if(!s)return s===0?s:0;if(s=So(s),s===Ke||s===-Ke){var f=s<0?-1:1;return f*Xe}return s===s?s:0}o(Tl,"toFinite");function lt(s){var f=Tl(s),m=f%1;return f===f?m?f-m:f:0}o(lt,"toInteger");function Eb(s){return s?vl(lt(s),0,ct):0}o(Eb,"toLength");function So(s){if(typeof s=="number")return s;if(Yi(s))return nr;if(Sr(s)){var f=typeof s.valueOf=="function"?s.valueOf():s;s=Sr(f)?f+"":f}if(typeof s!="string")return s===0?s:+s;s=Ss(s);var m=Wo.test(s);return m||Uo.test(s)?ye(s.slice(2),m?2:8):xu.test(s)?nr:+s}o(So,"toNumber");function _b(s){return Gn(s,Ai(s))}o(_b,"toPlainObject");function vA(s){return s?vl(lt(s),-Ge,Ge):s===0?s:0}o(vA,"toSafeInteger");function Dt(s){return s==null?"":wn(s)}o(Dt,"toString");var yA=ka(function(s,f){if(te(f)||Mi(f)){Gn(f,_n(f),s);return}for(var m in f)et.call(f,m)&&Qu(s,m,f[m])}),Tb=ka(function(s,f){Gn(f,Ai(f),s)}),Mg=ka(function(s,f,m,x){Gn(f,Ai(f),s,x)}),wA=ka(function(s,f,m,x){Gn(f,_n(f),s,x)}),xA=ts(Nd);function SA(s,f){var m=go(s);return f==null?m:kd(m,f)}o(SA,"create");var CA=st(function(s,f){s=mt(s);var m=-1,x=f.length,_=x>2?f[2]:e;for(_&&b(f[0],f[1],_)&&(x=1);++m1),L}),Gn(s,Hc(s),m),x&&(m=Dn(m,k|O|j,Sg));for(var _=f.length;_--;)of(m,f[_]);return m});function WA(s,f){return Nb(s,Lg(Be(f)))}o(WA,"omitBy");var UA=ts(function(s,f){return s==null?{}:og(s,f)});function Nb(s,f){if(s==null)return{};var m=yt(Hc(s),function(x){return[x]});return f=Be(f),sg(s,m,function(x,_){return f(x,_[0])})}o(Nb,"pickBy");function zA(s,f,m){f=Gi(f,s);var x=-1,_=f.length;for(_||(_=1,s=e);++x<_;){var L=s==null?e:s[Lt(f[x])];L===e&&(x=_,L=m),s=_l(L)?L.call(s):L}return s}o(zA,"result");function $A(s,f,m){return s==null?s:Zo(s,f,m)}o($A,"set");function jA(s,f,m,x){return x=typeof x=="function"?x:e,s==null?s:Zo(s,f,m,x)}o(jA,"setWith");var Lb=Pi(_n),Pb=Pi(Ai);function qA(s,f,m){var x=ot(s),_=x||Ra(s)||$c(s);if(f=Be(f,4),m==null){var L=s&&s.constructor;_?m=x?new L:[]:Sr(s)?m=_l(L)?go(ca(s)):{}:m={}}return(_?jn:hi)(s,function(F,z,Y){return f(m,F,z,Y)}),m}o(qA,"transform");function VA(s,f){return s==null?!0:of(s,f)}o(VA,"unset");function KA(s,f,m){return s==null?s:Mc(s,f,Ac(m))}o(KA,"update");function GA(s,f,m,x){return x=typeof x=="function"?x:e,s==null?s:Mc(s,f,Ac(m),x)}o(GA,"updateWith");function jc(s){return s==null?[]:sa(s,_n(s))}o(jc,"values");function YA(s){return s==null?[]:sa(s,Ai(s))}o(YA,"valuesIn");function XA(s,f,m){return m===e&&(m=f,f=e),m!==e&&(m=So(m),m=m===m?m:0),f!==e&&(f=So(f),f=f===f?f:0),vl(So(s),f,m)}o(XA,"clamp");function QA(s,f,m){return f=Tl(f),m===e?(m=f,f=0):m=Tl(m),s=So(s),ya(s,f,m)}o(QA,"inRange");function ZA(s,f,m){if(m&&typeof m!="boolean"&&b(s,f,m)&&(f=m=e),m===e&&(typeof f=="boolean"?(m=f,f=e):typeof s=="boolean"&&(m=s,s=e)),s===e&&f===e?(s=0,f=1):(s=Tl(s),f===e?(f=s,s=0):f=Tl(f)),s>f){var x=s;s=f,f=x}if(m||s%1||f%1){var _=Ns();return Zr(s+_*(f-s+Pu("1e-"+((_+"").length-1))),f)}return Pc(s,f)}o(ZA,"random");var JA=Na(function(s,f,m){return f=f.toLowerCase(),s+(m?Ob(f):f)});function Ob(s){return mw(Dt(s).toLowerCase())}o(Ob,"capitalize");function Mb(s){return s=Dt(s),s&&s.replace(ec,Wu).replace(sc,"")}o(Mb,"deburr");function e2(s,f,m){s=Dt(s),f=wn(f);var x=s.length;m=m===e?x:vl(lt(m),0,x);var _=m;return m-=f.length,m>=0&&s.slice(m,_)==f}o(e2,"endsWith");function t2(s){return s=Dt(s),s&&ds.test(s)?s.replace(or,gd):s}o(t2,"escape");function r2(s){return s=Dt(s),s&&Qf.test(s)?s.replace(tl,"\\$&"):s}o(r2,"escapeRegExp");var n2=Na(function(s,f,m){return s+(m?"-":"")+f.toLowerCase()}),i2=Na(function(s,f,m){return s+(m?" ":"")+f.toLowerCase()}),o2=vg("toLowerCase");function s2(s,f,m){s=Dt(s),f=lt(f);var x=f?wr(s):0;if(!f||x>=f)return s;var _=(f-x)/2;return Fc(ha(_),m)+s+Fc(da(_),m)}o(s2,"pad");function l2(s,f,m){s=Dt(s),f=lt(f);var x=f?wr(s):0;return f&&x>>0,m?(s=Dt(s),s&&(typeof f=="string"||f!=null&&!pw(f))&&(f=wn(f),!f&&qn(s))?Bs(Kt(s),0,m):s.split(f,m)):[]}o(d2,"split");var h2=Na(function(s,f,m){return s+(m?" ":"")+mw(f)});function m2(s,f,m){return s=Dt(s),m=m==null?0:vl(lt(m),0,s.length),f=wn(f),s.slice(m,m+f.length)==f}o(m2,"startsWith");function g2(s,f,m){var x=N.templateSettings;m&&b(s,f,m)&&(f=e),s=Dt(s),f=Mg({},f,x,Bc);var _=Mg({},f.imports,x.imports,Bc),L=_n(_),F=sa(_,L),z,Y,le=0,ae=f.interpolate||jt,ce="__p += '",Le=Cs((f.escape||jt).source+"|"+ae.source+"|"+(ae===el?Jf:jt).source+"|"+(f.evaluate||jt).source+"|$","g"),Fe="//# sourceURL="+(et.call(f,"sourceURL")?(f.sourceURL+"").replace(/\s/g," "):"lodash.templateSources["+ ++Lu+"]")+` -`;s.replace(Le,function(je,ht,gt,Xi,gi,Qi){return gt||(gt=Xi),ce+=s.slice(le,Qi).replace(Me,Uu),ht&&(z=!0,ce+=`' + +`)}i(r,"insertWrapDetails");function o(a){return at(a)||Jc(a)||!!(gg&&a&&a[gg])}i(o,"isFlattenable");function c(a,f){var m=typeof a;return f=f??Qe,!!f&&(m=="number"||m!="symbol"&&La.test(a))&&a>-1&&a%1==0&&a0){if(++f>=Ee)return arguments[0]}else f=0;return a.apply(e,arguments)}}i(Dt,"shortOut");function pr(a,f){var m=-1,x=a.length,T=x-1;for(f=f===e?x:f;++m1?a[f-1]:e;return m=typeof m=="function"?(a.pop(),m):e,GE(a,m)});function YE(a){var f=P(a);return f.__chain__=!0,f}i(YE,"chain");function xR(a,f){return f(a),a}i(xR,"tap");function nv(a,f){return f(a)}i(nv,"thru");var CR=Qo(function(a){var f=a.length,m=f?a[0]:0,x=this.__wrapped__,T=i(function(A){return ru(A,a)},"interceptor");return f>1||this.__actions__.length||!(x instanceof $e)||!c(m)?this.thru(T):(x=x.slice(m,+m+(f?1:0)),x.__actions__.push({func:nv,args:[T],thisArg:e}),new Lt(x,this.__chain__).thru(function(A){return f&&!A.length&&A.push(e),A}))});function _R(){return YE(this)}i(_R,"wrapperChain");function ER(){return new Lt(this.value(),this.__chain__)}i(ER,"wrapperCommit");function bR(){this.__values__===e&&(this.__values__=ub(this.value()));var a=this.__index__>=this.__values__.length,f=a?e:this.__values__[this.__index__++];return{done:a,value:f}}i(bR,"wrapperNext");function TR(){return this}i(TR,"wrapperToIterator");function kR(a){for(var f,m=this;m instanceof yn;){var x=Pn(m);x.__index__=0,x.__values__=e,f?T.__wrapped__=x:f=x;var T=x;m=m.__wrapped__}return T.__wrapped__=a,f}i(kR,"wrapperPlant");function NR(){var a=this.__wrapped__;if(a instanceof $e){var f=a;return this.__actions__.length&&(f=new $e(this)),f=f.reverse(),f.__actions__.push({func:nv,args:[Uw],thisArg:e}),new Lt(f,this.__chain__)}return this.thru(Uw)}i(NR,"wrapperReverse");function PR(){return zc(this.__wrapped__,this.__actions__)}i(PR,"wrapperValue");var AR=pu(function(a,f,m){kt.call(a,m)?++a[m]:bs(a,m,1)});function LR(a,f,m){var x=at(a)?wc:kw;return m&&u(a,f,m)&&(f=e),x(a,We(f,3))}i(LR,"every");function MR(a,f){var m=at(a)?jt:Xa;return m(a,We(f,3))}i(MR,"filter");var OR=bh(Zo),RR=bh(ma);function DR(a,f){return Br(iv(a,f),1)}i(DR,"flatMap");function IR(a,f){return Br(iv(a,f),je)}i(IR,"flatMapDeep");function FR(a,f,m){return m=m===e?1:ct(m),Br(iv(a,f),m)}i(FR,"flatMapDepth");function XE(a,f){var m=at(a)?Zn:So;return m(a,We(f,3))}i(XE,"forEach");function QE(a,f){var m=at(a)?Xf:dh;return m(a,We(f,3))}i(QE,"forEachRight");var BR=pu(function(a,f,m){kt.call(a,m)?a[m].push(f):bs(a,m,[f])});function HR(a,f,m,x){a=Wi(a)?a:Pd(a),m=m&&!x?ct(m):0;var T=a.length;return m<0&&(m=er(T+m,0)),uv(a)?m<=T&&a.indexOf(f,m)>-1:!!T&&Kt(a,f,m)>-1}i(HR,"includes");var zR=ut(function(a,f,m){var x=-1,T=typeof f=="function",A=Wi(a)?J(a.length):[];return So(a,function(H){A[++x]=T?Jr(f,H,m):su(H,f,m)}),A}),UR=pu(function(a,f,m){bs(a,m,f)});function iv(a,f){var m=at(a)?$t:gh;return m(a,We(f,3))}i(iv,"map");function WR(a,f,m,x){return a==null?[]:(at(f)||(f=f==null?[]:[f]),m=x?e:m,at(m)||(m=m==null?[]:[m]),Bg(a,f,m))}i(WR,"orderBy");var $R=pu(function(a,f,m){a[m?0:1].push(f)},function(){return[[],[]]});function VR(a,f,m){var x=at(a)?Sc:ed,T=arguments.length<3;return x(a,We(f,4),m,T,So)}i(VR,"reduce");function qR(a,f,m){var x=at(a)?Qf:ed,T=arguments.length<3;return x(a,We(f,4),m,T,dh)}i(qR,"reduceRight");function jR(a,f){var m=at(a)?jt:Xa;return m(a,av(We(f,3)))}i(jR,"reject");function KR(a){var f=at(a)?lh:Ug;return f(a)}i(KR,"sample");function GR(a,f,m){(m?u(a,f,m):f===e)?f=1:f=ct(f);var x=at(a)?bw:Wg;return x(a,f)}i(GR,"sampleSize");function YR(a){var f=at(a)?uh:Lw;return f(a)}i(YR,"shuffle");function XR(a){if(a==null)return 0;if(Wi(a))return uv(a)?gn(a):a.length;var f=zr(a);return f==st||f==wr?a.size:Ja(a).length}i(XR,"size");function QR(a,f,m){var x=at(a)?Qs:Bc;return m&&u(a,f,m)&&(f=e),x(a,We(f,3))}i(QR,"some");var JR=ut(function(a,f){if(a==null)return[];var m=f.length;return m>1&&u(a,f[0],f[1])?f=[]:m>2&&u(f[0],f[1],f[2])&&(f=[f[0]]),Bg(a,Br(f,1),[])}),ov=Jl||function(){return Rt.Date.now()};function ZR(a,f){if(typeof f!="function")throw new Bi(l);return a=ct(a),function(){if(--a<1)return f.apply(this,arguments)}}i(ZR,"after");function JE(a,f,m){return f=m?e:f,f=a&&f==null?a.length:f,wt(a,te,e,e,e,e,f)}i(JE,"ary");function ZE(a,f){var m;if(typeof f!="function")throw new Bi(l);return a=ct(a),function(){return--a>0&&(m=f.apply(this,arguments)),a<=1&&(f=e),m}}i(ZE,"before");var $w=ut(function(a,f,m){var x=j;if(m.length){var T=kn(m,vu($w));x|=D}return wt(a,x,f,m,T)}),eb=ut(function(a,f,m){var x=j|Y;if(m.length){var T=kn(m,vu(eb));x|=D}return wt(f,x,a,m,T)});function tb(a,f,m){f=m?e:f;var x=wt(a,F,e,e,e,e,e,f);return x.placeholder=tb.placeholder,x}i(tb,"curry");function rb(a,f,m){f=m?e:f;var x=wt(a,L,e,e,e,e,e,f);return x.placeholder=rb.placeholder,x}i(rb,"curryRight");function nb(a,f,m){var x,T,A,H,$,Q,le=0,ue=!1,fe=!1,Ne=!0;if(typeof a!="function")throw new Bi(l);f=ts(f)||0,_r(m)&&(ue=!!m.leading,fe="maxWait"in m,A=fe?er(ts(m.maxWait)||0,f):A,Ne="trailing"in m?!!m.trailing:Ne);function He($r){var Ps=x,ol=T;return x=T=e,le=$r,H=a.apply(ol,Ps),H}i(He,"invokeFunc");function Ke($r){return le=$r,$=Ie(ht,f),ue?He($r):H}i(Ke,"leadingEdge");function dt($r){var Ps=$r-Q,ol=$r-le,Cb=f-Ps;return fe?Pr(Cb,A-ol):Cb}i(dt,"remainingWait");function Ge($r){var Ps=$r-Q,ol=$r-le;return Q===e||Ps>=f||Ps<0||fe&&ol>=A}i(Ge,"shouldInvoke");function ht(){var $r=ov();if(Ge($r))return St($r);$=Ie(ht,dt($r))}i(ht,"timerExpired");function St($r){return $=e,Ne&&x?He($r):(x=T=e,H)}i(St,"trailingEdge");function Eo(){$!==e&&qg($),le=0,x=Q=T=$=e}i(Eo,"cancel");function Ci(){return $===e?H:St(ov())}i(Ci,"flush");function bo(){var $r=ov(),Ps=Ge($r);if(x=arguments,T=this,Q=$r,Ps){if($===e)return Ke(Q);if(fe)return qg($),$=Ie(ht,f),He(Q)}return $===e&&($=Ie(ht,f)),H}return i(bo,"debounced"),bo.cancel=Eo,bo.flush=Ci,bo}i(nb,"debounce");var eD=ut(function(a,f){return kg(a,1,f)}),tD=ut(function(a,f,m){return kg(a,ts(f)||0,m)});function rD(a){return wt(a,oe)}i(rD,"flip");function sv(a,f){if(typeof a!="function"||f!=null&&typeof f!="function")throw new Bi(l);var m=i(function(){var x=arguments,T=f?f.apply(this,x):x[0],A=m.cache;if(A.has(T))return A.get(T);var H=a.apply(this,x);return m.cache=A.set(T,H)||A,H},"memoized");return m.cache=new(sv.Cache||yo),m}i(sv,"memoize"),sv.Cache=yo;function av(a){if(typeof a!="function")throw new Bi(l);return function(){var f=arguments;switch(f.length){case 0:return!a.call(this);case 1:return!a.call(this,f[0]);case 2:return!a.call(this,f[0],f[1]);case 3:return!a.call(this,f[0],f[1],f[2])}return!a.apply(this,f)}}i(av,"negate");function nD(a){return ZE(2,a)}i(nD,"once");var iD=$c(function(a,f){f=f.length==1&&at(f[0])?$t(f[0],mn(We())):$t(Br(f,1),mn(We()));var m=f.length;return ut(function(x){for(var T=-1,A=Pr(x.length,m);++T=f}),Jc=hh(function(){return arguments}())?hh:function(a){return Lr(a)&&kt.call(a,"callee")&&!od.call(a,"callee")},at=J.isArray,wD=Xs?mn(Xs):au;function Wi(a){return a!=null&&lv(a.length)&&!nl(a)}i(Wi,"isArrayLike");function Wr(a){return Lr(a)&&Wi(a)}i(Wr,"isArrayLikeObject");function SD(a){return a===!0||a===!1||Lr(a)&&en(a)==Gt}i(SD,"isBoolean");var Su=hw||t1,xD=vi?mn(vi):Mg;function CD(a){return Lr(a)&&a.nodeType===1&&!Lh(a)}i(CD,"isElement");function _D(a){if(a==null)return!0;if(Wi(a)&&(at(a)||typeof a=="string"||typeof a.splice=="function"||Su(a)||Nd(a)||Jc(a)))return!a.length;var f=zr(a);if(f==st||f==wr)return!a.size;if(E(a))return!Ja(a).length;for(var m in a)if(kt.call(a,m))return!1;return!0}i(_D,"isEmpty");function ED(a,f){return ua(a,f)}i(ED,"isEqual");function bD(a,f,m){m=typeof m=="function"?m:e;var x=m?m(a,f):e;return x===e?ua(a,f,e,m):!!x}i(bD,"isEqualWith");function qw(a){if(!Lr(a))return!1;var f=en(a);return f==Ue||f==se||typeof a.message=="string"&&typeof a.name=="string"&&!Lh(a)}i(qw,"isError");function TD(a){return typeof a=="number"&&vg(a)}i(TD,"isFinite");function nl(a){if(!_r(a))return!1;var f=en(a);return f==Gr||f==Zt||f==yr||f==Ho}i(nl,"isFunction");function ob(a){return typeof a=="number"&&a==ct(a)}i(ob,"isInteger");function lv(a){return typeof a=="number"&&a>-1&&a%1==0&&a<=Qe}i(lv,"isLength");function _r(a){var f=typeof a;return a!=null&&(f=="object"||f=="function")}i(_r,"isObject");function Lr(a){return a!=null&&typeof a=="object"}i(Lr,"isObjectLike");var sb=Ze?mn(Ze):Ic;function kD(a,f){return a===f||mh(a,f,bt(f))}i(kD,"isMatch");function ND(a,f,m){return m=typeof m=="function"?m:e,mh(a,f,bt(f),m)}i(ND,"isMatchWith");function PD(a){return ab(a)&&a!=+a}i(PD,"isNaN");function AD(a){if(_(a))throw new Je(s);return Qa(a)}i(AD,"isNative");function LD(a){return a===null}i(LD,"isNull");function MD(a){return a==null}i(MD,"isNil");function ab(a){return typeof a=="number"||Lr(a)&&en(a)==Fe}i(ab,"isNumber");function Lh(a){if(!Lr(a)||en(a)!=bn)return!1;var f=ra(a);if(f===null)return!0;var m=kt.call(f,"constructor")&&f.constructor;return typeof m=="function"&&m instanceof m&&Xl.call(m)==Ua}i(Lh,"isPlainObject");var jw=Bl?mn(Bl):Rg;function OD(a){return ob(a)&&a>=-Qe&&a<=Qe}i(OD,"isSafeInteger");var lb=hn?mn(hn):Dg;function uv(a){return typeof a=="string"||!at(a)&&Lr(a)&&en(a)==fr}i(uv,"isString");function _o(a){return typeof a=="symbol"||Lr(a)&&en(a)==pt}i(_o,"isSymbol");var Nd=Hl?mn(Hl):Ig;function RD(a){return a===e}i(RD,"isUndefined");function DD(a){return Lr(a)&&zr(a)==Bn}i(DD,"isWeakMap");function ID(a){return Lr(a)&&en(a)==Mi}i(ID,"isWeakSet");var FD=jc(Za),BD=jc(function(a,f){return a<=f});function ub(a){if(!a)return[];if(Wi(a))return uv(a)?Xt(a):ei(a);if($a&&a[$a])return Qp(a[$a]());var f=zr(a),m=f==st?Kl:f==wr?w:Pd;return m(a)}i(ub,"toArray");function il(a){if(!a)return a===0?a:0;if(a=ts(a),a===je||a===-je){var f=a<0?-1:1;return f*ot}return a===a?a:0}i(il,"toFinite");function ct(a){var f=il(a),m=f%1;return f===f?m?f-m:f:0}i(ct,"toInteger");function cb(a){return a?Ts(ct(a),0,Pt):0}i(cb,"toLength");function ts(a){if(typeof a=="number")return a;if(_o(a))return It;if(_r(a)){var f=typeof a.valueOf=="function"?a.valueOf():a;a=_r(f)?f+"":f}if(typeof a!="string")return a===0?a:+a;a=Vl(a);var m=vs.test(a);return m||Wp.test(a)?ve(a.slice(2),m?2:8):ac.test(a)?It:+a}i(ts,"toNumber");function fb(a){return Co(a,$i(a))}i(fb,"toPlainObject");function HD(a){return a?Ts(ct(a),-Qe,Qe):a===0?a:0}i(HD,"toSafeInteger");function zt(a){return a==null?"":Si(a)}i(zt,"toString");var zD=Xo(function(a,f){if(E(f)||Wi(f)){Co(f,Ln(f),a);return}for(var m in f)kt.call(f,m)&&eu(a,m,f[m])}),db=Xo(function(a,f){Co(f,$i(f),a)}),cv=Xo(function(a,f,m,x){Co(f,$i(f),a,x)}),UD=Xo(function(a,f,m,x){Co(f,Ln(f),a,x)}),WD=Qo(ru);function $D(a,f){var m=Zl(a);return f==null?m:ch(m,f)}i($D,"create");var VD=ut(function(a,f){a=Ht(a);var m=-1,x=f.length,T=x>2?f[2]:e;for(T&&u(f[0],f[1],T)&&(x=1);++m1),A}),Co(a,Td(a),m),x&&(m=wi(m,b|k|R,Gc));for(var T=f.length;T--;)vd(m,f[T]);return m});function l2(a,f){return hb(a,av(We(f)))}i(l2,"omitBy");var u2=Qo(function(a,f){return a==null?{}:Hg(a,f)});function hb(a,f){if(a==null)return{};var m=$t(Td(a),function(x){return[x]});return f=We(f),zg(a,m,function(x,T){return f(x,T[0])})}i(hb,"pickBy");function c2(a,f,m){f=fa(f,a);var x=-1,T=f.length;for(T||(T=1,a=e);++xf){var x=a;a=f,f=x}if(m||a%1||f%1){var T=ih();return Pr(a+T*(f-a+Fl("1e-"+((T+"").length-1))),f)}return gd(a,f)}i(S2,"random");var x2=mu(function(a,f,m){return f=f.toLowerCase(),a+(m?vb(f):f)});function vb(a){return Yw(zt(a).toLowerCase())}i(vb,"capitalize");function yb(a){return a=zt(a),a&&a.replace(lc,Tc).replace(Vp,"")}i(yb,"deburr");function C2(a,f,m){a=zt(a),f=Si(f);var x=a.length;m=m===e?x:Ts(ct(m),0,x);var T=m;return m-=f.length,m>=0&&a.slice(m,T)==f}i(C2,"endsWith");function _2(a){return a=zt(a),a&&Na.test(a)?a.replace(Mt,Fi):a}i(_2,"escape");function E2(a){return a=zt(a),a&&nc.test(a)?a.replace(Aa,"\\$&"):a}i(E2,"escapeRegExp");var b2=mu(function(a,f,m){return a+(m?"-":"")+f.toLowerCase()}),T2=mu(function(a,f,m){return a+(m?" ":"")+f.toLowerCase()}),k2=Yg("toLowerCase");function N2(a,f,m){a=zt(a),f=ct(f);var x=f?gn(a):0;if(!f||x>=f)return a;var T=(f-x)/2;return Cd(Va(T),m)+a+Cd(Pc(T),m)}i(N2,"pad");function P2(a,f,m){a=zt(a),f=ct(f);var x=f?gn(a):0;return f&&x>>0,m?(a=zt(a),a&&(typeof f=="string"||f!=null&&!jw(f))&&(f=Si(f),!f&&xs(a))?ks(Xt(a),0,m):a.split(f,m)):[]}i(D2,"split");var I2=mu(function(a,f,m){return a+(m?" ":"")+Yw(f)});function F2(a,f,m){return a=zt(a),m=m==null?0:Ts(ct(m),0,a.length),f=Si(f),a.slice(m,m+f.length)==f}i(F2,"startsWith");function B2(a,f,m){var x=P.templateSettings;m&&u(a,f,m)&&(f=e),a=zt(a),f=cv({},f,x,Zg);var T=cv({},f.imports,x.imports,Zg),A=Ln(T),H=bc(T,A),$,Q,le=0,ue=f.interpolate||ao,fe="__p += '",Ne=Nc((f.escape||ao).source+"|"+ue.source+"|"+(ue===kr?Nl:ao).source+"|"+(f.evaluate||ao).source+"|$","g"),He="//# sourceURL="+(kt.call(f,"sourceURL")?(f.sourceURL+"").replace(/\s/g," "):"lodash.templateSources["+ ++Kp+"]")+` +`;a.replace(Ne,function(Ge,ht,St,Eo,Ci,bo){return St||(St=Eo),fe+=a.slice(le,bo).replace(qf,$o),ht&&($=!0,fe+=`' + __e(`+ht+`) + -'`),gi&&(Y=!0,ce+=`'; -`+gi+`; -__p += '`),gt&&(ce+=`' + -((__t = (`+gt+`)) == null ? '' : __t) + -'`),le=Qi+je.length,je}),ce+=`'; -`;var $e=et.call(f,"variable")&&f.variable;if(!$e)ce=`with (obj) { -`+ce+` +'`),Ci&&(Q=!0,fe+=`'; +`+Ci+`; +__p += '`),St&&(fe+=`' + +((__t = (`+St+`)) == null ? '' : __t) + +'`),le=bo+Ge.length,Ge}),fe+=`'; +`;var Ke=kt.call(f,"variable")&&f.variable;if(!Ke)fe=`with (obj) { +`+fe+` } -`;else if(ms.test($e))throw new Qe(h);ce=(Y?ce.replace(ir,""):ce).replace(Ul,"$1").replace(Ft,"$1;"),ce="function("+($e||"obj")+`) { -`+($e?"":`obj || (obj = {}); -`)+"var __t, __p = ''"+(z?", __e = _.escape":"")+(Y?`, __j = Array.prototype.join; +`;else if(Hp.test(Ke))throw new Je(h);fe=(Q?fe.replace(Tl,""):fe).replace(ze,"$1").replace(Qr,"$1;"),fe="function("+(Ke||"obj")+`) { +`+(Ke?"":`obj || (obj = {}); +`)+"var __t, __p = ''"+($?", __e = _.escape":"")+(Q?`, __j = Array.prototype.join; function print() { __p += __j.call(arguments, '') } `:`; -`)+ce+`return __p -}`;var ft=Db(function(){return bt(L,Fe+"return "+ce).apply(e,F)});if(ft.source=ce,cw(ft))throw ft;return ft}o(g2,"template");function v2(s){return Dt(s).toLowerCase()}o(v2,"toLower");function y2(s){return Dt(s).toUpperCase()}o(y2,"toUpper");function w2(s,f,m){if(s=Dt(s),s&&(m||f===e))return Ss(s);if(!s||!(f=wn(f)))return s;var x=Kt(s),_=Kt(f),L=qi(x,_),F=al(x,_)+1;return Bs(x,L,F).join("")}o(w2,"trim");function x2(s,f,m){if(s=Dt(s),s&&(m||f===e))return s.slice(0,gn(s)+1);if(!s||!(f=wn(f)))return s;var x=Kt(s),_=al(x,Kt(f))+1;return Bs(x,0,_).join("")}o(x2,"trimEnd");function S2(s,f,m){if(s=Dt(s),s&&(m||f===e))return s.replace(rl,"");if(!s||!(f=wn(f)))return s;var x=Kt(s),_=qi(x,Kt(f));return Bs(x,_).join("")}o(S2,"trimStart");function C2(s,f){var m=me,x=xe;if(Sr(f)){var _="separator"in f?f.separator:_;m="length"in f?lt(f.length):m,x="omission"in f?wn(f.omission):x}s=Dt(s);var L=s.length;if(qn(s)){var F=Kt(s);L=F.length}if(m>=L)return s;var z=m-wr(x);if(z<1)return x;var Y=F?Bs(F,0,z).join(""):s.slice(0,z);if(_===e)return Y+x;if(F&&(z+=Y.length-z),pw(_)){if(s.slice(z).search(_)){var le,ae=Y;for(_.global||(_=Cs(_.source,Dt(nl.exec(_))+"g")),_.lastIndex=0;le=_.exec(ae);)var ce=le.index;Y=Y.slice(0,ce===e?z:ce)}}else if(s.indexOf(wn(_),z)!=z){var Le=Y.lastIndexOf(_);Le>-1&&(Y=Y.slice(0,Le))}return Y+x}o(C2,"truncate");function b2(s){return s=Dt(s),s&&li.test(s)?s.replace(Wr,ci):s}o(b2,"unescape");var E2=Na(function(s,f,m){return s+(m?" ":"")+f.toUpperCase()}),mw=vg("toUpperCase");function Ab(s,f,m){return s=Dt(s),f=m?e:f,f===e?Ti(s)?Vn(s):Du(s):s.match(f)||[]}o(Ab,"words");var Db=st(function(s,f){try{return Ur(s,e,f)}catch(m){return cw(m)?m:new Qe(m)}}),_2=ts(function(s,f){return jn(f,function(m){m=Lt(m),yo(s,m,uw(s[m],s))}),s});function T2(s){var f=s==null?0:s.length,m=Be();return s=f?yt(s,function(x){if(typeof x[1]!="function")throw new Kn(d);return[m(x[0]),x[1]]}):[],st(function(x){for(var _=-1;++_Ge)return[];var m=ct,x=Zr(s,ct);f=Be(f),s-=ct;for(var _=Yo(x,f);++m0||f<0)?new dt(m):(s<0?m=m.takeRight(-s):s&&(m=m.drop(s)),f!==e&&(f=lt(f),m=f<0?m.dropRight(-f):m.take(f-s)),m)},dt.prototype.takeRightWhile=function(s){return this.reverse().takeWhile(s).reverse()},dt.prototype.toArray=function(){return this.take(ct)},hi(dt.prototype,function(s,f){var m=/^(?:filter|find|map|reject)|While$/.test(f),x=/^(?:head|last)$/.test(f),_=N[x?"take"+(f=="last"?"Right":""):f],L=x||/^find/.test(f);!_||(N.prototype[f]=function(){var F=this.__wrapped__,z=x?[1]:arguments,Y=F instanceof dt,le=z[0],ae=Y||ot(F),ce=o(function(ht){var gt=_.apply(N,$i([ht],z));return x&&Le?gt[0]:gt},"interceptor");ae&&m&&typeof le=="function"&&le.length!=1&&(Y=ae=!1);var Le=this.__chain__,Fe=!!this.__actions__.length,$e=L&&!Le,ft=Y&&!Fe;if(!L&&ae){F=ft?F:new dt(this);var je=s.apply(F,z);return je.__actions__.push({func:_g,args:[ce],thisArg:e}),new An(je,Le)}return $e&&ft?s.apply(this,z):(je=this.thru(ce),$e?x?je.value()[0]:je.value():je)})}),jn(["pop","push","shift","sort","splice","unshift"],function(s){var f=fa[s],m=/^(?:push|sort|unshift)$/.test(s)?"tap":"thru",x=/^(?:pop|shift)$/.test(s);N.prototype[s]=function(){var _=arguments;if(x&&!this.__chain__){var L=this.value();return f.apply(ot(L)?L:[],_)}return this[m](function(F){return f.apply(ot(F)?F:[],_)})}}),hi(dt.prototype,function(s,f){var m=N[f];if(m){var x=m.name+"";et.call(ga,x)||(ga[x]=[]),ga[x].push({name:f,func:m})}}),ga[sf(e,Z).name]=[{name:"wrapper",func:e}],dt.prototype.clone=qy,dt.prototype.reverse=Vy,dt.prototype.value=yd,N.prototype.at=eM,N.prototype.chain=tM,N.prototype.commit=rM,N.prototype.next=nM,N.prototype.plant=oM,N.prototype.reverse=sM,N.prototype.toJSON=N.prototype.valueOf=N.prototype.value=lM,N.prototype.first=N.prototype.head,_s&&(N.prototype[_s]=iM),N},"runInContext"),vn=zu();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(qt._=vn,define(function(){return vn})):hn?((hn.exports=vn)._=vn,ea._=vn):qt._=vn}).call(Np)});var $k=Ue((oS,sS)=>{(function(e,t){typeof oS=="object"&&typeof sS!="undefined"?sS.exports=t():typeof define=="function"&&define.amd?define(t):e.stable=t()})(oS,function(){"use strict";var e=o(function(l,d){return t(l.slice(),d)},"stable");e.inplace=function(l,d){var h=t(l,d);return h!==l&&n(h,null,l.length,l),l};function t(l,d){typeof d!="function"&&(d=o(function(k,O){return String(k).localeCompare(O)},"comp"));var h=l.length;if(h<=1)return l;for(var c=new Array(h),v=1;vv&&(j=v),B>v&&(B=v),X=O,J=j;;)if(X{(function(){"use strict";var e={}.hasOwnProperty;function t(){for(var n=[],l=0;l{(function(e,t){typeof FS=="object"&&typeof BS!="undefined"?BS.exports=t():typeof define=="function"&&define.amd?define(t):(e=e||self,e.CodeMirror=t())})(FS,function(){"use strict";var e=navigator.userAgent,t=navigator.platform,n=/gecko\/\d/i.test(e),l=/MSIE \d/.test(e),d=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(e),h=/Edge\/(\d+)/.exec(e),c=l||d||h,v=c&&(l?document.documentMode||6:+(h||d)[1]),C=!h&&/WebKit\//.test(e),k=C&&/Qt\/\d+\.\d+/.test(e),O=!h&&/Chrome\//.test(e),j=/Opera\//.test(e),B=/Apple Computer/.test(navigator.vendor),X=/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(e),J=/PhantomJS/.test(e),Z=B&&(/Mobile\/\w+/.test(e)||navigator.maxTouchPoints>2),R=/Android/.test(e),A=Z||R||/webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(e),I=Z||/Mac/.test(t),G=/\bCrOS\b/.test(e),K=/win/i.test(t),se=j&&e.match(/Version\/(\d*\.\d*)/);se&&(se=Number(se[1])),se&&se>=15&&(j=!1,C=!0);var ne=I&&(k||j&&(se==null||se<12.11)),pe=n||c&&v>=9;function me(r){return new RegExp("(^|\\s)"+r+"(?:$|\\s)\\s*")}o(me,"classTest");var xe=o(function(r,i){var u=r.className,a=me(i).exec(u);if(a){var p=u.slice(a.index+a[0].length);r.className=u.slice(0,a.index)+(p?a[1]+p:"")}},"rmClass");function Ve(r){for(var i=r.childNodes.length;i>0;--i)r.removeChild(r.firstChild);return r}o(Ve,"removeChildren");function tt(r,i){return Ve(r).appendChild(i)}o(tt,"removeChildrenAndAdd");function _e(r,i,u,a){var p=document.createElement(r);if(u&&(p.className=u),a&&(p.style.cssText=a),typeof i=="string")p.appendChild(document.createTextNode(i));else if(i)for(var g=0;g=i)return y+(i-g);y+=S-g,y+=u-y%u,g=S+1}}o(_t,"countColumn");var Ct=o(function(){this.id=null,this.f=null,this.time=0,this.handler=Hr(this.onTimeout,this)},"Delayed");Ct.prototype.onTimeout=function(r){r.id=0,r.time<=+new Date?r.f():setTimeout(r.handler,r.time-+new Date)},Ct.prototype.set=function(r,i){this.f=i;var u=+new Date+r;(!this.id||u=i)return a+Math.min(y,i-p);if(p+=g-a,p+=u-p%u,a=g+1,p>=i)return a}}o(Pr,"findColumn");var Gt=[""];function Yt(r){for(;Gt.length<=r;)Gt.push(Se(Gt)+" ");return Gt[r]}o(Yt,"spaceStr");function Se(r){return r[r.length-1]}o(Se,"lst");function Or(r,i){for(var u=[],a=0;a"\x80"&&(r.toUpperCase()!=r.toLowerCase()||cn.test(r))}o(Zt,"isWordCharBasic");function gr(r,i){return i?i.source.indexOf("\\w")>-1&&Zt(r)?!0:i.test(r):Zt(r)}o(gr,"isWordChar");function pt(r){for(var i in r)if(r.hasOwnProperty(i)&&r[i])return!1;return!0}o(pt,"isEmpty");var Ho=/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;function Cr(r){return r.charCodeAt(0)>=768&&Ho.test(r)}o(Cr,"isExtendingChar");function Ui(r,i,u){for(;(u<0?i>0:iu?-1:1;;){if(i==u)return i;var p=(i+u)/2,g=a<0?Math.ceil(p):Math.floor(p);if(g==i)return r(g)?i:u;r(g)?u=g:i=g+a}}o(pn,"findFirst");function zn(r,i,u,a){if(!r)return a(i,u,"ltr",0);for(var p=!1,g=0;gi||i==u&&y.to==i)&&(a(Math.max(y.from,i),Math.min(y.to,u),y.level==1?"rtl":"ltr",g),p=!0)}p||a(i,u,"ltr")}o(zn,"iterateBidiSections");var Si=null;function Ci(r,i,u){var a;Si=null;for(var p=0;pi)return p;g.to==i&&(g.from!=g.to&&u=="before"?a=p:Si=p),g.from==i&&(g.from!=g.to&&u!="before"?a=p:Si=p)}return a??Si}o(Ci,"getBidiPartAt");var $n=function(){var r="bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN",i="nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111";function u(E){return E<=247?r.charAt(E):1424<=E&&E<=1524?"R":1536<=E&&E<=1785?i.charAt(E-1536):1774<=E&&E<=2220?"r":8192<=E&&E<=8203?"w":E==8204?"b":"L"}o(u,"charType");var a=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,p=/[stwN]/,g=/[LRr]/,y=/[Lb1n]/,S=/[1n]/;function b(E,M,D){this.level=E,this.from=M,this.to=D}return o(b,"BidiSpan"),function(E,M){var D=M=="ltr"?"L":"R";if(E.length==0||M=="ltr"&&!a.test(E))return!1;for(var V=E.length,$=[],te=0;te-1&&(a[i]=p.slice(0,g).concat(p.slice(g+1)))}}}o(he,"off");function Te(r,i){var u=ee(r,i);if(!!u.length)for(var a=Array.prototype.slice.call(arguments,2),p=0;p0}o(Ft,"hasHandler");function Wr(r){r.prototype.on=function(i,u){H(this,i,u)},r.prototype.off=function(i,u){he(this,i,u)}}o(Wr,"eventMixin");function or(r){r.preventDefault?r.preventDefault():r.returnValue=!1}o(or,"e_preventDefault");function li(r){r.stopPropagation?r.stopPropagation():r.cancelBubble=!0}o(li,"e_stopPropagation");function ds(r){return r.defaultPrevented!=null?r.defaultPrevented:r.returnValue==!1}o(ds,"e_defaultPrevented");function lo(r){or(r),li(r)}o(lo,"e_stop");function bi(r){return r.target||r.srcElement}o(bi,"e_target");function el(r){var i=r.which;return i==null&&(r.button&1?i=1:r.button&2?i=3:r.button&4&&(i=2)),I&&r.ctrlKey&&i==1&&(i=3),i}o(el,"e_button");var hs=function(){if(c&&v<9)return!1;var r=_e("div");return"draggable"in r||"dragDrop"in r}(),dn;function id(r){if(dn==null){var i=_e("span","\u200B");tt(r,_e("span",[i,document.createTextNode("x")])),r.firstChild.offsetHeight!=0&&(dn=i.offsetWidth<=1&&i.offsetHeight>2&&!(c&&v<8))}var u=dn?_e("span","\u200B"):_e("span","\xA0",null,"display: inline-block; width: 1px; margin-right: -1px");return u.setAttribute("cm-text",""),u}o(id,"zeroWidthElement");var tl;function Qf(r){if(tl!=null)return tl;var i=tt(r,document.createTextNode("A\u062EA")),u=We(i,0,1).getBoundingClientRect(),a=We(i,1,2).getBoundingClientRect();return Ve(r),!u||u.left==u.right?!1:tl=a.right-u.right<3}o(Qf,"hasBadBidiRects");var rl=` +`)+fe+`return __p +}`;var dt=Sb(function(){return yt(A,He+"return "+fe).apply(e,H)});if(dt.source=fe,qw(dt))throw dt;return dt}i(B2,"template");function H2(a){return zt(a).toLowerCase()}i(H2,"toLower");function z2(a){return zt(a).toUpperCase()}i(z2,"toUpper");function U2(a,f,m){if(a=zt(a),a&&(m||f===e))return Vl(a);if(!a||!(f=Si(f)))return a;var x=Xt(a),T=Xt(f),A=yi(x,T),H=ea(x,T)+1;return ks(x,A,H).join("")}i(U2,"trim");function W2(a,f,m){if(a=zt(a),a&&(m||f===e))return a.slice(0,Un(a)+1);if(!a||!(f=Si(f)))return a;var x=Xt(a),T=ea(x,Xt(f))+1;return ks(x,0,T).join("")}i(W2,"trimEnd");function $2(a,f,m){if(a=zt(a),a&&(m||f===e))return a.replace(ic,"");if(!a||!(f=Si(f)))return a;var x=Xt(a),T=yi(x,Xt(f));return ks(x,T).join("")}i($2,"trimStart");function V2(a,f){var m=he,x=Re;if(_r(f)){var T="separator"in f?f.separator:T;m="length"in f?ct(f.length):m,x="omission"in f?Si(f.omission):x}a=zt(a);var A=a.length;if(xs(a)){var H=Xt(a);A=H.length}if(m>=A)return a;var $=m-gn(x);if($<1)return x;var Q=H?ks(H,0,$).join(""):a.slice(0,$);if(T===e)return Q+x;if(H&&($+=Q.length-$),jw(T)){if(a.slice($).search(T)){var le,ue=Q;for(T.global||(T=Nc(T.source,zt(Vf.exec(T))+"g")),T.lastIndex=0;le=T.exec(ue);)var fe=le.index;Q=Q.slice(0,fe===e?$:fe)}}else if(a.indexOf(Si(T),$)!=$){var Ne=Q.lastIndexOf(T);Ne>-1&&(Q=Q.slice(0,Ne))}return Q+x}i(V2,"truncate");function q2(a){return a=zt(a),a&&At.test(a)?a.replace(Yt,fo):a}i(q2,"unescape");var j2=mu(function(a,f,m){return a+(m?" ":"")+f.toUpperCase()}),Yw=Yg("toUpperCase");function wb(a,f,m){return a=zt(a),f=m?e:f,f===e?td(a)?kc(a):Zf(a):a.match(f)||[]}i(wb,"words");var Sb=ut(function(a,f){try{return Jr(a,e,f)}catch(m){return qw(m)?m:new Je(m)}}),K2=Qo(function(a,f){return Zn(f,function(m){m=ar(m),bs(a,m,$w(a[m],a))}),a});function G2(a){var f=a==null?0:a.length,m=We();return a=f?$t(a,function(x){if(typeof x[1]!="function")throw new Bi(l);return[m(x[0]),x[1]]}):[],ut(function(x){for(var T=-1;++TQe)return[];var m=Pt,x=Pr(a,Pt);f=We(f),a-=Pt;for(var T=Ec(x,f);++m0||f<0)?new $e(m):(a<0?m=m.takeRight(-a):a&&(m=m.drop(a)),f!==e&&(f=ct(f),m=f<0?m.dropRight(-f):m.take(f-a)),m)},$e.prototype.takeRightWhile=function(a){return this.reverse().takeWhile(a).reverse()},$e.prototype.toArray=function(){return this.take(Pt)},zi($e.prototype,function(a,f){var m=/^(?:filter|find|map|reject)|While$/.test(f),x=/^(?:head|last)$/.test(f),T=P[x?"take"+(f=="last"?"Right":""):f],A=x||/^find/.test(f);T&&(P.prototype[f]=function(){var H=this.__wrapped__,$=x?[1]:arguments,Q=H instanceof $e,le=$[0],ue=Q||at(H),fe=i(function(ht){var St=T.apply(P,Ii([ht],$));return x&&Ne?St[0]:St},"interceptor");ue&&m&&typeof le=="function"&&le.length!=1&&(Q=ue=!1);var Ne=this.__chain__,He=!!this.__actions__.length,Ke=A&&!Ne,dt=Q&&!He;if(!A&&ue){H=dt?H:new $e(this);var Ge=a.apply(H,$);return Ge.__actions__.push({func:nv,args:[fe],thisArg:e}),new Lt(Ge,Ne)}return Ke&&dt?a.apply(this,$):(Ge=this.thru(fe),Ke?x?Ge.value()[0]:Ge.value():Ge)})}),Zn(["pop","push","shift","sort","splice","unshift"],function(a){var f=rd[a],m=/^(?:push|sort|unshift)$/.test(a)?"tap":"thru",x=/^(?:pop|shift)$/.test(a);P.prototype[a]=function(){var T=arguments;if(x&&!this.__chain__){var A=this.value();return f.apply(at(A)?A:[],T)}return this[m](function(H){return f.apply(at(H)?H:[],T)})}}),zi($e.prototype,function(a,f){var m=P[f];if(m){var x=m.name+"";kt.call(ja,x)||(ja[x]=[]),ja[x].push({name:f,func:m})}}),ja[Sd(e,Y).name]=[{name:"wrapper",func:e}],$e.prototype.clone=Fr,$e.prototype.reverse=Oc,$e.prototype.value=yw,P.prototype.at=CR,P.prototype.chain=_R,P.prototype.commit=ER,P.prototype.next=bR,P.prototype.plant=kR,P.prototype.reverse=NR,P.prototype.toJSON=P.prototype.valueOf=P.prototype.value=PR,P.prototype.first=P.prototype.head,$a&&(P.prototype[$a]=TR),P},"runInContext"),vn=Yl();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(Rt._=vn,define(function(){return vn})):Ys?((Ys.exports=vn)._=vn,Yf._=vn):Rt._=vn}).call(sp)});var MP=rt((Px,Ax)=>{(function(e,t){typeof Px=="object"&&typeof Ax<"u"?Ax.exports=t():typeof define=="function"&&define.amd?define(t):e.stable=t()})(Px,function(){"use strict";var e=i(function(s,l){return t(s.slice(),l)},"stable");e.inplace=function(s,l){var h=t(s,l);return h!==s&&n(h,null,s.length,s),s};function t(s,l){typeof l!="function"&&(l=i(function(b,k){return String(b).localeCompare(k)},"comp"));var h=s.length;if(h<=1)return s;for(var d=new Array(h),v=1;vv&&(R=v),I>v&&(I=v),z=k,j=R;;)if(z{(function(){"use strict";var e={}.hasOwnProperty;function t(){for(var l="",h=0;h{(function(e,t){typeof n_=="object"&&typeof i_<"u"?i_.exports=t():typeof define=="function"&&define.amd?define(t):(e=e||self,e.CodeMirror=t())})(n_,function(){"use strict";var e=navigator.userAgent,t=navigator.platform,n=/gecko\/\d/i.test(e),s=/MSIE \d/.test(e),l=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(e),h=/Edge\/(\d+)/.exec(e),d=s||l||h,v=d&&(s?document.documentMode||6:+(h||l)[1]),S=!h&&/WebKit\//.test(e),b=S&&/Qt\/\d+\.\d+/.test(e),k=!h&&/Chrome\/(\d+)/.exec(e),R=k&&+k[1],I=/Opera\//.test(e),z=/Apple Computer/.test(navigator.vendor),j=/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(e),Y=/PhantomJS/.test(e),ne=z&&(/Mobile\/\w+/.test(e)||navigator.maxTouchPoints>2),F=/Android/.test(e),L=ne||F||/webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(e),D=ne||/Mac/.test(t),q=/\bCrOS\b/.test(e),te=/win/i.test(t),Z=I&&e.match(/Version\/(\d*\.\d*)/);Z&&(Z=Number(Z[1])),Z&&Z>=15&&(I=!1,S=!0);var oe=D&&(b||I&&(Z==null||Z<12.11)),he=n||d&&v>=9;function Re(r){return new RegExp("(^|\\s)"+r+"(?:$|\\s)\\s*")}i(Re,"classTest");var Ee=i(function(r,o){var c=r.className,u=Re(o).exec(c);if(u){var p=c.slice(u.index+u[0].length);r.className=c.slice(0,u.index)+(p?u[1]+p:"")}},"rmClass");function Ye(r){for(var o=r.childNodes.length;o>0;--o)r.removeChild(r.firstChild);return r}i(Ye,"removeChildren");function tt(r,o){return Ye(r).appendChild(o)}i(tt,"removeChildrenAndAdd");function xe(r,o,c,u){var p=document.createElement(r);if(c&&(p.className=c),u&&(p.style.cssText=u),typeof o=="string")p.appendChild(document.createTextNode(o));else if(o)for(var g=0;g=o)return y+(o-g);y+=C-g,y+=c-y%c,g=C+1}}i(Ft,"countColumn");var se=i(function(){this.id=null,this.f=null,this.time=0,this.handler=yr(this.onTimeout,this)},"Delayed");se.prototype.onTimeout=function(r){r.id=0,r.time<=+new Date?r.f():setTimeout(r.handler,r.time-+new Date)},se.prototype.set=function(r,o){this.f=o;var c=+new Date+r;(!this.id||c=o)return u+Math.min(y,o-p);if(p+=g-u,p+=c-p%c,u=g+1,p>=o)return u}}i(bn,"findColumn");var no=[""];function Ho(r){for(;no.length<=r;)no.push(lt(no)+" ");return no[r]}i(Ho,"spaceStr");function lt(r){return r[r.length-1]}i(lt,"lst");function wr(r,o){for(var c=[],u=0;u"\x80"&&(r.toUpperCase()!=r.toLowerCase()||Bn.test(r))}i(Mi,"isWordCharBasic");function Yr(r,o){return o?o.source.indexOf("\\w")>-1&&Mi(r)?!0:o.test(r):Mi(r)}i(Yr,"isWordChar");function Xr(r){for(var o in r)if(r.hasOwnProperty(o)&&r[o])return!1;return!0}i(Xr,"isEmpty");var oo=/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;function hi(r){return r.charCodeAt(0)>=768&&oo.test(r)}i(hi,"isExtendingChar");function Hn(r,o,c){for(;(c<0?o>0:oc?-1:1;;){if(o==c)return o;var p=(o+c)/2,g=u<0?Math.ceil(p):Math.floor(p);if(g==o)return r(g)?o:c;r(g)?c=g:o=g+u}}i(so,"findFirst");function bl(r,o,c,u){if(!r)return u(o,c,"ltr",0);for(var p=!1,g=0;go||o==c&&y.to==o)&&(u(Math.max(y.from,o),Math.min(y.to,c),y.level==1?"rtl":"ltr",g),p=!0)}p||u(o,c,"ltr")}i(bl,"iterateBidiSections");var X=null;function ee(r,o,c){var u;X=null;for(var p=0;po)return p;g.to==o&&(g.from!=g.to&&c=="before"?u=p:X=p),g.from==o&&(g.from!=g.to&&c!="before"?u=p:X=p)}return u??X}i(ee,"getBidiPartAt");var Ce=function(){var r="bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN",o="nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111";function c(E){return E<=247?r.charAt(E):1424<=E&&E<=1524?"R":1536<=E&&E<=1785?o.charAt(E-1536):1774<=E&&E<=2220?"r":8192<=E&&E<=8203?"w":E==8204?"b":"L"}i(c,"charType");var u=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,p=/[stwN]/,g=/[LRr]/,y=/[Lb1n]/,C=/[1n]/;function _(E,O,B){this.level=E,this.from=O,this.to=B}return i(_,"BidiSpan"),function(E,O){var B=O=="ltr"?"L":"R";if(E.length==0||O=="ltr"&&!u.test(E))return!1;for(var K=E.length,V=[],re=0;re-1&&(u[o]=p.slice(0,g).concat(p.slice(g+1)))}}}i(Yt,"off");function Mt(r,o){var c=Qr(r,o);if(c.length)for(var u=Array.prototype.slice.call(arguments,2),p=0;p0}i(pn,"hasHandler");function zn(r){r.prototype.on=function(o,c){ze(this,o,c)},r.prototype.off=function(o,c){Yt(this,o,c)}}i(zn,"eventMixin");function kr(r){r.preventDefault?r.preventDefault():r.returnValue=!1}i(kr,"e_preventDefault");function gs(r){r.stopPropagation?r.stopPropagation():r.cancelBubble=!0}i(gs,"e_stopPropagation");function Tn(r){return r.defaultPrevented!=null?r.defaultPrevented:r.returnValue==!1}i(Tn,"e_defaultPrevented");function Pa(r){kr(r),gs(r)}i(Pa,"e_stop");function Aa(r){return r.target||r.srcElement}i(Aa,"e_target");function nc(r){var o=r.which;return o==null&&(r.button&1?o=1:r.button&2?o=3:r.button&4&&(o=2)),D&&r.ctrlKey&&o==1&&(o=3),o}i(nc,"e_button");var ic=function(){if(d&&v<9)return!1;var r=xe("div");return"draggable"in r||"dragDrop"in r}(),oc;function $f(r){if(oc==null){var o=xe("span","\u200B");tt(r,xe("span",[o,document.createTextNode("x")])),r.firstChild.offsetHeight!=0&&(oc=o.offsetWidth<=1&&o.offsetHeight>2&&!(d&&v<8))}var c=oc?xe("span","\u200B"):xe("span","\xA0",null,"display: inline-block; width: 1px; margin-right: -1px");return c.setAttribute("cm-text",""),c}i($f,"zeroWidthElement");var sc;function Bp(r){if(sc!=null)return sc;var o=tt(r,document.createTextNode("A\u062EA")),c=je(o,0,1).getBoundingClientRect(),u=je(o,1,2).getBoundingClientRect();return Ye(r),!c||c.left==c.right?!1:sc=u.right-c.right<3}i(Bp,"hasBadBidiRects");var kl=` -b`.split(/\n/).length!=3?function(r){for(var i=0,u=[],a=r.length;i<=a;){var p=r.indexOf(` -`,i);p==-1&&(p=r.length);var g=r.slice(i,r.charAt(p-1)=="\r"?p-1:p),y=g.indexOf("\r");y!=-1?(u.push(g.slice(0,y)),i+=y+1):(u.push(g),i=p+1)}return u}:function(r){return r.split(/\r\n?|\n/)},od=window.getSelection?function(r){try{return r.selectionStart!=r.selectionEnd}catch(i){return!1}}:function(r){var i;try{i=r.ownerDocument.selection.createRange()}catch(u){}return!i||i.parentElement()!=r?!1:i.compareEndPoints("StartToEnd",i)!=0},Zf=function(){var r=_e("div");return"oncopy"in r?!0:(r.setAttribute("oncopy","return;"),typeof r.oncopy=="function")}(),wu=null;function sd(r){if(wu!=null)return wu;var i=tt(r,_e("span","x")),u=i.getBoundingClientRect(),a=We(i,0,1).getBoundingClientRect();return wu=Math.abs(u.left-a.left)>1}o(sd,"hasBadZoomedRects");var zl={},ms={};function ld(r,i){arguments.length>2&&(i.dependencies=Array.prototype.slice.call(arguments,2)),zl[r]=i}o(ld,"defineMode");function Jf(r,i){ms[r]=i}o(Jf,"defineMIME");function nl(r){if(typeof r=="string"&&ms.hasOwnProperty(r))r=ms[r];else if(r&&typeof r.name=="string"&&ms.hasOwnProperty(r.name)){var i=ms[r.name];typeof i=="string"&&(i={name:i}),r=si(i,r),r.name=i.name}else{if(typeof r=="string"&&/^[\w\-]+\/[\w\-]+\+xml$/.test(r))return nl("application/xml");if(typeof r=="string"&&/^[\w\-]+\/[\w\-]+\+json$/.test(r))return nl("application/json")}return typeof r=="string"?{name:r}:r||{name:"null"}}o(nl,"resolveMode");function xu(r,i){i=nl(i);var u=zl[i.name];if(!u)return xu(r,"text/plain");var a=u(r,i);if(Wo.hasOwnProperty(i.name)){var p=Wo[i.name];for(var g in p)!p.hasOwnProperty(g)||(a.hasOwnProperty(g)&&(a["_"+g]=a[g]),a[g]=p[g])}if(a.name=i.name,i.helperType&&(a.helperType=i.helperType),i.modeProps)for(var y in i.modeProps)a[y]=i.modeProps[y];return a}o(xu,"getMode");var Wo={};function ad(r,i){var u=Wo.hasOwnProperty(r)?Wo[r]:Wo[r]={};Qt(i,u)}o(ad,"extendMode");function Uo(r,i){if(i===!0)return i;if(r.copyState)return r.copyState(i);var u={};for(var a in i){var p=i[a];p instanceof Array&&(p=p.concat([])),u[a]=p}return u}o(Uo,"copyState");function $l(r,i){for(var u;r.innerMode&&(u=r.innerMode(i),!(!u||u.mode==r));)i=u.state,r=u.mode;return u||{mode:r,state:i}}o($l,"innerMode");function ec(r,i,u){return r.startState?r.startState(i,u):!0}o(ec,"startState");var jt=o(function(r,i,u){this.pos=this.start=0,this.string=r,this.tabSize=i||8,this.lastColumnPos=this.lastColumnValue=0,this.lineStart=0,this.lineOracle=u},"StringStream");jt.prototype.eol=function(){return this.pos>=this.string.length},jt.prototype.sol=function(){return this.pos==this.lineStart},jt.prototype.peek=function(){return this.string.charAt(this.pos)||void 0},jt.prototype.next=function(){if(this.posi},jt.prototype.eatSpace=function(){for(var r=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>r},jt.prototype.skipToEnd=function(){this.pos=this.string.length},jt.prototype.skipTo=function(r){var i=this.string.indexOf(r,this.pos);if(i>-1)return this.pos=i,!0},jt.prototype.backUp=function(r){this.pos-=r},jt.prototype.column=function(){return this.lastColumnPos0?null:(g&&i!==!1&&(this.pos+=g[0].length),g)}},jt.prototype.current=function(){return this.string.slice(this.start,this.pos)},jt.prototype.hideFirstChars=function(r,i){this.lineStart+=r;try{return i()}finally{this.lineStart-=r}},jt.prototype.lookAhead=function(r){var i=this.lineOracle;return i&&i.lookAhead(r)},jt.prototype.baseToken=function(){var r=this.lineOracle;return r&&r.baseToken(this.pos)};function Me(r,i){if(i-=r.first,i<0||i>=r.size)throw new Error("There is no line "+(i+r.first)+" in the document.");for(var u=r;!u.lines;)for(var a=0;;++a){var p=u.children[a],g=p.chunkSize();if(i=r.first&&iu?ue(u,Me(r,u).text.length):ud(i,Me(r,i.line).text.length)}o(He,"clipPos");function ud(r,i){var u=r.ch;return u==null||u>i?ue(r.line,i):u<0?ue(r.line,0):r}o(ud,"clipToLen");function ql(r,i){for(var u=[],a=0;athis.maxLookAhead&&(this.maxLookAhead=r),i},ui.prototype.baseToken=function(r){if(!this.baseTokens)return null;for(;this.baseTokens[this.baseTokenPos]<=r;)this.baseTokenPos+=2;var i=this.baseTokens[this.baseTokenPos+1];return{type:i&&i.replace(/( |^)overlay .*/,""),size:this.baseTokens[this.baseTokenPos]-r}},ui.prototype.nextLine=function(){this.line++,this.maxLookAhead>0&&this.maxLookAhead--},ui.fromSaved=function(r,i,u){return i instanceof uo?new ui(r,Uo(r.mode,i.state),u,i.lookAhead):new ui(r,Uo(r.mode,i),u)},ui.prototype.save=function(r){var i=r!==!1?Uo(this.doc.mode,this.state):this.state;return this.maxLookAhead>0?new uo(i,this.maxLookAhead):i};function tc(r,i,u,a){var p=[r.state.modeGen],g={};Gl(r,i.text,r.doc.mode,u,function(E,M){return p.push(E,M)},g,a);for(var y=u.state,S=o(function(E){u.baseTokens=p;var M=r.state.overlays[E],D=1,V=0;u.state=!0,Gl(r,i.text,M.mode,u,function($,te){for(var oe=D;V<$;){var de=p[D];de>$&&p.splice(D,1,$,p[D+1],de),D+=2,V=Math.min($,de)}if(!!te)if(M.opaque)p.splice(oe,D-oe,$,"overlay "+te),D=oe+2;else for(;oer.options.maxHighlightLength&&Uo(r.doc.mode,a.state),g=tc(r,i,a);p&&(a.state=p),i.stateAfter=a.save(!p),i.styles=g.styles,g.classes?i.styleClasses=g.classes:i.styleClasses&&(i.styleClasses=null),u===r.doc.highlightFrontier&&(r.doc.modeFrontier=Math.max(r.doc.modeFrontier,++r.doc.highlightFrontier))}return i.styles}o(_u,"getLineStyles");function $o(r,i,u){var a=r.doc,p=r.display;if(!a.mode.startState)return new ui(a,!0,i);var g=Yl(r,i,u),y=g>a.first&&Me(a,g-1).stateAfter,S=y?ui.fromSaved(a,y,g):new ui(a,ec(a.mode),g);return a.iter(g,i,function(b){vs(r,b.text,S);var E=S.line;b.stateAfter=E==i-1||E%5==0||E>=p.viewFrom&&Ei.start)return g}throw new Error("Mode "+r.name+" failed to advance stream.")}o(ol,"readToken");var Vl=o(function(r,i,u){this.start=r.start,this.end=r.pos,this.string=r.current(),this.type=i||null,this.state=u},"Token");function Kl(r,i,u,a){var p=r.doc,g=p.mode,y;i=He(p,i);var S=Me(p,i.line),b=$o(r,i.line,u),E=new jt(S.text,r.options.tabSize,b),M;for(a&&(M=[]);(a||E.posr.options.maxHighlightLength?(S=!1,y&&vs(r,i,a,M.pos),M.pos=i.length,D=null):D=fo(ol(u,M,a.state,V),g),V){var $=V[0].name;$&&(D="m-"+(D?$+" "+D:$))}if(!S||E!=D){for(;by;--S){if(S<=g.first)return g.first;var b=Me(g,S-1),E=b.stateAfter;if(E&&(!u||S+(E instanceof uo?E.lookAhead:0)<=g.modeFrontier))return S;var M=_t(b.text,null,r.options.tabSize);(p==null||a>M)&&(p=S-1,a=M)}return p}o(Yl,"findStartLine");function rc(r,i){if(r.modeFrontier=Math.min(r.modeFrontier,i),!(r.highlightFrontieru;a--){var p=Me(r,a).stateAfter;if(p&&(!(p instanceof uo)||a+p.lookAhead=i:g.to>i);(a||(a=[])).push(new co(y,g.from,b?null:g.to))}}return a}o(fd,"markedSpansBefore");function cd(r,i,u){var a;if(r)for(var p=0;p=i:g.to>i);if(S||g.from==i&&y.type=="bookmark"&&(!u||g.marker.insertLeft)){var b=g.from==null||(y.inclusiveLeft?g.from<=i:g.from0&&S)for(var Ne=0;Ne0)){var M=[b,1],D=ze(E.from,S.from),V=ze(E.to,S.to);(D<0||!y.inclusiveLeft&&!D)&&M.push({from:E.from,to:S.from}),(V>0||!y.inclusiveRight&&!V)&&M.push({from:S.to,to:E.to}),p.splice.apply(p,M),b+=M.length-3}}return p}o(Nu,"removeReadOnlyRanges");function lc(r){var i=r.markedSpans;if(!!i){for(var u=0;ui)&&(!a||Lu(a,g.marker)<0)&&(a=g.marker)}return a}o(Re,"collapsedSpanAround");function sl(r,i,u,a,p){var g=Me(r,i),y=_i&&g.markedSpans;if(y)for(var S=0;S=0&&D<=0||M<=0&&D>=0)&&(M<=0&&(b.marker.inclusiveRight&&p.inclusiveLeft?ze(E.to,u)>=0:ze(E.to,u)>0)||M>=0&&(b.marker.inclusiveRight&&p.inclusiveLeft?ze(E.from,a)<=0:ze(E.from,a)<0)))return!0}}}o(sl,"conflictingCollapsedRange");function vr(r){for(var i;i=Nt(r);)r=i.find(-1,!0).line;return r}o(vr,"visualLine");function Pu(r){for(var i;i=P(r);)r=i.find(1,!0).line;return r}o(Pu,"visualLineEnd");function ye(r){for(var i,u;i=P(r);)r=i.find(1,!0).line,(u||(u=[])).push(r);return u}o(ye,"visualLineContinued");function jo(r,i){var u=Me(r,i),a=vr(u);return u==a?i:vt(a)}o(jo,"visualLineNo");function pd(r,i){if(i>r.lastLine())return i;var u=Me(r,i),a;if(!qt(r,u))return i;for(;a=P(u);)u=a.find(1,!0).line;return vt(u)+1}o(pd,"visualLineEndNo");function qt(r,i){var u=_i&&i.markedSpans;if(u){for(var a=void 0,p=0;pi.maxLineLength&&(i.maxLineLength=p,i.maxLine=a)})}o(zi,"findMaxLine");var Ce=o(function(r,i,u){this.text=r,ac(this,i),this.height=u?u(this):1},"Line");Ce.prototype.lineNo=function(){return vt(this)},Wr(Ce);function ta(r,i,u,a){r.text=i,r.stateAfter&&(r.stateAfter=null),r.styles&&(r.styles=null),r.order!=null&&(r.order=null),lc(r),ac(r,u);var p=a?a(r):1;p!=r.height&&ai(r,p)}o(ta,"updateLine");function Ou(r){r.parent=null,lc(r)}o(Ou,"cleanUpLine");var nt={},uc={};function fi(r,i){if(!r||/^\s*$/.test(r))return null;var u=i.addModeClass?uc:nt;return u[r]||(u[r]=r.replace(/\S+/g,"cm-$&"))}o(fi,"interpretTokenStyle");function ll(r,i){var u=St("span",null,null,C?"padding-right: .1px":null),a={pre:St("pre",[u],"CodeMirror-line"),content:u,col:0,pos:0,cm:r,trailingSpace:!1,splitSpaces:r.getOption("lineWrapping")};i.measure={};for(var p=0;p<=(i.rest?i.rest.length:0);p++){var g=p?i.rest[p-1]:i.line,y=void 0;a.pos=0,a.addToken=ra,Qf(r.display.measure)&&(y=Mn(g,r.doc.direction))&&(a.addToken=dd(a.addToken,y)),a.map=[];var S=i!=r.display.externalMeasured&&vt(g);Vt(g,a,_u(r,g,S)),g.styleClasses&&(g.styleClasses.bgClass&&(a.bgClass=nr(g.styleClasses.bgClass,a.bgClass||"")),g.styleClasses.textClass&&(a.textClass=nr(g.styleClasses.textClass,a.textClass||""))),a.map.length==0&&a.map.push(0,0,a.content.appendChild(id(r.display.measure))),p==0?(i.measure.map=a.map,i.measure.cache={}):((i.measure.maps||(i.measure.maps=[])).push(a.map),(i.measure.caches||(i.measure.caches=[])).push({}))}if(C){var b=a.content.lastChild;(/\bcm-tab\b/.test(b.className)||b.querySelector&&b.querySelector(".cm-tab"))&&(a.content.className="cm-tab-wrap-hack")}return Te(r,"renderLine",r,i.line,a.pre),a.pre.className&&(a.textClass=nr(a.pre.className,a.textClass||"")),a}o(ll,"buildLineContent");function Ur(r){var i=_e("span","\u2022","cm-invalidchar");return i.title="\\u"+r.charCodeAt(0).toString(16),i.setAttribute("aria-label",i.title),i}o(Ur,"defaultSpecialCharPlaceholder");function ra(r,i,u,a,p,g,y){if(!!i){var S=r.splitSpaces?jn(i,r.trailingSpace):i,b=r.cm.state.specialChars,E=!1,M;if(!b.test(i))r.col+=i.length,M=document.createTextNode(S),r.map.push(r.pos,r.pos+i.length,M),c&&v<9&&(E=!0),r.pos+=i.length;else{M=document.createDocumentFragment();for(var D=0;;){b.lastIndex=D;var V=b.exec(i),$=V?V.index-D:i.length-D;if($){var te=document.createTextNode(S.slice(D,D+$));c&&v<9?M.appendChild(_e("span",[te])):M.appendChild(te),r.map.push(r.pos,r.pos+$,te),r.col+=$,r.pos+=$}if(!V)break;D+=$+1;var oe=void 0;if(V[0]==" "){var de=r.cm.options.tabSize,ge=de-r.col%de;oe=M.appendChild(_e("span",Yt(ge),"cm-tab")),oe.setAttribute("role","presentation"),oe.setAttribute("cm-text"," "),r.col+=ge}else V[0]=="\r"||V[0]==` -`?(oe=M.appendChild(_e("span",V[0]=="\r"?"\u240D":"\u2424","cm-invalidchar")),oe.setAttribute("cm-text",V[0]),r.col+=1):(oe=r.cm.options.specialCharPlaceholder(V[0]),oe.setAttribute("cm-text",V[0]),c&&v<9?M.appendChild(_e("span",[oe])):M.appendChild(oe),r.col+=1);r.map.push(r.pos,r.pos+1,oe),r.pos++}}if(r.trailingSpace=S.charCodeAt(i.length-1)==32,u||a||p||E||g||y){var be=u||"";a&&(be+=a),p&&(be+=p);var ve=_e("span",[M],be,g);if(y)for(var Ne in y)y.hasOwnProperty(Ne)&&Ne!="style"&&Ne!="class"&&ve.setAttribute(Ne,y[Ne]);return r.content.appendChild(ve)}r.content.appendChild(M)}}o(ra,"buildToken");function jn(r,i){if(r.length>1&&!/ /.test(r))return r;for(var u=i,a="",p=0;pE&&D.from<=E));V++);if(D.to>=M)return r(u,a,p,g,y,S,b);r(u,a.slice(0,D.to-E),p,g,null,S,b),g=null,a=a.slice(D.to-E),E=D.to}}}o(dd,"buildTokenBadBidi");function Mu(r,i,u,a){var p=!a&&u.widgetNode;p&&r.map.push(r.pos,r.pos+i,p),!a&&r.cm.display.input.needsContentAttribute&&(p||(p=r.content.appendChild(document.createElement("span"))),p.setAttribute("cm-marker",u.id)),p&&(r.cm.display.input.setUneditable(p),r.content.appendChild(p)),r.pos+=i,r.trailingSpace=!1}o(Mu,"buildCollapsedSpan");function Vt(r,i,u){var a=r.markedSpans,p=r.text,g=0;if(!a){for(var y=1;yb||it.collapsed&&De.to==b&&De.from==b)){if(De.to!=null&&De.to!=b&&$>De.to&&($=De.to,oe=""),it.className&&(te+=" "+it.className),it.css&&(V=(V?V+";":"")+it.css),it.startStyle&&De.from==b&&(de+=" "+it.startStyle),it.endStyle&&De.to==$&&(Ne||(Ne=[])).push(it.endStyle,De.to),it.title&&((be||(be={})).title=it.title),it.attributes)for(var Tt in it.attributes)(be||(be={}))[Tt]=it.attributes[Tt];it.collapsed&&(!ge||Lu(ge.marker,it)<0)&&(ge=De)}else De.from>b&&$>De.from&&($=De.from)}if(Ne)for(var br=0;br=S)break;for(var Sn=Math.min(S,$);;){if(M){var Cn=b+M.length;if(!ge){var dr=Cn>Sn?M.slice(0,Sn-b):M;i.addToken(i,dr,D?D+te:te,de,b+dr.length==$?oe:"",V,be)}if(Cn>=Sn){M=M.slice(Sn-b),b=Sn;break}b=Cn,de=""}M=p.slice(g,g=u[E++]),D=fi(u[E++],i.cm.options)}}}o(Vt,"insertLineContent");function xs(r,i,u){this.line=i,this.rest=ye(i),this.size=this.rest?vt(Se(this.rest))-u+1:1,this.node=this.text=null,this.hidden=qt(r,i)}o(xs,"LineView");function qo(r,i,u){for(var a=[],p,g=i;g2&&g.push((b.bottom+E.top)/2-u.top)}}g.push(u.bottom-u.top)}}o(cc,"ensureLineHeights");function Wu(r,i,u){if(r.line==i)return{map:r.measure.map,cache:r.measure.cache};for(var a=0;au)return{map:r.measure.maps[p],cache:r.measure.caches[p],before:!0}}o(Wu,"mapFromLineView");function gd(r,i){i=vr(i);var u=vt(i),a=r.display.externalMeasured=new xs(r.doc,i,u);a.lineN=u;var p=a.built=ll(r,a);return a.text=p.pre,tt(r.display.lineMeasure,p.pre),a}o(gd,"updateExternalMeasurement");function Uu(r,i,u,a){return Ti(r,qn(r,i),u,a)}o(Uu,"measureChar");function la(r,i){if(i>=r.display.viewFrom&&i=u.lineN&&ii)&&(g=b-S,p=g-1,i>=b&&(y="right")),p!=null){if(a=r[E+2],S==b&&u==(a.insertLeft?"left":"right")&&(y=u),u=="left"&&p==0)for(;E&&r[E-2]==r[E-3]&&r[E-1].insertLeft;)a=r[(E-=3)+2],y="left";if(u=="right"&&p==b-S)for(;E=0&&(u=r[p]).left==u.right;p--);return u}o(dc,"getUsefulRect");function Vi(r,i,u,a){var p=aa(i.map,u,a),g=p.node,y=p.start,S=p.end,b=p.collapse,E;if(g.nodeType==3){for(var M=0;M<4;M++){for(;y&&Cr(i.line.text.charAt(p.coverStart+y));)--y;for(;p.coverStart+S0&&(b=a="right");var D;r.options.lineWrapping&&(D=g.getClientRects()).length>1?E=D[a=="right"?D.length-1:0]:E=g.getBoundingClientRect()}if(c&&v<9&&!y&&(!E||!E.left&&!E.right)){var V=g.parentNode.getClientRects()[0];V?E={left:V.left,right:V.left+ua(r.display),top:V.top,bottom:V.bottom}:E=pc}for(var $=E.top-i.rect.top,te=E.bottom-i.rect.top,oe=($+te)/2,de=i.view.measure.heights,ge=0;ge=a.text.length?(b=a.text.length,E="before"):b<=0&&(b=0,E="after"),!S)return y(E=="before"?b-1:b,E=="before");function M(te,oe,de){var ge=S[oe],be=ge.level==1;return y(de?te-1:te,be!=de)}o(M,"getBidi");var D=Ci(S,b,E),V=Si,$=M(b,D,E=="before");return V!=null&&($.other=M(b,V,E!="before")),$}o(Vn,"cursorCoords");function zu(r,i){var u=0;i=He(r.doc,i),r.options.lineWrapping||(u=ua(r.display)*i.ch);var a=Me(r.doc,i.line),p=hn(a)+Ss(r.display);return{left:u,right:u,top:p,bottom:p+a.height}}o(zu,"estimateCoords");function vn(r,i,u,a,p){var g=ue(r,i,u);return g.xRel=p,a&&(g.outside=a),g}o(vn,"PosWithInfo");function q(r,i,u){var a=r.doc;if(u+=r.display.viewOffset,u<0)return vn(a.first,0,null,-1,-1);var p=ao(a,u),g=a.first+a.size-1;if(p>g)return vn(a.first+a.size-1,Me(a,g).text.length,null,1,1);i<0&&(i=0);for(var y=Me(a,p);;){var S=Qe(r,y,p,i,u),b=Re(y,S.ch+(S.xRel>0||S.outside>0?1:0));if(!b)return S;var E=b.find(1);if(E.line==p)return E;y=Me(a,p=E.line)}}o(q,"coordsChar");function re(r,i,u,a){a-=gn(i);var p=i.text.length,g=pn(function(y){return Ti(r,u,y-1).bottom<=a},p,0);return p=pn(function(y){return Ti(r,u,y).top>a},g,p),{begin:g,end:p}}o(re,"wrappedLineExtent");function Q(r,i,u,a){u||(u=qn(r,i));var p=ci(r,i,Ti(r,u,a),"line").top;return re(r,i,u,p)}o(Q,"wrappedLineExtentChar");function Pe(r,i,u,a){return r.bottom<=u?!1:r.top>u?!0:(a?r.left:r.right)>i}o(Pe,"boxIsAfter");function Qe(r,i,u,a,p){p-=hn(i);var g=qn(r,i),y=gn(i),S=0,b=i.text.length,E=!0,M=Mn(i,r.doc.direction);if(M){var D=(r.options.lineWrapping?Mr:bt)(r,i,u,g,M,a,p);E=D.level!=1,S=E?D.from:D.to-1,b=E?D.to:D.from-1}var V=null,$=null,te=pn(function(Ie){var De=Ti(r,g,Ie);return De.top+=y,De.bottom+=y,Pe(De,a,p,!1)?(De.top<=p&&De.left<=a&&(V=Ie,$=De),!0):!1},S,b),oe,de,ge=!1;if($){var be=a-$.left<$.right-a,ve=be==E;te=V+(ve?0:1),de=ve?"after":"before",oe=be?$.left:$.right}else{!E&&(te==b||te==S)&&te++,de=te==0?"after":te==i.text.length?"before":Ti(r,g,te-(E?1:0)).bottom+y<=p==E?"after":"before";var Ne=Vn(r,ue(u,te,de),"line",i,g);oe=Ne.left,ge=p=Ne.bottom?1:0}return te=Ui(i.text,te,1),vn(u,te,de,ge,a-oe)}o(Qe,"coordsCharInner");function bt(r,i,u,a,p,g,y){var S=pn(function(D){var V=p[D],$=V.level!=1;return Pe(Vn(r,ue(u,$?V.to:V.from,$?"before":"after"),"line",i,a),g,y,!0)},0,p.length-1),b=p[S];if(S>0){var E=b.level!=1,M=Vn(r,ue(u,E?b.from:b.to,E?"after":"before"),"line",i,a);Pe(M,g,y,!0)&&M.top>y&&(b=p[S-1])}return b}o(bt,"coordsBidiPart");function Mr(r,i,u,a,p,g,y){var S=re(r,i,a,y),b=S.begin,E=S.end;/\s/.test(i.text.charAt(E-1))&&E--;for(var M=null,D=null,V=0;V=E||$.to<=b)){var te=$.level!=1,oe=Ti(r,a,te?Math.min(E,$.to)-1:Math.max(b,$.from)).right,de=oede)&&(M=$,D=de)}}return M||(M=p[p.length-1]),M.fromE&&(M={from:M.from,to:E,level:M.level}),M}o(Mr,"coordsBidiPartWrapped");var mt;function Cs(r){if(r.cachedTextHeight!=null)return r.cachedTextHeight;if(mt==null){mt=_e("pre",null,"CodeMirror-line-like");for(var i=0;i<49;++i)mt.appendChild(document.createTextNode("x")),mt.appendChild(_e("br"));mt.appendChild(document.createTextNode("x"))}tt(r.measure,mt);var u=mt.offsetHeight/50;return u>3&&(r.cachedTextHeight=u),Ve(r.measure),u||1}o(Cs,"textHeight");function ua(r){if(r.cachedCharWidth!=null)return r.cachedCharWidth;var i=_e("span","xxxxxxxxxx"),u=_e("pre",[i],"CodeMirror-line-like");tt(r.measure,u);var a=i.getBoundingClientRect(),p=(a.right-a.left)/10;return p>2&&(r.cachedCharWidth=p),p||10}o(ua,"charWidth");function Kn(r){for(var i=r.display,u={},a={},p=i.gutters.clientLeft,g=i.gutters.firstChild,y=0;g;g=g.nextSibling,++y){var S=r.display.gutterSpecs[y].className;u[S]=g.offsetLeft+g.clientLeft+p,a[S]=g.clientWidth}return{fixedPos:fa(i),gutterTotalWidth:i.gutters.offsetWidth,gutterLeft:u,gutterWidth:a,wrapperWidth:i.wrapper.clientWidth}}o(Kn,"getDimensions");function fa(r){return r.scroller.getBoundingClientRect().left-r.sizer.getBoundingClientRect().left}o(fa,"compensateForHScroll");function Fm(r){var i=Cs(r.display),u=r.options.lineWrapping,a=u&&Math.max(5,r.display.scroller.clientWidth/ua(r.display)-3);return function(p){if(qt(r.doc,p))return 0;var g=0;if(p.widgets)for(var y=0;y0&&(E=Me(r.doc,b.line).text).length==b.ch){var M=_t(E,E.length,r.options.tabSize)-E.length;b=ue(b.line,Math.max(0,Math.round((g-sa(r.display).left)/ua(r.display))-M))}return b}o(po,"posFromMouse");function ho(r,i){if(i>=r.display.viewTo||(i-=r.display.viewFrom,i<0))return null;for(var u=r.display.view,a=0;ai)&&(p.updateLineNumbers=i),r.curOp.viewChanged=!0,i>=p.viewTo)_i&&jo(r.doc,i)p.viewFrom?Xo(r):(p.viewFrom+=a,p.viewTo+=a);else if(i<=p.viewFrom&&u>=p.viewTo)Xo(r);else if(i<=p.viewFrom){var g=fl(r,u,u+a,1);g?(p.view=p.view.slice(g.index),p.viewFrom=g.lineN,p.viewTo+=a):Xo(r)}else if(u>=p.viewTo){var y=fl(r,i,i,-1);y?(p.view=p.view.slice(0,y.index),p.viewTo=y.lineN):Xo(r)}else{var S=fl(r,i,i,-1),b=fl(r,u,u+a,1);S&&b?(p.view=p.view.slice(0,S.index).concat(qo(r,S.lineN,b.lineN)).concat(p.view.slice(b.index)),p.viewTo+=a):Xo(r)}var E=p.externalMeasured;E&&(u=p.lineN&&i=a.viewTo)){var g=a.view[ho(r,i)];if(g.node!=null){var y=g.changes||(g.changes=[]);ut(y,u)==-1&&y.push(u)}}}o(Es,"regLineChange");function Xo(r){r.display.viewFrom=r.display.viewTo=r.doc.first,r.display.view=[],r.display.viewOffset=0}o(Xo,"resetView");function fl(r,i,u,a){var p=ho(r,i),g,y=r.display.view;if(!_i||u==r.doc.first+r.doc.size)return{index:p,lineN:u};for(var S=r.display.viewFrom,b=0;b0){if(p==y.length-1)return null;g=S+y[p].size-i,p++}else g=S-i;i+=g,u+=g}for(;jo(r.doc,u)!=u;){if(p==(a<0?0:y.length-1))return null;u+=a*y[p-(a<0?1:0)].size,p+=a}return{index:p,lineN:u}}o(fl,"viewCuttingPoint");function Iy(r,i,u){var a=r.display,p=a.view;p.length==0||i>=a.viewTo||u<=a.viewFrom?(a.view=qo(r,i,u),a.viewFrom=i):(a.viewFrom>i?a.view=qo(r,i,a.viewFrom).concat(a.view):a.viewFromu&&(a.view=a.view.slice(0,ho(r,u)))),a.viewTo=u}o(Iy,"adjustView");function Bm(r){for(var i=r.display.view,u=0,a=0;a=r.display.viewTo||S.to().line0?i.blinker=setInterval(function(){r.hasFocus()||pl(r),i.cursorDiv.style.visibility=(u=!u)?"":"hidden"},r.options.cursorBlinkRate):r.options.cursorBlinkRate<0&&(i.cursorDiv.style.visibility="hidden")}}o(ca,"restartBlink");function vd(r){r.hasFocus()||(r.display.input.focus(),r.state.focused||pa(r))}o(vd,"ensureFocus");function hc(r){r.state.delayingBlurEvent=!0,setTimeout(function(){r.state.delayingBlurEvent&&(r.state.delayingBlurEvent=!1,r.state.focused&&pl(r))},100)}o(hc,"delayBlurEvent");function pa(r,i){r.state.delayingBlurEvent&&!r.state.draggingText&&(r.state.delayingBlurEvent=!1),r.options.readOnly!="nocursor"&&(r.state.focused||(Te(r,"focus",r,i),r.state.focused=!0,Xe(r.display.wrapper,"CodeMirror-focused"),!r.curOp&&r.display.selForContextMenu!=r.doc.sel&&(r.display.input.reset(),C&&setTimeout(function(){return r.display.input.reset(!0)},20)),r.display.input.receivedFocus()),ca(r))}o(pa,"onFocus");function pl(r,i){r.state.delayingBlurEvent||(r.state.focused&&(Te(r,"blur",r,i),r.state.focused=!1,xe(r.display.wrapper,"CodeMirror-focused")),clearInterval(r.display.blinker),setTimeout(function(){r.state.focused||(r.display.shift=!1)},150))}o(pl,"onBlur");function _s(r){for(var i=r.display,u=i.lineDiv.offsetTop,a=0;a.005||M<-.005)&&(ai(p.line,y),Ts(p.line),p.rest))for(var D=0;Dr.display.sizerWidth){var V=Math.ceil(S/ua(r.display));V>r.display.maxLineLength&&(r.display.maxLineLength=V,r.display.maxLine=p.line,r.display.maxLineChanged=!0)}}}}o(_s,"updateHeightsInViewport");function Ts(r){if(r.widgets)for(var i=0;i=y&&(g=ao(i,hn(Me(i,b))-r.wrapper.clientHeight),y=b)}return{from:g,to:Math.max(y,g+1)}}o(dl,"visibleLines");function Fy(r,i){if(!ir(r,"scrollCursorIntoView")){var u=r.display,a=u.sizer.getBoundingClientRect(),p=null;if(i.top+a.top<0?p=!0:i.bottom+a.top>(window.innerHeight||document.documentElement.clientHeight)&&(p=!1),p!=null&&!J){var g=_e("div","\u200B",null,`position: absolute; - top: `+(i.top-u.viewOffset-Ss(r.display))+`px; - height: `+(i.bottom-i.top+mn(r)+u.barHeight)+`px; - left: `+i.left+"px; width: "+Math.max(2,i.right-i.left)+"px;");r.display.lineSpace.appendChild(g),g.scrollIntoView(p),r.display.lineSpace.removeChild(g)}}}o(Fy,"maybeScrollWindow");function By(r,i,u,a){a==null&&(a=0);var p;!r.options.lineWrapping&&i==u&&(u=i.sticky=="before"?ue(i.line,i.ch+1,"before"):i,i=i.ch?ue(i.line,i.sticky=="before"?i.ch-1:i.ch,"after"):i);for(var g=0;g<5;g++){var y=!1,S=Vn(r,i),b=!u||u==i?S:Vn(r,u);p={left:Math.min(S.left,b.left),top:Math.min(S.top,b.top)-a,right:Math.max(S.left,b.left),bottom:Math.max(S.bottom,b.bottom)+a};var E=da(r,p),M=r.doc.scrollTop,D=r.doc.scrollLeft;if(E.scrollTop!=null&&(sr(r,E.scrollTop),Math.abs(r.doc.scrollTop-M)>1&&(y=!0)),E.scrollLeft!=null&&(hl(r,E.scrollLeft),Math.abs(r.doc.scrollLeft-D)>1&&(y=!0)),!y)break}return p}o(By,"scrollPosIntoView");function Hy(r,i){var u=da(r,i);u.scrollTop!=null&&sr(r,u.scrollTop),u.scrollLeft!=null&&hl(r,u.scrollLeft)}o(Hy,"scrollIntoView");function da(r,i){var u=r.display,a=Cs(r.display);i.top<0&&(i.top=0);var p=r.curOp&&r.curOp.scrollTop!=null?r.curOp.scrollTop:u.scroller.scrollTop,g=al(r),y={};i.bottom-i.top>g&&(i.bottom=i.top+g);var S=r.doc.height+zr(u),b=i.topS-a;if(i.topp+g){var M=Math.min(i.top,(E?S:i.bottom)-g);M!=p&&(y.scrollTop=M)}var D=r.options.fixedGutter?0:u.gutters.offsetWidth,V=r.curOp&&r.curOp.scrollLeft!=null?r.curOp.scrollLeft:u.scroller.scrollLeft-D,$=qi(r)-u.gutters.offsetWidth,te=i.right-i.left>$;return te&&(i.right=i.left+$),i.left<10?y.scrollLeft=0:i.left$+V-3&&(y.scrollLeft=i.right+(te?0:10)-$),y}o(da,"calculateScrollPos");function ha(r,i){i!=null&&(mc(r),r.curOp.scrollTop=(r.curOp.scrollTop==null?r.doc.scrollTop:r.curOp.scrollTop)+i)}o(ha,"addToScrollTop");function ks(r){mc(r);var i=r.getCursor();r.curOp.scrollToPos={from:i,to:i,margin:r.options.cursorScrollMargin}}o(ks,"ensureCursorVisible");function qu(r,i,u){(i!=null||u!=null)&&mc(r),i!=null&&(r.curOp.scrollLeft=i),u!=null&&(r.curOp.scrollTop=u)}o(qu,"scrollToCoords");function Wm(r,i){mc(r),r.curOp.scrollToPos=i}o(Wm,"scrollToRange");function mc(r){var i=r.curOp.scrollToPos;if(i){r.curOp.scrollToPos=null;var u=zu(r,i.from),a=zu(r,i.to);Um(r,u,a,i.margin)}}o(mc,"resolveScrollToPos");function Um(r,i,u,a){var p=da(r,{left:Math.min(i.left,u.left),top:Math.min(i.top,u.top)-a,right:Math.max(i.right,u.right),bottom:Math.max(i.bottom,u.bottom)+a});qu(r,p.scrollLeft,p.scrollTop)}o(Um,"scrollToCoordsRange");function sr(r,i){Math.abs(r.doc.scrollTop-i)<2||(n||wd(r,{top:i}),Zr(r,i,!0),n&&wd(r),go(r,100))}o(sr,"updateScrollTop");function Zr(r,i,u){i=Math.max(0,Math.min(r.display.scroller.scrollHeight-r.display.scroller.clientHeight,i)),!(r.display.scroller.scrollTop==i&&!u)&&(r.doc.scrollTop=i,r.display.scrollbars.setScrollTop(i),r.display.scroller.scrollTop!=i&&(r.display.scroller.scrollTop=i))}o(Zr,"setScrollTop");function hl(r,i,u,a){i=Math.max(0,Math.min(i,r.display.scroller.scrollWidth-r.display.scroller.clientWidth)),!((u?i==r.doc.scrollLeft:Math.abs(r.doc.scrollLeft-i)<2)&&!a)&&(r.doc.scrollLeft=i,zm(r),r.display.scroller.scrollLeft!=i&&(r.display.scroller.scrollLeft=i),r.display.scrollbars.setScrollLeft(i))}o(hl,"setScrollLeft");function Vu(r){var i=r.display,u=i.gutters.offsetWidth,a=Math.round(r.doc.height+zr(r.display));return{clientHeight:i.scroller.clientHeight,viewHeight:i.wrapper.clientHeight,scrollWidth:i.scroller.scrollWidth,clientWidth:i.scroller.clientWidth,viewWidth:i.wrapper.clientWidth,barLeft:r.options.fixedGutter?u:0,docHeight:a,scrollHeight:a+mn(r)+i.barHeight,nativeBarWidth:i.nativeBarWidth,gutterWidth:u}}o(Vu,"measureForScrollbars");var Ns=o(function(r,i,u){this.cm=u;var a=this.vert=_e("div",[_e("div",null,null,"min-width: 1px")],"CodeMirror-vscrollbar"),p=this.horiz=_e("div",[_e("div",null,null,"height: 100%; min-height: 1px")],"CodeMirror-hscrollbar");a.tabIndex=p.tabIndex=-1,r(a),r(p),H(a,"scroll",function(){a.clientHeight&&i(a.scrollTop,"vertical")}),H(p,"scroll",function(){p.clientWidth&&i(p.scrollLeft,"horizontal")}),this.checkedZeroWidth=!1,c&&v<8&&(this.horiz.style.minHeight=this.vert.style.minWidth="18px")},"NativeScrollbars");Ns.prototype.update=function(r){var i=r.scrollWidth>r.clientWidth+1,u=r.scrollHeight>r.clientHeight+1,a=r.nativeBarWidth;if(u){this.vert.style.display="block",this.vert.style.bottom=i?a+"px":"0";var p=r.viewHeight-(i?a:0);this.vert.firstChild.style.height=Math.max(0,r.scrollHeight-r.clientHeight+p)+"px"}else this.vert.style.display="",this.vert.firstChild.style.height="0";if(i){this.horiz.style.display="block",this.horiz.style.right=u?a+"px":"0",this.horiz.style.left=r.barLeft+"px";var g=r.viewWidth-r.barLeft-(u?a:0);this.horiz.firstChild.style.width=Math.max(0,r.scrollWidth-r.clientWidth+g)+"px"}else this.horiz.style.display="",this.horiz.firstChild.style.width="0";return!this.checkedZeroWidth&&r.clientHeight>0&&(a==0&&this.zeroWidthHack(),this.checkedZeroWidth=!0),{right:u?a:0,bottom:i?a:0}},Ns.prototype.setScrollLeft=function(r){this.horiz.scrollLeft!=r&&(this.horiz.scrollLeft=r),this.disableHoriz&&this.enableZeroWidthBar(this.horiz,this.disableHoriz,"horiz")},Ns.prototype.setScrollTop=function(r){this.vert.scrollTop!=r&&(this.vert.scrollTop=r),this.disableVert&&this.enableZeroWidthBar(this.vert,this.disableVert,"vert")},Ns.prototype.zeroWidthHack=function(){var r=I&&!X?"12px":"18px";this.horiz.style.height=this.vert.style.width=r,this.horiz.style.pointerEvents=this.vert.style.pointerEvents="none",this.disableHoriz=new Ct,this.disableVert=new Ct},Ns.prototype.enableZeroWidthBar=function(r,i,u){r.style.pointerEvents="auto";function a(){var p=r.getBoundingClientRect(),g=u=="vert"?document.elementFromPoint(p.right-1,(p.top+p.bottom)/2):document.elementFromPoint((p.right+p.left)/2,p.bottom-1);g!=r?r.style.pointerEvents="none":i.set(1e3,a)}o(a,"maybeDisable"),i.set(1e3,a)},Ns.prototype.clear=function(){var r=this.horiz.parentNode;r.removeChild(this.horiz),r.removeChild(this.vert)};var Ku=o(function(){},"NullScrollbars");Ku.prototype.update=function(){return{bottom:0,right:0}},Ku.prototype.setScrollLeft=function(){},Ku.prototype.setScrollTop=function(){},Ku.prototype.clear=function(){};function Ls(r,i){i||(i=Vu(r));var u=r.display.barWidth,a=r.display.barHeight;ma(r,i);for(var p=0;p<4&&u!=r.display.barWidth||a!=r.display.barHeight;p++)u!=r.display.barWidth&&r.options.lineWrapping&&_s(r),ma(r,Vu(r)),u=r.display.barWidth,a=r.display.barHeight}o(Ls,"updateScrollbars");function ma(r,i){var u=r.display,a=u.scrollbars.update(i);u.sizer.style.paddingRight=(u.barWidth=a.right)+"px",u.sizer.style.paddingBottom=(u.barHeight=a.bottom)+"px",u.heightForcer.style.borderBottom=a.bottom+"px solid transparent",a.right&&a.bottom?(u.scrollbarFiller.style.display="block",u.scrollbarFiller.style.height=a.bottom+"px",u.scrollbarFiller.style.width=a.right+"px"):u.scrollbarFiller.style.display="",a.bottom&&r.options.coverGutterNextToScrollbar&&r.options.fixedGutter?(u.gutterFiller.style.display="block",u.gutterFiller.style.height=a.bottom+"px",u.gutterFiller.style.width=i.gutterWidth+"px"):u.gutterFiller.style.display=""}o(ma,"updateScrollbarsInner");var gc={native:Ns,null:Ku};function ml(r){r.display.scrollbars&&(r.display.scrollbars.clear(),r.display.scrollbars.addClass&&xe(r.display.wrapper,r.display.scrollbars.addClass)),r.display.scrollbars=new gc[r.options.scrollbarStyle](function(i){r.display.wrapper.insertBefore(i,r.display.scrollbarFiller),H(i,"mousedown",function(){r.state.focused&&setTimeout(function(){return r.display.input.focus()},0)}),i.setAttribute("cm-not-content","true")},function(i,u){u=="horizontal"?hl(r,i):sr(r,i)},r),r.display.scrollbars.addClass&&Xe(r.display.wrapper,r.display.scrollbars.addClass)}o(ml,"initScrollbars");var Gu=0;function Ki(r){r.curOp={cm:r,viewChanged:!1,startHeight:r.doc.height,forceUpdate:!1,updateInput:0,typing:!1,changeObjs:null,cursorActivityHandlers:null,cursorActivityCalled:0,selectionChanged:!1,updateMaxLine:!1,scrollLeft:null,scrollTop:null,scrollToPos:null,focus:!1,id:++Gu,markArrays:null},$i(r.curOp)}o(Ki,"startOperation");function mo(r){var i=r.curOp;i&&hd(i,function(u){for(var a=0;a=u.viewTo)||u.maxLineChanged&&i.options.lineWrapping,r.update=r.mustUpdate&&new An(i,r.mustUpdate&&{top:r.scrollTop,ensure:r.scrollToPos},r.forceUpdate)}o(Wy,"endOperation_R1");function Uy(r){r.updatedDisplay=r.mustUpdate&&yd(r.cm,r.update)}o(Uy,"endOperation_W1");function zy(r){var i=r.cm,u=i.display;r.updatedDisplay&&_s(i),r.barMeasure=Vu(i),u.maxLineChanged&&!i.options.lineWrapping&&(r.adjustWidthTo=Uu(i,u.maxLine,u.maxLine.text.length).left+3,i.display.sizerWidth=r.adjustWidthTo,r.barMeasure.scrollWidth=Math.max(u.scroller.clientWidth,u.sizer.offsetLeft+r.adjustWidthTo+mn(i)+i.display.barWidth),r.maxScrollLeft=Math.max(0,u.sizer.offsetLeft+r.adjustWidthTo-qi(i))),(r.updatedDisplay||r.selectionChanged)&&(r.preparedSelection=u.input.prepareSelection())}o(zy,"endOperation_R2");function $y(r){var i=r.cm;r.adjustWidthTo!=null&&(i.display.sizer.style.minWidth=r.adjustWidthTo+"px",r.maxScrollLeft=r.display.viewTo)){var u=+new Date+r.options.workTime,a=$o(r,i.highlightFrontier),p=[];i.iter(a.line,Math.min(i.first+i.size,r.display.viewTo+500),function(g){if(a.line>=r.display.viewFrom){var y=g.styles,S=g.text.length>r.options.maxHighlightLength?Uo(i.mode,a.state):null,b=tc(r,g,a,!0);S&&(a.state=S),g.styles=b.styles;var E=g.styleClasses,M=b.classes;M?g.styleClasses=M:E&&(g.styleClasses=null);for(var D=!y||y.length!=g.styles.length||E!=M&&(!E||!M||E.bgClass!=M.bgClass||E.textClass!=M.textClass),V=0;!D&&Vu)return go(r,r.options.workDelay),!0}),i.highlightFrontier=a.line,i.modeFrontier=Math.max(i.modeFrontier,a.line),p.length&&Jr(r,function(){for(var g=0;g=u.viewFrom&&i.visible.to<=u.viewTo&&(u.updateLineNumbers==null||u.updateLineNumbers>=u.viewTo)&&u.renderedView==u.view&&Bm(r)==0)return!1;vo(r)&&(Xo(r),i.dims=Kn(r));var p=a.first+a.size,g=Math.max(i.visible.from-r.options.viewportMargin,a.first),y=Math.min(p,i.visible.to+r.options.viewportMargin);u.viewFromy&&u.viewTo-y<20&&(y=Math.min(p,u.viewTo)),_i&&(g=jo(r.doc,g),y=pd(r.doc,y));var S=g!=u.viewFrom||y!=u.viewTo||u.lastWrapHeight!=i.wrapperHeight||u.lastWrapWidth!=i.wrapperWidth;Iy(r,g,y),u.viewOffset=hn(Me(r.doc,u.viewFrom)),r.display.mover.style.top=u.viewOffset+"px";var b=Bm(r);if(!S&&b==0&&!i.force&&u.renderedView==u.view&&(u.updateLineNumbers==null||u.updateLineNumbers>=u.viewTo))return!1;var E=qy(r);return b>4&&(u.lineDiv.style.display="none"),Ky(r,u.updateLineNumbers,i.dims),b>4&&(u.lineDiv.style.display=""),u.renderedView=u.view,Vy(E),Ve(u.cursorDiv),Ve(u.selectionDiv),u.gutters.style.height=u.sizer.style.minHeight=0,S&&(u.lastWrapHeight=i.wrapperHeight,u.lastWrapWidth=i.wrapperWidth,go(r,400)),u.updateLineNumbers=null,!0}o(yd,"updateDisplayIfNeeded");function Ps(r,i){for(var u=i.viewport,a=!0;;a=!1){if(!a||!r.options.lineWrapping||i.oldDisplayWidth==qi(r)){if(u&&u.top!=null&&(u={top:Math.min(r.doc.height+zr(r.display)-al(r),u.top)}),i.visible=dl(r.display,r.doc,u),i.visible.from>=r.display.viewFrom&&i.visible.to<=r.display.viewTo)break}else a&&(i.visible=dl(r.display,r.doc,u));if(!yd(r,i))break;_s(r);var p=Vu(r);$u(r),Ls(r,p),Sd(r,p),i.force=!1}i.signal(r,"update",r),(r.display.viewFrom!=r.display.reportedViewFrom||r.display.viewTo!=r.display.reportedViewTo)&&(i.signal(r,"viewportChange",r,r.display.viewFrom,r.display.viewTo),r.display.reportedViewFrom=r.display.viewFrom,r.display.reportedViewTo=r.display.viewTo)}o(Ps,"postUpdateDisplay");function wd(r,i){var u=new An(r,i);if(yd(r,u)){_s(r),Ps(r,u);var a=Vu(r);$u(r),Ls(r,a),Sd(r,a),u.finish()}}o(wd,"updateDisplaySimple");function Ky(r,i,u){var a=r.display,p=r.options.lineNumbers,g=a.lineDiv,y=g.firstChild;function S(te){var oe=te.nextSibling;return C&&I&&r.display.currentWheelTarget==te?te.style.display="none":te.parentNode.removeChild(te),oe}o(S,"rm");for(var b=a.view,E=a.viewFrom,M=0;M-1&&($=!1),Du(r,D,E,u)),$&&(Ve(D.lineNumber),D.lineNumber.appendChild(document.createTextNode(jl(r.options,E)))),y=D.node.nextSibling}E+=D.size}for(;y;)y=S(y)}o(Ky,"patchDisplay");function xd(r){var i=r.gutters.offsetWidth;r.sizer.style.marginLeft=i+"px",yr(r,"gutterChanged",r)}o(xd,"updateGutterSpace");function Sd(r,i){r.display.sizer.style.minHeight=i.docHeight+"px",r.display.heightForcer.style.top=i.docHeight+"px",r.display.gutters.style.height=i.docHeight+r.display.barHeight+mn(r)+"px"}o(Sd,"setDocumentHeight");function zm(r){var i=r.display,u=i.view;if(!(!i.alignWidgets&&(!i.gutters.firstChild||!r.options.fixedGutter))){for(var a=fa(i)-i.scroller.scrollLeft+r.doc.scrollLeft,p=i.gutters.offsetWidth,g=a+"px",y=0;yy.clientWidth,b=y.scrollHeight>y.clientHeight;if(!!(a&&S||p&&b)){if(p&&I&&C){e:for(var E=i.target,M=g.view;E!=y;E=E.parentNode)for(var D=0;D=0&&ze(r,a.to())<=0)return u}return-1};var wt=o(function(r,i){this.anchor=r,this.head=i},"Range");wt.prototype.from=function(){return il(this.anchor,this.head)},wt.prototype.to=function(){return gs(this.anchor,this.head)},wt.prototype.empty=function(){return this.head.line==this.anchor.line&&this.head.ch==this.anchor.ch};function tn(r,i,u){var a=r&&r.options.selectionsMayTouch,p=i[u];i.sort(function(V,$){return ze(V.from(),$.from())}),u=ut(i,p);for(var g=1;g0:b>=0){var E=il(S.from(),y.from()),M=gs(S.to(),y.to()),D=S.empty()?y.from()==y.head:S.from()==S.head;g<=u&&--u,i.splice(--g,2,new wt(D?M:E,D?E:M))}}return new pi(i,u)}o(tn,"normalizeSelection");function Os(r,i){return new pi([new wt(r,i||r)],0)}o(Os,"simpleSelection");function Ms(r){return r.text?ue(r.from.line+r.text.length-1,Se(r.text).length+(r.text.length==1?r.from.ch:0)):r.to}o(Ms,"changeEnd");function Ni(r,i){if(ze(r,i.from)<0)return r;if(ze(r,i.to)<=0)return Ms(i);var u=r.line+i.text.length-(i.to.line-i.from.line)-1,a=r.ch;return r.line==i.to.line&&(a+=Ms(i).ch-i.to.ch),ue(u,a)}o(Ni,"adjustForChange");function bd(r,i){for(var u=[],a=0;a1&&r.remove(S.line+1,te-1),r.insert(S.line+1,ge)}yr(r,"change",r,i)}o(wc,"updateDoc");function As(r,i,u){function a(p,g,y){if(p.linked)for(var S=0;S1&&!r.done[r.done.length-2].ranges)return r.done.pop(),Se(r.done)}o(Qy,"lastChangeEvent");function yo(r,i,u,a){var p=r.history;p.undone.length=0;var g=+new Date,y,S;if((p.lastOp==a||p.lastOrigin==i.origin&&i.origin&&(i.origin.charAt(0)=="+"&&p.lastModTime>g-(r.cm?r.cm.options.historyEventDelay:500)||i.origin.charAt(0)=="*"))&&(y=Qy(p,p.lastOp==a)))S=Se(y.changes),ze(i.from,i.to)==0&&ze(i.from,S.to)==0?S.to=Ms(i):y.changes.push(Td(r,i));else{var b=Se(p.done);for((!b||!b.ranges)&&Dn(r.sel,p.done),y={changes:[Td(r,i)],generation:p.generation},p.done.push(y);p.done.length>p.undoDepth;)p.done.shift(),p.done[0].ranges||p.done.shift()}p.done.push(u),p.generation=++p.maxGeneration,p.lastModTime=p.lastSelTime=g,p.lastOp=p.lastSelOp=a,p.lastOrigin=p.lastSelOrigin=i.origin,S||Te(r,"historyAdded")}o(yo,"addChangeToHistory");function Nd(r,i,u,a){var p=i.charAt(0);return p=="*"||p=="+"&&u.ranges.length==a.ranges.length&&u.somethingSelected()==a.somethingSelected()&&new Date-r.history.lastSelTime<=(r.cm?r.cm.options.historyEventDelay:500)}o(Nd,"selectionEventCanBeMerged");function vl(r,i,u,a){var p=r.history,g=a&&a.origin;u==p.lastSelOp||g&&p.lastSelOrigin==g&&(p.lastModTime==p.lastSelTime&&p.lastOrigin==g||Nd(r,g,Se(p.done),i))?p.done[p.done.length-1]=i:Dn(i,p.done),p.lastSelTime=+new Date,p.lastSelOrigin=g,p.lastSelOp=u,a&&a.clearRedo!==!1&&kd(p.undone)}o(vl,"addSelectionToHistory");function Dn(r,i){var u=Se(i);u&&u.ranges&&u.equals(r)||i.push(r)}o(Dn,"pushSelectionToHistory");function Gm(r,i,u,a){var p=i["spans_"+r.id],g=0;r.iter(Math.max(r.first,u),Math.min(r.first+r.size,a),function(y){y.markedSpans&&((p||(p=i["spans_"+r.id]={}))[g]=y.markedSpans),++g})}o(Gm,"attachLocalSpans");function Ym(r){if(!r)return null;for(var i,u=0;u-1&&(Se(S)[D]=E[D],delete E[D])}}return a}o(di,"copyHistoryArray");function Sc(r,i,u,a){if(a){var p=r.anchor;if(u){var g=ze(i,p)<0;g!=ze(u,p)<0?(p=i,i=u):g!=ze(i,u)<0&&(i=u)}return new wt(p,i)}else return new wt(u||i,i)}o(Sc,"extendRange");function Cc(r,i,u,a,p){p==null&&(p=r.cm&&(r.cm.display.shift||r.extend)),$r(r,new pi([Sc(r.sel.primary(),i,u,p)],0),a)}o(Cc,"extendSelection");function Zu(r,i,u){for(var a=[],p=r.cm&&(r.cm.display.shift||r.extend),g=0;g=i.ch:S.to>i.ch))){if(p&&(Te(b,"beforeCursorEnter"),b.explicitlyCleared))if(g.markedSpans){--y;continue}else break;if(!b.atomic)continue;if(u){var D=b.find(a<0?1:-1),V=void 0;if((a<0?M:E)&&(D=_c(r,D,-a,D&&D.line==i.line?g:null)),D&&D.line==i.line&&(V=ze(D,u))&&(a<0?V<0:V>0))return yl(r,D,i,a,p)}var $=b.find(a<0?-1:1);return(a<0?E:M)&&($=_c(r,$,a,$.line==i.line?g:null)),$?yl(r,$,i,a,p):null}}return i}o(yl,"skipAtomicInner");function jr(r,i,u,a,p){var g=a||1,y=yl(r,i,u,g,p)||!p&&yl(r,i,u,g,!0)||yl(r,i,u,-g,p)||!p&&yl(r,i,u,-g,!0);return y||(r.cantEdit=!0,ue(r.first,0))}o(jr,"skipAtomic");function _c(r,i,u,a){return u<0&&i.ch==0?i.line>r.first?He(r,ue(i.line-1)):null:u>0&&i.ch==(a||Me(r,i.line)).text.length?i.line=0;--p)Tc(r,{from:a[p].from,to:a[p].to,text:p?[""]:i.text,origin:i.origin});else Tc(r,i)}}o(ya,"makeChange");function Tc(r,i){if(!(i.text.length==1&&i.text[0]==""&&ze(i.from,i.to)==0)){var u=bd(r,i);yo(r,i,u,r.cm?r.cm.curOp.id:NaN),xa(r,i,u,ku(r,i));var a=[];As(r,function(p,g){!g&&ut(a,p.history)==-1&&(eg(p.history,i),a.push(p.history)),xa(p,i,null,ku(p,i))})}}o(Tc,"makeChangeInner");function kc(r,i,u){var a=r.cm&&r.cm.state.suppressEdits;if(!(a&&!u)){for(var p=r.history,g,y=r.sel,S=i=="undo"?p.done:p.undone,b=i=="undo"?p.undone:p.done,E=0;E=0;--$){var te=V($);if(te)return te.v}}}}o(kc,"makeChangeFromHistory");function wa(r,i){if(i!=0&&(r.first+=i,r.sel=new pi(Or(r.sel.ranges,function(p){return new wt(ue(p.anchor.line+i,p.anchor.ch),ue(p.head.line+i,p.head.ch))}),r.sel.primIndex),r.cm)){et(r.cm,r.first,r.first-i,i);for(var u=r.cm.display,a=u.viewFrom;ar.lastLine())){if(i.from.lineg&&(i={from:i.from,to:ue(g,Me(r,g).text.length),text:[i.text[0]],origin:i.origin}),i.removed=Ei(r,i.from,i.to),u||(u=bd(r,i)),r.cm?Zy(r.cm,i,a):wc(r,i,a),hi(r,u,$t),r.cantEdit&&jr(r,ue(r.firstLine(),0))&&(r.cantEdit=!1)}}o(xa,"makeChangeSingleDoc");function Zy(r,i,u){var a=r.doc,p=r.display,g=i.from,y=i.to,S=!1,b=g.line;r.options.lineWrapping||(b=vt(vr(Me(a,g.line))),a.iter(b,y.line+1,function($){if($==p.maxLine)return S=!0,!0})),a.sel.contains(i.from,i.to)>-1&&Ul(r),wc(a,i,u,Fm(r)),r.options.lineWrapping||(a.iter(b,g.line+i.text.length,function($){var te=ws($);te>p.maxLineLength&&(p.maxLine=$,p.maxLineLength=te,p.maxLineChanged=!0,S=!1)}),S&&(r.curOp.updateMaxLine=!0)),rc(a,g.line),go(r,400);var E=i.text.length-(y.line-g.line)-1;i.full?et(r):g.line==y.line&&i.text.length==1&&!_d(r.doc,i)?Es(r,g.line,"text"):et(r,g.line,y.line+1,E);var M=Ft(r,"changes"),D=Ft(r,"change");if(D||M){var V={from:g,to:y,text:i.text,removed:i.removed,origin:i.origin};D&&yr(r,"change",r,V),M&&(r.curOp.changeObjs||(r.curOp.changeObjs=[])).push(V)}r.display.selForContextMenu=null}o(Zy,"makeChangeSingleDocInEditor");function Sa(r,i,u,a,p){var g;a||(a=u),ze(a,u)<0&&(g=[a,u],u=g[0],a=g[1]),typeof i=="string"&&(i=r.splitLines(i)),ya(r,{from:u,to:a,text:i,origin:p})}o(Sa,"replaceRange");function Ca(r,i,u,a){u1||!(this.children[0]instanceof ba))){var S=[];this.collapse(S),this.children=[new ba(S)],this.children[0].parent=this}},collapse:function(r){for(var i=0;i50){for(var y=p.lines.length%25+25,S=y;S10);r.parent.maybeSpill()}},iterN:function(r,i,u){for(var a=0;ar.display.maxLineLength&&(r.display.maxLine=E,r.display.maxLineLength=M,r.display.maxLineChanged=!0)}a!=null&&r&&this.collapsed&&et(r,a,p+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,r&&Ju(r.doc)),r&&yr(r,"markerCleared",r,this,a,p),i&&mo(r),this.parent&&this.parent.clear()}},Rs.prototype.find=function(r,i){r==null&&this.type=="bookmark"&&(r=1);for(var u,a,p=0;p0||y==0&&g.clearWhenEmpty!==!1)return g;if(g.replacedWith&&(g.collapsed=!0,g.widgetNode=St("span",[g.replacedWith],"CodeMirror-widget"),a.handleMouseEvents||g.widgetNode.setAttribute("cm-ignore-events","true"),a.insertLeft&&(g.widgetNode.insertLeft=!0)),g.collapsed){if(sl(r,i.line,i,u,g)||i.line!=u.line&&sl(r,u.line,i,u,g))throw new Error("Inserting collapsed marker partially overlapping an existing one");Ql()}g.addToHistory&&yo(r,{from:i,to:u,origin:"markText"},r.sel,NaN);var S=i.line,b=r.cm,E;if(r.iter(S,u.line+1,function(D){b&&g.collapsed&&!b.options.lineWrapping&&vr(D)==b.display.maxLine&&(E=!0),g.collapsed&&S!=i.line&&ai(D,0),oc(D,new co(g,S==i.line?i.ch:null,S==u.line?u.ch:null),r.cm&&r.cm.curOp),++S}),g.collapsed&&r.iter(i.line,u.line+1,function(D){qt(r,D)&&ai(D,0)}),g.clearOnEnter&&H(g,"beforeCursorEnter",function(){return g.clear()}),g.readOnly&&(nc(),(r.history.done.length||r.history.undone.length)&&r.clearHistory()),g.collapsed&&(g.id=++Nc,g.atomic=!0),b){if(E&&(b.curOp.updateMaxLine=!0),g.collapsed)et(b,i.line,u.line+1);else if(g.className||g.startStyle||g.endStyle||g.css||g.attributes||g.title)for(var M=i.line;M<=u.line;M++)Es(b,M,"text");g.atomic&&Ju(b.doc),yr(b,"markerAdded",b,g)}return g}o(Is,"markText");var Ea=o(function(r,i){this.markers=r,this.primary=i;for(var u=0;u=0;b--)ya(this,a[b]);S?bc(this,S):this.cm&&ks(this.cm)}),undo:N(function(){kc(this,"undo")}),redo:N(function(){kc(this,"redo")}),undoSelection:N(function(){kc(this,"undo",!0)}),redoSelection:N(function(){kc(this,"redo",!0)}),setExtending:function(r){this.extend=r},getExtending:function(){return this.extend},historySize:function(){for(var r=this.history,i=0,u=0,a=0;a=r.ch)&&i.push(p.marker.parent||p.marker)}return i},findMarks:function(r,i,u){r=He(this,r),i=He(this,i);var a=[],p=r.line;return this.iter(r.line,i.line+1,function(g){var y=g.markedSpans;if(y)for(var S=0;S=b.to||b.from==null&&p!=r.line||b.from!=null&&p==i.line&&b.from>=i.ch)&&(!u||u(b.marker))&&a.push(b.marker.parent||b.marker)}++p}),a},getAllMarks:function(){var r=[];return this.iter(function(i){var u=i.markedSpans;if(u)for(var a=0;ar)return i=r,!0;r-=g,++u}),He(this,ue(u,i))},indexFromPos:function(r){r=He(this,r);var i=r.ch;if(r.linei&&(i=r.from),r.to!=null&&r.to-1){i.state.draggingText(r),setTimeout(function(){return i.display.input.focus()},20);return}try{var M=r.dataTransfer.getData("Text");if(M){var D;if(i.state.draggingText&&!i.state.draggingText.copy&&(D=i.listSelections()),hi(i.doc,Os(u,u)),D)for(var V=0;V=0;S--)Sa(r.doc,"",a[S].from,a[S].to,"+delete");ks(r)})}o(mi,"deleteNearSelection");function of(r,i,u){var a=Ui(r.text,i+u,u);return a<0||a>r.text.length?null:a}o(of,"moveCharLogically");function Mc(r,i,u){var a=of(r,i.ch,u);return a==null?null:new ue(i.line,a,u<0?"after":"before")}o(Mc,"moveLogically");function _a(r,i,u,a,p){if(r){i.doc.direction=="rtl"&&(p=-p);var g=Mn(u,i.doc.direction);if(g){var y=p<0?Se(g):g[0],S=p<0==(y.level==1),b=S?"after":"before",E;if(y.level>0||i.doc.direction=="rtl"){var M=qn(i,u);E=p<0?u.text.length-1:0;var D=Ti(i,M,E).top;E=pn(function(V){return Ti(i,M,V).top==D},p<0==(y.level==1)?y.from:y.to-1,E),b=="before"&&(E=of(u,E,1))}else E=p<0?y.to:y.from;return new ue(a,E,b)}}return new ue(a,p<0?u.text.length:0,p<0?"before":"after")}o(_a,"endOfLine");function ag(r,i,u,a){var p=Mn(i,r.doc.direction);if(!p)return Mc(i,u,a);u.ch>=i.text.length?(u.ch=i.text.length,u.sticky="before"):u.ch<=0&&(u.ch=0,u.sticky="after");var g=Ci(p,u.ch,u.sticky),y=p[g];if(r.doc.direction=="ltr"&&y.level%2==0&&(a>0?y.to>u.ch:y.from=y.from&&V>=M.begin)){var $=D?"before":"after";return new ue(u.line,V,$)}}var te=o(function(ge,be,ve){for(var Ne=o(function(Tt,br){return br?new ue(u.line,S(Tt,1),"before"):new ue(u.line,Tt,"after")},"getRes");ge>=0&&ge0==(Ie.level!=1),it=De?ve.begin:S(ve.end,-1);if(Ie.from<=it&&it0?M.end:S(M.begin,-1);return de!=null&&!(a>0&&de==i.text.length)&&(oe=te(a>0?0:p.length-1,a,E(de)),oe)?oe:null}o(ag,"moveVisually");var xl={selectAll:Qm,singleSelection:function(r){return r.setSelection(r.getCursor("anchor"),r.getCursor("head"),$t)},killLine:function(r){return mi(r,function(i){if(i.empty()){var u=Me(r.doc,i.head.line).text.length;return i.head.ch==u&&i.head.line0)p=new ue(p.line,p.ch+1),r.replaceRange(g.charAt(p.ch-1)+g.charAt(p.ch-2),ue(p.line,p.ch-2),p,"+transpose");else if(p.line>r.doc.first){var y=Me(r.doc,p.line-1).text;y&&(p=new ue(p.line,1),r.replaceRange(g.charAt(0)+r.doc.lineSeparator()+y.charAt(y.length-1),ue(p.line-1,y.length-1),p,"+transpose"))}}u.push(new wt(p,p))}r.setSelections(u)})},newlineAndIndent:function(r){return Jr(r,function(){for(var i=r.listSelections(),u=i.length-1;u>=0;u--)r.replaceRange(r.doc.lineSeparator(),i[u].anchor,i[u].head,"+input");i=r.listSelections();for(var a=0;ar&&ze(i,this.pos)==0&&u==this.button};var qr,Gn;function ow(r,i){var u=+new Date;return Gn&&Gn.compare(u,r,i)?(qr=Gn=null,"triple"):qr&&qr.compare(u,r,i)?(Gn=new Rc(u,r,i),qr=null,"double"):(qr=new Rc(u,r,i),Gn=null,"single")}o(ow,"clickRepeat");function dg(r){var i=this,u=i.display;if(!(ir(i,r)||u.activeTouch&&u.input.supportsTouch())){if(u.input.ensurePolled(),u.shift=r.shiftKey,ji(u,r)){C||(u.scroller.draggable=!1,setTimeout(function(){return u.scroller.draggable=!0},100));return}if(!zd(i,r)){var a=po(i,r),p=el(r),g=a?ow(a,p):"single";window.focus(),p==1&&i.state.selectingText&&i.state.selectingText(r),!(a&&Ic(i,p,a,g,r))&&(p==1?a?hg(i,a,g,r):bi(r)==u.scroller&&or(r):p==2?(a&&Cc(i.doc,a),setTimeout(function(){return u.input.focus()},20)):p==3&&(pe?i.display.input.onContextMenu(r):hc(i)))}}}o(dg,"onMouseDown");function Ic(r,i,u,a,p){var g="Click";return a=="double"?g="Double"+g:a=="triple"&&(g="Triple"+g),g=(i==1?"Left":i==2?"Middle":"Right")+g,Ta(r,Rd(g,p),p,function(y){if(typeof y=="string"&&(y=xl[y]),!y)return!1;var S=!1;try{r.isReadOnly()&&(r.state.suppressEdits=!0),S=y(r,u)!=zt}finally{r.state.suppressEdits=!1}return S})}o(Ic,"handleMappedButton");function ka(r,i,u){var a=r.getOption("configureMouse"),p=a?a(r,i,u):{};if(p.unit==null){var g=G?u.shiftKey&&u.metaKey:u.altKey;p.unit=g?"rectangle":i=="single"?"char":i=="double"?"word":"line"}return(p.extend==null||r.doc.extend)&&(p.extend=r.doc.extend||u.shiftKey),p.addNew==null&&(p.addNew=I?u.metaKey:u.ctrlKey),p.moveOnDrag==null&&(p.moveOnDrag=!(I?u.altKey:u.ctrlKey)),p}o(ka,"configureMouse");function hg(r,i,u,a){c?setTimeout(Hr(vd,r),0):r.curOp.focus=Ge();var p=ka(r,u,a),g=r.doc.sel,y;r.options.dragDrop&&hs&&!r.isReadOnly()&&u=="single"&&(y=g.contains(i))>-1&&(ze((y=g.ranges[y]).from(),i)<0||i.xRel>0)&&(ze(y.to(),i)>0||i.xRel<0)?mg(r,a,i,p):vg(r,a,i,p)}o(hg,"leftButtonDown");function mg(r,i,u,a){var p=r.display,g=!1,y=lr(r,function(E){C&&(p.scroller.draggable=!1),r.state.draggingText=!1,r.state.delayingBlurEvent&&(r.hasFocus()?r.state.delayingBlurEvent=!1:hc(r)),he(p.wrapper.ownerDocument,"mouseup",y),he(p.wrapper.ownerDocument,"mousemove",S),he(p.scroller,"dragstart",b),he(p.scroller,"drop",y),g||(or(E),a.addNew||Cc(r.doc,u,null,null,a.extend),C&&!B||c&&v==9?setTimeout(function(){p.wrapper.ownerDocument.body.focus({preventScroll:!0}),p.input.focus()},20):p.input.focus())}),S=o(function(E){g=g||Math.abs(i.clientX-E.clientX)+Math.abs(i.clientY-E.clientY)>=10},"mouseMove"),b=o(function(){return g=!0},"dragStart");C&&(p.scroller.draggable=!0),r.state.draggingText=y,y.copy=!a.moveOnDrag,H(p.wrapper.ownerDocument,"mouseup",y),H(p.wrapper.ownerDocument,"mousemove",S),H(p.scroller,"dragstart",b),H(p.scroller,"drop",y),r.state.delayingBlurEvent=!0,setTimeout(function(){return p.input.focus()},20),p.scroller.dragDrop&&p.scroller.dragDrop()}o(mg,"leftButtonStartDrag");function gg(r,i,u){if(u=="char")return new wt(i,i);if(u=="word")return r.findWordAt(i);if(u=="line")return new wt(ue(i.line,0),He(r.doc,ue(i.line+1,0)));var a=u(r,i);return new wt(a.from,a.to)}o(gg,"rangeForUnit");function vg(r,i,u,a){c&&hc(r);var p=r.display,g=r.doc;or(i);var y,S,b=g.sel,E=b.ranges;if(a.addNew&&!a.extend?(S=g.sel.contains(u),S>-1?y=E[S]:y=new wt(u,u)):(y=g.sel.primary(),S=g.sel.primIndex),a.unit=="rectangle")a.addNew||(y=new wt(u,u)),u=po(r,i,!0,!0),S=-1;else{var M=gg(r,u,a.unit);a.extend?y=Sc(y,M.anchor,M.head,a.extend):y=M}a.addNew?S==-1?(S=E.length,$r(g,tn(r,E.concat([y]),S),{scroll:!1,origin:"*mouse"})):E.length>1&&E[S].empty()&&a.unit=="char"&&!a.extend?($r(g,tn(r,E.slice(0,S).concat(E.slice(S+1)),0),{scroll:!1,origin:"*mouse"}),b=g.sel):Ld(g,S,y,ie):(S=0,$r(g,new pi([y],0),ie),b=g.sel);var D=u;function V(ve){if(ze(D,ve)!=0)if(D=ve,a.unit=="rectangle"){for(var Ne=[],Ie=r.options.tabSize,De=_t(Me(g,u.line).text,u.ch,Ie),it=_t(Me(g,ve.line).text,ve.ch,Ie),Tt=Math.min(De,it),br=Math.max(De,it),At=Math.min(u.line,ve.line),Sn=Math.min(r.lastLine(),Math.max(u.line,ve.line));At<=Sn;At++){var Cn=Me(g,At).text,dr=Pr(Cn,Tt,Ie);Tt==br?Ne.push(new wt(ue(At,dr),ue(At,dr))):Cn.length>dr&&Ne.push(new wt(ue(At,dr),ue(At,Pr(Cn,br,Ie))))}Ne.length||Ne.push(new wt(u,u)),$r(g,tn(r,b.ranges.slice(0,S).concat(Ne),S),{origin:"*mouse",scroll:!1}),r.scrollIntoView(ve)}else{var Vr=y,Ar=gg(r,ve,a.unit),Lt=Vr.anchor,Bt;ze(Ar.anchor,Lt)>0?(Bt=Ar.head,Lt=il(Vr.from(),Ar.anchor)):(Bt=Ar.anchor,Lt=gs(Vr.to(),Ar.head));var ar=b.ranges.slice(0);ar[S]=Na(r,new wt(He(g,Lt),Bt)),$r(g,tn(r,ar,S),ie)}}o(V,"extendTo");var $=p.wrapper.getBoundingClientRect(),te=0;function oe(ve){var Ne=++te,Ie=po(r,ve,!0,a.unit=="rectangle");if(!!Ie)if(ze(Ie,D)!=0){r.curOp.focus=Ge(),V(Ie);var De=dl(p,g);(Ie.line>=De.to||Ie.line$.bottom?20:0;it&&setTimeout(lr(r,function(){te==Ne&&(p.scroller.scrollTop+=it,oe(ve))}),50)}}o(oe,"extend");function de(ve){r.state.selectingText=!1,te=1/0,ve&&(or(ve),p.input.focus()),he(p.wrapper.ownerDocument,"mousemove",ge),he(p.wrapper.ownerDocument,"mouseup",be),g.history.lastSelOrigin=null}o(de,"done");var ge=lr(r,function(ve){ve.buttons===0||!el(ve)?de(ve):oe(ve)}),be=lr(r,de);r.state.selectingText=be,H(p.wrapper.ownerDocument,"mousemove",ge),H(p.wrapper.ownerDocument,"mouseup",be)}o(vg,"leftButtonSelect");function Na(r,i){var u=i.anchor,a=i.head,p=Me(r.doc,u.line);if(ze(u,a)==0&&u.sticky==a.sticky)return i;var g=Mn(p);if(!g)return i;var y=Ci(g,u.ch,u.sticky),S=g[y];if(S.from!=u.ch&&S.to!=u.ch)return i;var b=y+(S.from==u.ch==(S.level!=1)?0:1);if(b==0||b==g.length)return i;var E;if(a.line!=u.line)E=(a.line-u.line)*(r.doc.direction=="ltr"?1:-1)>0;else{var M=Ci(g,a.ch,a.sticky),D=M-y||(a.ch-u.ch)*(S.level==1?-1:1);M==b-1||M==b?E=D<0:E=D>0}var V=g[b+(E?-1:0)],$=E==(V.level==1),te=$?V.from:V.to,oe=$?"after":"before";return u.ch==te&&u.sticky==oe?i:new wt(new ue(u.line,te,oe),a)}o(Na,"bidiSimplify");function La(r,i,u,a){var p,g;if(i.touches)p=i.touches[0].clientX,g=i.touches[0].clientY;else try{p=i.clientX,g=i.clientY}catch(V){return!1}if(p>=Math.floor(r.display.gutters.getBoundingClientRect().right))return!1;a&&or(i);var y=r.display,S=y.lineDiv.getBoundingClientRect();if(g>S.bottom||!Ft(r,u))return ds(i);g-=S.top-y.viewOffset;for(var b=0;b=p){var M=ao(r.doc,g),D=r.display.gutterSpecs[b];return Te(r,u,r,M,D.className,i),ds(i)}}}o(La,"gutterEvent");function zd(r,i){return La(r,i,"gutterClick",!0)}o(zd,"clickInGutter");function $d(r,i){ji(r.display,i)||yg(r,i)||ir(r,i,"contextmenu")||pe||r.display.input.onContextMenu(i)}o($d,"onContextMenu");function yg(r,i){return Ft(r,"gutterContextMenu")?La(r,i,"gutterContextMenu",!1):!1}o(yg,"contextMenuInGutter");function sf(r){r.display.wrapper.className=r.display.wrapper.className.replace(/\s*cm-s-\S+/g,"")+r.options.theme.replace(/(^|\s)\s*/g," cm-s-"),U(r)}o(sf,"themeChanged");var Sl={toString:function(){return"CodeMirror.Init"}},lf={},Pa={};function Fc(r){var i=r.optionHandlers;function u(a,p,g,y){r.defaults[a]=p,g&&(i[a]=y?function(S,b,E){E!=Sl&&g(S,b,E)}:g)}o(u,"option"),r.defineOption=u,r.Init=Sl,u("value","",function(a,p){return a.setValue(p)},!0),u("mode",null,function(a,p){a.doc.modeOption=p,Ed(a)},!0),u("indentUnit",2,Ed,!0),u("indentWithTabs",!1),u("smartIndent",!0),u("tabSize",4,function(a){Xu(a),U(a),et(a)},!0),u("lineSeparator",null,function(a,p){if(a.doc.lineSep=p,!!p){var g=[],y=a.doc.first;a.doc.iter(function(b){for(var E=0;;){var M=b.text.indexOf(p,E);if(M==-1)break;E=M+p.length,g.push(ue(y,M))}y++});for(var S=g.length-1;S>=0;S--)Sa(a.doc,p,g[S],ue(g[S].line,g[S].ch+p.length))}}),u("specialChars",/[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\ufeff\ufff9-\ufffc]/g,function(a,p,g){a.state.specialChars=new RegExp(p.source+(p.test(" ")?"":"| "),"g"),g!=Sl&&a.refresh()}),u("specialCharPlaceholder",Ur,function(a){return a.refresh()},!0),u("electricChars",!0),u("inputStyle",A?"contenteditable":"textarea",function(){throw new Error("inputStyle can not (yet) be changed in a running editor")},!0),u("spellcheck",!1,function(a,p){return a.getInputField().spellcheck=p},!0),u("autocorrect",!1,function(a,p){return a.getInputField().autocorrect=p},!0),u("autocapitalize",!1,function(a,p){return a.getInputField().autocapitalize=p},!0),u("rtlMoveVisually",!K),u("wholeLineUpdateBefore",!0),u("theme","default",function(a){sf(a),Yu(a)},!0),u("keyMap","default",function(a,p,g){var y=wn(p),S=g!=Sl&&wn(g);S&&S.detach&&S.detach(a,y),y.attach&&y.attach(a,S||null)}),u("extraKeys",null),u("configureMouse",null),u("lineWrapping",!1,wg,!0),u("gutters",[],function(a,p){a.display.gutterSpecs=Cd(p,a.options.lineNumbers),Yu(a)},!0),u("fixedGutter",!0,function(a,p){a.display.gutters.style.left=p?fa(a.display)+"px":"0",a.refresh()},!0),u("coverGutterNextToScrollbar",!1,function(a){return Ls(a)},!0),u("scrollbarStyle","native",function(a){ml(a),Ls(a),a.display.scrollbars.setScrollTop(a.doc.scrollTop),a.display.scrollbars.setScrollLeft(a.doc.scrollLeft)},!0),u("lineNumbers",!1,function(a,p){a.display.gutterSpecs=Cd(a.options.gutters,p),Yu(a)},!0),u("firstLineNumber",1,Yu,!0),u("lineNumberFormatter",function(a){return a},Yu,!0),u("showCursorWhenSelecting",!1,$u,!0),u("resetSelectionOnContextMenu",!0),u("lineWiseCopyCut",!0),u("pasteLinesPerSelection",!0),u("selectionsMayTouch",!1),u("readOnly",!1,function(a,p){p=="nocursor"&&(pl(a),a.display.input.blur()),a.display.input.readOnlyChanged(p)}),u("screenReaderLabel",null,function(a,p){p=p===""?null:p,a.display.input.screenReaderLabelChanged(p)}),u("disableInput",!1,function(a,p){p||a.display.input.reset()},!0),u("dragDrop",!0,sw),u("allowDropFileTypes",null),u("cursorBlinkRate",530),u("cursorScrollMargin",0),u("cursorHeight",1,$u,!0),u("singleCursorHeightPerLine",!0,$u,!0),u("workTime",100),u("workDelay",100),u("flattenSpans",!0,Xu,!0),u("addModeClass",!1,Xu,!0),u("pollInterval",100),u("undoDepth",200,function(a,p){return a.doc.history.undoDepth=p}),u("historyEventDelay",1250),u("viewportMargin",10,function(a){return a.refresh()},!0),u("maxHighlightLength",1e4,Xu,!0),u("moveInputWithCursor",!0,function(a,p){p||a.display.input.resetPosition()}),u("tabindex",null,function(a,p){return a.display.input.getField().tabIndex=p||""}),u("autofocus",null),u("direction","ltr",function(a,p){return a.doc.setDirection(p)},!0),u("phrases",null)}o(Fc,"defineOptions");function sw(r,i,u){var a=u&&u!=Sl;if(!i!=!a){var p=r.display.dragFunctions,g=i?H:he;g(r.display.scroller,"dragstart",p.start),g(r.display.scroller,"dragenter",p.enter),g(r.display.scroller,"dragover",p.over),g(r.display.scroller,"dragleave",p.leave),g(r.display.scroller,"drop",p.drop)}}o(sw,"dragDropChanged");function wg(r){r.options.lineWrapping?(Xe(r.display.wrapper,"CodeMirror-wrap"),r.display.sizer.style.minWidth="",r.display.sizerWidth=null):(xe(r.display.wrapper,"CodeMirror-wrap"),zi(r)),bs(r),et(r),U(r),setTimeout(function(){return Ls(r)},100)}o(wg,"wrappingChanged");function Mt(r,i){var u=this;if(!(this instanceof Mt))return new Mt(r,i);this.options=i=i?Qt(i):{},Qt(lf,i,!1);var a=i.value;typeof a=="string"?a=new yn(a,i.mode,null,i.lineSeparator,i.direction):i.mode&&(a.modeOption=i.mode),this.doc=a;var p=new Mt.inputStyles[i.inputStyle](this),g=this.display=new Gy(r,a,p,i);g.wrapper.CodeMirror=this,sf(this),i.lineWrapping&&(this.display.wrapper.className+=" CodeMirror-wrap"),ml(this),this.state={keyMaps:[],overlays:[],modeGen:0,overwrite:!1,delayingBlurEvent:!1,focused:!1,suppressEdits:!1,pasteIncoming:-1,cutIncoming:-1,selectingText:!1,draggingText:!1,highlight:new Ct,keySeq:null,specialChars:null},i.autofocus&&!A&&g.input.focus(),c&&v<11&&setTimeout(function(){return u.display.input.reset(!0)},20),xg(this),Dd(),Ki(this),this.curOp.forceUpdate=!0,Km(this,a),i.autofocus&&!A||this.hasFocus()?setTimeout(function(){u.hasFocus()&&!u.state.focused&&pa(u)},20):pl(this);for(var y in Pa)Pa.hasOwnProperty(y)&&Pa[y](this,i[y],Sl);vo(this),i.finishInit&&i.finishInit(this);for(var S=0;S20*20}o(y,"farAway"),H(i.scroller,"touchstart",function(b){if(!ir(r,b)&&!g(b)&&!zd(r,b)){i.input.ensurePolled(),clearTimeout(u);var E=+new Date;i.activeTouch={start:E,moved:!1,prev:E-a.end<=300?a:null},b.touches.length==1&&(i.activeTouch.left=b.touches[0].pageX,i.activeTouch.top=b.touches[0].pageY)}}),H(i.scroller,"touchmove",function(){i.activeTouch&&(i.activeTouch.moved=!0)}),H(i.scroller,"touchend",function(b){var E=i.activeTouch;if(E&&!ji(i,b)&&E.left!=null&&!E.moved&&new Date-E.start<300){var M=r.coordsChar(i.activeTouch,"page"),D;!E.prev||y(E,E.prev)?D=new wt(M,M):!E.prev.prev||y(E,E.prev.prev)?D=r.findWordAt(M):D=new wt(ue(M.line,0),He(r.doc,ue(M.line+1,0))),r.setSelection(D.anchor,D.head),r.focus(),or(b)}p()}),H(i.scroller,"touchcancel",p),H(i.scroller,"scroll",function(){i.scroller.clientHeight&&(sr(r,i.scroller.scrollTop),hl(r,i.scroller.scrollLeft,!0),Te(r,"scroll",r))}),H(i.scroller,"mousewheel",function(b){return qm(r,b)}),H(i.scroller,"DOMMouseScroll",function(b){return qm(r,b)}),H(i.wrapper,"scroll",function(){return i.wrapper.scrollTop=i.wrapper.scrollLeft=0}),i.dragFunctions={enter:function(b){ir(r,b)||lo(b)},over:function(b){ir(r,b)||(Md(r,b),lo(b))},start:function(b){return ew(r,b)},drop:lr(r,sg),leave:function(b){ir(r,b)||Ad(r)}};var S=i.input.getField();H(S,"keyup",function(b){return Ud.call(r,b)}),H(S,"keydown",lr(r,ug)),H(S,"keypress",lr(r,cg)),H(S,"focus",function(b){return pa(r,b)}),H(S,"blur",function(b){return pl(r,b)})}o(xg,"registerEventHandlers");var af=[];Mt.defineInitHook=function(r){return af.push(r)};function uf(r,i,u,a){var p=r.doc,g;u==null&&(u="add"),u=="smart"&&(p.mode.indent?g=$o(r,i).state:u="prev");var y=r.options.tabSize,S=Me(p,i),b=_t(S.text,null,y);S.stateAfter&&(S.stateAfter=null);var E=S.text.match(/^\s*/)[0],M;if(!a&&!/\S/.test(S.text))M=0,u="not";else if(u=="smart"&&(M=p.mode.indent(g,S.text.slice(E.length),S.text),M==zt||M>150)){if(!a)return;u="prev"}u=="prev"?i>p.first?M=_t(Me(p,i-1).text,null,y):M=0:u=="add"?M=b+r.options.indentUnit:u=="subtract"?M=b-r.options.indentUnit:typeof u=="number"&&(M=b+u),M=Math.max(0,M);var D="",V=0;if(r.options.indentWithTabs)for(var $=Math.floor(M/y);$;--$)V+=y,D+=" ";if(Vy,b=rl(i),E=null;if(S&&a.ranges.length>1)if(Pi&&Pi.text.join(` -`)==i){if(a.ranges.length%Pi.text.length==0){E=[];for(var M=0;M=0;V--){var $=a.ranges[V],te=$.from(),oe=$.to();$.empty()&&(u&&u>0?te=ue(te.line,te.ch-u):r.state.overwrite&&!S?oe=ue(oe.line,Math.min(Me(g,oe.line).text.length,oe.ch+Se(b).length)):S&&Pi&&Pi.lineWise&&Pi.text.join(` -`)==b.join(` -`)&&(te=oe=ue(te.line,0)));var de={from:te,to:oe,text:E?E[V%E.length]:b,origin:p||(S?"paste":r.state.cutIncoming>y?"cut":"+input")};ya(r.doc,de),yr(r,"inputRead",r,de)}i&&!S&&Sg(r,i),ks(r),r.curOp.updateInput<2&&(r.curOp.updateInput=D),r.curOp.typing=!0,r.state.pasteIncoming=r.state.cutIncoming=-1}o(Bc,"applyTextInput");function jd(r,i){var u=r.clipboardData&&r.clipboardData.getData("Text");if(u)return r.preventDefault(),!i.isReadOnly()&&!i.options.disableInput&&Jr(i,function(){return Bc(i,u,0,null,"paste")}),!0}o(jd,"handlePaste");function Sg(r,i){if(!(!r.options.electricChars||!r.options.smartIndent))for(var u=r.doc.sel,a=u.ranges.length-1;a>=0;a--){var p=u.ranges[a];if(!(p.head.ch>100||a&&u.ranges[a-1].head.line==p.head.line)){var g=r.getModeAt(p.head),y=!1;if(g.electricChars){for(var S=0;S-1){y=uf(r,p.head.line,"smart");break}}else g.electricInput&&g.electricInput.test(Me(r.doc,p.head.line).text.slice(0,p.head.ch))&&(y=uf(r,p.head.line,"smart"));y&&yr(r,"electricInput",r,p.head.line)}}}o(Sg,"triggerElectric");function qd(r){for(var i=[],u=[],a=0;ag&&(uf(this,S.head.line,a,!0),g=S.head.line,y==this.doc.sel.primIndex&&ks(this));else{var b=S.from(),E=S.to(),M=Math.max(g,b.line);g=Math.min(this.lastLine(),E.line-(E.ch?0:1))+1;for(var D=M;D0&&Ld(this.doc,y,new wt(b,V[y].to()),$t)}}}),getTokenAt:function(a,p){return Kl(this,a,p)},getLineTokens:function(a,p){return Kl(this,ue(a),p,!0)},getTokenTypeAt:function(a){a=He(this.doc,a);var p=_u(this,Me(this.doc,a.line)),g=0,y=(p.length-1)/2,S=a.ch,b;if(S==0)b=p[2];else for(;;){var E=g+y>>1;if((E?p[E*2-1]:0)>=S)y=E;else if(p[E*2+1]b&&(a=b,y=!0),S=Me(this.doc,a)}else S=a;return ci(this,S,{top:0,left:0},p||"page",g||y).top+(y?this.doc.height-hn(S):0)},defaultTextHeight:function(){return Cs(this.display)},defaultCharWidth:function(){return ua(this.display)},getViewport:function(){return{from:this.display.viewFrom,to:this.display.viewTo}},addWidget:function(a,p,g,y,S){var b=this.display;a=Vn(this,He(this.doc,a));var E=a.bottom,M=a.left;if(p.style.position="absolute",p.setAttribute("cm-ignore-events","true"),this.display.input.setUneditable(p),b.sizer.appendChild(p),y=="over")E=a.top;else if(y=="above"||y=="near"){var D=Math.max(b.wrapper.clientHeight,this.doc.height),V=Math.max(b.sizer.clientWidth,b.lineSpace.clientWidth);(y=="above"||a.bottom+p.offsetHeight>D)&&a.top>p.offsetHeight?E=a.top-p.offsetHeight:a.bottom+p.offsetHeight<=D&&(E=a.bottom),M+p.offsetWidth>V&&(M=V-p.offsetWidth)}p.style.top=E+"px",p.style.left=p.style.right="",S=="right"?(M=b.sizer.clientWidth-p.offsetWidth,p.style.right="0px"):(S=="left"?M=0:S=="middle"&&(M=(b.sizer.clientWidth-p.offsetWidth)/2),p.style.left=M+"px"),g&&Hy(this,{left:M,top:E,right:M+p.offsetWidth,bottom:E+p.offsetHeight})},triggerOnKeyDown:en(ug),triggerOnKeyPress:en(cg),triggerOnKeyUp:Ud,triggerOnMouseDown:en(dg),execCommand:function(a){if(xl.hasOwnProperty(a))return xl[a].call(null,this)},triggerElectric:en(function(a){Sg(this,a)}),findPosH:function(a,p,g,y){var S=1;p<0&&(S=-1,p=-p);for(var b=He(this.doc,a),E=0;E0&&M(g.charAt(y-1));)--y;for(;S.5||this.options.lineWrapping)&&bs(this),Te(this,"refresh",this)}),swapDoc:en(function(a){var p=this.doc;return p.cm=null,this.state.selectingText&&this.state.selectingText(),Km(this,a),U(this),this.display.input.reset(),qu(this,a.scrollLeft,a.scrollTop),this.curOp.forceScroll=!0,yr(this,"swapDoc",this,p),p}),phrase:function(a){var p=this.options.phrases;return p&&Object.prototype.hasOwnProperty.call(p,a)?p[a]:a},getInputField:function(){return this.display.input.getField()},getWrapperElement:function(){return this.display.wrapper},getScrollerElement:function(){return this.display.scroller},getGutterElement:function(){return this.display.gutters}},Wr(r),r.registerHelper=function(a,p,g){u.hasOwnProperty(a)||(u[a]=r[a]={_global:[]}),u[a][p]=g},r.registerGlobalHelper=function(a,p,g,y){r.registerHelper(a,p,y),u[a]._global.push({pred:g,val:y})}}o(ts,"addEditorMethods");function ff(r,i,u,a,p){var g=i,y=u,S=Me(r,i.line),b=p&&r.direction=="rtl"?-u:u;function E(){var be=i.line+b;return be=r.first+r.size?!1:(i=new ue(be,i.ch,i.sticky),S=Me(r,be))}o(E,"findNextLine");function M(be){var ve;if(a=="codepoint"){var Ne=S.text.charCodeAt(i.ch+(u>0?0:-1));if(isNaN(Ne))ve=null;else{var Ie=u>0?Ne>=55296&&Ne<56320:Ne>=56320&&Ne<57343;ve=new ue(i.line,Math.max(0,Math.min(S.text.length,i.ch+u*(Ie?2:1))),-u)}}else p?ve=ag(r.cm,S,i,u):ve=Mc(S,i,u);if(ve==null)if(!be&&E())i=_a(p,r.cm,S,i.line,b);else return!1;else i=ve;return!0}if(o(M,"moveOnce"),a=="char"||a=="codepoint")M();else if(a=="column")M(!0);else if(a=="word"||a=="group")for(var D=null,V=a=="group",$=r.cm&&r.cm.getHelper(i,"wordChars"),te=!0;!(u<0&&!M(!te));te=!1){var oe=S.text.charAt(i.ch)||` -`,de=gr(oe,$)?"w":V&&oe==` -`?"n":!V||/\s/.test(oe)?null:"p";if(V&&!te&&!de&&(de="s"),D&&D!=de){u<0&&(u=1,M(),i.sticky="after");break}if(de&&(D=de),u>0&&!M(!te))break}var ge=jr(r,i,g,y,!0);return Cu(g,ge)&&(ge.hitSide=!0),ge}o(ff,"findPosH");function Hc(r,i,u,a){var p=r.doc,g=i.left,y;if(a=="page"){var S=Math.min(r.display.wrapper.clientHeight,window.innerHeight||document.documentElement.clientHeight),b=Math.max(S-.5*Cs(r.display),3);y=(u>0?i.bottom:i.top)+u*b}else a=="line"&&(y=u>0?i.bottom+3:i.top-3);for(var E;E=q(r,g,y),!!E.outside;){if(u<0?y<=0:y>=p.height){E.hitSide=!0;break}y+=u*5}return E}o(Hc,"findPosV");var xt=o(function(r){this.cm=r,this.lastAnchorNode=this.lastAnchorOffset=this.lastFocusNode=this.lastFocusOffset=null,this.polling=new Ct,this.composing=null,this.gracePeriod=!1,this.readDOMTimeout=null},"ContentEditableInput");xt.prototype.init=function(r){var i=this,u=this,a=u.cm,p=u.div=r.lineDiv;p.contentEditable=!0,Cg(p,a.options.spellcheck,a.options.autocorrect,a.options.autocapitalize);function g(S){for(var b=S.target;b;b=b.parentNode){if(b==p)return!0;if(/\bCodeMirror-(?:line)?widget\b/.test(b.className))break}return!1}o(g,"belongsToInput"),H(p,"paste",function(S){!g(S)||ir(a,S)||jd(S,a)||v<=11&&setTimeout(lr(a,function(){return i.updateFromDOM()}),20)}),H(p,"compositionstart",function(S){i.composing={data:S.data,done:!1}}),H(p,"compositionupdate",function(S){i.composing||(i.composing={data:S.data,done:!1})}),H(p,"compositionend",function(S){i.composing&&(S.data!=i.composing.data&&i.readFromDOMSoon(),i.composing.done=!0)}),H(p,"touchstart",function(){return u.forceCompositionEnd()}),H(p,"input",function(){i.composing||i.readFromDOMSoon()});function y(S){if(!(!g(S)||ir(a,S))){if(a.somethingSelected())Oi({lineWise:!1,text:a.getSelections()}),S.type=="cut"&&a.replaceSelection("",null,"cut");else if(a.options.lineWiseCopyCut){var b=qd(a);Oi({lineWise:!0,text:b.text}),S.type=="cut"&&a.operation(function(){a.setSelections(b.ranges,0,$t),a.replaceSelection("",null,"cut")})}else return;if(S.clipboardData){S.clipboardData.clearData();var E=Pi.text.join(` -`);if(S.clipboardData.setData("Text",E),S.clipboardData.getData("Text")==E){S.preventDefault();return}}var M=bg(),D=M.firstChild;a.display.lineSpace.insertBefore(M,a.display.lineSpace.firstChild),D.value=Pi.text.join(` -`);var V=Ge();ct(D),setTimeout(function(){a.display.lineSpace.removeChild(M),V.focus(),V==p&&u.showPrimarySelection()},50)}}o(y,"onCopyCut"),H(p,"copy",y),H(p,"cut",y)},xt.prototype.screenReaderLabelChanged=function(r){r?this.div.setAttribute("aria-label",r):this.div.removeAttribute("aria-label")},xt.prototype.prepareSelection=function(){var r=ju(this.cm,!1);return r.focus=Ge()==this.div,r},xt.prototype.showSelection=function(r,i){!r||!this.cm.display.view.length||((r.focus||i)&&this.showPrimarySelection(),this.showMultipleSelections(r))},xt.prototype.getSelection=function(){return this.cm.display.wrapper.ownerDocument.getSelection()},xt.prototype.showPrimarySelection=function(){var r=this.getSelection(),i=this.cm,u=i.doc.sel.primary(),a=u.from(),p=u.to();if(i.display.viewTo==i.display.viewFrom||a.line>=i.display.viewTo||p.line=i.display.viewFrom&&cf(i,a)||{node:S[0].measure.map[2],offset:0},E=p.liner.firstLine()&&(a=ue(a.line-1,Me(r.doc,a.line-1).length)),p.ch==Me(r.doc,p.line).text.length&&p.linei.viewTo-1)return!1;var g,y,S;a.line==i.viewFrom||(g=ho(r,a.line))==0?(y=vt(i.view[0].line),S=i.view[0].node):(y=vt(i.view[g].line),S=i.view[g-1].node.nextSibling);var b=ho(r,p.line),E,M;if(b==i.view.length-1?(E=i.viewTo-1,M=i.lineDiv.lastChild):(E=vt(i.view[b+1].line)-1,M=i.view[b+1].node.previousSibling),!S)return!1;for(var D=r.doc.splitLines(Wc(r,S,M,y,E)),V=Ei(r.doc,ue(y,0),ue(E,Me(r.doc,E).text.length));D.length>1&&V.length>1;)if(Se(D)==Se(V))D.pop(),V.pop(),E--;else if(D[0]==V[0])D.shift(),V.shift(),y++;else break;for(var $=0,te=0,oe=D[0],de=V[0],ge=Math.min(oe.length,de.length);$a.ch&&be.charCodeAt(be.length-te-1)==ve.charCodeAt(ve.length-te-1);)$--,te++;D[D.length-1]=be.slice(0,be.length-te).replace(/^\u200b+/,""),D[0]=D[0].slice($).replace(/\u200b+$/,"");var Ie=ue(y,$),De=ue(E,V.length?Se(V).length-te:0);if(D.length>1||D[0]||ze(Ie,De))return Sa(r.doc,D,Ie,De,"+input"),!0},xt.prototype.ensurePolled=function(){this.forceCompositionEnd()},xt.prototype.reset=function(){this.forceCompositionEnd()},xt.prototype.forceCompositionEnd=function(){!this.composing||(clearTimeout(this.readDOMTimeout),this.composing=null,this.updateFromDOM(),this.div.blur(),this.div.focus())},xt.prototype.readFromDOMSoon=function(){var r=this;this.readDOMTimeout==null&&(this.readDOMTimeout=setTimeout(function(){if(r.readDOMTimeout=null,r.composing)if(r.composing.done)r.composing=null;else return;r.updateFromDOM()},80))},xt.prototype.updateFromDOM=function(){var r=this;(this.cm.isReadOnly()||!this.pollContent())&&Jr(this.cm,function(){return et(r.cm)})},xt.prototype.setUneditable=function(r){r.contentEditable="false"},xt.prototype.onKeyPress=function(r){r.charCode==0||this.composing||(r.preventDefault(),this.cm.isReadOnly()||lr(this.cm,Bc)(this.cm,String.fromCharCode(r.charCode==null?r.keyCode:r.charCode),0))},xt.prototype.readOnlyChanged=function(r){this.div.contentEditable=String(r!="nocursor")},xt.prototype.onContextMenu=function(){},xt.prototype.resetPosition=function(){},xt.prototype.needsContentAttribute=!0;function cf(r,i){var u=la(r,i.line);if(!u||u.hidden)return null;var a=Me(r.doc,i.line),p=Wu(u,a,i.line),g=Mn(a,r.doc.direction),y="left";if(g){var S=Ci(g,i.ch);y=S%2?"right":"left"}var b=aa(p.map,i.ch,y);return b.offset=b.collapse=="right"?b.end:b.start,b}o(cf,"posToDOM");function Oa(r){for(var i=r;i;i=i.parentNode)if(/CodeMirror-gutter-wrapper/.test(i.className))return!0;return!1}o(Oa,"isInGutter");function Be(r,i){return i&&(r.bad=!0),r}o(Be,"badPos");function Wc(r,i,u,a,p){var g="",y=!1,S=r.doc.lineSeparator(),b=!1;function E($){return function(te){return te.id==$}}o(E,"recognizeMarker");function M(){y&&(g+=S,b&&(g+=S),y=b=!1)}o(M,"close");function D($){$&&(M(),g+=$)}o(D,"addText");function V($){if($.nodeType==1){var te=$.getAttribute("cm-text");if(te){D(te);return}var oe=$.getAttribute("cm-marker"),de;if(oe){var ge=r.findMarks(ue(a,0),ue(p+1,0),E(+oe));ge.length&&(de=ge[0].find(0))&&D(Ei(r.doc,de.from,de.to).join(S));return}if($.getAttribute("contenteditable")=="false")return;var be=/^(pre|div|p|li|table|br)$/i.test($.nodeName);if(!/^br$/i.test($.nodeName)&&$.textContent.length==0)return;be&&M();for(var ve=0;ve<$.childNodes.length;ve++)V($.childNodes[ve]);/^(pre|p)$/i.test($.nodeName)&&(b=!0),be&&(y=!0)}else $.nodeType==3&&D($.nodeValue.replace(/\u200b/g,"").replace(/\u00a0/g," "))}for(o(V,"walk");V(i),i!=u;)i=i.nextSibling,b=!1;return g}o(Wc,"domTextBetween");function Ma(r,i,u){var a;if(i==r.display.lineDiv){if(a=r.display.lineDiv.childNodes[u],!a)return Be(r.clipPos(ue(r.display.viewTo-1)),!0);i=null,u=0}else for(a=i;;a=a.parentNode){if(!a||a==r.display.lineDiv)return null;if(a.parentNode&&a.parentNode==r.display.lineDiv)break}for(var p=0;p=9&&i.hasSelection&&(i.hasSelection=null),u.poll()}),H(p,"paste",function(y){ir(a,y)||jd(y,a)||(a.state.pasteIncoming=+new Date,u.fastPoll())});function g(y){if(!ir(a,y)){if(a.somethingSelected())Oi({lineWise:!1,text:a.getSelections()});else if(a.options.lineWiseCopyCut){var S=qd(a);Oi({lineWise:!0,text:S.text}),y.type=="cut"?a.setSelections(S.ranges,null,$t):(u.prevInput="",p.value=S.text.join(` -`),ct(p))}else return;y.type=="cut"&&(a.state.cutIncoming=+new Date)}}o(g,"prepareCopyCut"),H(p,"cut",g),H(p,"copy",g),H(r.scroller,"paste",function(y){if(!(ji(r,y)||ir(a,y))){if(!p.dispatchEvent){a.state.pasteIncoming=+new Date,u.focus();return}var S=new Event("paste");S.clipboardData=y.clipboardData,p.dispatchEvent(S)}}),H(r.lineSpace,"selectstart",function(y){ji(r,y)||or(y)}),H(p,"compositionstart",function(){var y=a.getCursor("from");u.composing&&u.composing.range.clear(),u.composing={start:y,range:a.markText(y,a.getCursor("to"),{className:"CodeMirror-composing"})}}),H(p,"compositionend",function(){u.composing&&(u.poll(),u.composing.range.clear(),u.composing=null)})},pr.prototype.createField=function(r){this.wrapper=bg(),this.textarea=this.wrapper.firstChild},pr.prototype.screenReaderLabelChanged=function(r){r?this.textarea.setAttribute("aria-label",r):this.textarea.removeAttribute("aria-label")},pr.prototype.prepareSelection=function(){var r=this.cm,i=r.display,u=r.doc,a=ju(r);if(r.options.moveInputWithCursor){var p=Vn(r,u.sel.primary().head,"div"),g=i.wrapper.getBoundingClientRect(),y=i.lineDiv.getBoundingClientRect();a.teTop=Math.max(0,Math.min(i.wrapper.clientHeight-10,p.top+y.top-g.top)),a.teLeft=Math.max(0,Math.min(i.wrapper.clientWidth-10,p.left+y.left-g.left))}return a},pr.prototype.showSelection=function(r){var i=this.cm,u=i.display;tt(u.cursorDiv,r.cursors),tt(u.selectionDiv,r.selection),r.teTop!=null&&(this.wrapper.style.top=r.teTop+"px",this.wrapper.style.left=r.teLeft+"px")},pr.prototype.reset=function(r){if(!(this.contextMenuPending||this.composing)){var i=this.cm;if(i.somethingSelected()){this.prevInput="";var u=i.getSelection();this.textarea.value=u,i.state.focused&&ct(this.textarea),c&&v>=9&&(this.hasSelection=u)}else r||(this.prevInput=this.textarea.value="",c&&v>=9&&(this.hasSelection=null))}},pr.prototype.getField=function(){return this.textarea},pr.prototype.supportsTouch=function(){return!1},pr.prototype.focus=function(){if(this.cm.options.readOnly!="nocursor"&&(!A||Ge()!=this.textarea))try{this.textarea.focus()}catch(r){}},pr.prototype.blur=function(){this.textarea.blur()},pr.prototype.resetPosition=function(){this.wrapper.style.top=this.wrapper.style.left=0},pr.prototype.receivedFocus=function(){this.slowPoll()},pr.prototype.slowPoll=function(){var r=this;this.pollingFast||this.polling.set(this.cm.options.pollInterval,function(){r.poll(),r.cm.state.focused&&r.slowPoll()})},pr.prototype.fastPoll=function(){var r=!1,i=this;i.pollingFast=!0;function u(){var a=i.poll();!a&&!r?(r=!0,i.polling.set(60,u)):(i.pollingFast=!1,i.slowPoll())}o(u,"p"),i.polling.set(20,u)},pr.prototype.poll=function(){var r=this,i=this.cm,u=this.textarea,a=this.prevInput;if(this.contextMenuPending||!i.state.focused||od(u)&&!a&&!this.composing||i.isReadOnly()||i.options.disableInput||i.state.keySeq)return!1;var p=u.value;if(p==a&&!i.somethingSelected())return!1;if(c&&v>=9&&this.hasSelection===p||I&&/[\uf700-\uf7ff]/.test(p))return i.display.input.reset(),!1;if(i.doc.sel==i.display.selForContextMenu){var g=p.charCodeAt(0);if(g==8203&&!a&&(a="\u200B"),g==8666)return this.reset(),this.cm.execCommand("undo")}for(var y=0,S=Math.min(a.length,p.length);y1e3||p.indexOf(` -`)>-1?u.value=r.prevInput="":r.prevInput=p,r.composing&&(r.composing.range.clear(),r.composing.range=i.markText(r.composing.start,i.getCursor("to"),{className:"CodeMirror-composing"}))}),!0},pr.prototype.ensurePolled=function(){this.pollingFast&&this.poll()&&(this.pollingFast=!1)},pr.prototype.onKeyPress=function(){c&&v>=9&&(this.hasSelection=null),this.fastPoll()},pr.prototype.onContextMenu=function(r){var i=this,u=i.cm,a=u.display,p=i.textarea;i.contextMenuPending&&i.contextMenuPending();var g=po(u,r),y=a.scroller.scrollTop;if(!g||j)return;var S=u.options.resetSelectionOnContextMenu;S&&u.doc.sel.contains(g)==-1&&lr(u,$r)(u.doc,Os(g),$t);var b=p.style.cssText,E=i.wrapper.style.cssText,M=i.wrapper.offsetParent.getBoundingClientRect();i.wrapper.style.cssText="position: static",p.style.cssText=`position: absolute; width: 30px; height: 30px; - top: `+(r.clientY-M.top-5)+"px; left: "+(r.clientX-M.left-5)+`px; - z-index: 1000; background: `+(c?"rgba(255, 255, 255, .05)":"transparent")+`; - outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);`;var D;C&&(D=window.scrollY),a.input.focus(),C&&window.scrollTo(null,D),a.input.reset(),u.somethingSelected()||(p.value=i.prevInput=" "),i.contextMenuPending=$,a.selForContextMenu=u.doc.sel,clearTimeout(a.detectingSelectAll);function V(){if(p.selectionStart!=null){var oe=u.somethingSelected(),de="\u200B"+(oe?p.value:"");p.value="\u21DA",p.value=de,i.prevInput=oe?"":"\u200B",p.selectionStart=1,p.selectionEnd=de.length,a.selForContextMenu=u.doc.sel}}o(V,"prepareSelectAllHack");function $(){if(i.contextMenuPending==$&&(i.contextMenuPending=!1,i.wrapper.style.cssText=E,p.style.cssText=b,c&&v<9&&a.scrollbars.setScrollTop(a.scroller.scrollTop=y),p.selectionStart!=null)){(!c||c&&v<9)&&V();var oe=0,de=o(function(){a.selForContextMenu==u.doc.sel&&p.selectionStart==0&&p.selectionEnd>0&&i.prevInput=="\u200B"?lr(u,Qm)(u):oe++<10?a.detectingSelectAll=setTimeout(de,500):(a.selForContextMenu=null,a.input.reset())},"poll");a.detectingSelectAll=setTimeout(de,200)}}if(o($,"rehide"),c&&v>=9&&V(),pe){lo(r);var te=o(function(){he(window,"mouseup",te),setTimeout($,20)},"mouseup");H(window,"mouseup",te)}else setTimeout($,50)},pr.prototype.readOnlyChanged=function(r){r||this.reset(),this.textarea.disabled=r=="nocursor",this.textarea.readOnly=!!r},pr.prototype.setUneditable=function(){},pr.prototype.needsContentAttribute=!1;function Vd(r,i){if(i=i?Qt(i):{},i.value=r.value,!i.tabindex&&r.tabIndex&&(i.tabindex=r.tabIndex),!i.placeholder&&r.placeholder&&(i.placeholder=r.placeholder),i.autofocus==null){var u=Ge();i.autofocus=u==r||r.getAttribute("autofocus")!=null&&u==document.body}function a(){r.value=S.getValue()}o(a,"save");var p;if(r.form&&(H(r.form,"submit",a),!i.leaveSubmitMethodAlone)){var g=r.form;p=g.submit;try{var y=g.submit=function(){a(),g.submit=p,g.submit(),g.submit=y}}catch(b){}}i.finishInit=function(b){b.save=a,b.getTextArea=function(){return r},b.toTextArea=function(){b.toTextArea=isNaN,a(),r.parentNode.removeChild(b.getWrapperElement()),r.style.display="",r.form&&(he(r.form,"submit",a),!i.leaveSubmitMethodAlone&&typeof r.form.submit=="function"&&(r.form.submit=p))}},r.style.display="none";var S=Mt(function(b){return r.parentNode.insertBefore(b,r.nextSibling)},i);return S}o(Vd,"fromTextArea");function Eg(r){r.off=he,r.on=H,r.wheelEventPixels=Yy,r.Doc=yn,r.splitLines=rl,r.countColumn=_t,r.findColumn=Pr,r.isWordChar=Zt,r.Pass=zt,r.signal=Te,r.Line=Ce,r.changeEnd=Ms,r.scrollbarModel=gc,r.Pos=ue,r.cmpPos=ze,r.modes=zl,r.mimeModes=ms,r.resolveMode=nl,r.getMode=xu,r.modeExtensions=Wo,r.extendMode=ad,r.copyState=Uo,r.startState=ec,r.innerMode=$l,r.commands=xl,r.keyMap=Jo,r.keyName=Id,r.isModifierKey=Oc,r.lookupKey=es,r.normalizeKeyMap=rw,r.StringStream=jt,r.SharedTextMarker=Ea,r.TextMarker=Rs,r.LineWidget=tf,r.e_preventDefault=or,r.e_stopPropagation=li,r.e_stop=lo,r.addClass=Xe,r.contains=Ke,r.rmClass=xe,r.keyNames=Fs}o(Eg,"addLegacyProps"),Fc(Mt),ts(Mt);var xn="iter insert remove copy getEditor constructor".split(" ");for(var Uc in yn.prototype)yn.prototype.hasOwnProperty(Uc)&&ut(xn,Uc)<0&&(Mt.prototype[Uc]=function(r){return function(){return r.apply(this.doc,arguments)}}(yn.prototype[Uc]));return Wr(yn),Mt.inputStyles={textarea:pr,contenteditable:xt},Mt.defineMode=function(r){!Mt.defaults.mode&&r!="null"&&(Mt.defaults.mode=r),ld.apply(this,arguments)},Mt.defineMIME=Jf,Mt.defineMode("null",function(){return{token:function(r){return r.skipToEnd()}}}),Mt.defineMIME("text/plain","null"),Mt.defineExtension=function(r,i){Mt.prototype[r]=i},Mt.defineDocExtension=function(r,i){yn.prototype[r]=i},Mt.fromTextArea=Vd,Eg(Mt),Mt.version="5.62.3",Mt})});var cL=Ue((oz,fL)=>{var T3=typeof Element!="undefined",k3=typeof Map=="function",N3=typeof Set=="function",L3=typeof ArrayBuffer=="function"&&!!ArrayBuffer.isView;function oy(e,t){if(e===t)return!0;if(e&&t&&typeof e=="object"&&typeof t=="object"){if(e.constructor!==t.constructor)return!1;var n,l,d;if(Array.isArray(e)){if(n=e.length,n!=t.length)return!1;for(l=n;l--!=0;)if(!oy(e[l],t[l]))return!1;return!0}var h;if(k3&&e instanceof Map&&t instanceof Map){if(e.size!==t.size)return!1;for(h=e.entries();!(l=h.next()).done;)if(!t.has(l.value[0]))return!1;for(h=e.entries();!(l=h.next()).done;)if(!oy(l.value[1],t.get(l.value[0])))return!1;return!0}if(N3&&e instanceof Set&&t instanceof Set){if(e.size!==t.size)return!1;for(h=e.entries();!(l=h.next()).done;)if(!t.has(l.value[0]))return!1;return!0}if(L3&&ArrayBuffer.isView(e)&&ArrayBuffer.isView(t)){if(n=e.length,n!=t.length)return!1;for(l=n;l--!=0;)if(e[l]!==t[l])return!1;return!0}if(e.constructor===RegExp)return e.source===t.source&&e.flags===t.flags;if(e.valueOf!==Object.prototype.valueOf)return e.valueOf()===t.valueOf();if(e.toString!==Object.prototype.toString)return e.toString()===t.toString();if(d=Object.keys(e),n=d.length,n!==Object.keys(t).length)return!1;for(l=n;l--!=0;)if(!Object.prototype.hasOwnProperty.call(t,d[l]))return!1;if(T3&&e instanceof Element)return!1;for(l=n;l--!=0;)if(!((d[l]==="_owner"||d[l]==="__v"||d[l]==="__o")&&e.$$typeof)&&!oy(e[d[l]],t[d[l]]))return!1;return!0}return e!==e&&t!==t}o(oy,"equal");fL.exports=o(function(t,n){try{return oy(t,n)}catch(l){if((l.message||"").match(/stack|recursion/i))return console.warn("react-fast-compare cannot handle circular refs"),!1;throw l}},"isEqual")});var EL=Ue((_9,bL)=>{"use strict";var U3="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED";bL.exports=U3});var NL=Ue((T9,kL)=>{"use strict";var z3=EL();function _L(){}o(_L,"emptyFunction");function TL(){}o(TL,"emptyFunctionWithReset");TL.resetWarningCache=_L;kL.exports=function(){function e(l,d,h,c,v,C){if(C!==z3){var k=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw k.name="Invariant Violation",k}}o(e,"shim"),e.isRequired=e;function t(){return e}o(t,"getShim");var n={array:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:TL,resetWarningCache:_L};return n.PropTypes=n,n}});var Sm=Ue((L9,LL)=>{LL.exports=NL()();var k9,N9});var fC=Ue((P9,PL)=>{PL.exports=o(function(t,n,l,d){var h=l?l.call(d,t,n):void 0;if(h!==void 0)return!!h;if(t===n)return!0;if(typeof t!="object"||!t||typeof n!="object"||!n)return!1;var c=Object.keys(t),v=Object.keys(n);if(c.length!==v.length)return!1;for(var C=Object.prototype.hasOwnProperty.bind(n),k=0;k{HL.exports=function(){return typeof Promise=="function"&&Promise.prototype&&Promise.prototype.then}});var du=Ue(qf=>{var pC,j3=[0,26,44,70,100,134,172,196,242,292,346,404,466,532,581,655,733,815,901,991,1085,1156,1258,1364,1474,1588,1706,1828,1921,2051,2185,2323,2465,2611,2761,2876,3034,3196,3362,3532,3706];qf.getSymbolSize=o(function(t){if(!t)throw new Error('"version" cannot be null or undefined');if(t<1||t>40)throw new Error('"version" should be in range from 1 to 40');return t*4+17},"getSymbolSize");qf.getSymbolTotalCodewords=o(function(t){return j3[t]},"getSymbolTotalCodewords");qf.getBCHDigit=function(e){let t=0;for(;e!==0;)t++,e>>>=1;return t};qf.setToSJISFunction=o(function(t){if(typeof t!="function")throw new Error('"toSJISFunc" is not a valid function.');pC=t},"setToSJISFunction");qf.isKanjiModeEnabled=function(){return typeof pC!="undefined"};qf.toSJIS=o(function(t){return pC(t)},"toSJIS")});var vy=Ue(Do=>{Do.L={bit:1};Do.M={bit:0};Do.Q={bit:3};Do.H={bit:2};function q3(e){if(typeof e!="string")throw new Error("Param is not a string");switch(e.toLowerCase()){case"l":case"low":return Do.L;case"m":case"medium":return Do.M;case"q":case"quartile":return Do.Q;case"h":case"high":return Do.H;default:throw new Error("Unknown EC Level: "+e)}}o(q3,"fromString");Do.isValid=o(function(t){return t&&typeof t.bit!="undefined"&&t.bit>=0&&t.bit<4},"isValid");Do.from=o(function(t,n){if(Do.isValid(t))return t;try{return q3(t)}catch(l){return n}},"from")});var $L=Ue((X9,zL)=>{function UL(){this.buffer=[],this.length=0}o(UL,"BitBuffer");UL.prototype={get:function(e){let t=Math.floor(e/8);return(this.buffer[t]>>>7-e%8&1)==1},put:function(e,t){for(let n=0;n>>t-n-1&1)==1)},getLengthInBits:function(){return this.length},putBit:function(e){let t=Math.floor(this.length/8);this.buffer.length<=t&&this.buffer.push(0),e&&(this.buffer[t]|=128>>>this.length%8),this.length++}};zL.exports=UL});var qL=Ue((Q9,jL)=>{function _m(e){if(!e||e<1)throw new Error("BitMatrix size must be defined and greater than 0");this.size=e,this.data=new Uint8Array(e*e),this.reservedBit=new Uint8Array(e*e)}o(_m,"BitMatrix");_m.prototype.set=function(e,t,n,l){let d=e*this.size+t;this.data[d]=n,l&&(this.reservedBit[d]=!0)};_m.prototype.get=function(e,t){return this.data[e*this.size+t]};_m.prototype.xor=function(e,t,n){this.data[e*this.size+t]^=n};_m.prototype.isReserved=function(e,t){return this.reservedBit[e*this.size+t]};jL.exports=_m});var VL=Ue(yy=>{var V3=du().getSymbolSize;yy.getRowColCoords=o(function(t){if(t===1)return[];let n=Math.floor(t/7)+2,l=V3(t),d=l===145?26:Math.ceil((l-13)/(2*n-2))*2,h=[l-7];for(let c=1;c{var K3=du().getSymbolSize,KL=7;GL.getPositions=o(function(t){let n=K3(t);return[[0,0],[n-KL,0],[0,n-KL]]},"getPositions")});var XL=Ue(rr=>{rr.Patterns={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7};var Vf={N1:3,N2:3,N3:40,N4:10};rr.isValid=o(function(t){return t!=null&&t!==""&&!isNaN(t)&&t>=0&&t<=7},"isValid");rr.from=o(function(t){return rr.isValid(t)?parseInt(t,10):void 0},"from");rr.getPenaltyN1=o(function(t){let n=t.size,l=0,d=0,h=0,c=null,v=null;for(let C=0;C=5&&(l+=Vf.N1+(d-5)),c=O,d=1),O=t.get(k,C),O===v?h++:(h>=5&&(l+=Vf.N1+(h-5)),v=O,h=1)}d>=5&&(l+=Vf.N1+(d-5)),h>=5&&(l+=Vf.N1+(h-5))}return l},"getPenaltyN1");rr.getPenaltyN2=o(function(t){let n=t.size,l=0;for(let d=0;d=10&&(d===1488||d===93)&&l++,h=h<<1&2047|t.get(v,c),v>=10&&(h===1488||h===93)&&l++}return l*Vf.N3},"getPenaltyN3");rr.getPenaltyN4=o(function(t){let n=0,l=t.data.length;for(let h=0;h{var hu=vy(),wy=[1,1,1,1,1,1,1,1,1,1,2,2,1,2,2,4,1,2,4,4,2,4,4,4,2,4,6,5,2,4,6,6,2,5,8,8,4,5,8,8,4,5,8,11,4,8,10,11,4,9,12,16,4,9,16,16,6,10,12,18,6,10,17,16,6,11,16,19,6,13,18,21,7,14,21,25,8,16,20,25,8,17,23,25,9,17,23,34,9,18,25,30,10,20,27,32,12,21,29,35,12,23,34,37,12,25,34,40,13,26,35,42,14,28,38,45,15,29,40,48,16,31,43,51,17,33,45,54,18,35,48,57,19,37,51,60,19,38,53,63,20,40,56,66,21,43,59,70,22,45,62,74,24,47,65,77,25,49,68,81],xy=[7,10,13,17,10,16,22,28,15,26,36,44,20,36,52,64,26,48,72,88,36,64,96,112,40,72,108,130,48,88,132,156,60,110,160,192,72,130,192,224,80,150,224,264,96,176,260,308,104,198,288,352,120,216,320,384,132,240,360,432,144,280,408,480,168,308,448,532,180,338,504,588,196,364,546,650,224,416,600,700,224,442,644,750,252,476,690,816,270,504,750,900,300,560,810,960,312,588,870,1050,336,644,952,1110,360,700,1020,1200,390,728,1050,1260,420,784,1140,1350,450,812,1200,1440,480,868,1290,1530,510,924,1350,1620,540,980,1440,1710,570,1036,1530,1800,570,1064,1590,1890,600,1120,1680,1980,630,1204,1770,2100,660,1260,1860,2220,720,1316,1950,2310,750,1372,2040,2430];dC.getBlocksCount=o(function(t,n){switch(n){case hu.L:return wy[(t-1)*4+0];case hu.M:return wy[(t-1)*4+1];case hu.Q:return wy[(t-1)*4+2];case hu.H:return wy[(t-1)*4+3];default:return}},"getBlocksCount");dC.getTotalCodewordsCount=o(function(t,n){switch(n){case hu.L:return xy[(t-1)*4+0];case hu.M:return xy[(t-1)*4+1];case hu.Q:return xy[(t-1)*4+2];case hu.H:return xy[(t-1)*4+3];default:return}},"getTotalCodewordsCount")});var QL=Ue(Cy=>{var Tm=new Uint8Array(512),Sy=new Uint8Array(256);o(function(){let t=1;for(let n=0;n<255;n++)Tm[n]=t,Sy[t]=n,t<<=1,t&256&&(t^=285);for(let n=255;n<512;n++)Tm[n]=Tm[n-255]},"initTables")();Cy.log=o(function(t){if(t<1)throw new Error("log("+t+")");return Sy[t]},"log");Cy.exp=o(function(t){return Tm[t]},"exp");Cy.mul=o(function(t,n){return t===0||n===0?0:Tm[Sy[t]+Sy[n]]},"mul")});var ZL=Ue(km=>{var mC=QL();km.mul=o(function(t,n){let l=new Uint8Array(t.length+n.length-1);for(let d=0;d=0;){let d=l[0];for(let c=0;c{var JL=ZL();function gC(e){this.genPoly=void 0,this.degree=e,this.degree&&this.initialize(this.degree)}o(gC,"ReedSolomonEncoder");gC.prototype.initialize=o(function(t){this.degree=t,this.genPoly=JL.generateECPolynomial(this.degree)},"initialize");gC.prototype.encode=o(function(t){if(!this.genPoly)throw new Error("Encoder not initialized");let n=new Uint8Array(t.length+this.degree);n.set(t);let l=JL.mod(n,this.genPoly),d=this.degree-l.length;if(d>0){let h=new Uint8Array(this.degree);return h.set(l,d),h}return l},"encode");eP.exports=gC});var vC=Ue(rP=>{rP.isValid=o(function(t){return!isNaN(t)&&t>=1&&t<=40},"isValid")});var yC=Ue(Bl=>{var nP="[0-9]+",Y3="[A-Z $%*+\\-./:]+",Nm="(?:[u3000-u303F]|[u3040-u309F]|[u30A0-u30FF]|[uFF00-uFFEF]|[u4E00-u9FAF]|[u2605-u2606]|[u2190-u2195]|u203B|[u2010u2015u2018u2019u2025u2026u201Cu201Du2225u2260]|[u0391-u0451]|[u00A7u00A8u00B1u00B4u00D7u00F7])+";Nm=Nm.replace(/u/g,"\\u");var X3="(?:(?![A-Z0-9 $%*+\\-./:]|"+Nm+`)(?:.|[\r -]))+`;Bl.KANJI=new RegExp(Nm,"g");Bl.BYTE_KANJI=new RegExp("[^A-Z0-9 $%*+\\-./:]+","g");Bl.BYTE=new RegExp(X3,"g");Bl.NUMERIC=new RegExp(nP,"g");Bl.ALPHANUMERIC=new RegExp(Y3,"g");var Q3=new RegExp("^"+Nm+"$"),Z3=new RegExp("^"+nP+"$"),J3=new RegExp("^[A-Z0-9 $%*+\\-./:]+$");Bl.testKanji=o(function(t){return Q3.test(t)},"testKanji");Bl.testNumeric=o(function(t){return Z3.test(t)},"testNumeric");Bl.testAlphanumeric=o(function(t){return J3.test(t)},"testAlphanumeric")});var mu=Ue(Xr=>{var eB=vC(),wC=yC();Xr.NUMERIC={id:"Numeric",bit:1<<0,ccBits:[10,12,14]};Xr.ALPHANUMERIC={id:"Alphanumeric",bit:1<<1,ccBits:[9,11,13]};Xr.BYTE={id:"Byte",bit:1<<2,ccBits:[8,16,16]};Xr.KANJI={id:"Kanji",bit:1<<3,ccBits:[8,10,12]};Xr.MIXED={bit:-1};Xr.getCharCountIndicator=o(function(t,n){if(!t.ccBits)throw new Error("Invalid mode: "+t);if(!eB.isValid(n))throw new Error("Invalid version: "+n);return n>=1&&n<10?t.ccBits[0]:n<27?t.ccBits[1]:t.ccBits[2]},"getCharCountIndicator");Xr.getBestModeForData=o(function(t){return wC.testNumeric(t)?Xr.NUMERIC:wC.testAlphanumeric(t)?Xr.ALPHANUMERIC:wC.testKanji(t)?Xr.KANJI:Xr.BYTE},"getBestModeForData");Xr.toString=o(function(t){if(t&&t.id)return t.id;throw new Error("Invalid mode")},"toString");Xr.isValid=o(function(t){return t&&t.bit&&t.ccBits},"isValid");function tB(e){if(typeof e!="string")throw new Error("Param is not a string");switch(e.toLowerCase()){case"numeric":return Xr.NUMERIC;case"alphanumeric":return Xr.ALPHANUMERIC;case"kanji":return Xr.KANJI;case"byte":return Xr.BYTE;default:throw new Error("Unknown mode: "+e)}}o(tB,"fromString");Xr.from=o(function(t,n){if(Xr.isValid(t))return t;try{return tB(t)}catch(l){return n}},"from")});var aP=Ue(Kf=>{var by=du(),rB=hC(),iP=vy(),gu=mu(),xC=vC(),oP=1<<12|1<<11|1<<10|1<<9|1<<8|1<<5|1<<2|1<<0,sP=by.getBCHDigit(oP);function nB(e,t,n){for(let l=1;l<=40;l++)if(t<=Kf.getCapacity(l,n,e))return l}o(nB,"getBestVersionForDataLength");function lP(e,t){return gu.getCharCountIndicator(e,t)+4}o(lP,"getReservedBitsCount");function iB(e,t){let n=0;return e.forEach(function(l){n+=lP(l.mode,t)+l.getBitsLength()}),n}o(iB,"getTotalBitsFromDataArray");function oB(e,t){for(let n=1;n<=40;n++)if(iB(e,n)<=Kf.getCapacity(n,t,gu.MIXED))return n}o(oB,"getBestVersionForMixedData");Kf.from=o(function(t,n){return xC.isValid(t)?parseInt(t,10):n},"from");Kf.getCapacity=o(function(t,n,l){if(!xC.isValid(t))throw new Error("Invalid QR Code version");typeof l=="undefined"&&(l=gu.BYTE);let d=by.getSymbolTotalCodewords(t),h=rB.getTotalCodewordsCount(t,n),c=(d-h)*8;if(l===gu.MIXED)return c;let v=c-lP(l,t);switch(l){case gu.NUMERIC:return Math.floor(v/10*3);case gu.ALPHANUMERIC:return Math.floor(v/11*2);case gu.KANJI:return Math.floor(v/13);case gu.BYTE:default:return Math.floor(v/8)}},"getCapacity");Kf.getBestVersionForData=o(function(t,n){let l,d=iP.from(n,iP.M);if(Array.isArray(t)){if(t.length>1)return oB(t,d);if(t.length===0)return 1;l=t[0]}else l=t;return nB(l.mode,l.getLength(),d)},"getBestVersionForData");Kf.getEncodedBits=o(function(t){if(!xC.isValid(t)||t<7)throw new Error("Invalid QR Code version");let n=t<<12;for(;by.getBCHDigit(n)-sP>=0;)n^=oP<{var SC=du(),uP=1<<10|1<<8|1<<5|1<<4|1<<2|1<<1|1<<0,sB=1<<14|1<<12|1<<10|1<<4|1<<1,fP=SC.getBCHDigit(uP);cP.getEncodedBits=o(function(t,n){let l=t.bit<<3|n,d=l<<10;for(;SC.getBCHDigit(d)-fP>=0;)d^=uP<{var lB=mu();function Kp(e){this.mode=lB.NUMERIC,this.data=e.toString()}o(Kp,"NumericData");Kp.getBitsLength=o(function(t){return 10*Math.floor(t/3)+(t%3?t%3*3+1:0)},"getBitsLength");Kp.prototype.getLength=o(function(){return this.data.length},"getLength");Kp.prototype.getBitsLength=o(function(){return Kp.getBitsLength(this.data.length)},"getBitsLength");Kp.prototype.write=o(function(t){let n,l,d;for(n=0;n+3<=this.data.length;n+=3)l=this.data.substr(n,3),d=parseInt(l,10),t.put(d,10);let h=this.data.length-n;h>0&&(l=this.data.substr(n),d=parseInt(l,10),t.put(d,h*3+1))},"write");dP.exports=Kp});var gP=Ue((c$,mP)=>{var aB=mu(),CC=["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"," ","$","%","*","+","-",".","/",":"];function Gp(e){this.mode=aB.ALPHANUMERIC,this.data=e}o(Gp,"AlphanumericData");Gp.getBitsLength=o(function(t){return 11*Math.floor(t/2)+6*(t%2)},"getBitsLength");Gp.prototype.getLength=o(function(){return this.data.length},"getLength");Gp.prototype.getBitsLength=o(function(){return Gp.getBitsLength(this.data.length)},"getBitsLength");Gp.prototype.write=o(function(t){let n;for(n=0;n+2<=this.data.length;n+=2){let l=CC.indexOf(this.data[n])*45;l+=CC.indexOf(this.data[n+1]),t.put(l,11)}this.data.length%2&&t.put(CC.indexOf(this.data[n]),6)},"write");mP.exports=Gp});var yP=Ue((p$,vP)=>{"use strict";vP.exports=o(function(t){for(var n=[],l=t.length,d=0;d=55296&&h<=56319&&l>d+1){var c=t.charCodeAt(d+1);c>=56320&&c<=57343&&(h=(h-55296)*1024+c-56320+65536,d+=1)}if(h<128){n.push(h);continue}if(h<2048){n.push(h>>6|192),n.push(h&63|128);continue}if(h<55296||h>=57344&&h<65536){n.push(h>>12|224),n.push(h>>6&63|128),n.push(h&63|128);continue}if(h>=65536&&h<=1114111){n.push(h>>18|240),n.push(h>>12&63|128),n.push(h>>6&63|128),n.push(h&63|128);continue}n.push(239,191,189)}return new Uint8Array(n).buffer},"encodeUtf8")});var xP=Ue((d$,wP)=>{var uB=yP(),fB=mu();function Yp(e){this.mode=fB.BYTE,typeof e=="string"&&(e=uB(e)),this.data=new Uint8Array(e)}o(Yp,"ByteData");Yp.getBitsLength=o(function(t){return t*8},"getBitsLength");Yp.prototype.getLength=o(function(){return this.data.length},"getLength");Yp.prototype.getBitsLength=o(function(){return Yp.getBitsLength(this.data.length)},"getBitsLength");Yp.prototype.write=function(e){for(let t=0,n=this.data.length;t{var cB=mu(),pB=du();function Xp(e){this.mode=cB.KANJI,this.data=e}o(Xp,"KanjiData");Xp.getBitsLength=o(function(t){return t*13},"getBitsLength");Xp.prototype.getLength=o(function(){return this.data.length},"getLength");Xp.prototype.getBitsLength=o(function(){return Xp.getBitsLength(this.data.length)},"getBitsLength");Xp.prototype.write=function(e){let t;for(t=0;t=33088&&n<=40956)n-=33088;else if(n>=57408&&n<=60351)n-=49472;else throw new Error("Invalid SJIS character: "+this.data[t]+` -Make sure your charset is UTF-8`);n=(n>>>8&255)*192+(n&255),e.put(n,13)}};SP.exports=Xp});var bP=Ue((m$,bC)=>{"use strict";var Lm={single_source_shortest_paths:function(e,t,n){var l={},d={};d[t]=0;var h=Lm.PriorityQueue.make();h.push(t,0);for(var c,v,C,k,O,j,B,X,J;!h.empty();){c=h.pop(),v=c.value,k=c.cost,O=e[v]||{};for(C in O)O.hasOwnProperty(C)&&(j=O[C],B=k+j,X=d[C],J=typeof d[C]=="undefined",(J||X>B)&&(d[C]=B,h.push(C,B),l[C]=v))}if(typeof n!="undefined"&&typeof d[n]=="undefined"){var Z=["Could not find a path from ",t," to ",n,"."].join("");throw new Error(Z)}return l},extract_shortest_path_from_predecessor_list:function(e,t){for(var n=[],l=t,d;l;)n.push(l),d=e[l],l=e[l];return n.reverse(),n},find_path:function(e,t,n){var l=Lm.single_source_shortest_paths(e,t,n);return Lm.extract_shortest_path_from_predecessor_list(l,n)},PriorityQueue:{make:function(e){var t=Lm.PriorityQueue,n={},l;e=e||{};for(l in t)t.hasOwnProperty(l)&&(n[l]=t[l]);return n.queue=[],n.sorter=e.sorter||t.default_sorter,n},default_sorter:function(e,t){return e.cost-t.cost},push:function(e,t){var n={value:e,cost:t};this.queue.push(n),this.queue.sort(this.sorter)},pop:function(){return this.queue.shift()},empty:function(){return this.queue.length===0}}};typeof bC!="undefined"&&(bC.exports=Lm)});var OP=Ue(Qp=>{var It=mu(),EP=hP(),_P=gP(),TP=xP(),kP=CP(),Pm=yC(),Ey=du(),dB=bP();function NP(e){return unescape(encodeURIComponent(e)).length}o(NP,"getStringByteLength");function Om(e,t,n){let l=[],d;for(;(d=e.exec(n))!==null;)l.push({data:d[0],index:d.index,mode:t,length:d[0].length});return l}o(Om,"getSegments");function LP(e){let t=Om(Pm.NUMERIC,It.NUMERIC,e),n=Om(Pm.ALPHANUMERIC,It.ALPHANUMERIC,e),l,d;return Ey.isKanjiModeEnabled()?(l=Om(Pm.BYTE,It.BYTE,e),d=Om(Pm.KANJI,It.KANJI,e)):(l=Om(Pm.BYTE_KANJI,It.BYTE,e),d=[]),t.concat(n,l,d).sort(function(c,v){return c.index-v.index}).map(function(c){return{data:c.data,mode:c.mode,length:c.length}})}o(LP,"getSegmentsFromString");function EC(e,t){switch(t){case It.NUMERIC:return EP.getBitsLength(e);case It.ALPHANUMERIC:return _P.getBitsLength(e);case It.KANJI:return kP.getBitsLength(e);case It.BYTE:return TP.getBitsLength(e)}}o(EC,"getSegmentBitsLength");function hB(e){return e.reduce(function(t,n){let l=t.length-1>=0?t[t.length-1]:null;return l&&l.mode===n.mode?(t[t.length-1].data+=n.data,t):(t.push(n),t)},[])}o(hB,"mergeSegments");function mB(e){let t=[];for(let n=0;n{var _y=du(),_C=vy(),vB=$L(),yB=qL(),wB=VL(),xB=YL(),TC=XL(),kC=hC(),SB=tP(),Ty=aP(),CB=pP(),bB=mu(),NC=OP();function EB(e,t){let n=e.size,l=xB.getPositions(t);for(let d=0;d=0&&v<=6&&(C===0||C===6)||C>=0&&C<=6&&(v===0||v===6)||v>=2&&v<=4&&C>=2&&C<=4?e.set(h+v,c+C,!0,!0):e.set(h+v,c+C,!1,!0))}}o(EB,"setupFinderPattern");function _B(e){let t=e.size;for(let n=8;n>v&1)==1,e.set(d,h,c,!0),e.set(h,d,c,!0)}o(kB,"setupVersionInfo");function LC(e,t,n){let l=e.size,d=CB.getEncodedBits(t,n),h,c;for(h=0;h<15;h++)c=(d>>h&1)==1,h<6?e.set(h,8,c,!0):h<8?e.set(h+1,8,c,!0):e.set(l-15+h,8,c,!0),h<8?e.set(8,l-h-1,c,!0):h<9?e.set(8,15-h-1+1,c,!0):e.set(8,15-h-1,c,!0);e.set(l-8,8,1,!0)}o(LC,"setupFormatInfo");function NB(e,t){let n=e.size,l=-1,d=n-1,h=7,c=0;for(let v=n-1;v>0;v-=2)for(v===6&&v--;;){for(let C=0;C<2;C++)if(!e.isReserved(d,v-C)){let k=!1;c>>h&1)==1),e.set(d,v-C,k),h--,h===-1&&(c++,h=7)}if(d+=l,d<0||n<=d){d-=l,l=-l;break}}}o(NB,"setupData");function LB(e,t,n){let l=new vB;n.forEach(function(C){l.put(C.mode.bit,4),l.put(C.getLength(),bB.getCharCountIndicator(C.mode,e)),C.write(l)});let d=_y.getSymbolTotalCodewords(e),h=kC.getTotalCodewordsCount(e,t),c=(d-h)*8;for(l.getLengthInBits()+4<=c&&l.put(0,4);l.getLengthInBits()%8!=0;)l.putBit(0);let v=(c-l.getLengthInBits())/8;for(let C=0;C1}i(Vf,"hasBadZoomedRects");var ac={},vs={};function Up(r,o){arguments.length>2&&(o.dependencies=Array.prototype.slice.call(arguments,2)),ac[r]=o}i(Up,"defineMode");function Wp(r,o){vs[r]=o}i(Wp,"defineMIME");function La(r){if(typeof r=="string"&&vs.hasOwnProperty(r))r=vs[r];else if(r&&typeof r.name=="string"&&vs.hasOwnProperty(r.name)){var o=vs[r.name];typeof o=="string"&&(o={name:o}),r=io(o,r),r.name=o.name}else{if(typeof r=="string"&&/^[\w\-]+\/[\w\-]+\+xml$/.test(r))return La("application/xml");if(typeof r=="string"&&/^[\w\-]+\/[\w\-]+\+json$/.test(r))return La("application/json")}return typeof r=="string"?{name:r}:r||{name:"null"}}i(La,"resolveMode");function lc(r,o){o=La(o);var c=ac[o.name];if(!c)return lc(r,"text/plain");var u=c(r,o);if(ao.hasOwnProperty(o.name)){var p=ao[o.name];for(var g in p)p.hasOwnProperty(g)&&(u.hasOwnProperty(g)&&(u["_"+g]=u[g]),u[g]=p[g])}if(u.name=o.name,o.helperType&&(u.helperType=o.helperType),o.modeProps)for(var y in o.modeProps)u[y]=o.modeProps[y];return u}i(lc,"getMode");var ao={};function qf(r,o){var c=ao.hasOwnProperty(r)?ao[r]:ao[r]={};Gt(o,c)}i(qf,"extendMode");function Oi(r,o){if(o===!0)return o;if(r.copyState)return r.copyState(o);var c={};for(var u in o){var p=o[u];p instanceof Array&&(p=p.concat([])),c[u]=p}return c}i(Oi,"copyState");function uc(r,o){for(var c;r.innerMode&&(c=r.innerMode(o),!(!c||c.mode==r));)o=c.state,r=c.mode;return c||{mode:r,state:o}}i(uc,"innerMode");function cc(r,o,c){return r.startState?r.startState(o,c):!0}i(cc,"startState");var or=i(function(r,o,c){this.pos=this.start=0,this.string=r,this.tabSize=o||8,this.lastColumnPos=this.lastColumnValue=0,this.lineStart=0,this.lineOracle=c},"StringStream");or.prototype.eol=function(){return this.pos>=this.string.length},or.prototype.sol=function(){return this.pos==this.lineStart},or.prototype.peek=function(){return this.string.charAt(this.pos)||void 0},or.prototype.next=function(){if(this.poso},or.prototype.eatSpace=function(){for(var r=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>r},or.prototype.skipToEnd=function(){this.pos=this.string.length},or.prototype.skipTo=function(r){var o=this.string.indexOf(r,this.pos);if(o>-1)return this.pos=o,!0},or.prototype.backUp=function(r){this.pos-=r},or.prototype.column=function(){return this.lastColumnPos0?null:(g&&o!==!1&&(this.pos+=g[0].length),g)}},or.prototype.current=function(){return this.string.slice(this.start,this.pos)},or.prototype.hideFirstChars=function(r,o){this.lineStart+=r;try{return o()}finally{this.lineStart-=r}},or.prototype.lookAhead=function(r){var o=this.lineOracle;return o&&o.lookAhead(r)},or.prototype.baseToken=function(){var r=this.lineOracle;return r&&r.baseToken(this.pos)};function Ae(r,o){if(o-=r.first,o<0||o>=r.size)throw new Error("There is no line "+(o+r.first)+" in the document.");for(var c=r;!c.lines;)for(var u=0;;++u){var p=c.children[u],g=p.chunkSize();if(o=r.first&&oc?ce(c,Ae(r,c).text.length):jf(o,Ae(r,o.line).text.length)}i(qe,"clipPos");function jf(r,o){var c=r.ch;return c==null||c>o?ce(r.line,o):c<0?ce(r.line,0):r}i(jf,"clipToLen");function Al(r,o){for(var c=[],u=0;uthis.maxLookAhead&&(this.maxLookAhead=r),o},mi.prototype.baseToken=function(r){if(!this.baseTokens)return null;for(;this.baseTokens[this.baseTokenPos]<=r;)this.baseTokenPos+=2;var o=this.baseTokens[this.baseTokenPos+1];return{type:o&&o.replace(/( |^)overlay .*/,""),size:this.baseTokens[this.baseTokenPos]-r}},mi.prototype.nextLine=function(){this.line++,this.maxLookAhead>0&&this.maxLookAhead--},mi.fromSaved=function(r,o,c){return o instanceof ws?new mi(r,Oi(r.mode,o.state),c,o.lookAhead):new mi(r,Oi(r.mode,o),c)},mi.prototype.save=function(r){var o=r!==!1?Oi(this.doc.mode,this.state):this.state;return this.maxLookAhead>0?new ws(o,this.maxLookAhead):o};function Ll(r,o,c,u){var p=[r.state.modeGen],g={};mc(r,o.text,r.doc.mode,c,function(E,O){return p.push(E,O)},g,u);for(var y=c.state,C=i(function(E){c.baseTokens=p;var O=r.state.overlays[E],B=1,K=0;c.state=!0,mc(r,o.text,O.mode,c,function(V,re){for(var ae=B;KV&&p.splice(B,1,V,p[B+1],pe),B+=2,K=Math.min(V,pe)}if(re)if(O.opaque)p.splice(ae,B-ae,V,"overlay "+re),B=ae+2;else for(;aer.options.maxHighlightLength&&Oi(r.doc.mode,u.state),g=Ll(r,o,u);p&&(u.state=p),o.stateAfter=u.save(!p),o.styles=g.styles,g.classes?o.styleClasses=g.classes:o.styleClasses&&(o.styleClasses=null),c===r.doc.highlightFrontier&&(r.doc.modeFrontier=Math.max(r.doc.modeFrontier,++r.doc.highlightFrontier))}return o.styles}i(Ml,"getLineStyles");function Ss(r,o,c){var u=r.doc,p=r.display;if(!u.mode.startState)return new mi(u,!0,o);var g=Dl(r,o,c),y=g>u.first&&Ae(u,g-1).stateAfter,C=y?mi.fromSaved(u,y,g):new mi(u,cc(u.mode),g);return u.iter(g,o,function(_){uo(r,_.text,C);var E=C.line;_.stateAfter=E==o-1||E%5==0||E>=p.viewFrom&&Eo.start)return g}throw new Error("Mode "+r.name+" failed to advance stream.")}i(Ks,"readToken");var pc=i(function(r,o,c){this.start=r.start,this.end=r.pos,this.string=r.current(),this.type=o||null,this.state=c},"Token");function Rl(r,o,c,u){var p=r.doc,g=p.mode,y;o=qe(p,o);var C=Ae(p,o.line),_=Ss(r,o.line,c),E=new or(C.text,r.options.tabSize,_),O;for(u&&(O=[]);(u||E.posr.options.maxHighlightLength?(C=!1,y&&uo(r,o,u,O.pos),O.pos=o.length,B=null):B=hc(Ks(c,O,u.state,K),g),K){var V=K[0].name;V&&(B="m-"+(B?V+" "+B:V))}if(!C||E!=B){for(;_y;--C){if(C<=g.first)return g.first;var _=Ae(g,C-1),E=_.stateAfter;if(E&&(!c||C+(E instanceof ws?E.lookAhead:0)<=g.modeFrontier))return C;var O=Ft(_.text,null,r.options.tabSize);(p==null||u>O)&&(p=C-1,u=O)}return p}i(Dl,"findStartLine");function Ra(r,o){if(r.modeFrontier=Math.min(r.modeFrontier,o),!(r.highlightFrontierc;u--){var p=Ae(r,u).stateAfter;if(p&&(!(p instanceof ws)||u+p.lookAhead=o:g.to>o);(u||(u=[])).push(new Il(y,g.from,_?null:g.to))}}return u}i(qp,"markedSpansBefore");function jp(r,o,c){var u;if(r)for(var p=0;p=o:g.to>o);if(C||g.from==o&&y.type=="bookmark"&&(!c||g.marker.insertLeft)){var _=g.from==null||(y.inclusiveLeft?g.from<=o:g.from0&&C)for(var ke=0;ke0)){var O=[_,1],B=Ve(E.from,C.from),K=Ve(E.to,C.to);(B<0||!y.inclusiveLeft&&!B)&&O.push({from:E.from,to:C.from}),(K>0||!y.inclusiveRight&&!K)&&O.push({from:C.to,to:E.to}),p.splice.apply(p,O),_+=O.length-3}}return p}i(Kp,"removeReadOnlyRanges");function Bt(r){var o=r.markedSpans;if(o){for(var c=0;co)&&(!u||Gs(u,g.marker)<0)&&(u=g.marker)}return u}i(Ia,"collapsedSpanAround");function Gp(r,o,c,u,p){var g=Ae(r,o),y=Di&&g.markedSpans;if(y)for(var C=0;C=0&&B<=0||O<=0&&B>=0)&&(O<=0&&(_.marker.inclusiveRight&&p.inclusiveLeft?Ve(E.to,c)>=0:Ve(E.to,c)>0)||O>=0&&(_.marker.inclusiveRight&&p.inclusiveLeft?Ve(E.from,u)<=0:Ve(E.from,u)<0)))return!0}}}i(Gp,"conflictingCollapsedRange");function Rt(r){for(var o;o=Fl(r);)r=o.find(-1,!0).line;return r}i(Rt,"visualLine");function Yf(r){for(var o;o=ve(r);)r=o.find(1,!0).line;return r}i(Yf,"visualLineEnd");function Ys(r){for(var o,c;o=ve(r);)r=o.find(1,!0).line,(c||(c=[])).push(r);return c}i(Ys,"visualLineContinued");function Fa(r,o){var c=Ae(r,o),u=Rt(c);return c==u?o:vt(u)}i(Fa,"visualLineNo");function Uo(r,o){if(o>r.lastLine())return o;var c=Ae(r,o),u;if(!Se(r,c))return o;for(;u=ve(c);)c=u.find(1,!0).line;return vt(c)+1}i(Uo,"visualLineEndNo");function Se(r,o){var c=Di&&o.markedSpans;if(c){for(var u=void 0,p=0;po.maxLineLength&&(o.maxLineLength=p,o.maxLine=u)})}i(Bl,"findMaxLine");var hn=i(function(r,o,c){this.text=r,Ot(this,o),this.height=c?c(this):1},"Line");hn.prototype.lineNo=function(){return vt(this)},zn(hn);function Hl(r,o,c,u){r.text=o,r.stateAfter&&(r.stateAfter=null),r.styles&&(r.styles=null),r.order!=null&&(r.order=null),Bt(r),Ot(r,c);var p=u?u(r):1;p!=r.height&&Ri(r,p)}i(Hl,"updateLine");function Jr(r){r.parent=null,Bt(r)}i(Jr,"cleanUpLine");var zl={},Zn={};function Xf(r,o){if(!r||/^\s*$/.test(r))return null;var c=o.addModeClass?Zn:zl;return c[r]||(c[r]=r.replace(/\S+/g,"cm-$&"))}i(Xf,"interpretTokenStyle");function wc(r,o){var c=Xe("span",null,null,S?"padding-right: .1px":null),u={pre:Xe("pre",[c],"CodeMirror-line"),content:c,col:0,pos:0,cm:r,trailingSpace:!1,splitSpaces:r.getOption("lineWrapping")};o.measure={};for(var p=0;p<=(o.rest?o.rest.length:0);p++){var g=p?o.rest[p-1]:o.line,y=void 0;u.pos=0,u.addToken=Ba,Bp(r.display.measure)&&(y=Me(g,r.doc.direction))&&(u.addToken=$t(u.addToken,y)),u.map=[];var C=o!=r.display.externalMeasured&&vt(g);Sc(g,u,Ml(r,g,C)),g.styleClasses&&(g.styleClasses.bgClass&&(u.bgClass=Pt(g.styleClasses.bgClass,u.bgClass||"")),g.styleClasses.textClass&&(u.textClass=Pt(g.styleClasses.textClass,u.textClass||""))),u.map.length==0&&u.map.push(0,0,u.content.appendChild($f(r.display.measure))),p==0?(o.measure.map=u.map,o.measure.cache={}):((o.measure.maps||(o.measure.maps=[])).push(u.map),(o.measure.caches||(o.measure.caches=[])).push({}))}if(S){var _=u.content.lastChild;(/\bcm-tab\b/.test(_.className)||_.querySelector&&_.querySelector(".cm-tab"))&&(u.content.className="cm-tab-wrap-hack")}return Mt(r,"renderLine",r,o.line,u.pre),u.pre.className&&(u.textClass=Pt(u.pre.className,u.textClass||"")),u}i(wc,"buildLineContent");function jt(r){var o=xe("span","\u2022","cm-invalidchar");return o.title="\\u"+r.charCodeAt(0).toString(16),o.setAttribute("aria-label",o.title),o}i(jt,"defaultSpecialCharPlaceholder");function Ba(r,o,c,u,p,g,y){if(o){var C=r.splitSpaces?Ul(o,r.trailingSpace):o,_=r.cm.state.specialChars,E=!1,O;if(!_.test(o))r.col+=o.length,O=document.createTextNode(C),r.map.push(r.pos,r.pos+o.length,O),d&&v<9&&(E=!0),r.pos+=o.length;else{O=document.createDocumentFragment();for(var B=0;;){_.lastIndex=B;var K=_.exec(o),V=K?K.index-B:o.length-B;if(V){var re=document.createTextNode(C.slice(B,B+V));d&&v<9?O.appendChild(xe("span",[re])):O.appendChild(re),r.map.push(r.pos,r.pos+V,re),r.col+=V,r.pos+=V}if(!K)break;B+=V+1;var ae=void 0;if(K[0]==" "){var pe=r.cm.options.tabSize,we=pe-r.col%pe;ae=O.appendChild(xe("span",Ho(we),"cm-tab")),ae.setAttribute("role","presentation"),ae.setAttribute("cm-text"," "),r.col+=we}else K[0]=="\r"||K[0]==` +`?(ae=O.appendChild(xe("span",K[0]=="\r"?"\u240D":"\u2424","cm-invalidchar")),ae.setAttribute("cm-text",K[0]),r.col+=1):(ae=r.cm.options.specialCharPlaceholder(K[0]),ae.setAttribute("cm-text",K[0]),d&&v<9?O.appendChild(xe("span",[ae])):O.appendChild(ae),r.col+=1);r.map.push(r.pos,r.pos+1,ae),r.pos++}}if(r.trailingSpace=C.charCodeAt(o.length-1)==32,c||u||p||E||g||y){var _e=c||"";u&&(_e+=u),p&&(_e+=p);var ye=xe("span",[O],_e,g);if(y)for(var ke in y)y.hasOwnProperty(ke)&&ke!="style"&&ke!="class"&&ye.setAttribute(ke,y[ke]);return r.content.appendChild(ye)}r.content.appendChild(O)}}i(Ba,"buildToken");function Ul(r,o){if(r.length>1&&!/ /.test(r))return r;for(var c=o,u="",p=0;pE&&B.from<=E));K++);if(B.to>=O)return r(c,u,p,g,y,C,_);r(c,u.slice(0,B.to-E),p,g,null,C,_),g=null,u=u.slice(B.to-E),E=B.to}}}i($t,"buildTokenBadBidi");function Ii(r,o,c,u){var p=!u&&c.widgetNode;p&&r.map.push(r.pos,r.pos+o,p),!u&&r.cm.display.input.needsContentAttribute&&(p||(p=r.content.appendChild(document.createElement("span"))),p.setAttribute("cm-marker",c.id)),p&&(r.cm.display.input.setUneditable(p),r.content.appendChild(p)),r.pos+=o,r.trailingSpace=!1}i(Ii,"buildCollapsedSpan");function Sc(r,o,c){var u=r.markedSpans,p=r.text,g=0;if(!u){for(var y=1;y_||it.collapsed&&Oe.to==_&&Oe.from==_)){if(Oe.to!=null&&Oe.to!=_&&V>Oe.to&&(V=Oe.to,ae=""),it.className&&(re+=" "+it.className),it.css&&(K=(K?K+";":"")+it.css),it.startStyle&&Oe.from==_&&(pe+=" "+it.startStyle),it.endStyle&&Oe.to==V&&(ke||(ke=[])).push(it.endStyle,Oe.to),it.title&&((_e||(_e={})).title=it.title),it.attributes)for(var Dt in it.attributes)(_e||(_e={}))[Dt]=it.attributes[Dt];it.collapsed&&(!we||Gs(we.marker,it)<0)&&(we=Oe)}else Oe.from>_&&V>Oe.from&&(V=Oe.from)}if(ke)for(var pr=0;pr=C)break;for(var ar=Math.min(C,V);;){if(O){var Sr=_+O.length;if(!we){var xr=Sr>ar?O.slice(0,ar-_):O;o.addToken(o,xr,B?B+re:re,pe,_+xr.length==V?ae:"",K,_e)}if(Sr>=ar){O=O.slice(ar-_),_=ar;break}_=Sr,pe=""}O=p.slice(g,g=c[E++]),B=Xf(c[E++],o.cm.options)}}}i(Sc,"insertLineContent");function Qf(r,o,c){this.line=o,this.rest=Ys(o),this.size=this.rest?vt(lt(this.rest))-c+1:1,this.node=this.text=null,this.hidden=Se(r,o)}i(Qf,"LineView");function Qs(r,o,c){for(var u=[],p,g=o;g2&&g.push((_.bottom+E.top)/2-c.top)}}g.push(c.bottom-c.top)}}i(xs,"ensureLineHeights");function td(r,o,c){if(r.line==o)return{map:r.measure.map,cache:r.measure.cache};if(r.rest){for(var u=0;uc)return{map:r.measure.maps[p],cache:r.measure.caches[p],before:!0}}}i(td,"mapFromLineView");function Qp(r,o){o=Rt(o);var c=vt(o),u=r.display.externalMeasured=new Qf(r.doc,o,c);u.lineN=c;var p=u.built=wc(r,u);return u.text=p.pre,tt(r.display.lineMeasure,p.pre),u}i(Qp,"updateExternalMeasurement");function Kl(r,o,c,u){return w(r,kn(r,o),c,u)}i(Kl,"measureChar");function Gl(r,o){if(o>=r.display.viewFrom&&o=c.lineN&&oo)&&(g=_-C,p=g-1,o>=_&&(y="right")),p!=null){if(u=r[E+2],C==_&&c==(u.insertLeft?"left":"right")&&(y=c),c=="left"&&p==0)for(;E&&r[E-2]==r[E-3]&&r[E-1].insertLeft;)u=r[(E-=3)+2],y="left";if(c=="right"&&p==_-C)for(;E=0&&(c=r[p]).left==c.right;p--);return c}i(W,"getUsefulRect");function gn(r,o,c,u){var p=U(o.map,c,u),g=p.node,y=p.start,C=p.end,_=p.collapse,E;if(g.nodeType==3){for(var O=0;O<4;O++){for(;y&&hi(o.line.text.charAt(p.coverStart+y));)--y;for(;p.coverStart+C0&&(_=u="right");var B;r.options.lineWrapping&&(B=g.getClientRects()).length>1?E=B[u=="right"?B.length-1:0]:E=g.getBoundingClientRect()}if(d&&v<9&&!y&&(!E||!E.left&&!E.right)){var K=g.parentNode.getClientRects()[0];K?E={left:K.left,right:K.left+Cs(r.display),top:K.top,bottom:K.bottom}:E=N}for(var V=E.top-o.rect.top,re=E.bottom-o.rect.top,ae=(V+re)/2,pe=o.view.measure.heights,we=0;we=u.text.length?(_=u.text.length,E="before"):_<=0&&(_=0,E="after"),!C)return y(E=="before"?_-1:_,E=="before");function O(re,ae,pe){var we=C[ae],_e=we.level==1;return y(pe?re-1:re,_e!=pe)}i(O,"getBidi");var B=ee(C,_,E),K=X,V=O(_,B,E=="before");return K!=null&&(V.other=O(_,K,E!="before")),V}i(J,"cursorCoords");function Pe(r,o){var c=0;o=qe(r.doc,o),r.options.lineWrapping||(c=Cs(r.display)*o.ch);var u=Ae(r.doc,o.line),p=vi(u)+ea(r.display);return{left:c,right:c,top:p,bottom:p+u.height}}i(Pe,"estimateCoords");function Je(r,o,c,u,p){var g=ce(r,o,c);return g.xRel=p,u&&(g.outside=u),g}i(Je,"PosWithInfo");function yt(r,o,c){var u=r.doc;if(c+=r.display.viewOffset,c<0)return Je(u.first,0,null,-1,-1);var p=ys(u,c),g=u.first+u.size-1;if(p>g)return Je(u.first+u.size-1,Ae(u,g).text.length,null,1,1);o<0&&(o=0);for(var y=Ae(u,p);;){var C=pw(r,y,p,o,c),_=Ia(y,C.ch+(C.xRel>0||C.outside>0?1:0));if(!_)return C;var E=_.find(1);if(E.line==p)return E;y=Ae(u,p=E.line)}}i(yt,"coordsChar");function Nr(r,o,c,u){u-=Yl(o);var p=o.text.length,g=so(function(y){return w(r,c,y-1).bottom<=u},p,0);return p=so(function(y){return w(r,c,y).top>u},g,p),{begin:g,end:p}}i(Nr,"wrappedLineExtent");function Ht(r,o,c,u){c||(c=kn(r,o));var p=vn(r,o,w(r,c,u),"line").top;return Nr(r,o,c,p)}i(Ht,"wrappedLineExtentChar");function Nc(r,o,c,u){return r.bottom<=c?!1:r.top>c?!0:(u?r.left:r.right)>o}i(Nc,"boxIsAfter");function pw(r,o,c,u,p){p-=vi(o);var g=kn(r,o),y=Yl(o),C=0,_=o.text.length,E=!0,O=Me(o,r.doc.direction);if(O){var B=(r.options.lineWrapping?rd:Bi)(r,o,c,g,O,u,p);E=B.level!=1,C=E?B.from:B.to-1,_=E?B.to:B.from-1}var K=null,V=null,re=so(function(Ie){var Oe=w(r,g,Ie);return Oe.top+=y,Oe.bottom+=y,Nc(Oe,u,p,!1)?(Oe.top<=p&&Oe.left<=u&&(K=Ie,V=Oe),!0):!1},C,_),ae,pe,we=!1;if(V){var _e=u-V.left=ke.bottom?1:0}return re=Hn(o.text,re,1),Je(c,re,pe,we,u-ae)}i(pw,"coordsCharInner");function Bi(r,o,c,u,p,g,y){var C=so(function(B){var K=p[B],V=K.level!=1;return Nc(J(r,ce(c,V?K.to:K.from,V?"before":"after"),"line",o,u),g,y,!0)},0,p.length-1),_=p[C];if(C>0){var E=_.level!=1,O=J(r,ce(c,E?_.from:_.to,E?"after":"before"),"line",o,u);Nc(O,g,y,!0)&&O.top>y&&(_=p[C-1])}return _}i(Bi,"coordsBidiPart");function rd(r,o,c,u,p,g,y){var C=Nr(r,o,u,y),_=C.begin,E=C.end;/\s/.test(o.text.charAt(E-1))&&E--;for(var O=null,B=null,K=0;K=E||V.to<=_)){var re=V.level!=1,ae=w(r,u,re?Math.min(E,V.to)-1:Math.max(_,V.from)).right,pe=aepe)&&(O=V,B=pe)}}return O||(O=p[p.length-1]),O.from<_&&(O={from:_,to:O.to,level:O.level}),O.to>E&&(O={from:O.from,to:E,level:O.level}),O}i(rd,"coordsBidiPartWrapped");var za;function po(r){if(r.cachedTextHeight!=null)return r.cachedTextHeight;if(za==null){za=xe("pre",null,"CodeMirror-line-like");for(var o=0;o<49;++o)za.appendChild(document.createTextNode("x")),za.appendChild(xe("br"));za.appendChild(document.createTextNode("x"))}tt(r.measure,za);var c=za.offsetHeight/50;return c>3&&(r.cachedTextHeight=c),Ye(r.measure),c||1}i(po,"textHeight");function Cs(r){if(r.cachedCharWidth!=null)return r.cachedCharWidth;var o=xe("span","xxxxxxxxxx"),c=xe("pre",[o],"CodeMirror-line-like");tt(r.measure,c);var u=o.getBoundingClientRect(),p=(u.right-u.left)/10;return p>2&&(r.cachedCharWidth=p),p||10}i(Cs,"charWidth");function Xl(r){for(var o=r.display,c={},u={},p=o.gutters.clientLeft,g=o.gutters.firstChild,y=0;g;g=g.nextSibling,++y){var C=r.display.gutterSpecs[y].className;c[C]=g.offsetLeft+g.clientLeft+p,u[C]=g.clientWidth}return{fixedPos:kt(o),gutterTotalWidth:o.gutters.offsetWidth,gutterLeft:c,gutterWidth:u,wrapperWidth:o.wrapper.clientWidth}}i(Xl,"getDimensions");function kt(r){return r.scroller.getBoundingClientRect().left-r.sizer.getBoundingClientRect().left}i(kt,"compensateForHScroll");function mg(r){var o=po(r.display),c=r.options.lineWrapping,u=c&&Math.max(5,r.display.scroller.clientWidth/Cs(r.display)-3);return function(p){if(Se(r.doc,p))return 0;var g=0;if(p.widgets)for(var y=0;y0&&(E=Ae(r.doc,_.line).text).length==_.ch){var O=Ft(E,E.length,r.options.tabSize)-E.length;_=ce(_.line,Math.max(0,Math.round((g-Tc(r.display).left)/Cs(r.display))-O))}return _}i(jo,"posFromMouse");function Ua(r,o){if(o>=r.display.viewTo||(o-=r.display.viewFrom,o<0))return null;for(var c=r.display.view,u=0;uo)&&(p.updateLineNumbers=o),r.curOp.viewChanged=!0,o>=p.viewTo)Di&&Fa(r.doc,o)p.viewFrom?ho(r):(p.viewFrom+=u,p.viewTo+=u);else if(o<=p.viewFrom&&c>=p.viewTo)ho(r);else if(o<=p.viewFrom){var g=mo(r,c,c+u,1);g?(p.view=p.view.slice(g.index),p.viewFrom=g.lineN,p.viewTo+=u):ho(r)}else if(c>=p.viewTo){var y=mo(r,o,o,-1);y?(p.view=p.view.slice(0,y.index),p.viewTo=y.lineN):ho(r)}else{var C=mo(r,o,o,-1),_=mo(r,c,c+u,1);C&&_?(p.view=p.view.slice(0,C.index).concat(Qs(r,C.lineN,_.lineN)).concat(p.view.slice(_.index)),p.viewTo+=u):ho(r)}var E=p.externalMeasured;E&&(c=p.lineN&&o=u.viewTo)){var g=u.view[Ua(r,o)];if(g.node!=null){var y=g.changes||(g.changes=[]);Ue(y,c)==-1&&y.push(c)}}}i(ta,"regLineChange");function ho(r){r.display.viewFrom=r.display.viewTo=r.doc.first,r.display.view=[],r.display.viewOffset=0}i(ho,"resetView");function mo(r,o,c,u){var p=Ua(r,o),g,y=r.display.view;if(!Di||c==r.doc.first+r.doc.size)return{index:p,lineN:c};for(var C=r.display.viewFrom,_=0;_0){if(p==y.length-1)return null;g=C+y[p].size-o,p++}else g=C-o;o+=g,c+=g}for(;Fa(r.doc,c)!=c;){if(p==(u<0?0:y.length-1))return null;c+=u*y[p-(u<0?1:0)].size,p+=u}return{index:p,lineN:c}}i(mo,"viewCuttingPoint");function id(r,o,c){var u=r.display,p=u.view;p.length==0||o>=u.viewTo||c<=u.viewFrom?(u.view=Qs(r,o,c),u.viewFrom=o):(u.viewFrom>o?u.view=Qs(r,o,u.viewFrom).concat(u.view):u.viewFromc&&(u.view=u.view.slice(0,Ua(r,c)))),u.viewTo=c}i(id,"adjustView");function Jp(r){for(var o=r.display.view,c=0,u=0;u=r.display.viewTo||_.to().line0?y:r.defaultCharWidth())+"px"}if(u.other){var C=c.appendChild(xe("div","\xA0","CodeMirror-cursor CodeMirror-secondarycursor"));C.style.display="",C.style.left=u.other.left+"px",C.style.top=u.other.top+"px",C.style.height=(u.other.bottom-u.other.top)*.85+"px"}}i(od,"drawSelectionCursor");function Wa(r,o){return r.top-o.top||r.left-o.left}i(Wa,"cmpCoords");function gg(r,o,c){var u=r.display,p=r.doc,g=document.createDocumentFragment(),y=Tc(r.display),C=y.left,_=Math.max(u.sizerWidth,$o(r)-u.sizer.offsetLeft)-y.right,E=p.direction=="ltr";function O(ye,ke,Ie,Oe){ke<0&&(ke=0),ke=Math.round(ke),Oe=Math.round(Oe),g.appendChild(xe("div",null,"CodeMirror-selected","position: absolute; left: "+ye+`px; + top: `+ke+"px; width: "+(Ie??_-ye)+`px; + height: `+(Oe-ke)+"px"))}i(O,"add");function B(ye,ke,Ie){var Oe=Ae(p,ye),it=Oe.text.length,Dt,pr;function Qt(xr,Pn){return ie(r,ce(ye,xr),"div",Oe,Pn)}i(Qt,"coords");function ar(xr,Pn,tn){var Ar=Ht(r,Oe,null,xr),Cr=Pn=="ltr"==(tn=="after")?"left":"right",lr=tn=="after"?Ar.begin:Ar.end-(/\s/.test(Oe.text.charAt(Ar.end-1))?2:1);return Qt(lr,Cr)[Cr]}i(ar,"wrapX");var Sr=Me(Oe,p.direction);return bl(Sr,ke||0,Ie??it,function(xr,Pn,tn,Ar){var Cr=tn=="ltr",lr=Qt(xr,Cr?"left":"right"),$n=Qt(Pn-1,Cr?"right":"left"),yu=ke==null&&xr==0,ha=Ie==null&&Pn==it,wn=Ar==0,Jo=!Sr||Ar==Sr.length-1;if($n.top-lr.top<=3){var Ur=(E?yu:ha)&&wn,Ph=(E?ha:yu)&&Jo,Zo=Ur?C:(Cr?lr:$n).left,ma=Ph?_:(Cr?$n:lr).right;O(Zo,lr.top,ma-Zo,lr.bottom)}else{var ga,An,wu,Ah;Cr?(ga=E&&yu&&wn?C:lr.left,An=E?_:ar(xr,tn,"before"),wu=E?C:ar(Pn,tn,"after"),Ah=E&&ha&&Jo?_:$n.right):(ga=E?ar(xr,tn,"before"):C,An=!E&&yu&&wn?_:lr.right,wu=!E&&ha&&Jo?C:$n.left,Ah=E?ar(Pn,tn,"after"):_),O(ga,lr.top,An-ga,lr.bottom),lr.bottom<$n.top&&O(C,lr.bottom,null,$n.top),O(wu,$n.top,Ah-wu,$n.bottom)}(!Dt||Wa(lr,Dt)<0)&&(Dt=lr),Wa($n,Dt)<0&&(Dt=$n),(!pr||Wa(lr,pr)<0)&&(pr=lr),Wa($n,pr)<0&&(pr=$n)}),{start:Dt,end:pr}}i(B,"drawForLine");var K=o.from(),V=o.to();if(K.line==V.line)B(K.line,K.ch,V.ch);else{var re=Ae(p,K.line),ae=Ae(p,V.line),pe=Rt(re)==Rt(ae),we=B(K.line,K.ch,pe?re.text.length+1:null).end,_e=B(V.line,pe?0:null,V.ch).start;pe&&(we.top<_e.top-2?(O(we.right,we.top,null,we.bottom),O(C,_e.top,_e.left,_e.bottom)):O(we.right,we.top,_e.left-we.right,we.bottom)),we.bottom<_e.top&&O(C,we.bottom,null,_e.top)}c.appendChild(g)}i(gg,"drawSelectionRange");function $a(r){if(r.state.focused){var o=r.display;clearInterval(o.blinker);var c=!0;o.cursorDiv.style.visibility="",r.options.cursorBlinkRate>0?o.blinker=setInterval(function(){r.hasFocus()||Jl(r),o.cursorDiv.style.visibility=(c=!c)?"":"hidden"},r.options.cursorBlinkRate):r.options.cursorBlinkRate<0&&(o.cursorDiv.style.visibility="hidden")}}i($a,"restartBlink");function na(r){r.hasFocus()||(r.display.input.focus(),r.state.focused||eh(r))}i(na,"ensureFocus");function Ql(r){r.state.delayingBlurEvent=!0,setTimeout(function(){r.state.delayingBlurEvent&&(r.state.delayingBlurEvent=!1,r.state.focused&&Jl(r))},100)}i(Ql,"delayBlurEvent");function eh(r,o){r.state.delayingBlurEvent&&!r.state.draggingText&&(r.state.delayingBlurEvent=!1),r.options.readOnly!="nocursor"&&(r.state.focused||(Mt(r,"focus",r,o),r.state.focused=!0,It(r.display.wrapper,"CodeMirror-focused"),!r.curOp&&r.display.selForContextMenu!=r.doc.sel&&(r.display.input.reset(),S&&setTimeout(function(){return r.display.input.reset(!0)},20)),r.display.input.receivedFocus()),$a(r))}i(eh,"onFocus");function Jl(r,o){r.state.delayingBlurEvent||(r.state.focused&&(Mt(r,"blur",r,o),r.state.focused=!1,Ee(r.display.wrapper,"CodeMirror-focused")),clearInterval(r.display.blinker),setTimeout(function(){r.state.focused||(r.display.shift=!1)},150))}i(Jl,"onBlur");function sd(r){for(var o=r.display,c=o.lineDiv.offsetTop,u=Math.max(0,o.scroller.getBoundingClientRect().top),p=o.lineDiv.getBoundingClientRect().top,g=0,y=0;y.005||V<-.005)&&(pr.display.sizerWidth){var ae=Math.ceil(O/Cs(r.display));ae>r.display.maxLineLength&&(r.display.maxLineLength=ae,r.display.maxLine=C.line,r.display.maxLineChanged=!0)}}}Math.abs(g)>2&&(o.scroller.scrollTop+=g)}i(sd,"updateHeightsInViewport");function Pc(r){if(r.widgets)for(var o=0;o=y&&(g=ys(o,vi(Ae(o,_))-r.wrapper.clientHeight),y=_)}return{from:g,to:Math.max(y,g+1)}}i(Va,"visibleLines");function th(r,o){if(!At(r,"scrollCursorIntoView")){var c=r.display,u=c.sizer.getBoundingClientRect(),p=null,g=c.wrapper.ownerDocument;if(o.top+u.top<0?p=!0:o.bottom+u.top>(g.defaultView.innerHeight||g.documentElement.clientHeight)&&(p=!1),p!=null&&!Y){var y=xe("div","\u200B",null,`position: absolute; + top: `+(o.top-c.viewOffset-ea(r.display))+`px; + height: `+(o.bottom-o.top+Fi(r)+c.barHeight)+`px; + left: `+o.left+"px; width: "+Math.max(2,o.right-o.left)+"px;");r.display.lineSpace.appendChild(y),y.scrollIntoView(p),r.display.lineSpace.removeChild(y)}}}i(th,"maybeScrollWindow");function hw(r,o,c,u){u==null&&(u=0);var p;!r.options.lineWrapping&&o==c&&(c=o.sticky=="before"?ce(o.line,o.ch+1,"before"):o,o=o.ch?ce(o.line,o.sticky=="before"?o.ch-1:o.ch,"after"):o);for(var g=0;g<5;g++){var y=!1,C=J(r,o),_=!c||c==o?C:J(r,c);p={left:Math.min(C.left,_.left),top:Math.min(C.top,_.top)-u,right:Math.max(C.left,_.left),bottom:Math.max(C.bottom,_.bottom)+u};var E=rh(r,p),O=r.doc.scrollTop,B=r.doc.scrollLeft;if(E.scrollTop!=null&&(Ac(r,E.scrollTop),Math.abs(r.doc.scrollTop-O)>1&&(y=!0)),E.scrollLeft!=null&&(go(r,E.scrollLeft),Math.abs(r.doc.scrollLeft-B)>1&&(y=!0)),!y)break}return p}i(hw,"scrollPosIntoView");function vg(r,o){var c=rh(r,o);c.scrollTop!=null&&Ac(r,c.scrollTop),c.scrollLeft!=null&&go(r,c.scrollLeft)}i(vg,"scrollIntoView");function rh(r,o){var c=r.display,u=po(r.display);o.top<0&&(o.top=0);var p=r.curOp&&r.curOp.scrollTop!=null?r.curOp.scrollTop:c.scroller.scrollTop,g=jl(r),y={};o.bottom-o.top>g&&(o.bottom=o.top+g);var C=r.doc.height+ql(c),_=o.topC-u;if(o.topp+g){var O=Math.min(o.top,(E?C:o.bottom)-g);O!=p&&(y.scrollTop=O)}var B=r.options.fixedGutter?0:c.gutters.offsetWidth,K=r.curOp&&r.curOp.scrollLeft!=null?r.curOp.scrollLeft:c.scroller.scrollLeft-B,V=$o(r)-c.gutters.offsetWidth,re=o.right-o.left>V;return re&&(o.right=o.left+V),o.left<10?y.scrollLeft=0:o.leftV+K-3&&(y.scrollLeft=o.right+(re?0:10)-V),y}i(rh,"calculateScrollPos");function nh(r,o){o!=null&&(ad(r),r.curOp.scrollTop=(r.curOp.scrollTop==null?r.doc.scrollTop:r.curOp.scrollTop)+o)}i(nh,"addToScrollTop");function er(r){ad(r);var o=r.getCursor();r.curOp.scrollToPos={from:o,to:o,margin:r.options.cursorScrollMargin}}i(er,"ensureCursorVisible");function Pr(r,o,c){(o!=null||c!=null)&&ad(r),o!=null&&(r.curOp.scrollLeft=o),c!=null&&(r.curOp.scrollTop=c)}i(Pr,"scrollToCoords");function mw(r,o){ad(r),r.curOp.scrollToPos=o}i(mw,"scrollToRange");function ad(r){var o=r.curOp.scrollToPos;if(o){r.curOp.scrollToPos=null;var c=Pe(r,o.from),u=Pe(r,o.to);ih(r,c,u,o.margin)}}i(ad,"resolveScrollToPos");function ih(r,o,c,u){var p=rh(r,{left:Math.min(o.left,c.left),top:Math.min(o.top,c.top)-u,right:Math.max(o.right,c.right),bottom:Math.max(o.bottom,c.bottom)+u});Pr(r,p.scrollLeft,p.scrollTop)}i(ih,"scrollToCoordsRange");function Ac(r,o){Math.abs(r.doc.scrollTop-o)<2||(n||vo(r,{top:o}),ld(r,o,!0),n&&vo(r),Oc(r,100))}i(Ac,"updateScrollTop");function ld(r,o,c){o=Math.max(0,Math.min(r.display.scroller.scrollHeight-r.display.scroller.clientHeight,o)),!(r.display.scroller.scrollTop==o&&!c)&&(r.doc.scrollTop=o,r.display.scrollbars.setScrollTop(o),r.display.scroller.scrollTop!=o&&(r.display.scroller.scrollTop=o))}i(ld,"setScrollTop");function go(r,o,c,u){o=Math.max(0,Math.min(o,r.display.scroller.scrollWidth-r.display.scroller.clientWidth)),!((c?o==r.doc.scrollLeft:Math.abs(r.doc.scrollLeft-o)<2)&&!u)&&(r.doc.scrollLeft=o,xg(r),r.display.scroller.scrollLeft!=o&&(r.display.scroller.scrollLeft=o),r.display.scrollbars.setScrollLeft(o))}i(go,"setScrollLeft");function qa(r){var o=r.display,c=o.gutters.offsetWidth,u=Math.round(r.doc.height+ql(r.display));return{clientHeight:o.scroller.clientHeight,viewHeight:o.wrapper.clientHeight,scrollWidth:o.scroller.scrollWidth,clientWidth:o.scroller.clientWidth,viewWidth:o.wrapper.clientWidth,barLeft:r.options.fixedGutter?c:0,docHeight:u,scrollHeight:u+Fi(r)+o.barHeight,nativeBarWidth:o.nativeBarWidth,gutterWidth:c}}i(qa,"measureForScrollbars");var Hi=i(function(r,o,c){this.cm=c;var u=this.vert=xe("div",[xe("div",null,null,"min-width: 1px")],"CodeMirror-vscrollbar"),p=this.horiz=xe("div",[xe("div",null,null,"height: 100%; min-height: 1px")],"CodeMirror-hscrollbar");u.tabIndex=p.tabIndex=-1,r(u),r(p),ze(u,"scroll",function(){u.clientHeight&&o(u.scrollTop,"vertical")}),ze(p,"scroll",function(){p.clientWidth&&o(p.scrollLeft,"horizontal")}),this.checkedZeroWidth=!1,d&&v<8&&(this.horiz.style.minHeight=this.vert.style.minWidth="18px")},"NativeScrollbars");Hi.prototype.update=function(r){var o=r.scrollWidth>r.clientWidth+1,c=r.scrollHeight>r.clientHeight+1,u=r.nativeBarWidth;if(c){this.vert.style.display="block",this.vert.style.bottom=o?u+"px":"0";var p=r.viewHeight-(o?u:0);this.vert.firstChild.style.height=Math.max(0,r.scrollHeight-r.clientHeight+p)+"px"}else this.vert.scrollTop=0,this.vert.style.display="",this.vert.firstChild.style.height="0";if(o){this.horiz.style.display="block",this.horiz.style.right=c?u+"px":"0",this.horiz.style.left=r.barLeft+"px";var g=r.viewWidth-r.barLeft-(c?u:0);this.horiz.firstChild.style.width=Math.max(0,r.scrollWidth-r.clientWidth+g)+"px"}else this.horiz.style.display="",this.horiz.firstChild.style.width="0";return!this.checkedZeroWidth&&r.clientHeight>0&&(u==0&&this.zeroWidthHack(),this.checkedZeroWidth=!0),{right:c?u:0,bottom:o?u:0}},Hi.prototype.setScrollLeft=function(r){this.horiz.scrollLeft!=r&&(this.horiz.scrollLeft=r),this.disableHoriz&&this.enableZeroWidthBar(this.horiz,this.disableHoriz,"horiz")},Hi.prototype.setScrollTop=function(r){this.vert.scrollTop!=r&&(this.vert.scrollTop=r),this.disableVert&&this.enableZeroWidthBar(this.vert,this.disableVert,"vert")},Hi.prototype.zeroWidthHack=function(){var r=D&&!j?"12px":"18px";this.horiz.style.height=this.vert.style.width=r,this.horiz.style.visibility=this.vert.style.visibility="hidden",this.disableHoriz=new se,this.disableVert=new se},Hi.prototype.enableZeroWidthBar=function(r,o,c){r.style.visibility="";function u(){var p=r.getBoundingClientRect(),g=c=="vert"?document.elementFromPoint(p.right-1,(p.top+p.bottom)/2):document.elementFromPoint((p.right+p.left)/2,p.bottom-1);g!=r?r.style.visibility="hidden":o.set(1e3,u)}i(u,"maybeDisable"),o.set(1e3,u)},Hi.prototype.clear=function(){var r=this.horiz.parentNode;r.removeChild(this.horiz),r.removeChild(this.vert)};var _s=i(function(){},"NullScrollbars");_s.prototype.update=function(){return{bottom:0,right:0}},_s.prototype.setScrollLeft=function(){},_s.prototype.setScrollTop=function(){},_s.prototype.clear=function(){};function Ko(r,o){o||(o=qa(r));var c=r.display.barWidth,u=r.display.barHeight;Lc(r,o);for(var p=0;p<4&&c!=r.display.barWidth||u!=r.display.barHeight;p++)c!=r.display.barWidth&&r.options.lineWrapping&&sd(r),Lc(r,qa(r)),c=r.display.barWidth,u=r.display.barHeight}i(Ko,"updateScrollbars");function Lc(r,o){var c=r.display,u=c.scrollbars.update(o);c.sizer.style.paddingRight=(c.barWidth=u.right)+"px",c.sizer.style.paddingBottom=(c.barHeight=u.bottom)+"px",c.heightForcer.style.borderBottom=u.bottom+"px solid transparent",u.right&&u.bottom?(c.scrollbarFiller.style.display="block",c.scrollbarFiller.style.height=u.bottom+"px",c.scrollbarFiller.style.width=u.right+"px"):c.scrollbarFiller.style.display="",u.bottom&&r.options.coverGutterNextToScrollbar&&r.options.fixedGutter?(c.gutterFiller.style.display="block",c.gutterFiller.style.height=u.bottom+"px",c.gutterFiller.style.width=o.gutterWidth+"px"):c.gutterFiller.style.display=""}i(Lc,"updateScrollbarsInner");var ja={native:Hi,null:_s};function yg(r){r.display.scrollbars&&(r.display.scrollbars.clear(),r.display.scrollbars.addClass&&Ee(r.display.wrapper,r.display.scrollbars.addClass)),r.display.scrollbars=new ja[r.options.scrollbarStyle](function(o){r.display.wrapper.insertBefore(o,r.display.scrollbarFiller),ze(o,"mousedown",function(){r.state.focused&&setTimeout(function(){return r.display.input.focus()},0)}),o.setAttribute("cm-not-content","true")},function(o,c){c=="horizontal"?go(r,o):Ac(r,o)},r),r.display.scrollbars.addClass&&It(r.display.wrapper,r.display.scrollbars.addClass)}i(yg,"initScrollbars");var gw=0;function Ka(r){r.curOp={cm:r,viewChanged:!1,startHeight:r.doc.height,forceUpdate:!1,updateInput:0,typing:!1,changeObjs:null,cursorActivityHandlers:null,cursorActivityCalled:0,selectionChanged:!1,updateMaxLine:!1,scrollLeft:null,scrollTop:null,scrollToPos:null,focus:!1,id:++gw,markArrays:null},Jf(r.curOp)}i(Ka,"startOperation");function Ga(r){var o=r.curOp;o&&xc(o,function(c){for(var u=0;u=c.viewTo)||c.maxLineChanged&&o.options.lineWrapping,r.update=r.mustUpdate&&new Go(o,r.mustUpdate&&{top:r.scrollTop,ensure:r.scrollToPos},r.forceUpdate)}i(ud,"endOperation_R1");function Mc(r){r.updatedDisplay=r.mustUpdate&&oh(r.cm,r.update)}i(Mc,"endOperation_W1");function wg(r){var o=r.cm,c=o.display;r.updatedDisplay&&sd(o),r.barMeasure=qa(o),c.maxLineChanged&&!o.options.lineWrapping&&(r.adjustWidthTo=Kl(o,c.maxLine,c.maxLine.text.length).left+3,o.display.sizerWidth=r.adjustWidthTo,r.barMeasure.scrollWidth=Math.max(c.scroller.clientWidth,c.sizer.offsetLeft+r.adjustWidthTo+Fi(o)+o.display.barWidth),r.maxScrollLeft=Math.max(0,c.sizer.offsetLeft+r.adjustWidthTo-$o(o))),(r.updatedDisplay||r.selectionChanged)&&(r.preparedSelection=c.input.prepareSelection())}i(wg,"endOperation_R2");function P(r){var o=r.cm;r.adjustWidthTo!=null&&(o.display.sizer.style.minWidth=r.adjustWidthTo+"px",r.maxScrollLeft=r.display.viewTo)){var c=+new Date+r.options.workTime,u=Ss(r,o.highlightFrontier),p=[];o.iter(u.line,Math.min(o.first+o.size,r.display.viewTo+500),function(g){if(u.line>=r.display.viewFrom){var y=g.styles,C=g.text.length>r.options.maxHighlightLength?Oi(o.mode,u.state):null,_=Ll(r,g,u,!0);C&&(u.state=C),g.styles=_.styles;var E=g.styleClasses,O=_.classes;O?g.styleClasses=O:E&&(g.styleClasses=null);for(var B=!y||y.length!=g.styles.length||E!=O&&(!E||!O||E.bgClass!=O.bgClass||E.textClass!=O.textClass),K=0;!B&&Kc)return Oc(r,r.options.workDelay),!0}),o.highlightFrontier=u.line,o.modeFrontier=Math.max(o.modeFrontier,u.line),p.length&&yn(r,function(){for(var g=0;g=c.viewFrom&&o.visible.to<=c.viewTo&&(c.updateLineNumbers==null||c.updateLineNumbers>=c.viewTo)&&c.renderedView==c.view&&Jp(r)==0)return!1;Cg(r)&&(ho(r),o.dims=Xl(r));var p=u.first+u.size,g=Math.max(o.visible.from-r.options.viewportMargin,u.first),y=Math.min(p,o.visible.to+r.options.viewportMargin);c.viewFromy&&c.viewTo-y<20&&(y=Math.min(p,c.viewTo)),Di&&(g=Fa(r.doc,g),y=Uo(r.doc,y));var C=g!=c.viewFrom||y!=c.viewTo||c.lastWrapHeight!=o.wrapperHeight||c.lastWrapWidth!=o.wrapperWidth;id(r,g,y),c.viewOffset=vi(Ae(r.doc,c.viewFrom)),r.display.mover.style.top=c.viewOffset+"px";var _=Jp(r);if(!C&&_==0&&!o.force&&c.renderedView==c.view&&(c.updateLineNumbers==null||c.updateLineNumbers>=c.viewTo))return!1;var E=Sw(r);return _>4&&(c.lineDiv.style.display="none"),Cw(r,c.updateLineNumbers,o.dims),_>4&&(c.lineDiv.style.display=""),c.renderedView=c.view,xw(E),Ye(c.cursorDiv),Ye(c.selectionDiv),c.gutters.style.height=c.sizer.style.minHeight=0,C&&(c.lastWrapHeight=o.wrapperHeight,c.lastWrapWidth=o.wrapperWidth,Oc(r,400)),c.updateLineNumbers=null,!0}i(oh,"updateDisplayIfNeeded");function Sg(r,o){for(var c=o.viewport,u=!0;;u=!1){if(!u||!r.options.lineWrapping||o.oldDisplayWidth==$o(r)){if(c&&c.top!=null&&(c={top:Math.min(r.doc.height+ql(r.display)-jl(r),c.top)}),o.visible=Va(r.display,r.doc,c),o.visible.from>=r.display.viewFrom&&o.visible.to<=r.display.viewTo)break}else u&&(o.visible=Va(r.display,r.doc,c));if(!oh(r,o))break;sd(r);var p=qa(r);ra(r),Ko(r,p),ah(r,p),o.force=!1}o.signal(r,"update",r),(r.display.viewFrom!=r.display.reportedViewFrom||r.display.viewTo!=r.display.reportedViewTo)&&(o.signal(r,"viewportChange",r,r.display.viewFrom,r.display.viewTo),r.display.reportedViewFrom=r.display.viewFrom,r.display.reportedViewTo=r.display.viewTo)}i(Sg,"postUpdateDisplay");function vo(r,o){var c=new Go(r,o);if(oh(r,c)){sd(r),Sg(r,c);var u=qa(r);ra(r),Ko(r,u),ah(r,u),c.finish()}}i(vo,"updateDisplaySimple");function Cw(r,o,c){var u=r.display,p=r.options.lineNumbers,g=u.lineDiv,y=g.firstChild;function C(re){var ae=re.nextSibling;return S&&D&&r.display.currentWheelTarget==re?re.style.display="none":re.parentNode.removeChild(re),ae}i(C,"rm");for(var _=u.view,E=u.viewFrom,O=0;O<_.length;O++){var B=_[O];if(!B.hidden)if(!B.node||B.node.parentNode!=g){var K=Xp(r,B,E,c);g.insertBefore(K,y)}else{for(;y!=B.node;)y=C(y);var V=p&&o!=null&&o<=E&&B.lineNumber;B.changes&&(Ue(B.changes,"gutter")>-1&&(V=!1),Cc(r,B,E,c)),V&&(Ye(B.lineNumber),B.lineNumber.appendChild(document.createTextNode(Oa(r.options,E)))),y=B.node.nextSibling}E+=B.size}for(;y;)y=C(y)}i(Cw,"patchDisplay");function sh(r){var o=r.gutters.offsetWidth;r.sizer.style.marginLeft=o+"px",Kt(r,"gutterChanged",r)}i(sh,"updateGutterSpace");function ah(r,o){r.display.sizer.style.minHeight=o.docHeight+"px",r.display.heightForcer.style.top=o.docHeight+"px",r.display.gutters.style.height=o.docHeight+r.display.barHeight+Fi(r)+"px"}i(ah,"setDocumentHeight");function xg(r){var o=r.display,c=o.view;if(!(!o.alignWidgets&&(!o.gutters.firstChild||!r.options.fixedGutter))){for(var u=kt(o)-o.scroller.scrollLeft+r.doc.scrollLeft,p=o.gutters.offsetWidth,g=u+"px",y=0;y=105&&(p.wrapper.style.clipPath="inset(0px)"),p.wrapper.setAttribute("translate","no"),d&&v<8&&(p.gutters.style.zIndex=-1,p.scroller.style.paddingRight=0),!S&&!(n&&L)&&(p.scroller.draggable=!0),r&&(r.appendChild?r.appendChild(p.wrapper):r(p.wrapper)),p.viewFrom=p.viewTo=o.first,p.reportedViewFrom=p.reportedViewTo=o.first,p.view=[],p.renderedView=null,p.externalMeasured=null,p.viewOffset=0,p.lastWrapHeight=p.lastWrapWidth=0,p.updateLineNumbers=null,p.nativeBarWidth=p.barHeight=p.barWidth=0,p.scrollbarsClipped=!1,p.lineNumWidth=p.lineNumInnerWidth=p.lineNumChars=null,p.alignWidgets=!1,p.cachedCharWidth=p.cachedTextHeight=p.cachedPaddingH=null,p.maxLine=null,p.maxLineLength=0,p.maxLineChanged=!1,p.wheelDX=p.wheelDY=p.wheelStartX=p.wheelStartY=null,p.shift=!1,p.selForContextMenu=null,p.activeTouch=null,p.gutterSpecs=yo(u.gutters,u.lineNumbers),_g(p),c.init(p)}i(_w,"Display");var cd=0,Es=null;d?Es=-.53:n?Es=15:k?Es=-.7:z&&(Es=-1/3);function ia(r){var o=r.wheelDeltaX,c=r.wheelDeltaY;return o==null&&r.detail&&r.axis==r.HORIZONTAL_AXIS&&(o=r.detail),c==null&&r.detail&&r.axis==r.VERTICAL_AXIS?c=r.detail:c==null&&(c=r.wheelDelta),{x:o,y:c}}i(ia,"wheelEventDelta");function Ew(r){var o=ia(r);return o.x*=Es,o.y*=Es,o}i(Ew,"wheelEventPixels");function Eg(r,o){k&&R==102&&(r.display.chromeScrollHack==null?r.display.sizer.style.pointerEvents="none":clearTimeout(r.display.chromeScrollHack),r.display.chromeScrollHack=setTimeout(function(){r.display.chromeScrollHack=null,r.display.sizer.style.pointerEvents=""},100));var c=ia(o),u=c.x,p=c.y,g=Es;o.deltaMode===0&&(u=o.deltaX,p=o.deltaY,g=1);var y=r.display,C=y.scroller,_=C.scrollWidth>C.clientWidth,E=C.scrollHeight>C.clientHeight;if(u&&_||p&&E){if(p&&D&&S){e:for(var O=o.target,B=y.view;O!=C;O=O.parentNode)for(var K=0;K=0&&Ve(r,u.to())<=0)return c}return-1};var Et=i(function(r,o){this.anchor=r,this.head=o},"Range");Et.prototype.from=function(){return zo(this.anchor,this.head)},Et.prototype.to=function(){return js(this.anchor,this.head)},Et.prototype.empty=function(){return this.head.line==this.anchor.line&&this.head.ch==this.anchor.ch};function wo(r,o,c){var u=r&&r.options.selectionsMayTouch,p=o[c];o.sort(function(K,V){return Ve(K.from(),V.from())}),c=Ue(o,p);for(var g=1;g0:_>=0){var E=zo(C.from(),y.from()),O=js(C.to(),y.to()),B=C.empty()?y.from()==y.head:C.from()==C.head;g<=c&&--c,o.splice(--g,2,new Et(B?O:E,B?E:O))}}return new sr(o,c)}i(wo,"normalizeSelection");function oa(r,o){return new sr([new Et(r,o||r)],0)}i(oa,"simpleSelection");function sa(r){return r.text?ce(r.from.line+r.text.length-1,lt(r.text).length+(r.text.length==1?r.from.ch:0)):r.to}i(sa,"changeEnd");function bg(r,o){if(Ve(r,o.from)<0)return r;if(Ve(r,o.to)<=0)return sa(o);var c=r.line+o.text.length-(o.to.line-o.from.line)-1,u=r.ch;return r.line==o.to.line&&(u+=sa(o).ch-o.to.ch),ce(c,u)}i(bg,"adjustForChange");function fd(r,o){for(var c=[],u=0;u1&&r.remove(C.line+1,re-1),r.insert(C.line+1,we)}Kt(r,"change",r,o)}i(tu,"updateDoc");function aa(r,o,c){function u(p,g,y){if(p.linked)for(var C=0;C1&&!r.done[r.done.length-2].ranges)return r.done.pop(),lt(r.done)}i(Tw,"lastChangeEvent");function fh(r,o,c,u){var p=r.history;p.undone.length=0;var g=+new Date,y,C;if((p.lastOp==u||p.lastOrigin==o.origin&&o.origin&&(o.origin.charAt(0)=="+"&&p.lastModTime>g-(r.cm?r.cm.options.historyEventDelay:500)||o.origin.charAt(0)=="*"))&&(y=Tw(p,p.lastOp==u)))C=lt(y.changes),Ve(o.from,o.to)==0&&Ve(o.from,C.to)==0?C.to=sa(o):y.changes.push(Ts(r,o));else{var _=lt(p.done);for((!_||!_.ranges)&&So(r.sel,p.done),y={changes:[Ts(r,o)],generation:p.generation},p.done.push(y);p.done.length>p.undoDepth;)p.done.shift(),p.done[0].ranges||p.done.shift()}p.done.push(c),p.generation=++p.maxGeneration,p.lastModTime=p.lastSelTime=g,p.lastOp=p.lastSelOp=u,p.lastOrigin=p.lastSelOrigin=o.origin,C||Mt(r,"historyAdded")}i(fh,"addChangeToHistory");function kg(r,o,c,u){var p=o.charAt(0);return p=="*"||p=="+"&&c.ranges.length==u.ranges.length&&c.somethingSelected()==u.somethingSelected()&&new Date-r.history.lastSelTime<=(r.cm?r.cm.options.historyEventDelay:500)}i(kg,"selectionEventCanBeMerged");function Dc(r,o,c,u){var p=r.history,g=u&&u.origin;c==p.lastSelOp||g&&p.lastSelOrigin==g&&(p.lastModTime==p.lastSelTime&&p.lastOrigin==g||kg(r,g,lt(p.done),o))?p.done[p.done.length-1]=o:So(o,p.done),p.lastSelTime=+new Date,p.lastSelOrigin=g,p.lastSelOp=c,u&&u.clearRedo!==!1&&wi(p.undone)}i(Dc,"addSelectionToHistory");function So(r,o){var c=lt(o);c&&c.ranges&&c.equals(r)||o.push(r)}i(So,"pushSelectionToHistory");function dh(r,o,c,u){var p=o["spans_"+r.id],g=0;r.iter(Math.max(r.first,c),Math.min(r.first+r.size,u),function(y){y.markedSpans&&((p||(p=o["spans_"+r.id]={}))[g]=y.markedSpans),++g})}i(dh,"attachLocalSpans");function kw(r){if(!r)return null;for(var o,c=0;c-1&&(lt(C)[B]=E[B],delete E[B])}}return u}i(Xa,"copyHistoryArray");function Br(r,o,c,u){if(u){var p=r.anchor;if(c){var g=Ve(o,p)<0;g!=Ve(c,p)<0?(p=o,o=c):g!=Ve(o,c)<0&&(o=c)}return new Et(p,o)}else return new Et(c||o,o)}i(Br,"extendRange");function nu(r,o,c,u,p){p==null&&(p=r.cm&&(r.cm.display.shift||r.extend)),Zr(r,new sr([Br(r.sel.primary(),o,c,p)],0),u)}i(nu,"extendSelection");function ph(r,o,c){for(var u=[],p=r.cm&&(r.cm.display.shift||r.extend),g=0;g=o.ch:C.to>o.ch))){if(p&&(Mt(_,"beforeCursorEnter"),_.explicitlyCleared))if(g.markedSpans){--y;continue}else break;if(!_.atomic)continue;if(c){var B=_.find(u<0?1:-1),K=void 0;if((u<0?O:E)&&(B=Lg(r,B,-u,B&&B.line==o.line?g:null)),B&&B.line==o.line&&(K=Ve(B,c))&&(u<0?K<0:K>0))return iu(r,B,o,u,p)}var V=_.find(u<0?-1:1);return(u<0?E:O)&&(V=Lg(r,V,u,V.line==o.line?g:null)),V?iu(r,V,o,u,p):null}}return o}i(iu,"skipAtomicInner");function ou(r,o,c,u,p){var g=u||1,y=iu(r,o,c,g,p)||!p&&iu(r,o,c,g,!0)||iu(r,o,c,-g,p)||!p&&iu(r,o,c,-g,!0);return y||(r.cantEdit=!0,ce(r.first,0))}i(ou,"skipAtomic");function Lg(r,o,c,u){return c<0&&o.ch==0?o.line>r.first?qe(r,ce(o.line-1)):null:c>0&&o.ch==(u||Ae(r,o.line)).text.length?o.line=0;--p)Mg(r,{from:u[p].from,to:u[p].to,text:p?[""]:o.text,origin:o.origin});else Mg(r,o)}}i(au,"makeChange");function Mg(r,o){if(!(o.text.length==1&&o.text[0]==""&&Ve(o.from,o.to)==0)){var c=fd(r,o);fh(r,o,c,r.cm?r.cm.curOp.id:NaN),Ic(r,o,c,yc(r,o));var u=[];aa(r,function(p,g){!g&&Ue(u,p.history)==-1&&(Ig(p.history,o),u.push(p.history)),Ic(p,o,null,yc(p,o))})}}i(Mg,"makeChangeInner");function ua(r,o,c){var u=r.cm&&r.cm.state.suppressEdits;if(!(u&&!c)){for(var p=r.history,g,y=r.sel,C=o=="undo"?p.done:p.undone,_=o=="undo"?p.undone:p.done,E=0;E=0;--V){var re=K(V);if(re)return re.v}}}}i(ua,"makeChangeFromHistory");function Og(r,o){if(o!=0&&(r.first+=o,r.sel=new sr(wr(r.sel.ranges,function(p){return new Et(ce(p.anchor.line+o,p.anchor.ch),ce(p.head.line+o,p.head.ch))}),r.sel.primIndex),r.cm)){Wn(r.cm,r.first,r.first-o,o);for(var c=r.cm.display,u=c.viewFrom;ur.lastLine())){if(o.from.lineg&&(o={from:o.from,to:ce(g,Ae(r,g).text.length),text:[o.text[0]],origin:o.origin}),o.removed=lo(r,o.from,o.to),c||(c=fd(r,o)),r.cm?mh(r.cm,o,u):tu(r,o,u),en(r,c,st),r.cantEdit&&ou(r,ce(r.firstLine(),0))&&(r.cantEdit=!1)}}i(Ic,"makeChangeSingleDoc");function mh(r,o,c){var u=r.doc,p=r.display,g=o.from,y=o.to,C=!1,_=g.line;r.options.lineWrapping||(_=vt(Rt(Ae(u,g.line))),u.iter(_,y.line+1,function(V){if(V==p.maxLine)return C=!0,!0})),u.sel.contains(o.from,o.to)>-1&&Na(r),tu(u,o,c,mg(r)),r.options.lineWrapping||(u.iter(_,g.line+o.text.length,function(V){var re=Ze(V);re>p.maxLineLength&&(p.maxLine=V,p.maxLineLength=re,p.maxLineChanged=!0,C=!1)}),C&&(r.curOp.updateMaxLine=!0)),Ra(u,g.line),Oc(r,400);var E=o.text.length-(y.line-g.line)-1;o.full?Wn(r):g.line==y.line&&o.text.length==1&&!eu(r.doc,o)?ta(r,g.line,"text"):Wn(r,g.line,y.line+1,E);var O=pn(r,"changes"),B=pn(r,"change");if(B||O){var K={from:g,to:y,text:o.text,removed:o.removed,origin:o.origin};B&&Kt(r,"change",r,K),O&&(r.curOp.changeObjs||(r.curOp.changeObjs=[])).push(K)}r.display.selForContextMenu=null}i(mh,"makeChangeSingleDocInEditor");function Qa(r,o,c,u,p){var g;u||(u=c),Ve(u,c)<0&&(g=[u,c],c=g[0],u=g[1]),typeof o=="string"&&(o=r.splitLines(o)),au(r,{from:c,to:u,text:o,origin:p})}i(Qa,"replaceRange");function Rg(r,o,c,u){c1||!(this.children[0]instanceof Ja))){var C=[];this.collapse(C),this.children=[new Ja(C)],this.children[0].parent=this}},"removeInner"),collapse:i(function(r){for(var o=0;o50){for(var y=p.lines.length%25+25,C=y;C10);r.parent.maybeSpill()}},"maybeSpill"),iterN:i(function(r,o,c){for(var u=0;ur.display.maxLineLength&&(r.display.maxLine=E,r.display.maxLineLength=O,r.display.maxLineChanged=!0)}u!=null&&r&&this.collapsed&&Wn(r,u,p+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,r&&Pg(r.doc)),r&&Kt(r,"markerCleared",r,this,u,p),o&&Ga(r),this.parent&&this.parent.clear()}},xo.prototype.find=function(r,o){r==null&&this.type=="bookmark"&&(r=1);for(var c,u,p=0;p0||y==0&&g.clearWhenEmpty!==!1)return g;if(g.replacedWith&&(g.collapsed=!0,g.widgetNode=Xe("span",[g.replacedWith],"CodeMirror-widget"),u.handleMouseEvents||g.widgetNode.setAttribute("cm-ignore-events","true"),u.insertLeft&&(g.widgetNode.insertLeft=!0)),g.collapsed){if(Gp(r,o.line,o,c,g)||o.line!=c.line&&Gp(r,c.line,o,c,g))throw new Error("Inserting collapsed marker partially overlapping an existing one");$p()}g.addToHistory&&fh(r,{from:o,to:c,origin:"markText"},r.sel,NaN);var C=o.line,_=r.cm,E;if(r.iter(C,c.line+1,function(B){_&&g.collapsed&&!_.options.lineWrapping&&Rt(B)==_.display.maxLine&&(E=!0),g.collapsed&&C!=o.line&&Ri(B,0),vc(B,new Il(g,C==o.line?o.ch:null,C==c.line?c.ch:null),r.cm&&r.cm.curOp),++C}),g.collapsed&&r.iter(o.line,c.line+1,function(B){Se(r,B)&&Ri(B,0)}),g.clearOnEnter&&ze(g,"beforeCursorEnter",function(){return g.clear()}),g.readOnly&&(Kf(),(r.history.done.length||r.history.undone.length)&&r.clearHistory()),g.collapsed&&(g.id=++vh,g.atomic=!0),_){if(E&&(_.curOp.updateMaxLine=!0),g.collapsed)Wn(_,o.line,c.line+1);else if(g.className||g.startStyle||g.endStyle||g.css||g.attributes||g.title)for(var O=o.line;O<=c.line;O++)ta(_,O,"text");g.atomic&&Pg(_.doc),Kt(_,"markerAdded",_,g)}return g}i(uu,"markText");var cu=i(function(r,o){this.markers=r,this.primary=o;for(var c=0;c=0;_--)au(this,u[_]);C?la(this,C):this.cm&&er(this.cm)}),undo:Fr(function(){ua(this,"undo")}),redo:Fr(function(){ua(this,"redo")}),undoSelection:Fr(function(){ua(this,"undo",!0)}),redoSelection:Fr(function(){ua(this,"redo",!0)}),setExtending:i(function(r){this.extend=r},"setExtending"),getExtending:i(function(){return this.extend},"getExtending"),historySize:i(function(){for(var r=this.history,o=0,c=0,u=0;u=r.ch)&&o.push(p.marker.parent||p.marker)}return o},"findMarksAt"),findMarks:i(function(r,o,c){r=qe(this,r),o=qe(this,o);var u=[],p=r.line;return this.iter(r.line,o.line+1,function(g){var y=g.markedSpans;if(y)for(var C=0;C=_.to||_.from==null&&p!=r.line||_.from!=null&&p==o.line&&_.from>=o.ch)&&(!c||c(_.marker))&&u.push(_.marker.parent||_.marker)}++p}),u},"findMarks"),getAllMarks:i(function(){var r=[];return this.iter(function(o){var c=o.markedSpans;if(c)for(var u=0;ur)return o=r,!0;r-=g,++c}),qe(this,ce(c,o))},"posFromIndex"),indexFromPos:i(function(r){r=qe(this,r);var o=r.ch;if(r.lineo&&(o=r.from),r.to!=null&&r.to-1){o.state.draggingText(r),setTimeout(function(){return o.display.input.focus()},20);return}try{var O=r.dataTransfer.getData("Text");if(O){var B;if(o.state.draggingText&&!o.state.draggingText.copy&&(B=o.listSelections()),en(o.doc,oa(c,c)),B)for(var K=0;K=0;C--)Qa(r.doc,"",u[C].from,u[C].to,"+delete");er(r)})}i(ca,"deleteNearSelection");function yd(r,o,c){var u=Hn(r.text,o+c,c);return u<0||u>r.text.length?null:u}i(yd,"moveCharLogically");function Uc(r,o,c){var u=yd(r,o.ch,c);return u==null?null:new ce(o.line,u,c<0?"after":"before")}i(Uc,"moveLogically");function Wc(r,o,c,u,p){if(r){o.doc.direction=="rtl"&&(p=-p);var g=Me(c,o.doc.direction);if(g){var y=p<0?lt(g):g[0],C=p<0==(y.level==1),_=C?"after":"before",E;if(y.level>0||o.doc.direction=="rtl"){var O=kn(o,c);E=p<0?c.text.length-1:0;var B=w(o,O,E).top;E=so(function(K){return w(o,O,K).top==B},p<0==(y.level==1)?y.from:y.to-1,E),_=="before"&&(E=yd(c,E,1))}else E=p<0?y.to:y.from;return new ce(u,E,_)}}return new ce(u,p<0?c.text.length:0,p<0?"before":"after")}i(Wc,"endOfLine");function fa(r,o,c,u){var p=Me(o,r.doc.direction);if(!p)return Uc(o,c,u);c.ch>=o.text.length?(c.ch=o.text.length,c.sticky="before"):c.ch<=0&&(c.ch=0,c.sticky="after");var g=ee(p,c.ch,c.sticky),y=p[g];if(r.doc.direction=="ltr"&&y.level%2==0&&(u>0?y.to>c.ch:y.from=y.from&&K>=O.begin)){var V=B?"before":"after";return new ce(c.line,K,V)}}var re=i(function(we,_e,ye){for(var ke=i(function(Dt,pr){return pr?new ce(c.line,C(Dt,1),"before"):new ce(c.line,Dt,"after")},"getRes");we>=0&&we0==(Ie.level!=1),it=Oe?ye.begin:C(ye.end,-1);if(Ie.from<=it&&it0?O.end:C(O.begin,-1);return pe!=null&&!(u>0&&pe==o.text.length)&&(ae=re(u>0?0:p.length-1,u,E(pe)),ae)?ae:null}i(fa,"moveVisually");var $c={selectAll:su,singleSelection:i(function(r){return r.setSelection(r.getCursor("anchor"),r.getCursor("head"),st)},"singleSelection"),killLine:i(function(r){return ca(r,function(o){if(o.empty()){var c=Ae(r.doc,o.head.line).text.length;return o.head.ch==c&&o.head.line0)p=new ce(p.line,p.ch+1),r.replaceRange(g.charAt(p.ch-1)+g.charAt(p.ch-2),ce(p.line,p.ch-2),p,"+transpose");else if(p.line>r.doc.first){var y=Ae(r.doc,p.line-1).text;y&&(p=new ce(p.line,1),r.replaceRange(g.charAt(0)+r.doc.lineSeparator()+y.charAt(y.length-1),ce(p.line-1,y.length-1),p,"+transpose"))}}c.push(new Et(p,p))}r.setSelections(c)})},"transposeChars"),newlineAndIndent:i(function(r){return yn(r,function(){for(var o=r.listSelections(),c=o.length-1;c>=0;c--)r.replaceRange(r.doc.lineSeparator(),o[c].anchor,o[c].head,"+input");o=r.listSelections();for(var u=0;ur&&Ve(o,this.pos)==0&&c==this.button};var Xo,hu;function Kg(r,o){var c=+new Date;return hu&&hu.compare(c,r,o)?(Xo=hu=null,"triple"):Xo&&Xo.compare(c,r,o)?(hu=new pu(c,r,o),Xo=null,"double"):(Xo=new pu(c,r,o),hu=null,"single")}i(Kg,"clickRepeat");function Gg(r){var o=this,c=o.display;if(!(At(o,r)||c.activeTouch&&c.input.supportsTouch())){if(c.input.ensurePolled(),c.shift=r.shiftKey,yi(c,r)){S||(c.scroller.draggable=!1,setTimeout(function(){return c.scroller.draggable=!0},100));return}if(!gu(o,r)){var u=jo(o,r),p=nc(r),g=u?Kg(u,p):"single";vr(o).focus(),p==1&&o.state.selectingText&&o.state.selectingText(r),!(u&&Yg(o,p,u,g,r))&&(p==1?u?qc(o,u,g,r):Aa(r)==c.scroller&&kr(r):p==2?(u&&nu(o.doc,u),setTimeout(function(){return c.input.focus()},20)):p==3&&(he?o.display.input.onContextMenu(r):Ql(o)))}}}i(Gg,"onMouseDown");function Yg(r,o,c,u,p){var g="Click";return u=="double"?g="Double"+g:u=="triple"&&(g="Triple"+g),g=(o==1?"Left":o==2?"Middle":"Right")+g,Vc(r,Sh(g,p),p,function(y){if(typeof y=="string"&&(y=$c[y]),!y)return!1;var C=!1;try{r.isReadOnly()&&(r.state.suppressEdits=!0),C=y(r,c)!=Zt}finally{r.state.suppressEdits=!1}return C})}i(Yg,"handleMappedButton");function mu(r,o,c){var u=r.getOption("configureMouse"),p=u?u(r,o,c):{};if(p.unit==null){var g=q?c.shiftKey&&c.metaKey:c.altKey;p.unit=g?"rectangle":o=="single"?"char":o=="double"?"word":"line"}return(p.extend==null||r.doc.extend)&&(p.extend=r.doc.extend||c.shiftKey),p.addNew==null&&(p.addNew=D?c.metaKey:c.ctrlKey),p.moveOnDrag==null&&(p.moveOnDrag=!(D?c.altKey:c.ctrlKey)),p}i(mu,"configureMouse");function qc(r,o,c,u){d?setTimeout(yr(na,r),0):r.curOp.focus=ot(gr(r));var p=mu(r,c,u),g=r.doc.sel,y;r.options.dragDrop&&ic&&!r.isReadOnly()&&c=="single"&&(y=g.contains(o))>-1&&(Ve((y=g.ranges[y]).from(),o)<0||o.xRel>0)&&(Ve(y.to(),o)>0||o.xRel<0)?Iw(r,u,o,p):Xg(r,u,o,p)}i(qc,"leftButtonDown");function Iw(r,o,c,u){var p=r.display,g=!1,y=Lt(r,function(E){S&&(p.scroller.draggable=!1),r.state.draggingText=!1,r.state.delayingBlurEvent&&(r.hasFocus()?r.state.delayingBlurEvent=!1:Ql(r)),Yt(p.wrapper.ownerDocument,"mouseup",y),Yt(p.wrapper.ownerDocument,"mousemove",C),Yt(p.scroller,"dragstart",_),Yt(p.scroller,"drop",y),g||(kr(E),u.addNew||nu(r.doc,c,null,null,u.extend),S&&!z||d&&v==9?setTimeout(function(){p.wrapper.ownerDocument.body.focus({preventScroll:!0}),p.input.focus()},20):p.input.focus())}),C=i(function(E){g=g||Math.abs(o.clientX-E.clientX)+Math.abs(o.clientY-E.clientY)>=10},"mouseMove"),_=i(function(){return g=!0},"dragStart");S&&(p.scroller.draggable=!0),r.state.draggingText=y,y.copy=!u.moveOnDrag,ze(p.wrapper.ownerDocument,"mouseup",y),ze(p.wrapper.ownerDocument,"mousemove",C),ze(p.scroller,"dragstart",_),ze(p.scroller,"drop",y),r.state.delayingBlurEvent=!0,setTimeout(function(){return p.input.focus()},20),p.scroller.dragDrop&&p.scroller.dragDrop()}i(Iw,"leftButtonStartDrag");function bh(r,o,c){if(c=="char")return new Et(o,o);if(c=="word")return r.findWordAt(o);if(c=="line")return new Et(ce(o.line,0),qe(r.doc,ce(o.line+1,0)));var u=c(r,o);return new Et(u.from,u.to)}i(bh,"rangeForUnit");function Xg(r,o,c,u){d&&Ql(r);var p=r.display,g=r.doc;kr(o);var y,C,_=g.sel,E=_.ranges;if(u.addNew&&!u.extend?(C=g.sel.contains(c),C>-1?y=E[C]:y=new Et(c,c)):(y=g.sel.primary(),C=g.sel.primIndex),u.unit=="rectangle")u.addNew||(y=new Et(c,c)),c=jo(r,o,!0,!0),C=-1;else{var O=bh(r,c,u.unit);u.extend?y=Br(y,O.anchor,O.head,u.extend):y=O}u.addNew?C==-1?(C=E.length,Zr(g,wo(r,E.concat([y]),C),{scroll:!1,origin:"*mouse"})):E.length>1&&E[C].empty()&&u.unit=="char"&&!u.extend?(Zr(g,wo(r,E.slice(0,C).concat(E.slice(C+1)),0),{scroll:!1,origin:"*mouse"}),_=g.sel):zi(g,C,y,Fe):(C=0,Zr(g,new sr([y],0),Fe),_=g.sel);var B=c;function K(ye){if(Ve(B,ye)!=0)if(B=ye,u.unit=="rectangle"){for(var ke=[],Ie=r.options.tabSize,Oe=Ft(Ae(g,c.line).text,c.ch,Ie),it=Ft(Ae(g,ye.line).text,ye.ch,Ie),Dt=Math.min(Oe,it),pr=Math.max(Oe,it),Qt=Math.min(c.line,ye.line),ar=Math.min(r.lastLine(),Math.max(c.line,ye.line));Qt<=ar;Qt++){var Sr=Ae(g,Qt).text,xr=bn(Sr,Dt,Ie);Dt==pr?ke.push(new Et(ce(Qt,xr),ce(Qt,xr))):Sr.length>xr&&ke.push(new Et(ce(Qt,xr),ce(Qt,bn(Sr,pr,Ie))))}ke.length||ke.push(new Et(c,c)),Zr(g,wo(r,_.ranges.slice(0,C).concat(ke),C),{origin:"*mouse",scroll:!1}),r.scrollIntoView(ye)}else{var Pn=y,tn=bh(r,ye,u.unit),Ar=Pn.anchor,Cr;Ve(tn.anchor,Ar)>0?(Cr=tn.head,Ar=zo(Pn.from(),tn.anchor)):(Cr=tn.anchor,Ar=js(Pn.to(),tn.head));var lr=_.ranges.slice(0);lr[C]=Sd(r,new Et(qe(g,Ar),Cr)),Zr(g,wo(r,lr,C),Fe)}}i(K,"extendTo");var V=p.wrapper.getBoundingClientRect(),re=0;function ae(ye){var ke=++re,Ie=jo(r,ye,!0,u.unit=="rectangle");if(Ie)if(Ve(Ie,B)!=0){r.curOp.focus=ot(gr(r)),K(Ie);var Oe=Va(p,g);(Ie.line>=Oe.to||Ie.lineV.bottom?20:0;it&&setTimeout(Lt(r,function(){re==ke&&(p.scroller.scrollTop+=it,ae(ye))}),50)}}i(ae,"extend");function pe(ye){r.state.selectingText=!1,re=1/0,ye&&(kr(ye),p.input.focus()),Yt(p.wrapper.ownerDocument,"mousemove",we),Yt(p.wrapper.ownerDocument,"mouseup",_e),g.history.lastSelOrigin=null}i(pe,"done");var we=Lt(r,function(ye){ye.buttons===0||!nc(ye)?pe(ye):ae(ye)}),_e=Lt(r,pe);r.state.selectingText=_e,ze(p.wrapper.ownerDocument,"mousemove",we),ze(p.wrapper.ownerDocument,"mouseup",_e)}i(Xg,"leftButtonSelect");function Sd(r,o){var c=o.anchor,u=o.head,p=Ae(r.doc,c.line);if(Ve(c,u)==0&&c.sticky==u.sticky)return o;var g=Me(p);if(!g)return o;var y=ee(g,c.ch,c.sticky),C=g[y];if(C.from!=c.ch&&C.to!=c.ch)return o;var _=y+(C.from==c.ch==(C.level!=1)?0:1);if(_==0||_==g.length)return o;var E;if(u.line!=c.line)E=(u.line-c.line)*(r.doc.direction=="ltr"?1:-1)>0;else{var O=ee(g,u.ch,u.sticky),B=O-y||(u.ch-c.ch)*(C.level==1?-1:1);O==_-1||O==_?E=B<0:E=B>0}var K=g[_+(E?-1:0)],V=E==(K.level==1),re=V?K.from:K.to,ae=V?"after":"before";return c.ch==re&&c.sticky==ae?o:new Et(new ce(c.line,re,ae),u)}i(Sd,"bidiSimplify");function Th(r,o,c,u){var p,g;if(o.touches)p=o.touches[0].clientX,g=o.touches[0].clientY;else try{p=o.clientX,g=o.clientY}catch{return!1}if(p>=Math.floor(r.display.gutters.getBoundingClientRect().right))return!1;u&&kr(o);var y=r.display,C=y.lineDiv.getBoundingClientRect();if(g>C.bottom||!pn(r,c))return Tn(o);g-=C.top-y.viewOffset;for(var _=0;_=p){var O=ys(r.doc,g),B=r.display.gutterSpecs[_];return Mt(r,c,r,O,B.className,o),Tn(o)}}}i(Th,"gutterEvent");function gu(r,o){return Th(r,o,"gutterClick",!0)}i(gu,"clickInGutter");function xd(r,o){yi(r.display,o)||Cd(r,o)||At(r,o,"contextmenu")||he||r.display.input.onContextMenu(o)}i(xd,"onContextMenu");function Cd(r,o){return pn(r,"gutterContextMenu")?Th(r,o,"gutterContextMenu",!1):!1}i(Cd,"contextMenuInGutter");function Qg(r){r.display.wrapper.className=r.display.wrapper.className.replace(/\s*cm-s-\S+/g,"")+r.options.theme.replace(/(^|\s)\s*/g," cm-s-"),Vo(r)}i(Qg,"themeChanged");var rl={toString:i(function(){return"CodeMirror.Init"},"toString")},jc={},Kc={};function kh(r){var o=r.optionHandlers;function c(u,p,g,y){r.defaults[u]=p,g&&(o[u]=y?function(C,_,E){E!=rl&&g(C,_,E)}:g)}i(c,"option"),r.defineOption=c,r.Init=rl,c("value","",function(u,p){return u.setValue(p)},!0),c("mode",null,function(u,p){u.doc.modeOption=p,uh(u)},!0),c("indentUnit",2,uh,!0),c("indentWithTabs",!1),c("smartIndent",!0),c("tabSize",4,function(u){Ya(u),Vo(u),Wn(u)},!0),c("lineSeparator",null,function(u,p){if(u.doc.lineSep=p,!!p){var g=[],y=u.doc.first;u.doc.iter(function(_){for(var E=0;;){var O=_.text.indexOf(p,E);if(O==-1)break;E=O+p.length,g.push(ce(y,O))}y++});for(var C=g.length-1;C>=0;C--)Qa(u.doc,p,g[C],ce(g[C].line,g[C].ch+p.length))}}),c("specialChars",/[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\u202d\u202e\u2066\u2067\u2069\ufeff\ufff9-\ufffc]/g,function(u,p,g){u.state.specialChars=new RegExp(p.source+(p.test(" ")?"":"| "),"g"),g!=rl&&u.refresh()}),c("specialCharPlaceholder",jt,function(u){return u.refresh()},!0),c("electricChars",!0),c("inputStyle",L?"contenteditable":"textarea",function(){throw new Error("inputStyle can not (yet) be changed in a running editor")},!0),c("spellcheck",!1,function(u,p){return u.getInputField().spellcheck=p},!0),c("autocorrect",!1,function(u,p){return u.getInputField().autocorrect=p},!0),c("autocapitalize",!1,function(u,p){return u.getInputField().autocapitalize=p},!0),c("rtlMoveVisually",!te),c("wholeLineUpdateBefore",!0),c("theme","default",function(u){Qg(u),Rc(u)},!0),c("keyMap","default",function(u,p,g){var y=zc(p),C=g!=rl&&zc(g);C&&C.detach&&C.detach(u,y),y.attach&&y.attach(u,C||null)}),c("extraKeys",null),c("configureMouse",null),c("lineWrapping",!1,Jg,!0),c("gutters",[],function(u,p){u.display.gutterSpecs=yo(p,u.options.lineNumbers),Rc(u)},!0),c("fixedGutter",!0,function(u,p){u.display.gutters.style.left=p?kt(u.display)+"px":"0",u.refresh()},!0),c("coverGutterNextToScrollbar",!1,function(u){return Ko(u)},!0),c("scrollbarStyle","native",function(u){yg(u),Ko(u),u.display.scrollbars.setScrollTop(u.doc.scrollTop),u.display.scrollbars.setScrollLeft(u.doc.scrollLeft)},!0),c("lineNumbers",!1,function(u,p){u.display.gutterSpecs=yo(u.options.gutters,p),Rc(u)},!0),c("firstLineNumber",1,Rc,!0),c("lineNumberFormatter",function(u){return u},Rc,!0),c("showCursorWhenSelecting",!1,ra,!0),c("resetSelectionOnContextMenu",!0),c("lineWiseCopyCut",!0),c("pasteLinesPerSelection",!0),c("selectionsMayTouch",!1),c("readOnly",!1,function(u,p){p=="nocursor"&&(Jl(u),u.display.input.blur()),u.display.input.readOnlyChanged(p)}),c("screenReaderLabel",null,function(u,p){p=p===""?null:p,u.display.input.screenReaderLabelChanged(p)}),c("disableInput",!1,function(u,p){p||u.display.input.reset()},!0),c("dragDrop",!0,Fw),c("allowDropFileTypes",null),c("cursorBlinkRate",530),c("cursorScrollMargin",0),c("cursorHeight",1,ra,!0),c("singleCursorHeightPerLine",!0,ra,!0),c("workTime",100),c("workDelay",100),c("flattenSpans",!0,Ya,!0),c("addModeClass",!1,Ya,!0),c("pollInterval",100),c("undoDepth",200,function(u,p){return u.doc.history.undoDepth=p}),c("historyEventDelay",1250),c("viewportMargin",10,function(u){return u.refresh()},!0),c("maxHighlightLength",1e4,Ya,!0),c("moveInputWithCursor",!0,function(u,p){p||u.display.input.resetPosition()}),c("tabindex",null,function(u,p){return u.display.input.getField().tabIndex=p||""}),c("autofocus",null),c("direction","ltr",function(u,p){return u.doc.setDirection(p)},!0),c("phrases",null)}i(kh,"defineOptions");function Fw(r,o,c){var u=c&&c!=rl;if(!o!=!u){var p=r.display.dragFunctions,g=o?ze:Yt;g(r.display.scroller,"dragstart",p.start),g(r.display.scroller,"dragenter",p.enter),g(r.display.scroller,"dragover",p.over),g(r.display.scroller,"dragleave",p.leave),g(r.display.scroller,"drop",p.drop)}}i(Fw,"dragDropChanged");function Jg(r){r.options.lineWrapping?(It(r.display.wrapper,"CodeMirror-wrap"),r.display.sizer.style.minWidth="",r.display.sizerWidth=null):(Ee(r.display.wrapper,"CodeMirror-wrap"),Bl(r)),nd(r),Wn(r),Vo(r),setTimeout(function(){return Ko(r)},100)}i(Jg,"wrappingChanged");function wt(r,o){var c=this;if(!(this instanceof wt))return new wt(r,o);this.options=o=o?Gt(o):{},Gt(jc,o,!1);var u=o.value;typeof u=="string"?u=new Nn(u,o.mode,null,o.lineSeparator,o.direction):o.mode&&(u.modeOption=o.mode),this.doc=u;var p=new wt.inputStyles[o.inputStyle](this),g=this.display=new _w(r,u,p,o);g.wrapper.CodeMirror=this,Qg(this),o.lineWrapping&&(this.display.wrapper.className+=" CodeMirror-wrap"),yg(this),this.state={keyMaps:[],overlays:[],modeGen:0,overwrite:!1,delayingBlurEvent:!1,focused:!1,suppressEdits:!1,pasteIncoming:-1,cutIncoming:-1,selectingText:!1,draggingText:!1,highlight:new se,keySeq:null,specialChars:null},o.autofocus&&!L&&g.input.focus(),d&&v<11&&setTimeout(function(){return c.display.input.reset(!0)},20),Zg(this),$g(),Ka(this),this.curOp.forceUpdate=!0,ch(this,u),o.autofocus&&!L||this.hasFocus()?setTimeout(function(){c.hasFocus()&&!c.state.focused&&eh(c)},20):Jl(this);for(var y in Kc)Kc.hasOwnProperty(y)&&Kc[y](this,o[y],rl);Cg(this),o.finishInit&&o.finishInit(this);for(var C=0;C<_d.length;++C)_d[C](this);Ga(this),S&&o.lineWrapping&&getComputedStyle(g.lineDiv).textRendering=="optimizelegibility"&&(g.lineDiv.style.textRendering="auto")}i(wt,"CodeMirror"),wt.defaults=jc,wt.optionHandlers=Kc;function Zg(r){var o=r.display;ze(o.scroller,"mousedown",Lt(r,Gg)),d&&v<11?ze(o.scroller,"dblclick",Lt(r,function(_){if(!At(r,_)){var E=jo(r,_);if(!(!E||gu(r,_)||yi(r.display,_))){kr(_);var O=r.findWordAt(E);nu(r.doc,O.anchor,O.head)}}})):ze(o.scroller,"dblclick",function(_){return At(r,_)||kr(_)}),ze(o.scroller,"contextmenu",function(_){return xd(r,_)}),ze(o.input.getField(),"contextmenu",function(_){o.scroller.contains(_.target)||xd(r,_)});var c,u={end:0};function p(){o.activeTouch&&(c=setTimeout(function(){return o.activeTouch=null},1e3),u=o.activeTouch,u.end=+new Date)}i(p,"finishTouch");function g(_){if(_.touches.length!=1)return!1;var E=_.touches[0];return E.radiusX<=1&&E.radiusY<=1}i(g,"isMouseLikeTouchEvent");function y(_,E){if(E.left==null)return!0;var O=E.left-_.left,B=E.top-_.top;return O*O+B*B>20*20}i(y,"farAway"),ze(o.scroller,"touchstart",function(_){if(!At(r,_)&&!g(_)&&!gu(r,_)){o.input.ensurePolled(),clearTimeout(c);var E=+new Date;o.activeTouch={start:E,moved:!1,prev:E-u.end<=300?u:null},_.touches.length==1&&(o.activeTouch.left=_.touches[0].pageX,o.activeTouch.top=_.touches[0].pageY)}}),ze(o.scroller,"touchmove",function(){o.activeTouch&&(o.activeTouch.moved=!0)}),ze(o.scroller,"touchend",function(_){var E=o.activeTouch;if(E&&!yi(o,_)&&E.left!=null&&!E.moved&&new Date-E.start<300){var O=r.coordsChar(o.activeTouch,"page"),B;!E.prev||y(E,E.prev)?B=new Et(O,O):!E.prev.prev||y(E,E.prev.prev)?B=r.findWordAt(O):B=new Et(ce(O.line,0),qe(r.doc,ce(O.line+1,0))),r.setSelection(B.anchor,B.head),r.focus(),kr(_)}p()}),ze(o.scroller,"touchcancel",p),ze(o.scroller,"scroll",function(){o.scroller.clientHeight&&(Ac(r,o.scroller.scrollTop),go(r,o.scroller.scrollLeft,!0),Mt(r,"scroll",r))}),ze(o.scroller,"mousewheel",function(_){return Eg(r,_)}),ze(o.scroller,"DOMMouseScroll",function(_){return Eg(r,_)}),ze(o.wrapper,"scroll",function(){return o.wrapper.scrollTop=o.wrapper.scrollLeft=0}),o.dragFunctions={enter:i(function(_){At(r,_)||Pa(_)},"enter"),over:i(function(_){At(r,_)||(ut(r,_),Pa(_))},"over"),start:i(function(_){return wh(r,_)},"start"),drop:Lt(r,Pw),leave:i(function(_){At(r,_)||Ug(r)},"leave")};var C=o.input.getField();ze(C,"keyup",function(_){return Co.call(r,_)}),ze(C,"keydown",Lt(r,Eh)),ze(C,"keypress",Lt(r,jg)),ze(C,"focus",function(_){return eh(r,_)}),ze(C,"blur",function(_){return Jl(r,_)})}i(Zg,"registerEventHandlers");var _d=[];wt.defineInitHook=function(r){return _d.push(r)};function Gc(r,o,c,u){var p=r.doc,g;c==null&&(c="add"),c=="smart"&&(p.mode.indent?g=Ss(r,o).state:c="prev");var y=r.options.tabSize,C=Ae(p,o),_=Ft(C.text,null,y);C.stateAfter&&(C.stateAfter=null);var E=C.text.match(/^\s*/)[0],O;if(!u&&!/\S/.test(C.text))O=0,c="not";else if(c=="smart"&&(O=p.mode.indent(g,C.text.slice(E.length),C.text),O==Zt||O>150)){if(!u)return;c="prev"}c=="prev"?o>p.first?O=Ft(Ae(p,o-1).text,null,y):O=0:c=="add"?O=_+r.options.indentUnit:c=="subtract"?O=_-r.options.indentUnit:typeof c=="number"&&(O=_+c),O=Math.max(0,O);var B="",K=0;if(r.options.indentWithTabs)for(var V=Math.floor(O/y);V;--V)K+=y,B+=" ";if(Ky,_=kl(o),E=null;if(C&&u.ranges.length>1)if(Ui&&Ui.text.join(` +`)==o){if(u.ranges.length%Ui.text.length==0){E=[];for(var O=0;O=0;K--){var V=u.ranges[K],re=V.from(),ae=V.to();V.empty()&&(c&&c>0?re=ce(re.line,re.ch-c):r.state.overwrite&&!C?ae=ce(ae.line,Math.min(Ae(g,ae.line).text.length,ae.ch+lt(_).length)):C&&Ui&&Ui.lineWise&&Ui.text.join(` +`)==_.join(` +`)&&(re=ae=ce(re.line,0)));var pe={from:re,to:ae,text:E?E[K%E.length]:_,origin:p||(C?"paste":r.state.cutIncoming>y?"cut":"+input")};au(r.doc,pe),Kt(r,"inputRead",r,pe)}o&&!C&&bd(r,o),er(r),r.curOp.updateInput<2&&(r.curOp.updateInput=B),r.curOp.typing=!0,r.state.pasteIncoming=r.state.cutIncoming=-1}i(Nh,"applyTextInput");function Qo(r,o){var c=r.clipboardData&&r.clipboardData.getData("Text");if(c)return r.preventDefault(),!o.isReadOnly()&&!o.options.disableInput&&o.hasFocus()&&yn(o,function(){return Nh(o,c,0,null,"paste")}),!0}i(Qo,"handlePaste");function bd(r,o){if(!(!r.options.electricChars||!r.options.smartIndent))for(var c=r.doc.sel,u=c.ranges.length-1;u>=0;u--){var p=c.ranges[u];if(!(p.head.ch>100||u&&c.ranges[u-1].head.line==p.head.line)){var g=r.getModeAt(p.head),y=!1;if(g.electricChars){for(var C=0;C-1){y=Gc(r,p.head.line,"smart");break}}else g.electricInput&&g.electricInput.test(Ae(r.doc,p.head.line).text.slice(0,p.head.ch))&&(y=Gc(r,p.head.line,"smart"));y&&Kt(r,"electricInput",r,p.head.line)}}}i(bd,"triggerElectric");function Td(r){for(var o=[],c=[],u=0;ug&&(Gc(this,C.head.line,u,!0),g=C.head.line,y==this.doc.sel.primIndex&&er(this));else{var _=C.from(),E=C.to(),O=Math.max(g,_.line);g=Math.min(this.lastLine(),E.line-(E.ch?0:1))+1;for(var B=O;B0&&zi(this.doc,y,new Et(_,K[y].to()),st)}}}),getTokenAt:i(function(u,p){return Rl(this,u,p)},"getTokenAt"),getLineTokens:i(function(u,p){return Rl(this,ce(u),p,!0)},"getLineTokens"),getTokenTypeAt:i(function(u){u=qe(this.doc,u);var p=Ml(this,Ae(this.doc,u.line)),g=0,y=(p.length-1)/2,C=u.ch,_;if(C==0)_=p[2];else for(;;){var E=g+y>>1;if((E?p[E*2-1]:0)>=C)y=E;else if(p[E*2+1]_&&(u=_,y=!0),C=Ae(this.doc,u)}else C=u;return vn(this,C,{top:0,left:0},p||"page",g||y).top+(y?this.doc.height-vi(C):0)},"heightAtLine"),defaultTextHeight:i(function(){return po(this.display)},"defaultTextHeight"),defaultCharWidth:i(function(){return Cs(this.display)},"defaultCharWidth"),getViewport:i(function(){return{from:this.display.viewFrom,to:this.display.viewTo}},"getViewport"),addWidget:i(function(u,p,g,y,C){var _=this.display;u=J(this,qe(this.doc,u));var E=u.bottom,O=u.left;if(p.style.position="absolute",p.setAttribute("cm-ignore-events","true"),this.display.input.setUneditable(p),_.sizer.appendChild(p),y=="over")E=u.top;else if(y=="above"||y=="near"){var B=Math.max(_.wrapper.clientHeight,this.doc.height),K=Math.max(_.sizer.clientWidth,_.lineSpace.clientWidth);(y=="above"||u.bottom+p.offsetHeight>B)&&u.top>p.offsetHeight?E=u.top-p.offsetHeight:u.bottom+p.offsetHeight<=B&&(E=u.bottom),O+p.offsetWidth>K&&(O=K-p.offsetWidth)}p.style.top=E+"px",p.style.left=p.style.right="",C=="right"?(O=_.sizer.clientWidth-p.offsetWidth,p.style.right="0px"):(C=="left"?O=0:C=="middle"&&(O=(_.sizer.clientWidth-p.offsetWidth)/2),p.style.left=O+"px"),g&&vg(this,{left:O,top:E,right:O+p.offsetWidth,bottom:E+p.offsetHeight})},"addWidget"),triggerOnKeyDown:$e(Eh),triggerOnKeyPress:$e(jg),triggerOnKeyUp:Co,triggerOnMouseDown:$e(Gg),execCommand:i(function(u){if($c.hasOwnProperty(u))return $c[u].call(null,this)},"execCommand"),triggerElectric:$e(function(u){bd(this,u)}),findPosH:i(function(u,p,g,y){var C=1;p<0&&(C=-1,p=-p);for(var _=qe(this.doc,u),E=0;E0&&O(g.charAt(y-1));)--y;for(;C.5||this.options.lineWrapping)&&nd(this),Mt(this,"refresh",this)}),swapDoc:$e(function(u){var p=this.doc;return p.cm=null,this.state.selectingText&&this.state.selectingText(),ch(this,u),Vo(this),this.display.input.reset(),Pr(this,u.scrollLeft,u.scrollTop),this.curOp.forceScroll=!0,Kt(this,"swapDoc",this,p),p}),phrase:i(function(u){var p=this.options.phrases;return p&&Object.prototype.hasOwnProperty.call(p,u)?p[u]:u},"phrase"),getInputField:i(function(){return this.display.input.getField()},"getInputField"),getWrapperElement:i(function(){return this.display.wrapper},"getWrapperElement"),getScrollerElement:i(function(){return this.display.scroller},"getScrollerElement"),getGutterElement:i(function(){return this.display.gutters},"getGutterElement")},zn(r),r.registerHelper=function(u,p,g){c.hasOwnProperty(u)||(c[u]=r[u]={_global:[]}),c[u][p]=g},r.registerGlobalHelper=function(u,p,g,y){r.registerHelper(u,p,y),c[u]._global.push({pred:g,val:y})}}i(vu,"addEditorMethods");function We(r,o,c,u,p){var g=o,y=c,C=Ae(r,o.line),_=p&&r.direction=="rtl"?-c:c;function E(){var _e=o.line+_;return _e=r.first+r.size?!1:(o=new ce(_e,o.ch,o.sticky),C=Ae(r,_e))}i(E,"findNextLine");function O(_e){var ye;if(u=="codepoint"){var ke=C.text.charCodeAt(o.ch+(c>0?0:-1));if(isNaN(ke))ye=null;else{var Ie=c>0?ke>=55296&&ke<56320:ke>=56320&&ke<57343;ye=new ce(o.line,Math.max(0,Math.min(C.text.length,o.ch+c*(Ie?2:1))),-c)}}else p?ye=fa(r.cm,C,o,c):ye=Uc(C,o,c);if(ye==null)if(!_e&&E())o=Wc(p,r.cm,C,o.line,_);else return!1;else o=ye;return!0}if(i(O,"moveOnce"),u=="char"||u=="codepoint")O();else if(u=="column")O(!0);else if(u=="word"||u=="group")for(var B=null,K=u=="group",V=r.cm&&r.cm.getHelper(o,"wordChars"),re=!0;!(c<0&&!O(!re));re=!1){var ae=C.text.charAt(o.ch)||` +`,pe=Yr(ae,V)?"w":K&&ae==` +`?"n":!K||/\s/.test(ae)?null:"p";if(K&&!re&&!pe&&(pe="s"),B&&B!=pe){c<0&&(c=1,O(),o.sticky="after");break}if(pe&&(B=pe),c>0&&!O(!re))break}var we=ou(r,o,g,y,!0);return qs(g,we)&&(we.hitSide=!0),we}i(We,"findPosH");function Qc(r,o,c,u){var p=r.doc,g=o.left,y;if(u=="page"){var C=Math.min(r.display.wrapper.clientHeight,vr(r).innerHeight||p(r).documentElement.clientHeight),_=Math.max(C-.5*po(r.display),3);y=(c>0?o.bottom:o.top)+c*_}else u=="line"&&(y=c>0?o.bottom+3:o.top-3);for(var E;E=yt(r,g,y),!!E.outside;){if(c<0?y<=0:y>=p.height){E.hitSide=!0;break}y+=c*5}return E}i(Qc,"findPosV");var bt=i(function(r){this.cm=r,this.lastAnchorNode=this.lastAnchorOffset=this.lastFocusNode=this.lastFocusOffset=null,this.polling=new se,this.composing=null,this.gracePeriod=!1,this.readDOMTimeout=null},"ContentEditableInput");bt.prototype.init=function(r){var o=this,c=this,u=c.cm,p=c.div=r.lineDiv;p.contentEditable=!0,Yc(p,u.options.spellcheck,u.options.autocorrect,u.options.autocapitalize);function g(C){for(var _=C.target;_;_=_.parentNode){if(_==p)return!0;if(/\bCodeMirror-(?:line)?widget\b/.test(_.className))break}return!1}i(g,"belongsToInput"),ze(p,"paste",function(C){!g(C)||At(u,C)||Qo(C,u)||v<=11&&setTimeout(Lt(u,function(){return o.updateFromDOM()}),20)}),ze(p,"compositionstart",function(C){o.composing={data:C.data,done:!1}}),ze(p,"compositionupdate",function(C){o.composing||(o.composing={data:C.data,done:!1})}),ze(p,"compositionend",function(C){o.composing&&(C.data!=o.composing.data&&o.readFromDOMSoon(),o.composing.done=!0)}),ze(p,"touchstart",function(){return c.forceCompositionEnd()}),ze(p,"input",function(){o.composing||o.readFromDOMSoon()});function y(C){if(!(!g(C)||At(u,C))){if(u.somethingSelected())Ed({lineWise:!1,text:u.getSelections()}),C.type=="cut"&&u.replaceSelection("",null,"cut");else if(u.options.lineWiseCopyCut){var _=Td(u);Ed({lineWise:!0,text:_.text}),C.type=="cut"&&u.operation(function(){u.setSelections(_.ranges,0,st),u.replaceSelection("",null,"cut")})}else return;if(C.clipboardData){C.clipboardData.clearData();var E=Ui.text.join(` +`);if(C.clipboardData.setData("Text",E),C.clipboardData.getData("Text")==E){C.preventDefault();return}}var O=Xc(),B=O.firstChild;Yc(B),u.display.lineSpace.insertBefore(O,u.display.lineSpace.firstChild),B.value=Ui.text.join(` +`);var K=ot(Wt(p));fn(B),setTimeout(function(){u.display.lineSpace.removeChild(O),K.focus(),K==p&&c.showPrimarySelection()},50)}}i(y,"onCopyCut"),ze(p,"copy",y),ze(p,"cut",y)},bt.prototype.screenReaderLabelChanged=function(r){r?this.div.setAttribute("aria-label",r):this.div.removeAttribute("aria-label")},bt.prototype.prepareSelection=function(){var r=Zp(this.cm,!1);return r.focus=ot(Wt(this.div))==this.div,r},bt.prototype.showSelection=function(r,o){!r||!this.cm.display.view.length||((r.focus||o)&&this.showPrimarySelection(),this.showMultipleSelections(r))},bt.prototype.getSelection=function(){return this.cm.display.wrapper.ownerDocument.getSelection()},bt.prototype.showPrimarySelection=function(){var r=this.getSelection(),o=this.cm,c=o.doc.sel.primary(),u=c.from(),p=c.to();if(o.display.viewTo==o.display.viewFrom||u.line>=o.display.viewTo||p.line=o.display.viewFrom&&da(o,u)||{node:C[0].measure.map[2],offset:0},E=p.liner.firstLine()&&(u=ce(u.line-1,Ae(r.doc,u.line-1).length)),p.ch==Ae(r.doc,p.line).text.length&&p.lineo.viewTo-1)return!1;var g,y,C;u.line==o.viewFrom||(g=Ua(r,u.line))==0?(y=vt(o.view[0].line),C=o.view[0].node):(y=vt(o.view[g].line),C=o.view[g-1].node.nextSibling);var _=Ua(r,p.line),E,O;if(_==o.view.length-1?(E=o.viewTo-1,O=o.lineDiv.lastChild):(E=vt(o.view[_+1].line)-1,O=o.view[_+1].node.previousSibling),!C)return!1;for(var B=r.doc.splitLines(ev(r,C,O,y,E)),K=lo(r.doc,ce(y,0),ce(E,Ae(r.doc,E).text.length));B.length>1&&K.length>1;)if(lt(B)==lt(K))B.pop(),K.pop(),E--;else if(B[0]==K[0])B.shift(),K.shift(),y++;else break;for(var V=0,re=0,ae=B[0],pe=K[0],we=Math.min(ae.length,pe.length);Vu.ch&&_e.charCodeAt(_e.length-re-1)==ye.charCodeAt(ye.length-re-1);)V--,re++;B[B.length-1]=_e.slice(0,_e.length-re).replace(/^\u200b+/,""),B[0]=B[0].slice(V).replace(/\u200b+$/,"");var Ie=ce(y,V),Oe=ce(E,K.length?lt(K).length-re:0);if(B.length>1||B[0]||Ve(Ie,Oe))return Qa(r.doc,B,Ie,Oe,"+input"),!0},bt.prototype.ensurePolled=function(){this.forceCompositionEnd()},bt.prototype.reset=function(){this.forceCompositionEnd()},bt.prototype.forceCompositionEnd=function(){this.composing&&(clearTimeout(this.readDOMTimeout),this.composing=null,this.updateFromDOM(),this.div.blur(),this.div.focus())},bt.prototype.readFromDOMSoon=function(){var r=this;this.readDOMTimeout==null&&(this.readDOMTimeout=setTimeout(function(){if(r.readDOMTimeout=null,r.composing)if(r.composing.done)r.composing=null;else return;r.updateFromDOM()},80))},bt.prototype.updateFromDOM=function(){var r=this;(this.cm.isReadOnly()||!this.pollContent())&&yn(this.cm,function(){return Wn(r.cm)})},bt.prototype.setUneditable=function(r){r.contentEditable="false"},bt.prototype.onKeyPress=function(r){r.charCode==0||this.composing||(r.preventDefault(),this.cm.isReadOnly()||Lt(this.cm,Nh)(this.cm,String.fromCharCode(r.charCode==null?r.keyCode:r.charCode),0))},bt.prototype.readOnlyChanged=function(r){this.div.contentEditable=String(r!="nocursor")},bt.prototype.onContextMenu=function(){},bt.prototype.resetPosition=function(){},bt.prototype.needsContentAttribute=!0;function da(r,o){var c=Gl(r,o.line);if(!c||c.hidden)return null;var u=Ae(r.doc,o.line),p=td(c,u,o.line),g=Me(u,r.doc.direction),y="left";if(g){var C=ee(g,o.ch);y=C%2?"right":"left"}var _=U(p.map,o.ch,y);return _.offset=_.collapse=="right"?_.end:_.start,_}i(da,"posToDOM");function Bw(r){for(var o=r;o;o=o.parentNode)if(/CodeMirror-gutter-wrapper/.test(o.className))return!0;return!1}i(Bw,"isInGutter");function pa(r,o){return o&&(r.bad=!0),r}i(pa,"badPos");function ev(r,o,c,u,p){var g="",y=!1,C=r.doc.lineSeparator(),_=!1;function E(V){return function(re){return re.id==V}}i(E,"recognizeMarker");function O(){y&&(g+=C,_&&(g+=C),y=_=!1)}i(O,"close");function B(V){V&&(O(),g+=V)}i(B,"addText");function K(V){if(V.nodeType==1){var re=V.getAttribute("cm-text");if(re){B(re);return}var ae=V.getAttribute("cm-marker"),pe;if(ae){var we=r.findMarks(ce(u,0),ce(p+1,0),E(+ae));we.length&&(pe=we[0].find(0))&&B(lo(r.doc,pe.from,pe.to).join(C));return}if(V.getAttribute("contenteditable")=="false")return;var _e=/^(pre|div|p|li|table|br)$/i.test(V.nodeName);if(!/^br$/i.test(V.nodeName)&&V.textContent.length==0)return;_e&&O();for(var ye=0;ye=9&&o.hasSelection&&(o.hasSelection=null),c.poll()}),ze(p,"paste",function(y){At(u,y)||Qo(y,u)||(u.state.pasteIncoming=+new Date,c.fastPoll())});function g(y){if(!At(u,y)){if(u.somethingSelected())Ed({lineWise:!1,text:u.getSelections()});else if(u.options.lineWiseCopyCut){var C=Td(u);Ed({lineWise:!0,text:C.text}),y.type=="cut"?u.setSelections(C.ranges,null,st):(c.prevInput="",p.value=C.text.join(` +`),fn(p))}else return;y.type=="cut"&&(u.state.cutIncoming=+new Date)}}i(g,"prepareCopyCut"),ze(p,"cut",g),ze(p,"copy",g),ze(r.scroller,"paste",function(y){if(!(yi(r,y)||At(u,y))){if(!p.dispatchEvent){u.state.pasteIncoming=+new Date,c.focus();return}var C=new Event("paste");C.clipboardData=y.clipboardData,p.dispatchEvent(C)}}),ze(r.lineSpace,"selectstart",function(y){yi(r,y)||kr(y)}),ze(p,"compositionstart",function(){var y=u.getCursor("from");c.composing&&c.composing.range.clear(),c.composing={start:y,range:u.markText(y,u.getCursor("to"),{className:"CodeMirror-composing"})}}),ze(p,"compositionend",function(){c.composing&&(c.poll(),c.composing.range.clear(),c.composing=null)})},dr.prototype.createField=function(r){this.wrapper=Xc(),this.textarea=this.wrapper.firstChild;var o=this.cm.options;Yc(this.textarea,o.spellcheck,o.autocorrect,o.autocapitalize)},dr.prototype.screenReaderLabelChanged=function(r){r?this.textarea.setAttribute("aria-label",r):this.textarea.removeAttribute("aria-label")},dr.prototype.prepareSelection=function(){var r=this.cm,o=r.display,c=r.doc,u=Zp(r);if(r.options.moveInputWithCursor){var p=J(r,c.sel.primary().head,"div"),g=o.wrapper.getBoundingClientRect(),y=o.lineDiv.getBoundingClientRect();u.teTop=Math.max(0,Math.min(o.wrapper.clientHeight-10,p.top+y.top-g.top)),u.teLeft=Math.max(0,Math.min(o.wrapper.clientWidth-10,p.left+y.left-g.left))}return u},dr.prototype.showSelection=function(r){var o=this.cm,c=o.display;tt(c.cursorDiv,r.cursors),tt(c.selectionDiv,r.selection),r.teTop!=null&&(this.wrapper.style.top=r.teTop+"px",this.wrapper.style.left=r.teLeft+"px")},dr.prototype.reset=function(r){if(!(this.contextMenuPending||this.composing&&r)){var o=this.cm;if(this.resetting=!0,o.somethingSelected()){this.prevInput="";var c=o.getSelection();this.textarea.value=c,o.state.focused&&fn(this.textarea),d&&v>=9&&(this.hasSelection=c)}else r||(this.prevInput=this.textarea.value="",d&&v>=9&&(this.hasSelection=null));this.resetting=!1}},dr.prototype.getField=function(){return this.textarea},dr.prototype.supportsTouch=function(){return!1},dr.prototype.focus=function(){if(this.cm.options.readOnly!="nocursor"&&(!L||ot(Wt(this.textarea))!=this.textarea))try{this.textarea.focus()}catch{}},dr.prototype.blur=function(){this.textarea.blur()},dr.prototype.resetPosition=function(){this.wrapper.style.top=this.wrapper.style.left=0},dr.prototype.receivedFocus=function(){this.slowPoll()},dr.prototype.slowPoll=function(){var r=this;this.pollingFast||this.polling.set(this.cm.options.pollInterval,function(){r.poll(),r.cm.state.focused&&r.slowPoll()})},dr.prototype.fastPoll=function(){var r=!1,o=this;o.pollingFast=!0;function c(){var u=o.poll();!u&&!r?(r=!0,o.polling.set(60,c)):(o.pollingFast=!1,o.slowPoll())}i(c,"p"),o.polling.set(20,c)},dr.prototype.poll=function(){var r=this,o=this.cm,c=this.textarea,u=this.prevInput;if(this.contextMenuPending||this.resetting||!o.state.focused||Hp(c)&&!u&&!this.composing||o.isReadOnly()||o.options.disableInput||o.state.keySeq)return!1;var p=c.value;if(p==u&&!o.somethingSelected())return!1;if(d&&v>=9&&this.hasSelection===p||D&&/[\uf700-\uf7ff]/.test(p))return o.display.input.reset(),!1;if(o.doc.sel==o.display.selForContextMenu){var g=p.charCodeAt(0);if(g==8203&&!u&&(u="\u200B"),g==8666)return this.reset(),this.cm.execCommand("undo")}for(var y=0,C=Math.min(u.length,p.length);y1e3||p.indexOf(` +`)>-1?c.value=r.prevInput="":r.prevInput=p,r.composing&&(r.composing.range.clear(),r.composing.range=o.markText(r.composing.start,o.getCursor("to"),{className:"CodeMirror-composing"}))}),!0},dr.prototype.ensurePolled=function(){this.pollingFast&&this.poll()&&(this.pollingFast=!1)},dr.prototype.onKeyPress=function(){d&&v>=9&&(this.hasSelection=null),this.fastPoll()},dr.prototype.onContextMenu=function(r){var o=this,c=o.cm,u=c.display,p=o.textarea;o.contextMenuPending&&o.contextMenuPending();var g=jo(c,r),y=u.scroller.scrollTop;if(!g||I)return;var C=c.options.resetSelectionOnContextMenu;C&&c.doc.sel.contains(g)==-1&&Lt(c,Zr)(c.doc,oa(g),st);var _=p.style.cssText,E=o.wrapper.style.cssText,O=o.wrapper.offsetParent.getBoundingClientRect();o.wrapper.style.cssText="position: static",p.style.cssText=`position: absolute; width: 30px; height: 30px; + top: `+(r.clientY-O.top-5)+"px; left: "+(r.clientX-O.left-5)+`px; + z-index: 1000; background: `+(d?"rgba(255, 255, 255, .05)":"transparent")+`; + outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);`;var B;S&&(B=p.ownerDocument.defaultView.scrollY),u.input.focus(),S&&p.ownerDocument.defaultView.scrollTo(null,B),u.input.reset(),c.somethingSelected()||(p.value=o.prevInput=" "),o.contextMenuPending=V,u.selForContextMenu=c.doc.sel,clearTimeout(u.detectingSelectAll);function K(){if(p.selectionStart!=null){var ae=c.somethingSelected(),pe="\u200B"+(ae?p.value:"");p.value="\u21DA",p.value=pe,o.prevInput=ae?"":"\u200B",p.selectionStart=1,p.selectionEnd=pe.length,u.selForContextMenu=c.doc.sel}}i(K,"prepareSelectAllHack");function V(){if(o.contextMenuPending==V&&(o.contextMenuPending=!1,o.wrapper.style.cssText=E,p.style.cssText=_,d&&v<9&&u.scrollbars.setScrollTop(u.scroller.scrollTop=y),p.selectionStart!=null)){(!d||d&&v<9)&&K();var ae=0,pe=i(function(){u.selForContextMenu==c.doc.sel&&p.selectionStart==0&&p.selectionEnd>0&&o.prevInput=="\u200B"?Lt(c,su)(c):ae++<10?u.detectingSelectAll=setTimeout(pe,500):(u.selForContextMenu=null,u.input.reset())},"poll");u.detectingSelectAll=setTimeout(pe,200)}}if(i(V,"rehide"),d&&v>=9&&K(),he){Pa(r);var re=i(function(){Yt(window,"mouseup",re),setTimeout(V,20)},"mouseup");ze(window,"mouseup",re)}else setTimeout(V,50)},dr.prototype.readOnlyChanged=function(r){r||this.reset(),this.textarea.disabled=r=="nocursor",this.textarea.readOnly=!!r},dr.prototype.setUneditable=function(){},dr.prototype.needsContentAttribute=!1;function tv(r,o){if(o=o?Gt(o):{},o.value=r.value,!o.tabindex&&r.tabIndex&&(o.tabindex=r.tabIndex),!o.placeholder&&r.placeholder&&(o.placeholder=r.placeholder),o.autofocus==null){var c=ot(Wt(r));o.autofocus=c==r||r.getAttribute("autofocus")!=null&&c==document.body}function u(){r.value=C.getValue()}i(u,"save");var p;if(r.form&&(ze(r.form,"submit",u),!o.leaveSubmitMethodAlone)){var g=r.form;p=g.submit;try{var y=g.submit=function(){u(),g.submit=p,g.submit(),g.submit=y}}catch{}}o.finishInit=function(_){_.save=u,_.getTextArea=function(){return r},_.toTextArea=function(){_.toTextArea=isNaN,u(),r.parentNode.removeChild(_.getWrapperElement()),r.style.display="",r.form&&(Yt(r.form,"submit",u),!o.leaveSubmitMethodAlone&&typeof r.form.submit=="function"&&(r.form.submit=p))}},r.style.display="none";var C=wt(function(_){return r.parentNode.insertBefore(_,r.nextSibling)},o);return C}i(tv,"fromTextArea");function zw(r){r.off=Yt,r.on=ze,r.wheelEventPixels=Ew,r.Doc=Nn,r.splitLines=kl,r.countColumn=Ft,r.findColumn=bn,r.isWordChar=Mi,r.Pass=Zt,r.signal=Mt,r.Line=hn,r.changeEnd=sa,r.scrollbarModel=ja,r.Pos=ce,r.cmpPos=Ve,r.modes=ac,r.mimeModes=vs,r.resolveMode=La,r.getMode=lc,r.modeExtensions=ao,r.extendMode=qf,r.copyState=Oi,r.startState=cc,r.innerMode=uc,r.commands=$c,r.keyMap=Yo,r.keyName=Hc,r.isModifierKey=vd,r.lookupKey=xi,r.normalizeKeyMap=Si,r.StringStream=or,r.SharedTextMarker=cu,r.TextMarker=xo,r.LineWidget=Za,r.e_preventDefault=kr,r.e_stopPropagation=gs,r.e_stop=Pa,r.addClass=It,r.contains=Qe,r.rmClass=Ee,r.keyNames=Hr}i(zw,"addLegacyProps"),kh(wt),vu(wt);var rv="iter insert remove copy getEditor constructor".split(" ");for(var kd in Nn.prototype)Nn.prototype.hasOwnProperty(kd)&&Ue(rv,kd)<0&&(wt.prototype[kd]=function(r){return function(){return r.apply(this.doc,arguments)}}(Nn.prototype[kd]));return zn(Nn),wt.inputStyles={textarea:dr,contenteditable:bt},wt.defineMode=function(r){!wt.defaults.mode&&r!="null"&&(wt.defaults.mode=r),Up.apply(this,arguments)},wt.defineMIME=Wp,wt.defineMode("null",function(){return{token:i(function(r){return r.skipToEnd()},"token")}}),wt.defineMIME("text/plain","null"),wt.defineExtension=function(r,o){wt.prototype[r]=o},wt.defineDocExtension=function(r,o){Nn.prototype[r]=o},wt.fromTextArea=tv,zw(wt),wt.version="5.65.16",wt})});var UL=rt((BK,zL)=>{var Fz=typeof Element<"u",Bz=typeof Map=="function",Hz=typeof Set=="function",zz=typeof ArrayBuffer=="function"&&!!ArrayBuffer.isView;function R0(e,t){if(e===t)return!0;if(e&&t&&typeof e=="object"&&typeof t=="object"){if(e.constructor!==t.constructor)return!1;var n,s,l;if(Array.isArray(e)){if(n=e.length,n!=t.length)return!1;for(s=n;s--!==0;)if(!R0(e[s],t[s]))return!1;return!0}var h;if(Bz&&e instanceof Map&&t instanceof Map){if(e.size!==t.size)return!1;for(h=e.entries();!(s=h.next()).done;)if(!t.has(s.value[0]))return!1;for(h=e.entries();!(s=h.next()).done;)if(!R0(s.value[1],t.get(s.value[0])))return!1;return!0}if(Hz&&e instanceof Set&&t instanceof Set){if(e.size!==t.size)return!1;for(h=e.entries();!(s=h.next()).done;)if(!t.has(s.value[0]))return!1;return!0}if(zz&&ArrayBuffer.isView(e)&&ArrayBuffer.isView(t)){if(n=e.length,n!=t.length)return!1;for(s=n;s--!==0;)if(e[s]!==t[s])return!1;return!0}if(e.constructor===RegExp)return e.source===t.source&&e.flags===t.flags;if(e.valueOf!==Object.prototype.valueOf&&typeof e.valueOf=="function"&&typeof t.valueOf=="function")return e.valueOf()===t.valueOf();if(e.toString!==Object.prototype.toString&&typeof e.toString=="function"&&typeof t.toString=="function")return e.toString()===t.toString();if(l=Object.keys(e),n=l.length,n!==Object.keys(t).length)return!1;for(s=n;s--!==0;)if(!Object.prototype.hasOwnProperty.call(t,l[s]))return!1;if(Fz&&e instanceof Element)return!1;for(s=n;s--!==0;)if(!((l[s]==="_owner"||l[s]==="__v"||l[s]==="__o")&&e.$$typeof)&&!R0(e[l[s]],t[l[s]]))return!1;return!0}return e!==e&&t!==t}i(R0,"equal");zL.exports=i(function(t,n){try{return R0(t,n)}catch(s){if((s.message||"").match(/stack|recursion/i))return console.warn("react-fast-compare cannot handle circular refs"),!1;throw s}},"isEqual")});var aM=rt((KY,sM)=>{sM.exports=function(){return typeof Promise=="function"&&Promise.prototype&&Promise.prototype.then}});var Qu=rt(Ff=>{var q_,eU=[0,26,44,70,100,134,172,196,242,292,346,404,466,532,581,655,733,815,901,991,1085,1156,1258,1364,1474,1588,1706,1828,1921,2051,2185,2323,2465,2611,2761,2876,3034,3196,3362,3532,3706];Ff.getSymbolSize=i(function(t){if(!t)throw new Error('"version" cannot be null or undefined');if(t<1||t>40)throw new Error('"version" should be in range from 1 to 40');return t*4+17},"getSymbolSize");Ff.getSymbolTotalCodewords=i(function(t){return eU[t]},"getSymbolTotalCodewords");Ff.getBCHDigit=function(e){let t=0;for(;e!==0;)t++,e>>>=1;return t};Ff.setToSJISFunction=i(function(t){if(typeof t!="function")throw new Error('"toSJISFunc" is not a valid function.');q_=t},"setToSJISFunction");Ff.isKanjiModeEnabled=function(){return typeof q_<"u"};Ff.toSJIS=i(function(t){return q_(t)},"toSJIS")});var q0=rt(ps=>{ps.L={bit:1};ps.M={bit:0};ps.Q={bit:3};ps.H={bit:2};function tU(e){if(typeof e!="string")throw new Error("Param is not a string");switch(e.toLowerCase()){case"l":case"low":return ps.L;case"m":case"medium":return ps.M;case"q":case"quartile":return ps.Q;case"h":case"high":return ps.H;default:throw new Error("Unknown EC Level: "+e)}}i(tU,"fromString");ps.isValid=i(function(t){return t&&typeof t.bit<"u"&&t.bit>=0&&t.bit<4},"isValid");ps.from=i(function(t,n){if(ps.isValid(t))return t;try{return tU(t)}catch{return n}},"from")});var cM=rt((JY,uM)=>{function lM(){this.buffer=[],this.length=0}i(lM,"BitBuffer");lM.prototype={get:i(function(e){let t=Math.floor(e/8);return(this.buffer[t]>>>7-e%8&1)===1},"get"),put:i(function(e,t){for(let n=0;n>>t-n-1&1)===1)},"put"),getLengthInBits:i(function(){return this.length},"getLengthInBits"),putBit:i(function(e){let t=Math.floor(this.length/8);this.buffer.length<=t&&this.buffer.push(0),e&&(this.buffer[t]|=128>>>this.length%8),this.length++},"putBit")};uM.exports=lM});var dM=rt((eX,fM)=>{function rg(e){if(!e||e<1)throw new Error("BitMatrix size must be defined and greater than 0");this.size=e,this.data=new Uint8Array(e*e),this.reservedBit=new Uint8Array(e*e)}i(rg,"BitMatrix");rg.prototype.set=function(e,t,n,s){let l=e*this.size+t;this.data[l]=n,s&&(this.reservedBit[l]=!0)};rg.prototype.get=function(e,t){return this.data[e*this.size+t]};rg.prototype.xor=function(e,t,n){this.data[e*this.size+t]^=n};rg.prototype.isReserved=function(e,t){return this.reservedBit[e*this.size+t]};fM.exports=rg});var pM=rt(j0=>{var rU=Qu().getSymbolSize;j0.getRowColCoords=i(function(t){if(t===1)return[];let n=Math.floor(t/7)+2,s=rU(t),l=s===145?26:Math.ceil((s-13)/(2*n-2))*2,h=[s-7];for(let d=1;d{var nU=Qu().getSymbolSize,hM=7;mM.getPositions=i(function(t){let n=nU(t);return[[0,0],[n-hM,0],[0,n-hM]]},"getPositions")});var vM=rt(ir=>{ir.Patterns={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7};var Bf={N1:3,N2:3,N3:40,N4:10};ir.isValid=i(function(t){return t!=null&&t!==""&&!isNaN(t)&&t>=0&&t<=7},"isValid");ir.from=i(function(t){return ir.isValid(t)?parseInt(t,10):void 0},"from");ir.getPenaltyN1=i(function(t){let n=t.size,s=0,l=0,h=0,d=null,v=null;for(let S=0;S=5&&(s+=Bf.N1+(l-5)),d=k,l=1),k=t.get(b,S),k===v?h++:(h>=5&&(s+=Bf.N1+(h-5)),v=k,h=1)}l>=5&&(s+=Bf.N1+(l-5)),h>=5&&(s+=Bf.N1+(h-5))}return s},"getPenaltyN1");ir.getPenaltyN2=i(function(t){let n=t.size,s=0;for(let l=0;l=10&&(l===1488||l===93)&&s++,h=h<<1&2047|t.get(v,d),v>=10&&(h===1488||h===93)&&s++}return s*Bf.N3},"getPenaltyN3");ir.getPenaltyN4=i(function(t){let n=0,s=t.data.length;for(let h=0;h{var Ju=q0(),K0=[1,1,1,1,1,1,1,1,1,1,2,2,1,2,2,4,1,2,4,4,2,4,4,4,2,4,6,5,2,4,6,6,2,5,8,8,4,5,8,8,4,5,8,11,4,8,10,11,4,9,12,16,4,9,16,16,6,10,12,18,6,10,17,16,6,11,16,19,6,13,18,21,7,14,21,25,8,16,20,25,8,17,23,25,9,17,23,34,9,18,25,30,10,20,27,32,12,21,29,35,12,23,34,37,12,25,34,40,13,26,35,42,14,28,38,45,15,29,40,48,16,31,43,51,17,33,45,54,18,35,48,57,19,37,51,60,19,38,53,63,20,40,56,66,21,43,59,70,22,45,62,74,24,47,65,77,25,49,68,81],G0=[7,10,13,17,10,16,22,28,15,26,36,44,20,36,52,64,26,48,72,88,36,64,96,112,40,72,108,130,48,88,132,156,60,110,160,192,72,130,192,224,80,150,224,264,96,176,260,308,104,198,288,352,120,216,320,384,132,240,360,432,144,280,408,480,168,308,448,532,180,338,504,588,196,364,546,650,224,416,600,700,224,442,644,750,252,476,690,816,270,504,750,900,300,560,810,960,312,588,870,1050,336,644,952,1110,360,700,1020,1200,390,728,1050,1260,420,784,1140,1350,450,812,1200,1440,480,868,1290,1530,510,924,1350,1620,540,980,1440,1710,570,1036,1530,1800,570,1064,1590,1890,600,1120,1680,1980,630,1204,1770,2100,660,1260,1860,2220,720,1316,1950,2310,750,1372,2040,2430];j_.getBlocksCount=i(function(t,n){switch(n){case Ju.L:return K0[(t-1)*4+0];case Ju.M:return K0[(t-1)*4+1];case Ju.Q:return K0[(t-1)*4+2];case Ju.H:return K0[(t-1)*4+3];default:return}},"getBlocksCount");j_.getTotalCodewordsCount=i(function(t,n){switch(n){case Ju.L:return G0[(t-1)*4+0];case Ju.M:return G0[(t-1)*4+1];case Ju.Q:return G0[(t-1)*4+2];case Ju.H:return G0[(t-1)*4+3];default:return}},"getTotalCodewordsCount")});var yM=rt(X0=>{var ng=new Uint8Array(512),Y0=new Uint8Array(256);i(function(){let t=1;for(let n=0;n<255;n++)ng[n]=t,Y0[t]=n,t<<=1,t&256&&(t^=285);for(let n=255;n<512;n++)ng[n]=ng[n-255]},"initTables")();X0.log=i(function(t){if(t<1)throw new Error("log("+t+")");return Y0[t]},"log");X0.exp=i(function(t){return ng[t]},"exp");X0.mul=i(function(t,n){return t===0||n===0?0:ng[Y0[t]+Y0[n]]},"mul")});var wM=rt(ig=>{var G_=yM();ig.mul=i(function(t,n){let s=new Uint8Array(t.length+n.length-1);for(let l=0;l=0;){let l=s[0];for(let d=0;d{var SM=wM();function Y_(e){this.genPoly=void 0,this.degree=e,this.degree&&this.initialize(this.degree)}i(Y_,"ReedSolomonEncoder");Y_.prototype.initialize=i(function(t){this.degree=t,this.genPoly=SM.generateECPolynomial(this.degree)},"initialize");Y_.prototype.encode=i(function(t){if(!this.genPoly)throw new Error("Encoder not initialized");let n=new Uint8Array(t.length+this.degree);n.set(t);let s=SM.mod(n,this.genPoly),l=this.degree-s.length;if(l>0){let h=new Uint8Array(this.degree);return h.set(s,l),h}return s},"encode");xM.exports=Y_});var X_=rt(_M=>{_M.isValid=i(function(t){return!isNaN(t)&&t>=1&&t<=40},"isValid")});var Q_=rt(_l=>{var EM="[0-9]+",oU="[A-Z $%*+\\-./:]+",og="(?:[u3000-u303F]|[u3040-u309F]|[u30A0-u30FF]|[uFF00-uFFEF]|[u4E00-u9FAF]|[u2605-u2606]|[u2190-u2195]|u203B|[u2010u2015u2018u2019u2025u2026u201Cu201Du2225u2260]|[u0391-u0451]|[u00A7u00A8u00B1u00B4u00D7u00F7])+";og=og.replace(/u/g,"\\u");var sU="(?:(?![A-Z0-9 $%*+\\-./:]|"+og+`)(?:.|[\r +]))+`;_l.KANJI=new RegExp(og,"g");_l.BYTE_KANJI=new RegExp("[^A-Z0-9 $%*+\\-./:]+","g");_l.BYTE=new RegExp(sU,"g");_l.NUMERIC=new RegExp(EM,"g");_l.ALPHANUMERIC=new RegExp(oU,"g");var aU=new RegExp("^"+og+"$"),lU=new RegExp("^"+EM+"$"),uU=new RegExp("^[A-Z0-9 $%*+\\-./:]+$");_l.testKanji=i(function(t){return aU.test(t)},"testKanji");_l.testNumeric=i(function(t){return lU.test(t)},"testNumeric");_l.testAlphanumeric=i(function(t){return uU.test(t)},"testAlphanumeric")});var Zu=rt(an=>{var cU=X_(),J_=Q_();an.NUMERIC={id:"Numeric",bit:1,ccBits:[10,12,14]};an.ALPHANUMERIC={id:"Alphanumeric",bit:2,ccBits:[9,11,13]};an.BYTE={id:"Byte",bit:4,ccBits:[8,16,16]};an.KANJI={id:"Kanji",bit:8,ccBits:[8,10,12]};an.MIXED={bit:-1};an.getCharCountIndicator=i(function(t,n){if(!t.ccBits)throw new Error("Invalid mode: "+t);if(!cU.isValid(n))throw new Error("Invalid version: "+n);return n>=1&&n<10?t.ccBits[0]:n<27?t.ccBits[1]:t.ccBits[2]},"getCharCountIndicator");an.getBestModeForData=i(function(t){return J_.testNumeric(t)?an.NUMERIC:J_.testAlphanumeric(t)?an.ALPHANUMERIC:J_.testKanji(t)?an.KANJI:an.BYTE},"getBestModeForData");an.toString=i(function(t){if(t&&t.id)return t.id;throw new Error("Invalid mode")},"toString");an.isValid=i(function(t){return t&&t.bit&&t.ccBits},"isValid");function fU(e){if(typeof e!="string")throw new Error("Param is not a string");switch(e.toLowerCase()){case"numeric":return an.NUMERIC;case"alphanumeric":return an.ALPHANUMERIC;case"kanji":return an.KANJI;case"byte":return an.BYTE;default:throw new Error("Unknown mode: "+e)}}i(fU,"fromString");an.from=i(function(t,n){if(an.isValid(t))return t;try{return fU(t)}catch{return n}},"from")});var PM=rt(Hf=>{var Q0=Qu(),dU=K_(),bM=q0(),ec=Zu(),Z_=X_(),kM=7973,TM=Q0.getBCHDigit(kM);function pU(e,t,n){for(let s=1;s<=40;s++)if(t<=Hf.getCapacity(s,n,e))return s}i(pU,"getBestVersionForDataLength");function NM(e,t){return ec.getCharCountIndicator(e,t)+4}i(NM,"getReservedBitsCount");function hU(e,t){let n=0;return e.forEach(function(s){let l=NM(s.mode,t);n+=l+s.getBitsLength()}),n}i(hU,"getTotalBitsFromDataArray");function mU(e,t){for(let n=1;n<=40;n++)if(hU(e,n)<=Hf.getCapacity(n,t,ec.MIXED))return n}i(mU,"getBestVersionForMixedData");Hf.from=i(function(t,n){return Z_.isValid(t)?parseInt(t,10):n},"from");Hf.getCapacity=i(function(t,n,s){if(!Z_.isValid(t))throw new Error("Invalid QR Code version");typeof s>"u"&&(s=ec.BYTE);let l=Q0.getSymbolTotalCodewords(t),h=dU.getTotalCodewordsCount(t,n),d=(l-h)*8;if(s===ec.MIXED)return d;let v=d-NM(s,t);switch(s){case ec.NUMERIC:return Math.floor(v/10*3);case ec.ALPHANUMERIC:return Math.floor(v/11*2);case ec.KANJI:return Math.floor(v/13);case ec.BYTE:default:return Math.floor(v/8)}},"getCapacity");Hf.getBestVersionForData=i(function(t,n){let s,l=bM.from(n,bM.M);if(Array.isArray(t)){if(t.length>1)return mU(t,l);if(t.length===0)return 1;s=t[0]}else s=t;return pU(s.mode,s.getLength(),l)},"getBestVersionForData");Hf.getEncodedBits=i(function(t){if(!Z_.isValid(t)||t<7)throw new Error("Invalid QR Code version");let n=t<<12;for(;Q0.getBCHDigit(n)-TM>=0;)n^=kM<{var eE=Qu(),LM=1335,gU=21522,AM=eE.getBCHDigit(LM);MM.getEncodedBits=i(function(t,n){let s=t.bit<<3|n,l=s<<10;for(;eE.getBCHDigit(l)-AM>=0;)l^=LM<{var vU=Zu();function Pp(e){this.mode=vU.NUMERIC,this.data=e.toString()}i(Pp,"NumericData");Pp.getBitsLength=i(function(t){return 10*Math.floor(t/3)+(t%3?t%3*3+1:0)},"getBitsLength");Pp.prototype.getLength=i(function(){return this.data.length},"getLength");Pp.prototype.getBitsLength=i(function(){return Pp.getBitsLength(this.data.length)},"getBitsLength");Pp.prototype.write=i(function(t){let n,s,l;for(n=0;n+3<=this.data.length;n+=3)s=this.data.substr(n,3),l=parseInt(s,10),t.put(l,10);let h=this.data.length-n;h>0&&(s=this.data.substr(n),l=parseInt(s,10),t.put(l,h*3+1))},"write");RM.exports=Pp});var FM=rt((NX,IM)=>{var yU=Zu(),tE=["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"," ","$","%","*","+","-",".","/",":"];function Ap(e){this.mode=yU.ALPHANUMERIC,this.data=e}i(Ap,"AlphanumericData");Ap.getBitsLength=i(function(t){return 11*Math.floor(t/2)+6*(t%2)},"getBitsLength");Ap.prototype.getLength=i(function(){return this.data.length},"getLength");Ap.prototype.getBitsLength=i(function(){return Ap.getBitsLength(this.data.length)},"getBitsLength");Ap.prototype.write=i(function(t){let n;for(n=0;n+2<=this.data.length;n+=2){let s=tE.indexOf(this.data[n])*45;s+=tE.indexOf(this.data[n+1]),t.put(s,11)}this.data.length%2&&t.put(tE.indexOf(this.data[n]),6)},"write");IM.exports=Ap});var HM=rt((AX,BM)=>{"use strict";BM.exports=i(function(t){for(var n=[],s=t.length,l=0;l=55296&&h<=56319&&s>l+1){var d=t.charCodeAt(l+1);d>=56320&&d<=57343&&(h=(h-55296)*1024+d-56320+65536,l+=1)}if(h<128){n.push(h);continue}if(h<2048){n.push(h>>6|192),n.push(h&63|128);continue}if(h<55296||h>=57344&&h<65536){n.push(h>>12|224),n.push(h>>6&63|128),n.push(h&63|128);continue}if(h>=65536&&h<=1114111){n.push(h>>18|240),n.push(h>>12&63|128),n.push(h>>6&63|128),n.push(h&63|128);continue}n.push(239,191,189)}return new Uint8Array(n).buffer},"encodeUtf8")});var UM=rt((MX,zM)=>{var wU=HM(),SU=Zu();function Lp(e){this.mode=SU.BYTE,typeof e=="string"&&(e=wU(e)),this.data=new Uint8Array(e)}i(Lp,"ByteData");Lp.getBitsLength=i(function(t){return t*8},"getBitsLength");Lp.prototype.getLength=i(function(){return this.data.length},"getLength");Lp.prototype.getBitsLength=i(function(){return Lp.getBitsLength(this.data.length)},"getBitsLength");Lp.prototype.write=function(e){for(let t=0,n=this.data.length;t{var xU=Zu(),CU=Qu();function Mp(e){this.mode=xU.KANJI,this.data=e}i(Mp,"KanjiData");Mp.getBitsLength=i(function(t){return t*13},"getBitsLength");Mp.prototype.getLength=i(function(){return this.data.length},"getLength");Mp.prototype.getBitsLength=i(function(){return Mp.getBitsLength(this.data.length)},"getBitsLength");Mp.prototype.write=function(e){let t;for(t=0;t=33088&&n<=40956)n-=33088;else if(n>=57408&&n<=60351)n-=49472;else throw new Error("Invalid SJIS character: "+this.data[t]+` +Make sure your charset is UTF-8`);n=(n>>>8&255)*192+(n&255),e.put(n,13)}};WM.exports=Mp});var VM=rt((IX,rE)=>{"use strict";var sg={single_source_shortest_paths:i(function(e,t,n){var s={},l={};l[t]=0;var h=sg.PriorityQueue.make();h.push(t,0);for(var d,v,S,b,k,R,I,z,j;!h.empty();){d=h.pop(),v=d.value,b=d.cost,k=e[v]||{};for(S in k)k.hasOwnProperty(S)&&(R=k[S],I=b+R,z=l[S],j=typeof l[S]>"u",(j||z>I)&&(l[S]=I,h.push(S,I),s[S]=v))}if(typeof n<"u"&&typeof l[n]>"u"){var Y=["Could not find a path from ",t," to ",n,"."].join("");throw new Error(Y)}return s},"single_source_shortest_paths"),extract_shortest_path_from_predecessor_list:i(function(e,t){for(var n=[],s=t,l;s;)n.push(s),l=e[s],s=e[s];return n.reverse(),n},"extract_shortest_path_from_predecessor_list"),find_path:i(function(e,t,n){var s=sg.single_source_shortest_paths(e,t,n);return sg.extract_shortest_path_from_predecessor_list(s,n)},"find_path"),PriorityQueue:{make:i(function(e){var t=sg.PriorityQueue,n={},s;e=e||{};for(s in t)t.hasOwnProperty(s)&&(n[s]=t[s]);return n.queue=[],n.sorter=e.sorter||t.default_sorter,n},"make"),default_sorter:i(function(e,t){return e.cost-t.cost},"default_sorter"),push:i(function(e,t){var n={value:e,cost:t};this.queue.push(n),this.queue.sort(this.sorter)},"push"),pop:i(function(){return this.queue.shift()},"pop"),empty:i(function(){return this.queue.length===0},"empty")}};typeof rE<"u"&&(rE.exports=sg)});var JM=rt(Op=>{var Ut=Zu(),KM=DM(),GM=FM(),YM=UM(),XM=$M(),ag=Q_(),J0=Qu(),_U=VM();function qM(e){return unescape(encodeURIComponent(e)).length}i(qM,"getStringByteLength");function lg(e,t,n){let s=[],l;for(;(l=e.exec(n))!==null;)s.push({data:l[0],index:l.index,mode:t,length:l[0].length});return s}i(lg,"getSegments");function QM(e){let t=lg(ag.NUMERIC,Ut.NUMERIC,e),n=lg(ag.ALPHANUMERIC,Ut.ALPHANUMERIC,e),s,l;return J0.isKanjiModeEnabled()?(s=lg(ag.BYTE,Ut.BYTE,e),l=lg(ag.KANJI,Ut.KANJI,e)):(s=lg(ag.BYTE_KANJI,Ut.BYTE,e),l=[]),t.concat(n,s,l).sort(function(d,v){return d.index-v.index}).map(function(d){return{data:d.data,mode:d.mode,length:d.length}})}i(QM,"getSegmentsFromString");function nE(e,t){switch(t){case Ut.NUMERIC:return KM.getBitsLength(e);case Ut.ALPHANUMERIC:return GM.getBitsLength(e);case Ut.KANJI:return XM.getBitsLength(e);case Ut.BYTE:return YM.getBitsLength(e)}}i(nE,"getSegmentBitsLength");function EU(e){return e.reduce(function(t,n){let s=t.length-1>=0?t[t.length-1]:null;return s&&s.mode===n.mode?(t[t.length-1].data+=n.data,t):(t.push(n),t)},[])}i(EU,"mergeSegments");function bU(e){let t=[];for(let n=0;n{var ew=Qu(),iE=q0(),kU=cM(),NU=dM(),PU=pM(),AU=gM(),aE=vM(),lE=K_(),LU=CM(),Z0=PM(),MU=OM(),OU=Zu(),oE=JM();function RU(e,t){let n=e.size,s=AU.getPositions(t);for(let l=0;l=0&&v<=6&&(S===0||S===6)||S>=0&&S<=6&&(v===0||v===6)||v>=2&&v<=4&&S>=2&&S<=4?e.set(h+v,d+S,!0,!0):e.set(h+v,d+S,!1,!0))}}i(RU,"setupFinderPattern");function DU(e){let t=e.size;for(let n=8;n>v&1)===1,e.set(l,h,d,!0),e.set(h,l,d,!0)}i(FU,"setupVersionInfo");function sE(e,t,n){let s=e.size,l=MU.getEncodedBits(t,n),h,d;for(h=0;h<15;h++)d=(l>>h&1)===1,h<6?e.set(h,8,d,!0):h<8?e.set(h+1,8,d,!0):e.set(s-15+h,8,d,!0),h<8?e.set(8,s-h-1,d,!0):h<9?e.set(8,15-h-1+1,d,!0):e.set(8,15-h-1,d,!0);e.set(s-8,8,1,!0)}i(sE,"setupFormatInfo");function BU(e,t){let n=e.size,s=-1,l=n-1,h=7,d=0;for(let v=n-1;v>0;v-=2)for(v===6&&v--;;){for(let S=0;S<2;S++)if(!e.isReserved(l,v-S)){let b=!1;d>>h&1)===1),e.set(l,v-S,b),h--,h===-1&&(d++,h=7)}if(l+=s,l<0||n<=l){l-=s,s=-s;break}}}i(BU,"setupData");function HU(e,t,n){let s=new kU;n.forEach(function(S){s.put(S.mode.bit,4),s.put(S.getLength(),OU.getCharCountIndicator(S.mode,e)),S.write(s)});let l=ew.getSymbolTotalCodewords(e),h=lE.getTotalCodewordsCount(e,t),d=(l-h)*8;for(s.getLengthInBits()+4<=d&&s.put(0,4);s.getLengthInBits()%8!==0;)s.putBit(0);let v=(d-s.getLengthInBits())/8;for(let S=0;S=7&&kB(C,t),NB(C,c),isNaN(l)&&(l=TC.getBestMask(C,LC.bind(null,C,n))),TC.applyMask(l,C),LC(C,n,l),{modules:C,version:t,errorCorrectionLevel:n,maskPattern:l,segments:d}}o(OB,"createSymbol");MP.create=o(function(t,n){if(typeof t=="undefined"||t==="")throw new Error("No input text");let l=_C.M,d,h;return typeof n!="undefined"&&(l=_C.from(n.errorCorrectionLevel,_C.M),d=Ty.from(n.version),h=TC.from(n.maskPattern),n.toSJISFunc&&_y.setToSJISFunction(n.toSJISFunc)),OB(t,d,l,h)},"create")});var PC=Ue(Gf=>{function DP(e){if(typeof e=="number"&&(e=e.toString()),typeof e!="string")throw new Error("Color should be defined as hex string");let t=e.slice().replace("#","").split("");if(t.length<3||t.length===5||t.length>8)throw new Error("Invalid hex color: "+e);(t.length===3||t.length===4)&&(t=Array.prototype.concat.apply([],t.map(function(l){return[l,l]}))),t.length===6&&t.push("F","F");let n=parseInt(t.join(""),16);return{r:n>>24&255,g:n>>16&255,b:n>>8&255,a:n&255,hex:"#"+t.slice(0,6).join("")}}o(DP,"hex2rgba");Gf.getOptions=o(function(t){t||(t={}),t.color||(t.color={});let n=typeof t.margin=="undefined"||t.margin===null||t.margin<0?4:t.margin,l=t.width&&t.width>=21?t.width:void 0,d=t.scale||4;return{width:l,scale:l?4:d,margin:n,color:{dark:DP(t.color.dark||"#000000ff"),light:DP(t.color.light||"#ffffffff")},type:t.type,rendererOpts:t.rendererOpts||{}}},"getOptions");Gf.getScale=o(function(t,n){return n.width&&n.width>=t+n.margin*2?n.width/(t+n.margin*2):n.scale},"getScale");Gf.getImageWidth=o(function(t,n){let l=Gf.getScale(t,n);return Math.floor((t+n.margin*2)*l)},"getImageWidth");Gf.qrToImageData=o(function(t,n,l){let d=n.modules.size,h=n.modules.data,c=Gf.getScale(d,l),v=Math.floor((d+l.margin*2)*c),C=l.margin*c,k=[l.color.light,l.color.dark];for(let O=0;O=C&&j>=C&&O{var OC=PC();function MB(e,t,n){e.clearRect(0,0,t.width,t.height),t.style||(t.style={}),t.height=n,t.width=n,t.style.height=n+"px",t.style.width=n+"px"}o(MB,"clearCanvas");function AB(){try{return document.createElement("canvas")}catch(e){throw new Error("You need to specify a canvas element")}}o(AB,"getCanvasElement");ky.render=o(function(t,n,l){let d=l,h=n;typeof d=="undefined"&&(!n||!n.getContext)&&(d=n,n=void 0),n||(h=AB()),d=OC.getOptions(d);let c=OC.getImageWidth(t.modules.size,d),v=h.getContext("2d"),C=v.createImageData(c,c);return OC.qrToImageData(C.data,t,d),MB(v,h,c),v.putImageData(C,0,0),h},"render");ky.renderToDataURL=o(function(t,n,l){let d=l;typeof d=="undefined"&&(!n||!n.getContext)&&(d=n,n=void 0),d||(d={});let h=ky.render(t,n,d),c=d.type||"image/png",v=d.rendererOpts||{};return h.toDataURL(c,v.quality)},"renderToDataURL")});var BP=Ue(FP=>{var DB=PC();function IP(e,t){let n=e.a/255,l=t+'="'+e.hex+'"';return n<1?l+" "+t+'-opacity="'+n.toFixed(2).slice(1)+'"':l}o(IP,"getColorAttrib");function MC(e,t,n){let l=e+t;return typeof n!="undefined"&&(l+=" "+n),l}o(MC,"svgCmd");function RB(e,t,n){let l="",d=0,h=!1,c=0;for(let v=0;v0&&C>0&&e[v-1]||(l+=h?MC("M",C+n,.5+k+n):MC("m",d,0),d=0,h=!1),C+1':"",k="',O='viewBox="0 0 '+v+" "+v+'"',j=d.width?'width="'+d.width+'" height="'+d.width+'" ':"",B=''+C+k+` -`;return typeof l=="function"&&l(null,B),B},"render")});var WP=Ue(Mm=>{var IB=WL(),AC=AP(),HP=RP(),FB=BP();function DC(e,t,n,l,d){let h=[].slice.call(arguments,1),c=h.length,v=typeof h[c-1]=="function";if(!v&&!IB())throw new Error("Callback required as last argument");if(v){if(c<2)throw new Error("Too few arguments provided");c===2?(d=n,n=t,t=l=void 0):c===3&&(t.getContext&&typeof d=="undefined"?(d=l,l=void 0):(d=l,l=n,n=t,t=void 0))}else{if(c<1)throw new Error("Too few arguments provided");return c===1?(n=t,t=l=void 0):c===2&&!t.getContext&&(l=n,n=t,t=void 0),new Promise(function(C,k){try{let O=AC.create(n,l);C(e(O,t,l))}catch(O){k(O)}})}try{let C=AC.create(n,l);d(null,e(C,t,l))}catch(C){d(C)}}o(DC,"renderCanvas");Mm.create=AC.create;Mm.toCanvas=DC.bind(null,HP.render);Mm.toDataURL=DC.bind(null,HP.renderToDataURL);Mm.toString=DC.bind(null,function(e,t,n){return FB.render(e,n)})});var nb=fe(Oe()),aO=fe(iu());var Gh=fe(Oe()),k4=fe(ak());var uk=fe(Oe()),ei=uk.default.createContext(null);function EI(e){e()}o(EI,"defaultNoopBatch");var fk=EI,ck=o(function(t){return fk=t},"setBatch"),pk=o(function(){return fk},"getBatch");var dk={notify:o(function(){},"notify")};function _I(){var e=pk(),t=null,n=null;return{clear:o(function(){t=null,n=null},"clear"),notify:o(function(){e(function(){for(var d=t;d;)d.callback(),d=d.next})},"notify"),get:o(function(){for(var d=[],h=t;h;)d.push(h),h=h.next;return d},"get"),subscribe:o(function(d){var h=!0,c=n={callback:d,next:null,prev:n};return c.prev?c.prev.next=c:t=c,o(function(){!h||t===null||(h=!1,c.next?c.next.prev=c.prev:n=c.prev,c.prev?c.prev.next=c.next:t=c.next)},"unsubscribe")},"subscribe")}}o(_I,"createListenerCollection");var Tp=function(){function e(n,l){this.store=n,this.parentSub=l,this.unsubscribe=null,this.listeners=dk,this.handleChangeWrapper=this.handleChangeWrapper.bind(this)}o(e,"Subscription");var t=e.prototype;return t.addNestedSub=o(function(l){return this.trySubscribe(),this.listeners.subscribe(l)},"addNestedSub"),t.notifyNestedSubs=o(function(){this.listeners.notify()},"notifyNestedSubs"),t.handleChangeWrapper=o(function(){this.onStateChange&&this.onStateChange()},"handleChangeWrapper"),t.isSubscribed=o(function(){return Boolean(this.unsubscribe)},"isSubscribed"),t.trySubscribe=o(function(){this.unsubscribe||(this.unsubscribe=this.parentSub?this.parentSub.addNestedSub(this.handleChangeWrapper):this.store.subscribe(this.handleChangeWrapper),this.listeners=_I())},"trySubscribe"),t.tryUnsubscribe=o(function(){this.unsubscribe&&(this.unsubscribe(),this.unsubscribe=null,this.listeners.clear(),this.listeners=dk)},"tryUnsubscribe"),e}();var Gv=fe(Oe()),Nf=typeof window!="undefined"&&typeof window.document!="undefined"&&typeof window.document.createElement!="undefined"?Gv.useLayoutEffect:Gv.useEffect;function TI(e){var t=e.store,n=e.context,l=e.children,d=(0,Gh.useMemo)(function(){var v=new Tp(t);return v.onStateChange=v.notifyNestedSubs,{store:t,subscription:v}},[t]),h=(0,Gh.useMemo)(function(){return t.getState()},[t]);Nf(function(){var v=d.subscription;return v.trySubscribe(),h!==t.getState()&&v.notifyNestedSubs(),function(){v.tryUnsubscribe(),v.onStateChange=null}},[d,h]);var c=n||ei;return Gh.default.createElement(c.Provider,{value:d},l)}o(TI,"Provider");var Ux=TI;function Po(){return Po=Object.assign||function(e){for(var t=1;t=0)&&(n[d]=e[d]);return n}o(ou,"_objectWithoutPropertiesLoose");var Xx=fe(Ek()),cr=fe(Oe()),Lk=fe(Nk());var jI=[],qI=[null,null];function VI(e,t){var n=e[1];return[t.payload,n+1]}o(VI,"storeStateUpdatesReducer");function Pk(e,t,n){Nf(function(){return e.apply(void 0,t)},n)}o(Pk,"useIsomorphicLayoutEffectWithArgs");function KI(e,t,n,l,d,h,c){e.current=l,t.current=d,n.current=!1,h.current&&(h.current=null,c())}o(KI,"captureWrapperProps");function GI(e,t,n,l,d,h,c,v,C,k){if(!!e){var O=!1,j=null,B=o(function(){if(!O){var Z=t.getState(),R,A;try{R=l(Z,d.current)}catch(I){A=I,j=I}A||(j=null),R===h.current?c.current||C():(h.current=R,v.current=R,c.current=!0,k({type:"STORE_UPDATED",payload:{error:A}}))}},"checkForUpdates");n.onStateChange=B,n.trySubscribe(),B();var X=o(function(){if(O=!0,n.tryUnsubscribe(),n.onStateChange=null,j)throw j},"unsubscribeWrapper");return X}}o(GI,"subscribeUpdates");var YI=o(function(){return[null,0]},"initStateUpdates");function m0(e,t){t===void 0&&(t={});var n=t,l=n.getDisplayName,d=l===void 0?function(ne){return"ConnectAdvanced("+ne+")"}:l,h=n.methodName,c=h===void 0?"connectAdvanced":h,v=n.renderCountProp,C=v===void 0?void 0:v,k=n.shouldHandleStateChanges,O=k===void 0?!0:k,j=n.storeKey,B=j===void 0?"store":j,X=n.withRef,J=X===void 0?!1:X,Z=n.forwardRef,R=Z===void 0?!1:Z,A=n.context,I=A===void 0?ei:A,G=ou(n,["getDisplayName","methodName","renderCountProp","shouldHandleStateChanges","storeKey","withRef","forwardRef","context"]);if(!1)var K;var se=I;return o(function(pe){var me=pe.displayName||pe.name||"Component",xe=d(me),Ve=Po({},G,{getDisplayName:d,methodName:c,renderCountProp:C,shouldHandleStateChanges:O,storeKey:B,displayName:xe,wrappedComponentName:me,WrappedComponent:pe}),tt=G.pure;function _e(Xe){return e(Xe.dispatch,Ve)}o(_e,"createChildSelector");var St=tt?cr.useMemo:function(Xe){return Xe()};function We(Xe){var nr=(0,cr.useMemo)(function(){var Cr=Xe.reactReduxForwardedRef,Ui=ou(Xe,["reactReduxForwardedRef"]);return[Xe.context,Cr,Ui]},[Xe]),ct=nr[0],Hr=nr[1],Qt=nr[2],_t=(0,cr.useMemo)(function(){return ct&&ct.Consumer&&(0,Lk.isContextConsumer)(cr.default.createElement(ct.Consumer,null))?ct:se},[ct,se]),Ct=(0,cr.useContext)(_t),ut=Boolean(Xe.store)&&Boolean(Xe.store.getState)&&Boolean(Xe.store.dispatch),Lr=Boolean(Ct)&&Boolean(Ct.store),zt=ut?Xe.store:Ct.store,$t=(0,cr.useMemo)(function(){return _e(zt)},[zt]),ie=(0,cr.useMemo)(function(){if(!O)return qI;var Cr=new Tp(zt,ut?null:Ct.subscription),Ui=Cr.notifyNestedSubs.bind(Cr);return[Cr,Ui]},[zt,ut,Ct]),rt=ie[0],Pr=ie[1],Gt=(0,cr.useMemo)(function(){return ut?Ct:Po({},Ct,{subscription:rt})},[ut,Ct,rt]),Yt=(0,cr.useReducer)(VI,jI,YI),Se=Yt[0],Or=Se[0],fn=Yt[1];if(Or&&Or.error)throw Or.error;var Un=(0,cr.useRef)(),si=(0,cr.useRef)(Qt),cn=(0,cr.useRef)(),Zt=(0,cr.useRef)(!1),gr=St(function(){return cn.current&&Qt===si.current?cn.current:$t(zt.getState(),Qt)},[zt,Or,Qt]);Pk(KI,[si,Un,Zt,Qt,gr,cn,Pr]),Pk(GI,[O,zt,rt,$t,si,Un,Zt,cn,Pr,fn],[zt,rt,$t]);var pt=(0,cr.useMemo)(function(){return cr.default.createElement(pe,Po({},gr,{ref:Hr}))},[Hr,pe,gr]),Ho=(0,cr.useMemo)(function(){return O?cr.default.createElement(_t.Provider,{value:Gt},pt):pt},[_t,pt,Gt]);return Ho}o(We,"ConnectFunction");var Ke=tt?cr.default.memo(We):We;if(Ke.WrappedComponent=pe,Ke.displayName=We.displayName=xe,R){var Ge=cr.default.forwardRef(o(function(nr,ct){return cr.default.createElement(Ke,Po({},nr,{reactReduxForwardedRef:ct}))},"forwardConnectRef"));return Ge.displayName=xe,Ge.WrappedComponent=pe,(0,Xx.default)(Ge,pe)}return(0,Xx.default)(Ke,pe)},"wrapWithConnect")}o(m0,"connectAdvanced");function Ok(e,t){return e===t?e!==0||t!==0||1/e==1/t:e!==e&&t!==t}o(Ok,"is");function kp(e,t){if(Ok(e,t))return!0;if(typeof e!="object"||e===null||typeof t!="object"||t===null)return!1;var n=Object.keys(e),l=Object.keys(t);if(n.length!==l.length)return!1;for(var d=0;d=0;l--){var d=t[l](e);if(d)return d}return function(h,c){throw new Error("Invalid value of type "+typeof e+" for "+n+" argument when connecting component "+c.wrappedComponentName+".")}}o(Jx,"match");function aF(e,t){return e===t}o(aF,"strictEqual");function uF(e){var t=e===void 0?{}:e,n=t.connectHOC,l=n===void 0?m0:n,d=t.mapStateToPropsFactories,h=d===void 0?Dk:d,c=t.mapDispatchToPropsFactories,v=c===void 0?Ak:c,C=t.mergePropsFactories,k=C===void 0?Rk:C,O=t.selectorFactory,j=O===void 0?Zx:O;return o(function(X,J,Z,R){R===void 0&&(R={});var A=R,I=A.pure,G=I===void 0?!0:I,K=A.areStatesEqual,se=K===void 0?aF:K,ne=A.areOwnPropsEqual,pe=ne===void 0?kp:ne,me=A.areStatePropsEqual,xe=me===void 0?kp:me,Ve=A.areMergedPropsEqual,tt=Ve===void 0?kp:Ve,_e=ou(A,["pure","areStatesEqual","areOwnPropsEqual","areStatePropsEqual","areMergedPropsEqual"]),St=Jx(X,h,"mapStateToProps"),We=Jx(J,v,"mapDispatchToProps"),Ke=Jx(Z,k,"mergeProps");return l(j,Po({methodName:"connect",getDisplayName:o(function(Xe){return"Connect("+Xe+")"},"getDisplayName"),shouldHandleStateChanges:Boolean(X),initMapStateToProps:St,initMapDispatchToProps:We,initMergeProps:Ke,pure:G,areStatesEqual:se,areOwnPropsEqual:pe,areStatePropsEqual:xe,areMergedPropsEqual:tt},_e))},"connect")}o(uF,"createConnect");var Hi=uF();var Fk=fe(Oe());var Ik=fe(Oe());function v0(){var e=(0,Ik.useContext)(ei);return e}o(v0,"useReduxContext");function y0(e){e===void 0&&(e=ei);var t=e===ei?v0:function(){return(0,Fk.useContext)(e)};return o(function(){var l=t(),d=l.store;return d},"useStore")}o(y0,"createStoreHook");var eS=y0();function Bk(e){e===void 0&&(e=ei);var t=e===ei?eS:y0(e);return o(function(){var l=t();return l.dispatch},"useDispatch")}o(Bk,"createDispatchHook");var Gs=Bk();var ro=fe(Oe());var fF=o(function(t,n){return t===n},"refEquality");function cF(e,t,n,l){var d=(0,ro.useReducer)(function(J){return J+1},0),h=d[1],c=(0,ro.useMemo)(function(){return new Tp(n,l)},[n,l]),v=(0,ro.useRef)(),C=(0,ro.useRef)(),k=(0,ro.useRef)(),O=(0,ro.useRef)(),j=n.getState(),B;try{if(e!==C.current||j!==k.current||v.current){var X=e(j);O.current===void 0||!t(X,O.current)?B=X:B=O.current}else B=O.current}catch(J){throw v.current&&(J.message+=` +`);let d=HU(t,n,l),v=ew.getSymbolSize(t),S=new NU(v);return RU(S,t),DU(S),IU(S,t),sE(S,n,0),t>=7&&FU(S,t),BU(S,d),isNaN(s)&&(s=aE.getBestMask(S,sE.bind(null,S,n))),aE.applyMask(s,S),sE(S,n,s),{modules:S,version:t,errorCorrectionLevel:n,maskPattern:s,segments:l}}i(UU,"createSymbol");ZM.create=i(function(t,n){if(typeof t>"u"||t==="")throw new Error("No input text");let s=iE.M,l,h;return typeof n<"u"&&(s=iE.from(n.errorCorrectionLevel,iE.M),l=Z0.from(n.version),h=aE.from(n.maskPattern),n.toSJISFunc&&ew.setToSJISFunction(n.toSJISFunc)),UU(t,l,s,h)},"create")});var uE=rt(zf=>{function tO(e){if(typeof e=="number"&&(e=e.toString()),typeof e!="string")throw new Error("Color should be defined as hex string");let t=e.slice().replace("#","").split("");if(t.length<3||t.length===5||t.length>8)throw new Error("Invalid hex color: "+e);(t.length===3||t.length===4)&&(t=Array.prototype.concat.apply([],t.map(function(s){return[s,s]}))),t.length===6&&t.push("F","F");let n=parseInt(t.join(""),16);return{r:n>>24&255,g:n>>16&255,b:n>>8&255,a:n&255,hex:"#"+t.slice(0,6).join("")}}i(tO,"hex2rgba");zf.getOptions=i(function(t){t||(t={}),t.color||(t.color={});let n=typeof t.margin>"u"||t.margin===null||t.margin<0?4:t.margin,s=t.width&&t.width>=21?t.width:void 0,l=t.scale||4;return{width:s,scale:s?4:l,margin:n,color:{dark:tO(t.color.dark||"#000000ff"),light:tO(t.color.light||"#ffffffff")},type:t.type,rendererOpts:t.rendererOpts||{}}},"getOptions");zf.getScale=i(function(t,n){return n.width&&n.width>=t+n.margin*2?n.width/(t+n.margin*2):n.scale},"getScale");zf.getImageWidth=i(function(t,n){let s=zf.getScale(t,n);return Math.floor((t+n.margin*2)*s)},"getImageWidth");zf.qrToImageData=i(function(t,n,s){let l=n.modules.size,h=n.modules.data,d=zf.getScale(l,s),v=Math.floor((l+s.margin*2)*d),S=s.margin*d,b=[s.color.light,s.color.dark];for(let k=0;k=S&&R>=S&&k{var cE=uE();function WU(e,t,n){e.clearRect(0,0,t.width,t.height),t.style||(t.style={}),t.height=n,t.width=n,t.style.height=n+"px",t.style.width=n+"px"}i(WU,"clearCanvas");function $U(){try{return document.createElement("canvas")}catch{throw new Error("You need to specify a canvas element")}}i($U,"getCanvasElement");tw.render=i(function(t,n,s){let l=s,h=n;typeof l>"u"&&(!n||!n.getContext)&&(l=n,n=void 0),n||(h=$U()),l=cE.getOptions(l);let d=cE.getImageWidth(t.modules.size,l),v=h.getContext("2d"),S=v.createImageData(d,d);return cE.qrToImageData(S.data,t,l),WU(v,h,d),v.putImageData(S,0,0),h},"render");tw.renderToDataURL=i(function(t,n,s){let l=s;typeof l>"u"&&(!n||!n.getContext)&&(l=n,n=void 0),l||(l={});let h=tw.render(t,n,l),d=l.type||"image/png",v=l.rendererOpts||{};return h.toDataURL(d,v.quality)},"renderToDataURL")});var oO=rt(iO=>{var VU=uE();function nO(e,t){let n=e.a/255,s=t+'="'+e.hex+'"';return n<1?s+" "+t+'-opacity="'+n.toFixed(2).slice(1)+'"':s}i(nO,"getColorAttrib");function fE(e,t,n){let s=e+t;return typeof n<"u"&&(s+=" "+n),s}i(fE,"svgCmd");function qU(e,t,n){let s="",l=0,h=!1,d=0;for(let v=0;v0&&S>0&&e[v-1]||(s+=h?fE("M",S+n,.5+b+n):fE("m",l,0),l=0,h=!1),S+1':"",b="',k='viewBox="0 0 '+v+" "+v+'"',I=''+S+b+` +`;return typeof s=="function"&&s(null,I),I},"render")});var aO=rt(ug=>{var jU=aM(),dE=eO(),sO=rO(),KU=oO();function pE(e,t,n,s,l){let h=[].slice.call(arguments,1),d=h.length,v=typeof h[d-1]=="function";if(!v&&!jU())throw new Error("Callback required as last argument");if(v){if(d<2)throw new Error("Too few arguments provided");d===2?(l=n,n=t,t=s=void 0):d===3&&(t.getContext&&typeof l>"u"?(l=s,s=void 0):(l=s,s=n,n=t,t=void 0))}else{if(d<1)throw new Error("Too few arguments provided");return d===1?(n=t,t=s=void 0):d===2&&!t.getContext&&(s=n,n=t,t=void 0),new Promise(function(S,b){try{let k=dE.create(n,s);S(e(k,t,s))}catch(k){b(k)}})}try{let S=dE.create(n,s);l(null,e(S,t,s))}catch(S){l(S)}}i(pE,"renderCanvas");ug.create=dE.create;ug.toCanvas=pE.bind(null,sO.render);ug.toDataURL=pE.bind(null,sO.renderToDataURL);ug.toString=pE.bind(null,function(e,t,n){return KU.render(e,n)})});var yO=rt((kee,vO)=>{"use strict";var hW="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED";vO.exports=hW});var CO=rt((Nee,xO)=>{"use strict";var mW=yO();function wO(){}i(wO,"emptyFunction");function SO(){}i(SO,"emptyFunctionWithReset");SO.resetWarningCache=wO;xO.exports=function(){function e(s,l,h,d,v,S){if(S!==mW){var b=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw b.name="Invariant Violation",b}}i(e,"shim"),e.isRequired=e;function t(){return e}i(t,"getShim");var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:SO,resetWarningCache:wO};return n.PropTypes=n,n}});var EO=rt((Mee,_O)=>{_O.exports=CO()();var Aee,Lee});var qE=de(be()),OO=de(rP());var mP=de(be(),1),gP=de(sP(),1),Ry=de(be(),1);var Tt="default"in Ry?Ry.default:Ry,aP=Symbol.for("react-redux-context"),lP=typeof globalThis<"u"?globalThis:{};function l4(){if(!Tt.createContext)return{};let e=lP[aP]??(lP[aP]=new Map),t=e.get(Tt.createContext);return t||(t=Tt.createContext(null),e.set(Tt.createContext,t)),t}i(l4,"getContext");var gl=l4(),vP=i(()=>{throw new Error("uSES not initialized!")},"notInitialized");function Ex(e=gl){return i(function(){return Tt.useContext(e)},"useReduxContext2")}i(Ex,"createReduxContextHook");var yP=Ex(),wP=vP,u4=i(e=>{wP=e},"initializeUseSelector"),c4=i((e,t)=>e===t,"refEquality");function f4(e=gl){let t=e===gl?yP:Ex(e),n=i((s,l={})=>{let{equalityFn:h=c4,devModeChecks:d={}}=typeof l=="function"?{equalityFn:l}:l,{store:v,subscription:S,getServerState:b,stabilityCheck:k,identityFunctionCheck:R}=t(),I=Tt.useRef(!0),z=Tt.useCallback({[s.name](Y){let ne=s(Y);if(0){if((L==="always"||L==="once"&&I.current)&&!h(ne,D))try{}catch(te){}if((F==="always"||F==="once"&&I.current)&&ne===Y)try{}catch(q){}}return ne}}[s.name],[s,k,d.stabilityCheck]),j=wP(S.addNestedSub,v.getState,b||v.getState,z,h);return Tt.useDebugValue(j),j},"useSelector2");return Object.assign(n,{withTypes:i(()=>n,"withTypes")}),n}i(f4,"createSelectorHook");var SP=f4(),d4=Symbol.for("react.element"),p4=Symbol.for("react.portal"),h4=Symbol.for("react.fragment"),m4=Symbol.for("react.strict_mode"),g4=Symbol.for("react.profiler"),v4=Symbol.for("react.provider"),y4=Symbol.for("react.context"),w4=Symbol.for("react.server_context"),xP=Symbol.for("react.forward_ref"),S4=Symbol.for("react.suspense"),x4=Symbol.for("react.suspense_list"),bx=Symbol.for("react.memo"),C4=Symbol.for("react.lazy"),jW=Symbol.for("react.offscreen"),KW=Symbol.for("react.client.reference"),_4=xP,E4=bx;function b4(e){if(typeof e=="object"&&e!==null){let t=e.$$typeof;switch(t){case d4:{let n=e.type;switch(n){case h4:case g4:case m4:case S4:case x4:return n;default:{let s=n&&n.$$typeof;switch(s){case w4:case y4:case xP:case C4:case bx:case v4:return s;default:return t}}}}case p4:return t}}}i(b4,"typeOf");function T4(e){return b4(e)===bx}i(T4,"isMemo");function k4(e,t,n,s,{areStatesEqual:l,areOwnPropsEqual:h,areStatePropsEqual:d}){let v=!1,S,b,k,R,I;function z(L,D){return S=L,b=D,k=e(S,b),R=t(s,b),I=n(k,R,b),v=!0,I}i(z,"handleFirstCall");function j(){return k=e(S,b),t.dependsOnOwnProps&&(R=t(s,b)),I=n(k,R,b),I}i(j,"handleNewPropsAndNewState");function Y(){return e.dependsOnOwnProps&&(k=e(S,b)),t.dependsOnOwnProps&&(R=t(s,b)),I=n(k,R,b),I}i(Y,"handleNewProps");function ne(){let L=e(S,b),D=!d(L,k);return k=L,D&&(I=n(k,R,b)),I}i(ne,"handleNewState");function F(L,D){let q=!h(D,b),te=!l(L,S,D,b);return S=L,b=D,q&&te?j():q?Y():te?ne():I}return i(F,"handleSubsequentCalls"),i(function(D,q){return v?F(D,q):z(D,q)},"pureFinalPropsSelector")}i(k4,"pureFinalPropsSelectorFactory");function N4(e,{initMapStateToProps:t,initMapDispatchToProps:n,initMergeProps:s,...l}){let h=t(e,l),d=n(e,l),v=s(e,l);return k4(h,d,v,e,l)}i(N4,"finalPropsSelectorFactory");function P4(e,t){let n={};for(let s in e){let l=e[s];typeof l=="function"&&(n[s]=(...h)=>t(l(...h)))}return n}i(P4,"bindActionCreators");function Cx(e){return i(function(n){let s=e(n);function l(){return s}return i(l,"constantSelector"),l.dependsOnOwnProps=!1,l},"initConstantSelector")}i(Cx,"wrapMapToPropsConstant");function uP(e){return e.dependsOnOwnProps?!!e.dependsOnOwnProps:e.length!==1}i(uP,"getDependsOnOwnProps");function CP(e,t){return i(function(s,{displayName:l}){let h=i(function(v,S){return h.dependsOnOwnProps?h.mapToProps(v,S):h.mapToProps(v,void 0)},"mapToPropsProxy");return h.dependsOnOwnProps=!0,h.mapToProps=i(function(v,S){h.mapToProps=e,h.dependsOnOwnProps=uP(e);let b=h(v,S);return typeof b=="function"&&(h.mapToProps=b,h.dependsOnOwnProps=uP(b),b=h(v,S)),b},"detectFactoryAndVerify"),h},"initProxySelector")}i(CP,"wrapMapToPropsFunc");function Tx(e,t){return(n,s)=>{throw new Error(`Invalid value of type ${typeof e} for ${t} argument when connecting component ${s.wrappedComponentName}.`)}}i(Tx,"createInvalidArgFactory");function A4(e){return e&&typeof e=="object"?Cx(t=>P4(e,t)):e?typeof e=="function"?CP(e,"mapDispatchToProps"):Tx(e,"mapDispatchToProps"):Cx(t=>({dispatch:t}))}i(A4,"mapDispatchToPropsFactory");function L4(e){return e?typeof e=="function"?CP(e,"mapStateToProps"):Tx(e,"mapStateToProps"):Cx(()=>({}))}i(L4,"mapStateToPropsFactory");function M4(e,t,n){return{...n,...e,...t}}i(M4,"defaultMergeProps");function O4(e){return i(function(n,{displayName:s,areMergedPropsEqual:l}){let h=!1,d;return i(function(S,b,k){let R=e(S,b,k);return h?l(R,d)||(d=R):(h=!0,d=R),d},"mergePropsProxy")},"initMergePropsProxy")}i(O4,"wrapMergePropsFunc");function R4(e){return e?typeof e=="function"?O4(e):Tx(e,"mergeProps"):()=>M4}i(R4,"mergePropsFactory");function D4(e){e()}i(D4,"defaultNoopBatch");function I4(){let e=null,t=null;return{clear(){e=null,t=null},notify(){D4(()=>{let n=e;for(;n;)n.callback(),n=n.next})},get(){let n=[],s=e;for(;s;)n.push(s),s=s.next;return n},subscribe(n){let s=!0,l=t={callback:n,next:null,prev:t};return l.prev?l.prev.next=l:e=l,i(function(){!s||e===null||(s=!1,l.next?l.next.prev=l.prev:t=l.prev,l.prev?l.prev.next=l.next:e=l.next)},"unsubscribe")}}}i(I4,"createListenerCollection");var cP={notify(){},get:i(()=>[],"get")};function _P(e,t){let n,s=cP,l=0,h=!1;function d(Y){k();let ne=s.subscribe(Y),F=!1;return()=>{F||(F=!0,ne(),R())}}i(d,"addNestedSub");function v(){s.notify()}i(v,"notifyNestedSubs");function S(){j.onStateChange&&j.onStateChange()}i(S,"handleChangeWrapper");function b(){return h}i(b,"isSubscribed");function k(){l++,n||(n=t?t.addNestedSub(S):e.subscribe(S),s=I4())}i(k,"trySubscribe");function R(){l--,n&&l===0&&(n(),n=void 0,s.clear(),s=cP)}i(R,"tryUnsubscribe");function I(){h||(h=!0,k())}i(I,"trySubscribeSelf");function z(){h&&(h=!1,R())}i(z,"tryUnsubscribeSelf");let j={addNestedSub:d,notifyNestedSubs:v,handleChangeWrapper:S,isSubscribed:b,trySubscribe:I,tryUnsubscribe:z,getListeners:i(()=>s,"getListeners")};return j}i(_P,"createSubscription");var F4=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",B4=typeof navigator<"u"&&navigator.product==="ReactNative",Dy=F4||B4?Tt.useLayoutEffect:Tt.useEffect;function fP(e,t){return e===t?e!==0||t!==0||1/e===1/t:e!==e&&t!==t}i(fP,"is");function Mo(e,t){if(fP(e,t))return!0;if(typeof e!="object"||e===null||typeof t!="object"||t===null)return!1;let n=Object.keys(e),s=Object.keys(t);if(n.length!==s.length)return!1;for(let l=0;l{bP=e},"initializeConnect"),G4=[null,null];function Y4(e,t,n){Dy(()=>e(...t),n)}i(Y4,"useIsomorphicLayoutEffectWithArgs");function X4(e,t,n,s,l,h){e.current=s,n.current=!1,l.current&&(l.current=null,h())}i(X4,"captureWrapperProps");function Q4(e,t,n,s,l,h,d,v,S,b,k){if(!e)return()=>{};let R=!1,I=null,z=i(()=>{if(R||!v.current)return;let Y=t.getState(),ne,F;try{ne=s(Y,l.current)}catch(L){F=L,I=L}F||(I=null),ne===h.current?d.current||b():(h.current=ne,S.current=ne,d.current=!0,k())},"checkForUpdates");return n.onStateChange=z,n.trySubscribe(),z(),i(()=>{if(R=!0,n.tryUnsubscribe(),n.onStateChange=null,I)throw I},"unsubscribeWrapper")}i(Q4,"subscribeUpdates");function J4(e,t){return e===t}i(J4,"strictEqual");function Z4(e,t,n,{pure:s,areStatesEqual:l=J4,areOwnPropsEqual:h=Mo,areStatePropsEqual:d=Mo,areMergedPropsEqual:v=Mo,forwardRef:S=!1,context:b=gl}={}){let k=b,R=L4(e),I=A4(t),z=R4(n),j=!!e;return i(ne=>{let F=ne.displayName||ne.name||"Component",L=`Connect(${F})`,D={shouldHandleStateChanges:j,displayName:L,wrappedComponentName:F,WrappedComponent:ne,initMapStateToProps:R,initMapDispatchToProps:I,initMergeProps:z,areStatesEqual:l,areStatePropsEqual:d,areOwnPropsEqual:h,areMergedPropsEqual:v};function q(oe){let[he,Re,Ee]=Tt.useMemo(()=>{let{reactReduxForwardedRef:st,...Fe}=oe;return[oe.context,st,Fe]},[oe]),Ye=Tt.useMemo(()=>{let st=k;return he?.Consumer,st},[he,k]),tt=Tt.useContext(Ye),xe=!!oe.store&&!!oe.store.getState&&!!oe.store.dispatch,Xe=!!tt&&!!tt.store,je=xe?oe.store:tt.store,Qe=Xe?tt.getServerState:je.getState,ot=Tt.useMemo(()=>N4(je.dispatch,D),[je]),[It,Pt]=Tt.useMemo(()=>{if(!j)return G4;let st=_P(je,xe?void 0:tt.subscription),Fe=st.notifyNestedSubs.bind(st);return[st,Fe]},[je,xe,tt]),fn=Tt.useMemo(()=>xe?tt:{...tt,subscription:It},[xe,tt,It]),dn=Tt.useRef(void 0),gr=Tt.useRef(Ee),Wt=Tt.useRef(void 0),vr=Tt.useRef(!1),yr=Tt.useRef(!1),Gt=Tt.useRef(void 0);Dy(()=>(yr.current=!0,()=>{yr.current=!1}),[]);let Ft=Tt.useMemo(()=>i(()=>Wt.current&&Ee===gr.current?Wt.current:ot(je.getState(),Ee),"selector"),[je,Ee]),se=Tt.useMemo(()=>i(Fe=>It?Q4(j,je,It,ot,gr,dn,vr,yr,Wt,Pt,Fe):()=>{},"subscribe"),[It]);Y4(X4,[gr,dn,vr,Ee,Wt,Pt]);let Ue;try{Ue=bP(se,Ft,Qe?()=>ot(Qe(),Ee):Ft)}catch(st){throw Gt.current&&(st.message+=` The error may be correlated with this previous error: -`+v.current.stack+` +${Gt.current.stack} -`),J}return Nf(function(){C.current=e,k.current=j,O.current=B,v.current=void 0}),Nf(function(){function J(){try{var Z=n.getState(),R=C.current(Z);if(t(R,O.current))return;O.current=R,k.current=Z}catch(A){v.current=A}h()}return o(J,"checkForUpdates"),c.onStateChange=J,c.trySubscribe(),J(),function(){return c.tryUnsubscribe()}},[n,c]),B}o(cF,"useSelectorWithStoreAndSubscription");function Hk(e){e===void 0&&(e=ei);var t=e===ei?v0:function(){return(0,ro.useContext)(e)};return o(function(l,d){d===void 0&&(d=fF);var h=t(),c=h.store,v=h.subscription,C=cF(l,d,c,v);return(0,ro.useDebugValue)(C),C},"useSelector")}o(Hk,"createSelectorHook");var tS=Hk();var rS=fe(iu());ck(rS.unstable_batchedUpdates);var Wn=fe(Oe());var Wk="UI_FLOWVIEW_SET_TAB",Uk="SET_CONTENT_VIEW_FOR",pF={tab:"request",contentViewFor:{}};function nS(e=pF,t){switch(t.type){case Uk:return Pt(ke({},e),{contentViewFor:Pt(ke({},e.contentViewFor),{[t.messageId]:t.contentView})});case Wk:return Pt(ke({},e),{tab:t.tab?t.tab:"request"});default:return e}}o(nS,"reducer");function Lf(e){return{type:Wk,tab:e}}o(Lf,"selectTab");function w0(e,t){return{type:Uk,messageId:e,contentView:t}}o(w0,"setContentViewFor");var zk=fe(Qh()),dF=fe(Oe());window._=zk.default;window.React=dF;var x0=o(function(e){if(e===0)return"0";for(var t=["b","kb","mb","gb","tb"],n=0;ne);n++);var l;return e%Math.pow(1024,n)==0?l=0:l=1,(e/Math.pow(1024,n)).toFixed(l)+t[n]},"formatSize"),S0=o(function(e){for(var t=e,n=["ms","s","min","h"],l=[1e3,60,60],d=0;Math.abs(t)>=l[d]&&dkt(e,ke({method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)},n));kt.post=(e,t,n={})=>kt(e,ke({method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)},n));function Pf(e,...t){return Ia(this,null,function*(){return yield(yield kt(`/commands/${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({arguments:t})})).json()})}o(Pf,"runCommand");var Jh={};Hb(Jh,{ADD:()=>fS,RECEIVE:()=>dS,REMOVE:()=>pS,SET_FILTER:()=>aS,SET_SORT:()=>uS,UPDATE:()=>cS,add:()=>gF,defaultState:()=>C0,receive:()=>wF,reduce:()=>Lp,remove:()=>yF,setFilter:()=>hS,setSort:()=>jk,update:()=>vF});var lS=fe($k()),aS="LIST_SET_FILTER",uS="LIST_SET_SORT",fS="LIST_ADD",cS="LIST_UPDATE",pS="LIST_REMOVE",dS="LIST_RECEIVE",C0={byId:{},list:[],listIndex:{},view:[],viewIndex:{}};function Lp(e=C0,t){let{byId:n,list:l,listIndex:d,view:h,viewIndex:c}=e;switch(t.type){case aS:h=(0,lS.default)(l.filter(t.filter),t.sort),c={},h.forEach((k,O)=>{c[k.id]=O});break;case uS:h=(0,lS.default)([...h],t.sort),c={},h.forEach((k,O)=>{c[k.id]=O});break;case fS:if(t.item.id in n)break;n=Pt(ke({},n),{[t.item.id]:t.item}),d=Pt(ke({},d),{[t.item.id]:l.length}),l=[...l,t.item],t.filter(t.item)&&({view:h,viewIndex:c}=qk(e,t.item,t.sort));break;case cS:n=Pt(ke({},n),{[t.item.id]:t.item}),l=[...l],l[d[t.item.id]]=t.item;let v=t.item.id in c,C=t.filter(t.item);C&&!v?{view:h,viewIndex:c}=qk(e,t.item,t.sort):!C&&v?{data:h,dataIndex:c}=mS(h,c,t.item.id):C&&v&&({view:h,viewIndex:c}=xF(e,t.item,t.sort));break;case pS:if(!(t.id in n))break;n=ke({},n),delete n[t.id],{data:l,dataIndex:d}=mS(l,d,t.id),t.id in c&&({data:h,dataIndex:c}=mS(h,c,t.id));break;case dS:l=t.list,d={},n={},l.forEach((k,O)=>{n[k.id]=k,d[k.id]=O}),h=l.filter(t.filter).sort(t.sort),c={},h.forEach((k,O)=>{c[k.id]=O});break}return{byId:n,list:l,listIndex:d,view:h,viewIndex:c}}o(Lp,"reduce");function hS(e=b0,t=Zh){return{type:aS,filter:e,sort:t}}o(hS,"setFilter");function jk(e=Zh){return{type:uS,sort:e}}o(jk,"setSort");function gF(e,t=b0,n=Zh){return{type:fS,item:e,filter:t,sort:n}}o(gF,"add");function vF(e,t=b0,n=Zh){return{type:cS,item:e,filter:t,sort:n}}o(vF,"update");function yF(e){return{type:pS,id:e}}o(yF,"remove");function wF(e,t=b0,n=Zh){return{type:dS,list:e,filter:t,sort:n}}o(wF,"receive");function qk(e,t,n){let l=SF(e.view,t,n),d=[...e.view],h=ke({},e.viewIndex);d.splice(l,0,t);for(let c=d.length-1;c>=l;c--)h[d[c].id]=c;return{view:d,viewIndex:h}}o(qk,"sortedInsert");function mS(e,t,n){let l=t[n],d=[...e],h=ke({},t);delete h[n],d.splice(l,1);for(let c=d.length-1;c>=l;c--)h[d[c].id]=c;return{data:d,dataIndex:h}}o(mS,"removeData");function xF(e,t,n){let l=[...e.view],d=ke({},e.viewIndex),h=d[t.id];for(l[h]=t;h+10;)l[h]=l[h+1],l[h+1]=t,d[t.id]=h+1,d[l[h].id]=h,++h;for(;h>0&&n(l[h],l[h-1])<0;)l[h]=l[h-1],l[h-1]=t,d[t.id]=h-1,d[l[h].id]=h,--h;return{view:l,viewIndex:d}}o(xF,"sortedUpdate");function SF(e,t,n){let l=0,d=e.length;for(;l>>1;n(t,e[h])>=0?l=h+1:d=h}return l}o(SF,"sortedIndex");function b0(){return!0}o(b0,"defaultFilter");function Zh(e,t){return 0}o(Zh,"defaultSort");var Vk={http:80,https:443},Kr=class{static getContentType(t){var n=Kr.get_first_header(t,/^Content-Type$/i);if(n)return n.split(";")[0].trim()}static get_first_header(t,n){let l=t;l._headerLookups||Object.defineProperty(l,"_headerLookups",{value:{},configurable:!1,enumerable:!1,writable:!1});let d=n.toString();if(!(d in l._headerLookups)){let h;for(let c=0;c{var t,n;switch(e.type){case"http":let l=e.request.contentLength||0;return e.response&&(l+=e.response.contentLength||0),e.websocket&&(l+=e.websocket.messages_meta.contentLength||0),l;case"tcp":case"udp":return e.messages_meta.contentLength||0;case"dns":return(n=(t=e.response)==null?void 0:t.size)!=null?n:0}},"getTotalSize"),E0=o(e=>e.type==="http"&&!e.websocket,"canReplay");var Of=function(){"use strict";function e(l,d){function h(){this.constructor=l}o(h,"ctor"),h.prototype=d.prototype,l.prototype=new h}o(e,"peg$subclass");function t(l,d,h,c){this.message=l,this.expected=d,this.found=h,this.location=c,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,t)}o(t,"peg$SyntaxError"),e(t,Error);function n(l){var d=arguments.length>1?arguments[1]:{},h=this,c={},v={start:Ou},C=Ou,k={type:"other",description:"filter expression"},O=o(function(w){return w},"peg$c1"),j={type:"other",description:"whitespace"},B=/^[ \t\n\r]/,X={type:"class",value:"[ \\t\\n\\r]",description:"[ \\t\\n\\r]"},J={type:"other",description:"control character"},Z=/^[|&!()~"]/,R={type:"class",value:'[|&!()~"]',description:'[|&!()~"]'},A={type:"other",description:"optional whitespace"},I="|",G={type:"literal",value:"|",description:'"|"'},K=o(function(w,T){return Au(w,T)},"peg$c11"),se="&",ne={type:"literal",value:"&",description:'"&"'},pe=o(function(w,T){return hd(w,T)},"peg$c14"),me="!",xe={type:"literal",value:"!",description:'"!"'},Ve=o(function(w){return Vo(w)},"peg$c17"),tt="(",_e={type:"literal",value:"(",description:'"("'},St=")",We={type:"literal",value:")",description:'")"'},Ke=o(function(w){return yr(w)},"peg$c22"),Ge="~all",Xe={type:"literal",value:"~all",description:'"~all"'},nr=o(function(){return fc},"peg$c25"),ct="~a",Hr={type:"literal",value:"~a",description:'"~a"'},Qt=o(function(){return Ko},"peg$c28"),_t="~b",Ct={type:"literal",value:"~b",description:'"~b"'},ut=o(function(w){return na(w)},"peg$c31"),Lr="~bq",zt={type:"literal",value:"~bq",description:'"~bq"'},$t=o(function(w){return Go(w)},"peg$c34"),ie="~bs",rt={type:"literal",value:"~bs",description:'"~bs"'},Pr=o(function(w){return md(w)},"peg$c37"),Gt="~c",Yt={type:"literal",value:"~c",description:'"~c"'},Se=o(function(w){return ia(w)},"peg$c40"),Or="~comment",fn={type:"literal",value:"~comment",description:'"~comment"'},Un=o(function(w){return Ru(w)},"peg$c43"),si="~d",cn={type:"literal",value:"~d",description:'"~d"'},Zt=o(function(w){return Iu(w)},"peg$c46"),gr="~dns",pt={type:"literal",value:"~dns",description:'"~dns"'},Ho=o(function(){return oa},"peg$c49"),Cr="~dst",Ui={type:"literal",value:"~dst",description:'"~dst"'},pn=o(function(w){return Fu(w)},"peg$c52"),zn="~e",Si={type:"literal",value:"~e",description:'"~e"'},Ci=o(function(){return Bu},"peg$c55"),$n="~h",Mn={type:"literal",value:"~h",description:'"~h"'},Js=o(function(w){return Hu(w)},"peg$c58"),H="~hq",ee={type:"literal",value:"~hq",description:'"~hq"'},he=o(function(w){return Yo(w)},"peg$c61"),Te="~hs",ir={type:"literal",value:"~hs",description:'"~hs"'},Ul=o(function(w){return ji(w)},"peg$c64"),Ft="~http",Wr={type:"literal",value:"~http",description:'"~http"'},or=o(function(){return Ss},"peg$c67"),li="~marked",ds={type:"literal",value:"~marked",description:'"~marked"'},lo=o(function(){return zr},"peg$c70"),bi="~marker",el={type:"literal",value:"~marker",description:'"~marker"'},hs=o(function(w){return sa(w)},"peg$c73"),dn="~m",id={type:"literal",value:"~m",description:'"~m"'},tl=o(function(w){return mn(w)},"peg$c76"),Qf="~q",rl={type:"literal",value:"~q",description:'"~q"'},od=o(function(){return qi},"peg$c79"),Zf="~replayq",wu={type:"literal",value:"~replayq",description:'"~replayq"'},sd=o(function(){return al},"peg$c82"),zl="~replays",ms={type:"literal",value:"~replays",description:'"~replays"'},ld=o(function(){return cc},"peg$c85"),Jf="~replay",nl={type:"literal",value:"~replay",description:'"~replay"'},xu=o(function(){return Wu},"peg$c88"),Wo="~src",ad={type:"literal",value:"~src",description:'"~src"'},Uo=o(function(w){return gd(w)},"peg$c91"),$l="~s",ec={type:"literal",value:"~s",description:'"~s"'},jt=o(function(){return Uu},"peg$c94"),Me="~tcp",Ei={type:"literal",value:"~tcp",description:'"~tcp"'},Su=o(function(){return la},"peg$c97"),ai="~udp",vt={type:"literal",value:"~udp",description:'"~udp"'},ao=o(function(){return qn},"peg$c100"),zo="~tq",jl={type:"literal",value:"~tq",description:'"~tq"'},ue=o(function(w){return Ti(w)},"peg$c103"),ze="~ts",Cu={type:"literal",value:"~ts",description:'"~ts"'},bu=o(function(w){return pc(w)},"peg$c106"),gs="~t",il={type:"literal",value:"~t",description:'"~t"'},Eu=o(function(w){return aa(w)},"peg$c109"),He="~u",ud={type:"literal",value:"~u",description:'"~u"'},ql=o(function(w){return dc(w)},"peg$c112"),uo="~websocket",ui={type:"literal",value:"~websocket",description:'"~websocket"'},tc=o(function(){return Vi},"peg$c115"),_u={type:"other",description:"integer"},$o=/^['"]/,vs={type:"class",value:`['"]`,description:`['"]`},Tu=/^[0-9]/,ol={type:"class",value:"[0-9]",description:"[0-9]"},Vl=o(function(w){return parseInt(w.join(""),10)},"peg$c121"),Kl={type:"other",description:"string"},fo='"',Gl={type:"literal",value:'"',description:'"\\""'},Yl=o(function(w){return w.join("")},"peg$c125"),rc="'",Xl={type:"literal",value:"'",description:`"'"`},_i=/^["\\]/,nc={type:"class",value:'["\\\\]',description:'["\\\\]'},Ql={type:"any",description:"any character"},co=o(function(w){return w},"peg$c131"),ys="\\",ic={type:"literal",value:"\\",description:'"\\\\"'},oc=/^['\\]/,fd={type:"class",value:"['\\\\]",description:"['\\\\]"},cd=/^['"\\]/,ku={type:"class",value:`['"\\\\]`,description:`['"\\\\]`},sc="n",Nu={type:"literal",value:"n",description:'"n"'},lc=o(function(){return` -`},"peg$c140"),ac="r",Zl={type:"literal",value:"r",description:'"r"'},Jl=o(function(){return"\r"},"peg$c143"),Lu="t",Ot={type:"literal",value:"t",description:'"t"'},Nt=o(function(){return" "},"peg$c146"),P=0,Re=0,sl=[{line:1,column:1,seenCR:!1}],vr=0,Pu=[],ye=0,jo;if("startRule"in d){if(!(d.startRule in v))throw new Error(`Can't start parsing from rule "`+d.startRule+'".');C=v[d.startRule]}function pd(){return l.substring(Re,P)}o(pd,"text");function qt(){return zi(Re,P)}o(qt,"location");function ea(w){throw ta(null,[{type:"other",description:w}],l.substring(Re,P),zi(Re,P))}o(ea,"expected");function hn(w){throw ta(w,null,l.substring(Re,P),zi(Re,P))}o(hn,"error");function ws(w){var T=sl[w],W,U;if(T)return T;for(W=w-1;!sl[W];)W--;for(T=sl[W],T={line:T.line,column:T.column,seenCR:T.seenCR};Wvr&&(vr=P,Pu=[]),Pu.push(w))}o(Ce,"peg$fail");function ta(w,T,W,U){function wr(gn){var ci=1;for(gn.sort(function(ul,ki){return ul.descriptionki.description?1:0});ci1?ki.slice(0,-1).join(", ")+" or "+ki[gn.length-1]:ki[0],zu=ci?'"'+ul(ci)+'"':"end of input","Expected "+Vn+" but "+zu+" found."}return o(Kt,"buildMessage"),T!==null&&wr(T),new t(w!==null?w:Kt(T,W),T,W,U)}o(ta,"peg$buildException");function Ou(){var w,T,W,U;return ye++,w=P,T=fi(),T!==c?(W=ll(),W!==c?(U=fi(),U!==c?(Re=w,T=O(W),w=T):(P=w,w=c)):(P=w,w=c)):(P=w,w=c),ye--,w===c&&(T=c,ye===0&&Ce(k)),w}o(Ou,"peg$parsestart");function nt(){var w,T;return ye++,B.test(l.charAt(P))?(w=l.charAt(P),P++):(w=c,ye===0&&Ce(X)),ye--,w===c&&(T=c,ye===0&&Ce(j)),w}o(nt,"peg$parsews");function uc(){var w,T;return ye++,Z.test(l.charAt(P))?(w=l.charAt(P),P++):(w=c,ye===0&&Ce(R)),ye--,w===c&&(T=c,ye===0&&Ce(J)),w}o(uc,"peg$parsecc");function fi(){var w,T;for(ye++,w=[],T=nt();T!==c;)w.push(T),T=nt();return ye--,w===c&&(T=c,ye===0&&Ce(A)),w}o(fi,"peg$parse__");function ll(){var w,T,W,U,wr,Kt;return w=P,T=Ur(),T!==c?(W=fi(),W!==c?(l.charCodeAt(P)===124?(U=I,P++):(U=c,ye===0&&Ce(G)),U!==c?(wr=fi(),wr!==c?(Kt=ll(),Kt!==c?(Re=w,T=K(T,Kt),w=T):(P=w,w=c)):(P=w,w=c)):(P=w,w=c)):(P=w,w=c)):(P=w,w=c),w===c&&(w=Ur()),w}o(ll,"peg$parseOrExpr");function Ur(){var w,T,W,U,wr,Kt;if(w=P,T=ra(),T!==c?(W=fi(),W!==c?(l.charCodeAt(P)===38?(U=se,P++):(U=c,ye===0&&Ce(ne)),U!==c?(wr=fi(),wr!==c?(Kt=Ur(),Kt!==c?(Re=w,T=pe(T,Kt),w=T):(P=w,w=c)):(P=w,w=c)):(P=w,w=c)):(P=w,w=c)):(P=w,w=c),w===c){if(w=P,T=ra(),T!==c){if(W=[],U=nt(),U!==c)for(;U!==c;)W.push(U),U=nt();else W=c;W!==c?(U=Ur(),U!==c?(Re=w,T=pe(T,U),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;w===c&&(w=ra())}return w}o(Ur,"peg$parseAndExpr");function ra(){var w,T,W,U;return w=P,l.charCodeAt(P)===33?(T=me,P++):(T=c,ye===0&&Ce(xe)),T!==c?(W=fi(),W!==c?(U=ra(),U!==c?(Re=w,T=Ve(U),w=T):(P=w,w=c)):(P=w,w=c)):(P=w,w=c),w===c&&(w=jn()),w}o(ra,"peg$parseNotExpr");function jn(){var w,T,W,U,wr,Kt;return w=P,l.charCodeAt(P)===40?(T=tt,P++):(T=c,ye===0&&Ce(_e)),T!==c?(W=fi(),W!==c?(U=ll(),U!==c?(wr=fi(),wr!==c?(l.charCodeAt(P)===41?(Kt=St,P++):(Kt=c,ye===0&&Ce(We)),Kt!==c?(Re=w,T=Ke(U),w=T):(P=w,w=c)):(P=w,w=c)):(P=w,w=c)):(P=w,w=c)):(P=w,w=c),w===c&&(w=dd()),w}o(jn,"peg$parseBindingExpr");function dd(){var w,T,W,U;if(w=P,l.substr(P,4)===Ge?(T=Ge,P+=4):(T=c,ye===0&&Ce(Xe)),T!==c&&(Re=w,T=nr()),w=T,w===c&&(w=P,l.substr(P,2)===ct?(T=ct,P+=2):(T=c,ye===0&&Ce(Hr)),T!==c&&(Re=w,T=Qt()),w=T,w===c)){if(w=P,l.substr(P,2)===_t?(T=_t,P+=2):(T=c,ye===0&&Ce(Ct)),T!==c){if(W=[],U=nt(),U!==c)for(;U!==c;)W.push(U),U=nt();else W=c;W!==c?(U=Vt(),U!==c?(Re=w,T=ut(U),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;if(w===c){if(w=P,l.substr(P,3)===Lr?(T=Lr,P+=3):(T=c,ye===0&&Ce(zt)),T!==c){if(W=[],U=nt(),U!==c)for(;U!==c;)W.push(U),U=nt();else W=c;W!==c?(U=Vt(),U!==c?(Re=w,T=$t(U),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;if(w===c){if(w=P,l.substr(P,3)===ie?(T=ie,P+=3):(T=c,ye===0&&Ce(rt)),T!==c){if(W=[],U=nt(),U!==c)for(;U!==c;)W.push(U),U=nt();else W=c;W!==c?(U=Vt(),U!==c?(Re=w,T=Pr(U),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;if(w===c){if(w=P,l.substr(P,2)===Gt?(T=Gt,P+=2):(T=c,ye===0&&Ce(Yt)),T!==c){if(W=[],U=nt(),U!==c)for(;U!==c;)W.push(U),U=nt();else W=c;W!==c?(U=Mu(),U!==c?(Re=w,T=Se(U),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;if(w===c){if(w=P,l.substr(P,8)===Or?(T=Or,P+=8):(T=c,ye===0&&Ce(fn)),T!==c){if(W=[],U=nt(),U!==c)for(;U!==c;)W.push(U),U=nt();else W=c;W!==c?(U=Vt(),U!==c?(Re=w,T=Un(U),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;if(w===c){if(w=P,l.substr(P,2)===si?(T=si,P+=2):(T=c,ye===0&&Ce(cn)),T!==c){if(W=[],U=nt(),U!==c)for(;U!==c;)W.push(U),U=nt();else W=c;W!==c?(U=Vt(),U!==c?(Re=w,T=Zt(U),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;if(w===c&&(w=P,l.substr(P,4)===gr?(T=gr,P+=4):(T=c,ye===0&&Ce(pt)),T!==c&&(Re=w,T=Ho()),w=T,w===c)){if(w=P,l.substr(P,4)===Cr?(T=Cr,P+=4):(T=c,ye===0&&Ce(Ui)),T!==c){if(W=[],U=nt(),U!==c)for(;U!==c;)W.push(U),U=nt();else W=c;W!==c?(U=Vt(),U!==c?(Re=w,T=pn(U),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;if(w===c&&(w=P,l.substr(P,2)===zn?(T=zn,P+=2):(T=c,ye===0&&Ce(Si)),T!==c&&(Re=w,T=Ci()),w=T,w===c)){if(w=P,l.substr(P,2)===$n?(T=$n,P+=2):(T=c,ye===0&&Ce(Mn)),T!==c){if(W=[],U=nt(),U!==c)for(;U!==c;)W.push(U),U=nt();else W=c;W!==c?(U=Vt(),U!==c?(Re=w,T=Js(U),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;if(w===c){if(w=P,l.substr(P,3)===H?(T=H,P+=3):(T=c,ye===0&&Ce(ee)),T!==c){if(W=[],U=nt(),U!==c)for(;U!==c;)W.push(U),U=nt();else W=c;W!==c?(U=Vt(),U!==c?(Re=w,T=he(U),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;if(w===c){if(w=P,l.substr(P,3)===Te?(T=Te,P+=3):(T=c,ye===0&&Ce(ir)),T!==c){if(W=[],U=nt(),U!==c)for(;U!==c;)W.push(U),U=nt();else W=c;W!==c?(U=Vt(),U!==c?(Re=w,T=Ul(U),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;if(w===c&&(w=P,l.substr(P,5)===Ft?(T=Ft,P+=5):(T=c,ye===0&&Ce(Wr)),T!==c&&(Re=w,T=or()),w=T,w===c&&(w=P,l.substr(P,7)===li?(T=li,P+=7):(T=c,ye===0&&Ce(ds)),T!==c&&(Re=w,T=lo()),w=T,w===c))){if(w=P,l.substr(P,7)===bi?(T=bi,P+=7):(T=c,ye===0&&Ce(el)),T!==c){if(W=[],U=nt(),U!==c)for(;U!==c;)W.push(U),U=nt();else W=c;W!==c?(U=Vt(),U!==c?(Re=w,T=hs(U),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;if(w===c){if(w=P,l.substr(P,2)===dn?(T=dn,P+=2):(T=c,ye===0&&Ce(id)),T!==c){if(W=[],U=nt(),U!==c)for(;U!==c;)W.push(U),U=nt();else W=c;W!==c?(U=Vt(),U!==c?(Re=w,T=tl(U),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;if(w===c&&(w=P,l.substr(P,2)===Qf?(T=Qf,P+=2):(T=c,ye===0&&Ce(rl)),T!==c&&(Re=w,T=od()),w=T,w===c&&(w=P,l.substr(P,8)===Zf?(T=Zf,P+=8):(T=c,ye===0&&Ce(wu)),T!==c&&(Re=w,T=sd()),w=T,w===c&&(w=P,l.substr(P,8)===zl?(T=zl,P+=8):(T=c,ye===0&&Ce(ms)),T!==c&&(Re=w,T=ld()),w=T,w===c&&(w=P,l.substr(P,7)===Jf?(T=Jf,P+=7):(T=c,ye===0&&Ce(nl)),T!==c&&(Re=w,T=xu()),w=T,w===c))))){if(w=P,l.substr(P,4)===Wo?(T=Wo,P+=4):(T=c,ye===0&&Ce(ad)),T!==c){if(W=[],U=nt(),U!==c)for(;U!==c;)W.push(U),U=nt();else W=c;W!==c?(U=Vt(),U!==c?(Re=w,T=Uo(U),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;if(w===c&&(w=P,l.substr(P,2)===$l?(T=$l,P+=2):(T=c,ye===0&&Ce(ec)),T!==c&&(Re=w,T=jt()),w=T,w===c&&(w=P,l.substr(P,4)===Me?(T=Me,P+=4):(T=c,ye===0&&Ce(Ei)),T!==c&&(Re=w,T=Su()),w=T,w===c&&(w=P,l.substr(P,4)===ai?(T=ai,P+=4):(T=c,ye===0&&Ce(vt)),T!==c&&(Re=w,T=ao()),w=T,w===c)))){if(w=P,l.substr(P,3)===zo?(T=zo,P+=3):(T=c,ye===0&&Ce(jl)),T!==c){if(W=[],U=nt(),U!==c)for(;U!==c;)W.push(U),U=nt();else W=c;W!==c?(U=Vt(),U!==c?(Re=w,T=ue(U),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;if(w===c){if(w=P,l.substr(P,3)===ze?(T=ze,P+=3):(T=c,ye===0&&Ce(Cu)),T!==c){if(W=[],U=nt(),U!==c)for(;U!==c;)W.push(U),U=nt();else W=c;W!==c?(U=Vt(),U!==c?(Re=w,T=bu(U),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;if(w===c){if(w=P,l.substr(P,2)===gs?(T=gs,P+=2):(T=c,ye===0&&Ce(il)),T!==c){if(W=[],U=nt(),U!==c)for(;U!==c;)W.push(U),U=nt();else W=c;W!==c?(U=Vt(),U!==c?(Re=w,T=Eu(U),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;if(w===c){if(w=P,l.substr(P,2)===He?(T=He,P+=2):(T=c,ye===0&&Ce(ud)),T!==c){if(W=[],U=nt(),U!==c)for(;U!==c;)W.push(U),U=nt();else W=c;W!==c?(U=Vt(),U!==c?(Re=w,T=ql(U),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;w===c&&(w=P,l.substr(P,10)===uo?(T=uo,P+=10):(T=c,ye===0&&Ce(ui)),T!==c&&(Re=w,T=tc()),w=T,w===c&&(w=P,T=Vt(),T!==c&&(Re=w,T=ql(T)),w=T))}}}}}}}}}}}}}}}}}return w}o(dd,"peg$parseExpr");function Mu(){var w,T,W,U;if(ye++,w=P,$o.test(l.charAt(P))?(T=l.charAt(P),P++):(T=c,ye===0&&Ce(vs)),T===c&&(T=null),T!==c){if(W=[],Tu.test(l.charAt(P))?(U=l.charAt(P),P++):(U=c,ye===0&&Ce(ol)),U!==c)for(;U!==c;)W.push(U),Tu.test(l.charAt(P))?(U=l.charAt(P),P++):(U=c,ye===0&&Ce(ol));else W=c;W!==c?($o.test(l.charAt(P))?(U=l.charAt(P),P++):(U=c,ye===0&&Ce(vs)),U===c&&(U=null),U!==c?(Re=w,T=Vl(W),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;return ye--,w===c&&(T=c,ye===0&&Ce(_u)),w}o(Mu,"peg$parseIntegerLiteral");function Vt(){var w,T,W,U;if(ye++,w=P,l.charCodeAt(P)===34?(T=fo,P++):(T=c,ye===0&&Ce(Gl)),T!==c){for(W=[],U=xs();U!==c;)W.push(U),U=xs();W!==c?(l.charCodeAt(P)===34?(U=fo,P++):(U=c,ye===0&&Ce(Gl)),U!==c?(Re=w,T=Yl(W),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;if(w===c){if(w=P,l.charCodeAt(P)===39?(T=rc,P++):(T=c,ye===0&&Ce(Xl)),T!==c){for(W=[],U=qo();U!==c;)W.push(U),U=qo();W!==c?(l.charCodeAt(P)===39?(U=rc,P++):(U=c,ye===0&&Ce(Xl)),U!==c?(Re=w,T=Yl(W),w=T):(P=w,w=c)):(P=w,w=c)}else P=w,w=c;if(w===c)if(w=P,T=P,ye++,W=uc(),ye--,W===c?T=void 0:(P=T,T=c),T!==c){if(W=[],U=yt(),U!==c)for(;U!==c;)W.push(U),U=yt();else W=c;W!==c?(Re=w,T=Yl(W),w=T):(P=w,w=c)}else P=w,w=c}return ye--,w===c&&(T=c,ye===0&&Ce(Kl)),w}o(Vt,"peg$parseStringLiteral");function xs(){var w,T,W;return w=P,T=P,ye++,_i.test(l.charAt(P))?(W=l.charAt(P),P++):(W=c,ye===0&&Ce(nc)),ye--,W===c?T=void 0:(P=T,T=c),T!==c?(l.length>P?(W=l.charAt(P),P++):(W=c,ye===0&&Ce(Ql)),W!==c?(Re=w,T=co(W),w=T):(P=w,w=c)):(P=w,w=c),w===c&&(w=P,l.charCodeAt(P)===92?(T=ys,P++):(T=c,ye===0&&Ce(ic)),T!==c?(W=$i(),W!==c?(Re=w,T=co(W),w=T):(P=w,w=c)):(P=w,w=c)),w}o(xs,"peg$parseDoubleStringChar");function qo(){var w,T,W;return w=P,T=P,ye++,oc.test(l.charAt(P))?(W=l.charAt(P),P++):(W=c,ye===0&&Ce(fd)),ye--,W===c?T=void 0:(P=T,T=c),T!==c?(l.length>P?(W=l.charAt(P),P++):(W=c,ye===0&&Ce(Ql)),W!==c?(Re=w,T=co(W),w=T):(P=w,w=c)):(P=w,w=c),w===c&&(w=P,l.charCodeAt(P)===92?(T=ys,P++):(T=c,ye===0&&Ce(ic)),T!==c?(W=$i(),W!==c?(Re=w,T=co(W),w=T):(P=w,w=c)):(P=w,w=c)),w}o(qo,"peg$parseSingleStringChar");function yt(){var w,T,W;return w=P,T=P,ye++,W=nt(),ye--,W===c?T=void 0:(P=T,T=c),T!==c?(l.length>P?(W=l.charAt(P),P++):(W=c,ye===0&&Ce(Ql)),W!==c?(Re=w,T=co(W),w=T):(P=w,w=c)):(P=w,w=c),w}o(yt,"peg$parseUnquotedStringChar");function $i(){var w,T;return cd.test(l.charAt(P))?(w=l.charAt(P),P++):(w=c,ye===0&&Ce(ku)),w===c&&(w=P,l.charCodeAt(P)===110?(T=sc,P++):(T=c,ye===0&&Ce(Nu)),T!==c&&(Re=w,T=lc()),w=T,w===c&&(w=P,l.charCodeAt(P)===114?(T=ac,P++):(T=c,ye===0&&Ce(Zl)),T!==c&&(Re=w,T=Jl()),w=T,w===c&&(w=P,l.charCodeAt(P)===116?(T=Lu,P++):(T=c,ye===0&&Ce(Ot)),T!==c&&(Re=w,T=Nt()),w=T))),w}o($i,"peg$parseEscapeSequence");function Au(w,T){function W(){return w.apply(this,arguments)||T.apply(this,arguments)}return o(W,"orFilter"),W.desc=w.desc+" or "+T.desc,W}o(Au,"or");function hd(w,T){function W(){return w.apply(this,arguments)&&T.apply(this,arguments)}return o(W,"andFilter"),W.desc=w.desc+" and "+T.desc,W}o(hd,"and");function Vo(w){function T(){return!w.apply(this,arguments)}return o(T,"notFilter"),T.desc="not "+w.desc,T}o(Vo,"not");function yr(w){function T(){return w.apply(this,arguments)}return o(T,"bindingFilter"),T.desc="("+w.desc+")",T}o(yr,"binding");function fc(w){return!0}o(fc,"allFilter"),fc.desc="all flows";var Du=[new RegExp("text/javascript"),new RegExp("application/x-javascript"),new RegExp("application/javascript"),new RegExp("text/css"),new RegExp("image/.*"),new RegExp("font/.*"),new RegExp("application/font.*")];function Ko(w){if(w.response){for(var T=Ys.getContentType(w.response),W=Du.length;W--;)if(Du[W].test(T))return!0}return!1}o(Ko,"assetFilter"),Ko.desc="is asset";function na(w){w=new RegExp(w,"i");function T(W){return!0}return o(T,"bodyFilter"),T.desc="body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10",T}o(na,"body");function Go(w){w=new RegExp(w,"i");function T(W){return!0}return o(T,"requestBodyFilter"),T.desc="body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10",T}o(Go,"requestBody");function md(w){w=new RegExp(w,"i");function T(W){return!0}return o(T,"responseBodyFilter"),T.desc="body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10",T}o(md,"responseBody");function ia(w){function T(W){return W.response&&W.response.status_code===w}return o(T,"responseCodeFilter"),T.desc="resp. code is "+w,T}o(ia,"responseCode");function Ru(w){w=new RegExp(w,"i");function T(W){return w.test(W.comment)}return o(T,"commentFilter"),T.desc="comment matches "+w,T}o(Ru,"comment");function Iu(w){w=new RegExp(w,"i");function T(W){return W.request&&(w.test(W.request.host)||w.test(W.request.pretty_host))}return o(T,"domainFilter"),T.desc="domain matches "+w,T}o(Iu,"domain");function oa(w){return w.type==="dns"}o(oa,"dnsFilter"),oa.desc="is a DNS Flow";function Fu(w){w=new RegExp(w,"i");function T(W){return!!W.server_conn.address&&w.test(W.server_conn.address[0]+":"+W.server_conn.address[1])}return o(T,"destinationFilter"),T.desc="destination address matches "+w,T}o(Fu,"destination");function Bu(w){return!!w.error}o(Bu,"errorFilter"),Bu.desc="has error";function Hu(w){w=new RegExp(w,"i");function T(W){return W.request&&Oo.match_header(W.request,w)||W.response&&Ys.match_header(W.response,w)}return o(T,"headerFilter"),T.desc="header matches "+w,T}o(Hu,"header");function Yo(w){w=new RegExp(w,"i");function T(W){return W.request&&Oo.match_header(W.request,w)}return o(T,"requestHeaderFilter"),T.desc="req. header matches "+w,T}o(Yo,"requestHeader");function ji(w){w=new RegExp(w,"i");function T(W){return W.response&&Ys.match_header(W.response,w)}return o(T,"responseHeaderFilter"),T.desc="resp. header matches "+w,T}o(ji,"responseHeader");function Ss(w){return w.type==="http"}o(Ss,"httpFilter"),Ss.desc="is an HTTP Flow";function zr(w){return w.marked}o(zr,"markedFilter"),zr.desc="is marked";function sa(w){w=new RegExp(w,"i");function T(W){return w.test(W.marked)}return o(T,"markerFilter"),T.desc="marker matches "+w,T}o(sa,"marker");function mn(w){w=new RegExp(w,"i");function T(W){return W.request&&w.test(W.request.method)}return o(T,"methodFilter"),T.desc="method matches "+w,T}o(mn,"method");function qi(w){return w.request&&!w.response}o(qi,"noResponseFilter"),qi.desc="has no response";function al(w){return w.is_replay==="request"}o(al,"clientReplayFilter"),al.desc="request has been replayed";function cc(w){return w.is_replay==="response"}o(cc,"serverReplayFilter"),cc.desc="response has been replayed";function Wu(w){return!!w.is_replay}o(Wu,"replayFilter"),Wu.desc="flow has been replayed";function gd(w){w=new RegExp(w,"i");function T(W){return!!W.client_conn.peername&&w.test(W.client_conn.peername[0]+":"+W.client_conn.peername[1])}return o(T,"sourceFilter"),T.desc="source address matches "+w,T}o(gd,"source");function Uu(w){return!!w.response}o(Uu,"responseFilter"),Uu.desc="has response";function la(w){return w.type==="tcp"}o(la,"tcpFilter"),la.desc="is a TCP Flow";function qn(w){return w.type==="udp"}o(qn,"udpFilter"),qn.desc="is a UDP Flow";function Ti(w){w=new RegExp(w,"i");function T(W){return W.request&&w.test(Oo.getContentType(W.request))}return o(T,"requestContentTypeFilter"),T.desc="req. content type matches "+w,T}o(Ti,"requestContentType");function pc(w){w=new RegExp(w,"i");function T(W){return W.response&&w.test(Ys.getContentType(W.response))}return o(T,"responseContentTypeFilter"),T.desc="resp. content type matches "+w,T}o(pc,"responseContentType");function aa(w){w=new RegExp(w,"i");function T(W){return W.request&&w.test(Oo.getContentType(W.request))||W.response&&w.test(Ys.getContentType(W.response))}return o(T,"contentTypeFilter"),T.desc="content type matches "+w,T}o(aa,"contentType");function dc(w){w=new RegExp(w,"i");function T(W){var U;if(W.type==="dns"){let wr=(U=W.request)==null?void 0:U.questions[0];return wr&&w.test(wr.name)}return W.request&&w.test(Oo.pretty_url(W.request))}return o(T,"urlFilter"),T.desc="url matches "+w,T}o(dc,"url");function Vi(w){return!!w.websocket}if(o(Vi,"websocketFilter"),Vi.desc="is a Websocket Flow",jo=C(),jo!==c&&P===l.length)return jo;throw jo!==c&&PxS,icon:()=>N0,method:()=>tm,path:()=>L0,quickactions:()=>Pp,size:()=>P0,status:()=>rm,time:()=>O0,timestamp:()=>M0,tls:()=>k0});var er=fe(Oe());var T0=fe(ti());var k0=o(({flow:e})=>er.default.createElement("td",{className:(0,T0.default)("col-tls",e.client_conn.tls_established?"col-tls-https":"col-tls-http")}),"tls");k0.headerName="";k0.sortKey=e=>e.type==="http"&&e.request.scheme;var N0=o(({flow:e})=>er.default.createElement("td",{className:"col-icon"},er.default.createElement("div",{className:(0,T0.default)("resource-icon",Kk(e))})),"icon");N0.headerName="";N0.sortKey=e=>Kk(e);var Kk=o(e=>{if(e.type!=="http")return`resource-icon-${e.type}`;if(e.websocket)return"resource-icon-websocket";if(!e.response)return"resource-icon-plain";var t=Ys.getContentType(e.response)||"";return e.response.status_code===304?"resource-icon-not-modified":300<=e.response.status_code&&e.response.status_code<400?"resource-icon-redirect":t.indexOf("image")>=0?"resource-icon-image":t.indexOf("javascript")>=0?"resource-icon-js":t.indexOf("css")>=0?"resource-icon-css":t.indexOf("html")>=0?"resource-icon-document":"resource-icon-plain"},"getIcon"),Gk=o(e=>{var t,n,l,d;switch(e.type){case"http":return Oo.pretty_url(e.request);case"tcp":case"udp":return`${e.client_conn.peername.join(":")} \u2194 ${(n=(t=e.server_conn)==null?void 0:t.address)==null?void 0:n.join(":")}`;case"dns":return`${e.request.questions.map(h=>`${h.name} ${h.type}`).join(", ")} = ${((d=(l=e.response)==null?void 0:l.answers.map(h=>h.data).join(", "))!=null?d:"...")||"?"}`}},"mainPath"),L0=o(({flow:e})=>{let t;return e.error&&(e.error.msg==="Connection killed."?t=er.default.createElement("i",{className:"fa fa-fw fa-times pull-right"}):t=er.default.createElement("i",{className:"fa fa-fw fa-exclamation pull-right"})),er.default.createElement("td",{className:"col-path"},e.is_replay==="request"&&er.default.createElement("i",{className:"fa fa-fw fa-repeat pull-right"}),e.intercepted&&er.default.createElement("i",{className:"fa fa-fw fa-pause pull-right"}),t,er.default.createElement("span",{className:"marker pull-right"},e.marked),Gk(e))},"path");L0.headerName="Path";L0.sortKey=e=>Gk(e);var tm=o(({flow:e})=>er.default.createElement("td",{className:"col-method"},tm.sortKey(e)),"method");tm.headerName="Method";tm.sortKey=e=>{switch(e.type){case"http":return e.websocket?e.client_conn.tls_established?"WSS":"WS":e.request.method;case"dns":return e.request.op_code;default:return e.type.toUpperCase()}};var rm=o(({flow:e})=>{let t="darkred";return e.type!=="http"&&e.type!="dns"||!e.response?er.default.createElement("td",{className:"col-status"}):(100<=e.response.status_code&&e.response.status_code<200?t="green":200<=e.response.status_code&&e.response.status_code<300?t="darkgreen":300<=e.response.status_code&&e.response.status_code<400?t="lightblue":(400<=e.response.status_code&&e.response.status_code<500||500<=e.response.status_code&&e.response.status_code<600)&&(t="red"),er.default.createElement("td",{className:"col-status",style:{color:t}},rm.sortKey(e)))},"status");rm.headerName="Status";rm.sortKey=e=>{var t,n;switch(e.type){case"http":return(t=e.response)==null?void 0:t.status_code;case"dns":return(n=e.response)==null?void 0:n.response_code;default:return}};var P0=o(({flow:e})=>er.default.createElement("td",{className:"col-size"},x0(wS(e))),"size");P0.headerName="Size";P0.sortKey=e=>wS(e);var O0=o(({flow:e})=>{let t=em(e),n=yS(e);return er.default.createElement("td",{className:"col-time"},t&&n?S0(1e3*(n-t)):"...")},"time");O0.headerName="Time";O0.sortKey=e=>{let t=em(e),n=yS(e);return t&&n&&n-t};var M0=o(({flow:e})=>{let t=em(e);return er.default.createElement("td",{className:"col-timestamp"},t?no(t):"...")},"timestamp");M0.headerName="Start time";M0.sortKey=e=>em(e);var Pp=o(({flow:e})=>{let t=Gs(),[n,l]=(0,er.useState)(!1),d=null;return e.intercepted?d=er.default.createElement("a",{href:"#",className:"quickaction",onClick:()=>t(Op(e))},er.default.createElement("i",{className:"fa fa-fw fa-play text-success"})):E0(e)&&(d=er.default.createElement("a",{href:"#",className:"quickaction",onClick:()=>t(Mp(e))},er.default.createElement("i",{className:"fa fa-fw fa-repeat text-primary"}))),er.default.createElement("td",{className:(0,T0.default)("col-quickactions",{hover:n}),onClick:()=>0},d?er.default.createElement("div",null,d):er.default.createElement(er.default.Fragment,null))},"quickactions");Pp.headerName="";Pp.sortKey=e=>0;var xS={icon:N0,method:tm,path:L0,quickactions:Pp,size:P0,status:rm,time:O0,timestamp:M0,tls:k0};var EF="FLOWS_ADD",_F="FLOWS_UPDATE",Yk="FLOWS_REMOVE",TF="FLOWS_RECEIVE",Xk="FLOWS_SELECT",Qk="FLOWS_SET_FILTER",Zk="FLOWS_SET_SORT",Jk="FLOWS_SET_HIGHLIGHT",kF=ke({highlight:void 0,filter:void 0,sort:{column:void 0,desc:!1},selected:[]},C0);function SS(e=kF,t){switch(t.type){case EF:case _F:case Yk:case TF:let n=Jh[t.cmd](t.data,eN(e.filter),CS(e.sort)),l=e.selected;if(t.type===Yk&&e.selected.includes(t.data)){if(e.selected.length>1)l=l.filter(d=>d!==t.data);else if(l=[],t.data in e.viewIndex&&e.view.length>1){let d=e.viewIndex[t.data],h;d===e.view.length-1?h=e.view[d-1]:h=e.view[d+1],l.push(h.id)}}return ke(Pt(ke({},e),{selected:l}),Lp(e,n));case Qk:return ke(Pt(ke({},e),{filter:t.filter}),Lp(e,hS(eN(t.filter),CS(e.sort))));case Jk:return Pt(ke({},e),{highlight:t.highlight});case Zk:return ke(Pt(ke({},e),{sort:t.sort}),Lp(e,jk(CS(t.sort))));case Xk:return Pt(ke({},e),{selected:t.flowIds});default:return e}}o(SS,"reducer");function eN(e){if(!!e)return Of.parse(e)}o(eN,"makeFilter");function CS({column:e,desc:t}){if(!e)return(l,d)=>0;let n=xS[e].sortKey;return(l,d)=>{let h=n(l),c=n(d);return h>c?t?-1:1:hkt(`/flows/${e.id}/resume`,{method:"POST"})}o(Op,"resume");function R0(){return e=>kt("/flows/resume",{method:"POST"})}o(R0,"resumeAll");function I0(e){return t=>kt(`/flows/${e.id}/kill`,{method:"POST"})}o(I0,"kill");function rN(){return e=>kt("/flows/kill",{method:"POST"})}o(rN,"killAll");function F0(e){return t=>kt(`/flows/${e.id}`,{method:"DELETE"})}o(F0,"remove");function B0(e){return t=>kt(`/flows/${e.id}/duplicate`,{method:"POST"})}o(B0,"duplicate");function Mp(e){return t=>kt(`/flows/${e.id}/replay`,{method:"POST"})}o(Mp,"replay");function H0(e){return t=>kt(`/flows/${e.id}/revert`,{method:"POST"})}o(H0,"revert");function Wi(e,t){return n=>kt.put(`/flows/${e.id}`,t)}o(Wi,"update");function nN(e,t,n){let l=new FormData;return t=new window.Blob([t],{type:"plain/text"}),l.append("file",t),d=>kt(`/flows/${e.id}/${n}/content.data`,{method:"POST",body:l})}o(nN,"uploadContent");function W0(){return e=>kt("/clear",{method:"POST"})}o(W0,"clear");function iN(e){let t=new FormData;return t.append("file",e),n=>kt("/flows/dump",{method:"POST",body:t})}o(iN,"upload");function Af(e){return{type:Xk,flowIds:e?[e]:[]}}o(Af,"select");var U0="UI_HIDE_MODAL",oN="UI_SET_ACTIVE_MODAL",NF={activeModal:void 0};function bS(e=NF,t){switch(t.type){case oN:return Pt(ke({},e),{activeModal:t.activeModal});case U0:return Pt(ke({},e),{activeModal:void 0});default:return e}}o(bS,"reducer");function sN(e){return{type:oN,activeModal:e}}o(sN,"setActiveModal");function z0(){return{type:U0}}o(z0,"hideModal");var ym=fe(Oe());var Ut=fe(Oe());var Dp=fe(Oe());var im=fe(Oe()),lN=fe(ti()),aN=(()=>{let e=document.createElement("div");return e.setAttribute("contenteditable","PLAINTEXT-ONLY"),e.contentEditable==="plaintext-only"?"plaintext-only":"true"})(),Ap=!1,Xs=class extends im.Component{constructor(){super(...arguments);this.input=im.default.createRef();this.isEditing=o(()=>{var t;return((t=this.input.current)==null?void 0:t.contentEditable)===aN},"isEditing");this.startEditing=o(()=>{if(!this.input.current)return console.error("unreachable");this.isEditing()||(this.suppress_events=!0,this.input.current.blur(),this.input.current.contentEditable=aN,window.requestAnimationFrame(()=>{var l,d;if(!this.input.current)return;this.input.current.focus(),this.suppress_events=!1;let t=document.createRange();t.selectNodeContents(this.input.current);let n=window.getSelection();n==null||n.removeAllRanges(),n==null||n.addRange(t),(d=(l=this.props).onEditStart)==null||d.call(l)}))},"startEditing");this.resetValue=o(()=>{var t,n;if(!this.input.current)return console.error("unreachable");this.input.current.textContent=this.props.content,(n=(t=this.props).onInput)==null||n.call(t,this.props.content)},"resetValue");this.finishEditing=o(()=>{if(!this.input.current)return console.error("unreachable");this.props.onEditDone(this.input.current.textContent||""),this.input.current.blur(),this.input.current.contentEditable="inherit"},"finishEditing");this.onPaste=o(t=>{t.preventDefault();let n=t.clipboardData.getData("text/plain");document.execCommand("insertHTML",!1,n)},"onPaste");this.suppress_events=!1;this.onMouseDown=o(t=>{Ap&&console.debug("onMouseDown",this.suppress_events),this.suppress_events=!0,window.addEventListener("mouseup",this.onMouseUp,{once:!0})},"onMouseDown");this.onMouseUp=o(t=>{var d;let n=t.target===this.input.current,l=!((d=window.getSelection())==null?void 0:d.toString());Ap&&console.warn("mouseUp",this.suppress_events,n,l),n&&l&&this.startEditing(),this.suppress_events=!1},"onMouseUp");this.onClick=o(t=>{Ap&&console.debug("onClick",this.suppress_events)},"onClick");this.onFocus=o(t=>{if(Ap&&console.debug("onFocus",this.props.content,this.suppress_events),!this.input.current)throw"unreachable";this.suppress_events||this.startEditing()},"onFocus");this.onInput=o(t=>{var n,l,d;(d=(l=this.props).onInput)==null||d.call(l,((n=this.input.current)==null?void 0:n.textContent)||"")},"onInput");this.onBlur=o(t=>{Ap&&console.debug("onBlur",this.props.content,this.suppress_events),!this.suppress_events&&this.finishEditing()},"onBlur");this.onKeyDown=o(t=>{var n,l;switch(Ap&&console.debug("keydown",t),t.stopPropagation(),t.key){case"Escape":t.preventDefault(),this.resetValue(),this.finishEditing();break;case"Enter":t.shiftKey||(t.preventDefault(),this.finishEditing());break;default:break}(l=(n=this.props).onKeyDown)==null||l.call(n,t)},"onKeyDown")}render(){let t=(0,lN.default)("inline-input",this.props.className);return im.default.createElement("span",{ref:this.input,tabIndex:0,className:t,placeholder:this.props.placeholder,onFocus:this.onFocus,onBlur:this.onBlur,onKeyDown:this.onKeyDown,onInput:this.onInput,onPaste:this.onPaste,onMouseDown:this.onMouseDown,onClick:this.onClick},this.props.content)}componentDidUpdate(t){var n,l;t.content!==this.props.content&&((l=(n=this.props).onInput)==null||l.call(n,this.props.content))}};o(Xs,"ValueEditor");var uN=fe(ti());function Df(e){let[t,n]=(0,Dp.useState)(e.isValid(e.content)),l=(0,Dp.useRef)(null),d=o(c=>{var v;e.isValid(c)?e.onEditDone(c):(v=l.current)==null||v.resetValue()},"onEditDone"),h=(0,uN.default)(e.className,t?"has-success":"has-warning");return Dp.default.createElement(Xs,Pt(ke({},e),{className:h,onInput:c=>n(e.isValid(c)),onEditDone:d,ref:l}))}o(Df,"ValidateEditor");function ES(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}o(ES,"_defineProperty");function fN(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);t&&(l=l.filter(function(d){return Object.getOwnPropertyDescriptor(e,d).enumerable})),n.push.apply(n,l)}return n}o(fN,"ownKeys");function $0(e){for(var t=1;tn[l.level])));case dN:case OF:return ke(ke({},e),Lp(e,Jh[t.cmd](t.data,l=>e.filters[l.level])));default:return e}}o(NS,"reduce");function gN(e){return{type:mN,filter:e}}o(gN,"toggleFilter");function Rp(){return{type:hN}}o(Rp,"toggleVisibility");function vN(e,t="web"){let n={id:Math.random().toString(),message:e,level:t};return{type:dN,cmd:"add",data:n}}o(vN,"add");var yN="UI_OPTION_UPDATE_START",wN="UI_OPTION_UPDATE_SUCCESS",xN="UI_OPTION_UPDATE_ERROR",AF={};function LS(e=AF,t){switch(t.type){case yN:return Pt(ke({},e),{[t.option]:{isUpdating:!0,value:t.value,error:!1}});case wN:return Pt(ke({},e),{[t.option]:void 0});case xN:let n=e[t.option].value;return typeof n=="boolean"&&(n=!n),Pt(ke({},e),{[t.option]:{value:n,isUpdating:!1,error:t.error}});case U0:return{};default:return e}}o(LS,"reducer");function SN(e,t){return{type:yN,option:e,value:t}}o(SN,"startUpdate");function CN(e){return{type:wN,option:e}}o(CN,"updateSuccess");function bN(e,t){return{type:xN,option:e,error:t}}o(bN,"updateError");var EN=q0({flow:nS,modal:bS,optionsEditor:LS});var ni;(function(h){h.INIT="CONNECTION_INIT",h.FETCHING="CONNECTION_FETCHING",h.ESTABLISHED="CONNECTION_ESTABLISHED",h.ERROR="CONNECTION_ERROR",h.OFFLINE="CONNECTION_OFFLINE"})(ni||(ni={}));var DF={state:ni.INIT,message:void 0};function PS(e=DF,t){switch(t.type){case ni.ESTABLISHED:case ni.FETCHING:case ni.ERROR:case ni.OFFLINE:return{state:t.type,message:t.message};default:return e}}o(PS,"reducer");function _N(){return{type:ni.FETCHING}}o(_N,"startFetching");function TN(){return{type:ni.ESTABLISHED}}o(TN,"connectionEstablished");function kN(e){return{type:ni.ERROR,message:e}}o(kN,"connectionError");var NN={add_upstream_certs_to_client_chain:!1,allow_hosts:[],anticache:!1,anticomp:!1,block_global:!0,block_list:[],block_private:!1,body_size_limit:void 0,cert_passphrase:void 0,certs:[],ciphers_client:void 0,ciphers_server:void 0,client_certs:void 0,client_replay:[],client_replay_concurrency:1,command_history:!0,confdir:"~/.mitmproxy",connect_addr:void 0,connection_strategy:"eager",console_focus_follow:!1,content_view_lines_cutoff:512,export_preserve_original_ip:!1,http2:!0,http2_ping_keepalive:58,ignore_hosts:[],intercept:void 0,intercept_active:!1,keep_host_header:!1,key_size:2048,listen_host:"",listen_port:void 0,map_local:[],map_remote:[],mode:["regular"],modify_body:[],modify_headers:[],normalize_outbound_headers:!0,onboarding:!0,onboarding_host:"mitm.it",onboarding_port:80,proxy_debug:!1,proxyauth:void 0,rawtcp:!0,rawudp:!0,readfile_filter:void 0,rfile:void 0,save_stream_file:void 0,save_stream_filter:void 0,scripts:[],server:!0,server_replay:[],server_replay_ignore_content:!1,server_replay_ignore_host:!1,server_replay_ignore_params:[],server_replay_ignore_payload_params:[],server_replay_ignore_port:!1,server_replay_kill_extra:!1,server_replay_nopop:!1,server_replay_refresh:!0,server_replay_use_headers:[],showhost:!1,ssl_insecure:!1,ssl_verify_upstream_trusted_ca:void 0,ssl_verify_upstream_trusted_confdir:void 0,stickyauth:void 0,stickycookie:void 0,stream_large_bodies:void 0,tcp_hosts:[],termlog_verbosity:"info",tls_version_client_max:"UNBOUNDED",tls_version_client_min:"TLS1_2",tls_version_server_max:"UNBOUNDED",tls_version_server_min:"TLS1_2",udp_hosts:[],upstream_auth:void 0,upstream_cert:!0,validate_inbound_headers:!0,view_filter:void 0,view_order:"time",view_order_reversed:!1,web_columns:["tls","icon","path","method","status","size","time"],web_debug:!1,web_host:"127.0.0.1",web_open_browser:!0,web_port:8081,web_static_viewer:"",websocket:!0};var OS="OPTIONS_RECEIVE",MS="OPTIONS_UPDATE";function AS(e=NN,t){switch(t.type){case OS:let n={};for(let[d,{value:h}]of Object.entries(t.data))n[d]=h;return n;case MS:let l=ke({},e);for(let[d,{value:h}]of Object.entries(t.data))l[d]=h;return l;default:return e}}o(AS,"reducer");function RF(e,t,n){return Ia(this,null,function*(){try{let l=yield kt.put("/options",{[e]:t});if(l.status===200)n(CN(e));else throw yield l.text()}catch(l){n(bN(e,l))}})}o(RF,"pureSendUpdate");var IF=RF;function Ip(e,t){return n=>{n(SN(e,t)),IF(e,t,n)}}o(Ip,"update");function LN(){return e=>kt("/options/save",{method:"POST"})}o(LN,"save");var PN="COMMANDBAR_TOGGLE_VISIBILITY",FF={visible:!1};function DS(e=FF,t){switch(t.type){case PN:return Pt(ke({},e),{visible:!e.visible});default:return e}}o(DS,"reducer");function V0(){return{type:PN}}o(V0,"toggleVisibility");function ON(e){return function(t){var n=t.dispatch,l=t.getState;return function(d){return function(h){return typeof h=="function"?h(n,l,e):d(h)}}}}o(ON,"createThunkMiddleware");var MN=ON();MN.withExtraArgument=ON;var AN=MN;var BF="STATE_RECEIVE",HF="STATE_UPDATE",WF={available:!1,version:"",contentViews:[],servers:[]};function RS(e=WF,t){switch(t.type){case BF:case HF:return ke(Pt(ke({},e),{available:!0}),t.data);default:return e}}o(RS,"reducer");var UF={},zF=o((e=UF,t)=>{switch(t.type){case OS:return t.data;case MS:return ke(ke({},e),t.data);default:return e}},"reducer"),DN=zF;var $F=window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__||kS,jF=q0({commandBar:DS,eventLog:NS,flows:SS,connection:PS,ui:EN,options:AS,options_meta:DN,backendState:RS}),qF=o(e=>TS(jF,e,$F(pN(AN))),"createAppStore"),Fp=qF(void 0),Xt=o(()=>Gs(),"useAppDispatch"),qe=tS;var io=fe(Oe());var RN=fe(Qh()),IN=fe(ti()),IS=class extends io.Component{constructor(){super(...arguments);this.container=io.default.createRef();this.nameInput=io.default.createRef();this.valueInput=io.default.createRef();this.render=o(()=>{let[t,n]=this.props.item;return io.default.createElement("div",{ref:this.container,className:"kv-row",onClick:this.onClick,onKeyDownCapture:this.onKeyDown},io.default.createElement(Xs,{ref:this.nameInput,className:"kv-key",content:t,onEditStart:this.props.onEditStart,onEditDone:l=>this.props.onEditDone([l,n])}),":\xA0",io.default.createElement(Xs,{ref:this.valueInput,className:"kv-value",content:n,onEditStart:this.props.onEditStart,onEditDone:l=>this.props.onEditDone([t,l]),placeholder:"empty"}))},"render");this.onClick=o(t=>{t.target===this.container.current&&this.props.onClickEmptyArea()},"onClick");this.onKeyDown=o(t=>{var n;t.target===((n=this.valueInput.current)==null?void 0:n.input.current)&&t.key==="Tab"&&this.props.onTabNext()},"onKeyDown")}};o(IS,"Row");var Bp=class extends io.Component{constructor(){super(...arguments);this.rowRefs={};this.state={currentList:this.props.data||[],initialList:this.props.data};this.render=o(()=>{this.rowRefs={};let t=this.state.currentList.map((n,l)=>io.default.createElement(IS,{key:l,item:n,onEditStart:()=>this.currentlyEditing=l,onEditDone:d=>this.onEditDone(l,d),onClickEmptyArea:()=>this.onClickEmptyArea(l),onTabNext:()=>this.onTabNext(l),ref:d=>this.rowRefs[l]=d}));return io.default.createElement("div",{className:(0,IN.default)("kv-editor",this.props.className),onMouseDown:this.onMouseDown},t,io.default.createElement("div",{onClick:n=>{n.preventDefault(),this.onClickEmptyArea(this.state.currentList.length-1)},className:"kv-add-row fa fa-plus-square-o",role:"button","aria-label":"Add"}))},"render");this.onEditDone=o((t,n)=>{let l=[...this.state.currentList];n[0]?l[t]=n:l.splice(t,1),this.currentlyEditing=void 0,(0,RN.isEqual)(this.state.currentList,l)||this.props.onChange(l),this.setState({currentList:l})},"onEditDone");this.onClickEmptyArea=o(t=>{if(this.justFinishedEditing)return;let n=[...this.state.currentList];n.splice(t+1,0,["",""]),this.setState({currentList:n},()=>{var l,d;return(d=(l=this.rowRefs[t+1])==null?void 0:l.nameInput.current)==null?void 0:d.startEditing()})},"onClickEmptyArea");this.onTabNext=o(t=>{t==this.state.currentList.length-1&&this.onClickEmptyArea(t)},"onTabNext");this.onMouseDown=o(t=>{this.justFinishedEditing=this.currentlyEditing},"onMouseDown")}static getDerivedStateFromProps(t,n){return t.data!==n.initialList?{currentList:t.data||[],initialList:t.data}:null}};o(Bp,"KeyValueListEditor");var tr=fe(Oe());var om=fe(Oe());function K0(e,t){let[n,l]=(0,om.useState)(),[d,h]=(0,om.useState)();return(0,om.useEffect)(()=>{d&&d.abort();let c=new AbortController;return kt(e,{signal:c.signal}).then(v=>{if(!v.ok)throw`${v.status} ${v.statusText}`.trim();return v.text()}).then(v=>{l(v)}).catch(v=>{c.signal.aborted||l(`Error getting content: ${v}.`)}),h(c),()=>{c.signal.aborted||c.abort()}},[e,t]),n}o(K0,"useContent");var sm=fe(Oe()),G0=sm.default.memo(o(function({icon:t,text:n,className:l,title:d,onOpenFile:h,onClick:c}){let v;return sm.default.createElement("a",{href:"#",onClick:C=>{v.click(),c&&c(C)},className:l,title:d},sm.default.createElement("i",{className:"fa fa-fw "+t}),n,sm.default.createElement("input",{ref:C=>v=C,className:"hidden",type:"file",onChange:C=>{C.preventDefault(),C.target.files&&C.target.files.length>0&&h(C.target.files[0]),v.value=""}}))},"FileChooser"));var Hp=fe(Oe()),FN=fe(ti());function kr({onClick:e,children:t,icon:n,disabled:l,className:d,title:h}){return Hp.createElement("button",{className:(0,FN.default)(d,"btn btn-default"),onClick:l?void 0:e,disabled:l,title:h},n&&Hp.createElement(Hp.Fragment,null,Hp.createElement("i",{className:"fa "+n}),"\xA0"),t)}o(kr,"Button");var am=fe(Oe()),$N=fe(Oe());var lm=fe(Oe()),HN=fe(ti()),WN=fe(BN()),UN=fe(Qh());function zN(e){return e&&e.replace(/\r\n|\r/g,` -`)}o(zN,"normalizeLineEndings");var Wp=class extends lm.Component{constructor(t){super(t);this.state={isFocused:!1}}getCodeMirrorInstance(){return this.props.codeMirrorInstance||WN.default}UNSAFE_componentWillMount(){this.props.path&&console.error("Warning: react-codemirror: the `path` prop has been changed to `name`")}componentDidMount(){let t=this.getCodeMirrorInstance();this.codeMirror=t.fromTextArea(this.textareaNode,this.props.options),this.codeMirror.on("change",this.codemirrorValueChanged.bind(this)),this.codeMirror.on("cursorActivity",this.cursorActivity.bind(this)),this.codeMirror.on("focus",this.focusChanged.bind(this,!0)),this.codeMirror.on("blur",this.focusChanged.bind(this,!1)),this.codeMirror.on("scroll",this.scrollChanged.bind(this)),this.codeMirror.setValue(this.props.defaultValue||this.props.value||"")}componentWillUnmount(){this.codeMirror&&this.codeMirror.toTextArea()}UNSAFE_componentWillReceiveProps(t){if(this.codeMirror&&t.value!==void 0&&t.value!==this.props.value&&zN(this.codeMirror.getValue())!==zN(t.value))if(this.props.preserveScrollPosition){var n=this.codeMirror.getScrollInfo();this.codeMirror.setValue(t.value),this.codeMirror.scrollTo(n.left,n.top)}else this.codeMirror.setValue(t.value);if(typeof t.options=="object")for(let l in t.options)t.options.hasOwnProperty(l)&&this.setOptionIfChanged(l,t.options[l])}setOptionIfChanged(t,n){let l=this.codeMirror.getOption(t);UN.default.isEqual(l,n)||this.codeMirror.setOption(t,n)}getCodeMirror(){return this.codeMirror}focus(){this.codeMirror&&this.codeMirror.focus()}focusChanged(t){this.setState({isFocused:t}),this.props.onFocusChange&&this.props.onFocusChange(t)}cursorActivity(t){this.props.onCursorActivity&&this.props.onCursorActivity(t)}scrollChanged(t){this.props.onScroll&&this.props.onScroll(t.getScrollInfo())}codemirrorValueChanged(t,n){this.props.onChange&&n.origin!=="setValue"&&this.props.onChange(t.getValue(),n)}render(){let t=(0,HN.default)("ReactCodeMirror",this.state.isFocused?"ReactCodeMirror--focused":null,this.props.className);return lm.createElement("div",{className:t},lm.createElement("textarea",{ref:n=>this.textareaNode=n,name:this.props.name||this.props.path,defaultValue:this.props.value,autoComplete:"off",autoFocus:this.props.autoFocus}))}};o(Wp,"CodeMirror"),Wp.defaultProps={preserveScrollPosition:!1};var um=class extends $N.Component{constructor(){super(...arguments);this.editor=am.createRef();this.getContent=o(()=>{var t;return(t=this.editor.current)==null?void 0:t.codeMirror.getValue()},"getContent");this.render=o(()=>{let t={lineNumbers:!0};return am.createElement("div",{className:"codeeditor",onKeyDown:n=>n.stopPropagation()},am.createElement(Wp,{ref:this.editor,value:this.props.initialContent,onChange:()=>0,options:t}))},"render")}};o(um,"CodeEditor");var Rf=fe(Oe()),VF=Rf.default.memo(o(function({lines:t,maxLines:n,showMore:l}){return t.length===0?null:Rf.default.createElement("pre",null,t.map((d,h)=>h===n?Rf.default.createElement("button",{key:"showmore",onClick:l,className:"btn btn-xs btn-info"},Rf.default.createElement("i",{className:"fa fa-angle-double-down","aria-hidden":"true"})," Show more"):Rf.default.createElement("div",{key:h},d.map(([c,v],C)=>Rf.default.createElement("span",{key:C,className:c},v)))))},"LineRenderer")),Y0=VF;var zf=fe(Oe());var xi=fe(Oe());var X0=fe(Oe());var HS=o(function(t){return t.reduce(function(n,l){var d=l[0],h=l[1];return n[d]=h,n},{})},"fromEntries"),WS=typeof window!="undefined"&&window.document&&window.document.createElement?X0.useLayoutEffect:X0.useEffect;var fu=fe(Oe());var Gr="top",Ln="bottom",ln="right",an="left",Q0="auto",su=[Gr,Ln,ln,an],Rl="start",Z0="end",jN="clippingParents",J0="viewport",Up="popper",qN="reference",US=su.reduce(function(e,t){return e.concat([t+"-"+Rl,t+"-"+Z0])},[]),ey=[].concat(su,[Q0]).reduce(function(e,t){return e.concat([t,t+"-"+Rl,t+"-"+Z0])},[]),KF="beforeRead",GF="read",YF="afterRead",XF="beforeMain",QF="main",ZF="afterMain",JF="beforeWrite",e3="write",t3="afterWrite",VN=[KF,GF,YF,XF,QF,ZF,JF,e3,t3];function Pn(e){return e?(e.nodeName||"").toLowerCase():null}o(Pn,"getNodeName");function Br(e){if(e==null)return window;if(e.toString()!=="[object Window]"){var t=e.ownerDocument;return t&&t.defaultView||window}return e}o(Br,"getWindow");function Il(e){var t=Br(e).Element;return e instanceof t||e instanceof Element}o(Il,"isElement");function Yr(e){var t=Br(e).HTMLElement;return e instanceof t||e instanceof HTMLElement}o(Yr,"isHTMLElement");function ty(e){if(typeof ShadowRoot=="undefined")return!1;var t=Br(e).ShadowRoot;return e instanceof t||e instanceof ShadowRoot}o(ty,"isShadowRoot");function r3(e){var t=e.state;Object.keys(t.elements).forEach(function(n){var l=t.styles[n]||{},d=t.attributes[n]||{},h=t.elements[n];!Yr(h)||!Pn(h)||(Object.assign(h.style,l),Object.keys(d).forEach(function(c){var v=d[c];v===!1?h.removeAttribute(c):h.setAttribute(c,v===!0?"":v)}))})}o(r3,"applyStyles");function n3(e){var t=e.state,n={popper:{position:t.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(t.elements.popper.style,n.popper),t.styles=n,t.elements.arrow&&Object.assign(t.elements.arrow.style,n.arrow),function(){Object.keys(t.elements).forEach(function(l){var d=t.elements[l],h=t.attributes[l]||{},c=Object.keys(t.styles.hasOwnProperty(l)?t.styles[l]:n[l]),v=c.reduce(function(C,k){return C[k]="",C},{});!Yr(d)||!Pn(d)||(Object.assign(d.style,v),Object.keys(h).forEach(function(C){d.removeAttribute(C)}))})}}o(n3,"effect");var KN={name:"applyStyles",enabled:!0,phase:"write",fn:r3,effect:n3,requires:["computeStyles"]};function On(e){return e.split("-")[0]}o(On,"getBasePlacement");var lu=Math.round;function Mo(e,t){t===void 0&&(t=!1);var n=e.getBoundingClientRect(),l=1,d=1;return Yr(e)&&t&&(l=n.width/e.offsetWidth||1,d=n.height/e.offsetHeight||1),{width:lu(n.width/l),height:lu(n.height/d),top:lu(n.top/d),right:lu(n.right/l),bottom:lu(n.bottom/d),left:lu(n.left/l),x:lu(n.left/l),y:lu(n.top/d)}}o(Mo,"getBoundingClientRect");function If(e){var t=Mo(e),n=e.offsetWidth,l=e.offsetHeight;return Math.abs(t.width-n)<=1&&(n=t.width),Math.abs(t.height-l)<=1&&(l=t.height),{x:e.offsetLeft,y:e.offsetTop,width:n,height:l}}o(If,"getLayoutRect");function fm(e,t){var n=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(n&&ty(n)){var l=t;do{if(l&&e.isSameNode(l))return!0;l=l.parentNode||l.host}while(l)}return!1}o(fm,"contains");function wi(e){return Br(e).getComputedStyle(e)}o(wi,"getComputedStyle");function zS(e){return["table","td","th"].indexOf(Pn(e))>=0}o(zS,"isTableElement");function Bn(e){return((Il(e)?e.ownerDocument:e.document)||window.document).documentElement}o(Bn,"getDocumentElement");function Fl(e){return Pn(e)==="html"?e:e.assignedSlot||e.parentNode||(ty(e)?e.host:null)||Bn(e)}o(Fl,"getParentNode");function GN(e){return!Yr(e)||wi(e).position==="fixed"?null:e.offsetParent}o(GN,"getTrueOffsetParent");function i3(e){var t=navigator.userAgent.toLowerCase().indexOf("firefox")!==-1,n=navigator.userAgent.indexOf("Trident")!==-1;if(n&&Yr(e)){var l=wi(e);if(l.position==="fixed")return null}for(var d=Fl(e);Yr(d)&&["html","body"].indexOf(Pn(d))<0;){var h=wi(d);if(h.transform!=="none"||h.perspective!=="none"||h.contain==="paint"||["transform","perspective"].indexOf(h.willChange)!==-1||t&&h.willChange==="filter"||t&&h.filter&&h.filter!=="none")return d;d=d.parentNode}return null}o(i3,"getContainingBlock");function as(e){for(var t=Br(e),n=GN(e);n&&zS(n)&&wi(n).position==="static";)n=GN(n);return n&&(Pn(n)==="html"||Pn(n)==="body"&&wi(n).position==="static")?t:n||i3(e)||t}o(as,"getOffsetParent");function Ff(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}o(Ff,"getMainAxisFromPlacement");var Ao=Math.max,au=Math.min,cm=Math.round;function Bf(e,t,n){return Ao(e,au(t,n))}o(Bf,"within");function pm(){return{top:0,right:0,bottom:0,left:0}}o(pm,"getFreshSideObject");function dm(e){return Object.assign({},pm(),e)}o(dm,"mergePaddingObject");function hm(e,t){return t.reduce(function(n,l){return n[l]=e,n},{})}o(hm,"expandToHashMap");var o3=o(function(t,n){return t=typeof t=="function"?t(Object.assign({},n.rects,{placement:n.placement})):t,dm(typeof t!="number"?t:hm(t,su))},"toPaddingObject");function s3(e){var t,n=e.state,l=e.name,d=e.options,h=n.elements.arrow,c=n.modifiersData.popperOffsets,v=On(n.placement),C=Ff(v),k=[an,ln].indexOf(v)>=0,O=k?"height":"width";if(!(!h||!c)){var j=o3(d.padding,n),B=If(h),X=C==="y"?Gr:an,J=C==="y"?Ln:ln,Z=n.rects.reference[O]+n.rects.reference[C]-c[C]-n.rects.popper[O],R=c[C]-n.rects.reference[C],A=as(h),I=A?C==="y"?A.clientHeight||0:A.clientWidth||0:0,G=Z/2-R/2,K=j[X],se=I-B[O]-j[J],ne=I/2-B[O]/2+G,pe=Bf(K,ne,se),me=C;n.modifiersData[l]=(t={},t[me]=pe,t.centerOffset=pe-ne,t)}}o(s3,"arrow");function l3(e){var t=e.state,n=e.options,l=n.element,d=l===void 0?"[data-popper-arrow]":l;d!=null&&(typeof d=="string"&&(d=t.elements.popper.querySelector(d),!d)||!fm(t.elements.popper,d)||(t.elements.arrow=d))}o(l3,"effect");var YN={name:"arrow",enabled:!0,phase:"main",fn:s3,effect:l3,requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};var a3={top:"auto",right:"auto",bottom:"auto",left:"auto"};function u3(e){var t=e.x,n=e.y,l=window,d=l.devicePixelRatio||1;return{x:cm(cm(t*d)/d)||0,y:cm(cm(n*d)/d)||0}}o(u3,"roundOffsetsByDPR");function XN(e){var t,n=e.popper,l=e.popperRect,d=e.placement,h=e.offsets,c=e.position,v=e.gpuAcceleration,C=e.adaptive,k=e.roundOffsets,O=k===!0?u3(h):typeof k=="function"?k(h):h,j=O.x,B=j===void 0?0:j,X=O.y,J=X===void 0?0:X,Z=h.hasOwnProperty("x"),R=h.hasOwnProperty("y"),A=an,I=Gr,G=window;if(C){var K=as(n),se="clientHeight",ne="clientWidth";K===Br(n)&&(K=Bn(n),wi(K).position!=="static"&&(se="scrollHeight",ne="scrollWidth")),K=K,d===Gr&&(I=Ln,J-=K[se]-l.height,J*=v?1:-1),d===an&&(A=ln,B-=K[ne]-l.width,B*=v?1:-1)}var pe=Object.assign({position:c},C&&a3);if(v){var me;return Object.assign({},pe,(me={},me[I]=R?"0":"",me[A]=Z?"0":"",me.transform=(G.devicePixelRatio||1)<2?"translate("+B+"px, "+J+"px)":"translate3d("+B+"px, "+J+"px, 0)",me))}return Object.assign({},pe,(t={},t[I]=R?J+"px":"",t[A]=Z?B+"px":"",t.transform="",t))}o(XN,"mapToStyles");function f3(e){var t=e.state,n=e.options,l=n.gpuAcceleration,d=l===void 0?!0:l,h=n.adaptive,c=h===void 0?!0:h,v=n.roundOffsets,C=v===void 0?!0:v;if(!1)var k;var O={placement:On(t.placement),popper:t.elements.popper,popperRect:t.rects.popper,gpuAcceleration:d};t.modifiersData.popperOffsets!=null&&(t.styles.popper=Object.assign({},t.styles.popper,XN(Object.assign({},O,{offsets:t.modifiersData.popperOffsets,position:t.options.strategy,adaptive:c,roundOffsets:C})))),t.modifiersData.arrow!=null&&(t.styles.arrow=Object.assign({},t.styles.arrow,XN(Object.assign({},O,{offsets:t.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:C})))),t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-placement":t.placement})}o(f3,"computeStyles");var QN={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:f3,data:{}};var ry={passive:!0};function c3(e){var t=e.state,n=e.instance,l=e.options,d=l.scroll,h=d===void 0?!0:d,c=l.resize,v=c===void 0?!0:c,C=Br(t.elements.popper),k=[].concat(t.scrollParents.reference,t.scrollParents.popper);return h&&k.forEach(function(O){O.addEventListener("scroll",n.update,ry)}),v&&C.addEventListener("resize",n.update,ry),function(){h&&k.forEach(function(O){O.removeEventListener("scroll",n.update,ry)}),v&&C.removeEventListener("resize",n.update,ry)}}o(c3,"effect");var ZN={name:"eventListeners",enabled:!0,phase:"write",fn:o(function(){},"fn"),effect:c3,data:{}};var p3={left:"right",right:"left",bottom:"top",top:"bottom"};function zp(e){return e.replace(/left|right|bottom|top/g,function(t){return p3[t]})}o(zp,"getOppositePlacement");var d3={start:"end",end:"start"};function ny(e){return e.replace(/start|end/g,function(t){return d3[t]})}o(ny,"getOppositeVariationPlacement");function Hf(e){var t=Br(e),n=t.pageXOffset,l=t.pageYOffset;return{scrollLeft:n,scrollTop:l}}o(Hf,"getWindowScroll");function Wf(e){return Mo(Bn(e)).left+Hf(e).scrollLeft}o(Wf,"getWindowScrollBarX");function $S(e){var t=Br(e),n=Bn(e),l=t.visualViewport,d=n.clientWidth,h=n.clientHeight,c=0,v=0;return l&&(d=l.width,h=l.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(c=l.offsetLeft,v=l.offsetTop)),{width:d,height:h,x:c+Wf(e),y:v}}o($S,"getViewportRect");function jS(e){var t,n=Bn(e),l=Hf(e),d=(t=e.ownerDocument)==null?void 0:t.body,h=Ao(n.scrollWidth,n.clientWidth,d?d.scrollWidth:0,d?d.clientWidth:0),c=Ao(n.scrollHeight,n.clientHeight,d?d.scrollHeight:0,d?d.clientHeight:0),v=-l.scrollLeft+Wf(e),C=-l.scrollTop;return wi(d||n).direction==="rtl"&&(v+=Ao(n.clientWidth,d?d.clientWidth:0)-h),{width:h,height:c,x:v,y:C}}o(jS,"getDocumentRect");function Uf(e){var t=wi(e),n=t.overflow,l=t.overflowX,d=t.overflowY;return/auto|scroll|overlay|hidden/.test(n+d+l)}o(Uf,"isScrollParent");function iy(e){return["html","body","#document"].indexOf(Pn(e))>=0?e.ownerDocument.body:Yr(e)&&Uf(e)?e:iy(Fl(e))}o(iy,"getScrollParent");function uu(e,t){var n;t===void 0&&(t=[]);var l=iy(e),d=l===((n=e.ownerDocument)==null?void 0:n.body),h=Br(l),c=d?[h].concat(h.visualViewport||[],Uf(l)?l:[]):l,v=t.concat(c);return d?v:v.concat(uu(Fl(c)))}o(uu,"listScrollParents");function $p(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}o($p,"rectToClientRect");function h3(e){var t=Mo(e);return t.top=t.top+e.clientTop,t.left=t.left+e.clientLeft,t.bottom=t.top+e.clientHeight,t.right=t.left+e.clientWidth,t.width=e.clientWidth,t.height=e.clientHeight,t.x=t.left,t.y=t.top,t}o(h3,"getInnerBoundingClientRect");function JN(e,t){return t===J0?$p($S(e)):Yr(t)?h3(t):$p(jS(Bn(e)))}o(JN,"getClientRectFromMixedType");function m3(e){var t=uu(Fl(e)),n=["absolute","fixed"].indexOf(wi(e).position)>=0,l=n&&Yr(e)?as(e):e;return Il(l)?t.filter(function(d){return Il(d)&&fm(d,l)&&Pn(d)!=="body"}):[]}o(m3,"getClippingParents");function qS(e,t,n){var l=t==="clippingParents"?m3(e):[].concat(t),d=[].concat(l,[n]),h=d[0],c=d.reduce(function(v,C){var k=JN(e,C);return v.top=Ao(k.top,v.top),v.right=au(k.right,v.right),v.bottom=au(k.bottom,v.bottom),v.left=Ao(k.left,v.left),v},JN(e,h));return c.width=c.right-c.left,c.height=c.bottom-c.top,c.x=c.left,c.y=c.top,c}o(qS,"getClippingRect");function Qs(e){return e.split("-")[1]}o(Qs,"getVariation");function mm(e){var t=e.reference,n=e.element,l=e.placement,d=l?On(l):null,h=l?Qs(l):null,c=t.x+t.width/2-n.width/2,v=t.y+t.height/2-n.height/2,C;switch(d){case Gr:C={x:c,y:t.y-n.height};break;case Ln:C={x:c,y:t.y+t.height};break;case ln:C={x:t.x+t.width,y:v};break;case an:C={x:t.x-n.width,y:v};break;default:C={x:t.x,y:t.y}}var k=d?Ff(d):null;if(k!=null){var O=k==="y"?"height":"width";switch(h){case Rl:C[k]=C[k]-(t[O]/2-n[O]/2);break;case Z0:C[k]=C[k]+(t[O]/2-n[O]/2);break;default:}}return C}o(mm,"computeOffsets");function us(e,t){t===void 0&&(t={});var n=t,l=n.placement,d=l===void 0?e.placement:l,h=n.boundary,c=h===void 0?jN:h,v=n.rootBoundary,C=v===void 0?J0:v,k=n.elementContext,O=k===void 0?Up:k,j=n.altBoundary,B=j===void 0?!1:j,X=n.padding,J=X===void 0?0:X,Z=dm(typeof J!="number"?J:hm(J,su)),R=O===Up?qN:Up,A=e.elements.reference,I=e.rects.popper,G=e.elements[B?R:O],K=qS(Il(G)?G:G.contextElement||Bn(e.elements.popper),c,C),se=Mo(A),ne=mm({reference:se,element:I,strategy:"absolute",placement:d}),pe=$p(Object.assign({},I,ne)),me=O===Up?pe:se,xe={top:K.top-me.top+Z.top,bottom:me.bottom-K.bottom+Z.bottom,left:K.left-me.left+Z.left,right:me.right-K.right+Z.right},Ve=e.modifiersData.offset;if(O===Up&&Ve){var tt=Ve[d];Object.keys(xe).forEach(function(_e){var St=[ln,Ln].indexOf(_e)>=0?1:-1,We=[Gr,Ln].indexOf(_e)>=0?"y":"x";xe[_e]+=tt[We]*St})}return xe}o(us,"detectOverflow");function VS(e,t){t===void 0&&(t={});var n=t,l=n.placement,d=n.boundary,h=n.rootBoundary,c=n.padding,v=n.flipVariations,C=n.allowedAutoPlacements,k=C===void 0?ey:C,O=Qs(l),j=O?v?US:US.filter(function(J){return Qs(J)===O}):su,B=j.filter(function(J){return k.indexOf(J)>=0});B.length===0&&(B=j);var X=B.reduce(function(J,Z){return J[Z]=us(e,{placement:Z,boundary:d,rootBoundary:h,padding:c})[On(Z)],J},{});return Object.keys(X).sort(function(J,Z){return X[J]-X[Z]})}o(VS,"computeAutoPlacement");function g3(e){if(On(e)===Q0)return[];var t=zp(e);return[ny(e),t,ny(t)]}o(g3,"getExpandedFallbackPlacements");function v3(e){var t=e.state,n=e.options,l=e.name;if(!t.modifiersData[l]._skip){for(var d=n.mainAxis,h=d===void 0?!0:d,c=n.altAxis,v=c===void 0?!0:c,C=n.fallbackPlacements,k=n.padding,O=n.boundary,j=n.rootBoundary,B=n.altBoundary,X=n.flipVariations,J=X===void 0?!0:X,Z=n.allowedAutoPlacements,R=t.options.placement,A=On(R),I=A===R,G=C||(I||!J?[zp(R)]:g3(R)),K=[R].concat(G).reduce(function(ut,Lr){return ut.concat(On(Lr)===Q0?VS(t,{placement:Lr,boundary:O,rootBoundary:j,padding:k,flipVariations:J,allowedAutoPlacements:Z}):Lr)},[]),se=t.rects.reference,ne=t.rects.popper,pe=new Map,me=!0,xe=K[0],Ve=0;Ve=0,Ke=We?"width":"height",Ge=us(t,{placement:tt,boundary:O,rootBoundary:j,altBoundary:B,padding:k}),Xe=We?St?ln:an:St?Ln:Gr;se[Ke]>ne[Ke]&&(Xe=zp(Xe));var nr=zp(Xe),ct=[];if(h&&ct.push(Ge[_e]<=0),v&&ct.push(Ge[Xe]<=0,Ge[nr]<=0),ct.every(function(ut){return ut})){xe=tt,me=!1;break}pe.set(tt,ct)}if(me)for(var Hr=J?3:1,Qt=o(function(Lr){var zt=K.find(function($t){var ie=pe.get($t);if(ie)return ie.slice(0,Lr).every(function(rt){return rt})});if(zt)return xe=zt,"break"},"_loop"),_t=Hr;_t>0;_t--){var Ct=Qt(_t);if(Ct==="break")break}t.placement!==xe&&(t.modifiersData[l]._skip=!0,t.placement=xe,t.reset=!0)}}o(v3,"flip");var eL={name:"flip",enabled:!0,phase:"main",fn:v3,requiresIfExists:["offset"],data:{_skip:!1}};function tL(e,t,n){return n===void 0&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}o(tL,"getSideOffsets");function rL(e){return[Gr,ln,Ln,an].some(function(t){return e[t]>=0})}o(rL,"isAnySideFullyClipped");function y3(e){var t=e.state,n=e.name,l=t.rects.reference,d=t.rects.popper,h=t.modifiersData.preventOverflow,c=us(t,{elementContext:"reference"}),v=us(t,{altBoundary:!0}),C=tL(c,l),k=tL(v,d,h),O=rL(C),j=rL(k);t.modifiersData[n]={referenceClippingOffsets:C,popperEscapeOffsets:k,isReferenceHidden:O,hasPopperEscaped:j},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":O,"data-popper-escaped":j})}o(y3,"hide");var nL={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:y3};function w3(e,t,n){var l=On(e),d=[an,Gr].indexOf(l)>=0?-1:1,h=typeof n=="function"?n(Object.assign({},t,{placement:e})):n,c=h[0],v=h[1];return c=c||0,v=(v||0)*d,[an,ln].indexOf(l)>=0?{x:v,y:c}:{x:c,y:v}}o(w3,"distanceAndSkiddingToXY");function x3(e){var t=e.state,n=e.options,l=e.name,d=n.offset,h=d===void 0?[0,0]:d,c=ey.reduce(function(O,j){return O[j]=w3(j,t.rects,h),O},{}),v=c[t.placement],C=v.x,k=v.y;t.modifiersData.popperOffsets!=null&&(t.modifiersData.popperOffsets.x+=C,t.modifiersData.popperOffsets.y+=k),t.modifiersData[l]=c}o(x3,"offset");var iL={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:x3};function S3(e){var t=e.state,n=e.name;t.modifiersData[n]=mm({reference:t.rects.reference,element:t.rects.popper,strategy:"absolute",placement:t.placement})}o(S3,"popperOffsets");var oL={name:"popperOffsets",enabled:!0,phase:"read",fn:S3,data:{}};function KS(e){return e==="x"?"y":"x"}o(KS,"getAltAxis");function C3(e){var t=e.state,n=e.options,l=e.name,d=n.mainAxis,h=d===void 0?!0:d,c=n.altAxis,v=c===void 0?!1:c,C=n.boundary,k=n.rootBoundary,O=n.altBoundary,j=n.padding,B=n.tether,X=B===void 0?!0:B,J=n.tetherOffset,Z=J===void 0?0:J,R=us(t,{boundary:C,rootBoundary:k,padding:j,altBoundary:O}),A=On(t.placement),I=Qs(t.placement),G=!I,K=Ff(A),se=KS(K),ne=t.modifiersData.popperOffsets,pe=t.rects.reference,me=t.rects.popper,xe=typeof Z=="function"?Z(Object.assign({},t.rects,{placement:t.placement})):Z,Ve={x:0,y:0};if(!!ne){if(h||v){var tt=K==="y"?Gr:an,_e=K==="y"?Ln:ln,St=K==="y"?"height":"width",We=ne[K],Ke=ne[K]+R[tt],Ge=ne[K]-R[_e],Xe=X?-me[St]/2:0,nr=I===Rl?pe[St]:me[St],ct=I===Rl?-me[St]:-pe[St],Hr=t.elements.arrow,Qt=X&&Hr?If(Hr):{width:0,height:0},_t=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:pm(),Ct=_t[tt],ut=_t[_e],Lr=Bf(0,pe[St],Qt[St]),zt=G?pe[St]/2-Xe-Lr-Ct-xe:nr-Lr-Ct-xe,$t=G?-pe[St]/2+Xe+Lr+ut+xe:ct+Lr+ut+xe,ie=t.elements.arrow&&as(t.elements.arrow),rt=ie?K==="y"?ie.clientTop||0:ie.clientLeft||0:0,Pr=t.modifiersData.offset?t.modifiersData.offset[t.placement][K]:0,Gt=ne[K]+zt-Pr-rt,Yt=ne[K]+$t-Pr;if(h){var Se=Bf(X?au(Ke,Gt):Ke,We,X?Ao(Ge,Yt):Ge);ne[K]=Se,Ve[K]=Se-We}if(v){var Or=K==="x"?Gr:an,fn=K==="x"?Ln:ln,Un=ne[se],si=Un+R[Or],cn=Un-R[fn],Zt=Bf(X?au(si,Gt):si,Un,X?Ao(cn,Yt):cn);ne[se]=Zt,Ve[se]=Zt-Un}}t.modifiersData[l]=Ve}}o(C3,"preventOverflow");var sL={name:"preventOverflow",enabled:!0,phase:"main",fn:C3,requiresIfExists:["offset"]};function GS(e){return{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}}o(GS,"getHTMLElementScroll");function YS(e){return e===Br(e)||!Yr(e)?Hf(e):GS(e)}o(YS,"getNodeScroll");function b3(e){var t=e.getBoundingClientRect(),n=t.width/e.offsetWidth||1,l=t.height/e.offsetHeight||1;return n!==1||l!==1}o(b3,"isElementScaled");function XS(e,t,n){n===void 0&&(n=!1);var l=Yr(t),d=Yr(t)&&b3(t),h=Bn(t),c=Mo(e,d),v={scrollLeft:0,scrollTop:0},C={x:0,y:0};return(l||!l&&!n)&&((Pn(t)!=="body"||Uf(h))&&(v=YS(t)),Yr(t)?(C=Mo(t,!0),C.x+=t.clientLeft,C.y+=t.clientTop):h&&(C.x=Wf(h))),{x:c.left+v.scrollLeft-C.x,y:c.top+v.scrollTop-C.y,width:c.width,height:c.height}}o(XS,"getCompositeRect");function E3(e){var t=new Map,n=new Set,l=[];e.forEach(function(h){t.set(h.name,h)});function d(h){n.add(h.name);var c=[].concat(h.requires||[],h.requiresIfExists||[]);c.forEach(function(v){if(!n.has(v)){var C=t.get(v);C&&d(C)}}),l.push(h)}return o(d,"sort"),e.forEach(function(h){n.has(h.name)||d(h)}),l}o(E3,"order");function QS(e){var t=E3(e);return VN.reduce(function(n,l){return n.concat(t.filter(function(d){return d.phase===l}))},[])}o(QS,"orderModifiers");function ZS(e){var t;return function(){return t||(t=new Promise(function(n){Promise.resolve().then(function(){t=void 0,n(e())})})),t}}o(ZS,"debounce");function JS(e){var t=e.reduce(function(n,l){var d=n[l.name];return n[l.name]=d?Object.assign({},d,l,{options:Object.assign({},d.options,l.options),data:Object.assign({},d.data,l.data)}):l,n},{});return Object.keys(t).map(function(n){return t[n]})}o(JS,"mergeByName");var lL={placement:"bottom",modifiers:[],strategy:"absolute"};function aL(){for(var e=arguments.length,t=new Array(e),n=0;nxi.default.createElement("li",{role:"separator",className:"divider"}),"Divider");function ii(l){var d=l,{onClick:e,children:t}=d,n=Ws(d,["onClick","children"]);return xi.default.createElement("li",null,xi.default.createElement("a",ke({href:"#",onClick:o(c=>{c.preventDefault(),e()},"click")},n),t))}o(ii,"MenuItem");var cu=xi.default.memo(o(function(v){var C=v,{text:t,children:n,options:l,className:d,onOpen:h}=C,c=Ws(C,["text","children","options","className","onOpen"]);let[k,O]=(0,xi.useState)(null),[j,B]=(0,xi.useState)(!1),[X,J]=(0,xi.useState)(null),{styles:Z,attributes:R}=tC(k,X,ke({},l)),A=o(G=>{B(G),h&&h(G)},"setOpen");(0,xi.useEffect)(()=>{!X||document.addEventListener("click",G=>{X.contains(G.target)?document.addEventListener("click",()=>A(!1),{once:!0}):(G.preventDefault(),G.stopPropagation(),A(!1))},{once:!0,capture:!0})},[X]);let I;return j?I=xi.default.createElement("ul",ke({className:"dropdown-menu show",ref:J,style:Z.popper},R.popper),n):I=null,xi.default.createElement(xi.default.Fragment,null,xi.default.createElement("a",ke({href:"#",ref:O,className:(0,dL.default)(d,{open:j}),onClick:G=>{G.preventDefault(),A(!0)}},c),t),I)},"Dropdown"));function gm({value:e,onChange:t}){let n=qe(d=>d.backendState.contentViews||[]),l=zf.default.createElement("span",null,zf.default.createElement("i",{className:"fa fa-fw fa-files-o"}),"\xA0",zf.default.createElement("b",null,"View:")," ",e.toLowerCase()," ",zf.default.createElement("span",{className:"caret"}));return zf.default.createElement(cu,{text:l,className:"btn btn-default btn-xs",options:{placement:"top-start"}},n.map(d=>zf.default.createElement(ii,{key:d,onClick:()=>t(d)},d.toLowerCase().replace("_"," "))))}o(gm,"ViewSelector");function rC({flow:e,message:t}){let n=Xt(),l=e.request===t?"request":"response",d=qe(J=>J.ui.flow.contentViewFor[e.id+l]||"Auto"),h=(0,tr.useRef)(null),[c,v]=(0,tr.useState)(qe(J=>J.options.content_view_lines_cutoff)),C=(0,tr.useCallback)(()=>v(Math.max(1024,c*2)),[c]),[k,O]=(0,tr.useState)(!1),j;k?j=Kr.getContentURL(e,t):j=Kr.getContentURL(e,t,d,c+1);let B=K0(j,t.contentHash),X=(0,tr.useMemo)(()=>{if(B&&!k)try{return JSON.parse(B)}catch(J){return{description:"Network Error",lines:[[["error",`${B}`]]]}}else return},[B]);if(k)return tr.default.createElement("div",{className:"contentview",key:"edit"},tr.default.createElement("div",{className:"controls"},tr.default.createElement("h5",null,"[Editing]"),tr.default.createElement(kr,{onClick:o(()=>Ia(this,null,function*(){var R;let Z=(R=h.current)==null?void 0:R.getContent();yield n(Wi(e,{[l]:{content:Z}})),O(!1)}),"save"),icon:"fa-check text-success",className:"btn-xs"},"Done"),"\xA0",tr.default.createElement(kr,{onClick:()=>O(!1),icon:"fa-times text-danger",className:"btn-xs"},"Cancel")),tr.default.createElement(um,{ref:h,initialContent:B||""}));{let J=X?X.description:"Loading...";return tr.default.createElement("div",{className:"contentview",key:"view"},tr.default.createElement("div",{className:"controls"},tr.default.createElement("h5",null,J),tr.default.createElement(kr,{onClick:()=>O(!0),icon:"fa-edit",className:"btn-xs"},"Edit"),"\xA0",tr.default.createElement(G0,{icon:"fa-upload",text:"Replace",title:"Upload a file to replace the content.",onOpenFile:Z=>n(nN(e,Z,l)),className:"btn btn-default btn-xs"}),"\xA0",tr.default.createElement(gm,{value:d,onChange:Z=>n(w0(e.id+l,Z))})),nC.matches(t)&&tr.default.createElement(nC,{flow:e,message:t}),tr.default.createElement(Y0,{lines:(X==null?void 0:X.lines)||[],maxLines:c,showMore:C}))}}o(rC,"HttpMessage");var O3=/^image\/(png|jpe?g|gif|webp|vnc.microsoft.icon|x-icon)$/i;nC.matches=e=>O3.test(Kr.getContentType(e)||"");function nC({flow:e,message:t}){return tr.default.createElement("div",{className:"flowview-image"},tr.default.createElement("img",{src:Kr.getContentURL(e,t),alt:"preview",className:"img-thumbnail"}))}o(nC,"ViewImage");function M3({flow:e}){let t=Xt();return Ut.createElement("div",{className:"first-line request-line"},Ut.createElement("div",null,Ut.createElement(Df,{content:e.request.method,onEditDone:n=>t(Wi(e,{request:{method:n}})),isValid:n=>n.length>0}),"\xA0",Ut.createElement(Df,{content:Oo.pretty_url(e.request),onEditDone:n=>t(Wi(e,{request:ke({path:""},gS(n))})),isValid:n=>{var l;return!!((l=gS(n))==null?void 0:l.host)}}),"\xA0",Ut.createElement(Df,{content:e.request.http_version,onEditDone:n=>t(Wi(e,{request:{http_version:n}})),isValid:vS})))}o(M3,"RequestLine");function A3({flow:e}){let t=Xt();return Ut.createElement("div",{className:"first-line response-line"},Ut.createElement(Df,{content:e.response.http_version,onEditDone:n=>t(Wi(e,{response:{http_version:n}})),isValid:vS}),"\xA0",Ut.createElement(Df,{content:e.response.status_code+"",onEditDone:n=>t(Wi(e,{response:{code:parseInt(n)}})),isValid:n=>/^\d+$/.test(n)}),e.response.http_version!=="HTTP/2.0"&&Ut.createElement(Ut.Fragment,null,"\xA0",Ut.createElement(Xs,{content:e.response.reason,onEditDone:n=>t(Wi(e,{response:{msg:n}}))})))}o(A3,"ResponseLine");function D3({flow:e,message:t}){let n=Xt(),l=e.request===t?"request":"response";return Ut.createElement(Bp,{className:"headers",data:t.headers,onChange:d=>n(Wi(e,{[l]:{headers:d}}))})}o(D3,"Headers");function R3({flow:e,message:t}){let n=Xt(),l=e.request===t?"request":"response";return!Kr.get_first_header(t,/^trailer$/i)?null:Ut.createElement(Ut.Fragment,null,Ut.createElement("hr",null),Ut.createElement("h5",null,"HTTP Trailers"),Ut.createElement(Bp,{className:"trailers",data:t.trailers,onChange:h=>n(Wi(e,{[l]:{trailers:h}}))}))}o(R3,"Trailers");var mL=Ut.memo(o(function({flow:t,message:n}){let l=t.request===n?"request":"response",d=t.request===n?M3:A3;return Ut.createElement("section",{className:l},Ut.createElement(d,{flow:t}),Ut.createElement(D3,{flow:t,message:n}),Ut.createElement("hr",null),Ut.createElement(rC,{key:t.id+l,flow:t,message:n}),Ut.createElement(R3,{flow:t,message:n}))},"Message"));function iC(){let e=qe(t=>t.flows.byId[t.flows.selected[0]]);return Ut.createElement(mL,{flow:e,message:e.request})}o(iC,"Request");iC.displayName="Request";function oC(){let e=qe(t=>t.flows.byId[t.flows.selected[0]]);return Ut.createElement(mL,{flow:e,message:e.response})}o(oC,"Response");oC.displayName="Response";var Ye=fe(Oe());var I3=o(({message:e})=>Ye.createElement("div",null,e.query?e.op_code:e.response_code,"\xA0",e.truncation?"(Truncated)":""),"Summary"),F3=o(({message:e})=>Ye.createElement(Ye.Fragment,null,Ye.createElement("h5",null,e.recursion_desired?"Recursive ":"","Question"),Ye.createElement("table",null,Ye.createElement("thead",null,Ye.createElement("tr",null,Ye.createElement("th",null,"Name"),Ye.createElement("th",null,"Type"),Ye.createElement("th",null,"Class"))),Ye.createElement("tbody",null,e.questions.map((t,n)=>Ye.createElement("tr",{key:n},Ye.createElement("td",null,t.name),Ye.createElement("td",null,t.type),Ye.createElement("td",null,t.class)))))),"Questions"),sC=o(({name:e,values:t})=>Ye.createElement(Ye.Fragment,null,Ye.createElement("h5",null,e),t.length>0?Ye.createElement("table",null,Ye.createElement("thead",null,Ye.createElement("tr",null,Ye.createElement("th",null,"Name"),Ye.createElement("th",null,"Type"),Ye.createElement("th",null,"Class"),Ye.createElement("th",null,"TTL"),Ye.createElement("th",null,"Data"))),Ye.createElement("tbody",null,t.map((n,l)=>Ye.createElement("tr",{key:l},Ye.createElement("td",null,n.name),Ye.createElement("td",null,n.type),Ye.createElement("td",null,n.class),Ye.createElement("td",null,n.ttl),Ye.createElement("td",null,n.data))))):"\u2014"),"ResourceRecords"),gL=o(({type:e,message:t})=>Ye.createElement("section",{className:"dns-"+e},Ye.createElement("div",{className:`first-line ${e}-line`},Ye.createElement(I3,{message:t})),Ye.createElement(F3,{message:t}),Ye.createElement("hr",null),Ye.createElement(sC,{name:`${t.authoritative_answer?"Authoritative ":""}${t.recursion_available?"Recursive ":""}Answer`,values:t.answers}),Ye.createElement("hr",null),Ye.createElement(sC,{name:"Authority",values:t.authorities}),Ye.createElement("hr",null),Ye.createElement(sC,{name:"Additional",values:t.additionals})),"Message");function lC(){let e=qe(t=>t.flows.byId[t.flows.selected[0]]);return Ye.createElement(gL,{type:"request",message:e.request})}o(lC,"Request");lC.displayName="Request";function aC(){let e=qe(t=>t.flows.byId[t.flows.selected[0]]);return Ye.createElement(gL,{type:"response",message:e.response})}o(aC,"Response");aC.displayName="Response";var Ee=fe(Oe());function vL({conn:e}){var n,l,d;let t=null;return"address"in e?t=Ee.createElement(Ee.Fragment,null,Ee.createElement("tr",null,Ee.createElement("td",null,"Address:"),Ee.createElement("td",null,(n=e.address)==null?void 0:n.join(":"))),e.peername&&Ee.createElement("tr",null,Ee.createElement("td",null,"Resolved address:"),Ee.createElement("td",null,e.peername.join(":"))),e.sockname&&Ee.createElement("tr",null,Ee.createElement("td",null,"Source address:"),Ee.createElement("td",null,e.sockname.join(":")))):((l=e.peername)==null?void 0:l[0])&&(t=Ee.createElement(Ee.Fragment,null,Ee.createElement("tr",null,Ee.createElement("td",null,"Address:"),Ee.createElement("td",null,(d=e.peername)==null?void 0:d.join(":"))))),Ee.createElement("table",{className:"connection-table"},Ee.createElement("tbody",null,t,e.sni?Ee.createElement("tr",null,Ee.createElement("td",null,Ee.createElement("abbr",{title:"TLS Server Name Indication"},"SNI"),":"),Ee.createElement("td",null,e.sni)):null,e.alpn?Ee.createElement("tr",null,Ee.createElement("td",null,Ee.createElement("abbr",{title:"ALPN protocol negotiated"},"ALPN"),":"),Ee.createElement("td",null,e.alpn)):null,e.tls_version?Ee.createElement("tr",null,Ee.createElement("td",null,"TLS Version:"),Ee.createElement("td",null,e.tls_version)):null,e.cipher?Ee.createElement("tr",null,Ee.createElement("td",null,"TLS Cipher:"),Ee.createElement("td",null,e.cipher)):null))}o(vL,"ConnectionInfo");function yL(e){return Ee.createElement("dl",{className:"cert-attributes"},e.map(([t,n])=>Ee.createElement(Ee.Fragment,{key:t},Ee.createElement("dt",null,t),Ee.createElement("dd",null,n))))}o(yL,"attrList");function B3({flow:e}){var n;let t=(n=e.server_conn)==null?void 0:n.cert;return t?Ee.createElement(Ee.Fragment,null,Ee.createElement("h4",{key:"name"},"Server Certificate"),Ee.createElement("table",{className:"certificate-table"},Ee.createElement("tbody",null,Ee.createElement("tr",null,Ee.createElement("td",null,"Type"),Ee.createElement("td",null,t.keyinfo[0],", ",t.keyinfo[1]," bits")),Ee.createElement("tr",null,Ee.createElement("td",null,"SHA256 digest"),Ee.createElement("td",null,t.sha256)),Ee.createElement("tr",null,Ee.createElement("td",null,"Valid from"),Ee.createElement("td",null,no(t.notbefore,{milliseconds:!1}))),Ee.createElement("tr",null,Ee.createElement("td",null,"Valid to"),Ee.createElement("td",null,no(t.notafter,{milliseconds:!1}))),Ee.createElement("tr",null,Ee.createElement("td",null,"Subject Alternative Names"),Ee.createElement("td",null,t.altnames.join(", "))),Ee.createElement("tr",null,Ee.createElement("td",null,"Subject"),Ee.createElement("td",null,yL(t.subject))),Ee.createElement("tr",null,Ee.createElement("td",null,"Issuer"),Ee.createElement("td",null,yL(t.issuer))),Ee.createElement("tr",null,Ee.createElement("td",null,"Serial"),Ee.createElement("td",null,t.serial))))):Ee.createElement(Ee.Fragment,null)}o(B3,"CertificateInfo");function sy({flow:e}){var t;return Ee.createElement("section",{className:"detail"},Ee.createElement("h4",null,"Client Connection"),Ee.createElement(vL,{conn:e.client_conn}),((t=e.server_conn)==null?void 0:t.address)&&Ee.createElement(Ee.Fragment,null,Ee.createElement("h4",null,"Server Connection"),Ee.createElement(vL,{conn:e.server_conn})),Ee.createElement(B3,{flow:e}))}o(sy,"Connection");sy.displayName="Connection";var vm=fe(Oe());function ly({flow:e}){return vm.createElement("section",{className:"error"},vm.createElement("div",{className:"alert alert-warning"},e.error.msg,vm.createElement("div",null,vm.createElement("small",null,no(e.error.timestamp)))))}o(ly,"Error");ly.displayName="Error";var fs=fe(Oe());function H3({t:e,deltaTo:t,title:n}){return e?fs.createElement("tr",null,fs.createElement("td",null,n,":"),fs.createElement("td",null,no(e),t&&fs.createElement("span",{className:"text-muted"},"(",S0(1e3*(e-t)),")"))):fs.createElement("tr",null)}o(H3,"TimeStamp");function ay({flow:e}){var l,d,h,c,v,C;let t;e.type==="http"?t=e.request.timestamp_start:t=e.client_conn.timestamp_start;let n=[{title:"Server conn. initiated",t:(l=e.server_conn)==null?void 0:l.timestamp_start,deltaTo:t},{title:"Server conn. TCP handshake",t:(d=e.server_conn)==null?void 0:d.timestamp_tcp_setup,deltaTo:t},{title:"Server conn. TLS handshake",t:(h=e.server_conn)==null?void 0:h.timestamp_tls_setup,deltaTo:t},{title:"Server conn. closed",t:(c=e.server_conn)==null?void 0:c.timestamp_end,deltaTo:t},{title:"Client conn. established",t:e.client_conn.timestamp_start,deltaTo:e.type==="http"?t:void 0},{title:"Client conn. TLS handshake",t:e.client_conn.timestamp_tls_setup,deltaTo:t},{title:"Client conn. closed",t:e.client_conn.timestamp_end,deltaTo:t}];return e.type==="http"&&n.push({title:"First request byte",t:e.request.timestamp_start},{title:"Request complete",t:e.request.timestamp_end,deltaTo:t},{title:"First response byte",t:(v=e.response)==null?void 0:v.timestamp_start,deltaTo:t},{title:"Response complete",t:(C=e.response)==null?void 0:C.timestamp_end,deltaTo:t}),fs.createElement("section",{className:"timing"},fs.createElement("h4",null,"Timing"),fs.createElement("table",{className:"timing-table"},fs.createElement("tbody",null,n.filter(k=>!!k.t).sort((k,O)=>k.t-O.t).map(k=>fs.createElement(H3,ke({key:k.title},k))))))}o(ay,"Timing");ay.displayName="Timing";var pu=fe(Oe());var Zs=fe(Oe()),jp=fe(Oe());function $f({flow:e,messages_meta:t}){let n=Xt(),l=qe(k=>k.ui.flow.contentViewFor[e.id+"messages"]||"Auto"),[d,h]=(0,jp.useState)(qe(k=>k.options.content_view_lines_cutoff)),c=(0,jp.useCallback)(()=>h(Math.max(1024,d*2)),[d]),v=K0(Kr.getContentURL(e,"messages",l,d+1),e.id+t.count),C=(0,jp.useMemo)(()=>{if(v)try{return JSON.parse(v)}catch(k){return{description:"Network Error",lines:[[["error",`${v}`]]]}}},[v])||[];return Zs.createElement("div",{className:"contentview"},Zs.createElement("div",{className:"controls"},Zs.createElement("h5",null,t.count," Messages"),Zs.createElement(gm,{value:l,onChange:k=>n(w0(e.id+"messages",k))})),C.map((k,O)=>{let j=`fa fa-fw fa-arrow-${k.from_client?"right text-primary":"left text-danger"}`,B=Zs.createElement("div",{key:O},Zs.createElement("small",null,Zs.createElement("i",{className:j}),Zs.createElement("span",{className:"pull-right"},k.timestamp&&no(k.timestamp))),Zs.createElement(Y0,{lines:k.lines,maxLines:d,showMore:c}));return d-=k.lines.length,B}))}o($f,"Messages");function uy({flow:e}){return pu.createElement("section",{className:"websocket"},pu.createElement("h4",null,"WebSocket"),pu.createElement($f,{flow:e,messages_meta:e.websocket.messages_meta}),pu.createElement(W3,{websocket:e.websocket}))}o(uy,"WebSocket");uy.displayName="WebSocket";function W3({websocket:e}){if(!e.timestamp_end)return null;let t=e.close_reason?`(${e.close_reason})`:"";return pu.createElement("div",null,pu.createElement("i",{className:"fa fa-fw fa-window-close text-muted"}),"\xA0 Closed by ",e.closed_by_client?"client":"server"," with code ",e.close_code," ",t,".",pu.createElement("small",{className:"pull-right"},no(e.timestamp_end)))}o(W3,"CloseSummary");var wL=fe(ti());var fy=fe(Oe());function cy({flow:e}){return fy.createElement("section",{className:"tcp"},fy.createElement("h4",null,"TCP Data"),fy.createElement($f,{flow:e,messages_meta:e.messages_meta}))}o(cy,"TcpMessages");cy.displayName="TCP Messages";var py=fe(Oe());function dy({flow:e}){return py.createElement("section",{className:"udp"},py.createElement("h4",null,"UDP Data"),py.createElement($f,{flow:e,messages_meta:e.messages_meta}))}o(dy,"UdpMessages");dy.displayName="UDP Messages";var xL={request:iC,response:oC,error:ly,connection:sy,timing:ay,websocket:uy,tcpmessages:cy,udpmessages:dy,dnsrequest:lC,dnsresponse:aC};function hy(e){let t;switch(e.type){case"http":t=["request","response","websocket"].filter(n=>e[n]);break;case"tcp":t=["tcpmessages"];break;case"udp":t=["udpmessages"];break;case"dns":t=["request","response"].filter(n=>e[n]).map(n=>"dns"+n);break}return e.error&&t.push("error"),t.push("connection"),t.push("timing"),t}o(hy,"tabsForFlow");function uC(){let e=Xt(),t=qe(h=>h.flows.byId[h.flows.selected[0]]),n=hy(t),l=qe(h=>h.ui.flow.tab);n.indexOf(l)<0&&(l==="response"&&t.error?l="error":l==="error"&&"response"in t?l="response":l=n[0]);let d=xL[l];return ym.createElement("div",{className:"flow-detail"},ym.createElement("nav",{className:"nav-tabs nav-tabs-sm"},n.map(h=>ym.createElement("a",{key:h,href:"#",className:(0,wL.default)({active:l===h}),onClick:c=>{c.preventDefault(),e(Lf(h))}},xL[h].displayName))),ym.createElement(d,{flow:t}))}o(uC,"FlowView");function SL(e){if(e.ctrlKey||e.metaKey)return()=>{};let t=e.key;return e.preventDefault(),(n,l)=>{let d=l().flows,h=d.byId[l().flows.selected[0]];switch(t){case"k":case"ArrowUp":n(Mf(d,-1));break;case"j":case"ArrowDown":n(Mf(d,1));break;case" ":case"PageDown":n(Mf(d,10));break;case"PageUp":n(Mf(d,-10));break;case"End":n(Mf(d,1e10));break;case"Home":n(Mf(d,-1e10));break;case"Escape":l().ui.modal.activeModal?n(z0()):n(Af(void 0));break;case"ArrowLeft":{if(!h)break;let c=hy(h),v=l().ui.flow.tab,C=c[(Math.max(0,c.indexOf(v))-1+c.length)%c.length];n(Lf(C));break}case"Tab":case"ArrowRight":{if(!h)break;let c=hy(h),v=l().ui.flow.tab,C=c[(Math.max(0,c.indexOf(v))+1)%c.length];n(Lf(C));break}case"d":{if(!h)return;n(F0(h));break}case"n":{Pf("view.flows.create","get","https://example.com/");break}case"D":{if(!h)return;n(B0(h));break}case"a":{h&&h.intercepted&&n(Op(h));break}case"A":{n(R0());break}case"r":{h&&n(Mp(h));break}case"v":{h&&h.modified&&n(H0(h));break}case"x":{h&&h.intercepted&&n(I0(h));break}case"X":{n(rN());break}case"z":{n(W0());break}default:return}}}o(SL,"onKeyDown");var Zp=fe(Oe());var wm=fe(Oe()),xm=fe(iu()),CL=fe(ti()),qp=class extends wm.Component{constructor(t,n){super(t,n);this.state={applied:!1,startX:0,startY:0},this.onMouseMove=this.onMouseMove.bind(this),this.onMouseDown=this.onMouseDown.bind(this),this.onMouseUp=this.onMouseUp.bind(this),this.onDragEnd=this.onDragEnd.bind(this)}onMouseDown(t){this.setState({startX:t.pageX,startY:t.pageY}),window.addEventListener("mousemove",this.onMouseMove),window.addEventListener("mouseup",this.onMouseUp),window.addEventListener("dragend",this.onDragEnd)}onDragEnd(){xm.default.findDOMNode(this).style.transform="",window.removeEventListener("dragend",this.onDragEnd),window.removeEventListener("mouseup",this.onMouseUp),window.removeEventListener("mousemove",this.onMouseMove)}onMouseUp(t){this.onDragEnd();let n=xm.default.findDOMNode(this),l=n.previousElementSibling,d=l.offsetHeight+t.pageY-this.state.startY;this.props.axis==="x"&&(d=l.offsetWidth+t.pageX-this.state.startX),l.style.flex=`0 0 ${Math.max(0,d)}px`,n.nextElementSibling.style.flex="1 1 auto",this.setState({applied:!0}),this.onResize()}onMouseMove(t){let n=0,l=0;this.props.axis==="x"?n=t.pageX-this.state.startX:l=t.pageY-this.state.startY,xm.default.findDOMNode(this).style.transform=`translate(${n}px, ${l}px)`}onResize(){window.setTimeout(()=>window.dispatchEvent(new CustomEvent("resize")),1)}reset(t){if(!this.state.applied)return;let n=xm.default.findDOMNode(this);n.previousElementSibling.style.flex="",n.nextElementSibling.style.flex="",t||this.setState({applied:!1}),this.onResize()}componentWillUnmount(){this.reset(!0)}render(){return wm.default.createElement("div",{className:(0,CL.default)("splitter",this.props.axis==="x"?"splitter-x":"splitter-y")},wm.default.createElement("div",{onMouseDown:this.onMouseDown,draggable:"true"}))}};o(qp,"Splitter"),qp.defaultProps={axis:"x"};var cs=fe(Oe()),bm=fe(Sm()),gy=fe(iu());var FL=fe(fC());var cC=fe(iu()),OL=Symbol("shouldStick"),ML=o(e=>e.scrollTop+e.clientHeight===e.scrollHeight,"isAtBottom"),my=o(e=>{var t;return Object.assign((o(t=class extends e{UNSAFE_componentWillUpdate(){let l=cC.default.findDOMNode(this);this[OL]=l.scrollTop&&ML(l),super.UNSAFE_componentWillUpdate&&super.UNSAFE_componentWillUpdate(),super.componentWillUpdate&&super.componentWillUpdate()}componentDidUpdate(){let l=cC.default.findDOMNode(this);this[OL]&&!ML(l)&&(l.scrollTop=l.scrollHeight),super.componentDidUpdate&&super.componentDidUpdate()}},"AutoScrollWrapper"),t.displayName=e.name,t),e)},"default");function jf(e=void 0){if(!e)return{start:0,end:0,paddingTop:0,paddingBottom:0};let{itemCount:t,rowHeight:n,viewportTop:l,viewportHeight:d,itemHeights:h}=e,c=l+d,v=0,C=0,k=0,O=0;if(h){let j=0;for(let B=0;B0&&jv.flows.sort.desc),l=qe(v=>v.flows.sort.column),d=qe(v=>v.options.web_columns),h=n?"sort-desc":"sort-asc",c=d.map(v=>nm[v]).filter(v=>v).concat(Pp);return Cm.createElement("tr",null,c.map(v=>Cm.createElement("th",{className:(0,AL.default)(`col-${v.name}`,l===v.name&&h),key:v.name,onClick:()=>t(tN(v.name===l&&n?void 0:v.name,v.name!==l?!1:!n))},v.headerName)))},"FlowTableHead"));var Vp=fe(Oe()),RL=fe(ti());var IL=Vp.default.memo(o(function({flow:t,selected:n,highlighted:l}){let d=Xt(),h=qe(k=>k.options.web_columns),c=(0,RL.default)({selected:n,highlighted:l,intercepted:t.intercepted,"has-request":t.type==="http"&&t.request,"has-response":t.type==="http"&&t.response}),v=(0,Vp.useCallback)(k=>{let O=k.target;for(;O.parentNode;){if(O.classList.contains("col-quickactions"))return;O=O.parentNode}d(Af(t.id))},[t]),C=h.map(k=>nm[k]).filter(k=>k).concat(Pp);return Vp.default.createElement("tr",{className:c,onClick:v},C.map(k=>Vp.default.createElement(k,{key:k.name,flow:t})))},"FlowRow"));var Em=class extends cs.Component{constructor(t,n){super(t,n);this.state={vScroll:jf()},this.onViewportUpdate=this.onViewportUpdate.bind(this)}UNSAFE_componentWillMount(){window.addEventListener("resize",this.onViewportUpdate)}componentDidMount(){this.onViewportUpdate()}UNSAFE_componentWillUnmount(){window.removeEventListener("resize",this.onViewportUpdate)}componentDidUpdate(){if(this.onViewportUpdate(),!this.shouldScrollIntoView)return;this.shouldScrollIntoView=!1;let{rowHeight:t,flows:n,selected:l}=this.props,d=gy.default.findDOMNode(this),h=gy.default.findDOMNode(this.refs.head),c=h?h.offsetHeight:0,v=n.indexOf(l)*t+c,C=v+t,k=d.scrollTop,O=d.offsetHeight;v-ck+O&&(d.scrollTop=C-O)}UNSAFE_componentWillReceiveProps(t){t.selected&&t.selected!==this.props.selected&&(this.shouldScrollIntoView=!0)}onViewportUpdate(){let t=gy.default.findDOMNode(this),n=t.scrollTop||0,l=jf({viewportTop:n,viewportHeight:t.offsetHeight||0,itemCount:this.props.flows.length,rowHeight:this.props.rowHeight});if(this.state.viewportTop!==n||!(0,FL.default)(this.state.vScroll,l)){let d=Math.min(n,l.end*this.props.rowHeight);this.setState({vScroll:l,viewportTop:d})}}render(){let{vScroll:t,viewportTop:n}=this.state,{flows:l,selected:d,highlight:h}=this.props,c=h?Of.parse(h):()=>!1;return cs.createElement("div",{className:"flow-table",onScroll:this.onViewportUpdate},cs.createElement("table",null,cs.createElement("thead",{ref:"head",style:{transform:`translateY(${n}px)`}},cs.createElement(DL,null)),cs.createElement("tbody",null,cs.createElement("tr",{style:{height:t.paddingTop}}),l.slice(t.start,t.end).map(v=>cs.createElement(IL,{key:v.id,flow:v,selected:v===d,highlighted:c(v)})),cs.createElement("tr",{style:{height:t.paddingBottom}}))))}};o(Em,"FlowTable"),Vc(Em,"propTypes",{flows:bm.default.array.isRequired,rowHeight:bm.default.number,highlight:bm.default.string,selected:bm.default.object}),Vc(Em,"defaultProps",{rowHeight:32});var $3=my(Em),BL=Hi(e=>({flows:e.flows.view,highlight:e.flows.highlight,selected:e.flows.byId[e.flows.selected[0]]}))($3);var Nr=fe(Oe()),Ny=fe(Oe());var UP=fe(WP());function RC(){let e=qe(n=>n.backendState.servers),t;return e.length===0?t="":e.length===1?t="Configure your client to use the following proxy server:":t="Configure your client to use one of the following proxy servers:",Nr.createElement("div",{style:{padding:"1em 2em"}},Nr.createElement("h3",null,"mitmproxy is running."),Nr.createElement("p",null,"No flows have been recorded yet.",Nr.createElement("br",null),t),Nr.createElement("ul",{className:"fa-ul"},e.map((n,l)=>Nr.createElement("li",{key:n.full_spec},Nr.createElement(BB,ke({},n))))))}o(RC,"CaptureSetup");function BB({description:e,listen_addrs:t,last_exception:n,is_running:l,full_spec:d,wireguard_conf:h}){let c=(0,Ny.useRef)(null);(0,Ny.useEffect)(()=>{h&&c.current&&UP.default.toCanvas(c.current,h,{margin:0,scale:3})},[h]);let v,C=t.length===1||t.length===2&&t[0][1]===t[1][1],k=t.every(B=>["::","0.0.0.0"].includes(B[0]));C&&k?v=iS(["*",t[0][1]]):v=t.map(iS).join(" and "),e=e[0].toUpperCase()+e.substr(1);let O,j;return n?(j="fa-exclamation text-error",O=Nr.createElement(Nr.Fragment,null,e," (",d,"):",Nr.createElement("br",null),n)):l?(j="fa-check text-success",O=`${e} listening at ${v}.`,h&&(O=Nr.createElement(Nr.Fragment,null,O,Nr.createElement("div",{className:"wireguard-config"},Nr.createElement("pre",null,h),Nr.createElement("canvas",{ref:c}))))):(j="fa-pause text-warning",O=Nr.createElement(Nr.Fragment,null,e," (",d,")")),Nr.createElement(Nr.Fragment,null,Nr.createElement("i",{className:`fa fa-li ${j}`}),O)}o(BB,"ServerDescription");function IC(){let e=qe(n=>!!n.flows.byId[n.flows.selected[0]]),t=qe(n=>n.flows.list.length>0);return Zp.createElement("div",{className:"main-view"},t?Zp.createElement(BL,null):Zp.createElement(RC,null),e&&Zp.createElement(qp,{key:"splitter"}),e&&Zp.createElement(uC,{key:"flowDetails"}))}o(IC,"MainView");var Io=fe(Oe()),GP=fe(ti());var oi=fe(Oe());var ps=fe(Oe()),Ly=fe(iu()),zP=fe(ti());var oo=fe(Oe());var so=class extends oo.Component{constructor(t,n){super(t,n);this.state={doc:so.doc}}componentDidMount(){so.xhr||(so.xhr=kt("/filter-help").then(t=>t.json()),so.xhr.catch(()=>{so.xhr=null})),this.state.doc||so.xhr.then(t=>{so.doc=t,this.setState({doc:t})})}render(){let{doc:t}=this.state;return t?oo.default.createElement("table",{className:"table table-condensed"},oo.default.createElement("tbody",null,t.commands.map(n=>oo.default.createElement("tr",{key:n[1],onClick:l=>this.props.selectHandler(n[0].split(" ")[0]+" ")},oo.default.createElement("td",null,n[0].replace(" ","\xA0")),oo.default.createElement("td",null,n[1]))),oo.default.createElement("tr",{key:"docs-link"},oo.default.createElement("td",{colSpan:2},oo.default.createElement("a",{href:"https://mitmproxy.org/docs/latest/concepts-filters/",target:"_blank"},oo.default.createElement("i",{className:"fa fa-external-link"}),"\xA0 mitmproxy docs"))))):oo.default.createElement("i",{className:"fa fa-spinner fa-spin"})}};o(so,"FilterDocs");var Yf=class extends ps.Component{constructor(t,n){super(t,n);this.state={value:this.props.value,focus:!1,mousefocus:!1},this.onChange=this.onChange.bind(this),this.onFocus=this.onFocus.bind(this),this.onBlur=this.onBlur.bind(this),this.onKeyDown=this.onKeyDown.bind(this),this.onMouseEnter=this.onMouseEnter.bind(this),this.onMouseLeave=this.onMouseLeave.bind(this),this.selectFilter=this.selectFilter.bind(this)}UNSAFE_componentWillReceiveProps(t){this.setState({value:t.value})}isValid(t){try{return t&&Of.parse(t),!0}catch(n){return!1}}getDesc(){if(!this.state.value)return ps.default.createElement(so,{selectHandler:this.selectFilter});try{return Of.parse(this.state.value).desc}catch(t){return""+t}}onChange(t){let n=t.target.value;this.setState({value:n}),this.isValid(n)&&this.props.onChange(n)}onFocus(){this.setState({focus:!0})}onBlur(){this.setState({focus:!1})}onMouseEnter(){this.setState({mousefocus:!0})}onMouseLeave(){this.setState({mousefocus:!1})}onKeyDown(t){(t.key==="Escape"||t.key==="Enter")&&(this.blur(),this.setState({mousefocus:!1})),t.stopPropagation()}selectFilter(t){this.setState({value:t}),Ly.default.findDOMNode(this.refs.input).focus()}blur(){Ly.default.findDOMNode(this.refs.input).blur()}select(){Ly.default.findDOMNode(this.refs.input).select()}render(){let{type:t,color:n,placeholder:l}=this.props,{value:d,focus:h,mousefocus:c}=this.state;return ps.default.createElement("div",{className:(0,zP.default)("filter-input input-group",{"has-error":!this.isValid(d)})},ps.default.createElement("span",{className:"input-group-addon"},ps.default.createElement("i",{className:"fa fa-fw fa-"+t,style:{color:n}})),ps.default.createElement("input",{type:"text",ref:"input",placeholder:l,className:"form-control",value:d,onChange:this.onChange,onFocus:this.onFocus,onBlur:this.onBlur,onKeyDown:this.onKeyDown}),(h||c)&&ps.default.createElement("div",{className:"popover bottom",onMouseEnter:this.onMouseEnter,onMouseLeave:this.onMouseLeave},ps.default.createElement("div",{className:"arrow"}),ps.default.createElement("div",{className:"popover-content"},this.getDesc())))}};o(Yf,"FilterInput");Jp.title="Start";function Jp(){return oi.createElement("div",{className:"main-menu"},oi.createElement("div",{className:"menu-group"},oi.createElement("div",{className:"menu-content"},oi.createElement(WB,null),oi.createElement(UB,null)),oi.createElement("div",{className:"menu-legend"},"Find")),oi.createElement("div",{className:"menu-group"},oi.createElement("div",{className:"menu-content"},oi.createElement(HB,null),oi.createElement(zB,null)),oi.createElement("div",{className:"menu-legend"},"Intercept")))}o(Jp,"StartMenu");function HB(){let e=Xt(),t=qe(n=>n.options.intercept);return oi.createElement(Yf,{value:t||"",placeholder:"Intercept",type:"pause",color:"hsl(208, 56%, 53%)",onChange:n=>e(Ip("intercept",n))})}o(HB,"InterceptInput");function WB(){let e=Xt(),t=qe(n=>n.flows.filter);return oi.createElement(Yf,{value:t||"",placeholder:"Search",type:"search",color:"black",onChange:n=>e(A0(n))})}o(WB,"FlowFilterInput");function UB(){let e=Xt(),t=qe(n=>n.flows.highlight);return oi.createElement(Yf,{value:t||"",placeholder:"Highlight",type:"tag",color:"hsl(48, 100%, 50%)",onChange:n=>e(D0(n))})}o(UB,"HighlightInput");function zB(){let e=Xt();return oi.createElement(kr,{className:"btn-sm",title:"[a]ccept all",icon:"fa-forward text-success",onClick:()=>e(R0())},"Resume All")}o(zB,"ResumeAll");var Qr=fe(Oe());var Xf=fe(Oe());function FC({value:e,onChange:t,children:n}){return Xf.createElement("div",{className:"menu-entry"},Xf.createElement("label",null,Xf.createElement("input",{type:"checkbox",checked:e,onChange:t}),n))}o(FC,"MenuToggle");function Py({name:e,children:t}){let n=Xt(),l=qe(d=>d.options[e]);return Xf.createElement(FC,{value:!!l,onChange:()=>n(Ip(e,!l))},t)}o(Py,"OptionsToggle");function $P(){let e=Gs(),t=qe(n=>n.eventLog.visible);return Xf.createElement(FC,{value:t,onChange:()=>e(Rp())},"Display Event Log")}o($P,"EventlogToggle");function jP(){let e=Gs(),t=qe(n=>n.commandBar.visible);return Xf.createElement(FC,{value:t,onChange:()=>e(V0())},"Display Command Bar")}o(jP,"CommandBarToggle");var BC=fe(Oe());function HC({children:e,resource:t}){let n=`https://docs.mitmproxy.org/stable/${t}`;return BC.createElement("a",{target:"_blank",href:n},e||BC.createElement("i",{className:"fa fa-question-circle"}))}o(HC,"DocsLink");var Oy=fe(Oe());function Ro({children:e}){return window.MITMWEB_STATIC?null:Oy.createElement(Oy.Fragment,null,e)}o(Ro,"HideInStatic");My.title="Options";function My(){let e=Xt(),t=o(()=>sN("OptionModal"),"openOptions");return Qr.createElement("div",null,Qr.createElement(Ro,null,Qr.createElement("div",{className:"menu-group"},Qr.createElement("div",{className:"menu-content"},Qr.createElement(kr,{title:"Open Options",icon:"fa-cogs text-primary",onClick:()=>e(t())},"Edit Options ",Qr.createElement("sup",null,"alpha"))),Qr.createElement("div",{className:"menu-legend"},"Options Editor")),Qr.createElement("div",{className:"menu-group"},Qr.createElement("div",{className:"menu-content"},Qr.createElement(Py,{name:"anticache"},"Strip cache headers ",Qr.createElement(HC,{resource:"overview-features/#anticache"})),Qr.createElement(Py,{name:"showhost"},"Use host header for display"),Qr.createElement(Py,{name:"ssl_insecure"},"Don't verify server certificates")),Qr.createElement("div",{className:"menu-legend"},"Quick Options"))),Qr.createElement("div",{className:"menu-group"},Qr.createElement("div",{className:"menu-content"},Qr.createElement($P,null),Qr.createElement(jP,null)),Qr.createElement("div",{className:"menu-legend"},"View Options")))}o(My,"OptionMenu");var Hn=fe(Oe());var qP=Hn.memo(o(function(){let t=Gs(),n=qe(l=>l.flows.filter);return Hn.createElement(cu,{className:"pull-left special",text:"File",options:{placement:"bottom-start"}},Hn.createElement("li",null,Hn.createElement(G0,{icon:"fa-folder-open",text:"\xA0Open...",onClick:l=>l.stopPropagation(),onOpenFile:l=>{t(iN(l)),document.body.click()}})),Hn.createElement(ii,{onClick:()=>location.replace("/flows/dump")},Hn.createElement("i",{className:"fa fa-fw fa-floppy-o"}),"\xA0Save"),Hn.createElement(ii,{onClick:()=>location.replace("/flows/dump?filter="+n)},Hn.createElement("i",{className:"fa fa-fw fa-floppy-o"}),"\xA0Save filtered"),Hn.createElement(ii,{onClick:()=>confirm("Delete all flows?")&&t(W0())},Hn.createElement("i",{className:"fa fa-fw fa-trash"}),"\xA0Clear All"),Hn.createElement(Ro,null,Hn.createElement(hL,null),Hn.createElement("li",null,Hn.createElement("a",{href:"http://mitm.it/",target:"_blank"},Hn.createElement("i",{className:"fa fa-fw fa-external-link"}),"\xA0Install Certificates..."))))},"FileMenu"));var at=fe(Oe());function VP(e){if(navigator.clipboard&&window.isSecureContext)return navigator.clipboard.writeText(e);{let t=document.createElement("textarea");t.value=e,t.style.position="absolute",t.style.opacity="0",document.body.appendChild(t);try{return t.focus(),t.select(),document.execCommand("copy"),Promise.resolve()}catch(n){return alert(e),Promise.reject(n)}finally{t.remove()}}}o(VP,"copyToClipboard");var ed=o((e,t)=>Ia(void 0,null,function*(){let n=yield Pf("export",t,`@${e.id}`);n.value?yield VP(n.value):n.error?alert(n.error):console.error(n)}),"copy");td.title="Flow";function td(){let e=Xt(),t=qe(n=>n.flows.byId[n.flows.selected[0]]);return t?at.createElement("div",{className:"flow-menu"},at.createElement(Ro,null,at.createElement("div",{className:"menu-group"},at.createElement("div",{className:"menu-content"},at.createElement(kr,{title:"[r]eplay flow",icon:"fa-repeat text-primary",onClick:()=>e(Mp(t)),disabled:!E0(t)},"Replay"),at.createElement(kr,{title:"[D]uplicate flow",icon:"fa-copy text-info",onClick:()=>e(B0(t))},"Duplicate"),at.createElement(kr,{disabled:!t||!t.modified,title:"revert changes to flow [V]",icon:"fa-history text-warning",onClick:()=>e(H0(t))},"Revert"),at.createElement(kr,{title:"[d]elete flow",icon:"fa-trash text-danger",onClick:()=>e(F0(t))},"Delete"),at.createElement(VB,{flow:t})),at.createElement("div",{className:"menu-legend"},"Flow Modification"))),at.createElement("div",{className:"menu-group"},at.createElement("div",{className:"menu-content"},at.createElement($B,{flow:t}),at.createElement(jB,{flow:t})),at.createElement("div",{className:"menu-legend"},"Export")),at.createElement(Ro,null,at.createElement("div",{className:"menu-group"},at.createElement("div",{className:"menu-content"},at.createElement(kr,{disabled:!t||!t.intercepted,title:"[a]ccept intercepted flow",icon:"fa-play text-success",onClick:()=>e(Op(t))},"Resume"),at.createElement(kr,{disabled:!t||!t.intercepted,title:"kill intercepted flow [x]",icon:"fa-times text-danger",onClick:()=>e(I0(t))},"Abort")),at.createElement("div",{className:"menu-legend"},"Interception")))):at.createElement("div",null)}o(td,"FlowMenu");var Ay=o(e=>{let t=window.open(e,"_blank","noopener,noreferrer");t&&(t.opener=null)},"openInNewTab");function $B({flow:e}){var t;if(e.type!=="http")return at.createElement(kr,{icon:"fa-download",onClick:()=>0,disabled:!0},"Download");if(e.request.contentLength&&!((t=e.response)==null?void 0:t.contentLength))return at.createElement(kr,{icon:"fa-download",onClick:()=>Ay(Kr.getContentURL(e,e.request))},"Download");if(e.response){let n=e.response;if(!e.request.contentLength&&e.response.contentLength)return at.createElement(kr,{icon:"fa-download",onClick:()=>Ay(Kr.getContentURL(e,n))},"Download");if(e.request.contentLength&&e.response.contentLength)return at.createElement(cu,{text:at.createElement(kr,{icon:"fa-download",onClick:()=>1},"Download\u25BE"),options:{placement:"bottom-start"}},at.createElement(ii,{onClick:()=>Ay(Kr.getContentURL(e,e.request))},"Download request"),at.createElement(ii,{onClick:()=>Ay(Kr.getContentURL(e,n))},"Download response"))}return null}o($B,"DownloadButton");function jB({flow:e}){return at.createElement(cu,{className:"",text:at.createElement(kr,{title:"Export flow.",icon:"fa-clone",onClick:()=>1,disabled:e.type!=="http"},"Export\u25BE"),options:{placement:"bottom-start"}},at.createElement(ii,{onClick:()=>ed(e,"raw_request")},"Copy raw request"),at.createElement(ii,{onClick:()=>ed(e,"raw_response")},"Copy raw response"),at.createElement(ii,{onClick:()=>ed(e,"raw")},"Copy raw request and response"),at.createElement(ii,{onClick:()=>ed(e,"curl")},"Copy as cURL"),at.createElement(ii,{onClick:()=>ed(e,"httpie")},"Copy as HTTPie"))}o(jB,"ExportButton");var qB={":red_circle:":"\u{1F534}",":orange_circle:":"\u{1F7E0}",":yellow_circle:":"\u{1F7E1}",":green_circle:":"\u{1F7E2}",":large_blue_circle:":"\u{1F535}",":purple_circle:":"\u{1F7E3}",":brown_circle:":"\u{1F7E4}"};function VB({flow:e}){let t=Xt();return at.createElement(cu,{className:"",text:at.createElement(kr,{title:"mark flow",icon:"fa-paint-brush text-success",onClick:()=>1},"Mark\u25BE"),options:{placement:"bottom-start"}},at.createElement(ii,{onClick:()=>t(Wi(e,{marked:""}))},"\u26AA (no marker)"),Object.entries(qB).map(([n,l])=>at.createElement(ii,{key:n,onClick:()=>t(Wi(e,{marked:n}))},l," ",n.replace(/[:_]/g," "))))}o(VB,"MarkButton");var vu=fe(Oe());var KP=vu.memo(o(function(){let t=qe(l=>l.connection.state),n=qe(l=>l.connection.message);switch(t){case ni.INIT:return vu.createElement("span",{className:"connection-indicator init"},"connecting\u2026");case ni.FETCHING:return vu.createElement("span",{className:"connection-indicator fetching"},"fetching data\u2026");case ni.ESTABLISHED:return vu.createElement("span",{className:"connection-indicator established"},"connected");case ni.ERROR:return vu.createElement("span",{className:"connection-indicator error",title:n},"connection lost");case ni.OFFLINE:return vu.createElement("span",{className:"connection-indicator offline"},"offline");default:let l=t;throw"unknown connection state"}},"ConnectionIndicator"));function WC(){let e=qe(v=>v.flows.selected.filter(C=>C in v.flows.byId)),[t,n]=(0,Io.useState)(()=>Jp),[l,d]=(0,Io.useState)(!1),h=[Jp,My];e.length>0?(l||(n(()=>td),d(!0)),h.push(td)):(l&&d(!1),t===td&&n(()=>Jp));function c(v,C){C.preventDefault(),n(()=>v)}return o(c,"handleClick"),Io.default.createElement("header",null,Io.default.createElement("nav",{className:"nav-tabs nav-tabs-lg"},Io.default.createElement(qP,null),h.map(v=>Io.default.createElement("a",{key:v.title,href:"#",className:(0,GP.default)({active:v===t}),onClick:C=>c(v,C)},v.title)),Io.default.createElement(Ro,null,Io.default.createElement(KP,null))),Io.default.createElement("div",null,Io.default.createElement(t,null)))}o(WC,"Header");var Je=fe(Oe()),YP=fe(ti());var Dy=function(){"use strict";function e(l,d){function h(){this.constructor=l}o(h,"ctor"),h.prototype=d.prototype,l.prototype=new h}o(e,"peg$subclass");function t(l,d,h,c){this.message=l,this.expected=d,this.found=h,this.location=c,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,t)}o(t,"peg$SyntaxError"),e(t,Error);function n(l){var d=arguments.length>1?arguments[1]:{},h=this,c={},v={Expr:Cr},C=Cr,k=o(function(H,ee){return[H,...ee]},"peg$c0"),O=o(function(H){return[H]},"peg$c1"),j=o(function(){return""},"peg$c2"),B={type:"other",description:"string"},X='"',J={type:"literal",value:'"',description:'"\\""'},Z=o(function(H){return H.join("")},"peg$c6"),R="'",A={type:"literal",value:"'",description:`"'"`},I=/^["\\]/,G={type:"class",value:'["\\\\]',description:'["\\\\]'},K={type:"any",description:"any character"},se=o(function(H){return H},"peg$c12"),ne="\\",pe={type:"literal",value:"\\",description:'"\\\\"'},me=/^['\\]/,xe={type:"class",value:"['\\\\]",description:"['\\\\]"},Ve=/^['"\\]/,tt={type:"class",value:`['"\\\\]`,description:`['"\\\\]`},_e="n",St={type:"literal",value:"n",description:'"n"'},We=o(function(){return` -`},"peg$c21"),Ke="r",Ge={type:"literal",value:"r",description:'"r"'},Xe=o(function(){return"\r"},"peg$c24"),nr="t",ct={type:"literal",value:"t",description:'"t"'},Hr=o(function(){return" "},"peg$c27"),Qt={type:"other",description:"whitespace"},_t=/^[ \t\n\r]/,Ct={type:"class",value:"[ \\t\\n\\r]",description:"[ \\t\\n\\r]"},ut={type:"other",description:"control character"},Lr=/^[|&!()~"]/,zt={type:"class",value:'[|&!()~"]',description:'[|&!()~"]'},$t={type:"other",description:"optional whitespace"},ie=0,rt=0,Pr=[{line:1,column:1,seenCR:!1}],Gt=0,Yt=[],Se=0,Or;if("startRule"in d){if(!(d.startRule in v))throw new Error(`Can't start parsing from rule "`+d.startRule+'".');C=v[d.startRule]}function fn(){return l.substring(rt,ie)}o(fn,"text");function Un(){return gr(rt,ie)}o(Un,"location");function si(H){throw Ho(null,[{type:"other",description:H}],l.substring(rt,ie),gr(rt,ie))}o(si,"expected");function cn(H){throw Ho(H,null,l.substring(rt,ie),gr(rt,ie))}o(cn,"error");function Zt(H){var ee=Pr[H],he,Te;if(ee)return ee;for(he=H-1;!Pr[he];)he--;for(ee=Pr[he],ee={line:ee.line,column:ee.column,seenCR:ee.seenCR};heGt&&(Gt=ie,Yt=[]),Yt.push(H))}o(pt,"peg$fail");function Ho(H,ee,he,Te){function ir(Ft){var Wr=1;for(Ft.sort(function(or,li){return or.descriptionli.description?1:0});Wr1?li.slice(0,-1).join(", ")+" or "+li[Ft.length-1]:li[0],lo=Wr?'"'+or(Wr)+'"':"end of input","Expected "+ds+" but "+lo+" found."}return o(Ul,"buildMessage"),ee!==null&&ir(ee),new t(H!==null?H:Ul(ee,he),ee,he,Te)}o(Ho,"peg$buildException");function Cr(){var H,ee,he,Te;if(H=ie,ee=Ui(),ee!==c){if(he=[],Te=$n(),Te!==c)for(;Te!==c;)he.push(Te),Te=$n();else he=c;he!==c?(Te=Cr(),Te!==c?(rt=H,ee=k(ee,Te),H=ee):(ie=H,H=c)):(ie=H,H=c)}else ie=H,H=c;if(H===c&&(H=ie,ee=Ui(),ee!==c&&(rt=H,ee=O(ee)),H=ee,H===c)){for(H=ie,ee=[],he=$n();he!==c;)ee.push(he),he=$n();ee!==c&&(rt=H,ee=j()),H=ee}return H}o(Cr,"peg$parseExpr");function Ui(){var H,ee,he,Te;if(Se++,H=ie,l.charCodeAt(ie)===34?(ee=X,ie++):(ee=c,Se===0&&pt(J)),ee!==c){for(he=[],Te=pn();Te!==c;)he.push(Te),Te=pn();he!==c?(l.charCodeAt(ie)===34?(Te=X,ie++):(Te=c,Se===0&&pt(J)),Te!==c?(rt=H,ee=Z(he),H=ee):(ie=H,H=c)):(ie=H,H=c)}else ie=H,H=c;if(H===c){if(H=ie,l.charCodeAt(ie)===39?(ee=R,ie++):(ee=c,Se===0&&pt(A)),ee!==c){for(he=[],Te=zn();Te!==c;)he.push(Te),Te=zn();he!==c?(l.charCodeAt(ie)===39?(Te=R,ie++):(Te=c,Se===0&&pt(A)),Te!==c?(rt=H,ee=Z(he),H=ee):(ie=H,H=c)):(ie=H,H=c)}else ie=H,H=c;if(H===c){if(H=ie,ee=ie,Se++,he=Mn(),Se--,he===c?ee=void 0:(ie=ee,ee=c),ee!==c){if(he=[],Te=Si(),Te!==c)for(;Te!==c;)he.push(Te),Te=Si();else he=c;he!==c?(rt=H,ee=Z(he),H=ee):(ie=H,H=c)}else ie=H,H=c;if(H===c){if(H=ie,l.charCodeAt(ie)===34?(ee=X,ie++):(ee=c,Se===0&&pt(J)),ee!==c){for(he=[],Te=pn();Te!==c;)he.push(Te),Te=pn();he!==c?(rt=H,ee=Z(he),H=ee):(ie=H,H=c)}else ie=H,H=c;if(H===c)if(H=ie,l.charCodeAt(ie)===39?(ee=R,ie++):(ee=c,Se===0&&pt(A)),ee!==c){for(he=[],Te=zn();Te!==c;)he.push(Te),Te=zn();he!==c?(rt=H,ee=Z(he),H=ee):(ie=H,H=c)}else ie=H,H=c}}}return Se--,H===c&&(ee=c,Se===0&&pt(B)),H}o(Ui,"peg$parseStringLiteral");function pn(){var H,ee,he;return H=ie,ee=ie,Se++,I.test(l.charAt(ie))?(he=l.charAt(ie),ie++):(he=c,Se===0&&pt(G)),Se--,he===c?ee=void 0:(ie=ee,ee=c),ee!==c?(l.length>ie?(he=l.charAt(ie),ie++):(he=c,Se===0&&pt(K)),he!==c?(rt=H,ee=se(he),H=ee):(ie=H,H=c)):(ie=H,H=c),H===c&&(H=ie,l.charCodeAt(ie)===92?(ee=ne,ie++):(ee=c,Se===0&&pt(pe)),ee!==c?(he=Ci(),he!==c?(rt=H,ee=se(he),H=ee):(ie=H,H=c)):(ie=H,H=c)),H}o(pn,"peg$parseDoubleStringChar");function zn(){var H,ee,he;return H=ie,ee=ie,Se++,me.test(l.charAt(ie))?(he=l.charAt(ie),ie++):(he=c,Se===0&&pt(xe)),Se--,he===c?ee=void 0:(ie=ee,ee=c),ee!==c?(l.length>ie?(he=l.charAt(ie),ie++):(he=c,Se===0&&pt(K)),he!==c?(rt=H,ee=se(he),H=ee):(ie=H,H=c)):(ie=H,H=c),H===c&&(H=ie,l.charCodeAt(ie)===92?(ee=ne,ie++):(ee=c,Se===0&&pt(pe)),ee!==c?(he=Ci(),he!==c?(rt=H,ee=se(he),H=ee):(ie=H,H=c)):(ie=H,H=c)),H}o(zn,"peg$parseSingleStringChar");function Si(){var H,ee,he;return H=ie,ee=ie,Se++,he=$n(),Se--,he===c?ee=void 0:(ie=ee,ee=c),ee!==c?(l.length>ie?(he=l.charAt(ie),ie++):(he=c,Se===0&&pt(K)),he!==c?(rt=H,ee=se(he),H=ee):(ie=H,H=c)):(ie=H,H=c),H}o(Si,"peg$parseUnquotedStringChar");function Ci(){var H,ee;return Ve.test(l.charAt(ie))?(H=l.charAt(ie),ie++):(H=c,Se===0&&pt(tt)),H===c&&(H=ie,l.charCodeAt(ie)===110?(ee=_e,ie++):(ee=c,Se===0&&pt(St)),ee!==c&&(rt=H,ee=We()),H=ee,H===c&&(H=ie,l.charCodeAt(ie)===114?(ee=Ke,ie++):(ee=c,Se===0&&pt(Ge)),ee!==c&&(rt=H,ee=Xe()),H=ee,H===c&&(H=ie,l.charCodeAt(ie)===116?(ee=nr,ie++):(ee=c,Se===0&&pt(ct)),ee!==c&&(rt=H,ee=Hr()),H=ee))),H}o(Ci,"peg$parseEscapeSequence");function $n(){var H,ee;return Se++,_t.test(l.charAt(ie))?(H=l.charAt(ie),ie++):(H=c,Se===0&&pt(Ct)),Se--,H===c&&(ee=c,Se===0&&pt(Qt)),H}o($n,"peg$parsews");function Mn(){var H,ee;return Se++,Lr.test(l.charAt(ie))?(H=l.charAt(ie),ie++):(H=c,Se===0&&pt(zt)),Se--,H===c&&(ee=c,Se===0&&pt(ut)),H}o(Mn,"peg$parsecc");function Js(){var H,ee;for(Se++,H=[],ee=$n();ee!==c;)H.push(ee),ee=$n();return Se--,H===c&&(ee=c,Se===0&&pt($t)),H}if(o(Js,"peg$parse__"),Or=C(),Or!==c&&ie===l.length)return Or;throw Or!==c&&ie{t&&t.current.addEventListener("DOMNodeInserted",n=>{let l=n.currentTarget;l.scroll({top:l.scrollHeight,behavior:"auto"})})},[]),Je.default.createElement("div",{className:"command-result",ref:t},e.map((n,l)=>Je.default.createElement("div",{key:l},Je.default.createElement("div",null,Je.default.createElement("strong",null,"$ ",n.command)),n.result)))}o(KB,"Results");function GB({nextArgs:e,currentArg:t,help:n,description:l,availableCommands:d}){let h=[];for(let c=0;c0&&Je.default.createElement("div",null,Je.default.createElement("strong",null,"Argument suggestion:")," ",h),(n==null?void 0:n.includes("->"))&&Je.default.createElement("div",null,Je.default.createElement("strong",null,"Signature help: "),n),l&&Je.default.createElement("div",null,"# ",l),Je.default.createElement("div",null,Je.default.createElement("strong",null,"Available Commands: "),Je.default.createElement("p",{className:"available-commands"},JSON.stringify(d)))))}o(GB,"CommandHelp");function zC(){let[e,t]=(0,Je.useState)(""),[n,l]=(0,Je.useState)(""),[d,h]=(0,Je.useState)(0),[c,v]=(0,Je.useState)([]),[C,k]=(0,Je.useState)([]),[O,j]=(0,Je.useState)({}),[B,X]=(0,Je.useState)([]),[J,Z]=(0,Je.useState)(0),[R,A]=(0,Je.useState)(""),[I,G]=(0,Je.useState)(""),[K,se]=(0,Je.useState)([]),[ne,pe]=(0,Je.useState)([]),[me,xe]=(0,Je.useState)(void 0);(0,Je.useEffect)(()=>{kt("/commands",{method:"GET"}).then(We=>We.json()).then(We=>{j(We),v(UC(We)),k(Object.keys(We))}).catch(We=>console.error(We))},[]),(0,Je.useEffect)(()=>{Pf("commands.history.get").then(We=>{pe(We.value)}).catch(We=>console.error(We))},[]);let Ve=o((We,Ke)=>{var ct,Hr,Qt;let Ge=Dy.parse(Ke),Xe=Dy.parse(We);A((ct=O[Ge[0]])==null?void 0:ct.signature_help),G(((Hr=O[Ge[0]])==null?void 0:Hr.help)||""),v(UC(O,Xe[0])),k(UC(O,Ge[0]));let nr=(Qt=O[Ge[0]])==null?void 0:Qt.parameters.map(_t=>_t.name);nr&&(X([Ge[0],...nr]),Z(Ge.length-1))},"parseCommand"),tt=o(We=>{t(We.target.value),l(We.target.value),h(0)},"onChange"),_e=o(We=>{if(We.key==="Enter"){let[Ke,...Ge]=Dy.parse(e);pe([...ne,e]),Pf("commands.history.add",e).catch(()=>0),kt.post(`/commands/${Ke}`,{arguments:Ge}).then(Xe=>Xe.json()).then(Xe=>{xe(void 0),X([]),se([...K,{command:e,result:JSON.stringify(Xe.value||Xe.error)}])}).catch(Xe=>{xe(void 0),X([]),se([...K,{command:e,result:Xe.toString()}])}),A(""),G(""),t(""),l(""),h(0),v(C)}if(We.key==="ArrowUp"){let Ke;me===void 0?Ke=ne.length-1:Ke=Math.max(0,me-1),t(ne[Ke]),l(ne[Ke]),xe(Ke)}if(We.key==="ArrowDown"){if(me===void 0)return;if(me==ne.length-1)t(""),l(""),xe(void 0);else{let Ke=me+1;t(ne[Ke]),l(ne[Ke]),xe(Ke)}}We.key==="Tab"&&(t(c[d]),h((d+1)%c.length),We.preventDefault()),We.stopPropagation()},"onKeyDown"),St=o(We=>{if(!e){k(Object.keys(O));return}Ve(n,e),We.stopPropagation()},"onKeyUp");return Je.default.createElement("div",{className:"command"},Je.default.createElement("div",{className:"command-title"},"Command Result"),Je.default.createElement(KB,{results:K}),Je.default.createElement(GB,{nextArgs:B,currentArg:J,help:R,description:I,availableCommands:C}),Je.default.createElement("div",{className:(0,YP.default)("command-input input-group")},Je.default.createElement("span",{className:"input-group-addon"},Je.default.createElement("i",{className:"fa fa-fw fa-terminal"})),Je.default.createElement("input",{type:"text",placeholder:"Enter command",className:"form-control",value:e||"",onChange:tt,onKeyDown:_e,onKeyUp:St})))}o(zC,"CommandBar");var Wl=fe(Oe()),rd=fe(Sm());var $C=fe(Oe());function jC({checked:e,onToggle:t,text:n}){return $C.default.createElement("div",{className:"btn btn-toggle "+(e?"btn-primary":"btn-default"),onClick:t},$C.default.createElement("i",{className:"fa fa-fw "+(e?"fa-check-square-o":"fa-square-o")}),"\xA0",n)}o(jC,"ToggleButton");var Hl=fe(Oe()),qC=fe(Sm()),XP=fe(iu()),QP=fe(fC());var Am=class extends Hl.Component{constructor(t){super(t);this.heights={},this.state={vScroll:jf()},this.onViewportUpdate=this.onViewportUpdate.bind(this)}componentDidMount(){window.addEventListener("resize",this.onViewportUpdate),this.onViewportUpdate()}componentWillUnmount(){window.removeEventListener("resize",this.onViewportUpdate)}componentDidUpdate(){this.onViewportUpdate()}onViewportUpdate(){let t=XP.default.findDOMNode(this),n=jf({itemCount:this.props.events.length,rowHeight:this.props.rowHeight,viewportTop:t.scrollTop,viewportHeight:t.offsetHeight,itemHeights:this.props.events.map(l=>this.heights[l.id])});(0,QP.default)(this.state.vScroll,n)||this.setState({vScroll:n})}setHeight(t,n){if(n&&!this.heights[t]){let l=n.offsetHeight;this.heights[t]!==l&&(this.heights[t]=l,this.onViewportUpdate())}}render(){let{vScroll:t}=this.state,{events:n}=this.props;return Hl.default.createElement("pre",{onScroll:this.onViewportUpdate},Hl.default.createElement("div",{style:{height:t.paddingTop}}),n.slice(t.start,t.end).map(l=>Hl.default.createElement("div",{key:l.id,ref:d=>this.setHeight(l.id,d)},Hl.default.createElement(YB,{event:l}),l.message)),Hl.default.createElement("div",{style:{height:t.paddingBottom}}))}};o(Am,"EventLogList"),Am.propTypes={events:qC.default.array.isRequired,rowHeight:qC.default.number},Am.defaultProps={rowHeight:18};function YB({event:e}){let t={web:"html5",debug:"bug",warn:"exclamation-triangle",error:"ban"}[e.level]||"info";return Hl.default.createElement("i",{className:`fa fa-fw fa-${t}`})}o(YB,"LogIcon");var ZP=my(Am);var Dm=class extends Wl.Component{constructor(t,n){super(t,n);this.state={height:this.props.defaultHeight},this.onDragStart=this.onDragStart.bind(this),this.onDragMove=this.onDragMove.bind(this),this.onDragStop=this.onDragStop.bind(this)}onDragStart(t){t.preventDefault(),this.dragStart=this.state.height+t.pageY,window.addEventListener("mousemove",this.onDragMove),window.addEventListener("mouseup",this.onDragStop),window.addEventListener("dragend",this.onDragStop)}onDragMove(t){t.preventDefault(),this.setState({height:this.dragStart-t.pageY})}onDragStop(t){t.preventDefault(),window.removeEventListener("mousemove",this.onDragMove)}render(){let{height:t}=this.state,{filters:n,events:l,toggleFilter:d,close:h}=this.props;return Wl.default.createElement("div",{className:"eventlog",style:{height:t}},Wl.default.createElement("div",{onMouseDown:this.onDragStart},"Eventlog",Wl.default.createElement("div",{className:"pull-right"},["debug","info","web","warn","error"].map(c=>Wl.default.createElement(jC,{key:c,text:c,checked:n[c],onToggle:()=>d(c)})),Wl.default.createElement("i",{onClick:h,className:"fa fa-close"}))),Wl.default.createElement(ZP,{events:l}))}};o(Dm,"PureEventLog"),Vc(Dm,"propTypes",{filters:rd.default.object.isRequired,events:rd.default.array.isRequired,toggleFilter:rd.default.func.isRequired,close:rd.default.func.isRequired,defaultHeight:rd.default.number}),Vc(Dm,"defaultProps",{defaultHeight:200});var JP=Hi(e=>({filters:e.eventLog.filters,events:e.eventLog.view}),{close:Rp,toggleFilter:gN})(Dm);var un=fe(Oe());function VC(){let e=qe(A=>A.backendState.version),{mode:t,intercept:n,showhost:l,upstream_cert:d,rawtcp:h,http2:c,websocket:v,anticache:C,anticomp:k,stickyauth:O,stickycookie:j,stream_large_bodies:B,listen_host:X,listen_port:J,server:Z,ssl_insecure:R}=qe(A=>A.options);return un.createElement("footer",null,t&&(t.length!==1||t[0]!=="regular")&&un.createElement("span",{className:"label label-success"},t.join(",")),n&&un.createElement("span",{className:"label label-success"},"Intercept: ",n),R&&un.createElement("span",{className:"label label-danger"},"ssl_insecure"),l&&un.createElement("span",{className:"label label-success"},"showhost"),!d&&un.createElement("span",{className:"label label-success"},"no-upstream-cert"),!h&&un.createElement("span",{className:"label label-success"},"no-raw-tcp"),!c&&un.createElement("span",{className:"label label-success"},"no-http2"),!v&&un.createElement("span",{className:"label label-success"},"no-websocket"),C&&un.createElement("span",{className:"label label-success"},"anticache"),k&&un.createElement("span",{className:"label label-success"},"anticomp"),O&&un.createElement("span",{className:"label label-success"},"stickyauth: ",O),j&&un.createElement("span",{className:"label label-success"},"stickycookie: ",j),B&&un.createElement("span",{className:"label label-success"},"stream: ",x0(B)),un.createElement("div",{className:"pull-right"},un.createElement(Ro,null,Z&&un.createElement("span",{className:"label label-primary",title:"HTTP Proxy Server Address"},X||"*",":",J||8080)),un.createElement("span",{className:"label label-default",title:"Mitmproxy Version"},"mitmproxy ",e)))}o(VC,"Footer");var JC=fe(Oe());var ZC=fe(Oe());var nd=fe(Oe());function KC({children:e}){return nd.createElement("div",null,nd.createElement("div",{className:"modal-backdrop fade in"}),nd.createElement("div",{className:"modal modal-visible",id:"optionsModal",tabIndex:-1,role:"dialog","aria-labelledby":"options"},nd.createElement("div",{className:"modal-dialog modal-lg",role:"document"},nd.createElement("div",{className:"modal-content"},e))))}o(KC,"ModalLayout");var mr=fe(Oe());var Fo=fe(Oe()),Bo=fe(Sm());var eO=fe(ti()),XB=o(e=>{e.key!=="Escape"&&e.stopPropagation()},"stopPropagation");GC.propTypes={value:Bo.default.bool.isRequired,onChange:Bo.default.func.isRequired};function GC(l){var d=l,{value:e,onChange:t}=d,n=Ws(d,["value","onChange"]);return Fo.default.createElement("div",{className:"checkbox"},Fo.default.createElement("label",null,Fo.default.createElement("input",ke({type:"checkbox",checked:e,onChange:h=>t(h.target.checked)},n)),"Enable"))}o(GC,"BooleanOption");YC.propTypes={value:Bo.default.string,onChange:Bo.default.func.isRequired};function YC(l){var d=l,{value:e,onChange:t}=d,n=Ws(d,["value","onChange"]);return Fo.default.createElement("input",ke({type:"text",value:e||"",onChange:h=>t(h.target.value)},n))}o(YC,"StringOption");function tO(e){return function(l){var d=l,{onChange:t}=d,n=Ws(d,["onChange"]);return Fo.default.createElement(e,ke({onChange:h=>t(h||null)},n))}}o(tO,"Optional");XC.propTypes={value:Bo.default.number.isRequired,onChange:Bo.default.func.isRequired};function XC(l){var d=l,{value:e,onChange:t}=d,n=Ws(d,["value","onChange"]);return Fo.default.createElement("input",ke({type:"number",value:e,onChange:h=>t(parseInt(h.target.value))},n))}o(XC,"NumberOption");rO.propTypes={value:Bo.default.string.isRequired,onChange:Bo.default.func.isRequired};function rO(d){var h=d,{value:e,onChange:t,choices:n}=h,l=Ws(h,["value","onChange","choices"]);return Fo.default.createElement("select",ke({onChange:c=>t(c.target.value),value:e},l),n.map(c=>Fo.default.createElement("option",{key:c,value:c},c)))}o(rO,"ChoicesOption");nO.propTypes={value:Bo.default.arrayOf(Bo.default.string).isRequired,onChange:Bo.default.func.isRequired};function nO(l){var d=l,{value:e,onChange:t}=d,n=Ws(d,["value","onChange"]);let h=Math.max(e.length,1);return Fo.default.createElement("textarea",ke({rows:h,value:e.join(` -`),onChange:c=>t(c.target.value.split(` -`))},n))}o(nO,"StringSequenceOption");var QB={bool:GC,str:YC,int:XC,"optional str":tO(YC),"optional int":tO(XC),"sequence of str":nO};function ZB({choices:e,type:t,value:n,onChange:l,name:d,error:h}){let c,v={};if(e)c=rO,v.choices=e;else if(c=QB[t],!c)throw`unknown option type ${t}`;return c!==GC&&(v.className="form-control"),Fo.default.createElement("div",{className:(0,eO.default)({"has-error":h})},Fo.default.createElement(c,ke({name:d,value:n,onChange:l,onKeyDown:XB},v)))}o(ZB,"PureOption");var iO=Hi((e,{name:t})=>ke(ke({},e.options_meta[t]),e.ui.optionsEditor[t]),(e,{name:t})=>({onChange:n=>e(Ip(t,n))}))(ZB);var Ry=fe(Qh());function JB({help:e}){return mr.default.createElement("div",{className:"help-block small"},e)}o(JB,"PureOptionHelp");var e4=Hi((e,{name:t})=>({help:e.options_meta[t].help}))(JB);function t4({error:e}){return e?mr.default.createElement("div",{className:"small text-danger"},e):null}o(t4,"PureOptionError");var r4=Hi((e,{name:t})=>({error:e.ui.optionsEditor[t]&&e.ui.optionsEditor[t].error}))(t4);function n4({value:e,defaultVal:t}){if(e===t)return null;if(typeof t=="boolean")t=t?"true":"false";else if(Array.isArray(t)){if(Ry.default.isEmpty(Ry.default.compact(e))&&Ry.default.isEmpty(t))return null;t="[ ]"}else t===""?t='""':t===null&&(t="null");return mr.default.createElement("div",{className:"small"},"Default: ",mr.default.createElement("strong",null," ",t," ")," ")}o(n4,"PureOptionDefault");var i4=Hi((e,{name:t})=>({value:e.options[t],defaultVal:e.options_meta[t].default}))(n4),QC=class extends mr.Component{constructor(t,n){super(t,n);this.state={title:"Options"}}componentWillUnmount(){}render(){let{hideModal:t,options:n}=this.props,{title:l}=this.state;return mr.default.createElement("div",null,mr.default.createElement("div",{className:"modal-header"},mr.default.createElement("button",{type:"button",className:"close","data-dismiss":"modal",onClick:()=>{t()}},mr.default.createElement("i",{className:"fa fa-fw fa-times"})),mr.default.createElement("div",{className:"modal-title"},mr.default.createElement("h4",null,l))),mr.default.createElement("div",{className:"modal-body"},mr.default.createElement("div",{className:"form-horizontal"},n.map(d=>mr.default.createElement("div",{key:d,className:"form-group"},mr.default.createElement("div",{className:"col-xs-6"},mr.default.createElement("label",{htmlFor:d},d),mr.default.createElement(e4,{name:d})),mr.default.createElement("div",{className:"col-xs-6"},mr.default.createElement(iO,{name:d}),mr.default.createElement(r4,{name:d}),mr.default.createElement(i4,{name:d})))))),mr.default.createElement("div",{className:"modal-footer"}))}};o(QC,"PureOptionModal");var oO=Hi(e=>({options:Object.keys(e.options_meta).sort()}),{hideModal:z0,save:LN})(QC);function o4(){return ZC.createElement(KC,null,ZC.createElement(oO,null))}o(o4,"OptionModal");var sO=[o4];function eb(){let e=qe(n=>n.ui.modal.activeModal),t=sO.find(n=>n.name===e);return e&&t!==void 0?JC.createElement(t,null):JC.createElement("div",null)}o(eb,"PureModal");var tb=class extends Wn.Component{constructor(){super(...arguments);this.state={};this.render=o(()=>{var l;let{showEventLog:t,showCommandBar:n}=this.props;return this.state.error?(console.log("ERR",this.state),Wn.default.createElement("div",{className:"container"},Wn.default.createElement("h1",null,"mitmproxy has crashed."),Wn.default.createElement("pre",null,this.state.error.stack,Wn.default.createElement("br",null),Wn.default.createElement("br",null),"Component Stack:",(l=this.state.errorInfo)==null?void 0:l.componentStack),Wn.default.createElement("p",null,"Please lodge a bug report at ",Wn.default.createElement("a",{href:"https://github.com/mitmproxy/mitmproxy/issues"},"https://github.com/mitmproxy/mitmproxy/issues"),"."))):Wn.default.createElement("div",{id:"container",tabIndex:0},Wn.default.createElement(WC,null),Wn.default.createElement(IC,null),n&&Wn.default.createElement(zC,{key:"commandbar"}),t&&Wn.default.createElement(JP,{key:"eventlog"}),Wn.default.createElement(VC,null),Wn.default.createElement(eb,null))},"render")}componentDidMount(){window.addEventListener("keydown",this.props.onKeyDown)}componentWillUnmount(){window.removeEventListener("keydown",this.props.onKeyDown)}componentDidCatch(t,n){this.setState({error:t,errorInfo:n})}};o(tb,"ProxyAppMain");var lO=Hi(e=>({showEventLog:e.eventLog.visible,showCommandBar:e.commandBar.visible}),{onKeyDown:SL})(tb);var yu={SEARCH:"s",HIGHLIGHT:"h",SHOW_EVENTLOG:"e",SHOW_COMMANDBAR:"c"};function s4(e){let[t,n]=window.location.hash.substr(1).split("?",2),l=t.substr(1).split("/");if(l[0]==="flows"&&l.length==3){let[d,h]=l.slice(1);e.dispatch(Af(d)),e.dispatch(Lf(h))}n&&n.split("&").forEach(d=>{let[h,c]=d.split("=",2);switch(c=decodeURIComponent(c),h){case yu.SEARCH:e.dispatch(A0(c));break;case yu.HIGHLIGHT:e.dispatch(D0(c));break;case yu.SHOW_EVENTLOG:e.getState().eventLog.visible||e.dispatch(Rp());break;case yu.SHOW_COMMANDBAR:e.getState().commandBar.visible||e.dispatch(V0());break;default:console.error(`unimplemented query arg: ${d}`)}})}o(s4,"updateStoreFromUrl");function l4(e){let t=e.getState(),n={[yu.SEARCH]:t.flows.filter,[yu.HIGHLIGHT]:t.flows.highlight,[yu.SHOW_EVENTLOG]:t.eventLog.visible,[yu.SHOW_COMMANDBAR]:t.commandBar.visible},l=Object.keys(n).filter(c=>n[c]).map(c=>`${c}=${encodeURIComponent(n[c])}`).join("&"),d;t.flows.selected.length>0?d=`/flows/${t.flows.selected[0]}/${t.ui.flow.tab}`:d="/flows",l&&(d+="?"+l);let h=window.location.pathname;h==="blank"&&(h="/"),window.location.hash.substr(1)!==d&&history.replaceState(void 0,"",`${h}#${d}`)}o(l4,"updateUrlFromStore");function rb(e){s4(e),e.subscribe(()=>l4(e))}o(rb,"initialize");var a4="reset",Rm=class{constructor(t){this.activeFetches={},this.store=t,this.connect()}connect(){this.socket=new WebSocket(location.origin.replace("http","ws")+"/updates"),this.socket.addEventListener("open",()=>this.onOpen()),this.socket.addEventListener("close",t=>this.onClose(t)),this.socket.addEventListener("message",t=>this.onMessage(JSON.parse(t.data))),this.socket.addEventListener("error",t=>this.onError(t))}onOpen(){this.fetchData("state"),this.fetchData("flows"),this.fetchData("events"),this.fetchData("options"),this.store.dispatch(_N())}fetchData(t){let n=[];this.activeFetches[t]=n,kt(`./${t}`).then(l=>l.json()).then(l=>{this.activeFetches[t]===n&&this.receive(t,l)})}onMessage(t){if(t.cmd===a4)return this.fetchData(t.resource);if(t.resource in this.activeFetches)this.activeFetches[t.resource].push(t);else{let n=`${t.resource}_${t.cmd}`.toUpperCase();this.store.dispatch(ke({type:n},t))}}receive(t,n){let l=`${t}_RECEIVE`.toUpperCase();this.store.dispatch({type:l,cmd:"receive",resource:t,data:n});let d=this.activeFetches[t];delete this.activeFetches[t],d.forEach(h=>this.onMessage(h)),Object.keys(this.activeFetches).length===0&&this.store.dispatch(TN())}onClose(t){this.store.dispatch(kN(`Connection closed at ${new Date().toUTCString()} with error code ${t.code}.`)),console.error("websocket connection closed",t)}onError(t){console.error("websocket connection errored",arguments)}};o(Rm,"WebsocketBackend");var Im=class{constructor(t){this.store=t,this.onOpen()}onOpen(){this.fetchData("flows"),this.fetchData("options")}fetchData(t){kt(`./${t}`).then(n=>n.json()).then(n=>{this.receive(t,n)})}receive(t,n){let l=`${t}_RECEIVE`.toUpperCase();this.store.dispatch({type:l,cmd:"receive",resource:t,data:n})}};o(Im,"StaticBackend");rb(Fp);window.MITMWEB_STATIC?window.backend=new Im(Fp):window.backend=new Rm(Fp);window.addEventListener("error",e=>{Fp.dispatch(vN(`${e.message} -${e.error.stack}`))});document.addEventListener("DOMContentLoaded",()=>{(0,aO.render)(nb.createElement(Ux,{store:Fp},nb.createElement(lO,null)),document.getElementById("mitmproxy"))});})(); -/* -object-assign -(c) Sindre Sorhus -@license MIT -*/ -/*! - Copyright (c) 2018 Jed Watson. - Licensed under the MIT License (MIT), see - http://jedwatson.github.io/classnames +`),st}Dy(()=>{Gt.current=void 0,Wt.current=void 0,dn.current=Ue});let Gr=Tt.useMemo(()=>Tt.createElement(ne,{...Ue,ref:Re}),[Re,ne,Ue]);return Tt.useMemo(()=>j?Tt.createElement(Ye.Provider,{value:fn},Gr):Gr,[Ye,Gr,fn])}i(q,"ConnectFunction");let Z=Tt.memo(q);if(Z.WrappedComponent=ne,Z.displayName=q.displayName=L,S){let he=Tt.forwardRef(i(function(Ee,Ye){return Tt.createElement(Z,{...Ee,reactReduxForwardedRef:Ye})},"forwardConnectRef"));return he.displayName=L,he.WrappedComponent=ne,_x(he,ne)}return _x(Z,ne)},"wrapWithConnect")}i(Z4,"connect");var Wu=Z4;function eB({store:e,context:t,children:n,serverState:s,stabilityCheck:l="once",identityFunctionCheck:h="once"}){let d=Tt.useMemo(()=>{let b=_P(e);return{store:e,subscription:b,getServerState:s?()=>s:void 0,stabilityCheck:l,identityFunctionCheck:h}},[e,s,l,h]),v=Tt.useMemo(()=>e.getState(),[e]);Dy(()=>{let{subscription:b}=d;return b.onStateChange=b.notifyNestedSubs,b.trySubscribe(),v!==e.getState()&&b.notifyNestedSubs(),()=>{b.tryUnsubscribe(),b.onStateChange=void 0}},[d,v]);let S=t||gl;return Tt.createElement(S.Provider,{value:d},n)}i(eB,"Provider");var TP=eB;function kP(e=gl){let t=e===gl?yP:Ex(e),n=i(()=>{let{store:s}=t();return s},"useStore2");return Object.assign(n,{withTypes:i(()=>n,"withTypes")}),n}i(kP,"createStoreHook");var tB=kP();function rB(e=gl){let t=e===gl?tB:kP(e),n=i(()=>t().dispatch,"useDispatch2");return Object.assign(n,{withTypes:i(()=>n,"withTypes")}),n}i(rB,"createDispatchHook");var NP=rB();u4(gP.useSyncExternalStoreWithSelector);K4(mP.useSyncExternalStore);var Jn=de(be());var PP="UI_FLOWVIEW_SET_TAB",AP="SET_CONTENT_VIEW_FOR",nB={tab:"request",contentViewFor:{}};function kx(e=nB,t){switch(t.type){case AP:return{...e,contentViewFor:{...e.contentViewFor,[t.messageId]:t.contentView}};case PP:return{...e,tab:t.tab?t.tab:"request"};default:return e}}i(kx,"reducer");function mf(e){return{type:PP,tab:e}}i(mf,"selectTab");function Iy(e,t){return{type:AP,messageId:e,contentView:t}}i(Iy,"setContentViewFor");var iB=de(Tm()),oB=de(be());window.React=oB;var Fy=i(function(e){if(e===0)return"0";let t=["b","kb","mb","gb","tb"],n=0;for(;ne);n++);let s;return e%Math.pow(1024,n)===0?s=0:s=1,(e/Math.pow(1024,n)).toFixed(s)+t[n]},"formatSize"),By=i(function(e){let t=e,n=["ms","s","min","h"],s=[1e3,60,60],l=0;for(;Math.abs(t)>=s[l]&&lxt(e,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(t),...n});xt.post=(e,t,n={})=>xt(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t),...n});async function gf(e,...t){return await(await xt(`/commands/${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({arguments:t})})).json()}i(gf,"runCommand");async function LP(e){try{await navigator.clipboard.write([new ClipboardItem({"text/plain":e})]);return}catch(s){console.warn(s)}let t=await e;try{await navigator.clipboard.writeText(t);return}catch(s){console.warn(s)}let n=document.createElement("textarea");n.value=t,n.style.position="absolute",n.style.opacity="0",document.body.appendChild(n);try{if(n.focus(),n.select(),!document.execCommand("copy"))throw"failed to copy"}catch{alert(t)}finally{n.remove()}}i(LP,"copyToClipboard");function km(e,t){let n=e.lastIndexOf(t);if(n===-1)return["",e];let s=e.slice(0,n),l=e.slice(n+t.length);return[s,l]}i(km,"rpartition");function Hy(e,t){let n=e.indexOf(t);if(n===-1)return[e,""];let s=e.slice(0,n),l=e.slice(n+t.length);return[s,l]}i(Hy,"partition");var Am={};_b(Am,{ADD:()=>Dx,RECEIVE:()=>Bx,REMOVE:()=>Fx,SET_FILTER:()=>Ox,SET_SORT:()=>Rx,UPDATE:()=>Ix,add:()=>lB,defaultState:()=>ap,receive:()=>fB,reduce:()=>$u,remove:()=>cB,setFilter:()=>Nm,setSort:()=>Hx,update:()=>uB});var Mx=de(MP());var Ox="LIST_SET_FILTER",Rx="LIST_SET_SORT",Dx="LIST_ADD",Ix="LIST_UPDATE",Fx="LIST_REMOVE",Bx="LIST_RECEIVE",ap={byId:{},list:[],listIndex:{},view:[],viewIndex:{}};function $u(e=ap,t){let{byId:n,list:s,listIndex:l,view:h,viewIndex:d}=e;switch(t.type){case Ox:h=(0,Mx.default)(s.filter(t.filter),t.sort),d={},h.forEach((v,S)=>{d[v.id]=S});break;case Rx:h=(0,Mx.default)([...h],t.sort),d={},h.forEach((v,S)=>{d[v.id]=S});break;case Dx:if(t.item.id in n)break;n={...n,[t.item.id]:t.item},l={...l,[t.item.id]:s.length},s=[...s,t.item],t.filter(t.item)&&({view:h,viewIndex:d}=OP(e,t.item,t.sort));break;case Ix:{n={...n,[t.item.id]:t.item},s=[...s],s[l[t.item.id]]=t.item;let v=t.item.id in d,S=t.filter(t.item);S&&!v?{view:h,viewIndex:d}=OP(e,t.item,t.sort):!S&&v?{data:h,dataIndex:d}=Lx(h,d,t.item.id):S&&v&&({view:h,viewIndex:d}=dB(e,t.item,t.sort));break}case Fx:if(!(t.id in n))break;n={...n},delete n[t.id],{data:s,dataIndex:l}=Lx(s,l,t.id),t.id in d&&({data:h,dataIndex:d}=Lx(h,d,t.id));break;case Bx:s=t.list,l={},n={},s.forEach((v,S)=>{n[v.id]=v,l[v.id]=S}),h=s.filter(t.filter).sort(t.sort),d={},h.forEach((v,S)=>{d[v.id]=S});break}return{byId:n,list:s,listIndex:l,view:h,viewIndex:d}}i($u,"reduce");function Nm(e=zy,t=Pm){return{type:Ox,filter:e,sort:t}}i(Nm,"setFilter");function Hx(e=Pm){return{type:Rx,sort:e}}i(Hx,"setSort");function lB(e,t=zy,n=Pm){return{type:Dx,item:e,filter:t,sort:n}}i(lB,"add");function uB(e,t=zy,n=Pm){return{type:Ix,item:e,filter:t,sort:n}}i(uB,"update");function cB(e){return{type:Fx,id:e}}i(cB,"remove");function fB(e,t=zy,n=Pm){return{type:Bx,list:e,filter:t,sort:n}}i(fB,"receive");function OP(e,t,n){let s=pB(e.view,t,n),l=[...e.view],h={...e.viewIndex};l.splice(s,0,t);for(let d=l.length-1;d>=s;d--)h[l[d].id]=d;return{view:l,viewIndex:h}}i(OP,"sortedInsert");function Lx(e,t,n){let s=t[n],l=[...e],h={...t};delete h[n],l.splice(s,1);for(let d=l.length-1;d>=s;d--)h[l[d].id]=d;return{data:l,dataIndex:h}}i(Lx,"removeData");function dB(e,t,n){let s=[...e.view],l={...e.viewIndex},h=l[t.id];for(s[h]=t;h+10;)s[h]=s[h+1],s[h+1]=t,l[t.id]=h+1,l[s[h].id]=h,++h;for(;h>0&&n(s[h],s[h-1])<0;)s[h]=s[h-1],s[h-1]=t,l[t.id]=h-1,l[s[h].id]=h,--h;return{view:s,viewIndex:l}}i(dB,"sortedUpdate");function pB(e,t,n){let s=0,l=e.length;for(;s>>1;n(t,e[h])>=0?s=h+1:l=h}return s}i(pB,"sortedIndex");function zy(){return!0}i(zy,"defaultFilter");function Pm(e,t){return 0}i(Pm,"defaultSort");var RP={http:80,https:443},Rn=class e{static{i(this,"MessageUtils")}static getContentType(t){let n=e.get_first_header(t,/^Content-Type$/i);if(n)return n.split(";")[0].trim()}static get_first_header(t,n){let s=t;s._headerLookups||Object.defineProperty(s,"_headerLookups",{value:{},configurable:!1,enumerable:!1,writable:!1});let l=n.toString();if(!(l in s._headerLookups)){let h;for(let d=0;d{switch(e.type){case"http":{let t=e.request.contentLength||0;return e.response&&(t+=e.response.contentLength||0),e.websocket&&(t+=e.websocket.messages_meta.contentLength||0),t}case"tcp":case"udp":return e.messages_meta.contentLength||0;case"dns":return e.response?.size??0}},"getTotalSize"),Uy=i(e=>e.type==="http"&&!e.websocket,"canReplay"),Vx=i(e=>{if(e.type!=="http")return e.client_conn.tls_version==="QUICv1"?"resource-icon-quic":`resource-icon-${e.type}`;if(e.websocket)return"resource-icon-websocket";if(!e.response)return"resource-icon-plain";let t=vl.getContentType(e.response)||"";return e.response.status_code===304?"resource-icon-not-modified":300<=e.response.status_code&&e.response.status_code<400?"resource-icon-redirect":t.indexOf("image")>=0?"resource-icon-image":t.indexOf("javascript")>=0?"resource-icon-js":t.indexOf("css")>=0?"resource-icon-css":t.indexOf("html")>=0?"resource-icon-document":"resource-icon-plain"},"getIcon"),qx=i(e=>{switch(e.type){case"http":return Bs.pretty_url(e.request);case"tcp":case"udp":return`${e.client_conn.peername.join(":")} \u2194 ${e.server_conn?.address?.join(":")}`;case"dns":return`${e.request.questions.map(t=>`${t.name} ${t.type}`).join(", ")} = ${(e.response?.answers.map(t=>t.data).join(", ")??"...")||"?"}`}},"mainPath"),jx=i(e=>{switch(e.type){case"http":return e.response?.status_code;case"dns":return e.response?.response_code;default:return}},"statusCode"),Kx=i(e=>{switch(e.type){case"http":return e.websocket?e.client_conn.tls_established?"WSS":"WS":e.request.method;case"dns":return e.request.op_code;default:return e.type.toUpperCase()}},"getMethod"),Gx=i(e=>{switch(e.type){case"http":return e.request.http_version;default:return""}},"getVersion"),DP={tls:i(e=>e.type==="http"&&e.request.scheme,"tls"),icon:Vx,index:i(()=>0,"index"),path:qx,method:Kx,version:Gx,status:jx,size:$x,time:i(e=>{let t=Lm(e),n=Wx(e);return t&&n&&n-t},"time"),timestamp:Lm,quickactions:i(()=>0,"quickactions"),comment:i(e=>e.comment,"comment")};var vf=function(){"use strict";function e(s,l){function h(){this.constructor=s}i(h,"ctor"),h.prototype=l.prototype,s.prototype=new h}i(e,"peg$subclass");function t(s,l,h,d){this.message=s,this.expected=l,this.found=h,this.location=d,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,t)}i(t,"peg$SyntaxError"),e(t,Error);function n(s){var l=arguments.length>1?arguments[1]:{},h=this,d={},v={start:vi},S=vi,b={type:"other",description:"filter expression"},k=i(function(w){return w},"peg$c1"),R={type:"other",description:"whitespace"},I=/^[ \t\n\r]/,z={type:"class",value:"[ \\t\\n\\r]",description:"[ \\t\\n\\r]"},j={type:"other",description:"control character"},Y=/^[|&!()~"]/,ne={type:"class",value:'[|&!()~"]',description:'[|&!()~"]'},F={type:"other",description:"optional whitespace"},L="|",D={type:"literal",value:"|",description:'"|"'},q=i(function(w,N){return Sc(w,N)},"peg$c11"),te="&",Z={type:"literal",value:"&",description:'"&"'},oe=i(function(w,N){return Qf(w,N)},"peg$c14"),he="!",Re={type:"literal",value:"!",description:'"!"'},Ee=i(function(w){return Qs(w)},"peg$c17"),Ye="(",tt={type:"literal",value:"(",description:'"("'},xe=")",Xe={type:"literal",value:")",description:'")"'},je=i(function(w){return Js(w)},"peg$c22"),Qe="~all",ot={type:"literal",value:"~all",description:'"~all"'},It=i(function(){return Jf},"peg$c25"),Pt="~a",fn={type:"literal",value:"~a",description:'"~a"'},dn=i(function(){return xc},"peg$c28"),gr="~b",Wt={type:"literal",value:"~b",description:'"~b"'},vr=i(function(w){return Wo(w)},"peg$c31"),yr="~bq",Gt={type:"literal",value:"~bq",description:'"~bq"'},Ft=i(function(w){return Kt(w)},"peg$c34"),se="~bs",Ue={type:"literal",value:"~bs",description:'"~bs"'},Gr=i(function(w){return Yp(w)},"peg$c37"),Zt="~c",st={type:"literal",value:"~c",description:'"~c"'},Fe=i(function(w){return Cc(w)},"peg$c40"),Fn="~comment",bn={type:"literal",value:"~comment",description:'"~comment"'},no=i(function(w){return Zs(w)},"peg$c43"),Ho="~d",lt={type:"literal",value:"~d",description:'"~d"'},wr=i(function(w){return _c(w)},"peg$c46"),fr="~dns",pt={type:"literal",value:"~dns",description:'"~dns"'},io=i(function(){return Ha},"peg$c49"),Bn="~dst",Mi={type:"literal",value:"~dst",description:'"~dst"'},Yr=i(function(w){return ed(w)},"peg$c52"),Xr="~e",oo={type:"literal",value:"~e",description:'"~e"'},hi=i(function(){return Wl},"peg$c55"),Hn="~h",so={type:"literal",value:"~h",description:'"~h"'},bl=i(function(w){return $l(w)},"peg$c58"),X="~hq",ee={type:"literal",value:"~hq",description:'"~hq"'},Ce=i(function(w){return Ec(w)},"peg$c61"),Me="~hs",Tl={type:"literal",value:"~hs",description:'"~hs"'},ze=i(function(w){return Xp(w)},"peg$c64"),Qr="~http",Yt={type:"literal",value:"~http",description:'"~http"'},Mt=i(function(){return Vl},"peg$c67"),At="~marked",Na={type:"literal",value:"~marked",description:'"~marked"'},pn=i(function(){return mn},"peg$c70"),zn="~marker",kr={type:"literal",value:"~marker",description:'"~marker"'},gs=i(function(w){return bc(w)},"peg$c73"),Tn="~m",Pa={type:"literal",value:"~m",description:'"~m"'},Aa=i(function(w){return co(w)},"peg$c76"),nc="~q",ic={type:"literal",value:"~q",description:'"~q"'},oc=i(function(){return yi},"peg$c79"),$f="~replayq",sc={type:"literal",value:"~replayq",description:'"~replayq"'},Bp=i(function(){return ea},"peg$c82"),kl="~replays",Hp={type:"literal",value:"~replays",description:'"~replays"'},zp=i(function(){return ql},"peg$c85"),Nl="~replay",Vf={type:"literal",value:"~replay",description:'"~replay"'},ac=i(function(){return Tc},"peg$c88"),vs="~src",Up={type:"literal",value:"~src",description:'"~src"'},Wp=i(function(w){return Fi(w)},"peg$c91"),La="~s",lc={type:"literal",value:"~s",description:'"~s"'},ao=i(function(){return $o},"peg$c94"),qf="~tcp",Oi={type:"literal",value:"~tcp",description:'"~tcp"'},uc=i(function(){return jl},"peg$c97"),cc="~udp",or={type:"literal",value:"~udp",description:'"~udp"'},Ae=i(function(){return xs},"peg$c100"),lo="~tq",Pl={type:"literal",value:"~tq",description:'"~tq"'},Ri=i(function(w){return td(w)},"peg$c103"),vt="~ts",ys={type:"literal",value:"~ts",description:'"~ts"'},Ma=i(function(w){return Qp(w)},"peg$c106"),Oa="~t",ce={type:"literal",value:"~t",description:'"~t"'},Ve=i(function(w){return Kl(w)},"peg$c109"),qs="~u",fc={type:"literal",value:"~u",description:'"~u"'},js=i(function(w){return Gl(w)},"peg$c112"),zo="~websocket",dc={type:"literal",value:"~websocket",description:'"~websocket"'},qe=i(function(){return kn},"peg$c115"),jf={type:"other",description:"integer"},Al=/^['"]/,ws={type:"class",value:`['"]`,description:`['"]`},mi=/^[0-9]/,Ll={type:"class",value:"[0-9]",description:"[0-9]"},Ml=i(function(w){return parseInt(w.join(""),10)},"peg$c121"),Ss={type:"other",description:"string"},uo='"',Ol={type:"literal",value:'"',description:'"\\""'},Ks=i(function(w){return w.join("")},"peg$c125"),pc="'",Rl={type:"literal",value:"'",description:`"'"`},hc=/^["\\]/,mc={type:"class",value:'["\\\\]',description:'["\\\\]'},Dl={type:"any",description:"any character"},Ra=i(function(w){return w},"peg$c131"),gc="\\",Di={type:"literal",value:"\\",description:'"\\\\"'},Kf=/^['\\]/,$p={type:"class",value:"['\\\\]",description:"['\\\\]"},Il=/^['"\\]/,Da={type:"class",value:`['"\\\\]`,description:`['"\\\\]`},Vp="n",vc={type:"literal",value:"n",description:'"n"'},qp=i(function(){return` +`},"peg$c140"),jp="r",yc={type:"literal",value:"r",description:'"r"'},Gf=i(function(){return"\r"},"peg$c143"),Kp="t",Bt={type:"literal",value:"t",description:'"t"'},Ot=i(function(){return" "},"peg$c146"),M=0,De=0,Gs=[{line:1,column:1,seenCR:!1}],gi=0,Fl=[],ve=0,Ia;if("startRule"in l){if(!(l.startRule in v))throw new Error(`Can't start parsing from rule "`+l.startRule+'".');S=v[l.startRule]}function Gp(){return s.substring(De,M)}i(Gp,"text");function Rt(){return Uo(De,M)}i(Rt,"location");function Yf(w){throw Xs(null,[{type:"other",description:w}],s.substring(De,M),Uo(De,M))}i(Yf,"expected");function Ys(w){throw Xs(w,null,s.substring(De,M),Uo(De,M))}i(Ys,"error");function Fa(w){var N=Gs[w],U,W;if(N)return N;for(U=w-1;!Gs[U];)U--;for(N=Gs[U],N={line:N.line,column:N.column,seenCR:N.seenCR};Ugi&&(gi=M,Fl=[]),Fl.push(w))}i(Se,"peg$fail");function Xs(w,N,U,W){function gn(Un){var fo=1;for(Un.sort(function(Vo,qo){return Vo.descriptionqo.description?1:0});fo1?qo.slice(0,-1).join(", ")+" or "+qo[Un.length-1]:qo[0],Yl=fo?'"'+Vo(fo)+'"':"end of input","Expected "+kc+" but "+Yl+" found."}return i(Xt,"buildMessage"),N!==null&&gn(N),new t(w!==null?w:Xt(N,U),N,U,W)}i(Xs,"peg$buildException");function vi(){var w,N,U,W;return ve++,w=M,N=hn(),N!==d?(U=Hl(),U!==d?(W=hn(),W!==d?(De=w,N=k(U),w=N):(M=w,w=d)):(M=w,w=d)):(M=w,w=d),ve--,w===d&&(N=d,ve===0&&Se(b)),w}i(vi,"peg$parsestart");function Ze(){var w,N;return ve++,I.test(s.charAt(M))?(w=s.charAt(M),M++):(w=d,ve===0&&Se(z)),ve--,w===d&&(N=d,ve===0&&Se(R)),w}i(Ze,"peg$parsews");function Bl(){var w,N;return ve++,Y.test(s.charAt(M))?(w=s.charAt(M),M++):(w=d,ve===0&&Se(ne)),ve--,w===d&&(N=d,ve===0&&Se(j)),w}i(Bl,"peg$parsecc");function hn(){var w,N;for(ve++,w=[],N=Ze();N!==d;)w.push(N),N=Ze();return ve--,w===d&&(N=d,ve===0&&Se(F)),w}i(hn,"peg$parse__");function Hl(){var w,N,U,W,gn,Xt;return w=M,N=Jr(),N!==d?(U=hn(),U!==d?(s.charCodeAt(M)===124?(W=L,M++):(W=d,ve===0&&Se(D)),W!==d?(gn=hn(),gn!==d?(Xt=Hl(),Xt!==d?(De=w,N=q(N,Xt),w=N):(M=w,w=d)):(M=w,w=d)):(M=w,w=d)):(M=w,w=d)):(M=w,w=d),w===d&&(w=Jr()),w}i(Hl,"peg$parseOrExpr");function Jr(){var w,N,U,W,gn,Xt;if(w=M,N=zl(),N!==d?(U=hn(),U!==d?(s.charCodeAt(M)===38?(W=te,M++):(W=d,ve===0&&Se(Z)),W!==d?(gn=hn(),gn!==d?(Xt=Jr(),Xt!==d?(De=w,N=oe(N,Xt),w=N):(M=w,w=d)):(M=w,w=d)):(M=w,w=d)):(M=w,w=d)):(M=w,w=d),w===d){if(w=M,N=zl(),N!==d){if(U=[],W=Ze(),W!==d)for(;W!==d;)U.push(W),W=Ze();else U=d;U!==d?(W=Jr(),W!==d?(De=w,N=oe(N,W),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;w===d&&(w=zl())}return w}i(Jr,"peg$parseAndExpr");function zl(){var w,N,U,W;return w=M,s.charCodeAt(M)===33?(N=he,M++):(N=d,ve===0&&Se(Re)),N!==d?(U=hn(),U!==d?(W=zl(),W!==d?(De=w,N=Ee(W),w=N):(M=w,w=d)):(M=w,w=d)):(M=w,w=d),w===d&&(w=Zn()),w}i(zl,"peg$parseNotExpr");function Zn(){var w,N,U,W,gn,Xt;return w=M,s.charCodeAt(M)===40?(N=Ye,M++):(N=d,ve===0&&Se(tt)),N!==d?(U=hn(),U!==d?(W=Hl(),W!==d?(gn=hn(),gn!==d?(s.charCodeAt(M)===41?(Xt=xe,M++):(Xt=d,ve===0&&Se(Xe)),Xt!==d?(De=w,N=je(W),w=N):(M=w,w=d)):(M=w,w=d)):(M=w,w=d)):(M=w,w=d)):(M=w,w=d),w===d&&(w=Xf()),w}i(Zn,"peg$parseBindingExpr");function Xf(){var w,N,U,W;if(w=M,s.substr(M,4)===Qe?(N=Qe,M+=4):(N=d,ve===0&&Se(ot)),N!==d&&(De=w,N=It()),w=N,w===d&&(w=M,s.substr(M,2)===Pt?(N=Pt,M+=2):(N=d,ve===0&&Se(fn)),N!==d&&(De=w,N=dn()),w=N,w===d)){if(w=M,s.substr(M,2)===gr?(N=gr,M+=2):(N=d,ve===0&&Se(Wt)),N!==d){if(U=[],W=Ze(),W!==d)for(;W!==d;)U.push(W),W=Ze();else U=d;U!==d?(W=jt(),W!==d?(De=w,N=vr(W),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;if(w===d){if(w=M,s.substr(M,3)===yr?(N=yr,M+=3):(N=d,ve===0&&Se(Gt)),N!==d){if(U=[],W=Ze(),W!==d)for(;W!==d;)U.push(W),W=Ze();else U=d;U!==d?(W=jt(),W!==d?(De=w,N=Ft(W),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;if(w===d){if(w=M,s.substr(M,3)===se?(N=se,M+=3):(N=d,ve===0&&Se(Ue)),N!==d){if(U=[],W=Ze(),W!==d)for(;W!==d;)U.push(W),W=Ze();else U=d;U!==d?(W=jt(),W!==d?(De=w,N=Gr(W),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;if(w===d){if(w=M,s.substr(M,2)===Zt?(N=Zt,M+=2):(N=d,ve===0&&Se(st)),N!==d){if(U=[],W=Ze(),W!==d)for(;W!==d;)U.push(W),W=Ze();else U=d;U!==d?(W=wc(),W!==d?(De=w,N=Fe(W),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;if(w===d){if(w=M,s.substr(M,8)===Fn?(N=Fn,M+=8):(N=d,ve===0&&Se(bn)),N!==d){if(U=[],W=Ze(),W!==d)for(;W!==d;)U.push(W),W=Ze();else U=d;U!==d?(W=jt(),W!==d?(De=w,N=no(W),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;if(w===d){if(w=M,s.substr(M,2)===Ho?(N=Ho,M+=2):(N=d,ve===0&&Se(lt)),N!==d){if(U=[],W=Ze(),W!==d)for(;W!==d;)U.push(W),W=Ze();else U=d;U!==d?(W=jt(),W!==d?(De=w,N=wr(W),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;if(w===d&&(w=M,s.substr(M,4)===fr?(N=fr,M+=4):(N=d,ve===0&&Se(pt)),N!==d&&(De=w,N=io()),w=N,w===d)){if(w=M,s.substr(M,4)===Bn?(N=Bn,M+=4):(N=d,ve===0&&Se(Mi)),N!==d){if(U=[],W=Ze(),W!==d)for(;W!==d;)U.push(W),W=Ze();else U=d;U!==d?(W=jt(),W!==d?(De=w,N=Yr(W),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;if(w===d&&(w=M,s.substr(M,2)===Xr?(N=Xr,M+=2):(N=d,ve===0&&Se(oo)),N!==d&&(De=w,N=hi()),w=N,w===d)){if(w=M,s.substr(M,2)===Hn?(N=Hn,M+=2):(N=d,ve===0&&Se(so)),N!==d){if(U=[],W=Ze(),W!==d)for(;W!==d;)U.push(W),W=Ze();else U=d;U!==d?(W=jt(),W!==d?(De=w,N=bl(W),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;if(w===d){if(w=M,s.substr(M,3)===X?(N=X,M+=3):(N=d,ve===0&&Se(ee)),N!==d){if(U=[],W=Ze(),W!==d)for(;W!==d;)U.push(W),W=Ze();else U=d;U!==d?(W=jt(),W!==d?(De=w,N=Ce(W),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;if(w===d){if(w=M,s.substr(M,3)===Me?(N=Me,M+=3):(N=d,ve===0&&Se(Tl)),N!==d){if(U=[],W=Ze(),W!==d)for(;W!==d;)U.push(W),W=Ze();else U=d;U!==d?(W=jt(),W!==d?(De=w,N=ze(W),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;if(w===d&&(w=M,s.substr(M,5)===Qr?(N=Qr,M+=5):(N=d,ve===0&&Se(Yt)),N!==d&&(De=w,N=Mt()),w=N,w===d&&(w=M,s.substr(M,7)===At?(N=At,M+=7):(N=d,ve===0&&Se(Na)),N!==d&&(De=w,N=pn()),w=N,w===d))){if(w=M,s.substr(M,7)===zn?(N=zn,M+=7):(N=d,ve===0&&Se(kr)),N!==d){if(U=[],W=Ze(),W!==d)for(;W!==d;)U.push(W),W=Ze();else U=d;U!==d?(W=jt(),W!==d?(De=w,N=gs(W),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;if(w===d){if(w=M,s.substr(M,2)===Tn?(N=Tn,M+=2):(N=d,ve===0&&Se(Pa)),N!==d){if(U=[],W=Ze(),W!==d)for(;W!==d;)U.push(W),W=Ze();else U=d;U!==d?(W=jt(),W!==d?(De=w,N=Aa(W),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;if(w===d&&(w=M,s.substr(M,2)===nc?(N=nc,M+=2):(N=d,ve===0&&Se(ic)),N!==d&&(De=w,N=oc()),w=N,w===d&&(w=M,s.substr(M,8)===$f?(N=$f,M+=8):(N=d,ve===0&&Se(sc)),N!==d&&(De=w,N=Bp()),w=N,w===d&&(w=M,s.substr(M,8)===kl?(N=kl,M+=8):(N=d,ve===0&&Se(Hp)),N!==d&&(De=w,N=zp()),w=N,w===d&&(w=M,s.substr(M,7)===Nl?(N=Nl,M+=7):(N=d,ve===0&&Se(Vf)),N!==d&&(De=w,N=ac()),w=N,w===d))))){if(w=M,s.substr(M,4)===vs?(N=vs,M+=4):(N=d,ve===0&&Se(Up)),N!==d){if(U=[],W=Ze(),W!==d)for(;W!==d;)U.push(W),W=Ze();else U=d;U!==d?(W=jt(),W!==d?(De=w,N=Wp(W),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;if(w===d&&(w=M,s.substr(M,2)===La?(N=La,M+=2):(N=d,ve===0&&Se(lc)),N!==d&&(De=w,N=ao()),w=N,w===d&&(w=M,s.substr(M,4)===qf?(N=qf,M+=4):(N=d,ve===0&&Se(Oi)),N!==d&&(De=w,N=uc()),w=N,w===d&&(w=M,s.substr(M,4)===cc?(N=cc,M+=4):(N=d,ve===0&&Se(or)),N!==d&&(De=w,N=Ae()),w=N,w===d)))){if(w=M,s.substr(M,3)===lo?(N=lo,M+=3):(N=d,ve===0&&Se(Pl)),N!==d){if(U=[],W=Ze(),W!==d)for(;W!==d;)U.push(W),W=Ze();else U=d;U!==d?(W=jt(),W!==d?(De=w,N=Ri(W),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;if(w===d){if(w=M,s.substr(M,3)===vt?(N=vt,M+=3):(N=d,ve===0&&Se(ys)),N!==d){if(U=[],W=Ze(),W!==d)for(;W!==d;)U.push(W),W=Ze();else U=d;U!==d?(W=jt(),W!==d?(De=w,N=Ma(W),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;if(w===d){if(w=M,s.substr(M,2)===Oa?(N=Oa,M+=2):(N=d,ve===0&&Se(ce)),N!==d){if(U=[],W=Ze(),W!==d)for(;W!==d;)U.push(W),W=Ze();else U=d;U!==d?(W=jt(),W!==d?(De=w,N=Ve(W),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;if(w===d){if(w=M,s.substr(M,2)===qs?(N=qs,M+=2):(N=d,ve===0&&Se(fc)),N!==d){if(U=[],W=Ze(),W!==d)for(;W!==d;)U.push(W),W=Ze();else U=d;U!==d?(W=jt(),W!==d?(De=w,N=js(W),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;w===d&&(w=M,s.substr(M,10)===zo?(N=zo,M+=10):(N=d,ve===0&&Se(dc)),N!==d&&(De=w,N=qe()),w=N,w===d&&(w=M,N=jt(),N!==d&&(De=w,N=js(N)),w=N))}}}}}}}}}}}}}}}}}return w}i(Xf,"peg$parseExpr");function wc(){var w,N,U,W;if(ve++,w=M,Al.test(s.charAt(M))?(N=s.charAt(M),M++):(N=d,ve===0&&Se(ws)),N===d&&(N=null),N!==d){if(U=[],mi.test(s.charAt(M))?(W=s.charAt(M),M++):(W=d,ve===0&&Se(Ll)),W!==d)for(;W!==d;)U.push(W),mi.test(s.charAt(M))?(W=s.charAt(M),M++):(W=d,ve===0&&Se(Ll));else U=d;U!==d?(Al.test(s.charAt(M))?(W=s.charAt(M),M++):(W=d,ve===0&&Se(ws)),W===d&&(W=null),W!==d?(De=w,N=Ml(U),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;return ve--,w===d&&(N=d,ve===0&&Se(jf)),w}i(wc,"peg$parseIntegerLiteral");function jt(){var w,N,U,W;if(ve++,w=M,s.charCodeAt(M)===34?(N=uo,M++):(N=d,ve===0&&Se(Ol)),N!==d){for(U=[],W=Ba();W!==d;)U.push(W),W=Ba();U!==d?(s.charCodeAt(M)===34?(W=uo,M++):(W=d,ve===0&&Se(Ol)),W!==d?(De=w,N=Ks(U),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;if(w===d){if(w=M,s.charCodeAt(M)===39?(N=pc,M++):(N=d,ve===0&&Se(Rl)),N!==d){for(U=[],W=Ul();W!==d;)U.push(W),W=Ul();U!==d?(s.charCodeAt(M)===39?(W=pc,M++):(W=d,ve===0&&Se(Rl)),W!==d?(De=w,N=Ks(U),w=N):(M=w,w=d)):(M=w,w=d)}else M=w,w=d;if(w===d)if(w=M,N=M,ve++,U=Bl(),ve--,U===d?N=void 0:(M=N,N=d),N!==d){if(U=[],W=$t(),W!==d)for(;W!==d;)U.push(W),W=$t();else U=d;U!==d?(De=w,N=Ks(U),w=N):(M=w,w=d)}else M=w,w=d}return ve--,w===d&&(N=d,ve===0&&Se(Ss)),w}i(jt,"peg$parseStringLiteral");function Ba(){var w,N,U;return w=M,N=M,ve++,hc.test(s.charAt(M))?(U=s.charAt(M),M++):(U=d,ve===0&&Se(mc)),ve--,U===d?N=void 0:(M=N,N=d),N!==d?(s.length>M?(U=s.charAt(M),M++):(U=d,ve===0&&Se(Dl)),U!==d?(De=w,N=Ra(U),w=N):(M=w,w=d)):(M=w,w=d),w===d&&(w=M,s.charCodeAt(M)===92?(N=gc,M++):(N=d,ve===0&&Se(Di)),N!==d?(U=Ii(),U!==d?(De=w,N=Ra(U),w=N):(M=w,w=d)):(M=w,w=d)),w}i(Ba,"peg$parseDoubleStringChar");function Ul(){var w,N,U;return w=M,N=M,ve++,Kf.test(s.charAt(M))?(U=s.charAt(M),M++):(U=d,ve===0&&Se($p)),ve--,U===d?N=void 0:(M=N,N=d),N!==d?(s.length>M?(U=s.charAt(M),M++):(U=d,ve===0&&Se(Dl)),U!==d?(De=w,N=Ra(U),w=N):(M=w,w=d)):(M=w,w=d),w===d&&(w=M,s.charCodeAt(M)===92?(N=gc,M++):(N=d,ve===0&&Se(Di)),N!==d?(U=Ii(),U!==d?(De=w,N=Ra(U),w=N):(M=w,w=d)):(M=w,w=d)),w}i(Ul,"peg$parseSingleStringChar");function $t(){var w,N,U;return w=M,N=M,ve++,U=Ze(),ve--,U===d?N=void 0:(M=N,N=d),N!==d?(s.length>M?(U=s.charAt(M),M++):(U=d,ve===0&&Se(Dl)),U!==d?(De=w,N=Ra(U),w=N):(M=w,w=d)):(M=w,w=d),w}i($t,"peg$parseUnquotedStringChar");function Ii(){var w,N;return Il.test(s.charAt(M))?(w=s.charAt(M),M++):(w=d,ve===0&&Se(Da)),w===d&&(w=M,s.charCodeAt(M)===110?(N=Vp,M++):(N=d,ve===0&&Se(vc)),N!==d&&(De=w,N=qp()),w=N,w===d&&(w=M,s.charCodeAt(M)===114?(N=jp,M++):(N=d,ve===0&&Se(yc)),N!==d&&(De=w,N=Gf()),w=N,w===d&&(w=M,s.charCodeAt(M)===116?(N=Kp,M++):(N=d,ve===0&&Se(Bt)),N!==d&&(De=w,N=Ot()),w=N))),w}i(Ii,"peg$parseEscapeSequence");function Sc(w,N){function U(){return w.apply(this,arguments)||N.apply(this,arguments)}return i(U,"orFilter"),U.desc=w.desc+" or "+N.desc,U}i(Sc,"or");function Qf(w,N){function U(){return w.apply(this,arguments)&&N.apply(this,arguments)}return i(U,"andFilter"),U.desc=w.desc+" and "+N.desc,U}i(Qf,"and");function Qs(w){function N(){return!w.apply(this,arguments)}return i(N,"notFilter"),N.desc="not "+w.desc,N}i(Qs,"not");function Js(w){function N(){return w.apply(this,arguments)}return i(N,"bindingFilter"),N.desc="("+w.desc+")",N}i(Js,"binding");function Jf(w){return!0}i(Jf,"allFilter"),Jf.desc="all flows";var Zf=[new RegExp("text/javascript"),new RegExp("application/x-javascript"),new RegExp("application/javascript"),new RegExp("text/css"),new RegExp("image/.*"),new RegExp("font/.*"),new RegExp("application/font.*")];function xc(w){if(w.response){for(var N=vl.getContentType(w.response),U=Zf.length;U--;)if(Zf[U].test(N))return!0}return!1}i(xc,"assetFilter"),xc.desc="is asset";function Wo(w){w=new RegExp(w,"i");function N(U){return!0}return i(N,"bodyFilter"),N.desc="body filters are not implemented yet, see https://github.com/mitmproxy/mitmproxy/issues/3609",N}i(Wo,"body");function Kt(w){w=new RegExp(w,"i");function N(U){return!0}return i(N,"requestBodyFilter"),N.desc="body filters are not implemented yet, see https://github.com/mitmproxy/mitmproxy/issues/3609",N}i(Kt,"requestBody");function Yp(w){w=new RegExp(w,"i");function N(U){return!0}return i(N,"responseBodyFilter"),N.desc="body filters are not implemented yet, see https://github.com/mitmproxy/mitmproxy/issues/3609",N}i(Yp,"responseBody");function Cc(w){function N(U){return U.response&&U.response.status_code===w}return i(N,"responseCodeFilter"),N.desc="resp. code is "+w,N}i(Cc,"responseCode");function Zs(w){w=new RegExp(w,"i");function N(U){return w.test(U.comment)}return i(N,"commentFilter"),N.desc="comment matches "+w,N}i(Zs,"comment");function _c(w){w=new RegExp(w,"i");function N(U){return U.request&&(w.test(U.request.host)||w.test(U.request.pretty_host))}return i(N,"domainFilter"),N.desc="domain matches "+w,N}i(_c,"domain");function Ha(w){return w.type==="dns"}i(Ha,"dnsFilter"),Ha.desc="is a DNS Flow";function ed(w){w=new RegExp(w,"i");function N(U){return!!U.server_conn.address&&w.test(U.server_conn.address[0]+":"+U.server_conn.address[1])}return i(N,"destinationFilter"),N.desc="destination address matches "+w,N}i(ed,"destination");function Wl(w){return!!w.error}i(Wl,"errorFilter"),Wl.desc="has error";function $l(w){w=new RegExp(w,"i");function N(U){return U.request&&Bs.match_header(U.request,w)||U.response&&vl.match_header(U.response,w)}return i(N,"headerFilter"),N.desc="header matches "+w,N}i($l,"header");function Ec(w){w=new RegExp(w,"i");function N(U){return U.request&&Bs.match_header(U.request,w)}return i(N,"requestHeaderFilter"),N.desc="req. header matches "+w,N}i(Ec,"requestHeader");function Xp(w){w=new RegExp(w,"i");function N(U){return U.response&&vl.match_header(U.response,w)}return i(N,"responseHeaderFilter"),N.desc="resp. header matches "+w,N}i(Xp,"responseHeader");function Vl(w){return w.type==="http"}i(Vl,"httpFilter"),Vl.desc="is an HTTP Flow";function mn(w){return w.marked}i(mn,"markedFilter"),mn.desc="is marked";function bc(w){w=new RegExp(w,"i");function N(U){return w.test(U.marked)}return i(N,"markerFilter"),N.desc="marker matches "+w,N}i(bc,"marker");function co(w){w=new RegExp(w,"i");function N(U){return U.request&&w.test(U.request.method)}return i(N,"methodFilter"),N.desc="method matches "+w,N}i(co,"method");function yi(w){return w.request&&!w.response}i(yi,"noResponseFilter"),yi.desc="has no response";function ea(w){return w.is_replay==="request"}i(ea,"clientReplayFilter"),ea.desc="request has been replayed";function ql(w){return w.is_replay==="response"}i(ql,"serverReplayFilter"),ql.desc="response has been replayed";function Tc(w){return!!w.is_replay}i(Tc,"replayFilter"),Tc.desc="flow has been replayed";function Fi(w){w=new RegExp(w,"i");function N(U){return!!U.client_conn.peername&&w.test(U.client_conn.peername[0]+":"+U.client_conn.peername[1])}return i(N,"sourceFilter"),N.desc="source address matches "+w,N}i(Fi,"source");function $o(w){return!!w.response}i($o,"responseFilter"),$o.desc="has response";function jl(w){return w.type==="tcp"}i(jl,"tcpFilter"),jl.desc="is a TCP Flow";function xs(w){return w.type==="udp"}i(xs,"udpFilter"),xs.desc="is a UDP Flow";function td(w){w=new RegExp(w,"i");function N(U){return U.request&&w.test(Bs.getContentType(U.request))}return i(N,"requestContentTypeFilter"),N.desc="req. content type matches "+w,N}i(td,"requestContentType");function Qp(w){w=new RegExp(w,"i");function N(U){return U.response&&w.test(vl.getContentType(U.response))}return i(N,"responseContentTypeFilter"),N.desc="resp. content type matches "+w,N}i(Qp,"responseContentType");function Kl(w){w=new RegExp(w,"i");function N(U){return U.request&&w.test(Bs.getContentType(U.request))||U.response&&w.test(vl.getContentType(U.response))}return i(N,"contentTypeFilter"),N.desc="content type matches "+w,N}i(Kl,"contentType");function Gl(w){w=new RegExp(w,"i");function N(U){if(U.type==="dns"){let W=U.request?.questions[0];return W&&w.test(W.name)}return U.request&&w.test(Bs.pretty_url(U.request))}return i(N,"urlFilter"),N.desc="url matches "+w,N}i(Gl,"url");function kn(w){return!!w.websocket}if(i(kn,"websocketFilter"),kn.desc="is a Websocket Flow",Ia=S(),Ia!==d&&M===s.length)return Ia;throw Ia!==d&&M1)s=s.filter(l=>l!==t.data);else if(s=[],t.data in e.viewIndex&&e.view.length>1){let l=e.viewIndex[t.data],h;l===e.view.length-1?h=e.view[l-1]:h=e.view[l+1],s.push(h.id)}}return{...e,selected:s,...$u(e,n)}}case HP:return{...e,filter:t.filter,...$u(e,Nm(FP(t.filter),Yx(e.sort)))};case UP:return{...e,highlight:t.highlight};case zP:return{...e,sort:t.sort,...$u(e,Hx(Yx(t.sort)))};case BP:return{...e,selected:t.flowIds};default:return e}}i(Xx,"reducer");function FP(e){if(e)return vf.parse(e)}i(FP,"makeFilter");function Yx({column:e,desc:t}){if(!e)return(s,l)=>0;let n=DP[e];return(s,l)=>{let h=n(s),d=n(l);return h>d?t?-1:1:hxt(`/flows/${e.id}/resume`,{method:"POST"})}i(lp,"resume");function Vy(){return()=>xt("/flows/resume",{method:"POST"})}i(Vy,"resumeAll");function qy(e){return()=>xt(`/flows/${e.id}/kill`,{method:"POST"})}i(qy,"kill");function $P(){return()=>xt("/flows/kill",{method:"POST"})}i($P,"killAll");function jy(e){return()=>xt(`/flows/${e.id}`,{method:"DELETE"})}i(jy,"remove");function Ky(e){return()=>xt(`/flows/${e.id}/duplicate`,{method:"POST"})}i(Ky,"duplicate");function up(e){return()=>xt(`/flows/${e.id}/replay`,{method:"POST"})}i(up,"replay");function Gy(e){return()=>xt(`/flows/${e.id}/revert`,{method:"POST"})}i(Gy,"revert");function si(e,t){return()=>xt.put(`/flows/${e.id}`,t)}i(si,"update");function VP(e,t,n){let s=new FormData;return t=new window.Blob([t],{type:"plain/text"}),s.append("file",t),()=>xt(`/flows/${e.id}/${n}/content.data`,{method:"POST",body:s})}i(VP,"uploadContent");function Yy(){return()=>xt("/clear",{method:"POST"})}i(Yy,"clear");function qP(e){let t=new FormData;return t.append("file",e),()=>xt("/flows/dump",{method:"POST",body:t})}i(qP,"upload");function yl(e){return{type:BP,flowIds:e?[e]:[]}}i(yl,"select");var Xy="UI_HIDE_MODAL",jP="UI_SET_ACTIVE_MODAL",xB={activeModal:void 0};function Qx(e=xB,t){switch(t.type){case jP:return{...e,activeModal:t.activeModal};case Xy:return{...e,activeModal:void 0};default:return e}}i(Qx,"reducer");function KP(e){return{type:jP,activeModal:e}}i(KP,"setActiveModal");function Qy(){return{type:Xy}}i(Qy,"hideModal");var ba=de(be());var qt=de(be());var fp=de(be());var Mm=de(be()),YP=de(ai());var GP=(()=>{let e=document.createElement("div");return e.setAttribute("contenteditable","PLAINTEXT-ONLY"),e.contentEditable==="plaintext-only"?"plaintext-only":"true"})(),cp=!1,gt=class extends Mm.Component{constructor(){super(...arguments);this.input=Mm.default.createRef();this.isEditing=i(()=>this.input.current?.contentEditable===GP,"isEditing");this.startEditing=i(()=>{if(!this.input.current)return console.error("unreachable");this.isEditing()||(this.suppress_events=!0,this.input.current.blur(),this.input.current.contentEditable=GP,window.requestAnimationFrame(()=>{if(!this.input.current)return;this.input.current.focus(),this.suppress_events=!1;let n=document.createRange();n.selectNodeContents(this.input.current);let s=window.getSelection();s?.removeAllRanges(),s?.addRange(n),this.props.onEditStart?.()}))},"startEditing");this.resetValue=i(()=>{if(!this.input.current)return console.error("unreachable");this.input.current.textContent=this.props.content,this.props.onInput?.(this.props.content)},"resetValue");this.finishEditing=i(()=>{if(!this.input.current)return console.error("unreachable");this.props.onEditDone(this.input.current.textContent||""),this.input.current.blur(),this.input.current.contentEditable="inherit"},"finishEditing");this.onPaste=i(n=>{n.preventDefault();let s=n.clipboardData.getData("text/plain");document.execCommand("insertHTML",!1,s)},"onPaste");this.suppress_events=!1;this.onMouseDown=i(n=>{cp&&console.debug("onMouseDown",this.suppress_events),this.suppress_events=!0,window.addEventListener("mouseup",this.onMouseUp,{once:!0})},"onMouseDown");this.onMouseUp=i(n=>{let s=n.target===this.input.current,l=!window.getSelection()?.toString();cp&&console.warn("mouseUp",this.suppress_events,s,l),s&&l&&this.startEditing(),this.suppress_events=!1},"onMouseUp");this.onClick=i(n=>{cp&&console.debug("onClick",this.suppress_events)},"onClick");this.onFocus=i(n=>{if(cp&&console.debug("onFocus",this.props.content,this.suppress_events),!this.input.current)throw"unreachable";this.suppress_events||this.startEditing()},"onFocus");this.onInput=i(n=>{this.props.onInput?.(this.input.current?.textContent||"")},"onInput");this.onBlur=i(n=>{cp&&console.debug("onBlur",this.props.content,this.suppress_events),!this.suppress_events&&this.finishEditing()},"onBlur");this.onKeyDown=i(n=>{switch(cp&&console.debug("keydown",n),n.stopPropagation(),n.key){case"Escape":n.preventDefault(),this.resetValue(),this.finishEditing();break;case"Enter":n.shiftKey||(n.preventDefault(),this.finishEditing());break;default:break}this.props.onKeyDown?.(n)},"onKeyDown")}static{i(this,"ValueEditor")}render(){let n=(0,YP.default)("inline-input",this.props.className);return Mm.default.createElement("span",{ref:this.input,tabIndex:0,className:n,placeholder:this.props.placeholder,onFocus:this.onFocus,onBlur:this.onBlur,onKeyDown:this.onKeyDown,onInput:this.onInput,onPaste:this.onPaste,onMouseDown:this.onMouseDown,onClick:this.onClick},this.props.content)}componentDidUpdate(n){n.content!==this.props.content&&this.props.onInput?.(this.props.content)}};var XP=de(ai());function wf(e){let[t,n]=(0,fp.useState)(e.isValid(e.content)),s=(0,fp.useRef)(null),l=i(d=>{e.isValid(d)?e.onEditDone(d):s.current?.resetValue()},"onEditDone"),h=(0,XP.default)(e.className,t?"has-success":"has-warning");return fp.default.createElement(gt,{...e,className:h,onInput:d=>n(e.isValid(d)),onEditDone:l,ref:s})}i(wf,"ValidateEditor");function Kn(e){return`Minified Redux error #${e}; visit https://redux.js.org/Errors?code=${e} for the full message or use the non-minified dev environment for full errors. `}i(Kn,"formatProdErrorMessage");var CB=typeof Symbol=="function"&&Symbol.observable||"@@observable",QP=CB,Zx=i(()=>Math.random().toString(36).substring(7).split("").join("."),"randomString"),_B={INIT:`@@redux/INIT${Zx()}`,REPLACE:`@@redux/REPLACE${Zx()}`,PROBE_UNKNOWN_ACTION:i(()=>`@@redux/PROBE_UNKNOWN_ACTION${Zx()}`,"PROBE_UNKNOWN_ACTION")},Zy=_B;function e0(e){if(typeof e!="object"||e===null)return!1;let t=e;for(;Object.getPrototypeOf(t)!==null;)t=Object.getPrototypeOf(t);return Object.getPrototypeOf(e)===t||Object.getPrototypeOf(e)===null}i(e0,"isPlainObject");function eC(e,t,n){if(typeof e!="function")throw new Error(Kn(2));if(typeof t=="function"&&typeof n=="function"||typeof n=="function"&&typeof arguments[3]=="function")throw new Error(Kn(0));if(typeof t=="function"&&typeof n>"u"&&(n=t,t=void 0),typeof n<"u"){if(typeof n!="function")throw new Error(Kn(1));return n(eC)(e,t)}let s=e,l=t,h=new Map,d=h,v=0,S=!1;function b(){d===h&&(d=new Map,h.forEach((ne,F)=>{d.set(F,ne)}))}i(b,"ensureCanMutateNextListeners");function k(){if(S)throw new Error(Kn(3));return l}i(k,"getState");function R(ne){if(typeof ne!="function")throw new Error(Kn(4));if(S)throw new Error(Kn(5));let F=!0;b();let L=v++;return d.set(L,ne),i(function(){if(F){if(S)throw new Error(Kn(6));F=!1,b(),d.delete(L),h=null}},"unsubscribe")}i(R,"subscribe");function I(ne){if(!e0(ne))throw new Error(Kn(7));if(typeof ne.type>"u")throw new Error(Kn(8));if(typeof ne.type!="string")throw new Error(Kn(17));if(S)throw new Error(Kn(9));try{S=!0,l=s(l,ne)}finally{S=!1}return(h=d).forEach(L=>{L()}),ne}i(I,"dispatch");function z(ne){if(typeof ne!="function")throw new Error(Kn(10));s=ne,I({type:Zy.REPLACE})}i(z,"replaceReducer");function j(){let ne=R;return{subscribe(F){if(typeof F!="object"||F===null)throw new Error(Kn(11));function L(){let q=F;q.next&&q.next(k())}return i(L,"observeState"),L(),{unsubscribe:ne(L)}},[QP](){return this}}}return i(j,"observable"),I({type:Zy.INIT}),{dispatch:I,subscribe:R,getState:k,replaceReducer:z,[QP]:j}}i(eC,"createStore");function EB(e){Object.keys(e).forEach(t=>{let n=e[t];if(typeof n(void 0,{type:Zy.INIT})>"u")throw new Error(Kn(12));if(typeof n(void 0,{type:Zy.PROBE_UNKNOWN_ACTION()})>"u")throw new Error(Kn(13))})}i(EB,"assertReducerShape");function dp(e){let t=Object.keys(e),n={};for(let d=0;d"u"){let ne=S&&S.type;throw new Error(Kn(14))}k[I]=Y,b=b||Y!==j}return b=b||s.length!==Object.keys(v).length,b?k:v},"combination")}i(dp,"combineReducers");function pp(...e){return e.length===0?t=>t:e.length===1?e[0]:e.reduce((t,n)=>(...s)=>t(n(...s)))}i(pp,"compose");function JP(...e){return t=>(n,s)=>{let l=t(n,s),h=i(()=>{throw new Error(Kn(15))},"dispatch"),d={getState:l.getState,dispatch:i((S,...b)=>h(S,...b),"dispatch")},v=e.map(S=>S(d));return h=pp(...v)(l.dispatch),{...l,dispatch:h}}}i(JP,"applyMiddleware");function ZP(e){return e0(e)&&"type"in e&&typeof e.type=="string"}i(ZP,"isAction");var oA=Symbol.for("immer-nothing"),eA=Symbol.for("immer-draftable"),Ro=Symbol.for("immer-state");function Hs(e,...t){throw new Error(`[Immer] minified error nr: ${e}. Full error at: https://bit.ly/3cXEKWf`)}i(Hs,"die");var hp=Object.getPrototypeOf;function _a(e){return!!e&&!!e[Ro]}i(_a,"isDraft");function zs(e){return e?sA(e)||Array.isArray(e)||!!e[eA]||!!e.constructor?.[eA]||o0(e)||s0(e):!1}i(zs,"isDraftable");var bB=Object.prototype.constructor.toString();function sA(e){if(!e||typeof e!="object")return!1;let t=hp(e);if(t===null)return!0;let n=Object.hasOwnProperty.call(t,"constructor")&&t.constructor;return n===Object?!0:typeof n=="function"&&Function.toString.call(n)===bB}i(sA,"isPlainObject");function t0(e,t){i0(e)===0?Reflect.ownKeys(e).forEach(n=>{t(n,e[n],e)}):e.forEach((n,s)=>t(s,n,e))}i(t0,"each");function i0(e){let t=e[Ro];return t?t.type_:Array.isArray(e)?1:o0(e)?2:s0(e)?3:0}i(i0,"getArchtype");function nC(e,t){return i0(e)===2?e.has(t):Object.prototype.hasOwnProperty.call(e,t)}i(nC,"has");function aA(e,t,n){let s=i0(e);s===2?e.set(t,n):s===3?e.add(n):e[t]=n}i(aA,"set");function TB(e,t){return e===t?e!==0||1/e===1/t:e!==e&&t!==t}i(TB,"is");function o0(e){return e instanceof Map}i(o0,"isMap");function s0(e){return e instanceof Set}i(s0,"isSet");function Sf(e){return e.copy_||e.base_}i(Sf,"latest");function iC(e,t){if(o0(e))return new Map(e);if(s0(e))return new Set(e);if(Array.isArray(e))return Array.prototype.slice.call(e);let n=sA(e);if(t===!0||t==="class_only"&&!n){let s=Object.getOwnPropertyDescriptors(e);delete s[Ro];let l=Reflect.ownKeys(s);for(let h=0;h1&&(e.set=e.add=e.clear=e.delete=kB),Object.freeze(e),t&&Object.entries(e).forEach(([n,s])=>uC(s,!0))),e}i(uC,"freeze");function kB(){Hs(2)}i(kB,"dontMutateFrozenCollections");function a0(e){return Object.isFrozen(e)}i(a0,"isFrozen");var NB={};function xf(e){let t=NB[e];return t||Hs(0,e),t}i(xf,"getPlugin");var Om;function lA(){return Om}i(lA,"getCurrentScope");function PB(e,t){return{drafts_:[],parent_:e,immer_:t,canAutoFreeze_:!0,unfinalizedDrafts_:0}}i(PB,"createScope");function tA(e,t){t&&(xf("Patches"),e.patches_=[],e.inversePatches_=[],e.patchListener_=t)}i(tA,"usePatchesInScope");function oC(e){sC(e),e.drafts_.forEach(AB),e.drafts_=null}i(oC,"revokeScope");function sC(e){e===Om&&(Om=e.parent_)}i(sC,"leaveScope");function rA(e){return Om=PB(Om,e)}i(rA,"enterScope");function AB(e){let t=e[Ro];t.type_===0||t.type_===1?t.revoke_():t.revoked_=!0}i(AB,"revokeDraft");function nA(e,t){t.unfinalizedDrafts_=t.drafts_.length;let n=t.drafts_[0];return e!==void 0&&e!==n?(n[Ro].modified_&&(oC(t),Hs(4)),zs(e)&&(e=r0(t,e),t.parent_||n0(t,e)),t.patches_&&xf("Patches").generateReplacementPatches_(n[Ro].base_,e,t.patches_,t.inversePatches_)):e=r0(t,n,[]),oC(t),t.patches_&&t.patchListener_(t.patches_,t.inversePatches_),e!==oA?e:void 0}i(nA,"processResult");function r0(e,t,n){if(a0(t))return t;let s=t[Ro];if(!s)return t0(t,(l,h)=>iA(e,s,t,l,h,n)),t;if(s.scope_!==e)return t;if(!s.modified_)return n0(e,s.base_,!0),s.base_;if(!s.finalized_){s.finalized_=!0,s.scope_.unfinalizedDrafts_--;let l=s.copy_,h=l,d=!1;s.type_===3&&(h=new Set(l),l.clear(),d=!0),t0(h,(v,S)=>iA(e,s,l,v,S,n,d)),n0(e,l,!1),n&&e.patches_&&xf("Patches").generatePatches_(s,n,e.patches_,e.inversePatches_)}return s.copy_}i(r0,"finalize");function iA(e,t,n,s,l,h,d){if(_a(l)){let v=h&&t&&t.type_!==3&&!nC(t.assigned_,s)?h.concat(s):void 0,S=r0(e,l,v);if(aA(n,s,S),_a(S))e.canAutoFreeze_=!1;else return}else d&&n.add(l);if(zs(l)&&!a0(l)){if(!e.immer_.autoFreeze_&&e.unfinalizedDrafts_<1)return;r0(e,l),(!t||!t.scope_.parent_)&&typeof s!="symbol"&&Object.prototype.propertyIsEnumerable.call(n,s)&&n0(e,l)}}i(iA,"finalizeProperty");function n0(e,t,n=!1){!e.parent_&&e.immer_.autoFreeze_&&e.canAutoFreeze_&&uC(t,n)}i(n0,"maybeFreeze");function LB(e,t){let n=Array.isArray(e),s={type_:n?1:0,scope_:t?t.scope_:lA(),modified_:!1,finalized_:!1,assigned_:{},parent_:t,base_:e,draft_:null,copy_:null,revoke_:null,isManual_:!1},l=s,h=cC;n&&(l=[s],h=Rm);let{revoke:d,proxy:v}=Proxy.revocable(l,h);return s.draft_=v,s.revoke_=d,v}i(LB,"createProxyProxy");var cC={get(e,t){if(t===Ro)return e;let n=Sf(e);if(!nC(n,t))return MB(e,n,t);let s=n[t];return e.finalized_||!zs(s)?s:s===tC(e.base_,t)?(rC(e),e.copy_[t]=lC(s,e)):s},has(e,t){return t in Sf(e)},ownKeys(e){return Reflect.ownKeys(Sf(e))},set(e,t,n){let s=uA(Sf(e),t);if(s?.set)return s.set.call(e.draft_,n),!0;if(!e.modified_){let l=tC(Sf(e),t),h=l?.[Ro];if(h&&h.base_===n)return e.copy_[t]=n,e.assigned_[t]=!1,!0;if(TB(n,l)&&(n!==void 0||nC(e.base_,t)))return!0;rC(e),aC(e)}return e.copy_[t]===n&&(n!==void 0||t in e.copy_)||Number.isNaN(n)&&Number.isNaN(e.copy_[t])||(e.copy_[t]=n,e.assigned_[t]=!0),!0},deleteProperty(e,t){return tC(e.base_,t)!==void 0||t in e.base_?(e.assigned_[t]=!1,rC(e),aC(e)):delete e.assigned_[t],e.copy_&&delete e.copy_[t],!0},getOwnPropertyDescriptor(e,t){let n=Sf(e),s=Reflect.getOwnPropertyDescriptor(n,t);return s&&{writable:!0,configurable:e.type_!==1||t!=="length",enumerable:s.enumerable,value:n[t]}},defineProperty(){Hs(11)},getPrototypeOf(e){return hp(e.base_)},setPrototypeOf(){Hs(12)}},Rm={};t0(cC,(e,t)=>{Rm[e]=function(){return arguments[0]=arguments[0][0],t.apply(this,arguments)}});Rm.deleteProperty=function(e,t){return Rm.set.call(this,e,t,void 0)};Rm.set=function(e,t,n){return cC.set.call(this,e[0],t,n,e[0])};function tC(e,t){let n=e[Ro];return(n?Sf(n):e)[t]}i(tC,"peek");function MB(e,t,n){let s=uA(t,n);return s?"value"in s?s.value:s.get?.call(e.draft_):void 0}i(MB,"readPropFromProto");function uA(e,t){if(!(t in e))return;let n=hp(e);for(;n;){let s=Object.getOwnPropertyDescriptor(n,t);if(s)return s;n=hp(n)}}i(uA,"getDescriptorFromProto");function aC(e){e.modified_||(e.modified_=!0,e.parent_&&aC(e.parent_))}i(aC,"markChanged");function rC(e){e.copy_||(e.copy_=iC(e.base_,e.scope_.immer_.useStrictShallowCopy_))}i(rC,"prepareCopy");var OB=class{static{i(this,"Immer2")}constructor(e){this.autoFreeze_=!0,this.useStrictShallowCopy_=!1,this.produce=(t,n,s)=>{if(typeof t=="function"&&typeof n!="function"){let h=n;n=t;let d=this;return i(function(S=h,...b){return d.produce(S,k=>n.call(this,k,...b))},"curriedProduce")}typeof n!="function"&&Hs(6),s!==void 0&&typeof s!="function"&&Hs(7);let l;if(zs(t)){let h=rA(this),d=lC(t,void 0),v=!0;try{l=n(d),v=!1}finally{v?oC(h):sC(h)}return tA(h,s),nA(l,h)}else if(!t||typeof t!="object"){if(l=n(t),l===void 0&&(l=t),l===oA&&(l=void 0),this.autoFreeze_&&uC(l,!0),s){let h=[],d=[];xf("Patches").generateReplacementPatches_(t,l,h,d),s(h,d)}return l}else Hs(1,t)},this.produceWithPatches=(t,n)=>{if(typeof t=="function")return(d,...v)=>this.produceWithPatches(d,S=>t(S,...v));let s,l;return[this.produce(t,n,(d,v)=>{s=d,l=v}),s,l]},typeof e?.autoFreeze=="boolean"&&this.setAutoFreeze(e.autoFreeze),typeof e?.useStrictShallowCopy=="boolean"&&this.setUseStrictShallowCopy(e.useStrictShallowCopy)}createDraft(e){zs(e)||Hs(8),_a(e)&&(e=fC(e));let t=rA(this),n=lC(e,void 0);return n[Ro].isManual_=!0,sC(t),n}finishDraft(e,t){let n=e&&e[Ro];(!n||!n.isManual_)&&Hs(9);let{scope_:s}=n;return tA(s,t),nA(void 0,s)}setAutoFreeze(e){this.autoFreeze_=e}setUseStrictShallowCopy(e){this.useStrictShallowCopy_=e}applyPatches(e,t){let n;for(n=t.length-1;n>=0;n--){let l=t[n];if(l.path.length===0&&l.op==="replace"){e=l.value;break}}n>-1&&(t=t.slice(n+1));let s=xf("Patches").applyPatches_;return _a(e)?s(e,t):this.produce(e,l=>s(l,t))}};function lC(e,t){let n=o0(e)?xf("MapSet").proxyMap_(e,t):s0(e)?xf("MapSet").proxySet_(e,t):LB(e,t);return(t?t.scope_:lA()).drafts_.push(n),n}i(lC,"createProxy");function fC(e){return _a(e)||Hs(10,e),cA(e)}i(fC,"current");function cA(e){if(!zs(e)||a0(e))return e;let t=e[Ro],n;if(t){if(!t.modified_)return t.base_;t.finalized_=!0,n=iC(e,t.scope_.immer_.useStrictShallowCopy_)}else n=iC(e,!0);return t0(n,(s,l)=>{aA(n,s,cA(l))}),t&&(t.finalized_=!1),n}i(cA,"currentImpl");var Do=new OB,l0=Do.produce,x8=Do.produceWithPatches.bind(Do),C8=Do.setAutoFreeze.bind(Do),_8=Do.setUseStrictShallowCopy.bind(Do),E8=Do.applyPatches.bind(Do),b8=Do.createDraft.bind(Do),T8=Do.finishDraft.bind(Do);function RB(e,t=`expected a function, instead received ${typeof e}`){if(typeof e!="function")throw new TypeError(t)}i(RB,"assertIsFunction");function DB(e,t=`expected an object, instead received ${typeof e}`){if(typeof e!="object")throw new TypeError(t)}i(DB,"assertIsObject");function IB(e,t="expected all items to be functions, instead received the following types: "){if(!e.every(n=>typeof n=="function")){let n=e.map(s=>typeof s=="function"?`function ${s.name||"unnamed"}()`:typeof s).join(", ");throw new TypeError(`${t}[${n}]`)}}i(IB,"assertIsArrayOfFunctions");var fA=i(e=>Array.isArray(e)?e:[e],"ensureIsArray");function FB(e){let t=Array.isArray(e[0])?e[0]:e;return IB(t,"createSelector expects all input-selectors to be functions, but received the following types: "),t}i(FB,"getDependencies");function BB(e,t){let n=[],{length:s}=e;for(let l=0;l{n=u0(),d.resetResultsCount()},d.resultsCount=()=>h,d.resetResultsCount=()=>{h=0},d}i(c0,"weakMapMemoize");function dC(e,...t){let n=typeof e=="function"?{memoize:e,memoizeOptions:t}:e,s=i((...l)=>{let h=0,d=0,v,S={},b=l.pop();typeof b=="object"&&(S=b,b=l.pop()),RB(b,`createSelector expects an output function after the inputs, but received: [${typeof b}]`);let k={...n,...S},{memoize:R,memoizeOptions:I=[],argsMemoize:z=c0,argsMemoizeOptions:j=[],devModeChecks:Y={}}=k,ne=fA(I),F=fA(j),L=FB(l),D=R(i(function(){return h++,b.apply(null,arguments)},"recomputationWrapper"),...ne),q=!0,te=z(i(function(){d++;let oe=BB(L,arguments);return v=D.apply(null,oe),v},"dependenciesChecker"),...F);return Object.assign(te,{resultFunc:b,memoizedResultFunc:D,dependencies:L,dependencyRecomputations:i(()=>d,"dependencyRecomputations"),resetDependencyRecomputations:i(()=>{d=0},"resetDependencyRecomputations"),lastResult:i(()=>v,"lastResult"),recomputations:i(()=>h,"recomputations"),resetRecomputations:i(()=>{h=0},"resetRecomputations"),memoize:R,argsMemoize:z})},"createSelector2");return Object.assign(s,{withTypes:i(()=>s,"withTypes")}),s}i(dC,"createSelectorCreator");var WB=dC(c0),$B=Object.assign((e,t=WB)=>{DB(e,`createStructuredSelector expects first argument to be an object where each property is a selector, instead received a ${typeof e}`);let n=Object.keys(e),s=n.map(h=>e[h]);return t(s,(...h)=>h.reduce((d,v,S)=>(d[n[S]]=v,d),{}))},{withTypes:i(()=>$B,"withTypes")});function pA(e){return i(({dispatch:n,getState:s})=>l=>h=>typeof h=="function"?h(n,s,e):l(h),"middleware")}i(pA,"createThunkMiddleware");var hA=pA(),mA=pA;var VB=i((...e)=>{let t=dC(...e),n=Object.assign((...s)=>{let l=t(...s),h=i((d,...v)=>l(_a(d)?fC(d):d,...v),"wrappedSelector");return Object.assign(h,l),h},{withTypes:i(()=>n,"withTypes")});return n},"createDraftSafeSelectorCreator"),H8=VB(c0),qB=typeof window<"u"&&window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__:function(){if(arguments.length!==0)return typeof arguments[0]=="object"?pp:pp.apply(null,arguments)},W8=typeof window<"u"&&window.__REDUX_DEVTOOLS_EXTENSION__?window.__REDUX_DEVTOOLS_EXTENSION__:function(){return function(e){return e}},jB=i(e=>e&&typeof e.match=="function","hasMatchFunction");function us(e,t){function n(...s){if(t){let l=t(...s);if(!l)throw new Error(nn(0));return{type:e,payload:l.payload,..."meta"in l&&{meta:l.meta},..."error"in l&&{error:l.error}}}return{type:e,payload:s[0]}}return i(n,"actionCreator"),n.toString=()=>`${e}`,n.type=e,n.match=s=>ZP(s)&&s.type===e,n}i(us,"createAction");var wA=class Dm extends Array{static{i(this,"_Tuple")}constructor(...t){super(...t),Object.setPrototypeOf(this,Dm.prototype)}static get[Symbol.species](){return Dm}concat(...t){return super.concat.apply(this,t)}prepend(...t){return t.length===1&&Array.isArray(t[0])?new Dm(...t[0].concat(this)):new Dm(...t.concat(this))}};function gA(e){return zs(e)?l0(e,()=>{}):e}i(gA,"freezeDraftable");function vA(e,t,n){if(e.has(t)){let l=e.get(t);return n.update&&(l=n.update(l,t,e),e.set(t,l)),l}if(!n.insert)throw new Error(nn(10));let s=n.insert(t,e);return e.set(t,s),s}i(vA,"emplace");function KB(e){return typeof e=="boolean"}i(KB,"isBoolean");var GB=i(()=>i(function(t){let{thunk:n=!0,immutableCheck:s=!0,serializableCheck:l=!0,actionCreatorCheck:h=!0}=t??{},d=new wA;return n&&(KB(n)?d.push(hA):d.push(mA(n.extraArgument))),d},"getDefaultMiddleware"),"buildGetDefaultMiddleware"),YB="RTK_autoBatch";var SA=i(e=>t=>{setTimeout(t,e)},"createQueueWithTimer"),XB=typeof window<"u"&&window.requestAnimationFrame?window.requestAnimationFrame:SA(10),QB=i((e={type:"raf"})=>t=>(...n)=>{let s=t(...n),l=!0,h=!1,d=!1,v=new Set,S=e.type==="tick"?queueMicrotask:e.type==="raf"?XB:e.type==="callback"?e.queueNotification:SA(e.timeout),b=i(()=>{d=!1,h&&(h=!1,v.forEach(k=>k()))},"notifyListeners");return Object.assign({},s,{subscribe(k){let R=i(()=>l&&k(),"wrappedListener"),I=s.subscribe(R);return v.add(k),()=>{I(),v.delete(k)}},dispatch(k){try{return l=!k?.meta?.[YB],h=!l,h&&(d||(d=!0,S(b))),s.dispatch(k)}finally{l=!0}}})},"autoBatchEnhancer"),JB=i(e=>i(function(n){let{autoBatch:s=!0}=n??{},l=new wA(e);return s&&l.push(QB(typeof s=="object"?s:void 0)),l},"getDefaultEnhancers"),"buildGetDefaultEnhancers"),Vu=!0;function xA(e){let t=GB(),{reducer:n=void 0,middleware:s,devTools:l=!0,preloadedState:h=void 0,enhancers:d=void 0}=e||{},v;if(typeof n=="function")v=n;else if(e0(n))v=dp(n);else throw new Error(nn(1));if(!Vu&&s&&typeof s!="function")throw new Error(nn(2));let S;if(typeof s=="function"){if(S=s(t),!Vu&&!Array.isArray(S))throw new Error(nn(3))}else S=t();if(!Vu&&S.some(j=>typeof j!="function"))throw new Error(nn(4));let b=pp;l&&(b=qB({trace:!Vu,...typeof l=="object"&&l}));let k=JP(...S),R=JB(k);if(!Vu&&d&&typeof d!="function")throw new Error(nn(5));let I=typeof d=="function"?d(R):R();if(!Vu&&!Array.isArray(I))throw new Error(nn(6));if(!Vu&&I.some(j=>typeof j!="function"))throw new Error(nn(7));!Vu&&S.length&&!I.includes(k)&&console.error("middlewares were provided, but middleware enhancer was not included in final enhancers - make sure to call `getDefaultEnhancers`");let z=b(...I);return eC(v,h,z)}i(xA,"configureStore");function CA(e){let t={},n=[],s,l={addCase(h,d){let v=typeof h=="string"?h:h.type;if(!v)throw new Error(nn(28));if(v in t)throw new Error(nn(29));return t[v]=d,l},addMatcher(h,d){return n.push({matcher:h,reducer:d}),l},addDefaultCase(h){return s=h,l}};return e(l),[t,n,s]}i(CA,"executeReducerBuilderCallback");function ZB(e){return typeof e=="function"}i(ZB,"isStateFunction");function eH(e,t){let[n,s,l]=CA(t),h;if(ZB(e))h=i(()=>gA(e()),"getInitialState");else{let v=gA(e);h=i(()=>v,"getInitialState")}function d(v=h(),S){let b=[n[S.type],...s.filter(({matcher:k})=>k(S)).map(({reducer:k})=>k)];return b.filter(k=>!!k).length===0&&(b=[l]),b.reduce((k,R)=>{if(R)if(_a(k)){let z=R(k,S);return z===void 0?k:z}else{if(zs(k))return l0(k,I=>R(I,S));{let I=R(k,S);if(I===void 0){if(k===null)return k;throw new Error(nn(9))}return I}}return k},v)}return i(d,"reducer"),d.getInitialState=h,d}i(eH,"createReducer");var tH="ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW",_A=i((e=21)=>{let t="",n=e;for(;n--;)t+=tH[Math.random()*64|0];return t},"nanoid"),rH=i((e,t)=>jB(e)?e.match(t):e(t),"matches");function nH(...e){return t=>e.some(n=>rH(n,t))}i(nH,"isAnyOf");var iH=["name","message","stack","code"],pC=class{static{i(this,"RejectWithValue")}constructor(e,t){this.payload=e,this.meta=t}_type},yA=class{static{i(this,"FulfillWithMeta")}constructor(e,t){this.payload=e,this.meta=t}_type},oH=i(e=>{if(typeof e=="object"&&e!==null){let t={};for(let n of iH)typeof e[n]=="string"&&(t[n]=e[n]);return t}return{message:String(e)}},"miniSerializeError"),Im=(()=>{function e(t,n,s){let l=us(t+"/fulfilled",(S,b,k,R)=>({payload:S,meta:{...R||{},arg:k,requestId:b,requestStatus:"fulfilled"}})),h=us(t+"/pending",(S,b,k)=>({payload:void 0,meta:{...k||{},arg:b,requestId:S,requestStatus:"pending"}})),d=us(t+"/rejected",(S,b,k,R,I)=>({payload:R,error:(s&&s.serializeError||oH)(S||"Rejected"),meta:{...I||{},arg:k,requestId:b,rejectedWithValue:!!R,requestStatus:"rejected",aborted:S?.name==="AbortError",condition:S?.name==="ConditionError"}}));function v(S){return(b,k,R)=>{let I=s?.idGenerator?s.idGenerator(S):_A(),z=new AbortController,j,Y;function ne(L){Y=L,z.abort()}i(ne,"abort");let F=async function(){let L;try{let q=s?.condition?.(S,{getState:k,extra:R});if(aH(q)&&(q=await q),q===!1||z.signal.aborted)throw{name:"ConditionError",message:"Aborted due to condition callback returning false."};let te=new Promise((Z,oe)=>{j=i(()=>{oe({name:"AbortError",message:Y||"Aborted"})},"abortHandler"),z.signal.addEventListener("abort",j)});b(h(I,S,s?.getPendingMeta?.({requestId:I,arg:S},{getState:k,extra:R}))),L=await Promise.race([te,Promise.resolve(n(S,{dispatch:b,getState:k,extra:R,requestId:I,signal:z.signal,abort:ne,rejectWithValue:i((Z,oe)=>new pC(Z,oe),"rejectWithValue"),fulfillWithValue:i((Z,oe)=>new yA(Z,oe),"fulfillWithValue")})).then(Z=>{if(Z instanceof pC)throw Z;return Z instanceof yA?l(Z.payload,I,S,Z.meta):l(Z,I,S)})])}catch(q){L=q instanceof pC?d(null,I,S,q.payload,q.meta):d(q,I,S)}finally{j&&z.signal.removeEventListener("abort",j)}return s&&!s.dispatchConditionRejection&&d.match(L)&&L.meta.condition||b(L),L}();return Object.assign(F,{abort:ne,requestId:I,arg:S,unwrap(){return F.then(sH)}})}}return i(v,"actionCreator"),Object.assign(v,{pending:h,rejected:d,fulfilled:l,settled:nH(d,l),typePrefix:t})}return i(e,"createAsyncThunk2"),e.withTypes=()=>e,e})();function sH(e){if(e.meta&&e.meta.rejectedWithValue)throw e.payload;if(e.error)throw e.error;return e.payload}i(sH,"unwrapResult");function aH(e){return e!==null&&typeof e=="object"&&typeof e.then=="function"}i(aH,"isThenable");var EA=Symbol.for("rtk-slice-createasyncthunk"),K8={[EA]:Im};function lH(e,t){return`${e}/${t}`}i(lH,"getType");function uH({creators:e}={}){let t=e?.asyncThunk?.[EA];return i(function(s){let{name:l,reducerPath:h=l}=s;if(!l)throw new Error(nn(11));typeof process<"u";let d=(typeof s.reducers=="function"?s.reducers(fH()):s.reducers)||{},v=Object.keys(d),S={sliceCaseReducersByName:{},sliceCaseReducersByType:{},actionCreators:{},sliceMatchers:[]},b={addCase(L,D){let q=typeof L=="string"?L:L.type;if(!q)throw new Error(nn(12));if(q in S.sliceCaseReducersByType)throw new Error(nn(13));return S.sliceCaseReducersByType[q]=D,b},addMatcher(L,D){return S.sliceMatchers.push({matcher:L,reducer:D}),b},exposeAction(L,D){return S.actionCreators[L]=D,b},exposeCaseReducer(L,D){return S.sliceCaseReducersByName[L]=D,b}};v.forEach(L=>{let D=d[L],q={reducerName:L,type:lH(l,L),createNotation:typeof s.reducers=="function"};pH(D)?mH(q,D,b,t):dH(q,D,b)});function k(){let[L={},D=[],q=void 0]=typeof s.extraReducers=="function"?CA(s.extraReducers):[s.extraReducers],te={...L,...S.sliceCaseReducersByType};return eH(s.initialState,Z=>{for(let oe in te)Z.addCase(oe,te[oe]);for(let oe of S.sliceMatchers)Z.addMatcher(oe.matcher,oe.reducer);for(let oe of D)Z.addMatcher(oe.matcher,oe.reducer);q&&Z.addDefaultCase(q)})}i(k,"buildReducer");let R=i(L=>L,"selectSelf"),I=new Map,z;function j(L,D){return z||(z=k()),z(L,D)}i(j,"reducer");function Y(){return z||(z=k()),z.getInitialState()}i(Y,"getInitialState");function ne(L,D=!1){function q(Z){let oe=Z[L];return typeof oe>"u"&&D&&(oe=Y()),oe}i(q,"selectSlice");function te(Z=R){let oe=vA(I,D,{insert:i(()=>new WeakMap,"insert")});return vA(oe,Z,{insert:i(()=>{let he={};for(let[Re,Ee]of Object.entries(s.selectors??{}))he[Re]=cH(Ee,Z,Y,D);return he},"insert")})}return i(te,"getSelectors"),{reducerPath:L,getSelectors:te,get selectors(){return te(q)},selectSlice:q}}i(ne,"makeSelectorProps");let F={name:l,reducer:j,actions:S.actionCreators,caseReducers:S.sliceCaseReducersByName,getInitialState:Y,...ne(h),injectInto(L,{reducerPath:D,...q}={}){let te=D??h;return L.inject({reducerPath:te,reducer:j},q),{...F,...ne(te,!0)}}};return F},"createSlice2")}i(uH,"buildCreateSlice");function cH(e,t,n,s){function l(h,...d){let v=t(h);return typeof v>"u"&&s&&(v=n()),e(v,...d)}return i(l,"wrapper"),l.unwrapped=e,l}i(cH,"wrapSelector");var on=uH();function fH(){function e(t,n){return{_reducerDefinitionType:"asyncThunk",payloadCreator:t,...n}}return i(e,"asyncThunk"),e.withTypes=()=>e,{reducer(t){return Object.assign({[t.name](...n){return t(...n)}}[t.name],{_reducerDefinitionType:"reducer"})},preparedReducer(t,n){return{_reducerDefinitionType:"reducerWithPrepare",prepare:t,reducer:n}},asyncThunk:e}}i(fH,"buildReducerCreators");function dH({type:e,reducerName:t,createNotation:n},s,l){let h,d;if("reducer"in s){if(n&&!hH(s))throw new Error(nn(17));h=s.reducer,d=s.prepare}else h=s;l.addCase(e,h).exposeCaseReducer(t,h).exposeAction(t,d?us(e,d):us(e))}i(dH,"handleNormalReducerDefinition");function pH(e){return e._reducerDefinitionType==="asyncThunk"}i(pH,"isAsyncThunkSliceReducerDefinition");function hH(e){return e._reducerDefinitionType==="reducerWithPrepare"}i(hH,"isCaseReducerWithPrepareDefinition");function mH({type:e,reducerName:t},n,s,l){if(!l)throw new Error(nn(18));let{payloadCreator:h,fulfilled:d,pending:v,rejected:S,settled:b,options:k}=n,R=l(e,h,k);s.exposeAction(t,R),d&&s.addCase(R.fulfilled,d),v&&s.addCase(R.pending,v),S&&s.addCase(R.rejected,S),b&&s.addMatcher(R.settled,b),s.exposeCaseReducer(t,{fulfilled:d||f0,pending:v||f0,rejected:S||f0,settled:b||f0})}i(mH,"handleThunkCaseReducerDefinition");function f0(){}i(f0,"noop");var bA="listener",TA="completed",kA="cancelled",G8=`task-${kA}`,Y8=`task-${TA}`,X8=`${bA}-${kA}`,Q8=`${bA}-${TA}`;var gH=i((e,t)=>{if(typeof e!="function")throw new Error(nn(32))},"assertFunction");var{assign:J8}=Object;var hC="listenerMiddleware";var vH=i(e=>{let{type:t,actionCreator:n,matcher:s,predicate:l,effect:h}=e;if(t)l=us(t).match;else if(n)t=n.type,l=n.match;else if(s)l=s;else if(!l)throw new Error(nn(21));return gH(h,"options.listener"),{predicate:l,type:t,effect:h}},"getListenerEntryPropsFrom"),yH=Object.assign(e=>{let{type:t,predicate:n,effect:s}=vH(e);return{id:_A(),effect:s,type:t,predicate:n,pending:new Set,unsubscribe:i(()=>{throw new Error(nn(22))},"unsubscribe")}},{withTypes:i(()=>yH,"withTypes")});var wH=Object.assign(us(`${hC}/add`),{withTypes:i(()=>wH,"withTypes")}),Z8=us(`${hC}/removeAll`),SH=Object.assign(us(`${hC}/remove`),{withTypes:i(()=>SH,"withTypes")});var e5=Symbol.for("rtk-state-proxy-original");function nn(e){return`Minified Redux Toolkit error #${e}; visit https://redux-toolkit.js.org/Errors?code=${e} for the full message or use the non-minified dev environment for full errors. `}i(nn,"formatProdErrorMessage");var Be=i(()=>NP(),"useAppDispatch"),me=SP,NA=Im.withTypes();var PA="EVENTS_ADD",xH="EVENTS_RECEIVE",AA="EVENTS_TOGGLE_VISIBILITY",LA="EVENTS_TOGGLE_FILTER",mC=(h=>(h.debug="debug",h.info="info",h.web="web",h.warn="warn",h.error="error",h))(mC||{}),CH={visible:!1,filters:{debug:!1,info:!0,web:!0,warn:!0,error:!0},...ap};function gC(e=CH,t){switch(t.type){case AA:return{...e,visible:!e.visible};case LA:{let n={...e.filters,[t.filter]:!e.filters[t.filter]};return{...e,filters:n,...$u(e,Nm(s=>n[s.level]))}}case PA:case xH:return{...e,...$u(e,Am[t.cmd](t.data,n=>e.filters[n.level]))};default:return e}}i(gC,"reduce");function MA(e){return{type:LA,filter:e}}i(MA,"toggleFilter");function mp(){return{type:AA}}i(mp,"toggleVisibility");function OA(e,t="web"){let n={id:Math.random().toString(),message:e,level:t};return{type:PA,cmd:"add",data:n}}i(OA,"add");var DA="UI_OPTION_UPDATE_START",IA="UI_OPTION_UPDATE_SUCCESS",FA="UI_OPTION_UPDATE_ERROR",_H={};function vC(e=_H,t){switch(t.type){case DA:return{...e,[t.option]:{isUpdating:!0,value:t.value,error:!1}};case IA:return{...e,[t.option]:void 0};case FA:{let n=e[t.option].value;return typeof n=="boolean"&&(n=!n),{...e,[t.option]:{value:n,isUpdating:!1,error:t.error}}}case Xy:return{};default:return e}}i(vC,"reducer");function BA(e,t){return{type:DA,option:e,value:t}}i(BA,"startUpdate");function HA(e){return{type:IA,option:e}}i(HA,"updateSuccess");function zA(e,t){return{type:FA,option:e,error:String(t)}}i(zA,"updateError");var bH=on({name:"ui/tabs",initialState:{current:1,isInitial:!0},reducers:{setCurrent(e,t){e.current=t.payload,e.isInitial=!1}}}),{actions:TH,reducer:kH}=bH,{setCurrent:Fm}=TH,UA=kH;var WA=dp({flow:kx,modal:Qx,optionsEditor:vC,tabs:UA});var NH={state:"CONNECTION_INIT",message:void 0};function yC(e=NH,t){switch(t.type){case"CONNECTION_ESTABLISHED":case"CONNECTION_FETCHING":case"CONNECTION_ERROR":case"CONNECTION_OFFLINE":return{state:t.type,message:t.message};default:return e}}i(yC,"reducer");function $A(){return{type:"CONNECTION_FETCHING"}}i($A,"startFetching");function VA(){return{type:"CONNECTION_ESTABLISHED"}}i(VA,"connectionEstablished");function qA(e){return{type:"CONNECTION_ERROR",message:e}}i(qA,"connectionError");var jA={add_upstream_certs_to_client_chain:!1,allow_hosts:[],anticache:!1,anticomp:!1,block_global:!0,block_list:[],block_private:!1,body_size_limit:void 0,cert_passphrase:void 0,certs:[],ciphers_client:void 0,ciphers_server:void 0,client_certs:void 0,client_replay:[],client_replay_concurrency:1,command_history:!0,confdir:"~/.mitmproxy",connect_addr:void 0,connection_strategy:"eager",console_focus_follow:!1,content_view_lines_cutoff:512,dns_name_servers:[],dns_use_hosts_file:!0,export_preserve_original_ip:!1,hardump:"",http2:!0,http2_ping_keepalive:58,http3:!0,http_connect_send_host_header:!0,ignore_hosts:[],intercept:void 0,intercept_active:!1,keep_alt_svc_header:!1,keep_host_header:!1,key_size:2048,listen_host:"",listen_port:void 0,map_local:[],map_remote:[],mode:["regular"],modify_body:[],modify_headers:[],normalize_outbound_headers:!0,onboarding:!0,onboarding_host:"mitm.it",proxy_debug:!1,proxyauth:void 0,rawtcp:!0,readfile_filter:void 0,request_client_cert:!1,rfile:void 0,save_stream_file:void 0,save_stream_filter:void 0,scripts:[],server:!0,server_replay:[],server_replay_extra:"forward",server_replay_ignore_content:!1,server_replay_ignore_host:!1,server_replay_ignore_params:[],server_replay_ignore_payload_params:[],server_replay_ignore_port:!1,server_replay_kill_extra:!1,server_replay_nopop:!1,server_replay_refresh:!0,server_replay_reuse:!1,server_replay_use_headers:[],show_ignored_hosts:!1,showhost:!1,ssl_insecure:!1,ssl_verify_upstream_trusted_ca:void 0,ssl_verify_upstream_trusted_confdir:void 0,stickyauth:void 0,stickycookie:void 0,stream_large_bodies:void 0,strip_ech:!0,tcp_hosts:[],termlog_verbosity:"info",tls_ecdh_curve_client:void 0,tls_ecdh_curve_server:void 0,tls_version_client_max:"UNBOUNDED",tls_version_client_min:"TLS1_2",tls_version_server_max:"UNBOUNDED",tls_version_server_min:"TLS1_2",udp_hosts:[],upstream_auth:void 0,upstream_cert:!0,validate_inbound_headers:!0,view_filter:void 0,view_order:"time",view_order_reversed:!1,web_columns:["tls","icon","path","method","status","size","time"],web_debug:!1,web_host:"127.0.0.1",web_open_browser:!0,web_port:8081,web_static_viewer:"",websocket:!0};var wC="OPTIONS_RECEIVE",SC="OPTIONS_UPDATE";function xC(e=jA,t){switch(t.type){case wC:{let n={};for(let[s,{value:l}]of Object.entries(t.data))n[s]=l;return n}case SC:{let n={...e};for(let[s,{value:l}]of Object.entries(t.data))n[s]=l;return n}default:return e}}i(xC,"reducer");async function AH(e,t,n){try{let s=await xt.put("/options",{[e]:t});if(s.status===200)n(HA(e));else throw await s.text()}catch(s){n(zA(e,s))}}i(AH,"pureSendUpdate");var LH=AH;function gp(e,t){return n=>{n(BA(e,t)),LH(e,t,n)}}i(gp,"update");var KA="COMMANDBAR_TOGGLE_VISIBILITY",OH={visible:!1};function CC(e=OH,t){switch(t.type){case KA:return{...e,visible:!e.visible};default:return e}}i(CC,"reducer");function d0(){return{type:KA}}i(d0,"toggleVisibility");var Gn=us("STATE_RECEIVE"),Yn=us("STATE_UPDATE"),RH={available:!1,version:"",contentViews:[],servers:{},platform:""};function _C(e=RH,t){switch(t.type){case Gn.type:case Yn.type:return{...e,available:!0,...t.payload};default:return e}}i(_C,"reducer");var DH={},IH=i((e=DH,t)=>{switch(t.type){case wC:return t.data;case SC:return{...e,...t.data};default:return e}},"reducer"),YA=IH;var Xi=i((e,t)=>t.listen_host&&t.listen_port?`${e}@${t.listen_host}:${t.listen_port}`:t.listen_port?`${e}@${t.listen_port}`:e,"includeListenAddress"),p0=i(e=>{let[t,n]=km(e,"@");t||(t=n,n="");let[s,l]=Hy(t,":"),h,d;if(n){let v;if(n.includes(":")?[h,v]=km(n,":"):(h="",v=n),v&&(d=parseInt(v,10),isNaN(d)||d<0||d>65535))throw new Error(`invalid port: ${v}`)}return{full_spec:e,name:s,data:l,listen_host:h,listen_port:d}},"parseSpec");var h0=i(e=>Xi("regular",e),"getSpec"),EC=i(({listen_host:e,listen_port:t})=>({ui_id:Math.random(),active:!0,listen_host:e,listen_port:t}),"parseRaw");var m0=i(e=>e.selectedProcesses?`local:${e.selectedProcesses}`:"local","getSpec"),bC=i(({data:e})=>({ui_id:Math.random(),active:!0,selectedProcesses:e}),"parseRaw");var g0=i(e=>{let t=e.file_path?`wireguard:${e.file_path}`:"wireguard";return Xi(t,e)},"getSpec"),TC=i(({data:e,listen_host:t,listen_port:n})=>({ui_id:Math.random(),active:!0,listen_host:t,listen_port:n,file_path:e}),"parseRaw");var v0=(b=>(b.HTTP="http",b.HTTPS="https",b.HTTP3="http3",b.TLS="tls",b.DTLS="dtls",b.TCP="tcp",b.UDP="udp",b.DNS="dns",b.QUIC="quic",b))(v0||{});var y0=i(()=>({active:!1,protocol:"https",destination:"",ui_id:Math.random()}),"defaultReverseState"),vp=i(e=>Xi(`reverse:${e.protocol}://${e.destination}`,e),"getSpec"),XA=i(({data:e,listen_host:t,listen_port:n})=>{let[s,l]=Hy(e,"://");return l||(l=s,s="https"),{ui_id:Math.random(),active:!0,protocol:s,destination:l,listen_host:t,listen_port:n}},"parseRaw");var w0=i(e=>Xi("transparent",e),"getSpec"),kC=i(({listen_host:e,listen_port:t})=>({ui_id:Math.random(),active:!0,listen_host:e,listen_port:t}),"parseRaw");var S0=i(e=>Xi("socks5",e),"getSpec"),NC=i(({listen_host:e,listen_port:t})=>({ui_id:Math.random(),active:!0,listen_host:e,listen_port:t}),"parseRaw");var x0=i(e=>Xi(`upstream:${e.destination}`,e),"getSpec"),PC=i(({data:e,listen_host:t,listen_port:n})=>({ui_id:Math.random(),active:!0,destination:e||"",listen_host:t,listen_port:n}),"parseRaw");var C0=i(e=>Xi("dns",e),"getSpec"),AC=i(({listen_host:e,listen_port:t})=>({ui_id:Math.random(),active:!0,listen_host:e,listen_port:t}),"parseRaw");var qu=i(e=>e.active&&!e.error,"isActiveMode");async function FH(e,t){let n=t.getState().modes,s=[...n.regular.filter(qu).map(h0),...n.local.filter(qu).map(m0),...n.wireguard.filter(qu).map(g0),...n.reverse.filter(qu).map(vp),...n.transparent.filter(qu).map(w0),...n.socks.filter(qu).map(S0),...n.upstream.filter(qu).map(x0),...n.dns.filter(qu).map(C0)],l=await xt.put("/options",{mode:s});if(l.status!==200)throw new Error(await l.text())}i(FH,"updateModes");function Ct(e){return NA(e,FH)}i(Ct,"createModeUpdateThunk");function _t(e,t,n){e.addCase(n.pending,(s,l)=>{let{server:h,value:d}=l.meta.arg,v=s.findIndex(S=>S.ui_id===h.ui_id);v>=0&&(s[v][t]=d,s[v].error=void 0)}),e.addCase(n.rejected,(s,l)=>{let{server:h}=l.meta.arg,d=s.findIndex(v=>v.ui_id===h.ui_id);d>=0&&(s[d].error=l.error.message)})}i(_t,"addSetter");function qr(e,t){return i(function(s,l){if(l.payload.servers){let h=Object.values(l.payload.servers).filter(d=>d.type===e).map(d=>p0(d.full_spec));if(h.length>0)return h.map(t);for(let d of s)d.active=!1}},"reducer")}i(qr,"updateState");var LC=Ct("modes/regular/setActive"),MC=Ct("modes/regular/setListenHost"),OC=Ct("modes/regular/setListenPort"),BH=[{active:!0,ui_id:Math.random()}],HH=on({name:"modes/regular",initialState:BH,reducers:{},extraReducers:i(e=>{_t(e,"active",LC),_t(e,"listen_host",MC),_t(e,"listen_port",OC),e.addCase(Gn,qr("regular",EC)),e.addCase(Yn,qr("regular",EC))},"extraReducers")}),QA=HH.reducer;var RC=Ct("modes/local/setActive"),yp=Ct("modes/local/setSelectedProcesses"),zH=[{active:!1,selectedProcesses:"",ui_id:Math.random()}],UH=on({name:"modes/local",initialState:zH,reducers:{},extraReducers:i(e=>{_t(e,"active",RC),_t(e,"selectedProcesses",yp),e.addCase(Gn,qr("local",bC)),e.addCase(Yn,qr("local",bC))},"extraReducers")}),JA=UH.reducer;var DC=Ct("modes/wireguard/setActive"),IC=Ct("modes/wireguard/setListenHost"),FC=Ct("modes/wireguard/setListenPort"),BC=Ct("modes/wireguard/setFilePath"),WH=[{active:!1,ui_id:Math.random()}],$H=on({name:"modes/wireguard",initialState:WH,reducers:{},extraReducers:i(e=>{_t(e,"active",DC),_t(e,"listen_host",IC),_t(e,"listen_port",FC),_t(e,"file_path",BC),e.addCase(Gn,qr("wireguard",TC)),e.addCase(Yn,qr("wireguard",TC))},"extraReducers")}),ZA=$H.reducer;var _0=Ct("modes/reverse/setActive"),HC=Ct("modes/reverse/setListenHost"),zC=Ct("modes/reverse/setListenPort"),UC=Ct("modes/reverse/setProtocol"),WC=Ct("modes/reverse/setDestination"),VH=[y0()],eL=on({name:"modes/reverse",initialState:VH,reducers:{addServer:i(e=>{e.push(y0())},"addServer"),removeServer:i((e,t)=>{let n=e.findIndex(s=>s.ui_id===t.payload.ui_id);n!==-1&&(e[n].active&&console.error("servers should be deactivated before removal"),e.splice(n,1))},"removeServer")},extraReducers:i(e=>{_t(e,"active",_0),_t(e,"listen_host",HC),_t(e,"listen_port",zC),_t(e,"protocol",UC),_t(e,"destination",WC),e.addCase(Gn,t),e.addCase(Yn,t);function t(n,s){if(s.payload.servers){let l=Object.fromEntries(Object.entries(s.payload.servers).filter(([d,v])=>v.type==="reverse").map(([d,v])=>[d,p0(d)])),h=[];for(let d of n){let v=vp(d),S=v in l;delete l[v],h.push({...d,active:S})}for(let d of Object.values(l))h.push(XA(d));return h.length>1&&Mo({...h[0],ui_id:void 0},{...y0(),ui_id:void 0})&&h.shift(),h}}i(t,"updateState")},"extraReducers")}),{addServer:tL,removeServer:rL}=eL.actions,nL=eL.reducer;var $C=Ct("modes/transparent/setActive"),VC=Ct("modes/transparent/setListenHost"),qC=Ct("modes/transparent/setListenPort"),qH=[{active:!1,ui_id:Math.random()}],jH=on({name:"modes/transparent",initialState:qH,reducers:{},extraReducers:i(e=>{_t(e,"active",$C),_t(e,"listen_host",VC),_t(e,"listen_port",qC),e.addCase(Gn,qr("transparent",kC)),e.addCase(Yn,qr("transparent",kC))},"extraReducers")}),iL=jH.reducer;var jC=Ct("modes/socks5/setActive"),KC=Ct("modes/socks5/setListenHost"),GC=Ct("modes/socks5/setListenPort"),KH=[{active:!1,ui_id:Math.random()}],GH=on({name:"modes/socks5",initialState:KH,reducers:{},extraReducers:i(e=>{_t(e,"active",jC),_t(e,"listen_host",KC),_t(e,"listen_port",GC),e.addCase(Gn,qr("socks5",NC)),e.addCase(Yn,qr("socks5",NC))},"extraReducers")}),oL=GH.reducer;var YC=Ct("modes/upstream/setActive"),XC=Ct("modes/upstream/setListenHost"),QC=Ct("modes/upstream/setListenPort"),JC=Ct("modes/upstream/setDestination"),YH=[{active:!1,destination:"",ui_id:Math.random()}],XH=on({name:"modes/upstream",initialState:YH,reducers:{},extraReducers:i(e=>{_t(e,"active",YC),_t(e,"listen_host",XC),_t(e,"listen_port",QC),_t(e,"destination",JC),e.addCase(Gn,qr("upstream",PC)),e.addCase(Yn,qr("upstream",PC))},"extraReducers")}),sL=XH.reducer;var ZC=Ct("modes/dns/setActive"),e_=Ct("modes/dns/setListenHost"),t_=Ct("modes/dns/setListenPort"),QH=[{active:!0,ui_id:Math.random()}],JH=on({name:"modes/dns",initialState:QH,reducers:{},extraReducers:i(e=>{_t(e,"active",ZC),_t(e,"listen_host",e_),_t(e,"listen_port",t_),e.addCase(Gn,qr("dns",AC)),e.addCase(Yn,qr("dns",AC))},"extraReducers")}),aL=JH.reducer;var ZH=dp({regular:QA,local:JA,wireguard:ZA,reverse:nL,transparent:iL,socks:oL,upstream:sL,dns:aL}),lL=ZH;var Cf=Im("fetchProcesses",async(e,{rejectWithValue:t})=>{try{return(await xt("/processes")).json()}catch(n){return t(n.message)}}),ez={currentProcesses:[],isLoading:!1},tz=on({name:"processes",initialState:ez,reducers:{},extraReducers:i(e=>{e.addCase(Cf.pending,t=>{t.isLoading=!0,t.error=void 0}),e.addCase(Cf.fulfilled,(t,n)=>{t.isLoading=!1,t.currentProcesses=n.payload}),e.addCase(Cf.rejected,(t,n)=>{t.isLoading=!1,t.error=n.payload})},"extraReducers")}),uL=tz.reducer;var rz={commandBar:CC,eventLog:gC,flows:Xx,connection:yC,modes:lL,ui:WA,options:xC,options_meta:YA,backendState:_C,processes:uL},_f=xA({reducer:rz,middleware:i(e=>e({immutableCheck:{warnAfter:5e5},serializableCheck:{warnAfter:5e5}}),"middleware")});var Io=de(be());var cL=de(Tm()),fL=de(ai());var r_=class extends Io.Component{constructor(){super(...arguments);this.container=Io.default.createRef();this.nameInput=Io.default.createRef();this.valueInput=Io.default.createRef();this.render=i(()=>{let[n,s]=this.props.item;return Io.default.createElement("div",{ref:this.container,className:"kv-row",onClick:this.onClick,onKeyDownCapture:this.onKeyDown},Io.default.createElement(gt,{ref:this.nameInput,className:"kv-key",content:n,onEditStart:this.props.onEditStart,onEditDone:l=>this.props.onEditDone([l,s])}),":\xA0",Io.default.createElement(gt,{ref:this.valueInput,className:"kv-value",content:s,onEditStart:this.props.onEditStart,onEditDone:l=>this.props.onEditDone([n,l]),placeholder:"empty"}))},"render");this.onClick=i(n=>{n.target===this.container.current&&this.props.onClickEmptyArea()},"onClick");this.onKeyDown=i(n=>{n.target===this.valueInput.current?.input.current&&n.key==="Tab"&&this.props.onTabNext()},"onKeyDown")}static{i(this,"Row")}},wp=class extends Io.Component{constructor(){super(...arguments);this.rowRefs={};this.state={currentList:this.props.data||[],initialList:this.props.data};this.render=i(()=>{this.rowRefs={};let n=this.state.currentList.map((s,l)=>Io.default.createElement(r_,{key:l,item:s,onEditStart:()=>this.currentlyEditing=l,onEditDone:h=>this.onEditDone(l,h),onClickEmptyArea:()=>this.onClickEmptyArea(l),onTabNext:()=>this.onTabNext(l),ref:h=>this.rowRefs[l]=h}));return Io.default.createElement("div",{className:(0,fL.default)("kv-editor",this.props.className),onMouseDown:this.onMouseDown},n,Io.default.createElement("div",{onClick:s=>{s.preventDefault(),this.onClickEmptyArea(this.state.currentList.length-1)},className:"kv-add-row fa fa-plus-square-o",role:"button","aria-label":"Add"}))},"render");this.onEditDone=i((n,s)=>{let l=[...this.state.currentList];s[0]?l[n]=s:l.splice(n,1),this.currentlyEditing=void 0,(0,cL.isEqual)(this.state.currentList,l)||this.props.onChange(l),this.setState({currentList:l})},"onEditDone");this.onClickEmptyArea=i(n=>{if(this.justFinishedEditing)return;let s=[...this.state.currentList];s.splice(n+1,0,["",""]),this.setState({currentList:s},()=>this.rowRefs[n+1]?.nameInput.current?.startEditing())},"onClickEmptyArea");this.onTabNext=i(n=>{n==this.state.currentList.length-1&&this.onClickEmptyArea(n)},"onTabNext");this.onMouseDown=i(n=>{this.justFinishedEditing=this.currentlyEditing},"onMouseDown")}static{i(this,"KeyValueListEditor")}static getDerivedStateFromProps(n,s){return n.data!==s.initialList?{currentList:n.data||[],initialList:n.data}:null}};var nr=de(be());var Bm=de(be());function E0(e,t){let[n,s]=(0,Bm.useState)(),[l,h]=(0,Bm.useState)();return(0,Bm.useEffect)(()=>{l&&l.abort();let d=new AbortController;return xt(e,{signal:d.signal}).then(v=>{if(!v.ok)throw`${v.status} ${v.statusText}`.trim();return v.text()}).then(v=>{s(v)}).catch(v=>{d.signal.aborted||s(`Error getting content: ${v}.`)}),h(d),()=>{d.signal.aborted||d.abort()}},[e,t]),n}i(E0,"useContent");var Hm=de(be());var b0=Hm.default.memo(i(function({icon:t,text:n,className:s,title:l,onOpenFile:h,onClick:d}){let v;return Hm.default.createElement("a",{href:"#",onClick:S=>{v.click(),d&&d(S)},className:s,title:l},Hm.default.createElement("i",{className:"fa fa-fw "+t}),n,Hm.default.createElement("input",{ref:S=>v=S,className:"hidden",type:"file",onChange:S=>{S.preventDefault(),S.target.files&&S.target.files.length>0&&h(S.target.files[0]),v.value=""}}))},"FileChooser"));var Sp=de(be()),dL=de(ai());function Or({onClick:e,children:t,icon:n,disabled:s,className:l,title:h}){return Sp.createElement("button",{className:(0,dL.default)(l,"btn btn-default"),onClick:s?void 0:e,disabled:s,title:h},n&&Sp.createElement(Sp.Fragment,null,Sp.createElement("i",{className:"fa "+n}),"\xA0"),t)}i(Or,"Button");var Wm=de(be()),yL=de(be());var zm=de(be()),mL=de(ai()),gL=de(pL()),vL=de(Tm());function hL(e){return e&&e.replace(/\r\n|\r/g,` +`)}i(hL,"normalizeLineEndings");var Um=class extends zm.Component{static{i(this,"CodeMirror")}constructor(t){super(t),this.state={isFocused:!1}}static{this.defaultProps={preserveScrollPosition:!1}}getCodeMirrorInstance(){return this.props.codeMirrorInstance||gL.default}UNSAFE_componentWillMount(){this.props.path&&console.error("Warning: react-codemirror: the `path` prop has been changed to `name`")}componentDidMount(){let t=this.getCodeMirrorInstance();this.codeMirror=t.fromTextArea(this.textareaNode,this.props.options),this.codeMirror.on("change",this.codemirrorValueChanged.bind(this)),this.codeMirror.on("cursorActivity",this.cursorActivity.bind(this)),this.codeMirror.on("focus",this.focusChanged.bind(this,!0)),this.codeMirror.on("blur",this.focusChanged.bind(this,!1)),this.codeMirror.on("scroll",this.scrollChanged.bind(this)),this.codeMirror.setValue(this.props.defaultValue||this.props.value||"")}componentWillUnmount(){this.codeMirror&&this.codeMirror.toTextArea()}UNSAFE_componentWillReceiveProps(t){if(this.codeMirror&&t.value!==void 0&&t.value!==this.props.value&&hL(this.codeMirror.getValue())!==hL(t.value))if(this.props.preserveScrollPosition){let n=this.codeMirror.getScrollInfo();this.codeMirror.setValue(t.value),this.codeMirror.scrollTo(n.left,n.top)}else this.codeMirror.setValue(t.value);if(typeof t.options=="object")for(let n in t.options)t.options.hasOwnProperty(n)&&this.setOptionIfChanged(n,t.options[n])}setOptionIfChanged(t,n){let s=this.codeMirror.getOption(t);(0,vL.isEqual)(s,n)||this.codeMirror.setOption(t,n)}getCodeMirror(){return this.codeMirror}focus(){this.codeMirror&&this.codeMirror.focus()}focusChanged(t){this.setState({isFocused:t}),this.props.onFocusChange&&this.props.onFocusChange(t)}cursorActivity(t){this.props.onCursorActivity&&this.props.onCursorActivity(t)}scrollChanged(t){this.props.onScroll&&this.props.onScroll(t.getScrollInfo())}codemirrorValueChanged(t,n){this.props.onChange&&n.origin!=="setValue"&&this.props.onChange(t.getValue(),n)}render(){let t=(0,mL.default)("ReactCodeMirror",this.state.isFocused?"ReactCodeMirror--focused":null,this.props.className);return zm.createElement("div",{className:t},zm.createElement("textarea",{ref:n=>this.textareaNode=n,name:this.props.name||this.props.path,defaultValue:this.props.value,autoComplete:"off",autoFocus:this.props.autoFocus}))}};var $m=class extends yL.Component{constructor(){super(...arguments);this.editor=Wm.createRef();this.getContent=i(()=>this.editor.current?.codeMirror.getValue(),"getContent");this.render=i(()=>{let n={lineNumbers:!0};return Wm.createElement("div",{className:"codeeditor",onKeyDown:s=>s.stopPropagation()},Wm.createElement(Um,{ref:this.editor,value:this.props.initialContent,onChange:()=>0,options:n}))},"render")}static{i(this,"CodeEditor")}};var Ef=de(be());var nz=Ef.default.memo(i(function({lines:t,maxLines:n,showMore:s}){return t.length===0?null:Ef.default.createElement("pre",null,t.map((l,h)=>h===n?Ef.default.createElement("button",{key:"showmore",onClick:s,className:"btn btn-xs btn-info"},Ef.default.createElement("i",{className:"fa fa-angle-double-down","aria-hidden":"true"})," ","Show more"):Ef.default.createElement("div",{key:h},l.map(([d,v],S)=>Ef.default.createElement("span",{key:S,className:d},v)))))},"LineRenderer")),T0=nz;var Of=de(be());var Ni=de(be());var k0=de(be());var o_=i(function(t){return t.reduce(function(n,s){var l=s[0],h=s[1];return n[l]=h,n},{})},"fromEntries"),s_=typeof window<"u"&&window.document&&window.document.createElement?k0.useLayoutEffect:k0.useEffect;var Gu=de(be()),WL=de(Sx());var Rr="top",Cn="bottom",sn="right",jr="left",N0="auto",ju=[Rr,Cn,sn,jr],Sl="start",bf="end",wL="clippingParents",P0="viewport",xp="popper",SL="reference",a_=ju.reduce(function(e,t){return e.concat([t+"-"+Sl,t+"-"+bf])},[]),A0=[].concat(ju,[N0]).reduce(function(e,t){return e.concat([t,t+"-"+Sl,t+"-"+bf])},[]),iz="beforeRead",oz="read",sz="afterRead",az="beforeMain",lz="main",uz="afterMain",cz="beforeWrite",fz="write",dz="afterWrite",xL=[iz,oz,sz,az,lz,uz,cz,fz,dz];function Dn(e){return e?(e.nodeName||"").toLowerCase():null}i(Dn,"getNodeName");function mr(e){if(e==null)return window;if(e.toString()!=="[object Window]"){var t=e.ownerDocument;return t&&t.defaultView||window}return e}i(mr,"getWindow");function cs(e){var t=mr(e).Element;return e instanceof t||e instanceof Element}i(cs,"isElement");function _n(e){var t=mr(e).HTMLElement;return e instanceof t||e instanceof HTMLElement}i(_n,"isHTMLElement");function Cp(e){if(typeof ShadowRoot>"u")return!1;var t=mr(e).ShadowRoot;return e instanceof t||e instanceof ShadowRoot}i(Cp,"isShadowRoot");function pz(e){var t=e.state;Object.keys(t.elements).forEach(function(n){var s=t.styles[n]||{},l=t.attributes[n]||{},h=t.elements[n];!_n(h)||!Dn(h)||(Object.assign(h.style,s),Object.keys(l).forEach(function(d){var v=l[d];v===!1?h.removeAttribute(d):h.setAttribute(d,v===!0?"":v)}))})}i(pz,"applyStyles");function hz(e){var t=e.state,n={popper:{position:t.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(t.elements.popper.style,n.popper),t.styles=n,t.elements.arrow&&Object.assign(t.elements.arrow.style,n.arrow),function(){Object.keys(t.elements).forEach(function(s){var l=t.elements[s],h=t.attributes[s]||{},d=Object.keys(t.styles.hasOwnProperty(s)?t.styles[s]:n[s]),v=d.reduce(function(S,b){return S[b]="",S},{});!_n(l)||!Dn(l)||(Object.assign(l.style,v),Object.keys(h).forEach(function(S){l.removeAttribute(S)}))})}}i(hz,"effect");var CL={name:"applyStyles",enabled:!0,phase:"write",fn:pz,effect:hz,requires:["computeStyles"]};function In(e){return e.split("-")[0]}i(In,"getBasePlacement");var Us=Math.max,Tf=Math.min,xl=Math.round;function _p(){var e=navigator.userAgentData;return e!=null&&e.brands&&Array.isArray(e.brands)?e.brands.map(function(t){return t.brand+"/"+t.version}).join(" "):navigator.userAgent}i(_p,"getUAString");function Vm(){return!/^((?!chrome|android).)*safari/i.test(_p())}i(Vm,"isLayoutViewport");function fs(e,t,n){t===void 0&&(t=!1),n===void 0&&(n=!1);var s=e.getBoundingClientRect(),l=1,h=1;t&&_n(e)&&(l=e.offsetWidth>0&&xl(s.width)/e.offsetWidth||1,h=e.offsetHeight>0&&xl(s.height)/e.offsetHeight||1);var d=cs(e)?mr(e):window,v=d.visualViewport,S=!Vm()&&n,b=(s.left+(S&&v?v.offsetLeft:0))/l,k=(s.top+(S&&v?v.offsetTop:0))/h,R=s.width/l,I=s.height/h;return{width:R,height:I,top:k,right:b+R,bottom:k+I,left:b,x:b,y:k}}i(fs,"getBoundingClientRect");function kf(e){var t=fs(e),n=e.offsetWidth,s=e.offsetHeight;return Math.abs(t.width-n)<=1&&(n=t.width),Math.abs(t.height-s)<=1&&(s=t.height),{x:e.offsetLeft,y:e.offsetTop,width:n,height:s}}i(kf,"getLayoutRect");function qm(e,t){var n=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(n&&Cp(n)){var s=t;do{if(s&&e.isSameNode(s))return!0;s=s.parentNode||s.host}while(s)}return!1}i(qm,"contains");function ki(e){return mr(e).getComputedStyle(e)}i(ki,"getComputedStyle");function l_(e){return["table","td","th"].indexOf(Dn(e))>=0}i(l_,"isTableElement");function Xn(e){return((cs(e)?e.ownerDocument:e.document)||window.document).documentElement}i(Xn,"getDocumentElement");function Cl(e){return Dn(e)==="html"?e:e.assignedSlot||e.parentNode||(Cp(e)?e.host:null)||Xn(e)}i(Cl,"getParentNode");function _L(e){return!_n(e)||ki(e).position==="fixed"?null:e.offsetParent}i(_L,"getTrueOffsetParent");function mz(e){var t=/firefox/i.test(_p()),n=/Trident/i.test(_p());if(n&&_n(e)){var s=ki(e);if(s.position==="fixed")return null}var l=Cl(e);for(Cp(l)&&(l=l.host);_n(l)&&["html","body"].indexOf(Dn(l))<0;){var h=ki(l);if(h.transform!=="none"||h.perspective!=="none"||h.contain==="paint"||["transform","perspective"].indexOf(h.willChange)!==-1||t&&h.willChange==="filter"||t&&h.filter&&h.filter!=="none")return l;l=l.parentNode}return null}i(mz,"getContainingBlock");function Ws(e){for(var t=mr(e),n=_L(e);n&&l_(n)&&ki(n).position==="static";)n=_L(n);return n&&(Dn(n)==="html"||Dn(n)==="body"&&ki(n).position==="static")?t:n||mz(e)||t}i(Ws,"getOffsetParent");function Nf(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}i(Nf,"getMainAxisFromPlacement");function Pf(e,t,n){return Us(e,Tf(t,n))}i(Pf,"within");function EL(e,t,n){var s=Pf(e,t,n);return s>n?n:s}i(EL,"withinMaxClamp");function jm(){return{top:0,right:0,bottom:0,left:0}}i(jm,"getFreshSideObject");function Km(e){return Object.assign({},jm(),e)}i(Km,"mergePaddingObject");function Gm(e,t){return t.reduce(function(n,s){return n[s]=e,n},{})}i(Gm,"expandToHashMap");var gz=i(function(t,n){return t=typeof t=="function"?t(Object.assign({},n.rects,{placement:n.placement})):t,Km(typeof t!="number"?t:Gm(t,ju))},"toPaddingObject");function vz(e){var t,n=e.state,s=e.name,l=e.options,h=n.elements.arrow,d=n.modifiersData.popperOffsets,v=In(n.placement),S=Nf(v),b=[jr,sn].indexOf(v)>=0,k=b?"height":"width";if(!(!h||!d)){var R=gz(l.padding,n),I=kf(h),z=S==="y"?Rr:jr,j=S==="y"?Cn:sn,Y=n.rects.reference[k]+n.rects.reference[S]-d[S]-n.rects.popper[k],ne=d[S]-n.rects.reference[S],F=Ws(h),L=F?S==="y"?F.clientHeight||0:F.clientWidth||0:0,D=Y/2-ne/2,q=R[z],te=L-I[k]-R[j],Z=L/2-I[k]/2+D,oe=Pf(q,Z,te),he=S;n.modifiersData[s]=(t={},t[he]=oe,t.centerOffset=oe-Z,t)}}i(vz,"arrow");function yz(e){var t=e.state,n=e.options,s=n.element,l=s===void 0?"[data-popper-arrow]":s;l!=null&&(typeof l=="string"&&(l=t.elements.popper.querySelector(l),!l)||qm(t.elements.popper,l)&&(t.elements.arrow=l))}i(yz,"effect");var bL={name:"arrow",enabled:!0,phase:"main",fn:vz,effect:yz,requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function ds(e){return e.split("-")[1]}i(ds,"getVariation");var wz={top:"auto",right:"auto",bottom:"auto",left:"auto"};function Sz(e,t){var n=e.x,s=e.y,l=t.devicePixelRatio||1;return{x:xl(n*l)/l||0,y:xl(s*l)/l||0}}i(Sz,"roundOffsetsByDPR");function TL(e){var t,n=e.popper,s=e.popperRect,l=e.placement,h=e.variation,d=e.offsets,v=e.position,S=e.gpuAcceleration,b=e.adaptive,k=e.roundOffsets,R=e.isFixed,I=d.x,z=I===void 0?0:I,j=d.y,Y=j===void 0?0:j,ne=typeof k=="function"?k({x:z,y:Y}):{x:z,y:Y};z=ne.x,Y=ne.y;var F=d.hasOwnProperty("x"),L=d.hasOwnProperty("y"),D=jr,q=Rr,te=window;if(b){var Z=Ws(n),oe="clientHeight",he="clientWidth";if(Z===mr(n)&&(Z=Xn(n),ki(Z).position!=="static"&&v==="absolute"&&(oe="scrollHeight",he="scrollWidth")),Z=Z,l===Rr||(l===jr||l===sn)&&h===bf){q=Cn;var Re=R&&Z===te&&te.visualViewport?te.visualViewport.height:Z[oe];Y-=Re-s.height,Y*=S?1:-1}if(l===jr||(l===Rr||l===Cn)&&h===bf){D=sn;var Ee=R&&Z===te&&te.visualViewport?te.visualViewport.width:Z[he];z-=Ee-s.width,z*=S?1:-1}}var Ye=Object.assign({position:v},b&&wz),tt=k===!0?Sz({x:z,y:Y},mr(n)):{x:z,y:Y};if(z=tt.x,Y=tt.y,S){var xe;return Object.assign({},Ye,(xe={},xe[q]=L?"0":"",xe[D]=F?"0":"",xe.transform=(te.devicePixelRatio||1)<=1?"translate("+z+"px, "+Y+"px)":"translate3d("+z+"px, "+Y+"px, 0)",xe))}return Object.assign({},Ye,(t={},t[q]=L?Y+"px":"",t[D]=F?z+"px":"",t.transform="",t))}i(TL,"mapToStyles");function xz(e){var t=e.state,n=e.options,s=n.gpuAcceleration,l=s===void 0?!0:s,h=n.adaptive,d=h===void 0?!0:h,v=n.roundOffsets,S=v===void 0?!0:v,b={placement:In(t.placement),variation:ds(t.placement),popper:t.elements.popper,popperRect:t.rects.popper,gpuAcceleration:l,isFixed:t.options.strategy==="fixed"};t.modifiersData.popperOffsets!=null&&(t.styles.popper=Object.assign({},t.styles.popper,TL(Object.assign({},b,{offsets:t.modifiersData.popperOffsets,position:t.options.strategy,adaptive:d,roundOffsets:S})))),t.modifiersData.arrow!=null&&(t.styles.arrow=Object.assign({},t.styles.arrow,TL(Object.assign({},b,{offsets:t.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:S})))),t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-placement":t.placement})}i(xz,"computeStyles");var kL={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:xz,data:{}};var L0={passive:!0};function Cz(e){var t=e.state,n=e.instance,s=e.options,l=s.scroll,h=l===void 0?!0:l,d=s.resize,v=d===void 0?!0:d,S=mr(t.elements.popper),b=[].concat(t.scrollParents.reference,t.scrollParents.popper);return h&&b.forEach(function(k){k.addEventListener("scroll",n.update,L0)}),v&&S.addEventListener("resize",n.update,L0),function(){h&&b.forEach(function(k){k.removeEventListener("scroll",n.update,L0)}),v&&S.removeEventListener("resize",n.update,L0)}}i(Cz,"effect");var NL={name:"eventListeners",enabled:!0,phase:"write",fn:i(function(){},"fn"),effect:Cz,data:{}};var _z={left:"right",right:"left",bottom:"top",top:"bottom"};function Ep(e){return e.replace(/left|right|bottom|top/g,function(t){return _z[t]})}i(Ep,"getOppositePlacement");var Ez={start:"end",end:"start"};function M0(e){return e.replace(/start|end/g,function(t){return Ez[t]})}i(M0,"getOppositeVariationPlacement");function Af(e){var t=mr(e),n=t.pageXOffset,s=t.pageYOffset;return{scrollLeft:n,scrollTop:s}}i(Af,"getWindowScroll");function Lf(e){return fs(Xn(e)).left+Af(e).scrollLeft}i(Lf,"getWindowScrollBarX");function u_(e,t){var n=mr(e),s=Xn(e),l=n.visualViewport,h=s.clientWidth,d=s.clientHeight,v=0,S=0;if(l){h=l.width,d=l.height;var b=Vm();(b||!b&&t==="fixed")&&(v=l.offsetLeft,S=l.offsetTop)}return{width:h,height:d,x:v+Lf(e),y:S}}i(u_,"getViewportRect");function c_(e){var t,n=Xn(e),s=Af(e),l=(t=e.ownerDocument)==null?void 0:t.body,h=Us(n.scrollWidth,n.clientWidth,l?l.scrollWidth:0,l?l.clientWidth:0),d=Us(n.scrollHeight,n.clientHeight,l?l.scrollHeight:0,l?l.clientHeight:0),v=-s.scrollLeft+Lf(e),S=-s.scrollTop;return ki(l||n).direction==="rtl"&&(v+=Us(n.clientWidth,l?l.clientWidth:0)-h),{width:h,height:d,x:v,y:S}}i(c_,"getDocumentRect");function Mf(e){var t=ki(e),n=t.overflow,s=t.overflowX,l=t.overflowY;return/auto|scroll|overlay|hidden/.test(n+l+s)}i(Mf,"isScrollParent");function O0(e){return["html","body","#document"].indexOf(Dn(e))>=0?e.ownerDocument.body:_n(e)&&Mf(e)?e:O0(Cl(e))}i(O0,"getScrollParent");function Ku(e,t){var n;t===void 0&&(t=[]);var s=O0(e),l=s===((n=e.ownerDocument)==null?void 0:n.body),h=mr(s),d=l?[h].concat(h.visualViewport||[],Mf(s)?s:[]):s,v=t.concat(d);return l?v:v.concat(Ku(Cl(d)))}i(Ku,"listScrollParents");function bp(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}i(bp,"rectToClientRect");function bz(e,t){var n=fs(e,!1,t==="fixed");return n.top=n.top+e.clientTop,n.left=n.left+e.clientLeft,n.bottom=n.top+e.clientHeight,n.right=n.left+e.clientWidth,n.width=e.clientWidth,n.height=e.clientHeight,n.x=n.left,n.y=n.top,n}i(bz,"getInnerBoundingClientRect");function PL(e,t,n){return t===P0?bp(u_(e,n)):cs(t)?bz(t,n):bp(c_(Xn(e)))}i(PL,"getClientRectFromMixedType");function Tz(e){var t=Ku(Cl(e)),n=["absolute","fixed"].indexOf(ki(e).position)>=0,s=n&&_n(e)?Ws(e):e;return cs(s)?t.filter(function(l){return cs(l)&&qm(l,s)&&Dn(l)!=="body"}):[]}i(Tz,"getClippingParents");function f_(e,t,n,s){var l=t==="clippingParents"?Tz(e):[].concat(t),h=[].concat(l,[n]),d=h[0],v=h.reduce(function(S,b){var k=PL(e,b,s);return S.top=Us(k.top,S.top),S.right=Tf(k.right,S.right),S.bottom=Tf(k.bottom,S.bottom),S.left=Us(k.left,S.left),S},PL(e,d,s));return v.width=v.right-v.left,v.height=v.bottom-v.top,v.x=v.left,v.y=v.top,v}i(f_,"getClippingRect");function Ym(e){var t=e.reference,n=e.element,s=e.placement,l=s?In(s):null,h=s?ds(s):null,d=t.x+t.width/2-n.width/2,v=t.y+t.height/2-n.height/2,S;switch(l){case Rr:S={x:d,y:t.y-n.height};break;case Cn:S={x:d,y:t.y+t.height};break;case sn:S={x:t.x+t.width,y:v};break;case jr:S={x:t.x-n.width,y:v};break;default:S={x:t.x,y:t.y}}var b=l?Nf(l):null;if(b!=null){var k=b==="y"?"height":"width";switch(h){case Sl:S[b]=S[b]-(t[k]/2-n[k]/2);break;case bf:S[b]=S[b]+(t[k]/2-n[k]/2);break;default:}}return S}i(Ym,"computeOffsets");function $s(e,t){t===void 0&&(t={});var n=t,s=n.placement,l=s===void 0?e.placement:s,h=n.strategy,d=h===void 0?e.strategy:h,v=n.boundary,S=v===void 0?wL:v,b=n.rootBoundary,k=b===void 0?P0:b,R=n.elementContext,I=R===void 0?xp:R,z=n.altBoundary,j=z===void 0?!1:z,Y=n.padding,ne=Y===void 0?0:Y,F=Km(typeof ne!="number"?ne:Gm(ne,ju)),L=I===xp?SL:xp,D=e.rects.popper,q=e.elements[j?L:I],te=f_(cs(q)?q:q.contextElement||Xn(e.elements.popper),S,k,d),Z=fs(e.elements.reference),oe=Ym({reference:Z,element:D,strategy:"absolute",placement:l}),he=bp(Object.assign({},D,oe)),Re=I===xp?he:Z,Ee={top:te.top-Re.top+F.top,bottom:Re.bottom-te.bottom+F.bottom,left:te.left-Re.left+F.left,right:Re.right-te.right+F.right},Ye=e.modifiersData.offset;if(I===xp&&Ye){var tt=Ye[l];Object.keys(Ee).forEach(function(xe){var Xe=[sn,Cn].indexOf(xe)>=0?1:-1,je=[Rr,Cn].indexOf(xe)>=0?"y":"x";Ee[xe]+=tt[je]*Xe})}return Ee}i($s,"detectOverflow");function d_(e,t){t===void 0&&(t={});var n=t,s=n.placement,l=n.boundary,h=n.rootBoundary,d=n.padding,v=n.flipVariations,S=n.allowedAutoPlacements,b=S===void 0?A0:S,k=ds(s),R=k?v?a_:a_.filter(function(j){return ds(j)===k}):ju,I=R.filter(function(j){return b.indexOf(j)>=0});I.length===0&&(I=R);var z=I.reduce(function(j,Y){return j[Y]=$s(e,{placement:Y,boundary:l,rootBoundary:h,padding:d})[In(Y)],j},{});return Object.keys(z).sort(function(j,Y){return z[j]-z[Y]})}i(d_,"computeAutoPlacement");function kz(e){if(In(e)===N0)return[];var t=Ep(e);return[M0(e),t,M0(t)]}i(kz,"getExpandedFallbackPlacements");function Nz(e){var t=e.state,n=e.options,s=e.name;if(!t.modifiersData[s]._skip){for(var l=n.mainAxis,h=l===void 0?!0:l,d=n.altAxis,v=d===void 0?!0:d,S=n.fallbackPlacements,b=n.padding,k=n.boundary,R=n.rootBoundary,I=n.altBoundary,z=n.flipVariations,j=z===void 0?!0:z,Y=n.allowedAutoPlacements,ne=t.options.placement,F=In(ne),L=F===ne,D=S||(L||!j?[Ep(ne)]:kz(ne)),q=[ne].concat(D).reduce(function(vr,yr){return vr.concat(In(yr)===N0?d_(t,{placement:yr,boundary:k,rootBoundary:R,padding:b,flipVariations:j,allowedAutoPlacements:Y}):yr)},[]),te=t.rects.reference,Z=t.rects.popper,oe=new Map,he=!0,Re=q[0],Ee=0;Ee=0,je=Xe?"width":"height",Qe=$s(t,{placement:Ye,boundary:k,rootBoundary:R,altBoundary:I,padding:b}),ot=Xe?xe?sn:jr:xe?Cn:Rr;te[je]>Z[je]&&(ot=Ep(ot));var It=Ep(ot),Pt=[];if(h&&Pt.push(Qe[tt]<=0),v&&Pt.push(Qe[ot]<=0,Qe[It]<=0),Pt.every(function(vr){return vr})){Re=Ye,he=!1;break}oe.set(Ye,Pt)}if(he)for(var fn=j?3:1,dn=i(function(yr){var Gt=q.find(function(Ft){var se=oe.get(Ft);if(se)return se.slice(0,yr).every(function(Ue){return Ue})});if(Gt)return Re=Gt,"break"},"_loop"),gr=fn;gr>0;gr--){var Wt=dn(gr);if(Wt==="break")break}t.placement!==Re&&(t.modifiersData[s]._skip=!0,t.placement=Re,t.reset=!0)}}i(Nz,"flip");var AL={name:"flip",enabled:!0,phase:"main",fn:Nz,requiresIfExists:["offset"],data:{_skip:!1}};function LL(e,t,n){return n===void 0&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}i(LL,"getSideOffsets");function ML(e){return[Rr,sn,Cn,jr].some(function(t){return e[t]>=0})}i(ML,"isAnySideFullyClipped");function Pz(e){var t=e.state,n=e.name,s=t.rects.reference,l=t.rects.popper,h=t.modifiersData.preventOverflow,d=$s(t,{elementContext:"reference"}),v=$s(t,{altBoundary:!0}),S=LL(d,s),b=LL(v,l,h),k=ML(S),R=ML(b);t.modifiersData[n]={referenceClippingOffsets:S,popperEscapeOffsets:b,isReferenceHidden:k,hasPopperEscaped:R},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":k,"data-popper-escaped":R})}i(Pz,"hide");var OL={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:Pz};function Az(e,t,n){var s=In(e),l=[jr,Rr].indexOf(s)>=0?-1:1,h=typeof n=="function"?n(Object.assign({},t,{placement:e})):n,d=h[0],v=h[1];return d=d||0,v=(v||0)*l,[jr,sn].indexOf(s)>=0?{x:v,y:d}:{x:d,y:v}}i(Az,"distanceAndSkiddingToXY");function Lz(e){var t=e.state,n=e.options,s=e.name,l=n.offset,h=l===void 0?[0,0]:l,d=A0.reduce(function(k,R){return k[R]=Az(R,t.rects,h),k},{}),v=d[t.placement],S=v.x,b=v.y;t.modifiersData.popperOffsets!=null&&(t.modifiersData.popperOffsets.x+=S,t.modifiersData.popperOffsets.y+=b),t.modifiersData[s]=d}i(Lz,"offset");var RL={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:Lz};function Mz(e){var t=e.state,n=e.name;t.modifiersData[n]=Ym({reference:t.rects.reference,element:t.rects.popper,strategy:"absolute",placement:t.placement})}i(Mz,"popperOffsets");var DL={name:"popperOffsets",enabled:!0,phase:"read",fn:Mz,data:{}};function p_(e){return e==="x"?"y":"x"}i(p_,"getAltAxis");function Oz(e){var t=e.state,n=e.options,s=e.name,l=n.mainAxis,h=l===void 0?!0:l,d=n.altAxis,v=d===void 0?!1:d,S=n.boundary,b=n.rootBoundary,k=n.altBoundary,R=n.padding,I=n.tether,z=I===void 0?!0:I,j=n.tetherOffset,Y=j===void 0?0:j,ne=$s(t,{boundary:S,rootBoundary:b,padding:R,altBoundary:k}),F=In(t.placement),L=ds(t.placement),D=!L,q=Nf(F),te=p_(q),Z=t.modifiersData.popperOffsets,oe=t.rects.reference,he=t.rects.popper,Re=typeof Y=="function"?Y(Object.assign({},t.rects,{placement:t.placement})):Y,Ee=typeof Re=="number"?{mainAxis:Re,altAxis:Re}:Object.assign({mainAxis:0,altAxis:0},Re),Ye=t.modifiersData.offset?t.modifiersData.offset[t.placement]:null,tt={x:0,y:0};if(Z){if(h){var xe,Xe=q==="y"?Rr:jr,je=q==="y"?Cn:sn,Qe=q==="y"?"height":"width",ot=Z[q],It=ot+ne[Xe],Pt=ot-ne[je],fn=z?-he[Qe]/2:0,dn=L===Sl?oe[Qe]:he[Qe],gr=L===Sl?-he[Qe]:-oe[Qe],Wt=t.elements.arrow,vr=z&&Wt?kf(Wt):{width:0,height:0},yr=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:jm(),Gt=yr[Xe],Ft=yr[je],se=Pf(0,oe[Qe],vr[Qe]),Ue=D?oe[Qe]/2-fn-se-Gt-Ee.mainAxis:dn-se-Gt-Ee.mainAxis,Gr=D?-oe[Qe]/2+fn+se+Ft+Ee.mainAxis:gr+se+Ft+Ee.mainAxis,Zt=t.elements.arrow&&Ws(t.elements.arrow),st=Zt?q==="y"?Zt.clientTop||0:Zt.clientLeft||0:0,Fe=(xe=Ye?.[q])!=null?xe:0,Fn=ot+Ue-Fe-st,bn=ot+Gr-Fe,no=Pf(z?Tf(It,Fn):It,ot,z?Us(Pt,bn):Pt);Z[q]=no,tt[q]=no-ot}if(v){var Ho,lt=q==="x"?Rr:jr,wr=q==="x"?Cn:sn,fr=Z[te],pt=te==="y"?"height":"width",io=fr+ne[lt],Bn=fr-ne[wr],Mi=[Rr,jr].indexOf(F)!==-1,Yr=(Ho=Ye?.[te])!=null?Ho:0,Xr=Mi?io:fr-oe[pt]-he[pt]-Yr+Ee.altAxis,oo=Mi?fr+oe[pt]+he[pt]-Yr-Ee.altAxis:Bn,hi=z&&Mi?EL(Xr,fr,oo):Pf(z?Xr:io,fr,z?oo:Bn);Z[te]=hi,tt[te]=hi-fr}t.modifiersData[s]=tt}}i(Oz,"preventOverflow");var IL={name:"preventOverflow",enabled:!0,phase:"main",fn:Oz,requiresIfExists:["offset"]};function h_(e){return{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}}i(h_,"getHTMLElementScroll");function m_(e){return e===mr(e)||!_n(e)?Af(e):h_(e)}i(m_,"getNodeScroll");function Rz(e){var t=e.getBoundingClientRect(),n=xl(t.width)/e.offsetWidth||1,s=xl(t.height)/e.offsetHeight||1;return n!==1||s!==1}i(Rz,"isElementScaled");function g_(e,t,n){n===void 0&&(n=!1);var s=_n(t),l=_n(t)&&Rz(t),h=Xn(t),d=fs(e,l,n),v={scrollLeft:0,scrollTop:0},S={x:0,y:0};return(s||!s&&!n)&&((Dn(t)!=="body"||Mf(h))&&(v=m_(t)),_n(t)?(S=fs(t,!0),S.x+=t.clientLeft,S.y+=t.clientTop):h&&(S.x=Lf(h))),{x:d.left+v.scrollLeft-S.x,y:d.top+v.scrollTop-S.y,width:d.width,height:d.height}}i(g_,"getCompositeRect");function Dz(e){var t=new Map,n=new Set,s=[];e.forEach(function(h){t.set(h.name,h)});function l(h){n.add(h.name);var d=[].concat(h.requires||[],h.requiresIfExists||[]);d.forEach(function(v){if(!n.has(v)){var S=t.get(v);S&&l(S)}}),s.push(h)}return i(l,"sort"),e.forEach(function(h){n.has(h.name)||l(h)}),s}i(Dz,"order");function v_(e){var t=Dz(e);return xL.reduce(function(n,s){return n.concat(t.filter(function(l){return l.phase===s}))},[])}i(v_,"orderModifiers");function y_(e){var t;return function(){return t||(t=new Promise(function(n){Promise.resolve().then(function(){t=void 0,n(e())})})),t}}i(y_,"debounce");function w_(e){var t=e.reduce(function(n,s){var l=n[s.name];return n[s.name]=l?Object.assign({},l,s,{options:Object.assign({},l.options,s.options),data:Object.assign({},l.data,s.data)}):s,n},{});return Object.keys(t).map(function(n){return t[n]})}i(w_,"mergeByName");var FL={placement:"bottom",modifiers:[],strategy:"absolute"};function BL(){for(var e=arguments.length,t=new Array(e),n=0;nNi.default.createElement("li",{role:"separator",className:"divider"}),"Divider");function li({onClick:e,children:t,...n}){return Ni.default.createElement("li",null,Ni.default.createElement("a",{href:"#",onClick:i(l=>{l.preventDefault(),e()},"click"),...n},t))}i(li,"MenuItem");var Yu=Ni.default.memo(i(function({text:t,children:n,options:s,className:l,onOpen:h,...d}){let[v,S]=(0,Ni.useState)(null),[b,k]=(0,Ni.useState)(!1),[R,I]=(0,Ni.useState)(null),{styles:z,attributes:j}=x_(v,R,{...s}),Y=i(F=>{k(F),h&&h(F)},"setOpen");(0,Ni.useEffect)(()=>{R&&document.addEventListener("click",F=>{R.contains(F.target)?document.addEventListener("click",()=>Y(!1),{once:!0}):(F.preventDefault(),F.stopPropagation(),Y(!1))},{once:!0,capture:!0})},[R]);let ne;return b?ne=Ni.default.createElement("ul",{className:"dropdown-menu show",ref:I,style:z.popper,...j.popper},n):ne=null,Ni.default.createElement(Ni.default.Fragment,null,Ni.default.createElement("a",{href:"#",ref:S,className:(0,VL.default)(l,{open:b}),onClick:F=>{F.preventDefault(),Y(!0)},...d},t),ne)},"Dropdown"));function Xm({value:e,onChange:t}){let n=me(l=>l.backendState.contentViews||[]),s=Of.default.createElement("span",null,Of.default.createElement("i",{className:"fa fa-fw fa-files-o"}),"\xA0",Of.default.createElement("b",null,"View:")," ",e.toLowerCase()," ",Of.default.createElement("span",{className:"caret"}));return Of.default.createElement(Yu,{text:s,className:"btn btn-default btn-xs",options:{placement:"top-end"}},n.map(l=>Of.default.createElement(li,{key:l,onClick:()=>t(l)},l.toLowerCase().replace("_"," "))))}i(Xm,"ViewSelector");function __({flow:e,message:t}){let n=Be(),s=e.request===t?"request":"response",l=me(j=>j.ui.flow.contentViewFor[e.id+s]||"Auto"),h=(0,nr.useRef)(null),[d,v]=(0,nr.useState)(me(j=>j.options.content_view_lines_cutoff)),S=(0,nr.useCallback)(()=>v(Math.max(1024,d*2)),[d]),[b,k]=(0,nr.useState)(!1),R;b?R=Rn.getContentURL(e,t):R=Rn.getContentURL(e,t,l,d+1);let I=E0(R,t.contentHash),z=(0,nr.useMemo)(()=>{if(I&&!b)try{return JSON.parse(I)}catch{return{description:"Network Error",lines:[[["error",`${I}`]]]}}else return},[I]);if(b)return nr.default.createElement("div",{className:"contentview",key:"edit"},nr.default.createElement("div",{className:"controls"},nr.default.createElement("h5",null,"[Editing]"),nr.default.createElement(Or,{onClick:i(async()=>{let Y=h.current?.getContent();await n(si(e,{[s]:{content:Y}})),k(!1)},"save"),icon:"fa-check text-success",className:"btn-xs"},"Done"),"\xA0",nr.default.createElement(Or,{onClick:()=>k(!1),icon:"fa-times text-danger",className:"btn-xs"},"Cancel")),nr.default.createElement($m,{ref:h,initialContent:I||""}));{let j=z?z.description:"Loading...";return nr.default.createElement("div",{className:"contentview",key:"view"},nr.default.createElement("div",{className:"controls"},nr.default.createElement("h5",null,j),nr.default.createElement(Or,{onClick:()=>k(!0),icon:"fa-edit",className:"btn-xs"},"Edit"),"\xA0",nr.default.createElement(b0,{icon:"fa-upload",text:"Replace",title:"Upload a file to replace the content.",onOpenFile:Y=>n(VP(e,Y,s)),className:"btn btn-default btn-xs"}),"\xA0",nr.default.createElement(Xm,{value:l,onChange:Y=>n(Iy(e.id+s,Y))})),C_.matches(t)&&nr.default.createElement(C_,{flow:e,message:t}),nr.default.createElement(T0,{lines:z?.lines||[],maxLines:d,showMore:S}))}}i(__,"HttpMessage");var Wz=/^image\/(png|jpe?g|gif|webp|vnc.microsoft.icon|x-icon|svg\+xml)$/i;C_.matches=e=>Wz.test(Rn.getContentType(e)||"");function C_({flow:e,message:t}){return nr.default.createElement("div",{className:"flowview-image"},nr.default.createElement("img",{src:Rn.getContentURL(e,t),alt:"preview",className:"img-thumbnail"}))}i(C_,"ViewImage");function $z({flow:e}){let t=Be();return qt.createElement("div",{className:"first-line request-line"},qt.createElement("div",null,qt.createElement(wf,{content:e.request.method,onEditDone:n=>t(si(e,{request:{method:n}})),isValid:n=>n.length>0}),"\xA0",qt.createElement(wf,{content:Bs.pretty_url(e.request),onEditDone:n=>t(si(e,{request:{path:"",...zx(n)}})),isValid:n=>!!zx(n)?.host}),"\xA0",qt.createElement(wf,{content:e.request.http_version,onEditDone:n=>t(si(e,{request:{http_version:n}})),isValid:Ux})))}i($z,"RequestLine");function Vz({flow:e}){let t=Be();return qt.createElement("div",{className:"first-line response-line"},qt.createElement(wf,{content:e.response.http_version,onEditDone:n=>t(si(e,{response:{http_version:n}})),isValid:Ux}),"\xA0",qt.createElement(wf,{content:e.response.status_code+"",onEditDone:n=>t(si(e,{response:{code:parseInt(n)}})),isValid:n=>/^\d+$/.test(n)}),e.response.http_version!=="HTTP/2.0"&&qt.createElement(qt.Fragment,null,"\xA0",qt.createElement(gt,{content:e.response.reason,onEditDone:n=>t(si(e,{response:{msg:n}}))})))}i(Vz,"ResponseLine");function qz({flow:e,message:t}){let n=Be(),s=e.request===t?"request":"response";return qt.createElement(wp,{className:"headers",data:t.headers,onChange:l=>n(si(e,{[s]:{headers:l}}))})}i(qz,"Headers");function jz({flow:e,message:t}){let n=Be(),s=e.request===t?"request":"response";return!Rn.get_first_header(t,/^trailer$/i)?null:qt.createElement(qt.Fragment,null,qt.createElement("hr",null),qt.createElement("h5",null,"HTTP Trailers"),qt.createElement(wp,{className:"trailers",data:t.trailers,onChange:h=>n(si(e,{[s]:{trailers:h}}))}))}i(jz,"Trailers");var jL=qt.memo(i(function({flow:t,message:n}){let s=t.request===n?"request":"response",l=t.request===n?$z:Vz;return qt.createElement("section",{className:s},qt.createElement(l,{flow:t}),qt.createElement(qz,{flow:t,message:n}),qt.createElement("hr",null),qt.createElement(__,{key:t.id+s,flow:t,message:n}),qt.createElement(jz,{flow:t,message:n}))},"Message"));function E_(){let e=me(t=>t.flows.byId[t.flows.selected[0]]);return qt.createElement(jL,{flow:e,message:e.request})}i(E_,"Request");E_.displayName="Request";function b_(){let e=me(t=>t.flows.byId[t.flows.selected[0]]);return qt.createElement(jL,{flow:e,message:e.response})}i(b_,"Response");b_.displayName="Response";var et=de(be());var Kz=i(({message:e})=>et.createElement("div",null,e.query?e.op_code:e.response_code,"\xA0",e.truncation?"(Truncated)":""),"Summary"),Gz=i(({message:e})=>et.createElement(et.Fragment,null,et.createElement("h5",null,e.recursion_desired?"Recursive ":"","Question"),et.createElement("table",null,et.createElement("thead",null,et.createElement("tr",null,et.createElement("th",null,"Name"),et.createElement("th",null,"Type"),et.createElement("th",null,"Class"))),et.createElement("tbody",null,e.questions.map((t,n)=>et.createElement("tr",{key:n},et.createElement("td",null,t.name),et.createElement("td",null,t.type),et.createElement("td",null,t.class)))))),"Questions"),T_=i(({name:e,values:t})=>et.createElement(et.Fragment,null,et.createElement("h5",null,e),t.length>0?et.createElement("table",null,et.createElement("thead",null,et.createElement("tr",null,et.createElement("th",null,"Name"),et.createElement("th",null,"Type"),et.createElement("th",null,"Class"),et.createElement("th",null,"TTL"),et.createElement("th",null,"Data"))),et.createElement("tbody",null,t.map((n,s)=>et.createElement("tr",{key:s},et.createElement("td",null,n.name),et.createElement("td",null,n.type),et.createElement("td",null,n.class),et.createElement("td",null,n.ttl),et.createElement("td",null,n.data))))):"\u2014"),"ResourceRecords"),KL=i(({type:e,message:t})=>et.createElement("section",{className:"dns-"+e},et.createElement("div",{className:`first-line ${e}-line`},et.createElement(Kz,{message:t})),et.createElement(Gz,{message:t}),et.createElement("hr",null),et.createElement(T_,{name:`${t.authoritative_answer?"Authoritative ":""}${t.recursion_available?"Recursive ":""}Answer`,values:t.answers}),et.createElement("hr",null),et.createElement(T_,{name:"Authority",values:t.authorities}),et.createElement("hr",null),et.createElement(T_,{name:"Additional",values:t.additionals})),"Message");function k_(){let e=me(t=>t.flows.byId[t.flows.selected[0]]);return et.createElement(KL,{type:"request",message:e.request})}i(k_,"Request");k_.displayName="Request";function N_(){let e=me(t=>t.flows.byId[t.flows.selected[0]]);return et.createElement(KL,{type:"response",message:e.response})}i(N_,"Response");N_.displayName="Response";var Te=de(be());function GL({conn:e}){let t=null;return"address"in e?t=Te.createElement(Te.Fragment,null,Te.createElement("tr",null,Te.createElement("td",null,"Address:"),Te.createElement("td",null,e.address?.join(":"))),e.peername&&Te.createElement("tr",null,Te.createElement("td",null,"Resolved address:"),Te.createElement("td",null,e.peername.join(":"))),e.sockname&&Te.createElement("tr",null,Te.createElement("td",null,"Source address:"),Te.createElement("td",null,e.sockname.join(":")))):e.peername?.[0]&&(t=Te.createElement(Te.Fragment,null,Te.createElement("tr",null,Te.createElement("td",null,"Address:"),Te.createElement("td",null,e.peername?.join(":"))))),Te.createElement("table",{className:"connection-table"},Te.createElement("tbody",null,t,e.sni?Te.createElement("tr",null,Te.createElement("td",null,Te.createElement("abbr",{title:"TLS Server Name Indication"},"SNI"),":"),Te.createElement("td",null,e.sni)):null,e.alpn?Te.createElement("tr",null,Te.createElement("td",null,Te.createElement("abbr",{title:"ALPN protocol negotiated"},"ALPN"),":"),Te.createElement("td",null,e.alpn)):null,e.tls_version?Te.createElement("tr",null,Te.createElement("td",null,"TLS Version:"),Te.createElement("td",null,e.tls_version)):null,e.cipher?Te.createElement("tr",null,Te.createElement("td",null,"TLS Cipher:"),Te.createElement("td",null,e.cipher)):null))}i(GL,"ConnectionInfo");function YL(e){return Te.createElement("dl",{className:"cert-attributes"},e.map(([t,n])=>Te.createElement(Te.Fragment,{key:t},Te.createElement("dt",null,t),Te.createElement("dd",null,n))))}i(YL,"attrList");function Yz({flow:e}){let t=e.server_conn?.cert;return t?Te.createElement(Te.Fragment,null,Te.createElement("h4",{key:"name"},"Server Certificate"),Te.createElement("table",{className:"certificate-table"},Te.createElement("tbody",null,Te.createElement("tr",null,Te.createElement("td",null,"Type"),Te.createElement("td",null,t.keyinfo[0],", ",t.keyinfo[1]," bits")),Te.createElement("tr",null,Te.createElement("td",null,"SHA256 digest"),Te.createElement("td",null,t.sha256)),Te.createElement("tr",null,Te.createElement("td",null,"Valid from"),Te.createElement("td",null,Oo(t.notbefore,{milliseconds:!1}))),Te.createElement("tr",null,Te.createElement("td",null,"Valid to"),Te.createElement("td",null,Oo(t.notafter,{milliseconds:!1}))),Te.createElement("tr",null,Te.createElement("td",null,"Subject Alternative Names"),Te.createElement("td",null,t.altnames.join(", "))),Te.createElement("tr",null,Te.createElement("td",null,"Subject"),Te.createElement("td",null,YL(t.subject))),Te.createElement("tr",null,Te.createElement("td",null,"Issuer"),Te.createElement("td",null,YL(t.issuer))),Te.createElement("tr",null,Te.createElement("td",null,"Serial"),Te.createElement("td",null,t.serial))))):Te.createElement(Te.Fragment,null)}i(Yz,"CertificateInfo");function D0({flow:e}){return Te.createElement("section",{className:"detail"},Te.createElement("h4",null,"Client Connection"),Te.createElement(GL,{conn:e.client_conn}),e.server_conn?.address&&Te.createElement(Te.Fragment,null,Te.createElement("h4",null,"Server Connection"),Te.createElement(GL,{conn:e.server_conn})),Te.createElement(Yz,{flow:e}))}i(D0,"Connection");D0.displayName="Connection";var Qm=de(be());function I0({flow:e}){return Qm.createElement("section",{className:"error"},Qm.createElement("div",{className:"alert alert-warning"},e.error.msg,Qm.createElement("div",null,Qm.createElement("small",null,Oo(e.error.timestamp)))))}i(I0,"Error");I0.displayName="Error";var Vs=de(be());function Xz({t:e,deltaTo:t,title:n}){return e?Vs.createElement("tr",null,Vs.createElement("td",null,n,":"),Vs.createElement("td",null,Oo(e),t&&Vs.createElement("span",{className:"text-muted"},"(",By(1e3*(e-t)),")"))):Vs.createElement("tr",null)}i(Xz,"TimeStamp");function F0({flow:e}){let t;e.type==="http"?t=e.request.timestamp_start:t=e.client_conn.timestamp_start;let n=[{title:"Server conn. initiated",t:e.server_conn?.timestamp_start,deltaTo:t},{title:"Server conn. TCP handshake",t:e.server_conn?.timestamp_tcp_setup,deltaTo:t},{title:"Server conn. TLS handshake",t:e.server_conn?.timestamp_tls_setup,deltaTo:t},{title:"Server conn. closed",t:e.server_conn?.timestamp_end,deltaTo:t},{title:"Client conn. established",t:e.client_conn.timestamp_start,deltaTo:e.type==="http"?t:void 0},{title:"Client conn. TLS handshake",t:e.client_conn.timestamp_tls_setup,deltaTo:t},{title:"Client conn. closed",t:e.client_conn.timestamp_end,deltaTo:t}];return e.type==="http"&&n.push({title:"First request byte",t:e.request.timestamp_start},{title:"Request complete",t:e.request.timestamp_end,deltaTo:t},{title:"First response byte",t:e.response?.timestamp_start,deltaTo:t},{title:"Response complete",t:e.response?.timestamp_end,deltaTo:t}),Vs.createElement("section",{className:"timing"},Vs.createElement("h4",null,"Timing"),Vs.createElement("table",{className:"timing-table"},Vs.createElement("tbody",null,n.filter(s=>!!s.t).sort((s,l)=>s.t-l.t).map(({title:s,t:l,deltaTo:h})=>Vs.createElement(Xz,{key:s,title:s,t:l,deltaTo:h})))))}i(F0,"Timing");F0.displayName="Timing";var Xu=de(be());var Ea=de(be()),Tp=de(be());function Rf({flow:e,messages_meta:t}){let n=Be(),s=me(k=>k.ui.flow.contentViewFor[e.id+"messages"]||"Auto"),[l,h]=(0,Tp.useState)(me(k=>k.options.content_view_lines_cutoff)),d=(0,Tp.useCallback)(()=>h(Math.max(1024,l*2)),[l]),v=E0(Rn.getContentURL(e,"messages",s,l+1),e.id+t.count),S=(0,Tp.useMemo)(()=>{if(v)try{return JSON.parse(v)}catch{return[{description:"Network Error",lines:[[["error",`${v}`]]]}]}},[v])||[],b=l;return Ea.createElement("div",{className:"contentview"},Ea.createElement("div",{className:"controls"},Ea.createElement("h5",null,t.count," Messages"),Ea.createElement(Xm,{value:s,onChange:k=>n(Iy(e.id+"messages",k))})),S.map((k,R)=>{let I=`fa fa-fw fa-arrow-${k.from_client?"right text-primary":"left text-danger"}`,z=Ea.createElement("div",{key:R},Ea.createElement("small",null,Ea.createElement("i",{className:I}),Ea.createElement("span",{className:"pull-right"},k.timestamp&&Oo(k.timestamp))),Ea.createElement(T0,{lines:k.lines,maxLines:b,showMore:d}));return b-=k.lines.length,z}))}i(Rf,"Messages");function B0({flow:e}){return Xu.createElement("section",{className:"websocket"},Xu.createElement("h4",null,"WebSocket"),Xu.createElement(Rf,{flow:e,messages_meta:e.websocket.messages_meta}),Xu.createElement(Qz,{websocket:e.websocket}))}i(B0,"WebSocket");B0.displayName="WebSocket";function Qz({websocket:e}){if(!e.timestamp_end)return null;let t=e.close_reason?`(${e.close_reason})`:"";return Xu.createElement("div",null,Xu.createElement("i",{className:"fa fa-fw fa-window-close text-muted"}),"\xA0 Closed by ",e.closed_by_client?"client":"server"," ","with code ",e.close_code," ",t,".",Xu.createElement("small",{className:"pull-right"},Oo(e.timestamp_end)))}i(Qz,"CloseSummary");var H0=de(be());function z0({flow:e}){let t=Be();return H0.createElement("section",{className:"timing"},H0.createElement("h4",null,"Comment"),H0.createElement(gt,{className:"kv-value",content:e.comment,onEditDone:n=>{t(si(e,{comment:n}))},placeholder:"empty"}))}i(z0,"Comment");z0.displayName="Comment";var QL=de(ai());var P_=de(be());function U0({flow:e}){return P_.createElement("section",{className:"tcp"},P_.createElement(Rf,{flow:e,messages_meta:e.messages_meta}))}i(U0,"TcpMessages");U0.displayName="Stream Data";var A_=de(be());function W0({flow:e}){return A_.createElement("section",{className:"udp"},A_.createElement(Rf,{flow:e,messages_meta:e.messages_meta}))}i(W0,"UdpMessages");W0.displayName="Datagrams";var XL={request:E_,response:b_,error:I0,connection:D0,timing:F0,websocket:B0,tcpmessages:U0,udpmessages:W0,dnsrequest:k_,dnsresponse:N_,comment:z0};function $0(e){let t;switch(e.type){case"http":t=["request","response","websocket"].filter(n=>e[n]);break;case"tcp":t=["tcpmessages"];break;case"udp":t=["udpmessages"];break;case"dns":t=["request","response"].filter(n=>e[n]).map(n=>"dns"+n);break}return e.error&&t.push("error"),t.push("connection"),t.push("timing"),t.push("comment"),t}i($0,"tabsForFlow");function L_(){let e=Be(),t=me(h=>h.flows.byId[h.flows.selected[0]]),n=me(h=>h.ui.flow.tab);if(t==null)return ba.createElement(ba.Fragment,null);let s=$0(t);s.indexOf(n)<0&&(n==="response"&&t.error?n="error":n==="error"&&"response"in t?n="response":n=s[0]);let l=XL[n];return ba.createElement("div",{className:"flow-detail"},ba.createElement("nav",{className:"nav-tabs nav-tabs-sm"},ba.createElement("button",{"data-testid":"close-button-id",className:"close-button",onClick:()=>e(yl(void 0))},ba.createElement("i",{className:"fa fa-times-circle"})),s.map(h=>ba.createElement("a",{key:h,href:"#",className:(0,QL.default)({active:n===h}),onClick:d=>{d.preventDefault(),e(mf(h))}},XL[h].displayName))),ba.createElement(l,{flow:t}))}i(L_,"FlowView");function JL(e){if(e.ctrlKey||e.metaKey)return()=>{};let t=e.key;return e.preventDefault(),(n,s)=>{let l=s().flows,h=l.byId[s().flows.selected[0]];switch(t){case"k":case"ArrowUp":n(yf(l,-1));break;case"j":case"ArrowDown":n(yf(l,1));break;case" ":case"PageDown":n(yf(l,10));break;case"PageUp":n(yf(l,-10));break;case"End":n(yf(l,1e10));break;case"Home":n(yf(l,-1e10));break;case"Escape":s().ui.modal.activeModal?n(Qy()):n(yl(void 0));break;case"ArrowLeft":{if(!h)break;let d=$0(h),v=s().ui.flow.tab,S=d[(Math.max(0,d.indexOf(v))-1+d.length)%d.length];n(mf(S));break}case"Tab":case"ArrowRight":{if(!h)break;let d=$0(h),v=s().ui.flow.tab,S=d[(Math.max(0,d.indexOf(v))+1)%d.length];n(mf(S));break}case"Delete":case"d":{if(!h)return;n(jy(h));break}case"n":{gf("view.flows.create","get","https://example.com/");break}case"D":{if(!h)return;n(Ky(h));break}case"a":{h&&h.intercepted&&n(lp(h));break}case"A":{n(Vy());break}case"r":{h&&n(up(h));break}case"v":{h&&h.modified&&n(Gy(h));break}case"x":{h&&h.intercepted&&n(qy(h));break}case"X":{n($P());break}case"z":{n(Yy());break}default:return}}}i(JL,"onKeyDown");var Ta=de(be());var kp=de(be()),ZL=de(ai());var Jm=class extends kp.Component{constructor(n,s){super(n,s);this.node=kp.default.createRef();this.state={applied:!1,startPos:0,dragPointer:.1},this.onLostPointerCapture=this.onLostPointerCapture.bind(this),this.onPointerDown=this.onPointerDown.bind(this),this.onPointerMove=this.onPointerMove.bind(this)}static{i(this,"Splitter")}static{this.defaultProps={axis:"x"}}onPointerDown(n){this.state.dragPointer===.1&&(n.target.setPointerCapture(n.pointerId),this.setState({startPos:this.props.axis==="x"?n.pageX:n.pageY,dragPointer:n.pointerId}))}onLostPointerCapture(n){if(this.state.dragPointer!==n.pointerId)return;let s=this.node.current,l=s.previousElementSibling,h=s.nextElementSibling;s.style.transform="",l.style.flex=`0 0 ${Math.max(0,(this.props.axis==="x"?l.offsetWidth+n.pageX:l.offsetHeight+n.pageY)-this.state.startPos)}px`,h.style.flex="1 1 auto",this.setState({applied:!0,dragPointer:.1}),this.onResize()}onPointerMove(n){this.state.dragPointer===n.pointerId&&(this.node.current.style.transform=this.props.axis==="x"?`translateX(${n.pageX-this.state.startPos}px)`:`translateY(${n.pageY-this.state.startPos}px)`)}onResize(){window.setTimeout(()=>window.dispatchEvent(new CustomEvent("resize")),1)}reset(n){this.state.applied&&(this.node.current?.previousElementSibling instanceof HTMLElement&&(this.node.current.previousElementSibling.style.flex=""),this.node.current?.nextElementSibling instanceof HTMLElement&&(this.node.current.nextElementSibling.style.flex=""),n||this.setState({applied:!1}),this.onResize())}componentWillUnmount(){this.reset(!0)}render(){return kp.default.createElement("div",{ref:this.node,className:(0,ZL.default)("splitter",this.props.axis==="x"?"splitter-x":"splitter-y")},kp.default.createElement("div",{onLostPointerCapture:this.onLostPointerCapture,onPointerDown:this.onPointerDown,onPointerMove:this.onPointerMove}))}};var Qi=de(be());var Zm=i(e=>{let t=e.current;return t===null||t.scrollTop===0?!1:Math.ceil(t.scrollTop)+t.clientHeight>=t.scrollHeight},"isAtBottom"),V0=i(e=>{e.current&&!Zm(e)&&(e.current.scrollTop=e.current.scrollHeight)},"adjustScrollTop");function Df(e=void 0){if(!e)return{start:0,end:0,paddingTop:0,paddingBottom:0};let{itemCount:t,rowHeight:n,viewportTop:s,viewportHeight:l,itemHeights:h}=e,d=s+l,v=0,S=0,b=0,k=0;if(h){let R=0;for(let I=0;I0&&R$_,default:()=>Zz,icon:()=>D_,index:()=>R_,method:()=>F_,path:()=>I_,quickactions:()=>If,size:()=>z_,status:()=>H_,time:()=>U_,timestamp:()=>W_,tls:()=>O_,version:()=>B_});var Jt=de(be());var M_=de(ai());var O_=i(({flow:e})=>Jt.default.createElement("td",{className:(0,M_.default)("col-tls",e.client_conn.tls_established?"col-tls-https":"col-tls-http")}),"tls");O_.headerName="";var R_=i(({flow:e})=>{let t=me(n=>n.flows.listIndex[e.id]);return Jt.default.createElement("td",{className:"col-index"},t+1)},"index");R_.headerName="#";var D_=i(({flow:e})=>Jt.default.createElement("td",{className:"col-icon"},Jt.default.createElement("div",{className:(0,M_.default)("resource-icon",Vx(e))})),"icon");D_.headerName="";var I_=i(({flow:e})=>{let t;return e.error&&(e.error.msg==="Connection killed."?t=Jt.default.createElement("i",{className:"fa fa-fw fa-times pull-right"}):t=Jt.default.createElement("i",{className:"fa fa-fw fa-exclamation pull-right"})),Jt.default.createElement("td",{className:"col-path"},e.is_replay==="request"&&Jt.default.createElement("i",{className:"fa fa-fw fa-repeat pull-right"}),e.intercepted&&Jt.default.createElement("i",{className:"fa fa-fw fa-pause pull-right"}),t,Jt.default.createElement("span",{className:"marker pull-right"},e.marked),qx(e))},"path");I_.headerName="Path";var F_=i(({flow:e})=>Jt.default.createElement("td",{className:"col-method"},Kx(e)),"method");F_.headerName="Method";var B_=i(({flow:e})=>Jt.default.createElement("td",{className:"col-http-version"},Gx(e)),"version");B_.headerName="Version";var H_=i(({flow:e})=>{let t="darkred";return e.type!=="http"&&e.type!="dns"||!e.response?Jt.default.createElement("td",{className:"col-status"}):(100<=e.response.status_code&&e.response.status_code<200?t="green":200<=e.response.status_code&&e.response.status_code<300?t="darkgreen":300<=e.response.status_code&&e.response.status_code<400?t="lightblue":(400<=e.response.status_code&&e.response.status_code<500||500<=e.response.status_code&&e.response.status_code<600)&&(t="red"),Jt.default.createElement("td",{className:"col-status",style:{color:t}},jx(e)))},"status");H_.headerName="Status";var z_=i(({flow:e})=>Jt.default.createElement("td",{className:"col-size"},Fy($x(e))),"size");z_.headerName="Size";var U_=i(({flow:e})=>{let t=Lm(e),n=Wx(e);return Jt.default.createElement("td",{className:"col-time"},t&&n?By(1e3*(n-t)):"...")},"time");U_.headerName="Time";var W_=i(({flow:e})=>{let t=Lm(e);return Jt.default.createElement("td",{className:"col-timestamp"},t?Oo(t):"...")},"timestamp");W_.headerName="Start time";var If=i(({flow:e})=>{let t=Be(),n=null;return e.intercepted?n=Jt.default.createElement("a",{href:"#",className:"quickaction",onClick:()=>t(lp(e))},Jt.default.createElement("i",{className:"fa fa-fw fa-play text-success"})):Uy(e)&&(n=Jt.default.createElement("a",{href:"#",className:"quickaction",onClick:()=>t(up(e))},Jt.default.createElement("i",{className:"fa fa-fw fa-repeat text-primary"}))),Jt.default.createElement("td",{className:"col-quickactions"},n?Jt.default.createElement("div",null,n):Jt.default.createElement(Jt.default.Fragment,null))},"quickactions");If.headerName="";var $_=i(({flow:e})=>{let t=e.comment;return Jt.default.createElement("td",{className:"col-comment"},t)},"comment");$_.headerName="Comment";var Jz={icon:D_,index:R_,method:F_,version:B_,path:I_,quickactions:If,size:z_,status:H_,time:U_,timestamp:W_,tls:O_,comment:$_},Zz=Jz;var rM=tg.memo(i(function(){let t=Be(),n=me(v=>v.flows.sort.desc),s=me(v=>v.flows.sort.column),l=me(v=>v.options.web_columns),h=n?"sort-desc":"sort-asc",d=l.map(v=>eg[v]).filter(v=>v).concat(If);return tg.createElement("tr",null,d.map(v=>tg.createElement("th",{className:(0,tM.default)(`col-${v.name}`,s===v.name&&h),key:v.name,onClick:()=>t(WP(v.name===s&&n?void 0:v.name,v.name!==s?!1:!n))},v.headerName)))},"FlowTableHead"));var Np=de(be()),nM=de(ai());var iM=Np.default.memo(i(function({flow:t,selected:n,highlighted:s}){let l=Be(),h=me(b=>b.options.web_columns),d=(0,nM.default)({selected:n,highlighted:s,intercepted:t.intercepted,"has-request":t.type==="http"&&t.request,"has-response":t.type==="http"&&t.response}),v=(0,Np.useCallback)(b=>{let k=b.target;for(;k.parentNode;){if(k.classList.contains("col-quickactions"))return;k=k.parentNode}l(yl(t.id))},[t]),S=h.map(b=>eg[b]).filter(b=>b).concat(If);return Np.default.createElement("tr",{className:d,onClick:v},S.map(b=>Np.default.createElement(b,{key:b.name,flow:t})))},"FlowRow"));var V_=class extends Qi.Component{constructor(n,s){super(n,s);this.viewport=Qi.createRef();this.head=Qi.createRef();this.state={vScroll:Df(),viewportTop:0},this.onViewportUpdate=this.onViewportUpdate.bind(this)}static{i(this,"PureFlowTable")}static{this.defaultProps={rowHeight:32}}componentDidMount(){window.addEventListener("resize",this.onViewportUpdate),this.onViewportUpdate()}componentWillUnmount(){window.removeEventListener("resize",this.onViewportUpdate)}getSnapshotBeforeUpdate(){return Zm(this.viewport)}componentDidUpdate(n,s,l){if(l&&V0(this.viewport),this.onViewportUpdate(),this.props.selected&&this.props.selected!==n.selected){let{rowHeight:d,flows:v,selected:S}=this.props,b=this.viewport.current,k=this.head.current,R=k?k.offsetHeight:0,I=v.indexOf(S)*d+R,z=I+d,j=b.scrollTop,Y=b.offsetHeight;I-Rj+Y&&(b.scrollTop=z-Y),this.onViewportUpdate()}}onViewportUpdate(){let n=this.viewport.current,s=n.scrollTop||0,l=Df({viewportTop:s,viewportHeight:n.offsetHeight||0,itemCount:this.props.flows.length,rowHeight:this.props.rowHeight});if(this.state.viewportTop!==s||!Mo(this.state.vScroll,l)){let h=Math.min(s,l.end*this.props.rowHeight);this.setState({vScroll:l,viewportTop:h})}}render(){let{vScroll:n,viewportTop:s}=this.state,{flows:l,selected:h,highlight:d}=this.props,v=d?vf.parse(d):()=>!1;return Qi.createElement("div",{className:"flow-table",onScroll:this.onViewportUpdate,ref:this.viewport},Qi.createElement("table",null,Qi.createElement("thead",{ref:this.head,style:{transform:`translateY(${s}px)`}},Qi.createElement(rM,null)),Qi.createElement("tbody",null,Qi.createElement("tr",{style:{height:n.paddingTop}}),l.slice(n.start,n.end).map(S=>Qi.createElement(iM,{key:S.id,flow:S,selected:S===h,highlighted:v(S)})),Qi.createElement("tr",{style:{height:n.paddingBottom}}))))}},oM=Wu(e=>({flows:e.flows.view,highlight:e.flows.highlight,selected:e.flows.byId[e.flows.selected[0]]}))(V_);var ln=de(be()),rw=de(be());var lO=de(aO());function hE(){return ln.createElement("div",{style:{padding:"1em 2em"}},ln.createElement("h3",null,"mitmproxy is running."),ln.createElement("p",null,"No flows have been recorded yet.",ln.createElement("br",null),"To start capturing traffic, please configure your settings in the Capture tab."))}i(hE,"CaptureSetup");function GU({description:e,listen_addrs:t,is_running:n,wireguard_conf:s,type:l}){let h=(0,rw.useRef)(null);(0,rw.useEffect)(()=>{s&&h.current&&lO.default.toCanvas(h.current,s,{margin:0,scale:3})},[s]);let d,v=t.length===1||t.length===2&&t[0][1]===t[1][1],S=t.every(k=>["::","0.0.0.0"].includes(k[0]));v&&S?d=Nx(["*",t[0][1]]):d=t.map(Nx).join(" and "),e=e[0].toUpperCase()+e.substr(1);let b;return n?b=ln.createElement(ln.Fragment,null,l==="local"?ln.createElement("div",{className:"text-success"},e," is active."):ln.createElement("div",{className:"text-success"},e," listening at ",d,"."),s&&ln.createElement("div",{className:"wireguard-config"},ln.createElement("pre",null,s),ln.createElement("canvas",{ref:h}))):b=ln.createElement(ln.Fragment,null,ln.createElement("div",{className:"text-warning"},e," starting...")),ln.createElement("div",null,b)}i(GU,"ServerDescription");function ui({error:e,backendState:t}){return ln.createElement("div",{className:"mode-status"},e?ln.createElement("div",{className:"text-danger"},e):t&&ln.createElement(GU,{...t}))}i(ui,"ServerStatus");var un=de(be());var Pi=de(be());var Rp=de(be());function ci({value:e,onChange:t,children:n,label:s}){let l=Rp.useId();return Rp.createElement("div",{className:"mode-entry"},Rp.createElement("input",{type:"checkbox",name:`mode-checkbox-${l}`,id:`mode-checkbox-${l}`,checked:e,onChange:t}),Rp.createElement("label",{htmlFor:`mode-checkbox-${l}`,style:{marginBottom:0,fontWeight:"normal"}},s),n)}i(ci,"ModeToggle");var Dr=de(be());var Fo=de(be());function fi({children:e,iconClass:t,classname:n,isVisible:s}){let l=Fo.useId(),h="--"+[...l].map(S=>S.charCodeAt(0).toString(16)).join(""),d=Fo.useRef(null),v=Fo.useRef(null);return Fo.useEffect(()=>{d.current.style.anchorName=h,v.current.style.positionAnchor=h},[]),Fo.useEffect(()=>{s===!0&&document.getElementById(l)?.showPopover()},[s]),Fo.createElement("div",{className:n?`mode-popover ${n}`:"mode-popover"},Fo.createElement("button",{popovertarget:l,ref:d},Fo.createElement("i",{className:t,"aria-hidden":"true"})),Fo.createElement("div",{id:l,popover:"auto",ref:v},e))}i(fi,"Popover");function mE({server:e}){let{currentProcesses:t,isLoading:n}=me(D=>D.processes),{selectedProcesses:s}=me(D=>D.modes.local[0]),[l,h]=Dr.useState([]),[d,v]=Dr.useState(""),S=Be(),{platform:b}=me(D=>D.backendState),k=i(D=>{v(D.target.value)},"handleInputChange"),R=i(D=>{let q=b.startsWith("win32")?"\\":"/";return km(D.executable,q)[1]},"extractProcessName"),I=i(D=>{let q=typeof D=="string"?D:R(D),te=s?`${s}, ${q}`:q;S(yp({server:e,value:te}))},"addProcessToSelection"),z=i(D=>{let q=s?.split(/,\s*/).filter(te=>te!==R(D)).join(", ");S(yp({server:e,value:q}))},"removeProcessFromSelection"),j=i(D=>{Y(D)&&s?z(D):I(D)},"handleApplicationClick"),Y=i(D=>{let q=R(D);return s?.includes(q)},"isSelected");Dr.useEffect(()=>{t.length===0&&S(Cf())},[]),Dr.useEffect(()=>{if(d){let D=t.filter(q=>R(q).toLowerCase().includes(d.toLowerCase()));h(D)}else l!==t&&h(t)},[d,t]);let ne=i(D=>{D.stopPropagation(),D.key==="Enter"&&(I(d),v(""))},"handleInputKeyDown"),[F,L]=Dr.useState(!1);return Dr.createElement("div",{className:"local-dropdown"},Dr.createElement("div",{className:"dropdown-header"},Dr.createElement("input",{type:"text",className:"autocomplete-input",placeholder:s&&s?.length>0?"Add more":"(all applications)",value:d,onChange:k,onKeyDown:ne,onClick:()=>L(!0),onBlur:()=>L(!1)}),Dr.createElement(fi,{iconClass:"fa fa-chevron-down",classname:"local-popover",isVisible:F},Dr.createElement("h4",null,"Current Applications running on machine"),n?Dr.createElement("i",{className:"fa fa-spinner","aria-hidden":"true"}):l.length>0?Dr.createElement("ul",{className:"dropdown-list"},l.map((D,q)=>Dr.createElement("li",{key:q,className:`dropdown-item ${Y(D)?"selected":""}`,onClick:()=>j(D),role:"menuitem"},Dr.createElement("div",{className:"process-details"},Dr.createElement("img",{className:"process-icon",src:`./executable-icon?path=${D.executable}`,loading:"lazy"}),Dr.createElement("span",{className:"process-name"},R(D)))))):Dr.createElement("span",null,"Press ",Dr.createElement("strong",null,"Enter")," to capture traffic for programs matching: ",Dr.createElement("strong",null,d)))))}i(mE,"LocalDropdown");function gE(){let e=me(s=>s.modes.local),t=me(s=>s.backendState.servers),n=e.map(s=>Pi.createElement(YU,{key:s.ui_id,server:s,backendState:t[m0(s)]}));return Pi.createElement("div",null,Pi.createElement("h4",{className:"mode-title"},"Local Applications"),Pi.createElement("p",{className:"mode-description"},"Transparently Intercept local application(s)."),n)}i(gE,"Local");function YU({server:e,backendState:t}){let n=Be(),s=me(d=>d.processes.error),l=e.error||t?.last_exception||s||void 0,h=i(d=>{let v=e.selectedProcesses?.split(/,\s*/).filter(S=>S!==d).join(", ");n(yp({server:e,value:v}))},"handleDeletionProcess");return Pi.createElement("div",{className:"mode-local"},Pi.createElement(ci,{value:e.active,label:"Intercept traffic for",onChange:()=>n(RC({server:e,value:!e.active}))},Pi.createElement("div",{className:"processes-container"},Pi.createElement("div",{className:"selected-processes"},e.selectedProcesses?.split(/,\s*/).filter(d=>d.trim()!=="").map(d=>Pi.createElement("div",{key:d,className:"selected-process"},d,Pi.createElement("i",{className:"fa fa-times","aria-hidden":"true",onClick:()=>h(d)})))),Pi.createElement("div",{className:"dropdown-container"},Pi.createElement(mE,{server:e}),Pi.createElement("i",{className:"fa fa-refresh","aria-hidden":"true",onClick:()=>n(Cf())})))),Pi.createElement(ui,{error:l,backendState:t}))}i(YU,"LocalRow");var Ji=de(be());function vE(){let e=me(s=>s.modes.regular),t=me(s=>s.backendState.servers),n=e.map(s=>Ji.createElement(XU,{key:s.ui_id,server:s,backendState:t[h0(s)]}));return Ji.createElement("div",null,Ji.createElement("h4",{className:"mode-title"},"Explicit HTTP(S) Proxy"),Ji.createElement("p",{className:"mode-description"},"You manually configure your client application or device to use an HTTP(S) proxy."),n)}i(vE,"Regular");function XU({server:e,backendState:t}){let n=Be(),s=e.error||t?.last_exception||void 0;return Ji.createElement("div",null,Ji.createElement(ci,{value:e.active,label:"Run HTTP/S Proxy",onChange:()=>n(LC({server:e,value:!e.active}))},Ji.createElement(fi,{iconClass:"fa fa-cog"},Ji.createElement("h4",null,"Advanced Configuration"),Ji.createElement("p",null,"Listen Host"),Ji.createElement(gt,{className:"mode-input",content:e.listen_host||"",onEditDone:l=>n(MC({server:e,value:l}))}),Ji.createElement("p",null,"Listen Port"),Ji.createElement(gt,{className:"mode-input",content:e.listen_port?e.listen_port.toString():"",placeholder:"8080",onEditDone:l=>n(OC({server:e,value:parseInt(l)}))}))),Ji.createElement(ui,{error:s,backendState:t}))}i(XU,"RegularRow");var di=de(be());function yE(){let e=me(s=>s.modes.wireguard),t=me(s=>s.backendState.servers),n=e.map(s=>di.createElement(QU,{key:s.ui_id,server:s,backendState:t[g0(s)]}));return di.createElement("div",null,di.createElement("h4",{className:"mode-title"},"WireGuard Server"),di.createElement("p",{className:"mode-description"},"Start a WireGuard\u2122 server and connect an external device for transparent proxying."),n)}i(yE,"Wireguard");function QU({server:e,backendState:t}){let n=Be(),s=e.error||t?.last_exception||void 0;return di.createElement("div",null,di.createElement(ci,{value:e.active,label:"Run WireGuard Server",onChange:()=>n(DC({server:e,value:!e.active}))},di.createElement(fi,{iconClass:"fa fa-cog"},di.createElement("h4",null,"Advanced Configuration"),di.createElement("p",null,"Listen Host"),di.createElement(gt,{className:"mode-input",content:e.listen_host||"",placeholder:"(all interfaces)",onEditDone:l=>n(IC({server:e,value:l}))}),di.createElement("p",null,"Listen Port"),di.createElement(gt,{className:"mode-input",content:e.listen_port?e.listen_port.toString():"",placeholder:"51820",onEditDone:l=>n(FC({server:e,value:parseInt(l)}))}),di.createElement("p",null,"Configuration File"),di.createElement(gt,{className:"mode-input",content:e.file_path||"",placeholder:"~/.mitmproxy/wireguard.conf",onEditDone:l=>n(BC({server:e,value:l}))}))),di.createElement(ui,{error:s,backendState:t}))}i(QU,"WireGuardRow");var Kr=de(be());function wE(){let e=Be(),t=me(s=>s.modes.reverse),n=me(s=>s.backendState.servers);return Kr.createElement("div",null,Kr.createElement("h4",{className:"mode-title"},"Reverse Proxy"),Kr.createElement("p",{className:"mode-description"},"Requests are forwarded to a preconfigured destination."),Kr.createElement("div",{className:"mode-reverse-servers"},t.map((s,l)=>Kr.createElement(JU,{key:s.ui_id,removable:l>0,server:s,backendState:n[vp(s)]})),Kr.createElement("div",{className:"mode-reverse-add-server",onClick:()=>e(tL())},Kr.createElement("i",{className:"fa fa-plus-square-o","aria-hidden":"true"}),"Add additional server")))}i(wE,"Reverse");function JU({removable:e,server:t,backendState:n}){let s=Be(),l=Object.values(v0),h=i(async()=>{t.active&&await s(_0({server:t,value:!1})).unwrap(),await s(rL(t))},"deleteServer"),d=t.error||n?.last_exception||void 0;return Kr.createElement("div",null,Kr.createElement(ci,{value:t.active,label:"Forward",onChange:()=>{s(_0({server:t,value:!t.active}))}},Kr.createElement("select",{name:"protocols",className:"mode-reverse-dropdown",value:t.protocol,onChange:v=>{s(UC({server:t,value:v.target.value}))}},l.map(v=>Kr.createElement("option",{key:v,value:v},v))),"traffic to",Kr.createElement(gt,{className:"mode-reverse-input",content:t.destination?.toString()||"",onEditDone:v=>s(WC({server:t,value:v})),placeholder:"example.com"}),Kr.createElement(fi,{iconClass:"fa fa-cog"},Kr.createElement("h4",null,"Advanced Configuration"),Kr.createElement("p",null,"Listen Host"),Kr.createElement(gt,{className:"mode-reverse-input",content:t.listen_host||"",onEditDone:v=>s(HC({server:t,value:v})),placeholder:"*"}),Kr.createElement("p",null,"Listen Port"),Kr.createElement(gt,{className:"mode-reverse-input",content:String(t.listen_port||""),onEditDone:v=>s(zC({server:t,value:v})),placeholder:"8080"})),e&&Kr.createElement("i",{className:"fa fa-fw fa-trash fa-lg","aria-hidden":"true",onClick:h})),Kr.createElement(ui,{error:d,backendState:n}))}i(JU,"ReverseToggleRow");var Ai=de(be());function SE(){let e=me(s=>s.modes.transparent),t=me(s=>s.backendState.servers),n=e.map(s=>Ai.createElement(ZU,{key:s.ui_id,server:s,backendState:t[w0(s)]}));return Ai.createElement("div",null,Ai.createElement("h4",{className:"mode-title"},"Transparent Proxy"),Ai.createElement("p",{className:"mode-description"},"You"," ",Ai.createElement("a",{href:"https://docs.mitmproxy.org/stable/howto-transparent/",style:{textDecoration:"underline",color:"inherit"}},"configure your routing table")," ","to send traffic through mitmproxy."),n)}i(SE,"Transparent");function ZU({server:e,backendState:t}){let n=Be(),s=e.error||t?.last_exception||void 0;return Ai.createElement("div",null,Ai.createElement(ci,{value:e.active,label:"Run Transparent Proxy",onChange:()=>n($C({server:e,value:!e.active}))},Ai.createElement(fi,{iconClass:"fa fa-cog"},Ai.createElement("h4",null,"Advanced Configuration"),Ai.createElement("p",null,"Listen Host"),Ai.createElement(gt,{className:"mode-input",content:e.listen_host||"",onEditDone:l=>n(VC({server:e,value:l}))}),Ai.createElement("p",null,"Listen Port"),Ai.createElement(gt,{className:"mode-input",content:e.listen_port?e.listen_port.toString():"",placeholder:"8080",onEditDone:l=>n(qC({server:e,value:parseInt(l)}))}))),Ai.createElement(ui,{error:s,backendState:t}))}i(ZU,"TransparentRow");var Zi=de(be());function xE(){let e=me(s=>s.modes.socks),t=me(s=>s.backendState.servers),n=e.map(s=>Zi.createElement(eW,{key:s.ui_id,server:s,backendState:t[S0(s)]}));return Zi.createElement("div",null,Zi.createElement("h4",{className:"mode-title"},"SOCKS Proxy"),Zi.createElement("p",{className:"mode-description"},"You manually configure your client application or device to use a SOCKSv5 proxy."),n)}i(xE,"Socks");function eW({server:e,backendState:t}){let n=Be(),s=e.error||t?.last_exception||void 0;return Zi.createElement("div",null,Zi.createElement(ci,{value:e.active,label:"Run SOCKS Proxy",onChange:()=>n(jC({server:e,value:!e.active}))},Zi.createElement(fi,{iconClass:"fa fa-cog"},Zi.createElement("h4",null,"Advanced Configuration"),Zi.createElement("p",null,"Listen Host"),Zi.createElement(gt,{className:"mode-input",content:e.listen_host||"",onEditDone:l=>n(KC({server:e,value:l}))}),Zi.createElement("p",null,"Listen Port"),Zi.createElement(gt,{className:"mode-input",content:e.listen_port?e.listen_port.toString():"",placeholder:"8080",onEditDone:l=>n(GC({server:e,value:parseInt(l)}))}))),Zi.createElement(ui,{error:s,backendState:t}))}i(eW,"SocksRow");var Li=de(be());function CE(){let e=me(s=>s.modes.upstream),t=me(s=>s.backendState.servers),n=e.map(s=>Li.createElement(tW,{key:s.ui_id,server:s,backendState:t[x0(s)]}));return Li.createElement("div",null,Li.createElement("h4",{className:"mode-title"},"Explicit HTTP(S) Proxy (With Upstream Proxy)"),Li.createElement("p",{className:"mode-description"},"All requests are forwarded to a second HTTP(S) proxy server."),n)}i(CE,"Upstream");function tW({server:e,backendState:t}){let n=Be(),s=e.error||t?.last_exception||void 0;return Li.createElement("div",null,Li.createElement(ci,{value:e.active,label:"Run HTTP/S Proxy and forward requests to",onChange:()=>{n(YC({server:e,value:!e.active}))}},Li.createElement(gt,{className:"mode-upstream-input",content:e.destination?.toString()||"",onEditDone:l=>n(JC({server:e,value:l})),placeholder:"http://example.com:8080"}),Li.createElement(fi,{iconClass:"fa fa-cog"},Li.createElement("h4",null,"Advanced Configuration"),Li.createElement("p",null,"Listen Host"),Li.createElement(gt,{className:"mode-input",content:e.listen_host||"",onEditDone:l=>n(XC({server:e,value:l}))}),Li.createElement("p",null,"Listen Port"),Li.createElement(gt,{className:"mode-input",content:e.listen_port?e.listen_port.toString():"",placeholder:"8080",onEditDone:l=>n(QC({server:e,value:parseInt(l)}))}))),Li.createElement(ui,{error:s,backendState:t}))}i(tW,"UpstreamRow");var eo=de(be());function _E(){let e=me(s=>s.modes.dns),t=me(s=>s.backendState.servers),n=e.map(s=>eo.createElement(rW,{key:s.ui_id,server:s,backendState:t[C0(s)]}));return eo.createElement("div",null,eo.createElement("h4",{className:"mode-title"},"DNS Server"),eo.createElement("p",{className:"mode-description"},"A recursive DNS resolver using the host's DNS configuration."),n)}i(_E,"Dns");function rW({server:e,backendState:t}){let n=Be(),s=e.error||t?.last_exception||void 0;return eo.createElement("div",null,eo.createElement(ci,{value:e.active,label:"Run DNS Server",onChange:()=>n(ZC({server:e,value:!e.active}))},eo.createElement(fi,{iconClass:"fa fa-cog"},eo.createElement("h4",null,"Advanced Configuration"),eo.createElement("p",null,"Listen Host"),eo.createElement(gt,{className:"mode-input",content:e.listen_host||"",onEditDone:l=>n(e_({server:e,value:l}))}),eo.createElement("p",null,"Listen Port"),eo.createElement(gt,{className:"mode-input",content:e.listen_port?e.listen_port.toString():"",placeholder:"8080",onEditDone:l=>n(t_({server:e,value:parseInt(l)}))}))),eo.createElement(ui,{error:s,backendState:t}))}i(rW,"DnsRow");var Dp=de(be());function nw({title:e,description:t}){return Dp.createElement("div",{className:"missing-mode-container"},Dp.createElement("div",{className:"title-icon-container"},Dp.createElement("h4",{className:"mode-title"},e),Dp.createElement("i",{className:"fa fa-exclamation-triangle","aria-hidden":"true"})),Dp.createElement("p",{className:"mode-description"},t))}i(nw,"MissingMode");function EE(){let e=me(t=>t.backendState.platform);return un.createElement("div",{className:"modes"},un.createElement("h2",null,"Intercept Traffic"),un.createElement("p",null,"Configure how you want to intercept traffic with mitmproxy."),un.createElement("div",{className:"modes-category green-left-border"},un.createElement("h3",null,"Recommended"),un.createElement("div",{className:"modes-container"},un.createElement(vE,null),e.startsWith("linux")?un.createElement(nw,{title:"Local Redirect Mode",description:"This mode is only supported on Windows and MacOS."}):un.createElement(gE,null),un.createElement(yE,null),un.createElement(wE,null))),un.createElement("div",{className:"modes-category gray-left-border"},un.createElement("h3",null,"Advanced"),un.createElement("div",{className:"modes-container"},un.createElement(xE,null),un.createElement(CE,null),un.createElement(_E,null),e.startsWith("win32")?un.createElement(nw,{title:"Transparent Proxy",description:"This mode is only supported on Linux and MacOS."}):un.createElement(SE,null))))}i(EE,"Modes");function bE(){let e=me(s=>!!s.flows.byId[s.flows.selected[0]]),t=me(s=>s.flows.list.length>0),n=me(s=>s.ui.tabs.current);return Ta.createElement("div",{className:"main-view"},n===0?Ta.createElement(EE,null):Ta.createElement(Ta.Fragment,null,t?Ta.createElement(oM,null):Ta.createElement(hE,null),e&&Ta.createElement(Jm,{key:"splitter"}),e&&Ta.createElement(L_,{key:"flowDetails"})))}i(bE,"MainView");var to=de(be()),mO=de(ai());var Qn=de(be());var iw=de(be());function hs({children:e}){return window.MITMWEB_STATIC?null:iw.createElement(iw.Fragment,null,e)}i(hs,"HideInStatic");var uO=Qn.memo(i(function(){let t=Be(),n=me(s=>s.flows.filter);return Qn.createElement(Yu,{className:"pull-left special",text:"File",options:{placement:"bottom-start"}},Qn.createElement("li",null,Qn.createElement(b0,{icon:"fa-folder-open",text:"\xA0Open...",onClick:s=>s.stopPropagation(),onOpenFile:s=>{t(qP(s)),document.body.click()}})),Qn.createElement(li,{onClick:()=>location.replace("/flows/dump")},Qn.createElement("i",{className:"fa fa-fw fa-floppy-o"}),"\xA0Save"),Qn.createElement(li,{onClick:()=>location.replace("/flows/dump?filter="+n)},Qn.createElement("i",{className:"fa fa-fw fa-floppy-o"}),"\xA0Save filtered"),Qn.createElement(li,{onClick:()=>confirm("Delete all flows?")&&t(Yy())},Qn.createElement("i",{className:"fa fa-fw fa-trash"}),"\xA0Clear All"),Qn.createElement(hs,null,Qn.createElement(qL,null),Qn.createElement("li",null,Qn.createElement("a",{href:"http://mitm.it/",target:"_blank",rel:"noreferrer"},Qn.createElement("i",{className:"fa fa-fw fa-external-link"}),"\xA0Install Certificates..."))))},"FileMenu"));var tc=de(be());var cO=tc.memo(i(function(){let t=me(s=>s.connection.state),n=me(s=>s.connection.message);switch(t){case"CONNECTION_INIT":return tc.createElement("span",{className:"connection-indicator init"},"connecting\u2026");case"CONNECTION_FETCHING":return tc.createElement("span",{className:"connection-indicator fetching"},"fetching data\u2026");case"CONNECTION_ESTABLISHED":return tc.createElement("span",{className:"connection-indicator established"},"connected");case"CONNECTION_ERROR":return tc.createElement("span",{className:"connection-indicator error",title:n},"connection lost");case"CONNECTION_OFFLINE":return tc.createElement("span",{className:"connection-indicator offline"},"offline")}},"ConnectionIndicator"));var ow=de(be());sw.title="Capture";function sw(){return ow.createElement(ow.Fragment,null)}i(sw,"CaptureMenu");var pi=de(be());var ms=de(be()),fO=de(ai());var Bo=de(be());var cg=class e extends Bo.Component{static{i(this,"FilterDocs")}constructor(t,n){super(t,n),this.state={doc:e.doc}}componentDidMount(){e.xhr||(e.xhr=xt("/filter-help").then(t=>t.json()),e.xhr.catch(()=>{e.xhr=null})),this.state.doc||e.xhr.then(t=>{e.doc=t,this.setState({doc:t})})}render(){let{doc:t}=this.state;return t?Bo.default.createElement("table",{className:"table table-condensed"},Bo.default.createElement("tbody",null,t.commands.map(n=>Bo.default.createElement("tr",{key:n[1],onClick:()=>this.props.selectHandler(n[0].split(" ")[0]+" ")},Bo.default.createElement("td",null,n[0].replace(" ","\xA0")),Bo.default.createElement("td",null,n[1]))),Bo.default.createElement("tr",{key:"docs-link"},Bo.default.createElement("td",{colSpan:2},Bo.default.createElement("a",{href:"https://mitmproxy.org/docs/latest/concepts-filters/",target:"_blank",rel:"noreferrer"},Bo.default.createElement("i",{className:"fa fa-external-link"}),"\xA0 mitmproxy docs"))))):Bo.default.createElement("i",{className:"fa fa-spinner fa-spin"})}};var Uf=class extends ms.Component{constructor(n,s){super(n,s);this.inputRef=ms.default.createRef();this.state={value:this.props.value,focus:!1,mousefocus:!1},this.onChange=this.onChange.bind(this),this.onFocus=this.onFocus.bind(this),this.onBlur=this.onBlur.bind(this),this.onKeyDown=this.onKeyDown.bind(this),this.onMouseEnter=this.onMouseEnter.bind(this),this.onMouseLeave=this.onMouseLeave.bind(this),this.selectFilter=this.selectFilter.bind(this)}static{i(this,"FilterInput")}UNSAFE_componentWillReceiveProps(n){this.setState({value:n.value})}isValid(n){try{return n&&vf.parse(n),!0}catch{return!1}}getDesc(){if(!this.state.value)return ms.default.createElement(cg,{selectHandler:this.selectFilter});try{return vf.parse(this.state.value).desc}catch(n){return""+n}}onChange(n){let s=n.target.value;this.setState({value:s}),this.isValid(s)&&this.props.onChange(s)}onFocus(){this.setState({focus:!0})}onBlur(){this.setState({focus:!1})}onMouseEnter(){this.setState({mousefocus:!0})}onMouseLeave(){this.setState({mousefocus:!1})}onKeyDown(n){(n.key==="Escape"||n.key==="Enter")&&(this.blur(),this.setState({mousefocus:!1})),n.stopPropagation()}selectFilter(n){this.setState({value:n}),this.inputRef.current?.focus()}blur(){this.inputRef.current?.blur()}select(){this.inputRef.current?.select()}render(){let{type:n,color:s,placeholder:l}=this.props,{value:h,focus:d,mousefocus:v}=this.state;return ms.default.createElement("div",{className:(0,fO.default)("filter-input input-group",{"has-error":!this.isValid(h)})},ms.default.createElement("span",{className:"input-group-addon"},ms.default.createElement("i",{className:"fa fa-fw fa-"+n,style:{color:s}})),ms.default.createElement("input",{type:"text",ref:this.inputRef,placeholder:l,className:"form-control",value:h,onChange:this.onChange,onFocus:this.onFocus,onBlur:this.onBlur,onKeyDown:this.onKeyDown}),(d||v)&&ms.default.createElement("div",{className:"popover bottom",onMouseEnter:this.onMouseEnter,onMouseLeave:this.onMouseLeave},ms.default.createElement("div",{className:"arrow"}),ms.default.createElement("div",{className:"popover-content"},this.getDesc())))}};aw.title="Flow List";function aw(){return pi.createElement("div",{className:"main-menu"},pi.createElement("div",{className:"menu-group"},pi.createElement("div",{className:"menu-content"},pi.createElement(oW,null),pi.createElement(sW,null)),pi.createElement("div",{className:"menu-legend"},"Find")),pi.createElement("div",{className:"menu-group"},pi.createElement("div",{className:"menu-content"},pi.createElement(iW,null),pi.createElement(aW,null)),pi.createElement("div",{className:"menu-legend"},"Intercept")))}i(aw,"FlowListMenu");function iW(){let e=Be(),t=me(n=>n.options.intercept);return pi.createElement(Uf,{value:t||"",placeholder:"Intercept",type:"pause",color:"hsl(208, 56%, 53%)",onChange:n=>e(gp("intercept",n))})}i(iW,"InterceptInput");function oW(){let e=Be(),t=me(n=>n.flows.filter);return pi.createElement(Uf,{value:t||"",placeholder:"Search",type:"search",color:"black",onChange:n=>e(Wy(n))})}i(oW,"FlowFilterInput");function sW(){let e=Be(),t=me(n=>n.flows.highlight);return pi.createElement(Uf,{value:t||"",placeholder:"Highlight",type:"tag",color:"hsl(48, 100%, 50%)",onChange:n=>e($y(n))})}i(sW,"HighlightInput");function aW(){let e=Be();return pi.createElement(Or,{className:"btn-sm",title:"[a]ccept all",icon:"fa-forward text-success",onClick:()=>e(Vy())},"Resume All")}i(aW,"ResumeAll");var cn=de(be());var Wf=de(be());function TE({value:e,onChange:t,children:n}){return Wf.createElement("div",{className:"menu-entry"},Wf.createElement("label",null,Wf.createElement("input",{type:"checkbox",checked:e,onChange:t}),n))}i(TE,"MenuToggle");function lw({name:e,children:t}){let n=Be(),s=me(l=>l.options[e]);return Wf.createElement(TE,{value:!!s,onChange:()=>n(gp(e,!s))},t)}i(lw,"OptionsToggle");function dO(){let e=Be(),t=me(n=>n.eventLog.visible);return Wf.createElement(TE,{value:t,onChange:()=>e(mp())},"Display Event Log")}i(dO,"EventlogToggle");function pO(){let e=Be(),t=me(n=>n.commandBar.visible);return Wf.createElement(TE,{value:t,onChange:()=>e(d0())},"Display Command Bar")}i(pO,"CommandBarToggle");var kE=de(be());function NE({children:e,resource:t}){let n=`https://docs.mitmproxy.org/stable/${t}`;return kE.createElement("a",{target:"_blank",href:n,rel:"noreferrer"},e||kE.createElement("i",{className:"fa fa-question-circle"}))}i(NE,"DocsLink");uw.title="Options";function uw(){let e=Be(),t=i(()=>KP("OptionModal"),"openOptions");return cn.createElement("div",null,cn.createElement(hs,null,cn.createElement("div",{className:"menu-group"},cn.createElement("div",{className:"menu-content"},cn.createElement(Or,{title:"Open Options",icon:"fa-cogs text-primary",onClick:()=>e(t())},"Edit Options ",cn.createElement("sup",null,"alpha"))),cn.createElement("div",{className:"menu-legend"},"Options Editor")),cn.createElement("div",{className:"menu-group"},cn.createElement("div",{className:"menu-content"},cn.createElement(lw,{name:"anticache"},"Strip cache headers"," ",cn.createElement(NE,{resource:"overview-features/#anticache"})),cn.createElement(lw,{name:"showhost"},"Use host header for display"),cn.createElement(lw,{name:"ssl_insecure"},"Don't verify server certificates")),cn.createElement("div",{className:"menu-legend"},"Quick Options"))),cn.createElement("div",{className:"menu-group"},cn.createElement("div",{className:"menu-content"},cn.createElement(dO,null),cn.createElement(pO,null)),cn.createElement("div",{className:"menu-legend"},"View Options")))}i(uw,"OptionMenu");var ft=de(be());var Ip=i(async(e,t)=>{let n=(async()=>{let s=await gf("export",t,`@${e.id}`);if(s.value)return s.value;throw s.error?s.error:s})();try{await LP(n)}catch(s){alert(s)}},"copy");fw.title="Flow";function fw(){let e=Be(),t=me(n=>n.flows.byId[n.flows.selected[0]]);return t?ft.createElement("div",{className:"flow-menu"},ft.createElement(hs,null,ft.createElement("div",{className:"menu-group"},ft.createElement("div",{className:"menu-content"},ft.createElement(Or,{title:"[r]eplay flow",icon:"fa-repeat text-primary",onClick:()=>e(up(t)),disabled:!Uy(t)},"Replay"),ft.createElement(Or,{title:"[D]uplicate flow",icon:"fa-copy text-info",onClick:()=>e(Ky(t))},"Duplicate"),ft.createElement(Or,{disabled:!t||!t.modified,title:"revert changes to flow [V]",icon:"fa-history text-warning",onClick:()=>e(Gy(t))},"Revert"),ft.createElement(Or,{title:"[d]elete flow",icon:"fa-trash text-danger",onClick:()=>e(jy(t))},"Delete"),ft.createElement(fW,{flow:t})),ft.createElement("div",{className:"menu-legend"},"Flow Modification"))),ft.createElement("div",{className:"menu-group"},ft.createElement("div",{className:"menu-content"},ft.createElement(lW,{flow:t}),ft.createElement(uW,{flow:t})),ft.createElement("div",{className:"menu-legend"},"Export")),ft.createElement(hs,null,ft.createElement("div",{className:"menu-group"},ft.createElement("div",{className:"menu-content"},ft.createElement(Or,{disabled:!t||!t.intercepted,title:"[a]ccept intercepted flow",icon:"fa-play text-success",onClick:()=>e(lp(t))},"Resume"),ft.createElement(Or,{disabled:!t||!t.intercepted,title:"kill intercepted flow [x]",icon:"fa-times text-danger",onClick:()=>e(qy(t))},"Abort")),ft.createElement("div",{className:"menu-legend"},"Interception")))):ft.createElement("div",null)}i(fw,"FlowMenu");var cw=i(e=>{let t=window.open(e,"_blank","noopener,noreferrer");t&&(t.opener=null)},"openInNewTab");function lW({flow:e}){if(e.type!=="http")return ft.createElement(Or,{icon:"fa-download",onClick:()=>0,disabled:!0},"Download");if(e.request.contentLength&&!e.response?.contentLength)return ft.createElement(Or,{icon:"fa-download",onClick:()=>cw(Rn.getContentURL(e,e.request))},"Download");if(e.response){let t=e.response;if(!e.request.contentLength&&e.response.contentLength)return ft.createElement(Or,{icon:"fa-download",onClick:()=>cw(Rn.getContentURL(e,t))},"Download");if(e.request.contentLength&&e.response.contentLength)return ft.createElement(Yu,{text:ft.createElement(Or,{icon:"fa-download",onClick:()=>1},"Download\u25BE"),options:{placement:"bottom-start"}},ft.createElement(li,{onClick:()=>cw(Rn.getContentURL(e,e.request))},"Download request"),ft.createElement(li,{onClick:()=>cw(Rn.getContentURL(e,t))},"Download response"))}return null}i(lW,"DownloadButton");function uW({flow:e}){return ft.createElement(Yu,{className:"",text:ft.createElement(Or,{title:"Export flow.",icon:"fa-clone",onClick:()=>1,disabled:e.type!=="http"},"Export\u25BE"),options:{placement:"bottom-start"}},ft.createElement(li,{onClick:()=>Ip(e,"raw_request")},"Copy raw request"),ft.createElement(li,{onClick:()=>Ip(e,"raw_response")},"Copy raw response"),ft.createElement(li,{onClick:()=>Ip(e,"raw")},"Copy raw request and response"),ft.createElement(li,{onClick:()=>Ip(e,"curl")},"Copy as cURL"),ft.createElement(li,{onClick:()=>Ip(e,"httpie")},"Copy as HTTPie"))}i(uW,"ExportButton");var cW={":red_circle:":"\u{1F534}",":orange_circle:":"\u{1F7E0}",":yellow_circle:":"\u{1F7E1}",":green_circle:":"\u{1F7E2}",":large_blue_circle:":"\u{1F535}",":purple_circle:":"\u{1F7E3}",":brown_circle:":"\u{1F7E4}"};function fW({flow:e}){let t=Be();return ft.createElement(Yu,{className:"",text:ft.createElement(Or,{title:"mark flow",icon:"fa-paint-brush text-success",onClick:()=>1},"Mark\u25BE"),options:{placement:"bottom-start"}},ft.createElement(li,{onClick:()=>t(si(e,{marked:""}))},"\u26AA (no marker)"),Object.entries(cW).map(([n,s])=>ft.createElement(li,{key:n,onClick:()=>t(si(e,{marked:n}))},s," ",n.replace(/[:_]/g," "))))}i(fW,"MarkButton");var hO={0:sw,1:aw,2:uw,3:fw};function PE(){let e=Be(),t=me(k=>k.ui.tabs.current),n=me(k=>k.flows.selected.filter(R=>R in k.flows.byId),Mo),[s,l]=(0,to.useState)(!1),h=me(k=>k.flows.list.length>0),d=me(k=>k.ui.tabs.isInitial),v=[0,1,2];n.length>0&&v.push(3),(0,to.useEffect)(()=>{h&&d&&e(Fm(1))},[h]),(0,to.useEffect)(()=>{n.length>0&&!s?(e(Fm(3)),l(!0)):n.length===0&&(s&&l(!1),t===3&&e(Fm(1)))},[n,s,t]);function S(k,R){R.preventDefault(),e(Fm(k))}i(S,"handleClick");let b=hO[t];return to.default.createElement("header",null,to.default.createElement("nav",{className:"nav-tabs nav-tabs-lg"},to.default.createElement(uO,null),v.map(k=>to.default.createElement("a",{key:k,href:"#",className:(0,mO.default)({active:k===t}),onClick:R=>S(k,R)},hO[k].title)),to.default.createElement(hs,null,to.default.createElement(cO,null))),to.default.createElement("div",null,to.default.createElement(b,null)))}i(PE,"Header");var nt=de(be()),gO=de(ai());var dw=function(){"use strict";function e(s,l){function h(){this.constructor=s}i(h,"ctor"),h.prototype=l.prototype,s.prototype=new h}i(e,"peg$subclass");function t(s,l,h,d){this.message=s,this.expected=l,this.found=h,this.location=d,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,t)}i(t,"peg$SyntaxError"),e(t,Error);function n(s){var l=arguments.length>1?arguments[1]:{},h=this,d={},v={Expr:Bn},S=Bn,b=i(function(X,ee){return[X,...ee]},"peg$c0"),k=i(function(X){return[X]},"peg$c1"),R=i(function(){return""},"peg$c2"),I={type:"other",description:"string"},z='"',j={type:"literal",value:'"',description:'"\\""'},Y=i(function(X){return X.join("")},"peg$c6"),ne="'",F={type:"literal",value:"'",description:`"'"`},L=/^["\\]/,D={type:"class",value:'["\\\\]',description:'["\\\\]'},q={type:"any",description:"any character"},te=i(function(X){return X},"peg$c12"),Z="\\",oe={type:"literal",value:"\\",description:'"\\\\"'},he=/^['\\]/,Re={type:"class",value:"['\\\\]",description:"['\\\\]"},Ee=/^['"\\]/,Ye={type:"class",value:`['"\\\\]`,description:`['"\\\\]`},tt="n",xe={type:"literal",value:"n",description:'"n"'},Xe=i(function(){return` +`},"peg$c21"),je="r",Qe={type:"literal",value:"r",description:'"r"'},ot=i(function(){return"\r"},"peg$c24"),It="t",Pt={type:"literal",value:"t",description:'"t"'},fn=i(function(){return" "},"peg$c27"),dn={type:"other",description:"whitespace"},gr=/^[ \t\n\r]/,Wt={type:"class",value:"[ \\t\\n\\r]",description:"[ \\t\\n\\r]"},vr={type:"other",description:"control character"},yr=/^[|&!()~"]/,Gt={type:"class",value:'[|&!()~"]',description:'[|&!()~"]'},Ft={type:"other",description:"optional whitespace"},se=0,Ue=0,Gr=[{line:1,column:1,seenCR:!1}],Zt=0,st=[],Fe=0,Fn;if("startRule"in l){if(!(l.startRule in v))throw new Error(`Can't start parsing from rule "`+l.startRule+'".');S=v[l.startRule]}function bn(){return s.substring(Ue,se)}i(bn,"text");function no(){return fr(Ue,se)}i(no,"location");function Ho(X){throw io(null,[{type:"other",description:X}],s.substring(Ue,se),fr(Ue,se))}i(Ho,"expected");function lt(X){throw io(X,null,s.substring(Ue,se),fr(Ue,se))}i(lt,"error");function wr(X){var ee=Gr[X],Ce,Me;if(ee)return ee;for(Ce=X-1;!Gr[Ce];)Ce--;for(ee=Gr[Ce],ee={line:ee.line,column:ee.column,seenCR:ee.seenCR};CeZt&&(Zt=se,st=[]),st.push(X))}i(pt,"peg$fail");function io(X,ee,Ce,Me){function Tl(Qr){var Yt=1;for(Qr.sort(function(Mt,At){return Mt.descriptionAt.description?1:0});Yt1?At.slice(0,-1).join(", ")+" or "+At[Qr.length-1]:At[0],pn=Yt?'"'+Mt(Yt)+'"':"end of input","Expected "+Na+" but "+pn+" found."}return i(ze,"buildMessage"),ee!==null&&Tl(ee),new t(X!==null?X:ze(ee,Ce),ee,Ce,Me)}i(io,"peg$buildException");function Bn(){var X,ee,Ce,Me;if(X=se,ee=Mi(),ee!==d){if(Ce=[],Me=Hn(),Me!==d)for(;Me!==d;)Ce.push(Me),Me=Hn();else Ce=d;Ce!==d?(Me=Bn(),Me!==d?(Ue=X,ee=b(ee,Me),X=ee):(se=X,X=d)):(se=X,X=d)}else se=X,X=d;if(X===d&&(X=se,ee=Mi(),ee!==d&&(Ue=X,ee=k(ee)),X=ee,X===d)){for(X=se,ee=[],Ce=Hn();Ce!==d;)ee.push(Ce),Ce=Hn();ee!==d&&(Ue=X,ee=R()),X=ee}return X}i(Bn,"peg$parseExpr");function Mi(){var X,ee,Ce,Me;if(Fe++,X=se,s.charCodeAt(se)===34?(ee=z,se++):(ee=d,Fe===0&&pt(j)),ee!==d){for(Ce=[],Me=Yr();Me!==d;)Ce.push(Me),Me=Yr();Ce!==d?(s.charCodeAt(se)===34?(Me=z,se++):(Me=d,Fe===0&&pt(j)),Me!==d?(Ue=X,ee=Y(Ce),X=ee):(se=X,X=d)):(se=X,X=d)}else se=X,X=d;if(X===d){if(X=se,s.charCodeAt(se)===39?(ee=ne,se++):(ee=d,Fe===0&&pt(F)),ee!==d){for(Ce=[],Me=Xr();Me!==d;)Ce.push(Me),Me=Xr();Ce!==d?(s.charCodeAt(se)===39?(Me=ne,se++):(Me=d,Fe===0&&pt(F)),Me!==d?(Ue=X,ee=Y(Ce),X=ee):(se=X,X=d)):(se=X,X=d)}else se=X,X=d;if(X===d){if(X=se,ee=se,Fe++,Ce=so(),Fe--,Ce===d?ee=void 0:(se=ee,ee=d),ee!==d){if(Ce=[],Me=oo(),Me!==d)for(;Me!==d;)Ce.push(Me),Me=oo();else Ce=d;Ce!==d?(Ue=X,ee=Y(Ce),X=ee):(se=X,X=d)}else se=X,X=d;if(X===d){if(X=se,s.charCodeAt(se)===34?(ee=z,se++):(ee=d,Fe===0&&pt(j)),ee!==d){for(Ce=[],Me=Yr();Me!==d;)Ce.push(Me),Me=Yr();Ce!==d?(Ue=X,ee=Y(Ce),X=ee):(se=X,X=d)}else se=X,X=d;if(X===d)if(X=se,s.charCodeAt(se)===39?(ee=ne,se++):(ee=d,Fe===0&&pt(F)),ee!==d){for(Ce=[],Me=Xr();Me!==d;)Ce.push(Me),Me=Xr();Ce!==d?(Ue=X,ee=Y(Ce),X=ee):(se=X,X=d)}else se=X,X=d}}}return Fe--,X===d&&(ee=d,Fe===0&&pt(I)),X}i(Mi,"peg$parseStringLiteral");function Yr(){var X,ee,Ce;return X=se,ee=se,Fe++,L.test(s.charAt(se))?(Ce=s.charAt(se),se++):(Ce=d,Fe===0&&pt(D)),Fe--,Ce===d?ee=void 0:(se=ee,ee=d),ee!==d?(s.length>se?(Ce=s.charAt(se),se++):(Ce=d,Fe===0&&pt(q)),Ce!==d?(Ue=X,ee=te(Ce),X=ee):(se=X,X=d)):(se=X,X=d),X===d&&(X=se,s.charCodeAt(se)===92?(ee=Z,se++):(ee=d,Fe===0&&pt(oe)),ee!==d?(Ce=hi(),Ce!==d?(Ue=X,ee=te(Ce),X=ee):(se=X,X=d)):(se=X,X=d)),X}i(Yr,"peg$parseDoubleStringChar");function Xr(){var X,ee,Ce;return X=se,ee=se,Fe++,he.test(s.charAt(se))?(Ce=s.charAt(se),se++):(Ce=d,Fe===0&&pt(Re)),Fe--,Ce===d?ee=void 0:(se=ee,ee=d),ee!==d?(s.length>se?(Ce=s.charAt(se),se++):(Ce=d,Fe===0&&pt(q)),Ce!==d?(Ue=X,ee=te(Ce),X=ee):(se=X,X=d)):(se=X,X=d),X===d&&(X=se,s.charCodeAt(se)===92?(ee=Z,se++):(ee=d,Fe===0&&pt(oe)),ee!==d?(Ce=hi(),Ce!==d?(Ue=X,ee=te(Ce),X=ee):(se=X,X=d)):(se=X,X=d)),X}i(Xr,"peg$parseSingleStringChar");function oo(){var X,ee,Ce;return X=se,ee=se,Fe++,Ce=Hn(),Fe--,Ce===d?ee=void 0:(se=ee,ee=d),ee!==d?(s.length>se?(Ce=s.charAt(se),se++):(Ce=d,Fe===0&&pt(q)),Ce!==d?(Ue=X,ee=te(Ce),X=ee):(se=X,X=d)):(se=X,X=d),X}i(oo,"peg$parseUnquotedStringChar");function hi(){var X,ee;return Ee.test(s.charAt(se))?(X=s.charAt(se),se++):(X=d,Fe===0&&pt(Ye)),X===d&&(X=se,s.charCodeAt(se)===110?(ee=tt,se++):(ee=d,Fe===0&&pt(xe)),ee!==d&&(Ue=X,ee=Xe()),X=ee,X===d&&(X=se,s.charCodeAt(se)===114?(ee=je,se++):(ee=d,Fe===0&&pt(Qe)),ee!==d&&(Ue=X,ee=ot()),X=ee,X===d&&(X=se,s.charCodeAt(se)===116?(ee=It,se++):(ee=d,Fe===0&&pt(Pt)),ee!==d&&(Ue=X,ee=fn()),X=ee))),X}i(hi,"peg$parseEscapeSequence");function Hn(){var X,ee;return Fe++,gr.test(s.charAt(se))?(X=s.charAt(se),se++):(X=d,Fe===0&&pt(Wt)),Fe--,X===d&&(ee=d,Fe===0&&pt(dn)),X}i(Hn,"peg$parsews");function so(){var X,ee;return Fe++,yr.test(s.charAt(se))?(X=s.charAt(se),se++):(X=d,Fe===0&&pt(Gt)),Fe--,X===d&&(ee=d,Fe===0&&pt(vr)),X}i(so,"peg$parsecc");function bl(){var X,ee;for(Fe++,X=[],ee=Hn();ee!==d;)X.push(ee),ee=Hn();return Fe--,X===d&&(ee=d,Fe===0&&pt(Ft)),X}if(i(bl,"peg$parse__"),Fn=S(),Fn!==d&&se===s.length)return Fn;throw Fn!==d&&se{t&&t.current.addEventListener("DOMNodeInserted",n=>{let s=n.currentTarget;s.scroll({top:s.scrollHeight,behavior:"auto"})})},[]),nt.default.createElement("div",{className:"command-result",ref:t},e.map((n,s)=>nt.default.createElement("div",{key:s},nt.default.createElement("div",null,nt.default.createElement("strong",null,"$ ",n.command)),n.result)))}i(dW,"Results");function pW({nextArgs:e,currentArg:t,help:n,description:s,availableCommands:l}){let h=[];for(let d=0;d0&&nt.default.createElement("div",null,nt.default.createElement("strong",null,"Argument suggestion:")," ",h),n?.includes("->")&&nt.default.createElement("div",null,nt.default.createElement("strong",null,"Signature help: "),n),s&&nt.default.createElement("div",null,"# ",s),nt.default.createElement("div",null,nt.default.createElement("strong",null,"Available Commands: "),nt.default.createElement("p",{className:"available-commands"},JSON.stringify(l)))))}i(pW,"CommandHelp");function LE(){let[e,t]=(0,nt.useState)(""),[n,s]=(0,nt.useState)(""),[l,h]=(0,nt.useState)(0),[d,v]=(0,nt.useState)([]),[S,b]=(0,nt.useState)([]),[k,R]=(0,nt.useState)({}),[I,z]=(0,nt.useState)([]),[j,Y]=(0,nt.useState)(0),[ne,F]=(0,nt.useState)(""),[L,D]=(0,nt.useState)(""),[q,te]=(0,nt.useState)([]),[Z,oe]=(0,nt.useState)([]),[he,Re]=(0,nt.useState)(void 0);(0,nt.useEffect)(()=>{xt("/commands",{method:"GET"}).then(Xe=>Xe.json()).then(Xe=>{R(Xe),v(AE(Xe)),b(Object.keys(Xe))}).catch(Xe=>console.error(Xe))},[]),(0,nt.useEffect)(()=>{gf("commands.history.get").then(Xe=>{oe(Xe.value)}).catch(Xe=>console.error(Xe))},[]);let Ee=i((Xe,je)=>{let Qe=dw.parse(je),ot=dw.parse(Xe);F(k[Qe[0]]?.signature_help),D(k[Qe[0]]?.help||""),v(AE(k,ot[0])),b(AE(k,Qe[0]));let It=k[Qe[0]]?.parameters.map(Pt=>Pt.name);It&&(z([Qe[0],...It]),Y(Qe.length-1))},"parseCommand"),Ye=i(Xe=>{t(Xe.target.value),s(Xe.target.value),h(0)},"onChange"),tt=i(Xe=>{if(Xe.key==="Enter"){let[je,...Qe]=dw.parse(e);oe([...Z,e]),gf("commands.history.add",e).catch(()=>0),xt.post(`/commands/${je}`,{arguments:Qe}).then(ot=>ot.json()).then(ot=>{Re(void 0),z([]),te([...q,{command:e,result:JSON.stringify(ot.value||ot.error)}])}).catch(ot=>{Re(void 0),z([]),te([...q,{command:e,result:ot.toString()}])}),F(""),D(""),t(""),s(""),h(0),v(S)}if(Xe.key==="ArrowUp"){let je;he===void 0?je=Z.length-1:je=Math.max(0,he-1),t(Z[je]),s(Z[je]),Re(je)}if(Xe.key==="ArrowDown"){if(he===void 0)return;if(he==Z.length-1)t(""),s(""),Re(void 0);else{let je=he+1;t(Z[je]),s(Z[je]),Re(je)}}Xe.key==="Tab"&&(t(d[l]),h((l+1)%d.length),Xe.preventDefault()),Xe.stopPropagation()},"onKeyDown"),xe=i(Xe=>{if(!e){b(Object.keys(k));return}Ee(n,e),Xe.stopPropagation()},"onKeyUp");return nt.default.createElement("div",{className:"command"},nt.default.createElement("div",{className:"command-title"},"Command Result"),nt.default.createElement(dW,{results:q}),nt.default.createElement(pW,{nextArgs:I,currentArg:j,help:ne,description:L,availableCommands:S}),nt.default.createElement("div",{className:(0,gO.default)("command-input input-group")},nt.default.createElement("span",{className:"input-group-addon"},nt.default.createElement("i",{className:"fa fa-fw fa-terminal"})),nt.default.createElement("input",{type:"text",placeholder:"Enter command",className:"form-control",value:e||"",onChange:Ye,onKeyDown:tt,onKeyUp:xe})))}i(LE,"CommandBar");var El=de(be());var ME=de(be());function OE({checked:e,onToggle:t,text:n}){return ME.default.createElement("div",{className:"btn btn-toggle "+(e?"btn-primary":"btn-default"),onClick:t},ME.default.createElement("i",{className:"fa fa-fw "+(e?"fa-check-square-o":"fa-square-o")}),"\xA0",n)}i(OE,"ToggleButton");var ka=de(be()),RE=de(EO());var fg=class extends ka.Component{constructor(n){super(n);this.viewport=ka.default.createRef();this.heights={},this.state={vScroll:Df()},this.onViewportUpdate=this.onViewportUpdate.bind(this)}static{i(this,"EventLogList")}static{this.propTypes={events:RE.default.array.isRequired,rowHeight:RE.default.number}}static{this.defaultProps={rowHeight:18}}componentDidMount(){window.addEventListener("resize",this.onViewportUpdate),this.onViewportUpdate()}componentWillUnmount(){window.removeEventListener("resize",this.onViewportUpdate)}getSnapshotBeforeUpdate(){return Zm(this.viewport)}componentDidUpdate(n,s,l){l&&V0(this.viewport),this.onViewportUpdate()}onViewportUpdate(){let n=this.viewport.current,s=Df({itemCount:this.props.events.length,rowHeight:this.props.rowHeight,viewportTop:n.scrollTop,viewportHeight:n.offsetHeight,itemHeights:this.props.events.map(l=>this.heights[l.id])});Mo(this.state.vScroll,s)||this.setState({vScroll:s})}setHeight(n,s){if(s&&!this.heights[n]){let l=s.offsetHeight;this.heights[n]!==l&&(this.heights[n]=l,this.onViewportUpdate())}}render(){let{vScroll:n}=this.state,{events:s}=this.props;return ka.default.createElement("pre",{ref:this.viewport,onScroll:this.onViewportUpdate},ka.default.createElement("div",{style:{height:n.paddingTop}}),s.slice(n.start,n.end).map(l=>ka.default.createElement("div",{key:l.id,ref:h=>this.setHeight(l.id,h)},ka.default.createElement(gW,{event:l}),l.message)),ka.default.createElement("div",{style:{height:n.paddingBottom}}))}};function gW({event:e}){let t={web:"html5",debug:"bug",warn:"exclamation-triangle",error:"ban"}[e.level]||"info";return ka.default.createElement("i",{className:`fa fa-fw fa-${t}`})}i(gW,"LogIcon");var DE=class extends El.Component{static{i(this,"PureEventLog")}static{this.defaultProps={defaultHeight:200}}constructor(t,n){super(t,n),this.state={height:this.props.defaultHeight},this.onDragStart=this.onDragStart.bind(this),this.onDragMove=this.onDragMove.bind(this),this.onDragStop=this.onDragStop.bind(this)}onDragStart(t){t.preventDefault(),this.dragStart=this.state.height+t.pageY,window.addEventListener("mousemove",this.onDragMove),window.addEventListener("mouseup",this.onDragStop),window.addEventListener("dragend",this.onDragStop)}onDragMove(t){t.preventDefault(),this.setState({height:this.dragStart-t.pageY})}onDragStop(t){t.preventDefault(),window.removeEventListener("mousemove",this.onDragMove)}render(){let{height:t}=this.state,{filters:n,events:s,toggleFilter:l,close:h}=this.props;return El.default.createElement("div",{className:"eventlog",style:{height:t}},El.default.createElement("div",{onMouseDown:this.onDragStart},"Eventlog",El.default.createElement("div",{className:"pull-right"},Object.values(mC).map(d=>El.default.createElement(OE,{key:d,text:d,checked:n[d],onToggle:()=>l(d)})),El.default.createElement("i",{onClick:h,className:"fa fa-close"}))),El.default.createElement(fg,{events:s}))}},bO=Wu(e=>({filters:e.eventLog.filters,events:e.eventLog.view}),{close:mp,toggleFilter:MA})(DE);var En=de(be());function IE(){let e=me(F=>F.backendState.version),{mode:t,intercept:n,showhost:s,upstream_cert:l,rawtcp:h,http2:d,websocket:v,anticache:S,anticomp:b,stickyauth:k,stickycookie:R,stream_large_bodies:I,listen_host:z,listen_port:j,server:Y,ssl_insecure:ne}=me(F=>F.options);return En.createElement("footer",null,t&&(t.length!==1||t[0]!=="regular")&&En.createElement("span",{className:"label label-success"},t.join(",")),n&&En.createElement("span",{className:"label label-success"},"Intercept: ",n),ne&&En.createElement("span",{className:"label label-danger"},"ssl_insecure"),s&&En.createElement("span",{className:"label label-success"},"showhost"),!l&&En.createElement("span",{className:"label label-success"},"no-upstream-cert"),!h&&En.createElement("span",{className:"label label-success"},"no-raw-tcp"),!d&&En.createElement("span",{className:"label label-success"},"no-http2"),!v&&En.createElement("span",{className:"label label-success"},"no-websocket"),S&&En.createElement("span",{className:"label label-success"},"anticache"),b&&En.createElement("span",{className:"label label-success"},"anticomp"),k&&En.createElement("span",{className:"label label-success"},"stickyauth: ",k),R&&En.createElement("span",{className:"label label-success"},"stickycookie: ",R),I&&En.createElement("span",{className:"label label-success"},"stream: ",Fy(I)),En.createElement("div",{className:"pull-right"},En.createElement(hs,null,Y&&En.createElement("span",{className:"label label-primary",title:"HTTP Proxy Server Address"},z||"*",":",j||8080)),En.createElement("span",{className:"label label-default",title:"Mitmproxy Version"},"mitmproxy ",e)))}i(IE,"Footer");var UE=de(be());var zE=de(be());var Fp=de(be());function FE({children:e}){return Fp.createElement("div",null,Fp.createElement("div",{className:"modal-backdrop fade in"}),Fp.createElement("div",{className:"modal modal-visible",id:"optionsModal",tabIndex:-1,role:"dialog","aria-labelledby":"options"},Fp.createElement("div",{className:"modal-dialog modal-lg",role:"document"},Fp.createElement("div",{className:"modal-content"},e))))}i(FE,"ModalLayout");var Ir=de(be());var dg=de(Tm());var ro=de(be());var PO=de(ai());var vW=i(e=>{e.key!=="Escape"&&e.stopPropagation()},"stopPropagation");function AO({value:e,onChange:t,...n}){return ro.default.createElement("div",{className:"checkbox"},ro.default.createElement("label",null,ro.default.createElement("input",{type:"checkbox",checked:e,onChange:s=>t(s.target.checked),...n}),"Enable"))}i(AO,"BooleanOption");function TO({value:e,onChange:t,...n}){return ro.default.createElement("input",{type:"text",value:e||"",onChange:s=>t(s.target.value),...n})}i(TO,"StringOption");function kO(e){return i(function({onChange:n,...s}){return ro.default.createElement(e,{onChange:l=>n(l||null),...s})},"OptionalWrapper")}i(kO,"Optional");function NO({value:e,onChange:t,...n}){return ro.default.createElement("input",{type:"number",value:e,onChange:s=>t(parseInt(s.target.value)),...n})}i(NO,"NumberOption");function yW({value:e,onChange:t,choices:n,...s}){return ro.default.createElement("select",{onChange:l=>t(l.target.value),value:e,...s},n.map(l=>ro.default.createElement("option",{key:l,value:l},l)))}i(yW,"ChoicesOption");function wW({value:e,onChange:t,...n}){let s=Math.max(e.length,1),[l,h]=ro.default.useState(e.join(` +`));return ro.default.createElement("textarea",{rows:s,value:l,onChange:i(v=>{let S=v.target.value;h(S),t(S.split(` +`).map(b=>b.trim()).filter(b=>b!==""))},"handleChange"),...n})}i(wW,"StringSequenceOption");var SW={bool:AO,str:TO,int:NO,"optional str":kO(TO),"optional int":kO(NO),"sequence of str":wW};function xW({choices:e,type:t,value:n,onChange:s,name:l,error:h}){let d,v={onChange:s,value:n};if(e)d=yW,v.choices=e;else if(d=SW[t],!d)throw`unknown option type ${t}`;return d!==AO&&(v.className="form-control"),ro.default.createElement("div",{className:(0,PO.default)({"has-error":h})},ro.default.createElement(d,{name:l,onKeyDown:vW,...v}))}i(xW,"PureOption");function BE({name:e}){let t=Be(),n=me(d=>d.options_meta[e]?.choices),s=me(d=>d.options_meta[e]?.type),l=me(d=>{let v=d.ui.optionsEditor[e];return v?v.value:d.options_meta[e]?.value}),h=me(d=>d.ui.optionsEditor[e]?.error);return ro.default.createElement(xW,{name:e,choices:n,type:s,value:l,error:h,onChange:d=>t(gp(e,d))})}i(BE,"OptionInput");function CW({name:e}){let t=me(n=>n.options_meta[e]?.help);return Ir.default.createElement("div",{className:"help-block small"},t)}i(CW,"OptionHelp");function _W({name:e}){let t=me(n=>n.options_meta[e]?.error);return t?Ir.default.createElement("div",{className:"small text-danger"},t):null}i(_W,"OptionError");function EW({value:e,defaultVal:t}){if(e===t)return null;if(typeof t=="boolean")t=t?"true":"false";else if(Array.isArray(t)){if((0,dg.isEmpty)((0,dg.compact)(e))&&(0,dg.isEmpty)(t))return null;t="[ ]"}else t===""?t='""':t===null&&(t="null");return Ir.default.createElement("div",{className:"small"},"Default: ",Ir.default.createElement("strong",null," ",t," ")," ")}i(EW,"PureOptionDefault");var bW=Wu((e,{name:t})=>({value:e.options[t],defaultVal:e.options_meta[t]?.default}))(EW);function HE(){let e=Be(),t=me(n=>Object.keys(n.options_meta),Mo).sort();return Ir.default.createElement("div",null,Ir.default.createElement("div",{className:"modal-header"},Ir.default.createElement("button",{type:"button",className:"close","data-dismiss":"modal",onClick:()=>e(Qy())},Ir.default.createElement("i",{className:"fa fa-fw fa-times"})),Ir.default.createElement("div",{className:"modal-title"},Ir.default.createElement("h4",null,"Options"))),Ir.default.createElement("div",{className:"modal-body"},Ir.default.createElement("div",{className:"form-horizontal"},t.map(n=>Ir.default.createElement("div",{key:n,className:"form-group"},Ir.default.createElement("div",{className:"col-xs-6"},Ir.default.createElement("label",{htmlFor:n},n),Ir.default.createElement(CW,{name:n})),Ir.default.createElement("div",{className:"col-xs-6"},Ir.default.createElement(BE,{name:n}),Ir.default.createElement(_W,{name:n}),Ir.default.createElement(bW,{name:n})))))),Ir.default.createElement("div",{className:"modal-footer"}))}i(HE,"OptionModal");function TW(){return zE.createElement(FE,null,zE.createElement(HE,null))}i(TW,"OptionModal");var LO=[TW];function WE(){let e=me(n=>n.ui.modal.activeModal),t=LO.find(n=>n.name===e);return e&&t!==void 0?UE.createElement(t,null):UE.createElement("div",null)}i(WE,"PureModal");var $E=class extends Jn.Component{constructor(){super(...arguments);this.state={};this.render=i(()=>{let{showEventLog:n,showCommandBar:s}=this.props;return this.state.error?(console.log("ERR",this.state),Jn.default.createElement("div",{className:"container"},Jn.default.createElement("h1",null,"mitmproxy has crashed."),Jn.default.createElement("pre",null,this.state.error.stack,Jn.default.createElement("br",null),Jn.default.createElement("br",null),"Component Stack:",this.state.errorInfo?.componentStack),Jn.default.createElement("p",null,"Please lodge a bug report at"," ",Jn.default.createElement("a",{href:"https://github.com/mitmproxy/mitmproxy/issues"},"https://github.com/mitmproxy/mitmproxy/issues"),"."))):Jn.default.createElement("div",{id:"container",tabIndex:0},Jn.default.createElement(PE,null),Jn.default.createElement(bE,null),s&&Jn.default.createElement(LE,{key:"commandbar"}),n&&Jn.default.createElement(bO,{key:"eventlog"}),Jn.default.createElement(IE,null),Jn.default.createElement(WE,null))},"render")}static{i(this,"ProxyAppMain")}componentDidMount(){window.addEventListener("keydown",this.props.onKeyDown)}componentWillUnmount(){window.removeEventListener("keydown",this.props.onKeyDown)}componentDidCatch(n,s){this.setState({error:n,errorInfo:s})}},MO=Wu(e=>({showEventLog:e.eventLog.visible,showCommandBar:e.commandBar.visible}),{onKeyDown:JL})($E);var rc={SEARCH:"s",HIGHLIGHT:"h",SHOW_EVENTLOG:"e",SHOW_COMMANDBAR:"c"};function kW(e){let[t,n]=window.location.hash.substr(1).split("?",2),s=t.substr(1).split("/");if(s[0]==="flows"&&s.length==3){let[l,h]=s.slice(1);e.dispatch(yl(l)),e.dispatch(mf(h))}n&&n.split("&").forEach(l=>{let[h,d]=l.split("=",2),v=decodeURIComponent(d);switch(h){case rc.SEARCH:e.dispatch(Wy(v));break;case rc.HIGHLIGHT:e.dispatch($y(v));break;case rc.SHOW_EVENTLOG:e.getState().eventLog.visible||e.dispatch(mp());break;case rc.SHOW_COMMANDBAR:e.getState().commandBar.visible||e.dispatch(d0());break;default:console.error(`unimplemented query arg: ${l}`)}})}i(kW,"updateStoreFromUrl");function NW(e){let t=e.getState(),n={[rc.SEARCH]:t.flows.filter,[rc.HIGHLIGHT]:t.flows.highlight,[rc.SHOW_EVENTLOG]:t.eventLog.visible,[rc.SHOW_COMMANDBAR]:t.commandBar.visible},s=Object.keys(n).filter(d=>n[d]).map(d=>`${d}=${encodeURIComponent(n[d])}`).join("&"),l;t.flows.selected.length>0?l=`/flows/${t.flows.selected[0]}/${t.ui.flow.tab}`:l="/flows",s&&(l+="?"+s);let h=window.location.pathname;h==="blank"&&(h="/"),window.location.hash.substr(1)!==l&&history.replaceState(void 0,"",`${h}#${l}`)}i(NW,"updateUrlFromStore");function VE(e){kW(e),e.subscribe(()=>NW(e))}i(VE,"initialize");var PW="reset",pg=class{static{i(this,"WebsocketBackend")}constructor(t){this.activeFetches={},this.store=t,this.connect()}connect(){this.socket=new WebSocket(location.origin.replace("http","ws")+location.pathname.replace(/\/$/,"")+"/updates"),this.socket.addEventListener("open",()=>this.onOpen()),this.socket.addEventListener("close",t=>this.onClose(t)),this.socket.addEventListener("message",t=>this.onMessage(JSON.parse(t.data))),this.socket.addEventListener("error",t=>this.onError(t))}onOpen(){this.fetchData("state"),this.fetchData("flows"),this.fetchData("events"),this.fetchData("options"),this.store.dispatch($A())}fetchData(t){let n=[];this.activeFetches[t]=n,xt(`./${t}`).then(s=>s.json()).then(s=>{this.activeFetches[t]===n&&this.receive(t,s)})}onMessage(t){if(t.cmd===PW)return this.fetchData(t.resource);if(t.resource in this.activeFetches)this.activeFetches[t.resource].push(t);else{let n=`${t.resource}_${t.cmd}`.toUpperCase();this.store.dispatch({type:n,...t})}}receive(t,n){let s=`${t}_RECEIVE`.toUpperCase();t==="state"?this.store.dispatch({type:s,payload:n}):this.store.dispatch({type:s,cmd:"receive",resource:t,data:n});let l=this.activeFetches[t];delete this.activeFetches[t],l.forEach(h=>this.onMessage(h)),Object.keys(this.activeFetches).length===0&&this.store.dispatch(VA())}onClose(t){this.store.dispatch(qA(`Connection closed at ${new Date().toUTCString()} with error code ${t.code}.`)),console.error("websocket connection closed",t)}onError(...t){console.error("websocket connection errored",t)}};var hg=class{static{i(this,"StaticBackend")}constructor(t){this.store=t,this.onOpen()}onOpen(){this.fetchData("flows"),this.fetchData("options")}fetchData(t){xt(`./${t}`).then(n=>n.json()).then(n=>{this.receive(t,n)})}receive(t,n){let s=`${t}_RECEIVE`.toUpperCase();this.store.dispatch({type:s,cmd:"receive",resource:t,data:n})}};VE(_f);window.MITMWEB_STATIC?window.backend=new hg(_f):window.backend=new pg(_f);window.addEventListener("error",e=>{_f.dispatch(OA(`${e.message} +${e.error.stack}`))});document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("mitmproxy");(0,OO.createRoot)(e).render(qE.createElement(TP,{store:_f},qE.createElement(MO,null)))});})(); +/*! Bundled license information: + +react/cjs/react.production.min.js: + (** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + *) + +scheduler/cjs/scheduler.production.min.js: + (** + * @license React + * scheduler.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + *) + +react-dom/cjs/react-dom.production.min.js: + (** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + *) + +use-sync-external-store/cjs/use-sync-external-store-with-selector.production.min.js: + (** + * @license React + * use-sync-external-store-with-selector.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + *) + +lodash/lodash.js: + (** + * @license + * Lodash + * Copyright OpenJS Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + *) + +stable/stable.js: + (*! stable.js 0.1.8, https://github.com/Two-Screen/stable *) + (*! © 2018 Angry Bytes and contributors. MIT licensed. *) + +classnames/index.js: + (*! + Copyright (c) 2018 Jed Watson. + Licensed under the MIT License (MIT), see + http://jedwatson.github.io/classnames + *) */ -/** - * @license - * Lodash - * Copyright OpenJS Foundation and other contributors - * Released under MIT license - * Based on Underscore.js 1.8.3 - * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors - */ -/** @license React v0.20.2 - * scheduler.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ -/** @license React v16.13.1 - * react-is.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ -/** @license React v17.0.2 - * react-dom.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ -/** @license React v17.0.2 - * react.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ -//! stable.js 0.1.8, https://github.com/Two-Screen/stable -//! © 2018 Angry Bytes and contributors. MIT licensed. //# sourceMappingURL=app.js.map diff --git a/mitmproxy/tools/web/static/images/resourceQuicIcon.png b/mitmproxy/tools/web/static/images/resourceQuicIcon.png new file mode 100644 index 0000000000..b5c871c546 Binary files /dev/null and b/mitmproxy/tools/web/static/images/resourceQuicIcon.png differ diff --git a/mitmproxy/tools/web/static/vendor.css b/mitmproxy/tools/web/static/vendor.css index d440a16923..ddb57b81f4 100644 --- a/mitmproxy/tools/web/static/vendor.css +++ b/mitmproxy/tools/web/static/vendor.css @@ -1,5 +1,5 @@ /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:"Glyphicons Halflings";src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format("embedded-opentype"),url(../fonts/glyphicons-halflings-regular.woff2) format("woff2"),url(../fonts/glyphicons-halflings-regular.woff) format("woff"),url(../fonts/glyphicons-halflings-regular.ttf) format("truetype"),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:transparent}body{font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:"\2014 \00A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:""}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:"\00A0 \2014"}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.row-no-gutters{margin-right:0;margin-left:0}.row-no-gutters [class*=col-]{padding-right:0;padding-left:0}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none;appearance:none}input[type=checkbox],input[type=radio]{margin:4px 0 0;line-height:normal}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],.input-group-sm input[type=time],input[type=date].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm,input[type=time].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],.input-group-lg input[type=time],input[type=date].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg,input[type=time].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;background-image:none;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;background-image:none;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;background-image:none;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;background-image:none;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;background-image:none;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:32px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-brand{float:left;height:32px;padding:6px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-right:15px;margin-top:-1px;margin-bottom:-1px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:3px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:6px;padding-bottom:6px}}.navbar-form{padding:10px 15px;margin-right:-15px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin-top:-1px;margin-bottom:-1px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:-1px;margin-bottom:-1px}.navbar-btn.btn-sm{margin-top:1px;margin-bottom:1px}.navbar-btn.btn-xs{margin-top:5px;margin-bottom:5px}.navbar-text{margin-top:6px;margin-bottom:6px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important;float:left}.navbar-right{float:right!important;float:right;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#fff;border-color:#e0e0e0}.navbar-default .navbar-brand{color:#303030}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#161616;background-color:transparent}.navbar-default .navbar-text{color:#303030}.navbar-default .navbar-nav>li>a{color:#303030}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#eee}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#eee}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#303030}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#eee}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e0e0e0}.navbar-default .navbar-link{color:#303030}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#303030}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.5}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none;appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.in{opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:12px;opacity:0}.tooltip.in{opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:14px;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover>.arrow{border-width:11px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-moz-transition:-moz-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;-moz-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);left:0}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);left:0}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:"\2039"}.carousel-control .icon-next:before{content:"\203a"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}/*! * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?v=4.7.0);src:url(fonts/fontawesome-webfont.eot?#iefix&v=4.7.0) format('embedded-opentype'),url(fonts/fontawesome-webfont.woff2?v=4.7.0) format('woff2'),url(fonts/fontawesome-webfont.woff?v=4.7.0) format('woff'),url(fonts/fontawesome-webfont.ttf?v=4.7.0) format('truetype'),url(fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular) format('svg');font-weight:400;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-webkit-transform:scale(1,-1);-ms-transform:scale(1,-1);transform:scale(1,-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-rotate-90{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-close:before,.fa-remove:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-cog:before,.fa-gear:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-repeat:before,.fa-rotate-right:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-exclamation-triangle:before,.fa-warning:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-cogs:before,.fa-gears:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-floppy-o:before,.fa-save:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-sort:before,.fa-unsorted:before{content:"\f0dc"}.fa-sort-desc:before,.fa-sort-down:before{content:"\f0dd"}.fa-sort-asc:before,.fa-sort-up:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-gavel:before,.fa-legal:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-bolt:before,.fa-flash:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-clipboard:before,.fa-paste:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-chain-broken:before,.fa-unlink:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:"\f150"}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:"\f151"}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:"\f152"}.fa-eur:before,.fa-euro:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-inr:before,.fa-rupee:before{content:"\f156"}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:"\f157"}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:"\f158"}.fa-krw:before,.fa-won:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-try:before,.fa-turkish-lira:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-bank:before,.fa-institution:before,.fa-university:before{content:"\f19c"}.fa-graduation-cap:before,.fa-mortar-board:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:"\f1c5"}.fa-file-archive-o:before,.fa-file-zip-o:before{content:"\f1c6"}.fa-file-audio-o:before,.fa-file-sound-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:"\f1d0"}.fa-empire:before,.fa-ge:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-paper-plane:before,.fa-send:before{content:"\f1d8"}.fa-paper-plane-o:before,.fa-send-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-bed:before,.fa-hotel:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-y-combinator:before,.fa-yc:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-television:before,.fa-tv:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:"\f2a3"}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-sign-language:before,.fa-signing:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-address-card:before,.fa-vcard:before{content:"\f2bb"}.fa-address-card-o:before,.fa-vcard-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} + */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?v=4.7.0);src:url(fonts/fontawesome-webfont.eot?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?v=4.7.0) format("woff2"),url(fonts/fontawesome-webfont.woff?v=4.7.0) format("woff"),url(fonts/fontawesome-webfont.ttf?v=4.7.0) format("truetype"),url(fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-webkit-transform:scale(1,-1);-ms-transform:scale(1,-1);transform:scale(1,-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-rotate-90{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-close:before,.fa-remove:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-cog:before,.fa-gear:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-repeat:before,.fa-rotate-right:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-exclamation-triangle:before,.fa-warning:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-cogs:before,.fa-gears:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-floppy-o:before,.fa-save:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-sort:before,.fa-unsorted:before{content:"\f0dc"}.fa-sort-desc:before,.fa-sort-down:before{content:"\f0dd"}.fa-sort-asc:before,.fa-sort-up:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-gavel:before,.fa-legal:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-bolt:before,.fa-flash:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-clipboard:before,.fa-paste:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-chain-broken:before,.fa-unlink:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:"\f150"}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:"\f151"}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:"\f152"}.fa-eur:before,.fa-euro:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-inr:before,.fa-rupee:before{content:"\f156"}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:"\f157"}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:"\f158"}.fa-krw:before,.fa-won:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-try:before,.fa-turkish-lira:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-bank:before,.fa-institution:before,.fa-university:before{content:"\f19c"}.fa-graduation-cap:before,.fa-mortar-board:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:"\f1c5"}.fa-file-archive-o:before,.fa-file-zip-o:before{content:"\f1c6"}.fa-file-audio-o:before,.fa-file-sound-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:"\f1d0"}.fa-empire:before,.fa-ge:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-paper-plane:before,.fa-send:before{content:"\f1d8"}.fa-paper-plane-o:before,.fa-send-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-bed:before,.fa-hotel:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-y-combinator:before,.fa-yc:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-television:before,.fa-tv:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:"\f2a3"}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-sign-language:before,.fa-signing:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-address-card:before,.fa-vcard:before{content:"\f2bb"}.fa-address-card-o:before,.fa-vcard-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} /*# sourceMappingURL=vendor.css.map */ diff --git a/mitmproxy/tools/web/static_viewer.py b/mitmproxy/tools/web/static_viewer.py index 7decf12de0..3f9b5dbc62 100644 --- a/mitmproxy/tools/web/static_viewer.py +++ b/mitmproxy/tools/web/static_viewer.py @@ -7,10 +7,12 @@ from collections.abc import Iterable from typing import Optional -from mitmproxy import contentviews, http +from mitmproxy import contentviews from mitmproxy import ctx +from mitmproxy import flow from mitmproxy import flowfilter -from mitmproxy import io, flow +from mitmproxy import http +from mitmproxy import io from mitmproxy import version from mitmproxy.tools.web.app import flow_to_json diff --git a/mitmproxy/tools/web/templates/index.html b/mitmproxy/tools/web/templates/index.html index 301a46f7fa..6e0b60be4c 100644 --- a/mitmproxy/tools/web/templates/index.html +++ b/mitmproxy/tools/web/templates/index.html @@ -1,15 +1,15 @@ - + - - - mitmproxy - - - - - - - -
    - + + + mitmproxy + + + + + + + +
    + diff --git a/mitmproxy/tools/web/web_columns.py b/mitmproxy/tools/web/web_columns.py new file mode 100644 index 0000000000..9db28850a5 --- /dev/null +++ b/mitmproxy/tools/web/web_columns.py @@ -0,0 +1,15 @@ +# Auto-generated by web/gen/web_columns.py +AVAILABLE_WEB_COLUMNS = [ + "icon", + "index", + "method", + "version", + "path", + "quickactions", + "size", + "status", + "time", + "timestamp", + "tls", + "comment", +] diff --git a/mitmproxy/tools/web/webaddons.py b/mitmproxy/tools/web/webaddons.py index 6d5970988a..7aa209d135 100644 --- a/mitmproxy/tools/web/webaddons.py +++ b/mitmproxy/tools/web/webaddons.py @@ -3,6 +3,7 @@ from collections.abc import Sequence from mitmproxy import ctx +from mitmproxy.tools.web.web_columns import AVAILABLE_WEB_COLUMNS class WebAddon: @@ -15,7 +16,7 @@ def load(self, loader): "web_columns", Sequence[str], ["tls", "icon", "path", "method", "status", "size", "time"], - "Columns to show in the flow list", + f"Columns to show in the flow list. Can be one of the following: {', '.join(AVAILABLE_WEB_COLUMNS)}", ) def running(self): diff --git a/mitmproxy/types.py b/mitmproxy/types.py index e3161aeeae..27c51d569a 100644 --- a/mitmproxy/types.py +++ b/mitmproxy/types.py @@ -1,13 +1,16 @@ import codecs -import os import glob +import os import re from collections.abc import Sequence -from typing import Any, Optional, TYPE_CHECKING, Union +from typing import Any +from typing import TYPE_CHECKING +from typing import Union from mitmproxy import exceptions from mitmproxy import flow -from mitmproxy.utils import emoji, strutils +from mitmproxy.utils import emoji +from mitmproxy.utils import strutils if TYPE_CHECKING: # pragma: no cover from mitmproxy.command import CommandManager @@ -470,7 +473,7 @@ def __init__(self, *types): for t in types: self.typemap[t.typ] = t() - def get(self, t: Optional[type], default=None) -> Optional[_BaseType]: + def get(self, t: type | None, default=None) -> _BaseType | None: if type(t) in self.typemap: return self.typemap[type(t)] return self.typemap.get(t, default) diff --git a/mitmproxy/udp.py b/mitmproxy/udp.py index accc294d41..2d716e9108 100644 --- a/mitmproxy/udp.py +++ b/mitmproxy/udp.py @@ -1,6 +1,7 @@ import time -from mitmproxy import connection, flow +from mitmproxy import connection +from mitmproxy import flow from mitmproxy.coretypes import serializable @@ -51,8 +52,15 @@ def __init__( super().__init__(client_conn, server_conn, live) self.messages = [] - _stateobject_attributes = flow.Flow._stateobject_attributes.copy() - _stateobject_attributes["messages"] = list[UDPMessage] + def get_state(self) -> serializable.State: + return { + **super().get_state(), + "messages": [m.get_state() for m in self.messages], + } + + def set_state(self, state: serializable.State) -> None: + self.messages = [UDPMessage.from_state(m) for m in state.pop("messages")] + super().set_state(state) def __repr__(self): return f"" diff --git a/mitmproxy/utils/arg_check.py b/mitmproxy/utils/arg_check.py index b6736e3ab2..070202de25 100644 --- a/mitmproxy/utils/arg_check.py +++ b/mitmproxy/utils/arg_check.py @@ -1,5 +1,5 @@ -import sys import re +import sys DEPRECATED = """ --confdir @@ -60,6 +60,7 @@ -f --filter --socks +--server-replay-nopop """ REPLACEMENTS = { @@ -75,7 +76,7 @@ "--upstream-trusted-confdir": "ssl_verify_upstream_trusted_confdir", "--upstream-trusted-ca": "ssl_verify_upstream_trusted_ca", "--no-onboarding": "onboarding", - "--no-pop": "server_replay_nopop", + "--no-pop": "server_replay_reuse", "--replay-ignore-content": "server_replay_ignore_content", "--replay-ignore-payload-param": "server_replay_ignore_payload_params", "--replay-ignore-param": "server_replay_ignore_params", @@ -102,6 +103,7 @@ "-f": "--view-filter", "--filter": "--view-filter", "--socks": "--mode socks5", + "--server-replay-nopop": "--server-replay-reuse", } @@ -125,19 +127,21 @@ def check(): "Please use `--proxyauth SPEC` instead.\n" 'SPEC Format: "username:pass", "any" to accept any user/pass combination,\n' '"@path" to use an Apache htpasswd file, or\n' - '"ldap[s]:url_server_ldap:dn_auth:password:dn_subtree" ' + '"ldap[s]:url_server_ldap[:port]:dn_auth:password:dn_subtree[?search_filter_key=...]" ' "for LDAP authentication.".format(option) ) for option in REPLACED.splitlines(): if option in args: - if isinstance(REPLACEMENTS.get(option), list): - new_options = REPLACEMENTS.get(option) + r = REPLACEMENTS.get(option) + if isinstance(r, list): + new_options = r else: - new_options = [REPLACEMENTS.get(option)] + new_options = [r] print( - "{} is deprecated.\n" - "Please use `{}` instead.".format(option, "` or `".join(new_options)) + "{} is deprecated.\n" "Please use `{}` instead.".format( + option, "` or `".join(new_options) + ) ) for option in DEPRECATED.splitlines(): diff --git a/mitmproxy/utils/asyncio_utils.py b/mitmproxy/utils/asyncio_utils.py index 44ec46077f..c545a13d65 100644 --- a/mitmproxy/utils/asyncio_utils.py +++ b/mitmproxy/utils/asyncio_utils.py @@ -1,7 +1,10 @@ import asyncio +import os +import sys import time from collections.abc import Coroutine -from typing import Optional +from collections.abc import Iterator +from contextlib import contextmanager from mitmproxy.utils import human @@ -10,7 +13,7 @@ def create_task( coro: Coroutine, *, name: str, - client: Optional[tuple] = None, + client: tuple | None = None, ) -> asyncio.Task: """ Like asyncio.create_task, but also store some debug info on the task object. @@ -24,10 +27,12 @@ def set_task_debug_info( task: asyncio.Task, *, name: str, - client: Optional[tuple] = None, + client: tuple | None = None, ) -> None: """Set debug info for an externally-spawned task.""" task.created = time.time() # type: ignore + if __debug__ is True and (test := os.environ.get("PYTEST_CURRENT_TEST", None)): + name = f"{name} [created in {test}]" task.set_name(name) if client: task.client = client # type: ignore @@ -36,7 +41,7 @@ def set_task_debug_info( def set_current_task_debug_info( *, name: str, - client: Optional[tuple] = None, + client: tuple | None = None, ) -> None: """Set debug info for the current task.""" task = asyncio.current_task() @@ -56,3 +61,28 @@ def task_repr(task: asyncio.Task) -> str: if client: client = f"{human.format_address(client)}: " return f"{client}{name}{age}" + + +@contextmanager +def install_exception_handler(handler) -> Iterator[None]: + loop = asyncio.get_running_loop() + existing = loop.get_exception_handler() + loop.set_exception_handler(handler) + try: + yield + finally: + loop.set_exception_handler(existing) + + +@contextmanager +def set_eager_task_factory() -> Iterator[None]: + loop = asyncio.get_running_loop() + if sys.version_info < (3, 12): # pragma: no cover + yield + else: + existing = loop.get_task_factory() + loop.set_task_factory(asyncio.eager_task_factory) # type: ignore + try: + yield + finally: + loop.set_task_factory(existing) diff --git a/mitmproxy/utils/data.py b/mitmproxy/utils/data.py index b715072178..baa8c6abd3 100644 --- a/mitmproxy/utils/data.py +++ b/mitmproxy/utils/data.py @@ -1,13 +1,15 @@ -import os.path import importlib import inspect +import os.path class Data: def __init__(self, name): self.name = name m = importlib.import_module(name) - dirname = os.path.dirname(inspect.getsourcefile(m)) + f = inspect.getsourcefile(m) + assert f is not None + dirname = os.path.dirname(f) self.dirname = os.path.abspath(dirname) def push(self, subpath): diff --git a/mitmproxy/utils/debug.py b/mitmproxy/utils/debug.py index a522d46a6f..fc38c68beb 100644 --- a/mitmproxy/utils/debug.py +++ b/mitmproxy/utils/debug.py @@ -18,11 +18,14 @@ def dump_system_info(): mitmproxy_version = version.get_dev_version() + openssl_version: str | bytes = SSL.SSLeay_version(SSL.SSLEAY_VERSION) + if isinstance(openssl_version, bytes): + openssl_version = openssl_version.decode() data = [ f"Mitmproxy: {mitmproxy_version}", f"Python: {platform.python_version()}", - f"OpenSSL: {SSL.SSLeay_version(SSL.SSLEAY_VERSION).decode()}", + f"OpenSSL: {openssl_version}", f"Platform: {platform.platform()}", ] return "\n".join(data) @@ -36,7 +39,7 @@ def dump_info(signal=None, frame=None, file=sys.stdout): # pragma: no cover try: import psutil - except: + except ModuleNotFoundError: print("(psutil not installed, skipping some debug info)") else: p = psutil.Process() diff --git a/mitmproxy/utils/emoji.py b/mitmproxy/utils/emoji.py index 2bb31432ea..e80cb73bfe 100644 --- a/mitmproxy/utils/emoji.py +++ b/mitmproxy/utils/emoji.py @@ -2,7 +2,6 @@ """ All of the emoji and characters that can be used as flow markers. """ - # auto-generated. run this file to refresh. emoji = { @@ -1853,11 +1852,13 @@ if __name__ == "__main__": # pragma: no cover - import requests import io import re import string from pathlib import Path + + import requests + from mitmproxy.tools.console.common import SYMBOL_MARK CHAR_MARKERS = list(string.ascii_letters) + list(string.digits) diff --git a/mitmproxy/utils/human.py b/mitmproxy/utils/human.py index ddb336340b..8d2d72f647 100644 --- a/mitmproxy/utils/human.py +++ b/mitmproxy/utils/human.py @@ -2,14 +2,13 @@ import functools import ipaddress import time -from typing import Optional SIZE_UNITS = { - "b": 1024 ** 0, - "k": 1024 ** 1, - "m": 1024 ** 2, - "g": 1024 ** 3, - "t": 1024 ** 4, + "b": 1024**0, + "k": 1024**1, + "m": 1024**2, + "g": 1024**3, + "t": 1024**4, } @@ -31,7 +30,7 @@ def pretty_size(size: int) -> str: @functools.lru_cache -def parse_size(s: Optional[str]) -> Optional[int]: +def parse_size(s: str | None) -> int | None: """ Parse a size with an optional k/m/... suffix. Invalid values raise a ValueError. For added convenience, passing `None` returns `None`. @@ -51,7 +50,7 @@ def parse_size(s: Optional[str]) -> Optional[int]: raise ValueError("Invalid size specification.") -def pretty_duration(secs: Optional[float]) -> str: +def pretty_duration(secs: float | None) -> str: formatters = [ (100, "{:.0f}s"), (10, "{:2.1f}s"), @@ -79,7 +78,7 @@ def format_timestamp_with_milli(s): @functools.lru_cache -def format_address(address: Optional[tuple]) -> str: +def format_address(address: tuple | None) -> str: """ This function accepts IPv4/IPv6 tuples and returns the formatted address string with port number diff --git a/mitmproxy/utils/magisk.py b/mitmproxy/utils/magisk.py index 286aee9269..815e513d91 100644 --- a/mitmproxy/utils/magisk.py +++ b/mitmproxy/utils/magisk.py @@ -1,13 +1,14 @@ -from zipfile import ZipFile import hashlib +import os +from zipfile import ZipFile + from cryptography import x509 from cryptography.hazmat.primitives import serialization -from mitmproxy import certs, ctx +from mitmproxy import certs +from mitmproxy import ctx from mitmproxy.options import CONF_BASENAME -import os - # The following 3 variables are for including in the magisk module as text file MODULE_PROP_TEXT = """id=mitmproxycert name=MITMProxy cert @@ -84,14 +85,14 @@ def get_ca_from_files() -> x509.Certificate: return certstore.default_ca._cert -def subject_hash_old(ca : x509.Certificate) -> str: +def subject_hash_old(ca: x509.Certificate) -> str: # Mimics the -subject_hash_old option of openssl used for android certificate names full_hash = hashlib.md5(ca.subject.public_bytes()).digest() - sho = (full_hash[0] | (full_hash[1] << 8) | (full_hash[2] << 16) | full_hash[3] << 24) + sho = full_hash[0] | (full_hash[1] << 8) | (full_hash[2] << 16) | full_hash[3] << 24 return hex(sho)[2:] -def write_magisk_module(path : str): +def write_magisk_module(path: str): # Makes a zip file that can be loaded by Magisk # Android certs are stored as DER files ca = get_ca_from_files() @@ -103,7 +104,9 @@ def write_magisk_module(path : str): zipp.writestr("config.sh", CONFIG_SH_TEXT) zipp.writestr("META-INF/com/google/android/updater-script", "#MAGISK") zipp.writestr("META-INF/com/google/android/update-binary", UPDATE_BINARY_TEXT) - zipp.writestr("common/file_contexts_image", "/magisk(/.*)? u:object_r:system_file:s0") + zipp.writestr( + "common/file_contexts_image", "/magisk(/.*)? u:object_r:system_file:s0" + ) zipp.writestr("common/post-fs-data.sh", "MODDIR=${0%/*}") zipp.writestr("common/service.sh", "MODDIR=${0%/*}") zipp.writestr("common/system.prop", "") diff --git a/mitmproxy/utils/signals.py b/mitmproxy/utils/signals.py index 423dddc334..e0a7f419ed 100644 --- a/mitmproxy/utils/signals.py +++ b/mitmproxy/utils/signals.py @@ -7,18 +7,19 @@ - supports type hints - supports async receivers. """ + from __future__ import annotations + import asyncio import inspect import weakref -from collections.abc import Callable, Awaitable -from typing import Any, Generic, TypeVar, cast - -try: - from typing import ParamSpec -except ImportError: # pragma: no cover - # Python 3.9 - from typing_extensions import ParamSpec +from collections.abc import Awaitable +from collections.abc import Callable +from typing import Any +from typing import cast +from typing import Generic +from typing import ParamSpec +from typing import TypeVar P = ParamSpec("P") R = TypeVar("R") @@ -37,7 +38,7 @@ def make_weak_ref(obj: Any) -> weakref.ReferenceType: # We're running into https://github.com/python/mypy/issues/6073 here, # which is why the base class is a mixin and not a generic superclass. class _SignalMixin: - def __init__(self): + def __init__(self) -> None: self.receivers: list[weakref.ref[Callable]] = [] def connect(self, receiver: Callable) -> None: @@ -85,11 +86,13 @@ def disconnect(self, receiver: Callable[P, Awaitable[None] | None]) -> None: super().disconnect(receiver) async def send(self, *args: P.args, **kwargs: P.kwargs) -> None: - await asyncio.gather(*[ - aws - for aws in super().notify(*args, **kwargs) - if aws is not None and inspect.isawaitable(aws) - ]) + await asyncio.gather( + *[ + aws + for aws in super().notify(*args, **kwargs) + if aws is not None and inspect.isawaitable(aws) + ] + ) # noinspection PyPep8Naming diff --git a/mitmproxy/utils/sliding_window.py b/mitmproxy/utils/sliding_window.py index dca71cbd6c..6c3853db88 100644 --- a/mitmproxy/utils/sliding_window.py +++ b/mitmproxy/utils/sliding_window.py @@ -1,12 +1,14 @@ import itertools -from typing import Iterable, Iterator, Optional, TypeVar +from collections.abc import Iterable +from collections.abc import Iterator +from typing import TypeVar T = TypeVar("T") def window( iterator: Iterable[T], behind: int = 0, ahead: int = 0 -) -> Iterator[tuple[Optional[T], ...]]: +) -> Iterator[tuple[T | None, ...]]: """ Sliding window for an iterator. @@ -20,9 +22,7 @@ def window( 2 3 None """ # TODO: move into utils - iters: list[Iterator[Optional[T]]] = list( - itertools.tee(iterator, behind + 1 + ahead) - ) + iters: list[Iterator[T | None]] = list(itertools.tee(iterator, behind + 1 + ahead)) for i in range(behind): iters[i] = itertools.chain((behind - i) * [None], iters[i]) for i in range(ahead): diff --git a/mitmproxy/utils/strutils.py b/mitmproxy/utils/strutils.py index 6f61ff54de..debda43d23 100644 --- a/mitmproxy/utils/strutils.py +++ b/mitmproxy/utils/strutils.py @@ -1,25 +1,21 @@ import codecs import io import re -from typing import Iterable, Union, overload - +from collections.abc import Iterable +from typing import overload # https://mypy.readthedocs.io/en/stable/more_types.html#function-overloading @overload -def always_bytes(str_or_bytes: None, *encode_args) -> None: - ... +def always_bytes(str_or_bytes: None, *encode_args) -> None: ... @overload -def always_bytes(str_or_bytes: Union[str, bytes], *encode_args) -> bytes: - ... +def always_bytes(str_or_bytes: str | bytes, *encode_args) -> bytes: ... -def always_bytes( - str_or_bytes: Union[None, str, bytes], *encode_args -) -> Union[None, bytes]: +def always_bytes(str_or_bytes: None | str | bytes, *encode_args) -> None | bytes: if str_or_bytes is None or isinstance(str_or_bytes, bytes): return str_or_bytes elif isinstance(str_or_bytes, str): @@ -31,16 +27,14 @@ def always_bytes( @overload -def always_str(str_or_bytes: None, *encode_args) -> None: - ... +def always_str(str_or_bytes: None, *encode_args) -> None: ... @overload -def always_str(str_or_bytes: Union[str, bytes], *encode_args) -> str: - ... +def always_str(str_or_bytes: str | bytes, *encode_args) -> str: ... -def always_str(str_or_bytes: Union[None, str, bytes], *decode_args) -> Union[None, str]: +def always_str(str_or_bytes: None | str | bytes, *decode_args) -> None | str: """ Returns, str_or_bytes unmodified, if @@ -60,7 +54,8 @@ def always_str(str_or_bytes: Union[None, str, bytes], *decode_args) -> Union[Non # (http://unicode.org/charts/PDF/U2400.pdf), but that turned out to render badly # with monospace fonts. We are back to "." therefore. _control_char_trans = { - x: ord(".") for x in range(32) # x + 0x2400 for unicode control group pictures + x: ord(".") + for x in range(32) # x + 0x2400 for unicode control group pictures } _control_char_trans[127] = ord(".") # 0x2421 _control_char_trans_newline = _control_char_trans.copy() @@ -236,7 +231,7 @@ def escape_special_areas( """ buf = io.StringIO() parts = split_special_areas(data, area_delimiter) - rex = re.compile(fr"[{control_characters}]") + rex = re.compile(rf"[{control_characters}]") for i, x in enumerate(parts): if i % 2: x = rex.sub(_move_to_private_code_plane, x) diff --git a/mitmproxy/utils/typecheck.py b/mitmproxy/utils/typecheck.py index 6279cfaeb4..a898a5b387 100644 --- a/mitmproxy/utils/typecheck.py +++ b/mitmproxy/utils/typecheck.py @@ -17,7 +17,7 @@ def check_option_type(name: str, value: typing.Any, typeinfo: Type) -> None: TypeError otherwise. This function supports only those types required for options. """ - e = TypeError("Expected {} for {}, but got {}.".format(typeinfo, name, type(value))) + e = TypeError(f"Expected {typeinfo} for {name}, but got {type(value)}.") origin = typing.get_origin(typeinfo) diff --git a/mitmproxy/utils/vt_codes.py b/mitmproxy/utils/vt_codes.py index e33a8f2492..9bc031bb7b 100644 --- a/mitmproxy/utils/vt_codes.py +++ b/mitmproxy/utils/vt_codes.py @@ -1,13 +1,18 @@ """ This module provides a method to detect if a given file object supports virtual terminal escape codes. """ + import os import sys from typing import IO if os.name == "nt": - from ctypes import byref, windll # type: ignore - from ctypes.wintypes import BOOL, DWORD, HANDLE, LPDWORD + from ctypes import byref # type: ignore + from ctypes import windll # type: ignore + from ctypes.wintypes import BOOL + from ctypes.wintypes import DWORD + from ctypes.wintypes import HANDLE + from ctypes.wintypes import LPDWORD ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 STD_OUTPUT_HANDLE = -11 @@ -49,7 +54,6 @@ def ensure_supported(f: IO[str]) -> bool: ) return ok - else: def ensure_supported(f: IO[str]) -> bool: diff --git a/mitmproxy/version.py b/mitmproxy/version.py index d1405ffe1a..5f6f3f07a2 100644 --- a/mitmproxy/version.py +++ b/mitmproxy/version.py @@ -2,12 +2,12 @@ import subprocess import sys -VERSION = "10.0.0.dev" +VERSION = "12.0.0.dev" MITMPROXY = "mitmproxy " + VERSION # Serialization format version. This is displayed nowhere, it just needs to be incremented by one # for each change in the file format. -FLOW_FORMAT_VERSION = 18 +FLOW_FORMAT_VERSION = 21 def get_dev_version() -> str: @@ -18,7 +18,7 @@ def get_dev_version() -> str: mitmproxy_version = VERSION here = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) - try: + try: # pragma: no cover # Check that we're in the mitmproxy repository: https://github.com/mitmproxy/mitmproxy/issues/3987 # cb0e3287090786fad566feb67ac07b8ef361b2c3 is the first mitmproxy commit. subprocess.run( diff --git a/mitmproxy/websocket.py b/mitmproxy/websocket.py index 7cac0a712c..5558916dca 100644 --- a/mitmproxy/websocket.py +++ b/mitmproxy/websocket.py @@ -5,15 +5,16 @@ This module only defines the classes for individual `WebSocketMessage`s and the `WebSocketData` container. """ + import time import warnings -from typing import Union -from typing import Optional +from dataclasses import dataclass +from dataclasses import field -from mitmproxy import stateobject -from mitmproxy.coretypes import serializable from wsproto.frame_protocol import Opcode +from mitmproxy.coretypes import serializable + WebSocketMessageState = tuple[int, bool, bytes, float, bool, bool] @@ -52,10 +53,10 @@ class WebSocketMessage(serializable.Serializable): def __init__( self, - type: Union[int, Opcode], + type: int | Opcode, from_client: bool, content: bytes, - timestamp: Optional[float] = None, + timestamp: float | None = None, dropped: bool = False, injected: bool = False, ) -> None: @@ -91,6 +92,12 @@ def set_state(self, state: WebSocketMessageState) -> None: ) = state self.type = Opcode(typ) + def _format_ws_message(self) -> bytes: + if self.from_client: + return b"[OUTGOING] " + self.content + else: + return b"[INCOMING] " + self.content + def __repr__(self): if self.type == Opcode.TEXT: return repr(self.content.decode(errors="replace")) @@ -144,45 +151,32 @@ def text(self, value: str) -> None: self.content = value.encode() -class WebSocketData(stateobject.StateObject): +@dataclass +class WebSocketData(serializable.SerializableDataclass): """ A data container for everything related to a single WebSocket connection. This is typically accessed as `mitmproxy.http.HTTPFlow.websocket`. """ - messages: list[WebSocketMessage] + messages: list[WebSocketMessage] = field(default_factory=list) """All `WebSocketMessage`s transferred over this connection.""" - closed_by_client: Optional[bool] = None + closed_by_client: bool | None = None """ `True` if the client closed the connection, `False` if the server closed the connection, `None` if the connection is active. """ - close_code: Optional[int] = None + close_code: int | None = None """[Close Code](https://tools.ietf.org/html/rfc6455#section-7.1.5)""" - close_reason: Optional[str] = None + close_reason: str | None = None """[Close Reason](https://tools.ietf.org/html/rfc6455#section-7.1.6)""" - timestamp_end: Optional[float] = None + timestamp_end: float | None = None """*Timestamp:* WebSocket connection closed.""" - _stateobject_attributes = dict( - messages=list[WebSocketMessage], - closed_by_client=bool, - close_code=int, - close_reason=str, - timestamp_end=float, - ) - - def __init__(self): - self.messages = [] - def __repr__(self): return f"" - @classmethod - def from_state(cls, state): - d = WebSocketData() - d.set_state(state) - return d + def _get_formatted_messages(self) -> bytes: + return b"\n".join(m._format_ws_message() for m in self.messages) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..1eb18c63e6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,328 @@ +[project] +name = "mitmproxy" +description = "An interactive, SSL/TLS-capable intercepting proxy for HTTP/1, HTTP/2, and WebSockets." +readme = "README.md" +requires-python = ">=3.10" +license = {file="LICENSE"} +authors = [{name = "Aldo Cortesi", email = "aldo@corte.si"}] +maintainers = [{name = "Maximilian Hils", email = "mitmproxy@maximilianhils.com"}] +dynamic = ["version"] + +classifiers = [ + "License :: OSI Approved :: MIT License", + "Development Status :: 5 - Production/Stable", + "Environment :: Console :: Curses", + "Operating System :: MacOS", + "Operating System :: POSIX", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Security", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: Proxy Servers", + "Topic :: System :: Networking :: Monitoring", + "Topic :: Software Development :: Testing", + "Typing :: Typed", +] + +# https://packaging.python.org/en/latest/discussions/install-requires-vs-requirements/#install-requires +# It is not considered best practice to use install_requires to pin dependencies to specific versions. +dependencies = [ + "aioquic>=1.1.0,<=1.2.0", + "asgiref>=3.2.10,<=3.8.1", + "Brotli>=1.0,<=1.1.0", + "certifi>=2019.9.11", # no upper bound here to get latest CA bundle + "cryptography>=42.0,<43.1", # relaxed upper bound here to get security fixes + "flask>=3.0,<=3.1.0", + "h11>=0.11,<=0.14.0", + "h2>=4.1,<=4.1.0", + "hyperframe>=6.0,<=6.0.1", + "kaitaistruct>=0.10,<=0.10", + "ldap3>=2.8,<=2.9.1", + "mitmproxy_rs>=0.10.7,<0.11", # relaxed upper bound here: we control this + "msgpack>=1.0.0,<=1.1.0", + "passlib>=1.6.5,<=1.7.4", + "pydivert>=2.0.3,<=2.1.0; sys_platform == 'win32'", + "pyOpenSSL>=22.1,<=24.2.1", + "pyparsing>=2.4.2,<=3.2.0", + "pyperclip<=1.9.0,>=1.9.0", + "ruamel.yaml>=0.16,<=0.18.6", + "sortedcontainers>=2.3,<=2.4.0", + "tornado<=6.4.1,>=6.4.1", + "typing-extensions>=4.3,<=4.11.0; python_version<'3.11'", + "urwid>=2.6.14,<=2.6.16", + "wsproto>=1.0,<=1.2.0", + "publicsuffix2>=2.20190812,<=2.20191221", + "zstandard>=0.15,<=0.23.0", + "falcon>=4.0.2", + "marshmallow>=3.0.0", + "falcon-apispec@git+https://github.com/browserup/falcon-apispec#egg=falcon-apispec", + "python-dateutil>=2.8.1", + "glom>=20.11.0", + "jsonschema>=3.2.0", + "jsonpath_ng>=1.5.3", + "typing-extensions>=4.3,<4.5; python_version<'3.12'", +] + +[project.optional-dependencies] +dev = [ + "click>=7.0,<=8.1.7", + "hypothesis>=6.104.2,<=6.119.3", + "pdoc>=14.5.1,<=15.0.0", + "pyinstaller==6.11.1", + "pyinstaller-hooks-contrib==2024.10", + "pytest-asyncio>=0.23.6,<=0.24.0", + "pytest-cov>=5.0.0,<=6.0.0", + "pytest-timeout>=2.3.1,<=2.3.1", + "pytest-xdist>=3.5.0,<=3.6.1", + "pytest>=8.2.2,<=8.3.3", + "requests>=2.9.1,<=2.32.3", + "tox>=4.15.1,<=4.23.2", + "wheel>=0.36.2,<=0.45.0", + "build>=0.10.0,<=1.2.2.post1", + "mypy>=1.10.1,<=1.13.0", + "ruff>=0.5.0,<=0.7.4", + "types-certifi>=2021.10.8.3,<=2021.10.8.3", + "types-Flask>=1.1.6,<=1.1.6", + "types-Werkzeug>=1.0.9,<=1.0.9", + "types-requests>=2.32.0.20240622,<=2.32.0.20241016", + "types-cryptography>=3.3.23.2,<=3.3.23.2", + "types-pyOpenSSL>=23.3.0.0,<=24.1.0.20240722", +] + +[project.urls] +Homepage = "https://mitmproxy.org" +Source = "https://github.com/mitmproxy/mitmproxy/" +Documentation = "https://docs.mitmproxy.org/stable/" +Issues = "https://github.com/mitmproxy/mitmproxy/issues" + +[project.scripts] +mitmproxy = "mitmproxy.tools.main:mitmproxy" +mitmdump = "mitmproxy.tools.main:mitmdump" +mitmweb = "mitmproxy.tools.main:mitmweb" +browserup-proxy = "mitmproxy.tools.main:browserupproxy" + +[project.entry-points.pyinstaller40] +hook-dirs = "mitmproxy.utils.pyinstaller:hook_dirs" + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.dynamic] +version = {attr = "mitmproxy.version.VERSION"} + +[tool.setuptools.packages.find] +include = ["mitmproxy*"] + +[tool.coverage.run] +branch = false +omit = [ + "*contrib*", + "*tnetstring*", + "*platform*", + "*main.py", +] + +[tool.coverage.report] +show_missing = true +exclude_lines = [ + "pragma: no cover", + "raise NotImplementedError", + "raise AssertionError", + "if typing.TYPE_CHECKING:", + "if TYPE_CHECKING:", + "@overload", + "@abstractmethod", + "assert_never", + "\\.\\.\\.", +] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +testpaths = "test" +addopts = "--capture=no --color=yes" +filterwarnings = [ + "ignore::DeprecationWarning:tornado.*:", + "ignore:datetime.datetime.utcnow:DeprecationWarning:aioquic.*:", + "error::RuntimeWarning", + "error::pytest.PytestUnraisableExceptionWarning", + # The following warning should only appear on Python 3.11 and below where eager_task_factory is not present + "default:coroutine 'ConnectionHandler.hook_task' was never awaited:RuntimeWarning", +] + +[tool.pytest.individual_coverage] +exclude = [ + "mitmproxy/addons/__init__.py", + "mitmproxy/addons/onboarding.py", + "mitmproxy/addons/onboardingapp/__init__.py", + "mitmproxy/contentviews/__init__.py", + "mitmproxy/contentviews/base.py", + "mitmproxy/contentviews/grpc.py", + "mitmproxy/contrib/*", + "mitmproxy/ctx.py", + "mitmproxy/exceptions.py", + "mitmproxy/flow.py", + "mitmproxy/io/io.py", + "mitmproxy/io/tnetstring.py", + "mitmproxy/log.py", + "mitmproxy/master.py", + "mitmproxy/net/check.py", + "mitmproxy/net/http/cookies.py", + "mitmproxy/net/http/multipart.py", + "mitmproxy/net/tls.py", + "mitmproxy/platform/__init__.py", + "mitmproxy/platform/linux.py", + "mitmproxy/platform/openbsd.py", + "mitmproxy/platform/osx.py", + "mitmproxy/platform/pf.py", + "mitmproxy/platform/windows.py", + "mitmproxy/proxy/__init__.py", + "mitmproxy/proxy/layers/http/__init__.py", + "mitmproxy/proxy/layers/http/_base.py", + "mitmproxy/proxy/layers/http/_events.py", + "mitmproxy/proxy/layers/http/_hooks.py", + "mitmproxy/proxy/layers/http/_http1.py", + "mitmproxy/proxy/layers/http/_http2.py", + "mitmproxy/proxy/layers/http/_http3.py", + "mitmproxy/proxy/layers/http/_http_h2.py", + "mitmproxy/proxy/layers/http/_http_h3.py", + "mitmproxy/proxy/layers/http/_upstream_proxy.py", + "mitmproxy/proxy/layers/tls.py", + "mitmproxy/proxy/server.py", + "mitmproxy/test/taddons.py", + "mitmproxy/test/tflow.py", + "mitmproxy/test/tutils.py", + "mitmproxy/tools/console/commander/commander.py", + "mitmproxy/tools/console/commandexecutor.py", + "mitmproxy/tools/console/commands.py", + "mitmproxy/tools/console/common.py", + "mitmproxy/tools/console/consoleaddons.py", + "mitmproxy/tools/console/eventlog.py", + "mitmproxy/tools/console/flowdetailview.py", + "mitmproxy/tools/console/flowlist.py", + "mitmproxy/tools/console/flowview.py", + "mitmproxy/tools/console/grideditor/base.py", + "mitmproxy/tools/console/grideditor/col_bytes.py", + "mitmproxy/tools/console/grideditor/col_subgrid.py", + "mitmproxy/tools/console/grideditor/col_text.py", + "mitmproxy/tools/console/grideditor/col_viewany.py", + "mitmproxy/tools/console/grideditor/editors.py", + "mitmproxy/tools/console/help.py", + "mitmproxy/tools/console/keybindings.py", + "mitmproxy/tools/console/keymap.py", + "mitmproxy/tools/console/layoutwidget.py", + "mitmproxy/tools/console/master.py", + "mitmproxy/tools/console/options.py", + "mitmproxy/tools/console/overlay.py", + "mitmproxy/tools/console/quickhelp.py", + "mitmproxy/tools/console/searchable.py", + "mitmproxy/tools/console/signals.py", + "mitmproxy/tools/console/statusbar.py", + "mitmproxy/tools/console/tabs.py", + "mitmproxy/tools/console/window.py", + "mitmproxy/tools/main.py", + "mitmproxy/tools/web/app.py", + "mitmproxy/tools/web/master.py", + "mitmproxy/tools/web/webaddons.py", + "mitmproxy/utils/bits.py", + "mitmproxy/utils/pyinstaller/*", + "mitmproxy/utils/vt_codes.py", +] + +[tool.mypy] +check_untyped_defs = true +ignore_missing_imports = true +files = [ + "mitmproxy", + "examples/addons", + "release/*.py", +] +exclude = [ + "^docs/", + "^release/build/", + "^examples/contrib/", +] + +[[tool.mypy.overrides]] +module = "mitmproxy.contrib.*" +ignore_errors = true + +[[tool.mypy.overrides]] +module = "tornado.*" +ignore_errors = true + +[[tool.mypy.overrides]] +module = "test.*" +ignore_errors = true + +[tool.ruff] +extend-exclude = ["mitmproxy/contrib/"] + +[tool.ruff.lint] +select = ["E", "F", "I"] +ignore = ["F541", "E501"] + + +[tool.ruff.lint.isort] +# these rules are a bit weird, but they mimic our existing reorder_python_imports style. +# if we break compatibility here, consider removing all customization + enforce absolute imports. +force-single-line = true +order-by-type = false +section-order = ["future", "standard-library", "third-party", "local-folder","first-party"] +no-lines-before = ["first-party"] +known-first-party = ["test", "mitmproxy", "mitmproxy_rs"] + +[tool.tox] +legacy_tox_ini = """ +[tox] +envlist = py, lint, mypy +skipsdist = True +toxworkdir={env:TOX_WORK_DIR:.tox} + +[testenv] +deps = + -e .[dev] +setenv = HOME = {envtmpdir} +commands = + mitmdump --version + pytest --timeout 60 -vv \ + --cov-report xml \ + --continue-on-collection-errors \ + --cov=mitmproxy --cov=release \ + {posargs} + +[testenv:old-dependencies] +uv_resolution = lowest-direct + +[testenv:lint] +commands = + ruff check . + +[testenv:filename_matching] +deps = +commands = + python ./test/filename_matching.py + +[testenv:mypy] +commands = + mypy {posargs} + +[testenv:individual_coverage] +commands = + python ./test/individual_coverage.py {posargs} + +[testenv:wheeltest] +recreate = True +deps = +commands = + pip install {posargs} + mitmproxy --version + mitmdump --version + mitmweb --version +""" diff --git a/release/README.md b/release/README.md index 0a199f16ff..b2c9402a3c 100644 --- a/release/README.md +++ b/release/README.md @@ -1,61 +1,62 @@ # Release Checklist -These steps assume you are on the correct branch and have a git remote called `origin` that points to the `browserup/mitmproxy` repo. If necessary, create a major version branch starting off the release tag (e.g. `git checkout -b v4.x v4.0.0`) first. - -- Update CHANGELOG. -- Verify that the compiled mitmweb assets are up-to-date. -- Verify that all CI tests pass. -- Verify that `mitmproxy/version.py` is correct. Remove `.dev` suffix if it exists. -- Tag the release and push to GitHub. - - `git tag v4.0.0` - - `git push origin v4.0.0` -- Wait for tag CI to complete. +1. Check if `mitmproxy-rs` needs a new release. +2. Make sure that `CHANGELOG.md` is up-to-date with all entries in the "Unreleased" section. +3. Invoke the [release workflow](https://github.com/browserup/mitmproxy/actions/workflows/release.yml) from the GitHub UI. +4. The spawned workflow runs will require manual confirmation on GitHub which you need to approve twice: + https://github.com/browserup/mitmproxy/actions +5. Once everything has been deployed, update the website. +6. Verify that the front-page download links for all platforms are working. ### GitHub Releases -- Create release notice on GitHub - [here](https://github.com/browserup/mitmproxy/releases/new) if not already - auto-created by the tag. -- We DO NOT upload release artifacts to GitHub anymore. Simply add the - following snippet to the notice: - `You can find the latest release packages at https://mitmproxy.org/downloads/.` +- CI will automatically create a GitHub release: + https://github.com/browserup/mitmproxy/releases ### PyPi -- The created wheel is uploaded to PyPi automatically. -- Please verify that https://pypi.python.org/pypi/mitmproxy has the latest version. - -### Homebrew - -- The Homebrew maintainers are typically very fast and detect our new relese - within a day. -- If you feel the need, you can run this from a macOS machine: - `brew bump-formula-pr --url https://github.com/browserup/mitmproxy/archive/v.tar.gz mitmproxy` +- CI will automatically push a wheel to GitHub: + https://pypi.python.org/pypi/mitmproxy ### Docker -- The docker image is built by our CI workers and pushed to Docker Hub automatically. -- Please verify that https://hub.docker.com/r/browserup/mitmproxy/tags/ has the latest version. -- Please verify that the latest tag points to the most recent image (same digest / hash). +- CI will automatically push images to Docker Hub: + https://hub.docker.com/r/browserup/mitmproxy/tags/ ### Docs -- `./build.py`. If everything looks alright, continue with -- `./upload-stable.sh`, -- `DOCS_ARCHIVE=true ./build.py`, and -- `./upload-archive.sh v4`. Doing this now already saves you from switching back to an old state on the next release. +- CI will automatically update the stable docs and create an archive version: + `https://docs.mitmproxy.org/archive/vMAJOR/` + +### Download Server + +- CI will automatically push binaries to our download S3 bucket: + https://mitmproxy.org/downloads/ + +### Microsoft Store + +- CI will automatically update the Microsoft Store version: + https://apps.microsoft.com/store/detail/mitmproxy/9NWNDLQMNZD7 +- There is a review process, binaries may take a day to show up. + +### Homebrew + +- The Homebrew maintainers are typically very fast and detect our new relese + within a day. +- If you feel the need, you can run this from a macOS machine: + `brew bump-cask-pr mitmproxy` ### Website - The website does not need to be updated for patch releases. New versions are automatically picked up once they are on the download server. - Update version here: - https://github.com/mitmproxy/www/blob/main/src/config.toml + https://github.com/mitmproxy/www/blob/main/src/config.toml - Update docs menu here: - https://github.com/mitmproxy/www/blob/main/src/themes/mitmproxy/layouts/partials/header.html + https://github.com/mitmproxy/www/blob/main/src/themes/mitmproxy/layouts/partials/header.html - Run `./build && ./upload-test`. - If everything looks alright at https://www-test.mitmproxy.org, run `./upload-prod`. ### Prepare for next release - Last but not least, bump the major version on main in - [https://github.com/browserup/mitmproxy/blob/main/mitmproxy/version.py](mitmproxy/version.py) and add a `.dev` suffix. + [https://github.com/browserup/mitmproxy/blob/main/mitmproxy/version.py](mitmproxy/version.py) and add a `.dev` suffix. diff --git a/release/build-and-deploy-docker.py b/release/build-and-deploy-docker.py deleted file mode 100644 index 69fe18c622..0000000000 --- a/release/build-and-deploy-docker.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 -""" -Building and deploying docker images is a bit of a special snowflake as we don't get a file we can upload/download -as an artifact. So we need to do everything in one job. -""" -import os -import shutil -import subprocess -from pathlib import Path -from typing import Optional - -# Security: No third-party dependencies here! - -root = Path(__file__).absolute().parent.parent - -ref = os.environ["GITHUB_REF"] -branch: Optional[str] = None -tag: Optional[str] = None -if ref.startswith("refs/heads/"): - branch = ref.replace("refs/heads/", "") -elif ref.startswith("refs/tags/"): - tag = ref.replace("refs/tags/", "") -else: - raise AssertionError - -(whl,) = root.glob("release/dist/mitmproxy-*-py3-none-any.whl") -docker_build_dir = root / "release/docker" -shutil.copy(whl, docker_build_dir / whl.name) - -# Build for this platform and test if it runs. -subprocess.check_call( - [ - "docker", - "buildx", - "build", - "--tag", - "localtesting", - "--load", - "--build-arg", - f"MITMPROXY_WHEEL={whl.name}", - ".", - ], - cwd=docker_build_dir, -) -r = subprocess.run( - [ - "docker", - "run", - "--rm", - "localtesting", - "mitmdump", - "--version", - ], - check=True, - capture_output=True, -) -print(r.stdout.decode()) -assert "Mitmproxy: " in r.stdout.decode() - -# Now we can deploy. -subprocess.check_call( - [ - "docker", - "login", - "-u", - os.environ["DOCKER_USERNAME"], - "-p", - os.environ["DOCKER_PASSWORD"], - ] -) - - -def _buildx(docker_tag): - subprocess.check_call( - [ - "docker", - "buildx", - "build", - "--tag", - docker_tag, - "--push", - "--platform", - "linux/amd64,linux/arm64", - "--build-arg", - f"MITMPROXY_WHEEL={whl.name}", - ".", - ], - cwd=docker_build_dir, - ) - - -if branch == "main": - _buildx("browserup/mitmproxy:dev") -elif branch == "citest": - _buildx("browserup/mitmproxy:citest") -elif tag: - _buildx(f"browserup/mitmproxy:{tag}") - _buildx("browserup/mitmproxy:latest") -else: - raise AssertionError diff --git a/release/build.py b/release/build.py old mode 100644 new mode 100755 index 2f33b38430..9c2947d224 --- a/release/build.py +++ b/release/build.py @@ -9,10 +9,10 @@ import subprocess import tarfile import urllib.request +import warnings import zipfile from datetime import datetime from pathlib import Path -from typing import Literal import click import cryptography.fernet @@ -46,10 +46,9 @@ def wheel(): subprocess.check_call( [ "python", - "setup.py", - "-q", - "bdist_wheel", - "--dist-dir", + "-m", + "build", + "--outdir", DIST_DIR, ] ) @@ -84,19 +83,39 @@ def archive(path: Path) -> tarfile.TarFile | ZipFile2: def version() -> str: - return os.environ.get("GITHUB_REF_NAME", "").replace("/", "-") or os.environ.get("BUILD_VERSION", "dev") - - -def operating_system() -> Literal["windows", "linux", "macos", "unknown"]: - pf = platform.system() - if pf == "Windows": - return "windows" - elif pf == "Linux": - return "linux" - elif pf == "Darwin": - return "macos" + if ref := os.environ.get("GITHUB_REF", ""): + if ref.startswith("refs/tags/") and not ref.startswith("refs/tags/v"): + raise AssertionError(f"Unexpected tag: {ref}") + return ( + ref.removeprefix("refs/heads/") + .removeprefix("refs/pull/") + .removeprefix("refs/tags/v") + .replace("/", "-") + ) else: - return "unknown" + return os.environ.get("BUILD_VERSION", "dev") + + +def operating_system() -> str: + match platform.system(): + case "Windows": + system = "windows" + case "Linux": + system = "linux" + case "Darwin": + system = "macos" + case other: + warnings.warn("Unexpected system.") + system = other + match platform.machine(): + case "AMD64" | "x86_64": + machine = "x86_64" + case "arm64": + machine = "arm64" + case other: + warnings.warn("Unexpected platform.") + machine = other + return f"{system}-{machine}" def _pyinstaller(specfile: str) -> None: @@ -108,7 +127,7 @@ def _pyinstaller(specfile: str) -> None: "--workpath", TEMP_DIR / "pyinstaller/temp", "--distpath", - TEMP_DIR / "pyinstaller/dist", + TEMP_DIR / "pyinstaller/out", specfile, ], cwd=here / "specs", @@ -117,32 +136,115 @@ def _pyinstaller(specfile: str) -> None: @cli.command() def standalone_binaries(): - """All platforms: Build the standalone binaries generated with PyInstaller""" + """Windows and Linux: Build the standalone binaries generated with PyInstaller""" with archive(DIST_DIR / f"mitmproxy-{version()}-{operating_system()}") as f: _pyinstaller("standalone.spec") + _test_binaries(TEMP_DIR / "pyinstaller/out") + for tool in ["mitmproxy", "mitmdump", "mitmweb"]: - executable = TEMP_DIR / "pyinstaller/dist" / tool + executable = TEMP_DIR / "pyinstaller/out" / tool if platform.system() == "Windows": executable = executable.with_suffix(".exe") - # Test if it works at all O:-) - print(f"> {executable} --version") - subprocess.check_call([executable, "--version"]) - f.add(str(executable), str(executable.name)) - print(f"Packed {f.name}.") + print(f"Packed {f.name!r}.") + + +@cli.command() +@click.option("--keychain") +@click.option("--team-id") +@click.option("--apple-id") +@click.option("--password") +def macos_app( + keychain: str | None, + team_id: str | None, + apple_id: str | None, + password: str | None, +) -> None: + """ + macOS: Build into mitmproxy.app. + + If you do not specify options, notarization is skipped. + """ + + _pyinstaller("onedir.spec") + _test_binaries(TEMP_DIR / "pyinstaller/out/mitmproxy.app/Contents/MacOS") + + if keychain: + assert isinstance(team_id, str) + assert isinstance(apple_id, str) + assert isinstance(password, str) + # Notarize the app bundle. + subprocess.check_call( + [ + "xcrun", + "notarytool", + "store-credentials", + "AC_PASSWORD", + *(["--keychain", keychain]), + *(["--team-id", team_id]), + *(["--apple-id", apple_id]), + *(["--password", password]), + ] + ) + subprocess.check_call( + [ + "ditto", + "-c", + "-k", + "--keepParent", + TEMP_DIR / "pyinstaller/out/mitmproxy.app", + TEMP_DIR / "notarize.zip", + ] + ) + subprocess.check_call( + [ + "xcrun", + "notarytool", + "submit", + TEMP_DIR / "notarize.zip", + *(["--keychain", keychain]), + *(["--keychain-profile", "AC_PASSWORD"]), + "--wait", + ] + ) + # 2023: it's not possible to staple to unix executables. + # subprocess.check_call([ + # "xcrun", + # "stapler", + # "staple", + # TEMP_DIR / "pyinstaller/out/mitmproxy.app", + # ]) + else: + warnings.warn("Notarization skipped.") + + with archive(DIST_DIR / f"mitmproxy-{version()}-{operating_system()}") as f: + f.add(str(TEMP_DIR / "pyinstaller/out/mitmproxy.app"), "mitmproxy.app") + print(f"Packed {f.name!r}.") def _ensure_pyinstaller_onedir(): - if not (TEMP_DIR / "pyinstaller/dist/onedir").exists(): - _pyinstaller("windows-dir.spec") + if not (TEMP_DIR / "pyinstaller/out/onedir").exists(): + _pyinstaller("onedir.spec") + _test_binaries(TEMP_DIR / "pyinstaller/out/onedir") + +def _test_binaries(binary_directory: Path) -> None: for tool in ["mitmproxy", "mitmdump", "mitmweb"]: + executable = binary_directory / tool + if platform.system() == "Windows": + executable = executable.with_suffix(".exe") + print(f"> {tool} --version") - executable = (TEMP_DIR / "pyinstaller/dist/onedir" / tool).with_suffix(".exe") subprocess.check_call([executable, "--version"]) + if tool == "mitmproxy": + continue # requires a TTY, which we don't have here. + + print(f"> {tool} -s selftest.py") + subprocess.check_call([executable, "-s", here / "selftest.py"]) + @cli.command() def msix_installer(): @@ -150,7 +252,7 @@ def msix_installer(): _ensure_pyinstaller_onedir() shutil.copytree( - TEMP_DIR / "pyinstaller/dist/onedir", + TEMP_DIR / "pyinstaller/out/onedir", TEMP_DIR / "msix", dirs_exist_ok=True, ) @@ -159,7 +261,13 @@ def msix_installer(): manifest = TEMP_DIR / "msix/AppxManifest.xml" app_version = version() if not re.match(r"\d+\.\d+\.\d+", app_version): - app_version = datetime.now().strftime("%y%m.%d.%H%M").replace(".0", ".").replace(".0", ".").replace(".0", ".") + app_version = ( + datetime.now() + .strftime("%y%m.%d.%H%M") + .replace(".0", ".") + .replace(".0", ".") + .replace(".0", ".") + ) manifest.write_text(manifest.read_text().replace("1.2.3", app_version)) makeappx_exe = ( @@ -184,26 +292,31 @@ def installbuilder_installer(): """Windows: Build the InstallBuilder installer.""" _ensure_pyinstaller_onedir() - IB_VERSION = "22.10.0" - IB_SETUP_SHA256 = "49cbfc3ee8de02426abc0c1b92839934bdb0bf0ea12d88388dde9e4102fc429f" + IB_VERSION = "23.4.0" + IB_SETUP_SHA256 = "e4ff212ed962f9e0030d918b8a6e4d6dd8a9adc8bf8bc1833459351ee649eff3" IB_DIR = here / "installbuilder" IB_SETUP = IB_DIR / "setup" / f"{IB_VERSION}-installer.exe" IB_CLI = Path( - rf"C:\Program Files\VMware InstallBuilder Enterprise {IB_VERSION}\bin\builder-cli.exe" + rf"C:\Program Files\InstallBuilder Enterprise {IB_VERSION}\bin\builder-cli.exe" ) IB_LICENSE = IB_DIR / "license.xml" if not IB_LICENSE.exists(): print("Decrypt InstallBuilder license...") f = cryptography.fernet.Fernet(os.environ["CI_BUILD_KEY"].encode()) - with open(IB_LICENSE.with_suffix(".xml.enc"), "rb") as infile, open( - IB_LICENSE, "wb" - ) as outfile: + with ( + open(IB_LICENSE.with_suffix(".xml.enc"), "rb") as infile, + open(IB_LICENSE, "wb") as outfile, + ): outfile.write(f.decrypt(infile.read())) if not IB_CLI.exists(): if not IB_SETUP.exists(): - print("Downloading InstallBuilder...") + url = ( + f"https://github.com/mitmproxy/installbuilder-mirror/releases/download/" + f"{IB_VERSION}/installbuilder-enterprise-{IB_VERSION}-windows-x64-installer.exe" + ) + print(f"Downloading InstallBuilder from {url}...") def report(block, blocksize, total): done = block * blocksize @@ -212,7 +325,7 @@ def report(block, blocksize, total): tmp = IB_SETUP.with_suffix(".tmp") urllib.request.urlretrieve( - f"https://clients.bitrock.com/installbuilder/installbuilder-enterprise-{IB_VERSION}-windows-x64-installer.exe", + url, tmp, reporthook=report, ) @@ -226,7 +339,9 @@ def report(block, blocksize, total): break ib_setup_hash.update(data) if ib_setup_hash.hexdigest() != IB_SETUP_SHA256: # pragma: no cover - raise RuntimeError(f"InstallBuilder hashes don't match: {ib_setup_hash.hexdigest()}") + raise RuntimeError( + f"InstallBuilder hashes don't match: {ib_setup_hash.hexdigest()}" + ) print("Install InstallBuilder...") subprocess.run( @@ -252,15 +367,16 @@ def report(block, blocksize, total): installer = DIST_DIR / f"mitmproxy-{version()}-windows-x64-installer.exe" assert installer.exists() + # unify filenames + installer = installer.rename( + installer.with_name(installer.name.replace("x64", "x86_64")) + ) + print("Run installer...") subprocess.run( [installer, "--mode", "unattended", "--unattendedmodeui", "none"], check=True ) - MITMPROXY_INSTALL_DIR = Path(rf"C:\Program Files\mitmproxy\bin") - for tool in ["mitmproxy", "mitmdump", "mitmweb"]: - executable = (MITMPROXY_INSTALL_DIR / tool).with_suffix(".exe") - print(f"> {executable} --version") - subprocess.check_call([executable, "--version"]) + _test_binaries(Path(r"C:\Program Files\mitmproxy\bin")) if __name__ == "__main__": diff --git a/release/deploy-microsoft-store.py b/release/deploy-microsoft-store.py index ca99e9f002..4df2d6f547 100755 --- a/release/deploy-microsoft-store.py +++ b/release/deploy-microsoft-store.py @@ -9,6 +9,7 @@ - https://docs.microsoft.com/en-us/windows/uwp/monetize/python-code-examples-for-the-windows-store-submission-api - https://docs.microsoft.com/en-us/windows/uwp/monetize/python-code-examples-for-submissions-game-options-and-trailers """ + import http.client import json import os diff --git a/release/deploy.py b/release/deploy.py index 9363109dd5..f9dee28c82 100755 --- a/release/deploy.py +++ b/release/deploy.py @@ -1,58 +1,107 @@ #!/usr/bin/env python3 import os -import re import subprocess from pathlib import Path -from typing import Optional + # Security: No third-party dependencies here! +root = Path(__file__).absolute().parent.parent + if __name__ == "__main__": ref = os.environ["GITHUB_REF"] - branch: Optional[str] = None - tag: Optional[str] = None + branch: str | None = None + tag: str | None = None if ref.startswith("refs/heads/"): branch = ref.replace("refs/heads/", "") elif ref.startswith("refs/tags/"): - tag = ref.replace("refs/tags/", "") + if not ref.startswith("refs/tags/v"): + raise AssertionError(f"Unexpected tag: {ref}") + tag = ref.replace("refs/tags/v", "") else: raise AssertionError # Upload binaries (be it release or snapshot) if tag: - # remove "v" prefix from version tags. - upload_dir = re.sub(r"^v([\d.]+)$", r"\1", tag) + upload_dir = tag else: upload_dir = f"branches/{branch}" - subprocess.check_call([ - "aws", "s3", "cp", - "--acl", "public-read", - f"./release/dist/", - f"s3://snapshots.mitmproxy.org/{upload_dir}/", - "--recursive", - ]) + # Ideally we could have R2 pull from S3 automatically, but that's not possible yet. So we upload to both. + print(f"Uploading binaries to snapshots.mitmproxy.org/{upload_dir}...") + subprocess.check_call( + [ + "aws", + "s3", + "sync", + "--delete", + *("--acl", "public-read"), + *("--exclude", "*.msix"), + root / "release/dist", + f"s3://snapshots.mitmproxy.org/{upload_dir}", + ] + ) + if tag: + # We can't scope R2 tokens, so they are only exposed in the deploy env. + print(f"Uploading binaries to downloads.mitmproxy.org/{upload_dir}...") + subprocess.check_call( + [ + "aws", + "s3", + "sync", + "--delete", + *("--acl", "public-read"), + *("--exclude", "*.msix"), + *( + "--endpoint-url", + f"https://{os.environ['R2_ACCOUNT_ID']}.r2.cloudflarestorage.com", + ), + root / "release/dist", + f"s3://downloads/{upload_dir}", + ], + env={ + **os.environ, + "AWS_REGION": "auto", + "AWS_ACCESS_KEY_ID": os.environ["R2_ACCESS_KEY_ID"], + "AWS_SECRET_ACCESS_KEY": os.environ["R2_SECRET_ACCESS_KEY"], + }, + ) # Upload releases to PyPI if tag: - whl, = Path("release/dist/").glob('mitmproxy-*-py3-none-any.whl') + print(f"Uploading wheel to PyPI...") + (whl,) = root.glob("release/dist/mitmproxy-*-py3-none-any.whl") subprocess.check_call(["twine", "upload", whl]) - # Upload dev docs - if branch == "main" or branch == "actions-hardening": # FIXME remove - subprocess.check_call([ - "aws", "configure", - "set", "preview.cloudfront", "true" - ]) - subprocess.check_call([ - "aws", "s3", - "sync", - "--delete", - "--acl", "public-read", - "docs/public", - "s3://docs.mitmproxy.org/dev" - ]) - subprocess.check_call([ - "aws", "cloudfront", - "create-invalidation", - "--distribution-id", "E1TH3USJHFQZ5Q", - "--paths", "/dev/*" - ]) + # Upload docs + def upload_docs(path: str, src: Path = root / "docs/public"): + subprocess.check_call(["aws", "configure", "set", "preview.cloudfront", "true"]) + subprocess.check_call( + [ + "aws", + "s3", + "sync", + "--delete", + "--acl", + "public-read", + src, + f"s3://docs.mitmproxy.org{path}", + ] + ) + subprocess.check_call( + [ + "aws", + "cloudfront", + "create-invalidation", + "--distribution-id", + "E1TH3USJHFQZ5Q", + "--paths", + f"{path}/*", + ] + ) + + if branch == "main": + print(f"Uploading dev docs...") + upload_docs("/dev") + if tag: + print(f"Uploading release docs...") + upload_docs("/stable") + upload_docs(f"/archive/v{tag.split('.')[0]}", src=root / "docs/archive") diff --git a/release/docker/DockerHub-README.md b/release/docker/DockerHub-README.md index 77ff770947..82f45b651c 100644 --- a/release/docker/DockerHub-README.md +++ b/release/docker/DockerHub-README.md @@ -2,47 +2,64 @@ Containerized version of [mitmproxy](https://mitmproxy.org/): an interactive, SSL/TLS-capable intercepting proxy for HTTP/1, HTTP/2, and WebSockets. -# Usage +## Usage + +To launch the terminal user interface of mitmproxy: ```sh -$ docker run --rm -it [-v ~/.mitmproxy:/home/mitmproxy/.mitmproxy] -p 8080:8080 browserup/mitmproxy +$ docker run --rm -it -v ~/.mitmproxy:/home/mitmproxy/.mitmproxy -p 8080:8080 mitmproxy/mitmproxy ``` -The *volume mount* is optional: It's to store the generated CA certificates. -Once started, mitmproxy listens as a HTTP proxy on `localhost:8080`: +Note: The `-v` for *volume mount* is optional. It allows to persist and reuse the generated CA certificates between runs, and for you to access them. +Without it, a new root CA would be generated on each container restart. + +Once started, mitmproxy listens as an HTTP proxy on `localhost:8080`: + ```sh $ http_proxy=http://localhost:8080/ curl http://example.com/ $ https_proxy=http://localhost:8080/ curl -k https://example.com/ ``` You can also start `mitmdump` by just adding that to the end of the command-line: + ```sh -$ docker run --rm -it -p 8080:8080 browserup/mitmproxy mitmdump +$ docker run --rm -it -p 8080:8080 mitmproxy/mitmproxy mitmdump +Proxy server listening at http://*:8080 +[...] ``` For `mitmweb`, you also need to expose port 8081: + ```sh # this makes :8081 accessible to the local machine only -$ docker run --rm -it -p 8080:8080 -p 127.0.0.1:8081:8081 browserup/mitmproxy mitmweb --web-host 0.0.0.0 +$ docker run --rm -it -p 8080:8080 -p 127.0.0.1:8081:8081 mitmproxy/mitmproxy mitmweb --web-host 0.0.0.0 +Web server listening at http://0.0.0.0:8081/ +No web browser found. Please open a browser and point it to http://0.0.0.0:8081/ +Proxy server listening at http://*:8080 +[...] ``` You can also pass options directly via the CLI: + ```sh -$ docker run --rm -it -p 8080:8080 browserup/mitmproxy mitmdump --set ssl_insecure=true +$ docker run --rm -it -p 8080:8080 mitmproxy/mitmproxy mitmdump --set ssl_insecure=true +Proxy server listening at http://*:8080 +[...] ``` -For further details, please consult the mitmproxy [documentation](http://docs.mitmproxy.org/en/stable/). +If `~/.mitmproxy/mitmproxy-ca.pem` is present in the container, mitmproxy will assume uid and gid from the file owner. +For further details, please consult the mitmproxy [documentation](https://docs.mitmproxy.org/en/stable/). -# Tags +## Tags The available release tags can be seen -[here](https://hub.docker.com/r/browserup/mitmproxy/tags/). +[here](https://hub.docker.com/r/mitmproxy/mitmproxy/tags/). * `dev` always tracks the git-master branch and represents the unstable development tree. * `latest` always points to the same image as the most recent stable release, including bugfix releases (e.g., `4.0.0` and `4.0.1`). * `X.Y.Z` tags contain the mitmproxy release with this version number. -# Security Notice +## Security Notice Dependencies in the Docker images are frozen on release, and can’t be updated in situ. This means that we necessarily capture any bugs or security issues that diff --git a/release/docker/Dockerfile b/release/docker/Dockerfile index 4fd025fc3a..e41cd2ab1e 100644 --- a/release/docker/Dockerfile +++ b/release/docker/Dockerfile @@ -1,18 +1,25 @@ -FROM python:3.9-buster as wheelbuilder +FROM python:3.13-bookworm AS wheelbuilder -ARG MITMPROXY_WHEEL -COPY $MITMPROXY_WHEEL /wheels/ -RUN pip install wheel && pip wheel --wheel-dir /wheels /wheels/${MITMPROXY_WHEEL} +COPY mitmproxy-*-py3-none-any.whl /wheels/ +RUN pip install wheel && pip wheel --wheel-dir /wheels /wheels/*.whl -FROM python:3.9-slim-buster +FROM python:3.13-slim-bookworm RUN useradd -mU mitmproxy RUN apt-get update \ - && apt-get install -y --no-install-recommends gosu \ + && apt-get install -y --no-install-recommends gosu nano \ && rm -rf /var/lib/apt/lists/* +RUN mkdir /home/mitmproxy/.mitmproxy \ + && chown mitmproxy:mitmproxy /home/mitmproxy/.mitmproxy + COPY --from=wheelbuilder /wheels /wheels -RUN pip install --no-index --find-links=/wheels mitmproxy + +RUN apt-get update \ + && apt-get install -y --no-install-recommends git \ + && pip install --no-build-isolation --find-links=/wheels mitmproxy \ + && rm -rf /var/lib/apt/lists/* + RUN rm -rf /wheels VOLUME /home/mitmproxy/.mitmproxy diff --git a/release/docker/README.md b/release/docker/README.md index d84382557d..b5744e4532 100644 --- a/release/docker/README.md +++ b/release/docker/README.md @@ -2,5 +2,4 @@ 1. Copy `mitmproxy-$VERSION-py3-none-any.whl` into this directory. You can get the latest public release at https://mitmproxy.org/downloads/. - 2. Replace $VERSION with your mitmproxy version and - run `docker build --build-arg MITMPROXY_WHEEL=mitmproxy-$VERSION-py3-none-any.whl .`. + 2. Run `docker build .`. diff --git a/release/docker/docker-entrypoint.sh b/release/docker/docker-entrypoint.sh index 3aaefe72fd..676ff5e37a 100755 --- a/release/docker/docker-entrypoint.sh +++ b/release/docker/docker-entrypoint.sh @@ -7,10 +7,19 @@ set -o nounset MITMPROXY_PATH="/home/mitmproxy/.mitmproxy" +if [ -f "$MITMPROXY_PATH/mitmproxy-ca.pem" ]; then + f="$MITMPROXY_PATH/mitmproxy-ca.pem" +else + f="$MITMPROXY_PATH" +fi +usermod -o \ + -u $(stat -c "%u" "$f") \ + -g $(stat -c "%g" "$f") \ + mitmproxy \ + >/dev/null # hide "usermod: no changes" + if [[ "$1" = "mitmdump" || "$1" = "mitmproxy" || "$1" = "mitmweb" ]]; then - mkdir -p "$MITMPROXY_PATH" - chown -R mitmproxy:mitmproxy "$MITMPROXY_PATH" - gosu mitmproxy "$@" + exec gosu mitmproxy "$@" else exec "$@" fi diff --git a/release/installbuilder/mitmproxy.xml b/release/installbuilder/mitmproxy.xml index 8b520f9183..43b3ae38c0 100644 --- a/release/installbuilder/mitmproxy.xml +++ b/release/installbuilder/mitmproxy.xml @@ -31,7 +31,7 @@ 1 - ../build/binaries/windows/onedir/* + ../build/pyinstaller/out/onedir/* run.ps1 @@ -130,4 +130,3 @@ - diff --git a/release/release.py b/release/release.py index ae0625fca8..c52b010314 100755 --- a/release/release.py +++ b/release/release.py @@ -46,7 +46,10 @@ def get_json(url: str) -> dict: branch = subprocess.run( ["git", "branch", "--show-current"], - cwd=root, check=True, capture_output=True, text=True + cwd=root, + check=True, + capture_output=True, + text=True, ).stdout.strip() print("➡️ Working dir clean?") @@ -56,7 +59,12 @@ def get_json(url: str) -> dict: print(f"⚠️ Skipping status check for {branch}.") else: print(f"➡️ CI is passing for {branch}?") - assert get_json(f"https://api.github.com/repos/{repo}/commits/{branch}/status")["state"] == "success" + assert ( + get_json(f"https://api.github.com/repos/{repo}/commits/{branch}/status")[ + "state" + ] + == "success" + ) print("➡️ Updating CHANGELOG.md...") changelog = root / "CHANGELOG.md" @@ -64,13 +72,15 @@ def get_json(url: str) -> dict: title = f"## {date}: mitmproxy {version}" cl = changelog.read_text("utf8") assert title not in cl - cl, ok = re.subn(r"(?<=## Unreleased: mitmproxy next)", f"\n\n\n\n{title}", cl) + cl, ok = re.subn(r"(?<=## Unreleased: mitmproxy next)", f"\n\n\n{title}", cl) assert ok == 1 changelog.write_text(cl, "utf8") print("➡️ Updating web assets...") subprocess.run(["npm", "ci"], cwd=root / "web", check=True, capture_output=True) - subprocess.run(["npm", "start", "prod"], cwd=root / "web", check=True, capture_output=True) + subprocess.run( + ["npm", "start", "prod"], cwd=root / "web", check=True, capture_output=True + ) print("➡️ Updating version...") version_py = root / "mitmproxy" / "version.py" @@ -80,13 +90,23 @@ def get_json(url: str) -> dict: version_py.write_text(ver, "utf8") print("➡️ Do release commit...") - subprocess.run(["git", "config", "user.email", "noreply@mitmproxy.org"], cwd=root, check=True) - subprocess.run(["git", "config", "user.name", "mitmproxy release bot"], cwd=root, check=True) - subprocess.run(["git", "commit", "-a", "-m", f"mitmproxy {version}"], cwd=root, check=True) - subprocess.run(["git", "tag", version], cwd=root, check=True) + subprocess.run( + ["git", "config", "user.email", "noreply@mitmproxy.org"], cwd=root, check=True + ) + subprocess.run( + ["git", "config", "user.name", "mitmproxy release bot"], cwd=root, check=True + ) + subprocess.run( + ["git", "commit", "-a", "-m", f"mitmproxy {version}"], cwd=root, check=True + ) + tag_name = f"v{version}" + subprocess.run(["git", "tag", tag_name], cwd=root, check=True) release_sha = subprocess.run( ["git", "rev-parse", "HEAD"], - cwd=root, check=True, capture_output=True, text=True + cwd=root, + check=True, + capture_output=True, + text=True, ).stdout.strip() if branch == "main": @@ -97,28 +117,46 @@ def get_json(url: str) -> dict: version_py.write_text(ver, "utf8") print("➡️ Reopen main for development...") - subprocess.run(["git", "commit", "-a", "-m", f"reopen main for development"], cwd=root, check=True) + subprocess.run( + ["git", "commit", "-a", "-m", f"reopen main for development"], + cwd=root, + check=True, + ) print("➡️ Pushing...") - subprocess.run(["git", "push", "--atomic", "origin", branch, version], cwd=root, check=True) + subprocess.run( + ["git", "push", "--atomic", "origin", branch, tag_name], cwd=root, check=True + ) print("➡️ Creating release on GitHub...") - subprocess.run(["gh", "release", "create", version, - "--title", f"mitmproxy {version}", - "--notes-file", "release/github-release-notes.txt"], cwd=root, check=True) - - # We currently have to use a personal access token, which auto-triggers CI. - # The default GITHUB_TOKEN cannot push to protected branches, - # see https://github.com/community/community/discussions/13836. - # print("➡️ Dispatching release workflow...") - # subprocess.run(["gh", "workflow", "run", "main.yml", "--ref", version], cwd=root, check=True) + subprocess.run( + [ + "gh", + "release", + "create", + tag_name, + "--title", + f"mitmproxy {version}", + "--notes-file", + "release/github-release-notes.txt", + ], + cwd=root, + check=True, + ) + + print("➡️ Dispatching release workflow...") + subprocess.run( + ["gh", "workflow", "run", "main.yml", "--ref", tag_name], cwd=root, check=True + ) print("") print("✅ CI is running now.") while True: print("⌛ Waiting for CI...") - workflows = get_json(f"https://api.github.com/repos/{repo}/actions/runs?head_sha={release_sha}")["workflow_runs"] + workflows = get_json( + f"https://api.github.com/repos/{repo}/actions/runs?head_sha={release_sha}" + )["workflow_runs"] all_done = True if not workflows: @@ -138,7 +176,7 @@ def get_json(url: str) -> dict: time.sleep(30) # relatively strict rate limits here. print("➡️ Checking GitHub Releases...") - resp = get(f"https://api.github.com/repos/{repo}/releases/tags/{version}") + resp = get(f"https://api.github.com/repos/{repo}/releases/tags/{tag_name}") assert resp.status == 200 print("➡️ Checking PyPI...") @@ -150,16 +188,23 @@ def get_json(url: str) -> dict: assert resp.status == 200 print(f"➡️ Checking Docker ({version} tag)...") - resp = get(f"https://hub.docker.com/v2/repositories/browserup/mitmproxy/tags/{version}") + resp = get( + f"https://hub.docker.com/v2/repositories/browserup/mitmproxy/tags/{version}" + ) assert resp.status == 200 if branch == "main": print("➡️ Checking Docker (latest tag)...") - docker_latest_data = get_json("https://hub.docker.com/v2/repositories/browserup/mitmproxy/tags/latest") + docker_latest_data = get_json( + "https://hub.docker.com/v2/repositories/browserup/mitmproxy/tags/latest" + ) docker_last_updated = datetime.datetime.fromisoformat( - docker_latest_data["last_updated"].replace("Z", "+00:00")) + docker_latest_data["last_updated"].replace("Z", "+00:00") + ) print(f"Last update: {docker_last_updated.isoformat(timespec='minutes')}") - assert docker_last_updated > datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(hours=2) + assert docker_last_updated > datetime.datetime.now( + datetime.timezone.utc + ) - datetime.timedelta(hours=2) print("") print("✅ All done. 🥳") diff --git a/release/selftest.py b/release/selftest.py new file mode 100644 index 0000000000..4ad7acb4f3 --- /dev/null +++ b/release/selftest.py @@ -0,0 +1,47 @@ +""" +This addons is used for binaries to perform a minimal selftest. Use like so: + + mitmdump -s selftest.py -p 0 +""" + +import asyncio +import logging +import ssl +import sys +from pathlib import Path + +from mitmproxy import ctx + + +def load(_): + # force a random port + ctx.options.listen_port = 0 + try: + ctx.options.web_open_browser = False + except KeyError: + pass + + +def running(): + # attach is somewhere so that it's not collected. + ctx.task = asyncio.create_task(make_request()) # type: ignore + + +async def make_request(): + try: + cafile = Path(ctx.options.confdir).expanduser() / "mitmproxy-ca.pem" + while not cafile.exists(): + await asyncio.sleep(0.01) + ssl_ctx = ssl.create_default_context(cafile=cafile) + port = ctx.master.addons.get("proxyserver").listen_addrs()[0][1] + reader, writer = await asyncio.open_connection("127.0.0.1", port, ssl=ssl_ctx) + writer.write(b"GET / HTTP/1.1\r\nHost: mitm.it\r\nConnection: close\r\n\r\n") + await writer.drain() + resp = await reader.read() + if b"This page is served by your local mitmproxy instance" not in resp: + raise RuntimeError(resp) + logging.info("Self-test successful.") + ctx.master.shutdown() + except Exception as e: + print(f"{e!r}") + sys.exit(1) diff --git a/release/specs/.mitmproxy-wrapper b/release/specs/.mitmproxy-wrapper new file mode 100644 index 0000000000..e6115a1b8e --- /dev/null +++ b/release/specs/.mitmproxy-wrapper @@ -0,0 +1,3 @@ +#!/bin/bash +dir=$(cd "$( dirname "${0}")" && pwd ) +open -a Terminal "${dir}/mitmproxy" diff --git a/release/specs/browserup-proxy.spec b/release/specs/browserup-proxy.spec deleted file mode 100644 index 20f1e00fea..0000000000 --- a/release/specs/browserup-proxy.spec +++ /dev/null @@ -1,34 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - - -block_cipher = None - - -a = Analysis(['browserup-proxy'], - pathex=['/home/kirill/dev/epic/bu/mitmproxy-fork/mitmproxy/release/specs'], - binaries=[], - datas=[], - hiddenimports=[], - hookspath=['/home/kirill/dev/epic/bu/mitmproxy-fork/mitmproxy/release/hooks'], - runtime_hooks=[], - excludes=['mitmproxy.tools.web', 'mitmproxy.tools.console'], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False) -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) -exe = EXE(pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - [], - name='browserup-proxy', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=True , icon='icon.ico') diff --git a/release/specs/icon.icns b/release/specs/icon.icns new file mode 100644 index 0000000000..96b4f14206 Binary files /dev/null and b/release/specs/icon.icns differ diff --git a/release/specs/macos-entitlements.plist b/release/specs/macos-entitlements.plist new file mode 100644 index 0000000000..d2f450ca08 --- /dev/null +++ b/release/specs/macos-entitlements.plist @@ -0,0 +1,13 @@ + + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + diff --git a/release/specs/windows-dir.spec b/release/specs/onedir.spec similarity index 57% rename from release/specs/windows-dir.spec rename to release/specs/onedir.spec index d80bd5efc4..aa92c8475c 100644 --- a/release/specs/windows-dir.spec +++ b/release/specs/onedir.spec @@ -1,4 +1,5 @@ from pathlib import Path +import platform from PyInstaller.building.api import PYZ, EXE, COLLECT from PyInstaller.building.build_main import Analysis @@ -8,6 +9,11 @@ assert SPECPATH == "." here = Path(r".") tools = ["mitmproxy", "mitmdump", "mitmweb", "browserup-proxy"] +if platform.system() == "Darwin": + icon = "icon.icns" +else: + icon = "icon.ico" + analysis = Analysis( tools, excludes=["tcl", "tk", "tkinter"], @@ -28,10 +34,12 @@ for tool in tools: name=tool, console=True, upx=False, - icon='icon.ico' + icon=icon, + codesign_identity='Developer ID Application', + entitlements_file=str(here / "macos-entitlements.plist"), )) -COLLECT( +coll = COLLECT( *executables, analysis.binaries, analysis.zipfiles, @@ -40,3 +48,15 @@ COLLECT( upx=False, name="onedir" ) + +if platform.system() == "Darwin": + from PyInstaller.building.osx import BUNDLE + app = BUNDLE( + # hack: add dummy executable that opens the terminal, + # workaround for https://github.com/pyinstaller/pyinstaller/pull/5419 + [(".mitmproxy-wrapper", str(here / ".mitmproxy-wrapper"), "EXECUTABLE")], + coll, + name='mitmproxy.app', + icon=icon, + bundle_identifier="org.mitmproxy", + ) diff --git a/release/specs/standalone.spec b/release/specs/standalone.spec index 768b2ade0d..bcc0f3b9c4 100644 --- a/release/specs/standalone.spec +++ b/release/specs/standalone.spec @@ -7,6 +7,11 @@ for tool in ["mitmproxy", "mitmdump", "mitmweb"]: if tool != "mitmproxy": excludes.append("mitmproxy.tools.console") + options = [] + if tool == "mitmdump": + # https://github.com/mitmproxy/mitmproxy/issues/6757 + options.append(("unbuffered", None, "OPTION")) + a = Analysis( [tool], excludes=excludes, @@ -19,8 +24,8 @@ for tool in ["mitmproxy", "mitmdump", "mitmweb"]: a.binaries, a.zipfiles, a.datas, - [], + options, name=tool, console=True, - icon='icon.ico', + icon="icon.ico", ) diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index c86df2d8fb..0000000000 --- a/requirements.txt +++ /dev/null @@ -1,48 +0,0 @@ -hpack~=4.0.0 -hyperframe~=6.0.1 -h2~=4.1.0 -h11~=0.14.0 -pip~=23.2.1 -attrs~=22.2.0 -wheel~=0.40.0 -cryptography~=38.0.4 -tornado~=6.2 -Jinja2~=3.1.2 -six~=1.16.0 -packaging~=23.0 -face~=20.1.1 -boltons~=23.0.0 -glom~=23.1.1 -MarkupSafe~=2.1.2 -PyYAML~=6.0 -Werkzeug~=2.2.3 -click~=8.1.3 -itsdangerous~=2.1.2 -pyasn1~=0.4.8 -urwid~=2.1.2 -setuptools~=67.6.1 -falcon~=3.1.1 -jsonschema~=4.17.3 -msgpack~=1.0.4 -marshmallow~=3.19.0 -apispec~=6.1.0 -asgiref~=3.5.2 -certifi~=2022.12.7 -cffi~=1.15.1 -passlib~=1.7.4 -zstandard~=0.19.0 -python-dateutil~=2.8.2 -ply~=3.11 -pycparser~=2.21 -pyparsing~=3.0.9 -pyperclip~=1.8.2 -sortedcontainers~=2.4.0 -pyrsistent~=0.19.3 -pycodestyle~=2.10.0 -Flask~=2.2.3 -wsproto~=1.2.0 -ldap3~=2.9.1 -uvicorn~=0.23.2 -Brotli~=1.0.9 -publicsuffix2~=2.20191221 -kaitaistruct~=0.10 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 4f9fd7e0b4..0000000000 --- a/setup.cfg +++ /dev/null @@ -1,80 +0,0 @@ -[flake8] -max-line-length = 140 -max-complexity = 25 -ignore = E203,E251,E252,C901,W292,W503,W504,W605,E722,E741,E126,F541 -exclude = mitmproxy/contrib/*,test/mitmproxy/data/*,release/build/* -addons = file,open,basestring,xrange,unicode,long,cmp - -[tool:pytest] -asyncio_mode = auto -testpaths = test -addopts = --capture=no --color=yes -filterwarnings = - ignore::DeprecationWarning:tornado.*: - -[coverage:run] -branch = False -omit = *contrib*, *tnetstring*, *platform*, *main.py - -[coverage:report] -show_missing = True -exclude_lines = - pragma: no cover - raise NotImplementedError - raise AssertionError - if typing.TYPE_CHECKING: - if TYPE_CHECKING: - @overload - @abstractmethod - \.\.\. - -[mypy] -ignore_missing_imports = True -files = mitmproxy,examples/addons,release - -[mypy-mitmproxy.contrib.*] -ignore_errors = True - -[mypy-tornado.*] -ignore_errors = True - -[mypy-test.*] -ignore_errors = True - -# https://github.com/python/mypy/issues/3004 -[mypy-http-modify-form,http-trailers] -ignore_errors = True - -[tool:full_coverage] -exclude = - mitmproxy/tools/ - release/hooks - -[tool:individual_coverage] -exclude = - mitmproxy/addons/onboarding.py - mitmproxy/addons/blocklist.py - mitmproxy/connections.py - mitmproxy/contentviews/base.py - mitmproxy/contentviews/grpc.py - mitmproxy/contentviews/http3.py - mitmproxy/ctx.py - mitmproxy/exceptions.py - mitmproxy/flow.py - mitmproxy/io/io.py - mitmproxy/io/tnetstring.py - mitmproxy/log.py - mitmproxy/master.py - mitmproxy/net/check.py - mitmproxy/net/http/cookies.py - mitmproxy/net/http/message.py - mitmproxy/net/http/multipart.py - mitmproxy/net/tls.py - mitmproxy/net/udp_wireguard.py - mitmproxy/options.py - mitmproxy/proxy/config.py - mitmproxy/proxy/server.py - mitmproxy/proxy/layers/tls.py - mitmproxy/utils/bits.py - mitmproxy/utils/vt_codes.py - mitmproxy/utils/pyinstaller diff --git a/setup.py b/setup.py deleted file mode 100644 index 2caa094212..0000000000 --- a/setup.py +++ /dev/null @@ -1,130 +0,0 @@ -import os -import re -from codecs import open - -from setuptools import find_packages, setup - -# Based on https://github.com/pypa/sampleproject/blob/main/setup.py -# and https://python-packaging-user-guide.readthedocs.org/ - -here = os.path.abspath(os.path.dirname(__file__)) - -with open(os.path.join(here, "README.md"), encoding="utf-8") as f: - long_description = f.read() -long_description_content_type = "text/markdown" - -with open(os.path.join(here, "mitmproxy/version.py")) as f: - match = re.search(r'VERSION = "(.+?)"', f.read()) - assert match - VERSION = match.group(1) - -setup( - name="mitmproxy", - version=VERSION, - description="An interactive, SSL/TLS-capable intercepting proxy for HTTP/1, HTTP/2, and WebSockets.", - long_description=long_description, - long_description_content_type=long_description_content_type, - url="http://mitmproxy.org", - author="Aldo Cortesi", - author_email="aldo@corte.si", - license="MIT", - classifiers=[ - "License :: OSI Approved :: MIT License", - "Development Status :: 5 - Production/Stable", - "Environment :: Console :: Curses", - "Operating System :: MacOS", - "Operating System :: POSIX", - "Operating System :: Microsoft :: Windows", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: Implementation :: CPython", - "Topic :: Security", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: Proxy Servers", - "Topic :: System :: Networking :: Monitoring", - "Topic :: Software Development :: Testing", - "Typing :: Typed", - ], - project_urls={ - "Documentation": "https://docs.mitmproxy.org/stable/", - "Source": "https://github.com/mitmproxy/mitmproxy/", - "Tracker": "https://github.com/mitmproxy/mitmproxy/issues", - }, - packages=find_packages( - include=[ - "mitmproxy", - "mitmproxy.*", - ] - ), - include_package_data=True, - entry_points={ - "console_scripts": [ - "mitmproxy = mitmproxy.tools.main:mitmproxy", - "mitmdump = mitmproxy.tools.main:mitmdump", - "mitmweb = mitmproxy.tools.main:mitmweb", - "browserup-proxy = mitmproxy.tools.main:browserupproxy", - ], - "pyinstaller40": [ - "hook-dirs = mitmproxy.utils.pyinstaller:hook_dirs", - ] - }, - python_requires=">=3.9", - # https://packaging.python.org/en/latest/discussions/install-requires-vs-requirements/#install-requires - # It is not considered best practice to use install_requires to pin dependencies to specific versions. - install_requires=[ - "asgiref>=3.2.10,<3.6", - "Brotli>=1.0,<1.1", - "certifi>=2019.9.11", # no semver here - this should always be on the last release! - "cryptography>=38.0,<38.1", - "flask>=1.1.1,<2.3", - "h11>=0.11,<0.15", - "h2>=4.1,<5", - "hyperframe>=6.0,<7", - "kaitaistruct>=0.10,<0.11", - "ldap3>=2.8,<2.10", - "mitmproxy_wireguard>=0.1.6,<0.2", - "msgpack>=1.0.0, <1.1.0", - "passlib>=1.6.5, <1.8", - "protobuf>=3.14,<5", - "pyOpenSSL>=22.1,<22.2", - "pyparsing>=2.4.2,<3.1", - "pyperclip>=1.6.0,<1.9", - "ruamel.yaml>=0.16,<0.18", - "sortedcontainers>=2.3,<2.5", - "tornado>=6.1,<7", - "urwid>=2.1.1,<2.2", - "wsproto>=1.0,<1.3", - "publicsuffix2>=2.20190812,<3", - "falcon>=3.1.0", - "marshmallow>=3.0.0", - "falcon-apispec@git+https://github.com/browserup/falcon-apispec#egg=falcon-apispec", - "python-dateutil>=2.8.1", - "glom>=20.11.0", - "jsonschema>=3.2.0", - "jsonpath_ng>=1.5.3", - "zstandard>=0.11,<0.20", - "typing-extensions>=4.3,<4.5; python_version<'3.12'", - ], - extras_require={ - ':sys_platform == "win32"': [ - "pydivert>=2.0.3,<2.2", - ], - "dev": [ - "click>=7.0,<8.2", - "hypothesis>=5.8,<7", - "parver>=0.1,<2.0", - "pdoc>=4.0.0", - "pyinstaller>=5.13.2", - "pytest-asyncio>=0.17,<0.21", - "pytest-cov>=2.7.1,<4.1", - "pytest-timeout>=1.3.3,<2.2", - "pytest-xdist>=2.1.0,<3.1", - "pytest>=6.1.0,<8", - "requests>=2.9.1,<3", - "tox>=3.5,<4", - "wheel>=0.36.2,<0.39", - ], - }, -) diff --git a/test/bench/benchmark.py b/test/bench/benchmark.py index 289df5c164..f570704843 100644 --- a/test/bench/benchmark.py +++ b/test/bench/benchmark.py @@ -46,7 +46,7 @@ def load(self, loader): "benchmark_save_path", str, "/tmp/profile", - "Destination for the .prof and and .bench result files", + "Destination for the .prof and .bench result files", ) ctx.options.update( mode="reverse:http://devd.io:10001", @@ -56,7 +56,7 @@ def load(self, loader): def running(self): if not self.started: self.started = True - asyncio.get_running_loop().create_task(self.procs()) + self._task = asyncio.create_task(self.procs()) def done(self): self.pr.dump_stats(ctx.options.benchmark_save_path + ".prof") diff --git a/test/conftest.py b/test/conftest.py index 89d78c0e9c..4ab6ac6c66 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,20 +1,25 @@ from __future__ import annotations + import asyncio import os +import platform import socket +import sys import pytest from mitmproxy.utils import data -pytest_plugins = ("test.full_coverage_plugin",) - skip_windows = pytest.mark.skipif(os.name == "nt", reason="Skipping due to Windows") skip_not_windows = pytest.mark.skipif( os.name != "nt", reason="Skipping due to not Windows" ) +skip_not_linux = pytest.mark.skipif( + platform.system() != "Linux", reason="Skipping due to not Linux" +) + try: s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) s.bind(("::1", 0)) @@ -27,6 +32,19 @@ skip_no_ipv6 = pytest.mark.skipif(no_ipv6, reason="Host has no IPv6 support") +class EagerTaskCreationEventLoopPolicy(asyncio.DefaultEventLoopPolicy): + def new_event_loop(self): + loop = super().new_event_loop() + if sys.version_info >= (3, 12): + loop.set_task_factory(asyncio.eager_task_factory) + return loop + + +@pytest.fixture(scope="session") +def event_loop_policy(request): + return EagerTaskCreationEventLoopPolicy() + + @pytest.fixture() def tdata(): return data.Data(__name__) @@ -46,7 +64,7 @@ async def await_log(self, text, timeout=2): return True else: await asyncio.sleep(0.01) - raise AssertionError(f"Did not find {text!r} in log:\n{self.caplog.text}.") + raise AssertionError(f"Did not find {text!r} in log:\n{self.caplog.text}") def clear(self) -> None: self.caplog.clear() diff --git a/test/examples/test_examples.py b/test/examples/test_examples.py index 50b08a1962..1cf0bd304c 100644 --- a/test/examples/test_examples.py +++ b/test/examples/test_examples.py @@ -1,8 +1,8 @@ from mitmproxy import contentviews +from mitmproxy.http import Headers +from mitmproxy.test import taddons from mitmproxy.test import tflow from mitmproxy.test import tutils -from mitmproxy.test import taddons -from mitmproxy.http import Headers class TestScripts: diff --git a/test/filename_matching.py b/test/filename_matching.py index 3e9878f020..9d64ede25f 100755 --- a/test/filename_matching.py +++ b/test/filename_matching.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 - +import glob import os import re -import glob import sys diff --git a/test/full_coverage_plugin.py b/test/full_coverage_plugin.py deleted file mode 100644 index 3d1b8b6787..0000000000 --- a/test/full_coverage_plugin.py +++ /dev/null @@ -1,143 +0,0 @@ -import os -import configparser -import pytest -import sys - -here = os.path.abspath(os.path.dirname(__file__)) - - -enable_coverage = False -coverage_values = [] -coverage_passed = True -no_full_cov = [] - - -def pytest_addoption(parser): - parser.addoption( - "--full-cov", - action="append", - dest="full_cov", - default=[], - help="Require full test coverage of 100%% for this module/path/filename (multi-allowed). Default: none", - ) - - parser.addoption( - "--no-full-cov", - action="append", - dest="no_full_cov", - default=[], - help="Exclude file from a parent 100%% coverage requirement (multi-allowed). Default: none", - ) - - -def pytest_configure(config): - global enable_coverage - global no_full_cov - - enable_coverage = ( - config.getoption("file_or_dir") - and len(config.getoption("file_or_dir")) == 0 - and config.getoption("full_cov") - and len(config.getoption("full_cov")) > 0 - and config.pluginmanager.getplugin("_cov") is not None - and config.pluginmanager.getplugin("_cov").cov_controller is not None - and config.pluginmanager.getplugin("_cov").cov_controller.cov is not None - ) - - c = configparser.ConfigParser() - c.read(os.path.join(here, "..", "setup.cfg")) - fs = c["tool:full_coverage"]["exclude"].split("\n") - no_full_cov = config.option.no_full_cov + [f.strip() for f in fs] - - -@pytest.hookimpl(hookwrapper=True) -def pytest_runtestloop(session): - global enable_coverage - global coverage_values - global coverage_passed - global no_full_cov - - if not enable_coverage: - yield - return - - cov = session.config.pluginmanager.getplugin("_cov").cov_controller.cov - - if os.name == "nt": - cov.exclude("pragma: windows no cover") - - if sys.platform == "darwin": - cov.exclude("pragma: osx no cover") - - if os.environ.get("OPENSSL") == "old": - cov.exclude("pragma: openssl-old no cover") - - yield - - coverage_values = {name: 0 for name in session.config.option.full_cov} - - prefix = os.getcwd() - - excluded_files = [os.path.normpath(f) for f in no_full_cov] - measured_files = [ - os.path.normpath(os.path.relpath(f, prefix)) - for f in cov.get_data().measured_files() - ] - measured_files = [ - f - for f in measured_files - if not any(f.startswith(excluded_f) for excluded_f in excluded_files) - ] - - for name in coverage_values.keys(): - files = [f for f in measured_files if f.startswith(os.path.normpath(name))] - try: - with open(os.devnull, "w") as null: - overall = cov.report(files, ignore_errors=True, file=null) - singles = [ - (s, cov.report(s, ignore_errors=True, file=null)) for s in files - ] - coverage_values[name] = (overall, singles) - except: - pass - - if any(v < 100 for v, _ in coverage_values.values()): - # make sure we get the EXIT_TESTSFAILED exit code - session.testsfailed += 1 - coverage_passed = False - - -def pytest_terminal_summary(terminalreporter, exitstatus, config): - global enable_coverage - global coverage_values - global coverage_passed - global no_full_cov - - if not enable_coverage: - return - - terminalreporter.write("\n") - if not coverage_passed: - markup = {"red": True, "bold": True} - msg = "FAIL: Full test coverage not reached!\n" - terminalreporter.write(msg, **markup) - - for name in sorted(coverage_values.keys()): - msg = f"Coverage for {name}: {coverage_values[name][0]:.2f}%\n" - if coverage_values[name][0] < 100: - markup = {"red": True, "bold": True} - for s, v in sorted(coverage_values[name][1]): - if v < 100: - msg += f" {s}: {v:.2f}%\n" - else: - markup = {"green": True} - terminalreporter.write(msg, **markup) - else: - msg = "SUCCESS: Full test coverage reached in modules and files:\n" - msg += "{}\n\n".format("\n".join(config.option.full_cov)) - terminalreporter.write(msg, green=True) - - msg = "\nExcluded files:\n" - for s in sorted(no_full_cov): - msg += f" {s}\n" - terminalreporter.write(msg) diff --git a/test/helper_tools/dumperview.py b/test/helper_tools/dumperview.py index 450b7f12f4..9c90f784bb 100755 --- a/test/helper_tools/dumperview.py +++ b/test/helper_tools/dumperview.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 import asyncio + import click from mitmproxy.addons import dumper -from mitmproxy.test import tflow from mitmproxy.test import taddons +from mitmproxy.test import tflow def run_async(coro): diff --git a/test/helper_tools/getcert b/test/helper_tools/getcert index 43ebf11dc2..841fac644d 100644 --- a/test/helper_tools/getcert +++ b/test/helper_tools/getcert @@ -2,9 +2,7 @@ import sys sys.path.insert(0, "../..") import socket -import tempfile import ssl -import subprocess addr = socket.gethostbyname(sys.argv[1]) print(ssl.get_server_certificate((addr, 443))) diff --git a/test/helper_tools/linkify-changelog.py b/test/helper_tools/linkify-changelog.py index f0db26175e..77558d87ce 100644 --- a/test/helper_tools/linkify-changelog.py +++ b/test/helper_tools/linkify-changelog.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -from pathlib import Path import re +from pathlib import Path changelog = Path(__file__).parent / "../../CHANGELOG.md" diff --git a/test/helper_tools/loggrep.py b/test/helper_tools/loggrep.py index a986e47c47..965f70fb6a 100755 --- a/test/helper_tools/loggrep.py +++ b/test/helper_tools/loggrep.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import fileinput -import sys import re +import sys if __name__ == "__main__": if len(sys.argv) < 3: @@ -11,7 +11,7 @@ port = sys.argv[1] matches = False for line in fileinput.input(sys.argv[2:]): - if re.match(r"^\[|(\d+\.){3}", line): + if re.search(r"^\[|(\d+\.){3}", line): matches = port in line if matches: print(line, end="") diff --git a/test/helper_tools/memoryleak.py b/test/helper_tools/memoryleak.py index d02482353b..12e733ae26 100644 --- a/test/helper_tools/memoryleak.py +++ b/test/helper_tools/memoryleak.py @@ -1,7 +1,9 @@ import gc import threading -from pympler import muppy, refbrowser + from OpenSSL import SSL +from pympler import muppy +from pympler import refbrowser # import os # os.environ["TK_LIBRARY"] = r"C:\Python27\tcl\tcl8.5" diff --git a/test/individual_coverage.py b/test/individual_coverage.py index e3c5ef5342..1d6648ca6e 100755 --- a/test/individual_coverage.py +++ b/test/individual_coverage.py @@ -1,108 +1,119 @@ #!/usr/bin/env python3 - -import io -import contextlib +import ast +import asyncio +import fnmatch import os +import re +import subprocess import sys -import glob -import multiprocessing -import configparser -import itertools -import pytest - - -def run_tests(src, test, fail): - stderr = io.StringIO() - stdout = io.StringIO() - with contextlib.redirect_stderr(stderr): - with contextlib.redirect_stdout(stdout): - e = pytest.main( - [ +from pathlib import Path + +import tomllib + +root = Path(__file__).parent.parent.absolute() + + +async def main(): + with open("pyproject.toml", "rb") as f: + data = tomllib.load(f) + + exclude = re.compile( + "|".join( + f"({fnmatch.translate(x)})" + for x in data["tool"]["pytest"]["individual_coverage"]["exclude"] + ) + ) + + sem = asyncio.Semaphore(os.cpu_count() or 1) + + async def run_tests(f: Path, should_fail: bool) -> None: + if f.name == "__init__.py": + mod = ast.parse(f.read_text()) + full_cov_on_import = all( + isinstance(stmt, (ast.ImportFrom, ast.Import, ast.Assign)) + for stmt in mod.body + ) + if full_cov_on_import: + if should_fail: + raise RuntimeError( + f"Remove {f} from tool.pytest.individual_coverage in pyproject.toml." + ) + else: + print(f"{f}: skip __init__.py file without logic") + return + + test_file = Path("test") / f.parent.with_name(f"test_{f.parent.name}.py") + else: + test_file = Path("test") / f.with_name(f"test_{f.name}") + + coverage_file = f".coverage-{str(f).replace('/','-')}" + + async with sem: + try: + proc = await asyncio.create_subprocess_exec( + "pytest", "-qq", "--disable-pytest-warnings", "--cov", - src.replace(".py", "").replace("/", "."), + str(f.with_suffix("")).replace("/", "."), "--cov-fail-under", "100", "--cov-report", "term-missing:skip-covered", - "-o", - "faulthandler_timeout=0", - test, - ] - ) + test_file, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env={ + "COVERAGE_FILE": coverage_file, + **os.environ, + }, + ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), 60) + except TimeoutError: + raise RuntimeError(f"{f}: timeout") + finally: + Path(coverage_file).unlink(missing_ok=True) - if e == 0: - if fail: - print( - "FAIL DUE TO UNEXPECTED SUCCESS:", - src, - "Please remove this file from setup.cfg tool:individual_coverage/exclude.", - ) - e = 42 - else: - print(".") - else: - if fail: - print("Ignoring allowed fail:", src) - e = 0 - else: - cov = [ - l - for l in stdout.getvalue().split("\n") - if (src in l) or ("was never imported" in l) - ] - if len(cov) == 1: - print("FAIL:", cov[0]) + if should_fail: + if proc.returncode != 0: + print(f"{f}: excluded") + else: + raise RuntimeError( + f"{f} is now fully covered by {test_file}. Remove it from tool.pytest.individual_coverage in pyproject.toml." + ) else: - print("FAIL:", src, test, stdout.getvalue(), stdout.getvalue()) - print(stderr.getvalue()) - print(stdout.getvalue()) - - sys.exit(e) - - -def start_pytest(src, test, fail): - # run pytest in a new process, otherwise imports and modules might conflict - proc = multiprocessing.Process(target=run_tests, args=(src, test, fail)) - proc.start() - proc.join() - return (src, test, proc.exitcode) - - -def main(): - c = configparser.ConfigParser() - c.read("setup.cfg") - fs = c["tool:individual_coverage"]["exclude"].strip().split("\n") - no_individual_cov = [f.strip() for f in fs] - - excluded = [ - "mitmproxy/contrib/", - "mitmproxy/test/", - "mitmproxy/tools/", - "mitmproxy/platform/", - ] - src_files = glob.glob("mitmproxy/**/*.py", recursive=True) - src_files = [f for f in src_files if os.path.basename(f) != "__init__.py"] - src_files = [ - f for f in src_files if not any(os.path.normpath(p) in f for p in excluded) - ] - if len(sys.argv) > 1: - src_files = [f for f in src_files if sys.argv[1] in str(f)] - - ps = [] - for src in sorted(src_files): - test = os.path.join( - "test", os.path.dirname(src), "test_" + os.path.basename(src) + if proc.returncode == 0: + print(f"{f}: ok") + else: + raise RuntimeError( + f"{f} is not fully covered by {test_file}:\n{stdout.decode(errors='ignore')}\n{stderr.decode(errors='ignore')}" + ) + + tasks = [] + for f in (root / "mitmproxy").glob("**/*.py"): + f = f.relative_to(root) + + if len(sys.argv) > 1 and sys.argv[1] not in str(f): + continue + + if f.name == "__init__.py" and f.stat().st_size == 0: + print(f"{f}: empty") + continue + + tasks.append( + asyncio.create_task(run_tests(f, should_fail=exclude.match(str(f)))) ) - if os.path.isfile(test): - ps.append((src, test, src in no_individual_cov)) - result = list(itertools.starmap(start_pytest, ps)) + exit_code = 0 + for task in asyncio.as_completed(tasks): + try: + await task + except RuntimeError as e: + print(e) + exit_code = 1 - if any(e != 0 for _, _, e in result): - sys.exit(1) + sys.exit(exit_code) if __name__ == "__main__": - main() + asyncio.run(main()) diff --git a/test/mitmproxy/addons/browserup/test_api.py b/test/mitmproxy/addons/browserup/test_api.py index 74485bc7dc..b837dd83c2 100644 --- a/test/mitmproxy/addons/browserup/test_api.py +++ b/test/mitmproxy/addons/browserup/test_api.py @@ -1,13 +1,13 @@ +import os +import tempfile + import falcon -from falcon import testing import pytest -from mitmproxy.addons.browserup import har_capture_addon +from falcon import testing + import mitmproxy.addons.browserup.browserup_addons_manager -import tempfile -import os -from mitmproxy.test import tflow -from mitmproxy.test import tutils -from mitmproxy.test import taddons +from mitmproxy.addons.browserup import har_capture_addon +from mitmproxy.test import taddons, tflow, tutils from mitmproxy.utils import data diff --git a/test/mitmproxy/addons/browserup/test_counters.py b/test/mitmproxy/addons/browserup/test_counters.py index 42dec0495e..2065b6dae3 100644 --- a/test/mitmproxy/addons/browserup/test_counters.py +++ b/test/mitmproxy/addons/browserup/test_counters.py @@ -1,9 +1,8 @@ import pytest -from mitmproxy.test import tflow -from mitmproxy.test import tutils -from mitmproxy.test import taddons + from mitmproxy import http from mitmproxy.addons.browserup import har_capture_addon +from mitmproxy.test import taddons, tflow, tutils class TestHARCounters: diff --git a/test/mitmproxy/addons/browserup/test_errors.py b/test/mitmproxy/addons/browserup/test_errors.py index 40a53fe398..38436bd201 100644 --- a/test/mitmproxy/addons/browserup/test_errors.py +++ b/test/mitmproxy/addons/browserup/test_errors.py @@ -1,9 +1,8 @@ import pytest -from mitmproxy.test import tflow -from mitmproxy.test import tutils -from mitmproxy.test import taddons + from mitmproxy import http from mitmproxy.addons.browserup import har_capture_addon +from mitmproxy.test import taddons, tflow, tutils class TestHARErrors: diff --git a/test/mitmproxy/addons/browserup/test_har_capture.py b/test/mitmproxy/addons/browserup/test_har_capture.py index 5a0b3f0fb4..0922e62453 100644 --- a/test/mitmproxy/addons/browserup/test_har_capture.py +++ b/test/mitmproxy/addons/browserup/test_har_capture.py @@ -1,18 +1,17 @@ import json -import pytest -import tempfile import os -from mitmproxy import http -from mitmproxy.test import tflow -from mitmproxy.test.tflow import twebsocketflow -from mitmproxy.test import tutils -from mitmproxy.test import taddons -from mitmproxy.net.http import cookies -from mitmproxy.utils import data -from mitmproxy.addons.browserup import har_capture_addon -from mitmproxy import websocket +import tempfile + +import pytest from wsproto.frame_protocol import Opcode + +from mitmproxy import http, websocket +from mitmproxy.addons.browserup import har_capture_addon from mitmproxy.addons.browserup.har.har_capture_types import HarCaptureTypes +from mitmproxy.net.http import cookies +from mitmproxy.test import taddons, tflow, tutils +from mitmproxy.test.tflow import twebsocketflow +from mitmproxy.utils import data class TestHARCapture: diff --git a/test/mitmproxy/addons/browserup/test_har_verifications.py b/test/mitmproxy/addons/browserup/test_har_verifications.py index e7d733614d..30bc811761 100644 --- a/test/mitmproxy/addons/browserup/test_har_verifications.py +++ b/test/mitmproxy/addons/browserup/test_har_verifications.py @@ -1,15 +1,12 @@ import pytest -from mitmproxy.test import tflow -from mitmproxy.test import tutils -from mitmproxy.test import taddons -from mitmproxy import http +from wsproto.frame_protocol import Opcode +from mitmproxy import http, websocket from mitmproxy.addons.browserup import har_capture_addon -from mitmproxy.addons.browserup.har.har_verifications import HarVerifications from mitmproxy.addons.browserup.har.har_capture_types import HarCaptureTypes +from mitmproxy.addons.browserup.har.har_verifications import HarVerifications +from mitmproxy.test import taddons, tflow, tutils from mitmproxy.test.tflow import twebsocketflow -from mitmproxy import websocket -from wsproto.frame_protocol import Opcode class TestHARVerifications: diff --git a/test/mitmproxy/addons/test_anticache.py b/test/mitmproxy/addons/test_anticache.py index b3eb00d332..a0746decc3 100644 --- a/test/mitmproxy/addons/test_anticache.py +++ b/test/mitmproxy/addons/test_anticache.py @@ -1,7 +1,6 @@ -from mitmproxy.test import tflow - from mitmproxy.addons import anticache from mitmproxy.test import taddons +from mitmproxy.test import tflow class TestAntiCache: diff --git a/test/mitmproxy/addons/test_anticomp.py b/test/mitmproxy/addons/test_anticomp.py index 92650332c7..70a97dbf56 100644 --- a/test/mitmproxy/addons/test_anticomp.py +++ b/test/mitmproxy/addons/test_anticomp.py @@ -1,7 +1,6 @@ -from mitmproxy.test import tflow - from mitmproxy.addons import anticomp from mitmproxy.test import taddons +from mitmproxy.test import tflow class TestAntiComp: diff --git a/test/mitmproxy/addons/test_asgiapp.py b/test/mitmproxy/addons/test_asgiapp.py index 0926f091e1..fd331cdd72 100644 --- a/test/mitmproxy/addons/test_asgiapp.py +++ b/test/mitmproxy/addons/test_asgiapp.py @@ -14,7 +14,6 @@ @tapp.route("/") def hello(): - print("CALLED") return "testapp" @@ -57,57 +56,70 @@ async def test_asgi_full(caplog): assert await ps.setup_servers() proxy_addr = ("127.0.0.1", ps.listen_addrs()[0][1]) - reader, writer = await asyncio.open_connection(*proxy_addr) + # We parallelize connection establishment/closure because those operations tend to be slow. + [ + (r1, w1), + (r2, w2), + (r3, w3), + (r4, w4), + (r5, w5), + ] = await asyncio.gather( + asyncio.open_connection(*proxy_addr), + asyncio.open_connection(*proxy_addr), + asyncio.open_connection(*proxy_addr), + asyncio.open_connection(*proxy_addr), + asyncio.open_connection(*proxy_addr), + ) + req = f"GET http://testapp:80/ HTTP/1.1\r\n\r\n" - writer.write(req.encode()) - header = await reader.readuntil(b"\r\n\r\n") + w1.write(req.encode()) + header = await r1.readuntil(b"\r\n\r\n") assert header.startswith(b"HTTP/1.1 200 OK") - body = await reader.readuntil(b"testapp") + body = await r1.readuntil(b"testapp") assert body == b"testapp" - writer.close() - await writer.wait_closed() - reader, writer = await asyncio.open_connection(*proxy_addr) req = f"GET http://testapp:80/parameters?param1=1¶m2=2 HTTP/1.1\r\n\r\n" - writer.write(req.encode()) - header = await reader.readuntil(b"\r\n\r\n") + w2.write(req.encode()) + header = await r2.readuntil(b"\r\n\r\n") assert header.startswith(b"HTTP/1.1 200 OK") - body = await reader.readuntil(b"}") + body = await r2.readuntil(b"}") assert body == b'{"param1": "1", "param2": "2"}' - writer.close() - await writer.wait_closed() - reader, writer = await asyncio.open_connection(*proxy_addr) req = f"POST http://testapp:80/requestbody HTTP/1.1\r\nContent-Length: 6\r\n\r\nHello!" - writer.write(req.encode()) - header = await reader.readuntil(b"\r\n\r\n") + w3.write(req.encode()) + header = await r3.readuntil(b"\r\n\r\n") assert header.startswith(b"HTTP/1.1 200 OK") - body = await reader.readuntil(b"}") + body = await r3.readuntil(b"}") assert body == b'{"body": "Hello!"}' - writer.close() - await writer.wait_closed() - reader, writer = await asyncio.open_connection(*proxy_addr) req = f"GET http://errapp:80/?foo=bar HTTP/1.1\r\n\r\n" - writer.write(req.encode()) - header = await reader.readuntil(b"\r\n\r\n") + w4.write(req.encode()) + header = await r4.readuntil(b"\r\n\r\n") assert header.startswith(b"HTTP/1.1 500") - body = await reader.readuntil(b"ASGI Error") + body = await r4.readuntil(b"ASGI Error") assert body == b"ASGI Error" - writer.close() - await writer.wait_closed() assert "ValueError" in caplog.text - reader, writer = await asyncio.open_connection(*proxy_addr) req = f"GET http://noresponseapp:80/ HTTP/1.1\r\n\r\n" - writer.write(req.encode()) - header = await reader.readuntil(b"\r\n\r\n") + w5.write(req.encode()) + header = await r5.readuntil(b"\r\n\r\n") assert header.startswith(b"HTTP/1.1 500") - body = await reader.readuntil(b"ASGI Error") + body = await r5.readuntil(b"ASGI Error") assert body == b"ASGI Error" - writer.close() - await writer.wait_closed() assert "no response sent" in caplog.text + w1.close() + w2.close() + w3.close() + w4.close() + w5.close() + await asyncio.gather( + w1.wait_closed(), + w2.wait_closed(), + w3.wait_closed(), + w4.wait_closed(), + w5.wait_closed(), + ) + tctx.configure(ps, server=False) assert await ps.setup_servers() diff --git a/test/mitmproxy/addons/test_block.py b/test/mitmproxy/addons/test_block.py index 3a7d0d4837..afcd508492 100644 --- a/test/mitmproxy/addons/test_block.py +++ b/test/mitmproxy/addons/test_block.py @@ -2,6 +2,7 @@ from mitmproxy import connection from mitmproxy.addons import block +from mitmproxy.proxy.mode_specs import ProxyMode from mitmproxy.test import taddons @@ -56,6 +57,20 @@ async def test_block_global(block_global, block_private, should_be_killed, addre ar = block.Block() with taddons.context(ar) as tctx: tctx.configure(ar, block_global=block_global, block_private=block_private) - client = connection.Client(address, ("127.0.0.1", 8080), 1607699500) + client = connection.Client(peername=address, sockname=("127.0.0.1", 8080)) ar.client_connected(client) assert bool(client.error) == should_be_killed + + +async def test_ignore_local_mode(): + """At least on macOS, local mode peername may be the client's public IP.""" + ar = block.Block() + with taddons.context(ar) as tctx: + tctx.configure(ar, block_private=True) + client = connection.Client( + peername=("192.168.1.1", 0), + sockname=("127.0.0.1", 8080), + proxy_mode=ProxyMode.parse("local"), + ) + ar.client_connected(client) + assert not client.error diff --git a/test/mitmproxy/addons/test_blocklist.py b/test/mitmproxy/addons/test_blocklist.py index 9187443b28..b7c7e536d3 100644 --- a/test/mitmproxy/addons/test_blocklist.py +++ b/test/mitmproxy/addons/test_blocklist.py @@ -22,20 +22,21 @@ def test_parse_spec_err(filter, err): class TestBlockList: @pytest.mark.parametrize( - "filter,status_code", + "filter,request_url,status_code", [ - (":~u example.org:404", 404), - (":~u example.com:404", None), - ("/!jpg/418", None), - ("/!png/418", 418), + (":~u example.org:404", b"https://example.org/images/test.jpg", 404), + (":~u example.com:404", b"https://example.org/images/test.jpg", None), + (":~u test:404", b"https://example.org/images/TEST.jpg", 404), + ("/!jpg/418", b"https://example.org/images/test.jpg", None), + ("/!png/418", b"https://example.org/images/test.jpg", 418), ], ) - def test_block(self, filter, status_code): + def test_block(self, filter, request_url, status_code): bl = blocklist.BlockList() with taddons.context(bl) as tctx: tctx.configure(bl, block_list=[filter]) f = tflow.tflow() - f.request.url = b"https://example.org/images/test.jpg" + f.request.url = request_url bl.request(f) if status_code is not None: assert f.response.status_code == status_code diff --git a/test/mitmproxy/addons/test_browser.py b/test/mitmproxy/addons/test_browser.py index 31cbe292af..d5bd38c5c5 100644 --- a/test/mitmproxy/addons/test_browser.py +++ b/test/mitmproxy/addons/test_browser.py @@ -6,7 +6,11 @@ def test_browser(caplog): caplog.set_level("INFO") - with mock.patch("subprocess.Popen") as po, mock.patch("shutil.which") as which, taddons.context(): + with ( + mock.patch("subprocess.Popen") as po, + mock.patch("shutil.which") as which, + taddons.context(), + ): which.return_value = "chrome" b = browser.Browser() b.start() @@ -15,6 +19,10 @@ def test_browser(caplog): b.start() assert "Starting additional browser" in caplog.text assert len(b.browser) == 2 + + b.start("unsupported-browser") + assert "Invalid browser name." in caplog.text + assert len(b.browser) == 2 b.done() assert not b.browser @@ -29,29 +37,30 @@ async def test_no_browser(caplog): assert "platform is not supported" in caplog.text -async def test_get_browser_cmd_executable(): +async def test_find_executable_cmd(): with mock.patch("shutil.which") as which: which.side_effect = lambda cmd: cmd == "chrome" - assert browser.get_browser_cmd() == ["chrome"] + assert browser.find_executable_cmd("chrome") == ["chrome"] -async def test_get_browser_cmd_no_executable(): +async def test_find_executable_cmd_no_executable(): with mock.patch("shutil.which") as which: which.return_value = False - assert browser.get_browser_cmd() is None + assert browser.find_executable_cmd("chrome") is None -async def test_get_browser_cmd_flatpak(): +async def test_find_flatpak_cmd(): def subprocess_run_mock(cmd, **kwargs): returncode = 0 if cmd == ["flatpak", "info", "com.google.Chrome"] else 1 return mock.Mock(returncode=returncode) - with mock.patch("shutil.which") as which, mock.patch( - "subprocess.run" - ) as subprocess_run: + with ( + mock.patch("shutil.which") as which, + mock.patch("subprocess.run") as subprocess_run, + ): which.side_effect = lambda cmd: cmd == "flatpak" subprocess_run.side_effect = subprocess_run_mock - assert browser.get_browser_cmd() == [ + assert browser.find_flatpak_cmd("com.google.Chrome") == [ "flatpak", "run", "-p", @@ -59,10 +68,30 @@ def subprocess_run_mock(cmd, **kwargs): ] -async def test_get_browser_cmd_no_flatpak(): - with mock.patch("shutil.which") as which, mock.patch( - "subprocess.run" - ) as subprocess_run: +async def test_find_flatpak_cmd_no_flatpak(): + with ( + mock.patch("shutil.which") as which, + mock.patch("subprocess.run") as subprocess_run, + ): which.side_effect = lambda cmd: cmd == "flatpak" subprocess_run.return_value = mock.Mock(returncode=1) - assert browser.get_browser_cmd() is None + assert browser.find_flatpak_cmd("com.google.Chrome") is None + + +async def test_browser_start_firefox(): + with ( + mock.patch("shutil.which") as which, + mock.patch("subprocess.Popen") as po, + taddons.context(), + ): + which.return_value = "firefox" + browser.Browser().start("firefox") + assert po.called + + +async def test_browser_start_firefox_not_found(caplog): + caplog.set_level("INFO") + with mock.patch("shutil.which") as which: + which.return_value = False + browser.Browser().start("firefox") + assert "platform is not supported" in caplog.text diff --git a/test/mitmproxy/addons/test_clientplayback.py b/test/mitmproxy/addons/test_clientplayback.py index 432111a2d7..b2f210606f 100644 --- a/test/mitmproxy/addons/test_clientplayback.py +++ b/test/mitmproxy/addons/test_clientplayback.py @@ -1,34 +1,64 @@ import asyncio +import ssl from contextlib import asynccontextmanager import pytest -from mitmproxy.addons.clientplayback import ClientPlayback, ReplayHandler +from mitmproxy.addons.clientplayback import ClientPlayback +from mitmproxy.addons.clientplayback import ReplayHandler from mitmproxy.addons.proxyserver import Proxyserver -from mitmproxy.exceptions import CommandError, OptionsError +from mitmproxy.addons.tlsconfig import TlsConfig from mitmproxy.connection import Address -from mitmproxy.test import taddons, tflow +from mitmproxy.exceptions import CommandError +from mitmproxy.exceptions import OptionsError +from mitmproxy.test import taddons +from mitmproxy.test import tflow @asynccontextmanager -async def tcp_server(handle_conn) -> Address: - server = await asyncio.start_server(handle_conn, "127.0.0.1", 0) +async def tcp_server(handle_conn, **server_args) -> Address: + """TCP server context manager that... + + 1. Exits only after all handlers have returned. + 2. Ensures that all handlers are closed properly. If we don't do that, + we get ghost errors in others tests from StreamWriter.__del__. + + Spawning a TCP server is relatively slow. Consider using in-memory networking for faster tests. + """ + if not hasattr(asyncio, "TaskGroup"): + pytest.skip("Skipped because asyncio.TaskGroup is unavailable.") + + tasks = asyncio.TaskGroup() + + async def handle_conn_wrapper( + reader: asyncio.StreamReader, + writer: asyncio.StreamWriter, + ) -> None: + try: + await handle_conn(reader, writer) + except Exception as e: + print(f"!!! TCP handler failed: {e}") + raise + finally: + if not writer.is_closing(): + writer.close() + await writer.wait_closed() + + async def _handle(r, w): + tasks.create_task(handle_conn_wrapper(r, w)) + + server = await asyncio.start_server(_handle, "127.0.0.1", 0, **server_args) await server.start_serving() - try: - yield server.sockets[0].getsockname() - finally: - server.close() + async with server: + async with tasks: + yield server.sockets[0].getsockname() -@pytest.mark.parametrize("mode", ["regular", "upstream", "err"]) +@pytest.mark.parametrize("mode", ["http", "https", "upstream", "err"]) @pytest.mark.parametrize("concurrency", [-1, 1]) -async def test_playback(mode, concurrency): - handler_ok = asyncio.Event() - +async def test_playback(tdata, mode, concurrency): async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): if mode == "err": - writer.close() - handler_ok.set() return req = await reader.readline() if mode == "upstream": @@ -36,18 +66,40 @@ async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): else: assert req == b"GET /path HTTP/1.1\r\n" req = await reader.readuntil(b"data") - assert req == (b"header: qvalue\r\n" b"content-length: 4\r\n" b"\r\n" b"data") + assert req == ( + b"header: qvalue\r\n" + b"content-length: 4\r\nHost: example.mitmproxy.org\r\n\r\n" + b"data" + ) writer.write(b"HTTP/1.1 204 No Content\r\n\r\n") await writer.drain() assert not await reader.read() - handler_ok.set() cp = ClientPlayback() ps = Proxyserver() - with taddons.context(cp, ps) as tctx: + tls = TlsConfig() + with taddons.context(cp, ps, tls) as tctx: tctx.configure(cp, client_replay_concurrency=concurrency) - async with tcp_server(handler) as addr: + server_args = {} + if mode == "https": + server_args["ssl"] = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + server_args["ssl"].load_cert_chain( + certfile=tdata.path( + "mitmproxy/net/data/verificationcerts/trusted-leaf.crt" + ), + keyfile=tdata.path( + "mitmproxy/net/data/verificationcerts/trusted-leaf.key" + ), + ) + tctx.configure( + tls, + ssl_verify_upstream_trusted_ca=tdata.path( + "mitmproxy/net/data/verificationcerts/trusted-root.crt" + ), + ) + + async with tcp_server(handler, **server_args) as addr: cp.running() flow = tflow.tflow(live=False) flow.request.content = b"data" @@ -57,25 +109,27 @@ async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): flow.request.host, flow.request.port = "address", 22 else: flow.request.host, flow.request.port = addr + if mode == "https": + flow.request.scheme = "https" + # Used for SNI + flow.request.host_header = "example.mitmproxy.org" cp.start_replay([flow]) assert cp.count() == 1 await asyncio.wait_for(cp.queue.join(), 5) - await asyncio.wait_for(handler_ok.wait(), 5) - cp.done() - if mode != "err": - assert flow.response.status_code == 204 + while cp.replay_tasks: + await asyncio.sleep(0.001) + if mode != "err": + assert flow.response.status_code == 204 + await cp.done() async def test_playback_https_upstream(): - handler_ok = asyncio.Event() - async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): conn_req = await reader.readuntil(b"\r\n\r\n") - assert conn_req == b"CONNECT address:22 HTTP/1.1\r\n\r\n" + assert conn_req == b"CONNECT address:22 HTTP/1.1\r\nHost: address:22\r\n\r\n" writer.write(b"HTTP/1.1 502 Bad Gateway\r\n\r\n") await writer.drain() assert not await reader.read() - handler_ok.set() cp = ClientPlayback() ps = Proxyserver() @@ -90,17 +144,17 @@ async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): cp.start_replay([flow]) assert cp.count() == 1 await asyncio.wait_for(cp.queue.join(), 5) - await asyncio.wait_for(handler_ok.wait(), 5) - cp.done() - assert flow.response is None - assert ( - str(flow.error) - == f"Upstream proxy {addr[0]}:{addr[1]} refused HTTP CONNECT request: 502 Bad Gateway" - ) + + assert flow.response is None + assert ( + str(flow.error) + == f"Upstream proxy {addr[0]}:{addr[1]} refused HTTP CONNECT request: 502 Bad Gateway" + ) + await cp.done() async def test_playback_crash(monkeypatch, caplog_async): - async def raise_err(): + async def raise_err(*_, **__): raise ValueError("oops") monkeypatch.setattr(ReplayHandler, "replay", raise_err) @@ -109,8 +163,9 @@ async def raise_err(): cp.running() cp.start_replay([tflow.tflow(live=False)]) await caplog_async.await_log("Client replay has crashed!") + assert "oops" in caplog_async.caplog.text assert cp.count() == 0 - cp.done() + await cp.done() def test_check(): diff --git a/test/mitmproxy/addons/test_command_history.py b/test/mitmproxy/addons/test_command_history.py index 915eddf3a6..7871e4809e 100644 --- a/test/mitmproxy/addons/test_command_history.py +++ b/test/mitmproxy/addons/test_command_history.py @@ -1,6 +1,6 @@ import os -from unittest.mock import patch from pathlib import Path +from unittest.mock import patch from mitmproxy.addons import command_history from mitmproxy.test import taddons diff --git a/test/mitmproxy/addons/test_comment.py b/test/mitmproxy/addons/test_comment.py index ba628cd49a..b3c9833bbf 100644 --- a/test/mitmproxy/addons/test_comment.py +++ b/test/mitmproxy/addons/test_comment.py @@ -1,5 +1,6 @@ -from mitmproxy.test import tflow, taddons from mitmproxy.addons.comment import Comment +from mitmproxy.test import taddons +from mitmproxy.test import tflow def test_comment(): diff --git a/test/mitmproxy/addons/test_core.py b/test/mitmproxy/addons/test_core.py index fc219769d3..be2cb1b139 100644 --- a/test/mitmproxy/addons/test_core.py +++ b/test/mitmproxy/addons/test_core.py @@ -1,8 +1,9 @@ +import pytest + +from mitmproxy import exceptions from mitmproxy.addons import core from mitmproxy.test import taddons from mitmproxy.test import tflow -from mitmproxy import exceptions -import pytest def test_set(): diff --git a/test/mitmproxy/addons/test_cut.py b/test/mitmproxy/addons/test_cut.py index ba045bdfd0..146bde170c 100644 --- a/test/mitmproxy/addons/test_cut.py +++ b/test/mitmproxy/addons/test_cut.py @@ -1,12 +1,14 @@ +from unittest import mock + +import pyperclip +import pytest + +from mitmproxy import certs +from mitmproxy import exceptions from mitmproxy.addons import cut from mitmproxy.addons import view -from mitmproxy import exceptions -from mitmproxy import certs from mitmproxy.test import taddons from mitmproxy.test import tflow -import pytest -import pyperclip -from unittest import mock def test_extract(tdata): @@ -56,9 +58,21 @@ def test_extract(tdata): assert "CERTIFICATE" in cut.extract("server_conn.certificate_list", tf) +def test_extract_websocket(): + tf = tflow.twebsocketflow(messages=True) + extracted_request_content = cut.extract("request.content", tf) + extracted_response_content = cut.extract("response.content", tf) + assert b"hello binary" in extracted_request_content + assert b"hello text" in extracted_request_content + assert b"it's me" in extracted_request_content + assert b"hello binary" in extracted_response_content + assert b"hello text" in extracted_response_content + assert b"it's me" in extracted_response_content + + def test_extract_str(): tf = tflow.tflow() - tf.request.raw_content = b"\xFF" + tf.request.raw_content = b"\xff" assert cut.extract_str("request.raw_content", tf) == r"b'\xff'" diff --git a/test/mitmproxy/addons/test_disable_h2c.py b/test/mitmproxy/addons/test_disable_h2c.py index 98ec0e3dda..4d55ecfe48 100644 --- a/test/mitmproxy/addons/test_disable_h2c.py +++ b/test/mitmproxy/addons/test_disable_h2c.py @@ -1,7 +1,8 @@ from mitmproxy import flow from mitmproxy.addons import disable_h2c -from mitmproxy.test import taddons, tutils +from mitmproxy.test import taddons from mitmproxy.test import tflow +from mitmproxy.test import tutils class TestDisableH2CleartextUpgrade: diff --git a/test/mitmproxy/addons/test_dns_resolver.py b/test/mitmproxy/addons/test_dns_resolver.py index db91894b44..11e97ed8ab 100644 --- a/test/mitmproxy/addons/test_dns_resolver.py +++ b/test/mitmproxy/addons/test_dns_resolver.py @@ -1,135 +1,154 @@ import asyncio -import ipaddress import socket -from typing import Callable +import sys +import typing import pytest +import mitmproxy_rs from mitmproxy import dns -from mitmproxy.addons import dns_resolver, proxyserver -from mitmproxy.connection import Address +from mitmproxy.addons import dns_resolver +from mitmproxy.addons import proxyserver from mitmproxy.proxy.mode_specs import ProxyMode -from mitmproxy.test import taddons, tflow, tutils +from mitmproxy.test import taddons +from mitmproxy.test import tflow +from mitmproxy.test import tutils -async def test_simple(monkeypatch): - monkeypatch.setattr( - dns_resolver, "resolve_message", lambda _, __: asyncio.sleep(0, "resp") - ) - +async def test_ignores_reverse_mode(): dr = dns_resolver.DnsResolver() with taddons.context(dr, proxyserver.Proxyserver()): f = tflow.tdnsflow() - await dr.dns_request(f) - assert f.response + f.client_conn.proxy_mode = ProxyMode.parse("dns") + assert dr._should_resolve(f) + + f.client_conn.proxy_mode = ProxyMode.parse("wireguard") + f.server_conn.address = ("10.0.0.53", 53) + assert dr._should_resolve(f) - f = tflow.tdnsflow() f.client_conn.proxy_mode = ProxyMode.parse("reverse:dns://8.8.8.8") - await dr.dns_request(f) - assert not f.response - - -class DummyLoop: - async def getnameinfo(self, socketaddr: Address, flags: int = 0): - assert flags == socket.NI_NAMEREQD - if socketaddr[0] in ("8.8.8.8", "2001:4860:4860::8888"): - return ("dns.google", "") - e = socket.gaierror() - e.errno = socket.EAI_NONAME - raise e - - async def getaddrinfo(self, host: str, port: int, *, family: int): - e = socket.gaierror() - e.errno = socket.EAI_NONAME - if family == socket.AF_INET: - if host == "dns.google": - return [(socket.AF_INET, None, None, None, ("8.8.8.8", port))] - elif family == socket.AF_INET6: - if host == "dns.google": - return [ - ( - socket.AF_INET6, - None, - None, - None, - ("2001:4860:4860::8888", port, None, None), - ) - ] - else: - e.errno = socket.EAI_FAMILY - raise e - - -async def test_resolve(): - async def fail_with(question: dns.Question, code: int): - with pytest.raises(dns_resolver.ResolveError) as ex: - await dns_resolver.resolve_question(question, DummyLoop()) - assert ex.value.response_code == code - - async def succeed_with( - question: dns.Question, check: Callable[[dns.ResourceRecord], bool] - ): - assert any( - map(check, await dns_resolver.resolve_question(question, DummyLoop())) + assert not dr._should_resolve(f) + + +def _err(): + raise RuntimeError("failed to get name servers") + + +async def test_name_servers(caplog, monkeypatch): + dr = dns_resolver.DnsResolver() + with taddons.context(dr) as tctx: + assert dr.name_servers() == mitmproxy_rs.dns.get_system_dns_servers() + + tctx.options.dns_name_servers = ["1.1.1.1"] + assert dr.name_servers() == ["1.1.1.1"] + + monkeypatch.setattr(mitmproxy_rs.dns, "get_system_dns_servers", _err) + tctx.options.dns_name_servers = [] + assert dr.name_servers() == [] + assert "Failed to get system dns servers" in caplog.text + + +async def lookup(name: str): + match name: + case "ipv4.example.com": + return ["1.2.3.4"] + case "ipv6.example.com": + return ["::1"] + case "no-a-records.example.com": + raise socket.gaierror(socket.EAI_NODATA) + case "no-network.example.com": + raise socket.gaierror(socket.EAI_AGAIN) + case _: + raise socket.gaierror(socket.EAI_NONAME) + + +async def getaddrinfo(host: str, *_, **__): + return [[None, None, None, None, [ip]] for ip in await lookup(host)] + + +Domain = typing.Literal[ + "nxdomain.example.com", + "no-a-records.example.com", + "no-network.example.com", + "txt.example.com", + "ipv4.example.com", + "ipv6.example.com", +] +# We use literals here instead of bools because that makes the test easier to parse. +HostsFile = typing.Literal["hosts", "no-hosts"] +NameServers = typing.Literal["nameservers", "no-nameservers"] + + +@pytest.mark.parametrize("hosts_file", typing.get_args(HostsFile)) +@pytest.mark.parametrize("name_servers", typing.get_args(NameServers)) +@pytest.mark.parametrize("domain", typing.get_args(Domain)) +async def test_lookup( + domain: Domain, hosts_file: HostsFile, name_servers: NameServers, monkeypatch +): + if name_servers == "nameservers": + monkeypatch.setattr( + mitmproxy_rs.dns, "get_system_dns_servers", lambda: ["8.8.8.8"] + ) + monkeypatch.setattr( + mitmproxy_rs.dns.DnsResolver, "lookup_ipv4", lambda _, name: lookup(name) + ) + monkeypatch.setattr( + mitmproxy_rs.dns.DnsResolver, "lookup_ipv6", lambda _, name: lookup(name) + ) + else: + monkeypatch.setattr(mitmproxy_rs.dns, "get_system_dns_servers", lambda: []) + monkeypatch.setattr(asyncio.get_running_loop(), "getaddrinfo", getaddrinfo) + + dr = dns_resolver.DnsResolver() + match domain: + case "txt.example.com": + typ = dns.types.TXT + case "ipv6.example.com": + typ = dns.types.AAAA + case _: + typ = dns.types.A + + with taddons.context(dr) as tctx: + tctx.options.dns_use_hosts_file = hosts_file == "hosts" + req = tutils.tdnsreq( + questions=[ + dns.Question(domain, typ, dns.classes.IN), + ] ) + flow = tflow.tdnsflow(req=req) + await dr.dns_request(flow) - await fail_with( - dns.Question("dns.google", dns.types.A, dns.classes.CH), - dns.response_codes.NOTIMP, - ) - await fail_with( - dns.Question("not.exists", dns.types.A, dns.classes.IN), - dns.response_codes.NXDOMAIN, - ) - await fail_with( - dns.Question("dns.google", dns.types.SOA, dns.classes.IN), - dns.response_codes.NOTIMP, - ) - await fail_with( - dns.Question("totally.invalid", dns.types.PTR, dns.classes.IN), - dns.response_codes.FORMERR, - ) - await fail_with( - dns.Question("invalid.in-addr.arpa", dns.types.PTR, dns.classes.IN), - dns.response_codes.FORMERR, - ) - await fail_with( - dns.Question("0.0.0.1.in-addr.arpa", dns.types.PTR, dns.classes.IN), - dns.response_codes.NXDOMAIN, - ) - - await succeed_with( - dns.Question("dns.google", dns.types.A, dns.classes.IN), - lambda rr: rr.ipv4_address == ipaddress.IPv4Address("8.8.8.8"), - ) - await succeed_with( - dns.Question("dns.google", dns.types.AAAA, dns.classes.IN), - lambda rr: rr.ipv6_address == ipaddress.IPv6Address("2001:4860:4860::8888"), - ) - await succeed_with( - dns.Question("8.8.8.8.in-addr.arpa", dns.types.PTR, dns.classes.IN), - lambda rr: rr.domain_name == "dns.google", - ) - await succeed_with( - dns.Question( - "8.8.8.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.6.8.4.0.6.8.4.1.0.0.2.ip6.arpa", - dns.types.PTR, - dns.classes.IN, - ), - lambda rr: rr.domain_name == "dns.google", - ) - - req = tutils.tdnsreq() - req.query = False - assert ( - await dns_resolver.resolve_message(req, DummyLoop()) - ).response_code == dns.response_codes.REFUSED - req.query = True - req.op_code = dns.op_codes.IQUERY - assert ( - await dns_resolver.resolve_message(req, DummyLoop()) - ).response_code == dns.response_codes.NOTIMP - req.op_code = dns.op_codes.QUERY - resp = await dns_resolver.resolve_message(req, DummyLoop()) - assert resp.response_code == dns.response_codes.NOERROR - assert filter(lambda rr: str(rr.ipv4_address) == "8.8.8.8", resp.answers) + match (domain, name_servers, hosts_file): + case [_, "no-nameservers", "no-hosts"]: + assert flow.error + case ["nxdomain.example.com", _, _]: + assert flow.response.response_code == dns.response_codes.NXDOMAIN + case ["no-network.example.com", _, _]: + assert flow.response.response_code == dns.response_codes.SERVFAIL + case ["no-a-records.example.com", _, _]: + if sys.platform == "win32": + # On Windows, EAI_NONAME and EAI_NODATA are the same constant (11001)... + assert flow.response.response_code == dns.response_codes.NXDOMAIN + else: + assert flow.response.response_code == dns.response_codes.NOERROR + assert not flow.response.answers + case ["txt.example.com", "nameservers", _]: + assert flow.server_conn.address == ("8.8.8.8", 53) + case ["txt.example.com", "no-nameservers", _]: + assert flow.error + case ["ipv4.example.com", "nameservers", _]: + assert flow.response.answers[0].data == b"\x01\x02\x03\x04" + case ["ipv4.example.com", "no-nameservers", "hosts"]: + assert flow.response.answers[0].data == b"\x01\x02\x03\x04" + case ["ipv6.example.com", "nameservers", _]: + assert ( + flow.response.answers[0].data + == b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01" + ) + case ["ipv6.example.com", "no-nameservers", "hosts"]: + assert ( + flow.response.answers[0].data + == b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01" + ) + case other: + typing.assert_never(other) diff --git a/test/mitmproxy/addons/test_dumper.py b/test/mitmproxy/addons/test_dumper.py index b861c8d9e2..9db3bd1cbd 100644 --- a/test/mitmproxy/addons/test_dumper.py +++ b/test/mitmproxy/addons/test_dumper.py @@ -7,6 +7,7 @@ from mitmproxy import exceptions from mitmproxy.addons import dumper from mitmproxy.http import Headers +from mitmproxy.net.dns import response_codes from mitmproxy.test import taddons from mitmproxy.test import tflow from mitmproxy.test import tutils @@ -96,7 +97,7 @@ def test_simple(): def test_echo_body(): f = tflow.tflow(resp=True) f.response.headers["content-type"] = "text/html" - f.response.content = b"foo bar voing\n" * 100 + f.response.content = b"foo bar voing\n" * 600 sio = io.StringIO() d = dumper.Dumper(sio) @@ -107,6 +108,21 @@ def test_echo_body(): assert "cut off" in t +def test_echo_body_custom_cutoff(): + f = tflow.tflow(resp=True) + f.response.headers["content-type"] = "text/html" + f.response.content = b"foo bar voing\n" * 4 + + sio = io.StringIO() + d = dumper.Dumper(sio) + with taddons.context(d) as ctx: + ctx.configure(d, flow_detail=3) + ctx.configure(d, content_view_lines_cutoff=3) + d._echo_message(f.response, f) + t = sio.getvalue() + assert "cut off" in t + + def test_echo_trailer(): sio = io.StringIO() d = dumper.Dumper(sio) @@ -117,7 +133,7 @@ def test_echo_trailer(): f.request.headers["content-type"] = "text/html" f.request.headers["transfer-encoding"] = "chunked" f.request.headers["trailer"] = "my-little-request-trailer" - f.request.content = b"some request content\n" * 100 + f.request.content = b"some request content\n" * 600 f.request.trailers = Headers( [(b"my-little-request-trailer", b"foobar-request-trailer")] ) @@ -226,6 +242,12 @@ def test_dns(): assert "8.8.8.8" in sio.getvalue() sio.truncate(0) + f = tflow.tdnsflow() + f.response = f.request.fail(response_codes.NOTIMP) + d.dns_response(f) + assert "NOTIMP" in sio.getvalue() + sio.truncate(0) + f = tflow.tdnsflow(err=True) d.dns_error(f) assert "error" in sio.getvalue() @@ -279,6 +301,27 @@ def test_http2(): assert "HTTP/2.0 200 OK" in sio.getvalue() +def test_quic(): + sio = io.StringIO() + d = dumper.Dumper(sio) + with taddons.context(d): + f = tflow.ttcpflow() + f.client_conn.tls_version = "QUICv1" + # TODO: This should not be metadata, this should be typed attributes. + f.metadata["quic_stream_id_client"] = 1 + f.metadata["quic_stream_id_server"] = 1 + d.tcp_message(f) + assert "quic stream 1" in sio.getvalue() + + f2 = tflow.tudpflow() + f2.client_conn.tls_version = "QUICv1" + # TODO: This should not be metadata, this should be typed attributes. + f2.metadata["quic_stream_id_client"] = 1 + f2.metadata["quic_stream_id_server"] = 1 + d.udp_message(f2) + assert "quic stream 1" in sio.getvalue() + + def test_styling(): sio = io.StringIO() diff --git a/test/mitmproxy/addons/test_errorcheck.py b/test/mitmproxy/addons/test_errorcheck.py index 16de948007..bc71f6aaf1 100644 --- a/test/mitmproxy/addons/test_errorcheck.py +++ b/test/mitmproxy/addons/test_errorcheck.py @@ -1,22 +1,43 @@ +import logging + import pytest from mitmproxy.addons.errorcheck import ErrorCheck from mitmproxy.tools import main -def test_errorcheck(tdata, capsys): +@pytest.mark.parametrize("run_main", [main.mitmdump, main.mitmproxy]) +def test_errorcheck(tdata, capsys, run_main): """Integration test: Make sure that we catch errors on startup an exit.""" with pytest.raises(SystemExit): - main.mitmproxy( + run_main( [ + "-n", "-s", tdata.path("mitmproxy/data/addonscripts/load_error.py"), ] ) - assert "Error on startup" in capsys.readouterr().err + assert "Error logged during startup" in capsys.readouterr().err async def test_no_error(): e = ErrorCheck() await e.shutdown_if_errored() e.finish() + + +async def test_error_message(capsys): + e = ErrorCheck() + logging.error("wat") + logging.error("wat") + with pytest.raises(SystemExit): + await e.shutdown_if_errored() + assert "Errors logged during startup, exiting..." in capsys.readouterr().err + + +async def test_repeat_error_on_stderr(capsys): + e = ErrorCheck(repeat_errors_on_stderr=True) + logging.error("wat") + with pytest.raises(SystemExit): + await e.shutdown_if_errored() + assert "Error logged during startup:\nwat" in capsys.readouterr().err diff --git a/test/mitmproxy/addons/test_export.py b/test/mitmproxy/addons/test_export.py index 17187d85e4..af1fcdf937 100644 --- a/test/mitmproxy/addons/test_export.py +++ b/test/mitmproxy/addons/test_export.py @@ -1,15 +1,15 @@ import os import shlex +from unittest import mock -import pytest import pyperclip +import pytest from mitmproxy import exceptions from mitmproxy.addons import export # heh +from mitmproxy.test import taddons from mitmproxy.test import tflow from mitmproxy.test import tutils -from mitmproxy.test import taddons -from unittest import mock @pytest.fixture @@ -58,6 +58,11 @@ def udp_flow(): return tflow.tudpflow() +@pytest.fixture +def websocket_flow(): + return tflow.twebsocketflow() + + @pytest.fixture(scope="module") def export_curl(): e = export.Export() @@ -217,6 +222,11 @@ def test_udp(self, udp_flow): ): export.raw(udp_flow) + def test_websocket(self, websocket_flow): + assert b"hello binary" in export.raw(websocket_flow) + assert b"hello text" in export.raw(websocket_flow) + assert b"it's me" in export.raw(websocket_flow) + class TestRawRequest: def test_get(self, get_request): @@ -286,6 +296,10 @@ def test_export(tmp_path) -> None: assert qr(f) os.unlink(f) + e.file("raw", tflow.twebsocketflow(), f) + assert qr(f) + os.unlink(f) + @pytest.mark.parametrize( "exception, log_message", diff --git a/test/mitmproxy/addons/test_intercept.py b/test/mitmproxy/addons/test_intercept.py index 3cabfda283..57b16bad13 100644 --- a/test/mitmproxy/addons/test_intercept.py +++ b/test/mitmproxy/addons/test_intercept.py @@ -1,7 +1,7 @@ import pytest -from mitmproxy.addons import intercept from mitmproxy import exceptions +from mitmproxy.addons import intercept from mitmproxy.test import taddons from mitmproxy.test import tflow @@ -89,3 +89,17 @@ async def test_udp(): f = tflow.tudpflow() await tctx.cycle(r, f) assert not f.intercepted + + +async def test_websocket_message(): + r = intercept.Intercept() + with taddons.context(r) as tctx: + tctx.configure(r, intercept='~b "hello binary"') + f = tflow.twebsocketflow() + await tctx.cycle(r, f) + assert f.intercepted + + tctx.configure(r, intercept_active=False) + f = tflow.twebsocketflow() + await tctx.cycle(r, f) + assert not f.intercepted diff --git a/test/mitmproxy/addons/test_keepserving.py b/test/mitmproxy/addons/test_keepserving.py index 99459bf37b..8ee85f8ce8 100644 --- a/test/mitmproxy/addons/test_keepserving.py +++ b/test/mitmproxy/addons/test_keepserving.py @@ -1,8 +1,8 @@ import asyncio +from mitmproxy import command from mitmproxy.addons import keepserving from mitmproxy.test import taddons -from mitmproxy import command class Dummy: diff --git a/test/mitmproxy/addons/test_maplocal.py b/test/mitmproxy/addons/test_maplocal.py index 917664718c..7edf4e8e92 100644 --- a/test/mitmproxy/addons/test_maplocal.py +++ b/test/mitmproxy/addons/test_maplocal.py @@ -3,10 +3,12 @@ import pytest -from mitmproxy.addons.maplocal import MapLocal, MapLocalSpec, file_candidates -from mitmproxy.utils.spec import parse_spec +from mitmproxy.addons.maplocal import file_candidates +from mitmproxy.addons.maplocal import MapLocal +from mitmproxy.addons.maplocal import MapLocalSpec from mitmproxy.test import taddons from mitmproxy.test import tflow +from mitmproxy.utils.spec import parse_spec @pytest.mark.parametrize( diff --git a/test/mitmproxy/addons/test_mapremote.py b/test/mitmproxy/addons/test_mapremote.py index 2aff3468d9..f20ca20efd 100644 --- a/test/mitmproxy/addons/test_mapremote.py +++ b/test/mitmproxy/addons/test_mapremote.py @@ -27,6 +27,16 @@ def test_simple(self): mr.request(f) assert f.request.url == "https://mitmproxy.org/img/test.jpg" + def test_host_header(self): + mr = mapremote.MapRemote() + with taddons.context(mr) as tctx: + tctx.configure(mr, map_remote=["|http://[^/]+|http://example.com:4444"]) + f = tflow.tflow() + f.request.url = b"http://example.org/example" + f.request.headers["Host"] = "example.org" + mr.request(f) + assert f.request.headers.get("Host", "") == "example.com:4444" + def test_is_killed(self): mr = mapremote.MapRemote() with taddons.context(mr) as tctx: diff --git a/test/mitmproxy/addons/test_modifybody.py b/test/mitmproxy/addons/test_modifybody.py index 25f3c3b420..3d52900169 100644 --- a/test/mitmproxy/addons/test_modifybody.py +++ b/test/mitmproxy/addons/test_modifybody.py @@ -1,6 +1,7 @@ import pytest from mitmproxy.addons import modifybody +from mitmproxy.addons import proxyserver from mitmproxy.test import taddons from mitmproxy.test import tflow from mitmproxy.test.tutils import tresp @@ -14,6 +15,13 @@ def test_configure(self): with pytest.raises(Exception, match="Cannot parse modify_body"): tctx.configure(mb, modify_body=["/"]) + def test_warn_conflict(self, caplog): + caplog.set_level("DEBUG") + mb = modifybody.ModifyBody() + with taddons.context(mb, proxyserver.Proxyserver()) as tctx: + tctx.configure(mb, stream_large_bodies="3m", modify_body=["one/two/three"]) + assert "Streamed bodies will not be modified" in caplog.text + def test_simple(self): mb = modifybody.ModifyBody() with taddons.context(mb) as tctx: diff --git a/test/mitmproxy/addons/test_modifyheaders.py b/test/mitmproxy/addons/test_modifyheaders.py index 430c824b9b..d5bf8936b7 100644 --- a/test/mitmproxy/addons/test_modifyheaders.py +++ b/test/mitmproxy/addons/test_modifyheaders.py @@ -1,6 +1,7 @@ import pytest -from mitmproxy.addons.modifyheaders import parse_modify_spec, ModifyHeaders +from mitmproxy.addons.modifyheaders import ModifyHeaders +from mitmproxy.addons.modifyheaders import parse_modify_spec from mitmproxy.test import taddons from mitmproxy.test import tflow from mitmproxy.test.tutils import tresp @@ -40,48 +41,48 @@ def test_modify_headers(self): tctx.configure(mh, modify_headers=["/~q/one/two", "/~s/one/three"]) f = tflow.tflow() f.request.headers["one"] = "xxx" - mh.request(f) + mh.requestheaders(f) assert f.request.headers["one"] == "two" f = tflow.tflow(resp=True) f.response.headers["one"] = "xxx" - mh.response(f) + mh.responseheaders(f) assert f.response.headers["one"] == "three" tctx.configure(mh, modify_headers=["/~s/one/two", "/~s/one/three"]) f = tflow.tflow(resp=True) f.request.headers["one"] = "xxx" f.response.headers["one"] = "xxx" - mh.response(f) + mh.responseheaders(f) assert f.response.headers.get_all("one") == ["two", "three"] tctx.configure(mh, modify_headers=["/~q/one/two", "/~q/one/three"]) f = tflow.tflow() f.request.headers["one"] = "xxx" - mh.request(f) + mh.requestheaders(f) assert f.request.headers.get_all("one") == ["two", "three"] # test removal of existing headers tctx.configure(mh, modify_headers=["/~q/one/", "/~s/one/"]) f = tflow.tflow() f.request.headers["one"] = "xxx" - mh.request(f) + mh.requestheaders(f) assert "one" not in f.request.headers f = tflow.tflow(resp=True) f.response.headers["one"] = "xxx" - mh.response(f) + mh.responseheaders(f) assert "one" not in f.response.headers tctx.configure(mh, modify_headers=["/one/"]) f = tflow.tflow() f.request.headers["one"] = "xxx" - mh.request(f) + mh.requestheaders(f) assert "one" not in f.request.headers f = tflow.tflow(resp=True) f.response.headers["one"] = "xxx" - mh.response(f) + mh.responseheaders(f) assert "one" not in f.response.headers # test modifying a header that is also part of the filter expression @@ -94,7 +95,7 @@ def test_modify_headers(self): ) f = tflow.tflow() f.request.headers["user-agent"] = "Hello, it's me, Mozilla" - mh.request(f) + mh.requestheaders(f) assert "Definitely not Mozilla ;)" == f.request.headers["user-agent"] @pytest.mark.parametrize("take", [True, False]) @@ -105,13 +106,13 @@ def test_taken(self, take): f = tflow.tflow() if take: f.response = tresp() - mh.request(f) + mh.requestheaders(f) assert (f.request.headers["content-length"] == "42") ^ take f = tflow.tflow(resp=True) if take: f.kill() - mh.response(f) + mh.responseheaders(f) assert (f.response.headers["content-length"] == "42") ^ take @@ -124,7 +125,7 @@ def test_simple(self, tmpdir): tctx.configure(mh, modify_headers=["/~q/one/@" + str(tmpfile)]) f = tflow.tflow() f.request.headers["one"] = "xxx" - mh.request(f) + mh.requestheaders(f) assert f.request.headers["one"] == "two" async def test_nonexistent(self, tmpdir, caplog): @@ -141,5 +142,5 @@ async def test_nonexistent(self, tmpdir, caplog): tmpfile.remove() f = tflow.tflow() f.request.content = b"foo" - mh.request(f) + mh.requestheaders(f) assert "Could not read" in caplog.text diff --git a/test/mitmproxy/addons/test_next_layer.py b/test/mitmproxy/addons/test_next_layer.py index 8b592cb5b1..10dfa3b6b5 100644 --- a/test/mitmproxy/addons/test_next_layer.py +++ b/test/mitmproxy/addons/test_next_layer.py @@ -1,23 +1,39 @@ -from typing import Optional +from __future__ import annotations + +import dataclasses +import logging +from collections.abc import Sequence +from dataclasses import dataclass +from functools import partial from unittest.mock import MagicMock import pytest -from mitmproxy import connection +from mitmproxy.addons.next_layer import _starts_like_quic +from mitmproxy.addons.next_layer import NeedsMoreData from mitmproxy.addons.next_layer import NextLayer +from mitmproxy.addons.next_layer import stack_match +from mitmproxy.connection import Address +from mitmproxy.connection import Client +from mitmproxy.connection import TlsVersion +from mitmproxy.connection import TransportProtocol +from mitmproxy.proxy.context import Context +from mitmproxy.proxy.layer import Layer +from mitmproxy.proxy.layers import ClientQuicLayer +from mitmproxy.proxy.layers import ClientTLSLayer +from mitmproxy.proxy.layers import DNSLayer +from mitmproxy.proxy.layers import HttpLayer +from mitmproxy.proxy.layers import modes +from mitmproxy.proxy.layers import RawQuicLayer +from mitmproxy.proxy.layers import ServerQuicLayer +from mitmproxy.proxy.layers import ServerTLSLayer +from mitmproxy.proxy.layers import TCPLayer +from mitmproxy.proxy.layers import UDPLayer from mitmproxy.proxy.layers.http import HTTPMode -from mitmproxy.proxy import context, layer, layers +from mitmproxy.proxy.layers.http import HttpStream +from mitmproxy.proxy.layers.tls import HTTP1_ALPNS +from mitmproxy.proxy.mode_specs import ProxyMode from mitmproxy.test import taddons -from mitmproxy.test import tflow - - -@pytest.fixture -def tctx(): - context.Context( - connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), - tctx.options, - ) - client_hello_no_extensions = bytes.fromhex( "1603030065" # record header @@ -36,164 +52,845 @@ def tctx(): "170018" ) - dtls_client_hello_with_extensions = bytes.fromhex( - "16fefd00000000000000000085" # record layer - "010000790000000000000079" # handshake layer + "16fefd00000000000000000085" # record layer + "010000790000000000000079" # handshake layer "fefd62bf0e0bf809df43e7669197be831919878b1a72c07a584d3c0a8ca6665878010000000cc02bc02fc00ac014c02cc0" "3001000043000d0010000e0403050306030401050106010807ff01000100000a00080006001d00170018000b00020100001" "7000000000010000e00000b6578616d706c652e636f6d" ) +quic_client_hello = bytes.fromhex( + "ca0000000108c0618c84b54541320823fcce946c38d8210044e6a93bbb283593f75ffb6f2696b16cfdcb5b1255" + "577b2af5fc5894188c9568bc65eef253faf7f0520e41341cfa81d6aae573586665ce4e1e41676364820402feec" + "a81f3d22dbb476893422069066104a43e121c951a08c53b83f960becf99cf5304d5bc5346f52f472bd1a04d192" + "0bae025064990d27e5e4c325ac46121d3acadebe7babdb96192fb699693d65e2b2e21c53beeb4f40b50673a2f6" + "c22091cb7c76a845384fedee58df862464d1da505a280bfef91ca83a10bebbcb07855219dbc14aecf8a48da049" + "d03c77459b39d5355c95306cd03d6bdb471694fa998ca3b1f875ce87915b88ead15c5d6313a443f39aad808922" + "57ddfa6b4a898d773bb6fb520ede47ebd59d022431b1054a69e0bbbdf9f0fb32fc8bcc4b6879dd8cd5389474b1" + "99e18333e14d0347740a11916429a818bb8d93295d36e99840a373bb0e14c8b3adcf5e2165e70803f15316fd5e" + "5eeec04ae68d98f1adb22c54611c80fcd8ece619dbdf97b1510032ec374b7a71f94d9492b8b8cb56f56556dd97" + "edf1e50fa90e868ff93636a365678bdf3ee3f8e632588cd506b6f44fbfd4d99988238fbd5884c98f6a124108c1" + "878970780e42b111e3be6215776ef5be5a0205915e6d720d22c6a81a475c9e41ba94e4983b964cb5c8e1f40607" + "76d1d8d1adcef7587ea084231016bd6ee2643d11a3a35eb7fe4cca2b3f1a4b21e040b0d426412cca6c4271ea63" + "fb54ed7f57b41cd1af1be5507f87ea4f4a0c997367e883291de2f1b8a49bdaa52bae30064351b1139703400730" + "18a4104344ec6b4454b50a42e804bc70e78b9b3c82497273859c82ed241b643642d76df6ceab8f916392113a62" + "b231f228c7300624d74a846bec2f479ab8a8c3461f91c7bf806236e3bd2f54ba1ef8e2a1e0bfdde0c5ad227f7d" + "364c52510b1ade862ce0c8d7bd24b6d7d21c99b34de6d177eb3d575787b2af55060d76d6c2060befbb7953a816" + "6f66ad88ecf929dbb0ad3a16cf7dfd39d925e0b4b649c6d0c07ad46ed0229c17fb6a1395f16e1b138aab3af760" + "2b0ac762c4f611f7f3468997224ffbe500a7c53f92f65e41a3765a9f1d7e3f78208f5b4e147962d8c97d6c1a80" + "91ffc36090b2043d71853616f34c2185dc883c54ab6d66e10a6c18e0b9a4742597361f8554a42da3373241d0c8" + "54119bfadccffaf2335b2d97ffee627cb891bda8140a39399f853da4859f7e19682e152243efbaffb662edd19b" + "3819a74107c7dbe05ecb32e79dcdb1260f153b1ef133e978ccca3d9e400a7ed6c458d77e2956d2cb897b7a298b" + "fe144b5defdc23dfd2adf69f1fb0917840703402d524987ae3b1dcb85229843c9a419ef46e1ba0ba7783f2a2ec" + "d057a57518836aef2a7839ebd3688da98b54c942941f642e434727108d59ea25875b3050ca53d4637c76cbcbb9" + "e972c2b0b781131ee0a1403138b55486fe86bbd644920ee6aa578e3bab32d7d784b5c140295286d90c99b14823" + "1487f7ea64157001b745aa358c9ea6bec5a8d8b67a7534ec1f7648ff3b435911dfc3dff798d32fbf2efe2c1fcc" + "278865157590572387b76b78e727d3e7682cb501cdcdf9a0f17676f99d9aa67f10edccc9a92080294e88bf28c2" + "a9f32ae535fdb27fff7706540472abb9eab90af12b2bea005da189874b0ca69e6ae1690a6f2adf75be3853c94e" + "fd8098ed579c20cb37be6885d8d713af4ba52958cee383089b98ed9cb26e11127cf88d1b7d254f15f7903dd7ed" + "297c0013924e88248684fe8f2098326ce51aa6e5" +) -class TestNextLayer: - def test_configure(self): - nl = NextLayer() - with taddons.context(nl) as tctx: - with pytest.raises(Exception, match="mutually exclusive"): - tctx.configure( - nl, allow_hosts=["example.org"], ignore_hosts=["example.com"] - ) +quic_short_header_packet = bytes.fromhex( + "52e23539dde270bb19f7a8b63b7bcf3cdacf7d3dc68a7e00318bfa2dac3bad12cb7d78112efb5bcb1ee8e0b347" + "641cccd2736577d0178b4c4c4e97a8e9e2af1d28502e58c4882223e70c4d5124c4b016855340e982c5c453d61d" + "7d0720be075fce3126de3f0d54dc059150e0f80f1a8db5e542eb03240b0a1db44a322fb4fd3c6f2e054b369e14" + "5a5ff925db617d187ec65a7f00d77651968e74c1a9ddc3c7fab57e8df821b07e103264244a3a03d17984e29933" +) - def test_ignore_connection(self): - nl = NextLayer() - with taddons.context(nl) as tctx: - assert not nl.ignore_connection(("example.com", 443), b"") +dns_query = bytes.fromhex("002a01000001000000000000076578616d706c6503636f6d0000010001") - tctx.configure(nl, ignore_hosts=["example.com"]) - assert nl.ignore_connection(("example.com", 443), b"") - assert nl.ignore_connection(("example.com", 1234), b"") - assert nl.ignore_connection(("com", 443), b"") is False - assert nl.ignore_connection(None, b"") is False - assert nl.ignore_connection(None, client_hello_no_extensions) is False - assert nl.ignore_connection(None, client_hello_with_extensions) - assert nl.ignore_connection(None, client_hello_with_extensions[:-5]) is None - # invalid clienthello - assert ( - nl.ignore_connection( - None, client_hello_no_extensions[:9] + b"\x00" * 200 - ) - is False - ) - # different server name and SNI - assert nl.ignore_connection(("decoy", 1234), client_hello_with_extensions) - - tctx.configure(nl, ignore_hosts=[], allow_hosts=["example.com"]) - assert nl.ignore_connection(("example.com", 443), b"") is False - assert nl.ignore_connection(("example.org", 443), b"") - # different server name and SNI - assert ( - nl.ignore_connection(("decoy", 1234), client_hello_with_extensions) - is False - ) +# Custom protocol with just base64-encoded messages +# https://github.com/mitmproxy/mitmproxy/pull/7087 +custom_base64_proto = b"AAAAAAAAAAAAAAAAAAAAAA==\n" - def test_next_layer(self, monkeypatch): - ctx = MagicMock() - ctx.client.transport_protocol = "tcp" - nl_layer = layer.NextLayer(ctx) - monkeypatch.setattr(nl_layer, "data_client", lambda: b"\x16\x03\x03") - nl = NextLayer() +http_get = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" +http_get_absolute = b"GET http://example.com/ HTTP/1.1\r\n\r\n" + +http_connect = b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n" - with taddons.context(nl): - nl.next_layer(nl_layer) - assert nl_layer.layer - def test_next_layer2(self): +class TestNextLayer: + @pytest.mark.parametrize( + "ignore, allow, transport_protocol, server_address, data_client, result", + [ + # ignore + pytest.param( + [], [], "tcp", "example.com", b"", False, id="nothing ignored" + ), + pytest.param( + ["example.com"], [], "tcp", "example.com", b"", True, id="address" + ), + pytest.param( + ["192.0.2.1"], [], "tcp", "example.com", b"", True, id="ip address" + ), + pytest.param( + ["2001:db8::1"], + [], + "tcp", + "ipv6.example.com", + b"", + True, + id="ipv6 address", + ), + pytest.param( + ["example.com:443"], + [], + "tcp", + "example.com", + b"", + True, + id="port matches", + ), + pytest.param( + ["example.com:123"], + [], + "tcp", + "example.com", + b"", + False, + id="port does not match", + ), + pytest.param( + ["example.com"], + [], + "tcp", + "192.0.2.1", + http_get, + True, + id="http host header", + ), + pytest.param( + ["example.com"], + [], + "tcp", + "192.0.2.1", + http_get.replace(b"Host", b"X-Host"), + False, + id="http host header missing", + ), + pytest.param( + ["example.com"], + [], + "tcp", + "192.0.2.1", + http_get.split(b"\r\n", 1)[0], + NeedsMoreData, + id="incomplete http host header", + ), + pytest.param( + ["example.com"], + [], + "tcp", + "com", + b"", + False, + id="partial address match", + ), + pytest.param( + ["example.com"], + [], + "tcp", + None, + b"", + False, + id="no destination info", + ), + pytest.param( + ["example.com"], + [], + "tcp", + None, + client_hello_no_extensions, + False, + id="no sni", + ), + pytest.param( + ["example.com"], + [], + "tcp", + "192.0.2.1", + client_hello_with_extensions, + True, + id="sni", + ), + pytest.param( + ["example.com"], + [], + "tcp", + "192.0.2.1", + client_hello_with_extensions[:-5], + NeedsMoreData, + id="incomplete client hello", + ), + pytest.param( + ["example.com"], + [], + "tcp", + "192.0.2.1", + client_hello_no_extensions[:9] + b"\x00" * 200, + False, + id="invalid client hello", + ), + pytest.param( + ["example.com"], + [], + "tcp", + "decoy", + client_hello_with_extensions, + True, + id="sni mismatch", + ), + pytest.param( + ["example.com"], + [], + "udp", + "192.0.2.1", + dtls_client_hello_with_extensions, + True, + id="dtls sni", + ), + pytest.param( + ["example.com"], + [], + "udp", + "192.0.2.1", + dtls_client_hello_with_extensions[:-5], + NeedsMoreData, + id="incomplete dtls client hello", + ), + pytest.param( + ["example.com"], + [], + "udp", + "192.0.2.1", + dtls_client_hello_with_extensions[:9] + b"\x00" * 200, + False, + id="invalid dtls client hello", + ), + pytest.param( + ["example.com"], + [], + "udp", + "192.0.2.1", + quic_client_hello, + True, + id="quic sni", + ), + # allow + pytest.param( + [], + ["example.com"], + "tcp", + "example.com", + b"", + False, + id="allow: allow", + ), + pytest.param( + [], + ["example.com"], + "tcp", + "example.org", + b"", + True, + id="allow: ignore", + ), + pytest.param( + [], + ["example.com"], + "tcp", + "192.0.2.1", + client_hello_with_extensions, + False, + id="allow: sni", + ), + pytest.param( + [], + ["existing-sni.example"], + "tcp", + "192.0.2.1", + b"", + False, + id="allow: sni from parent layer", + ), + pytest.param( + [], + ["example.com"], + "tcp", + "decoy", + client_hello_with_extensions, + False, + id="allow: sni mismatch", + ), + # allow with ignore + pytest.param( + ["binary.example.com"], + ["example.com"], + "tcp", + "example.com", + b"", + False, + id="allow+ignore: allowed and not ignored", + ), + pytest.param( + ["binary.example.com"], + ["example.com"], + "tcp", + "binary.example.org", + b"", + True, + id="allow+ignore: allowed but ignored", + ), + ], + ) + def test_ignore_connection( + self, + ignore: list[str], + allow: list[str], + transport_protocol: TransportProtocol, + server_address: str, + data_client: bytes, + result: bool | type[NeedsMoreData], + ): nl = NextLayer() - ctx = MagicMock() - ctx.client.alpn = None - ctx.server.address = ("example.com", 443) - ctx.client.transport_protocol = "tcp" with taddons.context(nl) as tctx: - ctx.layers = [layers.modes.HttpProxy(ctx)] - - assert nl._next_layer(ctx, b"", b"") is None - - tctx.configure(nl, ignore_hosts=["example.com"]) - assert isinstance(nl._next_layer(ctx, b"123", b""), layers.TCPLayer) - assert nl._next_layer(ctx, client_hello_no_extensions[:10], b"") is None - - tctx.configure(nl, ignore_hosts=[]) - assert isinstance( - nl._next_layer(ctx, client_hello_no_extensions, b""), - layers.ServerTLSLayer, - ) - assert isinstance(ctx.layers[-1], layers.ClientTLSLayer) - - ctx.layers = [layers.modes.HttpProxy(ctx)] - assert isinstance( - nl._next_layer(ctx, client_hello_no_extensions, b""), - layers.ClientTLSLayer, + if ignore: + tctx.configure(nl, ignore_hosts=ignore) + if allow: + tctx.configure(nl, allow_hosts=allow) + ctx = Context( + Client( + peername=("192.168.0.42", 51234), + sockname=("0.0.0.0", 8080), + sni="existing-sni.example", + ), + tctx.options, ) + ctx.client.transport_protocol = transport_protocol + if server_address: + ctx.server.address = (server_address, 443) + ctx.server.peername = ( + ("2001:db8::1", 443, 0, 0) + if server_address.startswith("ipv6") + else ("192.0.2.1", 443) + ) + if result is NeedsMoreData: + with pytest.raises(NeedsMoreData): + nl._ignore_connection(ctx, data_client, b"") + else: + assert nl._ignore_connection(ctx, data_client, b"") is result - ctx.layers = [layers.modes.HttpProxy(ctx)] - assert isinstance( - nl._next_layer(ctx, b"GET http://example.com/ HTTP/1.1\r\n", b""), - layers.HttpLayer, - ) - assert ctx.layers[-1].mode == HTTPMode.regular + def test_show_ignored_hosts(self, monkeypatch): + nl = NextLayer() - ctx.layers = [layers.modes.HttpUpstreamProxy(ctx)] - assert isinstance( - nl._next_layer(ctx, b"GET http://example.com/ HTTP/1.1\r\n", b""), - layers.HttpLayer, + with taddons.context(nl) as tctx: + m = MagicMock() + m.context = Context( + Client(peername=("192.168.0.42", 51234), sockname=("0.0.0.0", 8080)), + tctx.options, ) - assert ctx.layers[-1].mode == HTTPMode.upstream - - tctx.configure(nl, tcp_hosts=["example.com"]) - assert isinstance(nl._next_layer(ctx, b"123", b""), layers.TCPLayer) - - tctx.configure(nl, tcp_hosts=[]) - assert isinstance(nl._next_layer(ctx, b"GET /foo", b""), layers.HttpLayer) - assert isinstance(nl._next_layer(ctx, b"", b"hello"), layers.TCPLayer) - - def test_next_layer_udp(self): - def is_ignored_udp(layer: Optional[layer.Layer]): - return isinstance(layer, layers.UDPLayer) and layer.flow is None + m.context.layers = [modes.TransparentProxy(m.context)] + m.context.server.address = ("example.com", 42) + tctx.configure(nl, ignore_hosts=["example.com"]) - def is_intercepted_udp(layer: Optional[layer.Layer]): - return isinstance(layer, layers.UDPLayer) and layer.flow is not None + # Connection is ignored (not-MITM'ed) + assert nl._ignore_connection(m.context, http_get, b"") is True + # No flow is being set (i.e. nothing shown in UI) + assert nl._next_layer(m.context, http_get, b"").flow is None + # ... until `--show-ignored-hosts` is set: + tctx.configure(nl, show_ignored_hosts=True) + assert nl._next_layer(m.context, http_get, b"").flow is not None + def test_next_layer(self, monkeypatch, caplog): + caplog.set_level(logging.DEBUG) nl = NextLayer() - ctx = MagicMock() - ctx.client.alpn = None - ctx.server.address = ("example.com", 443) - ctx.client.transport_protocol = "udp" - with taddons.context(nl) as tctx: - ctx.layers = [layers.modes.HttpProxy(ctx)] - tctx.configure(nl, rawudp=False) - assert is_ignored_udp(nl._next_layer(ctx, b"", b"")) - - ctx.layers = [layers.modes.HttpProxy(ctx)] - tctx.configure(nl, rawudp=True) - assert is_intercepted_udp(nl._next_layer(ctx, b"", b"")) - ctx.layers = [layers.modes.HttpProxy(ctx)] - ctx.server.address = ("nomatch.com", 443) + with taddons.context(nl) as tctx: + m = MagicMock() + m.context = Context( + Client(peername=("192.168.0.42", 51234), sockname=("0.0.0.0", 8080)), + tctx.options, + ) + m.context.layers = [modes.TransparentProxy(m.context)] + m.context.server.address = ("example.com", 42) tctx.configure(nl, ignore_hosts=["example.com"]) - assert is_intercepted_udp(nl._next_layer(ctx, dtls_client_hello_with_extensions[:50], b"")) - assert is_ignored_udp(nl._next_layer(ctx, dtls_client_hello_with_extensions, b"")) - ctx.layers = [layers.modes.HttpProxy(ctx)] - ctx.server.address = ("example.com", 443) - assert is_ignored_udp(nl._next_layer(ctx, dtls_client_hello_with_extensions[:50], b"")) + m.layer = preexisting = object() + nl.next_layer(m) + assert m.layer is preexisting - ctx.layers = [layers.modes.HttpProxy(ctx)] - tctx.configure(nl, ignore_hosts=[]) - assert isinstance(nl._next_layer(ctx, dtls_client_hello_with_extensions, b""), layers.ClientTLSLayer) + m.layer = None + monkeypatch.setattr(m, "data_client", lambda: http_get) + nl.next_layer(m) + assert m.layer - ctx.layers = [layers.modes.HttpProxy(ctx)] - tctx.configure(nl, udp_hosts=["example.com"]) - assert isinstance(nl._next_layer(ctx, tflow.tdnsreq().packed, b""), layers.UDPLayer) + m.layer = None + monkeypatch.setattr( + m, "data_client", lambda: client_hello_with_extensions[:-5] + ) + nl.next_layer(m) + assert not m.layer + assert "Deferring layer decision" in caplog.text + + +@dataclass +class TConf: + before: list[type[Layer]] + after: list[type[Layer]] + proxy_mode: str = "regular" + transport_protocol: TransportProtocol = "tcp" + tls_version: TlsVersion = None + data_client: bytes = b"" + data_server: bytes = b"" + ignore_hosts: Sequence[str] = () + tcp_hosts: Sequence[str] = () + udp_hosts: Sequence[str] = () + ignore_conn: bool = False + server_address: Address | None = None + alpn: bytes | None = None + + +explicit_proxy_configs = [ + pytest.param( + TConf( + before=[modes.HttpProxy], + after=[modes.HttpProxy, HttpLayer], + data_client=http_connect, + ), + id=f"explicit proxy: regular http connect", + ), + pytest.param( + TConf( + before=[modes.HttpProxy], + after=[modes.HttpProxy, HttpLayer], + ignore_hosts=[".+"], + data_client=http_connect, + ), + id=f"explicit proxy: regular http connect disregards ignore_hosts", + ), + pytest.param( + TConf( + before=[modes.HttpProxy], + after=[modes.HttpProxy, HttpLayer], + ignore_hosts=[".+"], + data_client=http_get_absolute, + ), + id=f"explicit proxy: HTTP over regular proxy disregards ignore_hosts", + ), + pytest.param( + TConf( + before=[modes.HttpProxy], + after=[modes.HttpProxy, ClientTLSLayer, HttpLayer], + data_client=client_hello_no_extensions, + ), + id=f"explicit proxy: secure web proxy", + ), + pytest.param( + TConf( + before=[modes.HttpUpstreamProxy], + after=[modes.HttpUpstreamProxy, HttpLayer], + ), + id=f"explicit proxy: upstream proxy", + ), + pytest.param( + TConf( + before=[modes.HttpUpstreamProxy], + after=[modes.HttpUpstreamProxy, ClientQuicLayer, HttpLayer], + transport_protocol="udp", + ), + id=f"explicit proxy: experimental http3", + ), + pytest.param( + TConf( + before=[ + modes.HttpProxy, + partial(HttpLayer, mode=HTTPMode.regular), + partial(HttpStream, stream_id=1), + ], + after=[modes.HttpProxy, HttpLayer, HttpStream, HttpLayer], + data_client=b"GET / HTTP/1.1\r\n", + ), + id=f"explicit proxy: HTTP over regular proxy", + ), + pytest.param( + TConf( + before=[ + modes.HttpProxy, + partial(HttpLayer, mode=HTTPMode.regular), + partial(HttpStream, stream_id=1), + ], + after=[ + modes.HttpProxy, + HttpLayer, + HttpStream, + ServerTLSLayer, + ClientTLSLayer, + ], + data_client=client_hello_with_extensions, + ), + id=f"explicit proxy: TLS over regular proxy", + ), + pytest.param( + TConf( + before=[ + modes.HttpProxy, + partial(HttpLayer, mode=HTTPMode.regular), + partial(HttpStream, stream_id=1), + ServerTLSLayer, + ClientTLSLayer, + ], + after=[ + modes.HttpProxy, + HttpLayer, + HttpStream, + ServerTLSLayer, + ClientTLSLayer, + HttpLayer, + ], + data_client=b"GET / HTTP/1.1\r\n", + ), + id=f"explicit proxy: HTTPS over regular proxy", + ), + pytest.param( + TConf( + before=[ + modes.HttpProxy, + partial(HttpLayer, mode=HTTPMode.regular), + partial(HttpStream, stream_id=1), + ], + after=[modes.HttpProxy, HttpLayer, HttpStream, TCPLayer], + data_client=b"\xff", + ), + id=f"explicit proxy: TCP over regular proxy", + ), +] + +reverse_proxy_configs = [] +for proto_plain, proto_enc, app_layer in [ + ("udp", "dtls", UDPLayer), + ("tcp", "tls", TCPLayer), + ("http", "https", HttpLayer), +]: + if proto_plain == "udp": + data_client = dtls_client_hello_with_extensions + else: + data_client = client_hello_with_extensions + + reverse_proxy_configs.extend( + [ + pytest.param( + TConf( + before=[modes.ReverseProxy], + after=[modes.ReverseProxy, app_layer], + proxy_mode=f"reverse:{proto_plain}://example.com:42", + ), + id=f"reverse proxy: {proto_plain} -> {proto_plain}", + ), + pytest.param( + TConf( + before=[modes.ReverseProxy], + after=[ + modes.ReverseProxy, + ServerTLSLayer, + ClientTLSLayer, + app_layer, + ], + proxy_mode=f"reverse:{proto_enc}://example.com:42", + data_client=data_client, + ), + id=f"reverse proxy: {proto_enc} -> {proto_enc}", + ), + pytest.param( + TConf( + before=[modes.ReverseProxy], + after=[modes.ReverseProxy, ClientTLSLayer, app_layer], + proxy_mode=f"reverse:{proto_plain}://example.com:42", + data_client=data_client, + ), + id=f"reverse proxy: {proto_enc} -> {proto_plain}", + ), + pytest.param( + TConf( + before=[modes.ReverseProxy], + after=[modes.ReverseProxy, ServerTLSLayer, app_layer], + proxy_mode=f"reverse:{proto_enc}://example.com:42", + ), + id=f"reverse proxy: {proto_plain} -> {proto_enc}", + ), + ] + ) - ctx.layers = [layers.modes.HttpProxy(ctx)] - tctx.configure(nl, udp_hosts=[]) - assert isinstance(nl._next_layer(ctx, tflow.tdnsreq().packed, b""), layers.DNSLayer) +reverse_proxy_configs.extend( + [ + pytest.param( + TConf( + before=[modes.ReverseProxy], + after=[modes.ReverseProxy, DNSLayer], + proxy_mode="reverse:dns://example.com:53", + ), + id="reverse proxy: dns", + ), + pytest.param( + http3 := TConf( + before=[modes.ReverseProxy], + after=[modes.ReverseProxy, ServerQuicLayer, ClientQuicLayer, HttpLayer], + proxy_mode="reverse:http3://example.com", + ), + id="reverse proxy: http3", + ), + pytest.param( + dataclasses.replace( + http3, + proxy_mode="reverse:https://example.com", + transport_protocol="udp", + ), + id="reverse proxy: http3 in https mode", + ), + pytest.param( + TConf( + before=[modes.ReverseProxy], + after=[ + modes.ReverseProxy, + ServerQuicLayer, + ClientQuicLayer, + RawQuicLayer, + ], + proxy_mode="reverse:quic://example.com", + ), + id="reverse proxy: quic", + ), + pytest.param( + TConf( + before=[modes.ReverseProxy], + after=[modes.ReverseProxy, TCPLayer], + proxy_mode=f"reverse:http://example.com", + ignore_hosts=["example.com"], + server_address=("example.com", 80), + data_client=http_get, + ignore_conn=True, + ), + id="reverse proxy: ignore_hosts", + ), + ] +) - def test_next_layer_invalid_proto(self): - nl = NextLayer() - ctx = MagicMock() - ctx.client.transport_protocol = "invalid" - with taddons.context(nl): - with pytest.raises(AssertionError): - nl._next_layer(ctx, b"", b"") +transparent_proxy_configs = [ + pytest.param( + TConf( + before=[modes.TransparentProxy], + after=[modes.TransparentProxy, ServerTLSLayer, ClientTLSLayer], + data_client=client_hello_no_extensions, + ), + id=f"transparent proxy: tls", + ), + pytest.param( + TConf( + before=[modes.TransparentProxy], + after=[modes.TransparentProxy, ServerTLSLayer, ClientTLSLayer], + data_client=dtls_client_hello_with_extensions, + transport_protocol="udp", + ), + id=f"transparent proxy: dtls", + ), + pytest.param( + quic := TConf( + before=[modes.TransparentProxy], + after=[modes.TransparentProxy, ServerQuicLayer, ClientQuicLayer], + data_client=quic_client_hello, + transport_protocol="udp", + server_address=("192.0.2.1", 443), + ), + id="transparent proxy: quic", + ), + pytest.param( + dataclasses.replace( + quic, + data_client=quic_short_header_packet, + ), + id="transparent proxy: existing quic session", + ), + pytest.param( + TConf( + before=[modes.TransparentProxy], + after=[modes.TransparentProxy, TCPLayer], + data_server=b"220 service ready", + ), + id="transparent proxy: raw tcp", + ), + pytest.param( + http := TConf( + before=[modes.TransparentProxy], + after=[modes.TransparentProxy, HttpLayer], + server_address=("192.0.2.1", 80), + data_client=http_get, + ), + id="transparent proxy: http", + ), + pytest.param( + TConf( + before=[modes.TransparentProxy, ServerTLSLayer, ClientTLSLayer], + after=[modes.TransparentProxy, ServerTLSLayer, ClientTLSLayer, HttpLayer], + data_client=b"GO /method-too-short-for-heuristic HTTP/1.1\r\n", + alpn=HTTP1_ALPNS[0], + ), + id=f"transparent proxy: http via ALPN", + ), + pytest.param( + TConf( + before=[modes.TransparentProxy], + after=[modes.TransparentProxy, TCPLayer], + server_address=("192.0.2.1", 23), + data_client=b"SSH-2.0-OpenSSH_9.7", + ), + id="transparent proxy: ssh", + ), + pytest.param( + dataclasses.replace( + http, + tcp_hosts=["192.0.2.1"], + after=[modes.TransparentProxy, TCPLayer], + ), + id="transparent proxy: tcp_hosts", + ), + pytest.param( + dataclasses.replace( + http, + ignore_hosts=["192.0.2.1"], + after=[modes.TransparentProxy, TCPLayer], + ignore_conn=True, + ), + id="transparent proxy: ignore_hosts", + ), + pytest.param( + TConf( + before=[modes.TransparentProxy], + after=[modes.TransparentProxy, TCPLayer], + data_client=custom_base64_proto, + ), + id="transparent proxy: full alpha tcp", + ), + pytest.param( + udp := TConf( + before=[modes.TransparentProxy], + after=[modes.TransparentProxy, UDPLayer], + server_address=("192.0.2.1", 553), + transport_protocol="udp", + data_client=b"\xff", + ), + id="transparent proxy: raw udp", + ), + pytest.param( + dns := dataclasses.replace( + udp, + after=[modes.TransparentProxy, DNSLayer], + data_client=dns_query, + server_address=("192.0.2.1", 53), + ), + id="transparent proxy: dns over udp", + ), + pytest.param( + dataclasses.replace( + dns, + transport_protocol="tcp", + ), + id="transparent proxy: dns over tcp", + ), + pytest.param( + dataclasses.replace( + udp, + udp_hosts=["192.0.2.1"], + after=[modes.TransparentProxy, UDPLayer], + ), + id="transparent proxy: udp_hosts", + ), + pytest.param( + TConf( + before=[modes.TransparentProxy], + after=[modes.TransparentProxy, DNSLayer], + proxy_mode="wireguard", + server_address=("10.0.0.53", 53), + ignore_hosts=[".+"], + transport_protocol="udp", + data_client=dns_query, + ), + id="wireguard proxy: dns should not be ignored", + ), + pytest.param( + TConf( + before=[modes.TransparentProxy, ServerQuicLayer, ClientQuicLayer], + after=[ + modes.TransparentProxy, + ServerQuicLayer, + ClientQuicLayer, + RawQuicLayer, + ], + data_client=b"", + alpn=b"doq", + tls_version="QUICv1", + ), + id=f"transparent proxy: non-http quic", + ), +] + + +@pytest.mark.parametrize( + "test_conf", + [ + *explicit_proxy_configs, + *reverse_proxy_configs, + *transparent_proxy_configs, + ], +) +def test_next_layer( + test_conf: TConf, +): + nl = NextLayer() + with taddons.context(nl) as tctx: + tctx.configure( + nl, + ignore_hosts=test_conf.ignore_hosts, + tcp_hosts=test_conf.tcp_hosts, + udp_hosts=test_conf.udp_hosts, + ) + + ctx = Context( + Client( + peername=("192.168.0.42", 51234), + sockname=("0.0.0.0", 8080), + alpn=test_conf.alpn, + ), + tctx.options, + ) + ctx.server.address = test_conf.server_address + ctx.client.transport_protocol = test_conf.transport_protocol + ctx.client.tls_version = test_conf.tls_version + ctx.client.proxy_mode = ProxyMode.parse(test_conf.proxy_mode) + ctx.layers = [x(ctx) for x in test_conf.before] + nl._next_layer( + ctx, + data_client=test_conf.data_client, + data_server=test_conf.data_server, + ) + assert stack_match(ctx, test_conf.after), f"Unexpected stack: {ctx.layers}" + + last_layer = ctx.layers[-1] + if isinstance(last_layer, (UDPLayer, TCPLayer)): + assert bool(last_layer.flow) ^ test_conf.ignore_conn + + +def test_starts_like_quic(): + assert not _starts_like_quic(b"", ("192.0.2.1", 443)) + assert not _starts_like_quic(dtls_client_hello_with_extensions, ("192.0.2.1", 443)) + + # Long Header - we can get definite answers from version numbers. + assert _starts_like_quic(quic_client_hello, None) + quic_version_negotation_grease = bytes.fromhex( + "ca0a0a0a0a08c0618c84b54541320823fcce946c38d8210044e6a93bbb283593f75ffb6f2696b16cfdcb5b1255" + ) + assert _starts_like_quic(quic_version_negotation_grease, None) + + # Short Header - port-based is the best we can do. + assert _starts_like_quic(quic_short_header_packet, ("192.0.2.1", 443)) + assert not _starts_like_quic(quic_short_header_packet, ("192.0.2.1", 444)) diff --git a/test/mitmproxy/addons/test_proxyauth.py b/test/mitmproxy/addons/test_proxyauth.py index 9bdb5b7ce3..e715e9d11d 100644 --- a/test/mitmproxy/addons/test_proxyauth.py +++ b/test/mitmproxy/addons/test_proxyauth.py @@ -165,11 +165,25 @@ def test_configure(self, monkeypatch, tdata): ) assert isinstance(pa.validator, proxyauth.Ldap) + ctx.configure( + pa, + proxyauth="ldap:localhost:1234:cn=default,dc=cdhdt,dc=com:password:dc=cdhdt,dc=com?search_filter_key=SamAccountName", + ) + assert isinstance(pa.validator, proxyauth.Ldap) + with pytest.raises( exceptions.OptionsError, match="Invalid LDAP specification" ): ctx.configure(pa, proxyauth="ldap:test:test:test") + with pytest.raises( + exceptions.OptionsError, match="Invalid LDAP specification" + ): + ctx.configure( + pa, + proxyauth="ldap:localhost:1234:cn=default,dc=cdhdt,dc=com:password:ou=application,dc=cdhdt,dc=com?key=1", + ) + with pytest.raises( exceptions.OptionsError, match="Invalid LDAP specification" ): @@ -225,12 +239,18 @@ def test_handlers(self): assert not f2.response assert f2.metadata["proxyauth"] == ("test", "test") + f3 = tflow.tflow() + f3.is_replay = True + up.requestheaders(f3) + assert not f2.response + @pytest.mark.parametrize( "spec", [ "ldaps:localhost:cn=default,dc=cdhdt,dc=com:password:ou=application,dc=cdhdt,dc=com", "ldap:localhost:1234:cn=default,dc=cdhdt,dc=com:password:ou=application,dc=cdhdt,dc=com", + "ldap:localhost:1234:cn=default,dc=cdhdt,dc=com:password:ou=application,dc=cdhdt,dc=com?search_filter_key=cn", ], ) def test_ldap(monkeypatch, spec): diff --git a/test/mitmproxy/addons/test_proxyserver.py b/test/mitmproxy/addons/test_proxyserver.py index e8eecf80de..eccc98f1ee 100644 --- a/test/mitmproxy/addons/test_proxyserver.py +++ b/test/mitmproxy/addons/test_proxyserver.py @@ -1,31 +1,52 @@ +from __future__ import annotations + import asyncio +import ssl +from collections.abc import AsyncGenerator +from collections.abc import Callable from contextlib import asynccontextmanager -import socket +from dataclasses import dataclass +from typing import Any +from typing import ClassVar +from typing import TypeVar from unittest.mock import Mock import pytest - +from aioquic.asyncio.protocol import QuicConnectionProtocol +from aioquic.asyncio.server import QuicServer +from aioquic.h3 import events as h3_events +from aioquic.h3.connection import FrameUnexpected +from aioquic.h3.connection import H3Connection +from aioquic.quic import events as quic_events +from aioquic.quic.configuration import QuicConfiguration +from aioquic.quic.connection import QuicConnection +from aioquic.quic.connection import QuicConnectionError + +from .test_clientplayback import tcp_server import mitmproxy.platform -from mitmproxy import dns, exceptions +import mitmproxy_rs +from mitmproxy import dns +from mitmproxy import exceptions from mitmproxy.addons import dns_resolver +from mitmproxy.addons.next_layer import NextLayer from mitmproxy.addons.proxyserver import Proxyserver +from mitmproxy.addons.tlsconfig import TlsConfig from mitmproxy.connection import Address -from mitmproxy.net import udp -from mitmproxy.proxy import layers, server_hooks -from mitmproxy.proxy.layers import tls -from mitmproxy.proxy.layers.http import HTTPMode -from mitmproxy.test import taddons, tflow -from mitmproxy.test.tflow import tclient_conn, tserver_conn +from mitmproxy.proxy import layers +from mitmproxy.proxy import server_hooks +from mitmproxy.test import taddons +from mitmproxy.test import tflow +from mitmproxy.test.tflow import tclient_conn +from mitmproxy.test.tflow import tserver_conn from mitmproxy.test.tutils import tdnsreq +from mitmproxy.utils import data + +tlsdata = data.Data(__name__) class HelperAddon: def __init__(self): self.flows = [] - self.layers = [ - lambda ctx: layers.HttpLayer(ctx, HTTPMode.regular), - lambda ctx: layers.TCPLayer(ctx), - ] def request(self, f): self.flows.append(f) @@ -33,19 +54,6 @@ def request(self, f): def tcp_start(self, f): self.flows.append(f) - def next_layer(self, nl): - nl.layer = self.layers.pop(0)(nl.context) - - -@asynccontextmanager -async def tcp_server(handle_conn) -> Address: - server = await asyncio.start_server(handle_conn, "127.0.0.1", 0) - await server.start_serving() - try: - yield server.sockets[0].getsockname() - finally: - server.close() - async def test_start_stop(caplog_async): caplog_async.set_level("INFO") @@ -56,12 +64,12 @@ async def server_handler( assert await reader.readuntil(b"\r\n\r\n") == b"GET /hello HTTP/1.1\r\n\r\n" writer.write(b"HTTP/1.1 204 No Content\r\n\r\n") await writer.drain() - writer.close() ps = Proxyserver() - with taddons.context(ps) as tctx: - state = HelperAddon() - tctx.master.addons.add(state) + nl = NextLayer() + state = HelperAddon() + + with taddons.context(ps, nl, state) as tctx: async with tcp_server(server_handler) as addr: tctx.configure(ps, listen_host="127.0.0.1", listen_port=0) assert not ps.servers @@ -80,9 +88,11 @@ async def server_handler( ) assert repr(ps) == "Proxyserver(1 active conns)" - await ps.setup_servers() # assert this can always be called without side effects + await ( + ps.setup_servers() + ) # assert this can always be called without side effects tctx.configure(ps, server=False) - await caplog_async.await_log("Stopped HTTP(S) proxy at") + await caplog_async.await_log("stopped") if ps.servers.is_updating: async with ps.servers._lock: pass # wait until start/stop is finished. @@ -91,19 +101,27 @@ async def server_handler( assert state.flows[0].request.path == "/hello" assert state.flows[0].response.status_code == 204 - # Waiting here until everything is really torn down... takes some effort. - conn_handler = list(ps.connections.values())[0] - client_handler = conn_handler.transports[conn_handler.client].handler writer.close() await writer.wait_closed() - try: - await client_handler - except asyncio.CancelledError: - pass - for _ in range(5): - # Get all other scheduled coroutines to run. - await asyncio.sleep(0) - assert repr(ps) == "Proxyserver(0 active conns)" + await _wait_for_connection_closes(ps) + + +async def _wait_for_connection_closes(ps: Proxyserver): + # Waiting here until everything is really torn down... takes some effort. + client_handlers = [ + conn_handler.transports[conn_handler.client].handler + for conn_handler in ps.connections.values() + if conn_handler.client in conn_handler.transports + ] + for client_handler in client_handlers: + try: + await asyncio.wait_for(client_handler, 5) + except asyncio.CancelledError: + pass + for _ in range(5): + # Get all other scheduled coroutines to run. + await asyncio.sleep(0) + assert not ps.connections async def test_inject() -> None: @@ -114,9 +132,10 @@ async def server_handler( writer.write(s.upper()) ps = Proxyserver() - with taddons.context(ps) as tctx: - state = HelperAddon() - tctx.master.addons.add(state) + nl = NextLayer() + state = HelperAddon() + + with taddons.context(ps, nl, state) as tctx: async with tcp_server(server_handler) as addr: tctx.configure(ps, listen_host="127.0.0.1", listen_port=0) assert await ps.setup_servers() @@ -138,6 +157,10 @@ async def server_handler( ps.inject_tcp(state.flows[0], True, b"c") assert await reader.read(1) == b"c" + writer.close() + await writer.wait_closed() + await _wait_for_connection_closes(ps) + async def test_inject_fail(caplog) -> None: ps = Proxyserver() @@ -146,6 +169,11 @@ async def test_inject_fail(caplog) -> None: ps.inject_tcp(tflow.tflow(), True, b"test") assert "Cannot inject TCP messages into non-TCP flows." in caplog.text + ps.inject_udp(tflow.tflow(), True, b"test") + assert "Cannot inject UDP messages into non-UDP flows." in caplog.text + ps.inject_udp(tflow.tudpflow(), True, b"test") + assert "Flow is not from a live connection." in caplog.text + ps.inject_websocket(tflow.twebsocketflow(), True, b"test") assert "Flow is not from a live connection." in caplog.text ps.inject_websocket(tflow.ttcpflow(), True, b"test") @@ -180,6 +208,7 @@ async def test_self_connect(): assert "Request destination unknown" in server.error tctx.configure(ps, server=False) assert await ps.setup_servers() + await _wait_for_connection_closes(ps) def test_options(): @@ -202,7 +231,7 @@ def test_options(): tctx.configure(ps, mode=["invalid!"]) with pytest.raises(exceptions.OptionsError): tctx.configure(ps, mode=["regular", "reverse:example.com"]) - tctx.configure(ps, mode=["regular"], server=False) + tctx.configure(ps, mode=["regular", "local"], server=False) async def test_startup_err(monkeypatch, caplog) -> None: @@ -233,24 +262,21 @@ async def _raise(*_): setattr(server, "stop", _raise) tctx.configure(ps, server=False) await caplog_async.await_log("cannot close") + await _wait_for_connection_closes(ps) -class DummyResolver: - async def dns_request(self, flow: dns.DNSFlow) -> None: - flow.response = await dns_resolver.resolve_message(flow.request, self) +async def lookup_ipv4(): + return await asyncio.sleep(0, ["8.8.8.8"]) - async def getaddrinfo(self, host: str, port: int, *, family: int): - if family == socket.AF_INET and host == "dns.google": - return [(socket.AF_INET, None, None, None, ("8.8.8.8", port))] - e = socket.gaierror() - e.errno = socket.EAI_NONAME - raise e +async def test_dns(caplog_async, monkeypatch) -> None: + monkeypatch.setattr( + mitmproxy_rs.dns.DnsResolver, "lookup_ipv4", lambda _, __: lookup_ipv4() + ) -async def test_dns(caplog_async) -> None: caplog_async.set_level("INFO") ps = Proxyserver() - with taddons.context(ps, DummyResolver()) as tctx: + with taddons.context(ps, dns_resolver.DnsResolver()) as tctx: tctx.configure( ps, mode=["dns@127.0.0.1:0"], @@ -260,29 +286,33 @@ async def test_dns(caplog_async) -> None: await caplog_async.await_log("DNS server listening at") assert ps.servers dns_addr = ps.servers["dns@127.0.0.1:0"].listen_addrs[0] - r, w = await udp.open_connection(*dns_addr) + s = await mitmproxy_rs.udp.open_udp_connection(*dns_addr) req = tdnsreq() - w.write(req.packed) - resp = dns.Message.unpack(await r.read(udp.MAX_DATAGRAM_SIZE)) + s.write(req.packed) + resp = dns.Message.unpack(await s.read(65535)) assert req.id == resp.id and "8.8.8.8" in str(resp) assert len(ps.connections) == 1 - w.write(req.packed) - resp = dns.Message.unpack(await r.read(udp.MAX_DATAGRAM_SIZE)) + s.write(req.packed) + resp = dns.Message.unpack(await s.read(65535)) assert req.id == resp.id and "8.8.8.8" in str(resp) assert len(ps.connections) == 1 req.id = req.id + 1 - w.write(req.packed) - resp = dns.Message.unpack(await r.read(udp.MAX_DATAGRAM_SIZE)) + s.write(req.packed) + resp = dns.Message.unpack(await s.read(65535)) assert req.id == resp.id and "8.8.8.8" in str(resp) assert len(ps.connections) == 1 - dns_layer = ps.connections[("udp", w.get_extra_info("sockname"), dns_addr)].layer - assert isinstance(dns_layer, layers.DNSLayer) - assert len(dns_layer.flows) == 2 + (dns_conn,) = ps.connections.values() + assert isinstance(dns_conn.layer, layers.DNSLayer) + assert len(dns_conn.layer.flows) == 2 - w.write(b"\x00") + s.write(b"\x00") await caplog_async.await_log("sent an invalid message") tctx.configure(ps, server=False) - await caplog_async.await_log("Stopped DNS server at") + await caplog_async.await_log("stopped") + + s.close() + await s.wait_closed() + await _wait_for_connection_closes(ps) def test_validation_no_transparent(monkeypatch): @@ -304,46 +334,549 @@ def test_transparent_init(monkeypatch): @asynccontextmanager -async def udp_server(handle_conn) -> Address: - server = await udp.start_server(handle_conn, "127.0.0.1", 0) +async def udp_server( + handle_datagram: Callable[ + [asyncio.DatagramTransport, bytes, tuple[str, int]], None + ], +) -> Address: + class ServerProtocol(asyncio.DatagramProtocol): + def connection_made(self, transport): + self.transport = transport + + def datagram_received(self, data, addr): + handle_datagram(self.transport, data, addr) + + loop = asyncio.get_running_loop() + transport, _ = await loop.create_datagram_endpoint( + lambda: ServerProtocol(), + local_addr=("127.0.0.1", 0), + ) + socket = transport.get_extra_info("socket") + try: - yield server.sockets[0].getsockname() + yield socket.getsockname() finally: - server.close() + transport.close() -async def test_dtls(monkeypatch, caplog_async) -> None: +async def test_udp(caplog_async) -> None: caplog_async.set_level("INFO") - def server_handler( - transport: asyncio.DatagramTransport, - data: bytes, - remote_addr: Address, - _: Address, + def handle_datagram( + transport: asyncio.DatagramTransport, + data: bytes, + remote_addr: Address, ): assert data == b"\x16" transport.sendto(b"\x01", remote_addr) ps = Proxyserver() + nl = NextLayer() - # We just want to relay the messages and skip the handshake. - monkeypatch.setattr(tls, "ServerTLSLayer", layers.UDPLayer) - - with taddons.context(ps) as tctx: - state = HelperAddon() - tctx.master.addons.add(state) - async with udp_server(server_handler) as server_addr: - mode = f"reverse:dtls://{server_addr[0]}:{server_addr[1]}@127.0.0.1:0" + with taddons.context(ps, nl) as tctx: + async with udp_server(handle_datagram) as server_addr: + mode = f"reverse:udp://{server_addr[0]}:{server_addr[1]}@127.0.0.1:0" tctx.configure(ps, mode=[mode]) assert await ps.setup_servers() ps.running() - await caplog_async.await_log(f"reverse proxy to dtls://{server_addr[0]}:{server_addr[1]} listening") + await caplog_async.await_log( + f"reverse proxy to udp://{server_addr[0]}:{server_addr[1]} listening" + ) assert ps.servers addr = ps.servers[mode].listen_addrs[0] - r, w = await udp.open_connection(*addr) - w.write(b"\x16") - assert b"\x01" == await r.read(udp.MAX_DATAGRAM_SIZE) + stream = await mitmproxy_rs.udp.open_udp_connection(*addr) + stream.write(b"\x16") + assert b"\x01" == await stream.read(65535) assert repr(ps) == "Proxyserver(1 active conns)" assert len(ps.connections) == 1 tctx.configure(ps, server=False) - await caplog_async.await_log("Stopped reverse proxy to dtls") + await caplog_async.await_log("stopped") + + stream.close() + await stream.wait_closed() + await _wait_for_connection_closes(ps) + + +class H3EchoServer(QuicConnectionProtocol): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._seen_headers: set[int] = set() + self.http: H3Connection | None = None + + def http_headers_received(self, event: h3_events.HeadersReceived) -> None: + assert event.push_id is None + headers: dict[bytes, bytes] = {} + for name, value in event.headers: + headers[name] = value + response = [] + if event.stream_id not in self._seen_headers: + self._seen_headers.add(event.stream_id) + assert headers[b":authority"] == b"example.mitmproxy.org" + assert headers[b":method"] == b"GET" + assert headers[b":path"] == b"/test" + response.append((b":status", b"200")) + response.append((b"x-response", headers[b"x-request"])) + self.http.send_headers( + stream_id=event.stream_id, headers=response, end_stream=event.stream_ended + ) + self.transmit() + + def http_data_received(self, event: h3_events.DataReceived) -> None: + assert event.push_id is None + assert event.stream_id in self._seen_headers + try: + self.http.send_data( + stream_id=event.stream_id, + data=event.data, + end_stream=event.stream_ended, + ) + except FrameUnexpected: + if event.data or not event.stream_ended: + raise + self._quic.send_stream_data( + stream_id=event.stream_id, + data=b"", + end_stream=True, + ) + self.transmit() + + def http_event_received(self, event: h3_events.H3Event) -> None: + if isinstance(event, h3_events.HeadersReceived): + self.http_headers_received(event) + elif isinstance(event, h3_events.DataReceived): + self.http_data_received(event) + else: + raise AssertionError(event) + + def quic_event_received(self, event: quic_events.QuicEvent) -> None: + if isinstance(event, quic_events.ProtocolNegotiated): + self.http = H3Connection(self._quic) + if self.http is not None: + for http_event in self.http.handle_event(event): + self.http_event_received(http_event) + + +class QuicDatagramEchoServer(QuicConnectionProtocol): + def quic_event_received(self, event: quic_events.QuicEvent) -> None: + if isinstance(event, quic_events.DatagramFrameReceived): + self._quic.send_datagram_frame(event.data) + self.transmit() + + +@asynccontextmanager +async def quic_server( + create_protocol, alpn: list[str] +) -> AsyncGenerator[Address, None]: + configuration = QuicConfiguration( + is_client=False, + alpn_protocols=alpn, + max_datagram_frame_size=65536, + ) + configuration.load_cert_chain( + certfile=tlsdata.path("../net/data/verificationcerts/trusted-leaf.crt"), + keyfile=tlsdata.path("../net/data/verificationcerts/trusted-leaf.key"), + ) + loop = asyncio.get_running_loop() + transport, server = await loop.create_datagram_endpoint( + lambda: QuicServer( + configuration=configuration, + create_protocol=create_protocol, + ), + local_addr=("127.0.0.1", 0), + ) + try: + yield transport.get_extra_info("sockname") + finally: + server.close() + + +class QuicClient(QuicConnectionProtocol): + TIMEOUT: ClassVar[int] = 10 + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._waiter = self._loop.create_future() + + def quic_event_received(self, event: quic_events.QuicEvent) -> None: + if not self._waiter.done(): + if isinstance(event, quic_events.ConnectionTerminated): + self._waiter.set_exception( + QuicConnectionError( + event.error_code, event.frame_type, event.reason_phrase + ) + ) + elif isinstance(event, quic_events.HandshakeCompleted): + self._waiter.set_result(None) + + def connection_lost(self, exc: Exception | None) -> None: + if not self._waiter.done(): + self._waiter.set_exception(exc) + return super().connection_lost(exc) + + async def wait_handshake(self) -> None: + return await asyncio.wait_for(self._waiter, timeout=QuicClient.TIMEOUT) + + +class QuicDatagramClient(QuicClient): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._datagram: asyncio.Future[bytes] = self._loop.create_future() + + def quic_event_received(self, event: quic_events.QuicEvent) -> None: + super().quic_event_received(event) + if not self._datagram.done(): + if isinstance(event, quic_events.DatagramFrameReceived): + self._datagram.set_result(event.data) + elif isinstance(event, quic_events.ConnectionTerminated): + self._datagram.set_exception( + QuicConnectionError( + event.error_code, event.frame_type, event.reason_phrase + ) + ) + + def send_datagram(self, data: bytes) -> None: + self._quic.send_datagram_frame(data) + self.transmit() + + async def recv_datagram(self) -> bytes: + return await asyncio.wait_for(self._datagram, timeout=QuicClient.TIMEOUT) + + +@dataclass +class H3Response: + waiter: asyncio.Future[H3Response] + stream_id: int + headers: h3_events.H3Event | None = None + data: bytes | None = None + trailers: h3_events.H3Event | None = None + callback: Callable[[str], None] | None = None + + async def wait_result(self) -> H3Response: + return await asyncio.wait_for(self.waiter, timeout=QuicClient.TIMEOUT) + + def __setattr__(self, name: str, value: Any) -> None: + super().__setattr__(name, value) + if self.callback: + self.callback(name) + + +class H3Client(QuicClient): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._responses: dict[int, H3Response] = dict() + self.http = H3Connection(self._quic) + + def http_headers_received(self, event: h3_events.HeadersReceived) -> None: + assert event.push_id is None + response = self._responses[event.stream_id] + if response.waiter.done(): + return + if response.headers is None: + response.headers = event.headers + if event.stream_ended: + response.waiter.set_result(response) + elif response.trailers is None: + response.trailers = event.headers + if event.stream_ended: + response.waiter.set_result(response) + else: + response.waiter.set_exception(Exception("Headers after trailers received.")) + + def http_data_received(self, event: h3_events.DataReceived) -> None: + assert event.push_id is None + response = self._responses[event.stream_id] + if response.waiter.done(): + return + if response.headers is None: + response.waiter.set_exception(Exception("Data without headers received.")) + elif response.trailers is None: + if response.data is None: + response.data = event.data + else: + response.data = response.data + event.data + if event.stream_ended: + response.waiter.set_result(response) + elif event.data or not event.stream_ended: + response.waiter.set_exception(Exception("Data after trailers received.")) + else: + response.waiter.set_result(response) + + def http_event_received(self, event: h3_events.H3Event) -> None: + if isinstance(event, h3_events.HeadersReceived): + self.http_headers_received(event) + elif isinstance(event, h3_events.DataReceived): + self.http_data_received(event) + else: + raise AssertionError(event) + + def quic_event_received(self, event: quic_events.QuicEvent) -> None: + super().quic_event_received(event) + for http_event in self.http.handle_event(event): + self.http_event_received(http_event) + + def request( + self, + headers: h3_events.Headers, + data: bytes | None = None, + trailers: h3_events.Headers | None = None, + end_stream: bool = True, + ) -> H3Response: + stream_id = self._quic.get_next_available_stream_id() + self.http.send_headers( + stream_id=stream_id, + headers=headers, + end_stream=data is None and trailers is None and end_stream, + ) + if data is not None: + self.http.send_data( + stream_id=stream_id, + data=data, + end_stream=trailers is None and end_stream, + ) + if trailers is not None: + self.http.send_headers( + stream_id=stream_id, + headers=trailers, + end_stream=end_stream, + ) + waiter = self._loop.create_future() + response = H3Response(waiter=waiter, stream_id=stream_id) + self._responses[stream_id] = response + self.transmit() + return response + + +T = TypeVar("T", bound=QuicClient) + + +@asynccontextmanager +async def quic_connect( + cls: type[T], + alpn: list[str], + address: Address, +) -> AsyncGenerator[T, None]: + configuration = QuicConfiguration( + is_client=True, + alpn_protocols=alpn, + server_name="example.mitmproxy.org", + verify_mode=ssl.CERT_NONE, + max_datagram_frame_size=65536, + ) + loop = asyncio.get_running_loop() + transport, protocol = await loop.create_datagram_endpoint( + lambda: cls(QuicConnection(configuration=configuration)), + local_addr=("127.0.0.1", 0), + ) + assert isinstance(protocol, cls) + try: + protocol.connect(address) + await protocol.wait_handshake() + yield protocol + finally: + protocol.close() + await protocol.wait_closed() + transport.close() + + +async def _test_echo(client: H3Client, strict: bool) -> None: + def assert_no_data(response: H3Response): + if strict: + assert response.data is None + else: + assert not response.data + + headers = [ + (b":scheme", b"https"), + (b":authority", b"example.mitmproxy.org"), + (b":method", b"GET"), + (b":path", b"/test"), + ] + r1 = await client.request( + headers=headers + [(b"x-request", b"justheaders")], + data=None, + trailers=None, + ).wait_result() + assert r1.headers == [ + (b":status", b"200"), + (b"x-response", b"justheaders"), + ] + assert_no_data(r1) + assert r1.trailers is None + + r2 = await client.request( + headers=headers + [(b"x-request", b"hasdata")], + data=b"echo", + trailers=None, + ).wait_result() + assert r2.headers == [ + (b":status", b"200"), + (b"x-response", b"hasdata"), + ] + assert r2.data == b"echo" + assert r2.trailers is None + + r3 = await client.request( + headers=headers + [(b"x-request", b"nodata")], + data=None, + trailers=[(b"x-request", b"buttrailers")], + ).wait_result() + assert r3.headers == [ + (b":status", b"200"), + (b"x-response", b"nodata"), + ] + assert_no_data(r3) + assert r3.trailers == [(b"x-response", b"buttrailers")] + + r4 = await client.request( + headers=headers + [(b"x-request", b"this")], + data=b"has", + trailers=[(b"x-request", b"everything")], + ).wait_result() + assert r4.headers == [ + (b":status", b"200"), + (b"x-response", b"this"), + ] + assert r4.data == b"has" + assert r4.trailers == [(b"x-response", b"everything")] + + # the following test makes sure that we behave properly if end_stream is sent separately + r5 = client.request( + headers=headers + [(b"x-request", b"this")], + data=b"has", + trailers=[(b"x-request", b"everything but end_stream")], + end_stream=False, + ) + if not strict: + trailer_waiter = asyncio.get_running_loop().create_future() + r5.callback = lambda name: name != "trailers" or trailer_waiter.set_result(None) + await asyncio.wait_for(trailer_waiter, timeout=QuicClient.TIMEOUT) + assert r5.trailers is not None + assert not r5.waiter.done() + else: + await asyncio.sleep(0) + client._quic.send_stream_data( + stream_id=r5.stream_id, + data=b"", + end_stream=True, + ) + client.transmit() + await r5.wait_result() + assert r5.headers == [ + (b":status", b"200"), + (b"x-response", b"this"), + ] + assert r5.data == b"has" + assert r5.trailers == [(b"x-response", b"everything but end_stream")] + + +@pytest.mark.parametrize("scheme", ["http3", "quic"]) +async def test_reverse_http3_and_quic_stream(caplog_async, scheme: str) -> None: + caplog_async.set_level("INFO") + ps = Proxyserver() + nl = NextLayer() + ta = TlsConfig() + with taddons.context(ps, nl, ta) as tctx: + tctx.options.keep_host_header = True + ta.configure(["confdir"]) + async with quic_server(H3EchoServer, alpn=["h3"]) as server_addr: + mode = f"reverse:{scheme}://{server_addr[0]}:{server_addr[1]}@127.0.0.1:0" + tctx.configure( + ta, + ssl_verify_upstream_trusted_ca=tlsdata.path( + "../net/data/verificationcerts/trusted-root.crt" + ), + ) + tctx.configure(ps, mode=[mode]) + assert await ps.setup_servers() + ps.running() + await caplog_async.await_log( + f"reverse proxy to {scheme}://{server_addr[0]}:{server_addr[1]} listening" + ) + assert ps.servers + addr = ps.servers[mode].listen_addrs[0] + async with quic_connect(H3Client, alpn=["h3"], address=addr) as client: + await _test_echo(client, strict=scheme == "http3") + assert len(ps.connections) == 1 + + tctx.configure(ps, server=False) + await caplog_async.await_log(f"stopped") + await _wait_for_connection_closes(ps) + + +async def test_reverse_quic_datagram(caplog_async) -> None: + caplog_async.set_level("INFO") + ps = Proxyserver() + nl = NextLayer() + ta = TlsConfig() + with taddons.context(ps, nl, ta) as tctx: + tctx.options.keep_host_header = True + ta.configure(["confdir"]) + async with quic_server(QuicDatagramEchoServer, alpn=["dgram"]) as server_addr: + mode = f"reverse:quic://{server_addr[0]}:{server_addr[1]}@127.0.0.1:0" + tctx.configure( + ta, + ssl_verify_upstream_trusted_ca=tlsdata.path( + "../net/data/verificationcerts/trusted-root.crt" + ), + ) + tctx.configure(ps, mode=[mode]) + assert await ps.setup_servers() + ps.running() + await caplog_async.await_log( + f"reverse proxy to quic://{server_addr[0]}:{server_addr[1]} listening" + ) + assert ps.servers + addr = ps.servers[mode].listen_addrs[0] + async with quic_connect( + QuicDatagramClient, alpn=["dgram"], address=addr + ) as client: + client.send_datagram(b"echo") + assert await client.recv_datagram() == b"echo" + + tctx.configure(ps, server=False) + await caplog_async.await_log("stopped") + await _wait_for_connection_closes(ps) + + +@pytest.mark.skip("HTTP/3 for regular mode is not fully supported yet") +async def test_regular_http3(caplog_async, monkeypatch) -> None: + caplog_async.set_level("INFO") + ps = Proxyserver() + nl = NextLayer() + ta = TlsConfig() + with taddons.context(ps, nl, ta) as tctx: + ta.configure(["confdir"]) + async with quic_server(H3EchoServer, alpn=["h3"]) as server_addr: + orig_open_connection = mitmproxy_rs.udp.open_udp_connection + + async def open_connection_path( + host: str, port: int, *args, **kwargs + ) -> mitmproxy_rs.Stream: + if host == "example.mitmproxy.org" and port == 443: + host = server_addr[0] + port = server_addr[1] + return orig_open_connection(host, port, *args, **kwargs) + + monkeypatch.setattr( + mitmproxy_rs.udp, "open_udp_connection", open_connection_path + ) + mode = f"http3@127.0.0.1:0" + tctx.configure( + ta, + ssl_verify_upstream_trusted_ca=tlsdata.path( + "../net/data/verificationcerts/trusted-root.crt" + ), + ) + tctx.configure(ps, mode=[mode]) + assert await ps.setup_servers() + ps.running() + await caplog_async.await_log(f"HTTP3 proxy listening") + assert ps.servers + addr = ps.servers[mode].listen_addrs[0] + async with quic_connect(H3Client, alpn=["h3"], address=addr) as client: + await _test_echo(client=client, strict=True) + assert len(ps.connections) == 1 + + tctx.configure(ps, server=False) + await caplog_async.await_log("stopped") + await _wait_for_connection_closes(ps) diff --git a/test/mitmproxy/addons/test_readfile.py b/test/mitmproxy/addons/test_readfile.py index 5c01e95e84..bc1dd383a9 100644 --- a/test/mitmproxy/addons/test_readfile.py +++ b/test/mitmproxy/addons/test_readfile.py @@ -1,8 +1,8 @@ import asyncio import io +from unittest import mock import pytest -from unittest import mock import mitmproxy.io from mitmproxy import exceptions @@ -54,13 +54,21 @@ async def test_read(self, tmpdir, data, corrupt_data, caplog_async): tf = tmpdir.join("tfile") - with mock.patch("mitmproxy.master.Master.load_flow") as mck: - tf.write(data.getvalue()) - tctx.configure(rf, rfile=str(tf), readfile_filter=".*") - mck.assert_not_awaited() - rf.running() + load_called = asyncio.Event() + + async def load_flow(*_, **__): + load_called.set() + + tctx.master.load_flow = load_flow + + tf.write(data.getvalue()) + tctx.configure(rf, rfile=str(tf), readfile_filter=".*") + assert not load_called.is_set() + rf.running() + await load_called.wait() + + while rf.reading(): await asyncio.sleep(0) - mck.assert_awaited() tf.write(corrupt_data.getvalue()) tctx.configure(rf, rfile=str(tf)) diff --git a/test/mitmproxy/addons/test_save.py b/test/mitmproxy/addons/test_save.py index 8361409ac8..b14f5ccace 100644 --- a/test/mitmproxy/addons/test_save.py +++ b/test/mitmproxy/addons/test_save.py @@ -196,4 +196,4 @@ def _raise(*_): with pytest.raises(SystemExit): sa.response(f) - assert "Error while writing" in capsys.readouterr().err \ No newline at end of file + assert "Error while writing" in capsys.readouterr().err diff --git a/test/mitmproxy/addons/test_savehar.py b/test/mitmproxy/addons/test_savehar.py new file mode 100644 index 0000000000..753938fde6 --- /dev/null +++ b/test/mitmproxy/addons/test_savehar.py @@ -0,0 +1,249 @@ +import json +import zlib +from pathlib import Path + +import pytest + +from mitmproxy import io +from mitmproxy import types +from mitmproxy import version +from mitmproxy.addons.save import Save +from mitmproxy.addons.savehar import SaveHar +from mitmproxy.connection import Server +from mitmproxy.exceptions import OptionsError +from mitmproxy.http import Headers +from mitmproxy.http import Request +from mitmproxy.http import Response +from mitmproxy.test import taddons +from mitmproxy.test import tflow +from mitmproxy.test import tutils + +test_dir = Path(__file__).parent.parent + + +def test_write_error(): + s = SaveHar() + + with pytest.raises(FileNotFoundError): + s.export_har([], types.Path("unknown_dir/testing_flow.har")) + + +@pytest.mark.parametrize( + "header, expected", + [ + (Headers([(b"cookie", b"foo=bar")]), [{"name": "foo", "value": "bar"}]), + ( + Headers([(b"cookie", b"foo=bar"), (b"cookie", b"foo=baz")]), + [{"name": "foo", "value": "bar"}, {"name": "foo", "value": "baz"}], + ), + ], +) +def test_request_cookies(header: Headers, expected: list[dict]): + s = SaveHar() + req = Request.make("GET", "https://exampls.com", "", header) + assert s.format_multidict(req.cookies) == expected + + +@pytest.mark.parametrize( + "header, expected", + [ + ( + Headers( + [ + ( + b"set-cookie", + b"foo=bar; path=/; domain=.googls.com; priority=high", + ) + ] + ), + [ + { + "name": "foo", + "value": "bar", + "path": "/", + "domain": ".googls.com", + "httpOnly": False, + "secure": False, + } + ], + ), + ( + Headers( + [ + ( + b"set-cookie", + b"foo=bar; path=/; domain=.googls.com; Secure; HttpOnly; priority=high", + ), + ( + b"set-cookie", + b"fooz=baz; path=/; domain=.googls.com; priority=high; SameSite=none", + ), + ] + ), + [ + { + "name": "foo", + "value": "bar", + "path": "/", + "domain": ".googls.com", + "httpOnly": True, + "secure": True, + }, + { + "name": "fooz", + "value": "baz", + "path": "/", + "domain": ".googls.com", + "httpOnly": False, + "secure": False, + "sameSite": "none", + }, + ], + ), + ], +) +def test_response_cookies(header: Headers, expected: list[dict]): + s = SaveHar() + resp = Response.make(200, "", header) + assert s.format_response_cookies(resp) == expected + + +def test_seen_server_conn(): + s = SaveHar() + + flow = tflow.twebsocketflow() + + servers_seen: set[Server] = set() + servers_seen.add(flow.server_conn) + + calculated_timings = s.flow_entry(flow, servers_seen)["timings"] + + assert calculated_timings["connect"] == -1.0 + assert calculated_timings["ssl"] == -1.0 + + +def test_timestamp_end(): + s = SaveHar() + servers_seen: set[Server] = set() + flow = tflow.twebsocketflow() + + assert s.flow_entry(flow, set())["timings"]["send"] == 1000 + + flow.request.timestamp_end = None + calculated_timings = s.flow_entry(flow, servers_seen)["timings"] + + assert calculated_timings["send"] == 0 + + +def test_tls_setup(): + s = SaveHar() + servers_seen: set[Server] = set() + flow = tflow.twebsocketflow() + flow.server_conn.timestamp_tls_setup = None + + assert s.flow_entry(flow, servers_seen)["timings"]["ssl"] == -1.0 + + +def test_binary_content(): + resp_content = SaveHar().make_har( + [tflow.tflow(resp=tutils.tresp(content=b"foo" + b"\xff" * 10))] + )["log"]["entries"][0]["response"]["content"] + assert resp_content == { + "compression": 0, + "encoding": "base64", + "mimeType": "", + "size": 13, + "text": "Zm9v/////////////w==", + } + + +@pytest.mark.parametrize( + "log_file", [pytest.param(x, id=x.stem) for x in test_dir.glob("data/flows/*.mitm")] +) +def test_savehar(log_file: Path, tmp_path: Path, monkeypatch): + monkeypatch.setattr(version, "VERSION", "1.2.3") + s = SaveHar() + + flows = io.read_flows_from_paths([log_file]) + + s.export_har(flows, types.Path(tmp_path / "testing_flow.har")) + expected_har = json.loads(log_file.with_suffix(".har").read_bytes()) + actual_har = json.loads(Path(tmp_path / "testing_flow.har").read_bytes()) + + assert actual_har == expected_har + + +def test_flow_entry(): + """https://github.com/mitmproxy/mitmproxy/issues/6579""" + s = SaveHar() + req = Request.make("CONNECT", "https://test.test/") + flow = tflow.tflow(req=req) + servers_seen: set[Server] = set() + + flow_entry = s.flow_entry(flow, servers_seen) + + assert flow_entry["request"]["url"].startswith("https") + + +class TestHardumpOption: + def test_simple(self, capsys): + s = SaveHar() + with taddons.context(s) as tctx: + tctx.configure(s, hardump="-") + + s.response(tflow.tflow()) + + s.error(tflow.tflow()) + + ws = tflow.twebsocketflow() + s.response(ws) + s.websocket_end(ws) + + s.done() + + out = json.loads(capsys.readouterr().out) + assert len(out["log"]["entries"]) == 3 + + def test_filter(self, capsys): + s = SaveHar() + with taddons.context(s, Save()) as tctx: + tctx.configure(s, hardump="-", save_stream_filter="~b foo") + with pytest.raises(OptionsError): + tctx.configure(s, save_stream_filter="~~") + + s.response(tflow.tflow(req=tflow.treq(content=b"foo"))) + s.response(tflow.tflow()) + + s.done() + + out = json.loads(capsys.readouterr().out) + assert len(out["log"]["entries"]) == 1 + + def test_free(self): + s = SaveHar() + with taddons.context(s, Save()) as tctx: + tctx.configure(s, hardump="-") + s.response(tflow.tflow()) + assert s.flows + tctx.configure(s, hardump="") + assert not s.flows + + def test_compressed(self, tmp_path): + s = SaveHar() + with taddons.context(s, Save()) as tctx: + tctx.configure(s, hardump=str(tmp_path / "out.zhar")) + + s.response(tflow.tflow()) + s.done() + + out = json.loads(zlib.decompress((tmp_path / "out.zhar").read_bytes())) + assert len(out["log"]["entries"]) == 1 + + +if __name__ == "__main__": + version.VERSION = "1.2.3" + s = SaveHar() + for file in test_dir.glob("data/flows/*.mitm"): + path = open(file, "rb") + flows = list(io.FlowReader(path).stream()) + s.export_har(flows, types.Path(test_dir / f"data/flows/{file.stem}.har")) diff --git a/test/mitmproxy/addons/test_script.py b/test/mitmproxy/addons/test_script.py index fba36a5562..e2dfc0b2b9 100644 --- a/test/mitmproxy/addons/test_script.py +++ b/test/mitmproxy/addons/test_script.py @@ -14,7 +14,6 @@ from mitmproxy.test import tflow from mitmproxy.tools import main - # We want this to be speedy for testing script.ReloadInterval = 0.1 @@ -26,7 +25,7 @@ def test_load_script(tmp_path, tdata, caplog): assert ns.addons script.load_script("nonexistent") - assert "No such file or directory" in caplog.text + assert "FileNotFoundError" in caplog.text (tmp_path / "error.py").write_text("this is invalid syntax") script.load_script(str(tmp_path / "error.py")) @@ -123,16 +122,24 @@ async def test_exception(self, tdata, caplog_async): await caplog_async.await_log("error.py") sc.done() - async def test_optionexceptions(self, tdata, caplog_async): - with taddons.context() as tctx: - sc = script.Script( + def test_import_error(self, monkeypatch, tdata, caplog): + monkeypatch.setattr(sys, "frozen", True, raising=False) + script.Script( + tdata.path("mitmproxy/data/addonscripts/import_error.py"), + reload=False, + ) + assert ( + "Note that mitmproxy's binaries include their own Python environment" + in caplog.text + ) + + def test_configure_error(self, tdata, caplog): + with taddons.context(): + script.Script( tdata.path("mitmproxy/data/addonscripts/configure.py"), - True, + False, ) - tctx.master.addons.add(sc) - tctx.configure(sc) - await caplog_async.await_log("Options Error") - sc.done() + assert "Options Error" in caplog.text async def test_addon(self, tdata, caplog_async): caplog_async.set_level("INFO") @@ -175,7 +182,9 @@ async def test_script_run(self, tdata, caplog_async): with taddons.context(sc): sc.script_run([tflow.tflow(resp=True)], rp) await caplog_async.await_log("recorder response") - debug = [i.msg for i in caplog_async.caplog.records if i.levelname == "DEBUG"] + debug = [ + i.msg for i in caplog_async.caplog.records if i.levelname == "DEBUG" + ] assert debug == [ "recorder configure", "recorder running", @@ -232,18 +241,6 @@ async def test_script_deletion(self, tdata, caplog_async): assert not tctx.options.scripts assert not sl.addons - async def test_script_error_handler(self, caplog): - path = "/sample/path/example.py" - exc = SyntaxError - msg = "Error raised" - tb = True - with taddons.context(): - script.script_error_handler(path, exc, msg, tb) - assert "/sample/path/example.py" in caplog.text - assert "Error raised" in caplog.text - assert "lineno" in caplog.text - assert "NoneType" in caplog.text - async def test_order(self, tdata, caplog_async): caplog_async.set_level("DEBUG") rec = tdata.path("mitmproxy/data/addonscripts/recorder") @@ -259,7 +256,9 @@ async def test_order(self, tdata, caplog_async): ], ) await caplog_async.await_log("configure") - debug = [i.msg for i in caplog_async.caplog.records if i.levelname == "DEBUG"] + debug = [ + i.msg for i in caplog_async.caplog.records if i.levelname == "DEBUG" + ] assert debug == [ "a load", "a configure", @@ -283,7 +282,9 @@ async def test_order(self, tdata, caplog_async): ) await caplog_async.await_log("b configure") - debug = [i.msg for i in caplog_async.caplog.records if i.levelname == "DEBUG"] + debug = [ + i.msg for i in caplog_async.caplog.records if i.levelname == "DEBUG" + ] assert debug == [ "c configure", "a configure", @@ -299,7 +300,9 @@ async def test_order(self, tdata, caplog_async): ], ) await caplog_async.await_log("e configure") - debug = [i.msg for i in caplog_async.caplog.records if i.levelname == "DEBUG"] + debug = [ + i.msg for i in caplog_async.caplog.records if i.levelname == "DEBUG" + ] assert debug == [ "c done", "b done", @@ -325,6 +328,7 @@ def test_order(tdata, capsys): ] ) time = r"\[[\d:.]+\] " + out = capsys.readouterr().out assert re.match( rf"{time}Loading script.+recorder.py\n" rf"{time}\('recorder', 'load', .+\n" @@ -332,5 +336,5 @@ def test_order(tdata, capsys): rf"{time}Loading script.+shutdown.py\n" rf"{time}\('recorder', 'running', .+\n" rf"{time}\('recorder', 'done', .+\n$", - capsys.readouterr().out, + out, ) diff --git a/test/mitmproxy/addons/test_serverplayback.py b/test/mitmproxy/addons/test_serverplayback.py index 77d4f28367..7ae12c2eed 100644 --- a/test/mitmproxy/addons/test_serverplayback.py +++ b/test/mitmproxy/addons/test_serverplayback.py @@ -58,6 +58,27 @@ def test_server_playback(): assert not sp.flowmap +def test_add_flows(): + sp = serverplayback.ServerPlayback() + with taddons.context(sp) as tctx: + tctx.configure(sp) + f1 = tflow.tflow(resp=True) + f2 = tflow.tflow(resp=True) + + sp.load_flows([f1]) + sp.add_flows([f2]) + + assert sp.next_flow(f1) + assert sp.flowmap + assert sp.next_flow(f2) + assert not sp.flowmap + + sp.add_flows([f1]) + assert sp.flowmap + assert sp.next_flow(f1) + assert not sp.flowmap + + def test_ignore_host(): sp = serverplayback.ServerPlayback() with taddons.context(sp) as tctx: @@ -211,10 +232,10 @@ def test_load(): assert not s.next_flow(r) -def test_load_with_server_replay_nopop(): +def test_load_with_server_replay_reuse(): s = serverplayback.ServerPlayback() with taddons.context(s) as tctx: - tctx.configure(s, server_replay_nopop=True) + tctx.configure(s, server_replay_reuse=True) r = tflow.tflow(resp=True) r.request.headers["key"] = "one" @@ -304,6 +325,24 @@ def multipart_setter(r, **kwargs): thash(r, r2, multipart_setter) +def test_runtime_modify_params(): + s = serverplayback.ServerPlayback() + with taddons.context(s) as tctx: + r = tflow.tflow(resp=True) + r.request.path = "/test?param1=1" + r2 = tflow.tflow(resp=True) + r2.request.path = "/test" + + s.load_flows([r]) + hash = next(iter(s.flowmap.keys())) + + tctx.configure(s, server_replay_ignore_params=["param1"]) + hash_mod = next(iter(s.flowmap.keys())) + + assert hash != hash_mod + assert hash_mod == s._hash(r2) + + def test_server_playback_full(): s = serverplayback.ServerPlayback() with taddons.context(s) as tctx: @@ -343,6 +382,45 @@ async def test_server_playback_kill(): assert f.error +async def test_server_playback_kill_new_option(): + s = serverplayback.ServerPlayback() + with taddons.context(s) as tctx: + tctx.configure(s, server_replay_refresh=True, server_replay_extra="kill") + + f = tflow.tflow() + f.response = mitmproxy.test.tutils.tresp(content=f.request.content) + s.load_flows([f]) + + f = tflow.tflow() + f.request.host = "nonexistent" + await tctx.cycle(s, f) + assert f.error + + +@pytest.mark.parametrize( + "option,status", + [ + ("204", 204), + ("400", 400), + ("404", 404), + ("500", 500), + ], +) +async def test_server_playback_404(option, status): + s = serverplayback.ServerPlayback() + with taddons.context(s) as tctx: + tctx.configure(s, server_replay_refresh=True, server_replay_extra=option) + + f = tflow.tflow() + f.response = mitmproxy.test.tutils.tresp(content=f.request.content) + s.load_flows([f]) + + f = tflow.tflow() + f.request.host = "nonexistent" + s.request(f) + assert f.response.status_code == status + + def test_server_playback_response_deleted(): """ The server playback addon holds references to flows that can be modified by the user in the meantime. diff --git a/test/mitmproxy/addons/test_stickyauth.py b/test/mitmproxy/addons/test_stickyauth.py index 7b422fdd17..a684b8162a 100644 --- a/test/mitmproxy/addons/test_stickyauth.py +++ b/test/mitmproxy/addons/test_stickyauth.py @@ -1,10 +1,9 @@ import pytest -from mitmproxy.test import tflow -from mitmproxy.test import taddons - -from mitmproxy.addons import stickyauth from mitmproxy import exceptions +from mitmproxy.addons import stickyauth +from mitmproxy.test import taddons +from mitmproxy.test import tflow def test_configure(): diff --git a/test/mitmproxy/addons/test_stickycookie.py b/test/mitmproxy/addons/test_stickycookie.py index d3edbbdb76..687deaf09d 100644 --- a/test/mitmproxy/addons/test_stickycookie.py +++ b/test/mitmproxy/addons/test_stickycookie.py @@ -1,9 +1,8 @@ import pytest -from mitmproxy.test import tflow -from mitmproxy.test import taddons - from mitmproxy.addons import stickycookie +from mitmproxy.test import taddons +from mitmproxy.test import tflow from mitmproxy.test import tutils as ntutils @@ -122,9 +121,9 @@ def test_response_delete(self): # Test that a cookie is be deleted # by setting the expire time in the past f = self._response(sc, "duffer=zafar; Path=/", "www.google.com") - f.response.headers[ - "Set-Cookie" - ] = "duffer=; Expires=Thu, 01-Jan-1970 00:00:00 GMT" + f.response.headers["Set-Cookie"] = ( + "duffer=; Expires=Thu, 01-Jan-1970 00:00:00 GMT" + ) sc.response(f) assert not sc.jar.keys() diff --git a/test/mitmproxy/addons/test_strip_dns_https_records.py b/test/mitmproxy/addons/test_strip_dns_https_records.py new file mode 100644 index 0000000000..1c80f78dca --- /dev/null +++ b/test/mitmproxy/addons/test_strip_dns_https_records.py @@ -0,0 +1,85 @@ +from mitmproxy import dns +from mitmproxy.addons import strip_dns_https_records +from mitmproxy.net.dns import https_records +from mitmproxy.net.dns import types +from mitmproxy.net.dns.https_records import SVCParamKeys +from mitmproxy.test import taddons +from mitmproxy.test import tflow +from mitmproxy.test import tutils + + +class TestStripECH: + def test_strip_ech(self): + se = strip_dns_https_records.StripDnsHttpsRecords() + with taddons.context(se) as tctx: + params1 = { + SVCParamKeys.PORT.value: b"\x01\xbb", + SVCParamKeys.ECH.value: b"testbytes", + } + params2 = {SVCParamKeys.PORT.value: b"\x01\xbb"} + record1 = https_records.HTTPSRecord(1, "example.com", params1) + record2 = https_records.HTTPSRecord(1, "example.com", params2) + answers = [ + dns.ResourceRecord( + "dns.google", + dns.types.A, + dns.classes.IN, + 32, + b"\x08\x08\x08\x08", + ), + dns.ResourceRecord( + "dns.google", + dns.types.HTTPS, + dns.classes.IN, + 32, + https_records.pack(record1), + ), + dns.ResourceRecord( + "dns.google", + dns.types.HTTPS, + dns.classes.IN, + 32, + https_records.pack(record2), + ), + ] + resp = tutils.tdnsresp(answers=answers) + f = tflow.tdnsflow(resp=resp) + tctx.configure(se, strip_ech=True) + se.dns_response(f) + assert all( + answer.https_ech is None + for answer in f.response.answers + if answer.type == types.HTTPS + ) + + def test_strip_alpn(self): + se = strip_dns_https_records.StripDnsHttpsRecords() + with taddons.context(se) as tctx: + record2 = https_records.HTTPSRecord( + 1, + "example.com", + { + SVCParamKeys.ALPN.value: b"\x02h2\x02h3", + }, + ) + answers = [ + dns.ResourceRecord( + "dns.google", + dns.types.HTTPS, + dns.classes.IN, + 32, + https_records.pack(record2), + ) + ] + f = tflow.tdnsflow(resp=tutils.tdnsresp(answers=answers)) + + se.dns_response(f) + assert f.response.answers[0].https_alpn == (b"h2", b"h3") + + tctx.configure(se, http3=False) + se.dns_response(f) + assert f.response.answers[0].https_alpn == (b"h2",) + + f.response.answers[0].https_alpn = [b"h3"] + se.dns_response(f) + assert f.response.answers[0].https_alpn is None diff --git a/test/mitmproxy/addons/test_termlog.py b/test/mitmproxy/addons/test_termlog.py index 62573e218c..5ee5517a64 100644 --- a/test/mitmproxy/addons/test_termlog.py +++ b/test/mitmproxy/addons/test_termlog.py @@ -1,4 +1,3 @@ -import asyncio import builtins import io import logging @@ -13,18 +12,7 @@ @pytest.fixture(autouse=True) def ensure_cleanup(): yield - assert not any( - isinstance(x, termlog.TermLogHandler) - for x in logging.root.handlers - ) - - -async def test_delayed_teardown(): - t = termlog.TermLog() - t.done() - assert t.logger in logging.root.handlers - await asyncio.sleep(0) - assert t.logger not in logging.root.handlers + assert not any(isinstance(x, termlog.TermLogHandler) for x in logging.root.handlers) def test_output(capsys): @@ -42,7 +30,7 @@ def test_output(capsys): assert "two" not in out assert "three" in out assert "four" in out - t.done() + t.uninstall() async def test_styling(monkeypatch) -> None: @@ -55,7 +43,7 @@ async def test_styling(monkeypatch) -> None: logging.warning("hello") assert "\x1b[33mhello\x1b[0m" in f.getvalue() - t.done() + t.uninstall() async def test_cannot_print(monkeypatch) -> None: @@ -72,4 +60,4 @@ def _raise(*args, **kwargs): assert exc_info.value.args[0] == 1 - t.done() + t.uninstall() diff --git a/test/mitmproxy/addons/test_tlsconfig.py b/test/mitmproxy/addons/test_tlsconfig.py index 535d30f428..66a1fbb49c 100644 --- a/test/mitmproxy/addons/test_tlsconfig.py +++ b/test/mitmproxy/addons/test_tlsconfig.py @@ -1,17 +1,26 @@ +import ipaddress +import logging import ssl import time from pathlib import Path -from typing import Union import pytest - +from cryptography import x509 from OpenSSL import SSL -from mitmproxy import certs, connection, tls + +from mitmproxy import certs +from mitmproxy import connection +from mitmproxy import options +from mitmproxy import tls from mitmproxy.addons import tlsconfig +from mitmproxy.net import tls as net_tls from mitmproxy.proxy import context -from mitmproxy.proxy.layers import modes, tls as proxy_tls +from mitmproxy.proxy.layers import modes +from mitmproxy.proxy.layers import quic +from mitmproxy.proxy.layers import tls as proxy_tls from mitmproxy.test import taddons from test.mitmproxy.proxy.layers import test_tls +from test.mitmproxy.proxy.layers.quic import test__stream_layers as test_quic def test_alpn_select_callback(): @@ -60,6 +69,17 @@ def test_alpn_select_callback(): here = Path(__file__).parent +def _ctx(opts: options.Options) -> context.Context: + return context.Context( + connection.Client( + peername=("client", 1234), + sockname=("127.0.0.1", 8080), + timestamp_start=1605699329, + ), + opts, + ) + + class TestTlsConfig: def test_configure(self, tdata): ta = tlsconfig.TlsConfig() @@ -67,6 +87,9 @@ def test_configure(self, tdata): with pytest.raises(Exception, match="file does not exist"): tctx.configure(ta, certs=["*=nonexistent"]) + with pytest.raises(Exception, match="Invalid ECDH curve"): + tctx.configure(ta, tls_ecdh_curve_client="invalid") + with pytest.raises(Exception, match="Invalid certificate format"): tctx.configure( ta, @@ -86,16 +109,65 @@ def test_configure(self, tdata): ) assert ta.certstore.certs + def test_configure_tls_version(self, caplog): + caplog.set_level(logging.INFO) + ta = tlsconfig.TlsConfig() + with taddons.context(ta) as tctx: + for attr in [ + "tls_version_client_min", + "tls_version_client_max", + "tls_version_server_min", + "tls_version_server_max", + ]: + caplog.clear() + tctx.configure(ta, **{attr: "SSL3"}) + assert ( + f"{attr} has been set to SSL3, " + "which is not supported by the current OpenSSL build." + ) in caplog.text + caplog.clear() + tctx.configure(ta, tls_version_client_min="UNBOUNDED") + assert ( + "tls_version_client_min has been set to UNBOUNDED. " + "Note that your OpenSSL build only supports the following TLS versions" + ) in caplog.text + + def test_configure_ciphers(self, caplog): + caplog.set_level(logging.INFO) + ta = tlsconfig.TlsConfig() + with taddons.context(ta) as tctx: + tctx.configure( + ta, + tls_version_client_min="TLS1", + ciphers_client="ALL", + ) + assert ( + "With tls_version_client_min set to TLS1, " + 'ciphers_client must include "@SECLEVEL=0" for insecure TLS versions to work.' + ) in caplog.text + caplog.clear() + + tctx.configure( + ta, + ciphers_server="ALL", + ) + assert not caplog.text + tctx.configure( + ta, + tls_version_server_min="SSL3", + ) + assert ( + "With tls_version_server_min set to SSL3, " + 'ciphers_server must include "@SECLEVEL=0" for insecure TLS versions to work.' + ) in caplog.text + def test_get_cert(self, tdata): """Test that we generate a certificate matching the connection's context.""" ta = tlsconfig.TlsConfig() with taddons.context(ta) as tctx: ta.configure(["confdir"]) - ctx = context.Context( - connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), - tctx.options, - ) + ctx = _ctx(tctx.options) # Edge case first: We don't have _any_ idea about the server nor is there a SNI, # so we just return our local IP as subject. @@ -111,39 +183,43 @@ def test_get_cert(self, tdata): ctx.server.certificate_list = [certs.Cert.from_pem(f.read())] entry = ta.get_cert(ctx) assert entry.cert.cn == "example.mitmproxy.org" - assert entry.cert.altnames == [ - "example.mitmproxy.org", - "server-address.example", - ] + assert entry.cert.altnames == x509.GeneralNames( + [ + x509.DNSName("example.mitmproxy.org"), + x509.IPAddress(ipaddress.ip_address("127.0.0.1")), + x509.DNSName("server-address.example"), + ] + ) # And now we also incorporate SNI. - ctx.client.sni = "sni.example" + ctx.client.sni = "🌈.sni.example" entry = ta.get_cert(ctx) - assert entry.cert.altnames == ["example.mitmproxy.org", "sni.example"] + assert entry.cert.altnames == x509.GeneralNames( + [ + x509.DNSName("example.mitmproxy.org"), + x509.DNSName("xn--og8h.sni.example"), + x509.DNSName("server-address.example"), + ] + ) with open(tdata.path("mitmproxy/data/invalid-subject.pem"), "rb") as f: ctx.server.certificate_list = [certs.Cert.from_pem(f.read())] - with pytest.warns( - UserWarning, match="Country names should be two characters" - ): + with pytest.warns(UserWarning): assert ta.get_cert(ctx) # does not raise def test_tls_clienthello(self): # only really testing for coverage here, there's no point in mirroring the individual conditions ta = tlsconfig.TlsConfig() with taddons.context(ta) as tctx: - ctx = context.Context( - connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), - tctx.options, - ) + ctx = _ctx(tctx.options) ch = tls.ClientHelloData(ctx, None) # type: ignore ta.tls_clienthello(ch) assert not ch.establish_server_tls_first def do_handshake( self, - tssl_client: Union[test_tls.SSLTest, SSL.Connection], - tssl_server: Union[test_tls.SSLTest, SSL.Connection], + tssl_client: test_tls.SSLTest | SSL.Connection, + tssl_server: test_tls.SSLTest | SSL.Connection, ) -> bool: # ClientHello with pytest.raises((ssl.SSLWantReadError, SSL.WantReadError)): @@ -162,6 +238,16 @@ def do_handshake( return True + def quic_do_handshake( + self, + tssl_client: test_quic.SSLTest, + tssl_server: test_quic.SSLTest, + ) -> bool: + tssl_server.write(tssl_client.read()) + tssl_client.write(tssl_server.read()) + tssl_server.write(tssl_client.read()) + return tssl_client.handshake_completed() and tssl_server.handshake_completed() + def test_tls_start_client(self, tdata): ta = tlsconfig.TlsConfig() with taddons.context(ta) as tctx: @@ -173,10 +259,7 @@ def test_tls_start_client(self, tdata): ], ciphers_client="ECDHE-ECDSA-AES128-GCM-SHA256", ) - ctx = context.Context( - connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), - tctx.options, - ) + ctx = _ctx(tctx.options) tls_start = tls.TlsData(ctx.client, context=ctx) ta.tls_start_client(tls_start) @@ -192,27 +275,55 @@ def test_tls_start_client(self, tdata): ("DNS", "example.mitmproxy.org"), ) - def test_tls_start_server_cannot_verify(self): + def test_quic_start_client(self, tdata): ta = tlsconfig.TlsConfig() with taddons.context(ta) as tctx: - ctx = context.Context( - connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), - tctx.options, + ta.configure(["confdir"]) + tctx.configure( + ta, + certs=[ + tdata.path("mitmproxy/net/data/verificationcerts/trusted-leaf.pem") + ], + ciphers_client="CHACHA20_POLY1305_SHA256", + ) + ctx = _ctx(tctx.options) + + tls_start = quic.QuicTlsData(ctx.client, context=ctx) + ta.quic_start_client(tls_start) + settings_server = tls_start.settings + settings_server.alpn_protocols = ["h3"] + tssl_server = test_quic.SSLTest(server_side=True, settings=settings_server) + + # assert that a preexisting settings is not overwritten + ta.quic_start_client(tls_start) + assert settings_server is tls_start.settings + + tssl_client = test_quic.SSLTest(alpn=["h3"]) + assert self.quic_do_handshake(tssl_client, tssl_server) + san = tssl_client.quic.tls._peer_certificate.extensions.get_extension_for_class( + x509.SubjectAlternativeName ) + assert san.value.get_values_for_type(x509.DNSName) == [ + "example.mitmproxy.org" + ] + + def test_tls_start_server_cannot_verify(self): + ta = tlsconfig.TlsConfig() + with taddons.context(ta) as tctx: + ctx = _ctx(tctx.options) ctx.server.address = ("example.mitmproxy.org", 443) ctx.server.sni = "" # explicitly opt out of using the address. tls_start = tls.TlsData(ctx.server, context=ctx) - with pytest.raises(ValueError, match="Cannot validate certificate hostname without SNI"): + with pytest.raises( + ValueError, match="Cannot validate certificate hostname without SNI" + ): ta.tls_start_server(tls_start) def test_tls_start_server_verify_failed(self): ta = tlsconfig.TlsConfig() with taddons.context(ta) as tctx: - ctx = context.Context( - connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), - tctx.options, - ) + ctx = _ctx(tctx.options) ctx.client.alpn_offers = [b"h2"] ctx.client.cipher_list = ["TLS_AES_256_GCM_SHA384", "ECDHE-RSA-AES128-SHA"] ctx.server.address = ("example.mitmproxy.org", 443) @@ -228,10 +339,7 @@ def test_tls_start_server_verify_failed(self): def test_tls_start_server_verify_ok(self, hostname, tdata): ta = tlsconfig.TlsConfig() with taddons.context(ta) as tctx: - ctx = context.Context( - connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), - tctx.options, - ) + ctx = _ctx(tctx.options) ctx.server.address = (hostname, 443) tctx.configure( ta, @@ -251,13 +359,38 @@ def test_tls_start_server_verify_ok(self, hostname, tdata): tssl_server = test_tls.SSLTest(server_side=True, sni=hostname.encode()) assert self.do_handshake(tssl_client, tssl_server) - def test_tls_start_server_insecure(self): + @pytest.mark.parametrize("hostname", ["example.mitmproxy.org", "192.0.2.42"]) + def test_quic_start_server_verify_ok(self, hostname, tdata): ta = tlsconfig.TlsConfig() with taddons.context(ta) as tctx: - ctx = context.Context( - connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), - tctx.options, + ctx = _ctx(tctx.options) + ctx.server.address = (hostname, 443) + tctx.configure( + ta, + ssl_verify_upstream_trusted_ca=tdata.path( + "mitmproxy/net/data/verificationcerts/trusted-root.crt" + ), ) + + tls_start = quic.QuicTlsData(ctx.server, context=ctx) + ta.quic_start_server(tls_start) + settings_client = tls_start.settings + settings_client.alpn_protocols = ["h3"] + tssl_client = test_quic.SSLTest(settings=settings_client) + + # assert that a preexisting ssl_conn is not overwritten + ta.quic_start_server(tls_start) + assert settings_client is tls_start.settings + + tssl_server = test_quic.SSLTest( + server_side=True, sni=hostname.encode(), alpn=["h3"] + ) + assert self.quic_do_handshake(tssl_client, tssl_server) + + def test_tls_start_server_insecure(self): + ta = tlsconfig.TlsConfig() + with taddons.context(ta) as tctx: + ctx = _ctx(tctx.options) ctx.server.address = ("example.mitmproxy.org", 443) tctx.configure( @@ -273,13 +406,29 @@ def test_tls_start_server_insecure(self): tssl_server = test_tls.SSLTest(server_side=True) assert self.do_handshake(tssl_client, tssl_server) - def test_alpn_selection(self): + def test_quic_start_server_insecure(self): ta = tlsconfig.TlsConfig() with taddons.context(ta) as tctx: - ctx = context.Context( - connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), - tctx.options, + ctx = _ctx(tctx.options) + ctx.server.address = ("example.mitmproxy.org", 443) + ctx.client.alpn_offers = [b"h3"] + + tctx.configure( + ta, + ssl_verify_upstream_trusted_ca=None, + ssl_insecure=True, + ciphers_server="CHACHA20_POLY1305_SHA256", ) + tls_start = quic.QuicTlsData(ctx.server, context=ctx) + ta.quic_start_server(tls_start) + tssl_client = test_quic.SSLTest(settings=tls_start.settings) + tssl_server = test_quic.SSLTest(server_side=True, alpn=["h3"]) + assert self.quic_do_handshake(tssl_client, tssl_server) + + def test_alpn_selection(self): + ta = tlsconfig.TlsConfig() + with taddons.context(ta) as tctx: + ctx = _ctx(tctx.options) ctx.server.address = ("example.mitmproxy.org", 443) tls_start = tls.TlsData(ctx.server, context=ctx) @@ -292,12 +441,14 @@ def assert_alpn(http2, client_offers, expected): assert ctx.server.alpn_offers == expected assert_alpn( - True, proxy_tls.HTTP_ALPNS + (b"foo",), proxy_tls.HTTP_ALPNS + (b"foo",) + True, + (proxy_tls.HTTP2_ALPN, *proxy_tls.HTTP1_ALPNS, b"foo"), + (proxy_tls.HTTP2_ALPN, *proxy_tls.HTTP1_ALPNS, b"foo"), ) assert_alpn( False, - proxy_tls.HTTP_ALPNS + (b"foo",), - proxy_tls.HTTP1_ALPNS + (b"foo",), + (proxy_tls.HTTP2_ALPN, *proxy_tls.HTTP1_ALPNS, b"foo"), + (*proxy_tls.HTTP1_ALPNS, b"foo"), ) assert_alpn(True, [], []) assert_alpn(False, [], []) @@ -319,10 +470,7 @@ def test_no_h2_proxy(self, tdata): ], ) - ctx = context.Context( - connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), - tctx.options, - ) + ctx = _ctx(tctx.options) # mock up something that looks like a secure web proxy. ctx.layers = [modes.HttpProxy(ctx), 123] tls_start = tls.TlsData(ctx.client, context=ctx) @@ -339,10 +487,7 @@ def test_no_h2_proxy(self, tdata): def test_client_cert_file(self, tdata, client_certs): ta = tlsconfig.TlsConfig() with taddons.context(ta) as tctx: - ctx = context.Context( - connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), - tctx.options, - ) + ctx = _ctx(tctx.options) ctx.server.address = ("example.mitmproxy.org", 443) tctx.configure( ta, @@ -366,3 +511,17 @@ async def test_ca_expired(self, monkeypatch, caplog): with taddons.context(ta): ta.configure(["confdir"]) assert "The mitmproxy certificate authority has expired" in caplog.text + + +def test_default_ciphers(): + assert ( + tlsconfig._default_ciphers(net_tls.Version.TLS1_3) == tlsconfig._DEFAULT_CIPHERS + ) + assert ( + tlsconfig._default_ciphers(net_tls.Version.SSL3) + == tlsconfig._DEFAULT_CIPHERS_WITH_SECLEVEL_0 + ) + assert ( + tlsconfig._default_ciphers(net_tls.Version.UNBOUNDED) + == tlsconfig._DEFAULT_CIPHERS_WITH_SECLEVEL_0 + ) diff --git a/test/mitmproxy/addons/test_update_alt_svc.py b/test/mitmproxy/addons/test_update_alt_svc.py new file mode 100644 index 0000000000..e0509d2e80 --- /dev/null +++ b/test/mitmproxy/addons/test_update_alt_svc.py @@ -0,0 +1,44 @@ +from mitmproxy import http +from mitmproxy.addons import update_alt_svc +from mitmproxy.proxy.mode_specs import ProxyMode +from mitmproxy.test import taddons +from mitmproxy.test import tflow + + +def test_simple(): + header = 'h3="example.com:443"; ma=3600, h2=":443"; ma=3600' + modified = update_alt_svc.update_alt_svc_header(header, 1234) + assert modified == 'h3=":1234"; ma=3600, h2=":1234"; ma=3600' + + +def test_updates_alt_svc_header(): + upd = update_alt_svc.UpdateAltSvc() + with taddons.context(upd) as ctx: + headers = http.Headers( + host="example.com", + content_type="application/xml", + alt_svc='h3="example.com:443"; ma=3600, h2=":443"; ma=3600', + ) + resp = tflow.tresp(headers=headers) + f = tflow.tflow(resp=resp) + f.client_conn.sockname = ("", 1234) + + upd.responseheaders(f) + assert ( + f.response.headers["alt-svc"] + == 'h3="example.com:443"; ma=3600, h2=":443"; ma=3600' + ) + + ctx.options.keep_alt_svc_header = True + f.client_conn.proxy_mode = ProxyMode.parse("reverse:https://example.com") + upd.responseheaders(f) + assert ( + f.response.headers["alt-svc"] + == 'h3="example.com:443"; ma=3600, h2=":443"; ma=3600' + ) + + ctx.options.keep_alt_svc_header = False + upd.responseheaders(f) + assert ( + f.response.headers["alt-svc"] == 'h3=":1234"; ma=3600, h2=":1234"; ma=3600' + ) diff --git a/test/mitmproxy/addons/test_upstream_auth.py b/test/mitmproxy/addons/test_upstream_auth.py index 1eb8eb0131..883dabc2f3 100644 --- a/test/mitmproxy/addons/test_upstream_auth.py +++ b/test/mitmproxy/addons/test_upstream_auth.py @@ -1,11 +1,12 @@ import base64 + import pytest from mitmproxy import exceptions +from mitmproxy.addons import upstream_auth from mitmproxy.proxy.mode_specs import ProxyMode from mitmproxy.test import taddons from mitmproxy.test import tflow -from mitmproxy.addons import upstream_auth def test_configure(): diff --git a/test/mitmproxy/addons/test_view.py b/test/mitmproxy/addons/test_view.py index 7c1b041cf6..5ee0873c8f 100644 --- a/test/mitmproxy/addons/test_view.py +++ b/test/mitmproxy/addons/test_view.py @@ -1,14 +1,14 @@ import pytest -from mitmproxy.test import tflow - -from mitmproxy.addons import view -from mitmproxy import flowfilter from mitmproxy import exceptions +from mitmproxy import flowfilter from mitmproxy import io +from mitmproxy.addons import view from mitmproxy.test import taddons +from mitmproxy.test import tflow from mitmproxy.tools.console import consoleaddons -from mitmproxy.tools.console.common import render_marker, SYMBOL_MARK +from mitmproxy.tools.console.common import render_marker +from mitmproxy.tools.console.common import SYMBOL_MARK def tft(*, method="get", start=0): @@ -303,16 +303,16 @@ def test_resolve(): v.set_filter(f) v[0].marked = True - def m(l): - return [i.request.method for i in l] + def methods(flows): + return [i.request.method for i in flows] - assert m(tctx.command(v.resolve, "~m get")) == ["GET", "GET"] - assert m(tctx.command(v.resolve, "~m put")) == ["PUT", "PUT"] - assert m(tctx.command(v.resolve, "@shown")) == ["GET", "GET"] - assert m(tctx.command(v.resolve, "@hidden")) == ["PUT", "PUT"] - assert m(tctx.command(v.resolve, "@marked")) == ["GET"] - assert m(tctx.command(v.resolve, "@unmarked")) == ["PUT", "GET", "PUT"] - assert m(tctx.command(v.resolve, "@all")) == ["GET", "PUT", "GET", "PUT"] + assert methods(tctx.command(v.resolve, "~m get")) == ["GET", "GET"] + assert methods(tctx.command(v.resolve, "~m put")) == ["PUT", "PUT"] + assert methods(tctx.command(v.resolve, "@shown")) == ["GET", "GET"] + assert methods(tctx.command(v.resolve, "@hidden")) == ["PUT", "PUT"] + assert methods(tctx.command(v.resolve, "@marked")) == ["GET"] + assert methods(tctx.command(v.resolve, "@unmarked")) == ["PUT", "GET", "PUT"] + assert methods(tctx.command(v.resolve, "@all")) == ["GET", "PUT", "GET", "PUT"] with pytest.raises(exceptions.CommandError, match="Invalid filter expression"): tctx.command(v.resolve, "~") @@ -682,7 +682,7 @@ def test_configure(): [ [":default:", SYMBOL_MARK], ["X", "X"], - [":grapes:", "\N{grapes}"], + [":grapes:", "\N{GRAPES}"], [":not valid:", SYMBOL_MARK], [":weird", SYMBOL_MARK], ], diff --git a/test/mitmproxy/contentviews/image/test_image_parser.py b/test/mitmproxy/contentviews/image/test_image_parser.py index cd97d53390..c4180b6f21 100644 --- a/test/mitmproxy/contentviews/image/test_image_parser.py +++ b/test/mitmproxy/contentviews/image/test_image_parser.py @@ -189,6 +189,7 @@ def test_parse_jpeg(filename, metadata, tdata): assert metadata == image_parser.parse_jpeg(f.read()) +# fmt: off @pytest.mark.parametrize( "filename, metadata", { @@ -219,3 +220,4 @@ def test_parse_jpeg(filename, metadata, tdata): def test_ico(filename, metadata, tdata): with open(tdata.path(filename), "rb") as f: assert metadata == image_parser.parse_ico(f.read()) +# fmt: on diff --git a/test/mitmproxy/contentviews/image/test_view.py b/test/mitmproxy/contentviews/image/test_view.py index 67c4b81b4e..61c0c6379e 100644 --- a/test/mitmproxy/contentviews/image/test_view.py +++ b/test/mitmproxy/contentviews/image/test_view.py @@ -1,5 +1,5 @@ -from mitmproxy.contentviews import image from .. import full_eval +from mitmproxy.contentviews import image def test_view_image(tdata): diff --git a/test/mitmproxy/contentviews/test_api.py b/test/mitmproxy/contentviews/test_api.py index c943610b07..259343fab3 100644 --- a/test/mitmproxy/contentviews/test_api.py +++ b/test/mitmproxy/contentviews/test_api.py @@ -81,6 +81,18 @@ def test_get_message_content_view(): desc, lines, err = contentviews.get_message_content_view("raw", r, f) assert desc == "[cannot decode] Raw" + del r.headers["content-encoding"] + r.headers["content-type"] = "multipart/form-data; boundary=AaB03x" + r.content = b""" +--AaB03x +Content-Disposition: form-data; name="submit-name" + +Larry +--AaB03x + """.strip() + desc, lines, err = contentviews.get_message_content_view("multipart form", r, f) + assert desc == "Multipart form" + r.content = None desc, lines, err = contentviews.get_message_content_view("raw", r, f) assert list(lines) == [[("error", "content missing")]] diff --git a/test/mitmproxy/contentviews/test_auto.py b/test/mitmproxy/contentviews/test_auto.py index 459d839f0a..63f41754da 100644 --- a/test/mitmproxy/contentviews/test_auto.py +++ b/test/mitmproxy/contentviews/test_auto.py @@ -1,6 +1,6 @@ +from . import full_eval from mitmproxy.contentviews import auto from mitmproxy.test import tflow -from . import full_eval def test_view_auto(): @@ -46,8 +46,8 @@ def test_view_auto(): ) assert f[0] == "Unknown Image" - f = v(b"\xFF" * 30) - assert f[0] == "Hex" + f = v(b"\xff" * 30) + assert f[0] == "Hexdump" f = v( b"", diff --git a/test/mitmproxy/contentviews/test_base.py b/test/mitmproxy/contentviews/test_base.py index cd879bfdaa..efa971534e 100644 --- a/test/mitmproxy/contentviews/test_base.py +++ b/test/mitmproxy/contentviews/test_base.py @@ -1,4 +1,5 @@ import pytest + from mitmproxy.contentviews import base diff --git a/test/mitmproxy/contentviews/test_css.py b/test/mitmproxy/contentviews/test_css.py index 7474a6b36f..a2192d12f0 100644 --- a/test/mitmproxy/contentviews/test_css.py +++ b/test/mitmproxy/contentviews/test_css.py @@ -1,7 +1,7 @@ import pytest -from mitmproxy.contentviews import css from . import full_eval +from mitmproxy.contentviews import css @pytest.mark.parametrize( diff --git a/test/mitmproxy/contentviews/test_dns.py b/test/mitmproxy/contentviews/test_dns.py new file mode 100644 index 0000000000..f10296ade6 --- /dev/null +++ b/test/mitmproxy/contentviews/test_dns.py @@ -0,0 +1,21 @@ +from . import full_eval +from mitmproxy.contentviews import dns + +DNS_HTTPS_RECORD_RESPONSE = bytes.fromhex( + "00008180000100010000000107746c732d656368036465760000410001c00c004100010000003c00520001000005004b0049fe0d00" + "452b00200020015881d41a3e2ef8f2208185dc479245d20624ddd0918a8056f2e26af47e2628000800010001000100034012707562" + "6c69632e746c732d6563682e646576000000002904d0000000000000" +) + + +def test_simple(): + v = full_eval(dns.ViewDns()) + assert v(DNS_HTTPS_RECORD_RESPONSE) + assert not v(b"foobar") + + +def test_render_priority(): + v = dns.ViewDns() + assert v.render_priority(b"", content_type="application/dns-message") + assert not v.render_priority(b"", content_type="text/plain") + assert not v.render_priority(b"") diff --git a/test/mitmproxy/contentviews/test_graphql.py b/test/mitmproxy/contentviews/test_graphql.py index a38eedea00..f10af244ff 100644 --- a/test/mitmproxy/contentviews/test_graphql.py +++ b/test/mitmproxy/contentviews/test_graphql.py @@ -1,8 +1,8 @@ from hypothesis import given from hypothesis.strategies import binary -from mitmproxy.contentviews import graphql from . import full_eval +from mitmproxy.contentviews import graphql def test_render_priority(): @@ -19,6 +19,7 @@ def test_render_priority(): assert 0 == v.render_priority( b"""[{"xquery": "query P { \\n }"}]""", content_type="application/json" ) + assert 0 == v.render_priority(b"""[]""", content_type="application/json") assert 0 == v.render_priority(b"}", content_type="application/json") diff --git a/test/mitmproxy/contentviews/test_grpc.py b/test/mitmproxy/contentviews/test_grpc.py index 8d296fe65d..6a88035260 100644 --- a/test/mitmproxy/contentviews/test_grpc.py +++ b/test/mitmproxy/contentviews/test_grpc.py @@ -1,16 +1,16 @@ +import struct + import pytest +from . import full_eval from mitmproxy.contentviews import grpc -from mitmproxy.contentviews.grpc import ( - ViewGrpcProtobuf, - ViewConfig, - ProtoParser, - parse_grpc_messages, -) +from mitmproxy.contentviews.grpc import parse_grpc_messages +from mitmproxy.contentviews.grpc import ProtoParser +from mitmproxy.contentviews.grpc import ViewConfig +from mitmproxy.contentviews.grpc import ViewGrpcProtobuf from mitmproxy.net.encoding import encode -from mitmproxy.test import tflow, tutils -import struct -from . import full_eval +from mitmproxy.test import tflow +from mitmproxy.test import tutils datadir = "mitmproxy/contentviews/test_grpc_data/" diff --git a/test/mitmproxy/contentviews/test_hex.py b/test/mitmproxy/contentviews/test_hex.py index 90db4bd7c1..201aa14275 100644 --- a/test/mitmproxy/contentviews/test_hex.py +++ b/test/mitmproxy/contentviews/test_hex.py @@ -1,14 +1,26 @@ -from mitmproxy.contentviews import hex from . import full_eval +from mitmproxy.contentviews import hex + + +class TestHexDump: + def test_view_hex(self): + v = full_eval(hex.ViewHexDump()) + assert v(b"foo") + def test_render_priority(self): + v = hex.ViewHexDump() + assert not v.render_priority(b"ascii") + assert v.render_priority(b"\xff") + assert not v.render_priority(b"") -def test_view_hex(): - v = full_eval(hex.ViewHex()) - assert v(b"foo") +class TestHexStream: + def test_view_hex(self): + v = full_eval(hex.ViewHexStream()) + assert v(b"foo") -def test_render_priority(): - v = hex.ViewHex() - assert not v.render_priority(b"ascii") - assert v.render_priority(b"\xFF") - assert not v.render_priority(b"") + def test_render_priority(self): + v = hex.ViewHexStream() + assert not v.render_priority(b"ascii") + assert v.render_priority(b"\xff") + assert not v.render_priority(b"") diff --git a/test/mitmproxy/contentviews/test_http3.py b/test/mitmproxy/contentviews/test_http3.py index 157ee6914a..0ffc9c1115 100644 --- a/test/mitmproxy/contentviews/test_http3.py +++ b/test/mitmproxy/contentviews/test_http3.py @@ -1,58 +1,56 @@ import pytest +from . import full_eval +from mitmproxy.contentviews import http3 from mitmproxy.tcp import TCPMessage from mitmproxy.test import tflow -from mitmproxy.contentviews import http3 - -from . import full_eval - -if http3 is None: - pytest.skip("HTTP/3 not available.", allow_module_level=True) -@pytest.mark.parametrize("data", [ - # HEADERS - b"\x01\x1d\x00\x00\xd1\xc1\xd7P\x8a\x08\x9d\\\x0b\x81p\xdcx\x0f\x03_P\x88%\xb6P\xc3\xab\xbc\xda\xe0\xdd", - # broken HEADERS - b"\x01\x1d\x00\x00\xd1\xc1\xd7P\x8a\x08\x9d\\\x0b\x81p\xdcx\x0f\x03_P\x88%\xb6P\xc3\xab\xff\xff\xff\xff", - # headers + data - ( - b'\x01@I\x00\x00\xdb_\'\x93I|\xa5\x89\xd3M\x1fj\x12q\xd8\x82\xa6\x0bP\xb0\xd0C\x1b_M\x90\xd0bXt\x1eT\xad\x8f~\xfdp' - b'\xeb\xc8\xc0\x97\x07V\x96\xd0z\xbe\x94\x08\x94\xdcZ\xd4\x10\x04%\x02\xe5\xc6\xde\xb8\x17\x14\xc5\xa3\x7fT\x03315' - b'\x00A;\r\n<' - b'TITLE>Not Found\r\n\r\n

    Not Found

    \r\n

    HTTP Error 404. The requested resource is not found.

    \r\n\r\n' - ), - b"", -]) +@pytest.mark.parametrize( + "data", + [ + # HEADERS + b"\x01\x1d\x00\x00\xd1\xc1\xd7P\x8a\x08\x9d\\\x0b\x81p\xdcx\x0f\x03_P\x88%\xb6P\xc3\xab\xbc\xda\xe0\xdd", + # broken HEADERS + b"\x01\x1d\x00\x00\xd1\xc1\xd7P\x8a\x08\x9d\\\x0b\x81p\xdcx\x0f\x03_P\x88%\xb6P\xc3\xab\xff\xff\xff\xff", + # headers + data + ( + b"\x01@I\x00\x00\xdb_'\x93I|\xa5\x89\xd3M\x1fj\x12q\xd8\x82\xa6\x0bP\xb0\xd0C\x1b_M\x90\xd0bXt\x1eT\xad\x8f~\xfdp" + b"\xeb\xc8\xc0\x97\x07V\x96\xd0z\xbe\x94\x08\x94\xdcZ\xd4\x10\x04%\x02\xe5\xc6\xde\xb8\x17\x14\xc5\xa3\x7fT\x03315" + b'\x00A;\r\n<' + b'TITLE>Not Found\r\n\r\n

    Not Found

    \r\n

    HTTP Error 404. The requested resource is not found.

    \r\n\r\n" + ), + b"", + ], +) def test_view_http3(data): v = full_eval(http3.ViewHttp3()) - t = tflow.ttcpflow(messages=[ - TCPMessage(from_client=len(data) > 16, content=data) - ]) + t = tflow.ttcpflow(messages=[TCPMessage(from_client=len(data) > 16, content=data)]) t.metadata["quic_is_unidirectional"] = False - assert (v(b"", flow=t, tcp_message=t.messages[0])) - - -@pytest.mark.parametrize("data", [ - # SETTINGS - b"\x00\x04\r\x06\xff\xff\xff\xff\xff\xff\xff\xff\x01\x00\x07\x00", - # unknown setting - b"\x00\x04\r\x3f\xff\xff\xff\xff\xff\xff\xff\xff\x01\x00\x07\x00", - # out of bounds - b"\x00\x04\r\x06\xff\xff\xff\xff\xff\xff\xff\xff\x01\x00\x42\x00", - # incomplete - b"\x00\x04\r\x06\xff\xff\xff", - # QPACK encoder stream - b"\x02", -]) + assert v(b"", flow=t, tcp_message=t.messages[0]) + + +@pytest.mark.parametrize( + "data", + [ + # SETTINGS + b"\x00\x04\r\x06\xff\xff\xff\xff\xff\xff\xff\xff\x01\x00\x07\x00", + # unknown setting + b"\x00\x04\r\x3f\xff\xff\xff\xff\xff\xff\xff\xff\x01\x00\x07\x00", + # out of bounds + b"\x00\x04\r\x06\xff\xff\xff\xff\xff\xff\xff\xff\x01\x00\x42\x00", + # incomplete + b"\x00\x04\r\x06\xff\xff\xff", + # QPACK encoder stream + b"\x02", + ], +) def test_view_http3_unidirectional(data): v = full_eval(http3.ViewHttp3()) - t = tflow.ttcpflow(messages=[ - TCPMessage(from_client=len(data) > 16, content=data) - ]) + t = tflow.ttcpflow(messages=[TCPMessage(from_client=len(data) > 16, content=data)]) t.metadata["quic_is_unidirectional"] = True - assert (v(b"", flow=t, tcp_message=t.messages[0])) + assert v(b"", flow=t, tcp_message=t.messages[0]) def test_render_priority(): diff --git a/test/mitmproxy/contentviews/test_javascript.py b/test/mitmproxy/contentviews/test_javascript.py index c050adee4f..64647446d2 100644 --- a/test/mitmproxy/contentviews/test_javascript.py +++ b/test/mitmproxy/contentviews/test_javascript.py @@ -1,7 +1,7 @@ import pytest -from mitmproxy.contentviews import javascript from . import full_eval +from mitmproxy.contentviews import javascript def test_view_javascript(): diff --git a/test/mitmproxy/contentviews/test_json.py b/test/mitmproxy/contentviews/test_json.py index 5b3883060f..abfbc461f4 100644 --- a/test/mitmproxy/contentviews/test_json.py +++ b/test/mitmproxy/contentviews/test_json.py @@ -1,8 +1,8 @@ from hypothesis import given from hypothesis.strategies import binary -from mitmproxy.contentviews import json from . import full_eval +from mitmproxy.contentviews import json def test_parse_json(): @@ -12,37 +12,88 @@ def test_parse_json(): assert json.parse_json( b'{"foo" : "\xe4\xb8\x96\xe7\x95\x8c"}' ) # utf8 with chinese characters - assert json.parse_json(b'{"foo" : "\xFF"}') is json.PARSE_ERROR + assert json.parse_json(b'{"foo" : "\xff"}') is json.PARSE_ERROR def test_format_json(): assert list(json.format_json({"data": ["str", 42, True, False, None, {}, []]})) assert list(json.format_json({"string": "test"})) == [ - [('text', '{'), ('text', '')], - [('text', ' '), ('Token_Name_Tag', '"string"'), ('text', ': '), ('Token_Literal_String', '"test"'), ('text', '')], - [('text', ''), ('text', '}')]] + [("text", "{"), ("text", "")], + [ + ("text", " "), + ("Token_Name_Tag", '"string"'), + ("text", ": "), + ("Token_Literal_String", '"test"'), + ("text", ""), + ], + [("text", ""), ("text", "}")], + ] assert list(json.format_json({"num": 4})) == [ - [('text', '{'), ('text', '')], - [('text', ' '), ('Token_Name_Tag', '"num"'), ('text', ': '), ('Token_Literal_Number', '4'), ('text', '')], - [('text', ''), ('text', '}')]] + [("text", "{"), ("text", "")], + [ + ("text", " "), + ("Token_Name_Tag", '"num"'), + ("text", ": "), + ("Token_Literal_Number", "4"), + ("text", ""), + ], + [("text", ""), ("text", "}")], + ] assert list(json.format_json({"bool": True})) == [ - [('text', '{'), ('text', '')], - [('text', ' '), ('Token_Name_Tag', '"bool"'), ('text', ': '), ('Token_Keyword_Constant', 'true'), ('text', '')], - [('text', ''), ('text', '}')]] + [("text", "{"), ("text", "")], + [ + ("text", " "), + ("Token_Name_Tag", '"bool"'), + ("text", ": "), + ("Token_Keyword_Constant", "true"), + ("text", ""), + ], + [("text", ""), ("text", "}")], + ] assert list(json.format_json({"object": {"int": 1}})) == [ - [('text', '{'), ('text', '')], - [('text', ' '), ('Token_Name_Tag', '"object"'), ('text', ': '), ('text', '{'), ('text', '')], - [('text', ' '), ('Token_Name_Tag', '"int"'), ('text', ': '), ('Token_Literal_Number', '1'), ('text', '')], - [('text', ' '), ('text', '}'), ('text', '')], - [('text', ''), ('text', '}')]] + [("text", "{"), ("text", "")], + [ + ("text", " "), + ("Token_Name_Tag", '"object"'), + ("text", ": "), + ("text", "{"), + ("text", ""), + ], + [ + ("text", " "), + ("Token_Name_Tag", '"int"'), + ("text", ": "), + ("Token_Literal_Number", "1"), + ("text", ""), + ], + [("text", " "), ("text", "}"), ("text", "")], + [("text", ""), ("text", "}")], + ] assert list(json.format_json({"list": ["string", 1, True]})) == [ - [('text', '{'), ('text', '')], - [('text', ' '), ('Token_Name_Tag', '"list"'), ('text', ': '), ('text', '[')], - [('Token_Literal_String', ' "string"'), ('text', ',')], - [('Token_Literal_Number', ' 1'), ('text', ',')], - [('Token_Keyword_Constant', ' true'), ('text', '')], - [('text', ' '), ('text', ']'), ('text', '')], - [('text', ''), ('text', '}')]] + [("text", "{"), ("text", "")], + [("text", " "), ("Token_Name_Tag", '"list"'), ("text", ": "), ("text", "[")], + [("Token_Literal_String", ' "string"'), ("text", ",")], + [("Token_Literal_Number", " 1"), ("text", ",")], + [("Token_Keyword_Constant", " true"), ("text", "")], + [("text", " "), ("text", "]"), ("text", "")], + [("text", ""), ("text", "}")], + ] + assert list(json.format_json({"list": []})) == [ + [("text", "{"), ("text", "")], + [ + ("text", " "), + ("Token_Name_Tag", '"list"'), + ("text", ": "), + ("text", "[]"), + ("text", ""), + ], + [("text", ""), ("text", "}")], + ] + assert list(json.format_json(None)) == [[("Token_Keyword_Constant", "null")]] + assert list(json.format_json(True)) == [[("Token_Keyword_Constant", "true")]] + assert list(json.format_json(1)) == [[("Token_Literal_Number", "1")]] + assert list(json.format_json("test")) == [[("Token_Literal_String", '"test"')]] + assert list(json.format_json([])) == [[("text", "[]")]] def test_view_json(): @@ -53,6 +104,7 @@ def test_view_json(): assert v(b"[1, 2, 3, 4, 5]") assert v(b'{"foo" : 3}') assert v(b'{"foo": true, "nullvalue": null}') + assert v(b"[]") @given(binary()) diff --git a/test/mitmproxy/contentviews/test_mqtt.py b/test/mitmproxy/contentviews/test_mqtt.py index 7acc335412..9c966eb25a 100644 --- a/test/mitmproxy/contentviews/test_mqtt.py +++ b/test/mitmproxy/contentviews/test_mqtt.py @@ -1,16 +1,22 @@ import pytest -from mitmproxy.contentviews import mqtt from . import full_eval +from mitmproxy.contentviews import mqtt @pytest.mark.parametrize( "data,expected_text", [ - pytest.param(b"\xC0\x00", "[PINGREQ]", id="PINGREQ"), - pytest.param(b"\xD0\x00", "[PINGRESP]", id="PINGRESP"), - pytest.param(b"\x90\x00", "Packet type SUBACK is not supported yet!", id="SUBACK"), - pytest.param(b"\xA0\x00", "Packet type UNSUBSCRIBE is not supported yet!", id="UNSUBSCRIBE"), + pytest.param(b"\xc0\x00", "[PINGREQ]", id="PINGREQ"), + pytest.param(b"\xd0\x00", "[PINGRESP]", id="PINGRESP"), + pytest.param( + b"\x90\x00", "Packet type SUBACK is not supported yet!", id="SUBACK" + ), + pytest.param( + b"\xa0\x00", + "Packet type UNSUBSCRIBE is not supported yet!", + id="UNSUBSCRIBE", + ), pytest.param( b"\x82\x31\x00\x03\x00\x2cxxxx/yy/zzzzzz/56:6F:5E:6A:01:05/messages/in\x01", "[SUBSCRIBE] sent topic filters: 'xxxx/yy/zzzzzz/56:6F:5E:6A:01:05/messages/in'", @@ -52,10 +58,7 @@ def test_view_mqtt(data, expected_text): assert output == [[("text", expected_text)]] -@pytest.mark.parametrize( - "data", - [b"\xC0\xFF\xFF\xFF\xFF"] -) +@pytest.mark.parametrize("data", [b"\xc0\xff\xff\xff\xff"]) def test_mqtt_malformed(data): v = full_eval(mqtt.ViewMQTT()) with pytest.raises(Exception): diff --git a/test/mitmproxy/contentviews/test_msgpack.py b/test/mitmproxy/contentviews/test_msgpack.py index eeba8b2d13..d3c53d3a97 100644 --- a/test/mitmproxy/contentviews/test_msgpack.py +++ b/test/mitmproxy/contentviews/test_msgpack.py @@ -1,10 +1,9 @@ from hypothesis import given from hypothesis.strategies import binary - from msgpack import packb -from mitmproxy.contentviews import msgpack from . import full_eval +from mitmproxy.contentviews import msgpack def msgpack_encode(content): @@ -18,32 +17,112 @@ def test_parse_msgpack(): def test_format_msgpack(): - assert list(msgpack.format_msgpack({"string": "test", "int": 1, "float": 1.44, "bool": True})) == [ - [('text', '{')], - [('text', ''), ('text', ' '), ('Token_Name_Tag', '"string"'), ('text', ': '), ('Token_Literal_String', '"test"'), ('text', ',')], - [('text', ''), ('text', ' '), ('Token_Name_Tag', '"int"'), ('text', ': '), ('Token_Literal_Number', '1'), ('text', ',')], - [('text', ''), ('text', ' '), ('Token_Name_Tag', '"float"'), ('text', ': '), ('Token_Literal_Number', '1.44'), ('text', ',')], - [('text', ''), ('text', ' '), ('Token_Name_Tag', '"bool"'), ('text', ': '), ('Token_Keyword_Constant', 'True')], - [('text', ''), ('text', '}')] + assert list( + msgpack.format_msgpack( + {"string": "test", "int": 1, "float": 1.44, "bool": True} + ) + ) == [ + [("text", "{")], + [ + ("text", ""), + ("text", " "), + ("Token_Name_Tag", '"string"'), + ("text", ": "), + ("Token_Literal_String", '"test"'), + ("text", ","), + ], + [ + ("text", ""), + ("text", " "), + ("Token_Name_Tag", '"int"'), + ("text", ": "), + ("Token_Literal_Number", "1"), + ("text", ","), + ], + [ + ("text", ""), + ("text", " "), + ("Token_Name_Tag", '"float"'), + ("text", ": "), + ("Token_Literal_Number", "1.44"), + ("text", ","), + ], + [ + ("text", ""), + ("text", " "), + ("Token_Name_Tag", '"bool"'), + ("text", ": "), + ("Token_Keyword_Constant", "True"), + ], + [("text", ""), ("text", "}")], ] - assert list(msgpack.format_msgpack({"object": {"key": "value"}, "list": [1]})) == [ - [('text', '{')], - [('text', ''), ('text', ' '), ('Token_Name_Tag', '"object"'), ('text', ': '), ('text', '{')], - [('text', ' '), ('text', ' '), ('Token_Name_Tag', '"key"'), ('text', ': '), ('Token_Literal_String', '"value"')], - [('text', ' '), ('text', '}'), ('text', ',')], - [('text', ''), ('text', ' '), ('Token_Name_Tag', '"list"'), ('text', ': '), ('text', '[')], - [('text', ' '), ('text', ' '), ('Token_Literal_Number', '1')], - [('text', ' '), ('text', ']')], - [('text', ''), ('text', '}')]] + assert list( + msgpack.format_msgpack({"object": {"key": "value"}, "list": [0, 0, 1, 0, 0]}) + ) == [ + [("text", "{")], + [ + ("text", ""), + ("text", " "), + ("Token_Name_Tag", '"object"'), + ("text", ": "), + ("text", "{"), + ], + [ + ("text", " "), + ("text", " "), + ("Token_Name_Tag", '"key"'), + ("text", ": "), + ("Token_Literal_String", '"value"'), + ], + [("text", " "), ("text", "}"), ("text", ",")], + [ + ("text", ""), + ("text", " "), + ("Token_Name_Tag", '"list"'), + ("text", ": "), + ("text", "["), + ], + [ + ("text", " "), + ("text", " "), + ("Token_Literal_Number", "0"), + ("text", ","), + ], + [ + ("text", " "), + ("text", " "), + ("Token_Literal_Number", "0"), + ("text", ","), + ], + [ + ("text", " "), + ("text", " "), + ("Token_Literal_Number", "1"), + ("text", ","), + ], + [ + ("text", " "), + ("text", " "), + ("Token_Literal_Number", "0"), + ("text", ","), + ], + [("text", " "), ("text", " "), ("Token_Literal_Number", "0")], + [("text", " "), ("text", "]")], + [("text", ""), ("text", "}")], + ] - assert list(msgpack.format_msgpack('string')) == [[('Token_Literal_String', '"string"')]] + assert list(msgpack.format_msgpack("string")) == [ + [("Token_Literal_String", '"string"')] + ] - assert list(msgpack.format_msgpack(1.2)) == [[('Token_Literal_Number', '1.2')]] + assert list(msgpack.format_msgpack(1.2)) == [[("Token_Literal_Number", "1.2")]] - assert list(msgpack.format_msgpack(True)) == [[('Token_Keyword_Constant', 'True')]] + assert list(msgpack.format_msgpack(True)) == [[("Token_Keyword_Constant", "True")]] - assert list(msgpack.format_msgpack(b'\x01\x02\x03')) == [[('text', "b'\\x01\\x02\\x03'")]] + assert list(msgpack.format_msgpack(b"\x01\x02\x03")) == [ + [("text", "b'\\x01\\x02\\x03'")] + ] def test_view_msgpack(): diff --git a/test/mitmproxy/contentviews/test_multipart.py b/test/mitmproxy/contentviews/test_multipart.py index da1f723e00..a9b43c5f87 100644 --- a/test/mitmproxy/contentviews/test_multipart.py +++ b/test/mitmproxy/contentviews/test_multipart.py @@ -1,5 +1,6 @@ -from mitmproxy.contentviews import multipart from . import full_eval +from mitmproxy.contentviews import multipart +from mitmproxy.test import tutils def test_view_multipart(): @@ -13,6 +14,14 @@ def test_view_multipart(): """.strip() assert view(v, content_type="multipart/form-data; boundary=AaB03x") + req = tutils.treq() + req.headers["content-type"] = "multipart/form-data; boundary=AaB03x" + req.content = v + + assert view( + v, content_type="multipart/form-data; boundary=AaB03x", http_message=req + ) + assert not view(v) assert not view(v, content_type="multipart/form-data") diff --git a/test/mitmproxy/contentviews/test_protobuf.py b/test/mitmproxy/contentviews/test_protobuf.py index 5f8d84d2e8..99d6768ede 100644 --- a/test/mitmproxy/contentviews/test_protobuf.py +++ b/test/mitmproxy/contentviews/test_protobuf.py @@ -1,7 +1,7 @@ import pytest -from mitmproxy.contentviews import protobuf from . import full_eval +from mitmproxy.contentviews import protobuf datadir = "mitmproxy/contentviews/test_protobuf_data/" diff --git a/test/mitmproxy/contentviews/test_query.py b/test/mitmproxy/contentviews/test_query.py index af47a02f81..b4b1408eff 100644 --- a/test/mitmproxy/contentviews/test_query.py +++ b/test/mitmproxy/contentviews/test_query.py @@ -1,6 +1,6 @@ +from . import full_eval from mitmproxy.contentviews import query from mitmproxy.test import tutils -from . import full_eval def test_view_query(): diff --git a/test/mitmproxy/contentviews/test_raw.py b/test/mitmproxy/contentviews/test_raw.py index d9fa44f898..6e59e29052 100644 --- a/test/mitmproxy/contentviews/test_raw.py +++ b/test/mitmproxy/contentviews/test_raw.py @@ -1,10 +1,20 @@ -from mitmproxy.contentviews import raw from . import full_eval +from mitmproxy.contentviews import raw def test_view_raw(): v = full_eval(raw.ViewRaw()) assert v(b"foo") + # unicode + assert v("🫠".encode()) == ( + "Raw", + [[("text", "🫠".encode())]], + ) + # invalid utf8 + assert v(b"\xff") == ( + "Raw", + [[("text", b"\xff")]], + ) def test_render_priority(): diff --git a/test/mitmproxy/contentviews/test_urlencoded.py b/test/mitmproxy/contentviews/test_urlencoded.py index 84c33dfcea..a91dab7366 100644 --- a/test/mitmproxy/contentviews/test_urlencoded.py +++ b/test/mitmproxy/contentviews/test_urlencoded.py @@ -1,6 +1,6 @@ +from . import full_eval from mitmproxy.contentviews import urlencoded from mitmproxy.net.http import url -from . import full_eval def test_view_urlencoded(): @@ -12,7 +12,7 @@ def test_view_urlencoded(): d = url.encode([("adsfa", "")]).encode() assert v(d) - assert not v(b"\xFF\x00") + assert not v(b"\xff\x00") def test_render_priority(): diff --git a/test/mitmproxy/contentviews/test_wbxml.py b/test/mitmproxy/contentviews/test_wbxml.py index e37f0da21f..d81a1c5439 100644 --- a/test/mitmproxy/contentviews/test_wbxml.py +++ b/test/mitmproxy/contentviews/test_wbxml.py @@ -1,5 +1,5 @@ -from mitmproxy.contentviews import wbxml from . import full_eval +from mitmproxy.contentviews import wbxml datadir = "mitmproxy/contentviews/test_wbxml_data/" @@ -7,7 +7,7 @@ def test_wbxml(tdata): v = full_eval(wbxml.ViewWBXML()) - assert v(b"\x03\x01\x6A\x00") == ("WBXML", [[("text", '')]]) + assert v(b"\x03\x01\x6a\x00") == ("WBXML", [[("text", '')]]) assert v(b"foo") is None path = tdata.path( diff --git a/test/mitmproxy/contentviews/test_xml_html.py b/test/mitmproxy/contentviews/test_xml_html.py index 4bb007972e..de2b8d59f5 100644 --- a/test/mitmproxy/contentviews/test_xml_html.py +++ b/test/mitmproxy/contentviews/test_xml_html.py @@ -1,7 +1,7 @@ import pytest -from mitmproxy.contentviews import xml_html from . import full_eval +from mitmproxy.contentviews import xml_html datadir = "mitmproxy/contentviews/test_xml_html_data/" diff --git a/test/mitmproxy/coretypes/test_bidi.py b/test/mitmproxy/coretypes/test_bidi.py index 3bdad3c2c8..b4cff33cba 100644 --- a/test/mitmproxy/coretypes/test_bidi.py +++ b/test/mitmproxy/coretypes/test_bidi.py @@ -1,4 +1,5 @@ import pytest + from mitmproxy.coretypes import bidi diff --git a/test/mitmproxy/coretypes/test_serializable.py b/test/mitmproxy/coretypes/test_serializable.py index 8617a75ee6..e549c5b1f8 100644 --- a/test/mitmproxy/coretypes/test_serializable.py +++ b/test/mitmproxy/coretypes/test_serializable.py @@ -1,6 +1,16 @@ +from __future__ import annotations + import copy +import dataclasses +import enum +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Literal + +import pytest from mitmproxy.coretypes import serializable +from mitmproxy.coretypes.serializable import SerializableDataclass class SerializableDummy(serializable.Serializable): @@ -34,3 +44,159 @@ def test_copy_id(self): b = a.copy() assert a.get_state()["id"] != b.get_state()["id"] assert a.get_state()["foo"] == b.get_state()["foo"] + + +@dataclass +class Simple(SerializableDataclass): + x: int + y: str | None + + +@dataclass +class SerializableChild(SerializableDataclass): + foo: Simple + maybe_foo: Simple | None + + +@dataclass +class Inheritance(Simple): + z: bool + + +class TEnum(enum.Enum): + A = 1 + B = 2 + + +@dataclass +class TLiteral(SerializableDataclass): + lit: Literal["foo", "bar"] + + +@dataclass +class BuiltinChildren(SerializableDataclass): + a: list[int] | None + b: dict[str, int] | None + c: tuple[int, int] | None + d: list[Simple] + e: TEnum | None + + +@dataclass +class Defaults(SerializableDataclass): + z: int | None = 42 + + +@dataclass +class Unsupported(SerializableDataclass): + a: Mapping[str, int] + + +@dataclass +class Addr(SerializableDataclass): + peername: tuple[str, int] + + +@dataclass(frozen=True) +class Frozen(SerializableDataclass): + x: int + + +@dataclass +class FrozenWrapper(SerializableDataclass): + f: Frozen + + +class TestSerializableDataclass: + @pytest.mark.parametrize( + "cls, state", + [ + (Simple, {"x": 42, "y": "foo"}), + (Simple, {"x": 42, "y": None}), + (SerializableChild, {"foo": {"x": 42, "y": "foo"}, "maybe_foo": None}), + ( + SerializableChild, + {"foo": {"x": 42, "y": "foo"}, "maybe_foo": {"x": 42, "y": "foo"}}, + ), + (Inheritance, {"x": 42, "y": "foo", "z": True}), + ( + BuiltinChildren, + { + "a": [1, 2, 3], + "b": {"foo": 42}, + "c": (1, 2), + "d": [{"x": 42, "y": "foo"}], + "e": 1, + }, + ), + (BuiltinChildren, {"a": None, "b": None, "c": None, "d": [], "e": None}), + (TLiteral, {"lit": "foo"}), + ], + ) + def test_roundtrip(self, cls, state): + a = cls.from_state(copy.deepcopy(state)) + assert a.get_state() == state + + def test_set(self): + s = SerializableChild(foo=Simple(x=42, y=None), maybe_foo=Simple(x=43, y=None)) + s.set_state({"foo": {"x": 44, "y": None}, "maybe_foo": None}) + assert s.foo.x == 44 + assert s.maybe_foo is None + with pytest.raises(ValueError, match="Unexpected fields"): + Simple(0, "").set_state({"x": 42, "y": "foo", "z": True}) + + def test_invalid_none(self): + with pytest.raises(ValueError): + Simple.from_state({"x": None, "y": "foo"}) + + def test_defaults(self): + a = Defaults() + assert a.get_state() == {"z": 42} + + def test_invalid_type(self): + with pytest.raises(ValueError): + Simple.from_state({"x": 42, "y": 42}) + with pytest.raises(ValueError): + BuiltinChildren.from_state( + {"a": None, "b": None, "c": ("foo",), "d": [], "e": None} + ) + + def test_invalid_key(self): + with pytest.raises(ValueError): + Simple.from_state({"x": 42, "y": "foo", "z": True}) + + def test_invalid_type_in_list(self): + with pytest.raises(ValueError, match="Invalid value for x"): + BuiltinChildren.from_state( + { + "a": None, + "b": None, + "c": None, + "d": [{"x": "foo", "y": "foo"}], + "e": None, + } + ) + + def test_unsupported_type(self): + with pytest.raises(TypeError): + Unsupported.from_state({"a": "foo"}) + + def test_literal(self): + assert TLiteral.from_state({"lit": "foo"}).get_state() == {"lit": "foo"} + with pytest.raises(ValueError): + TLiteral.from_state({"lit": "unknown"}) + + def test_peername(self): + assert Addr.from_state({"peername": ("addr", 42)}).get_state() == { + "peername": ("addr", 42) + } + assert Addr.from_state({"peername": ("addr", 42, 0, 0)}).get_state() == { + "peername": ("addr", 42, 0, 0) + } + + def test_set_immutable(self): + w = FrozenWrapper(Frozen(42)) + with pytest.raises(dataclasses.FrozenInstanceError): + w.f.set_state({"x": 43}) + w.set_state({"f": {"x": 43}}) + assert w.f.x == 43 diff --git a/test/mitmproxy/data/addonscripts/addon.py b/test/mitmproxy/data/addonscripts/addon.py index b09ef59ba2..43debfe9bd 100644 --- a/test/mitmproxy/data/addonscripts/addon.py +++ b/test/mitmproxy/data/addonscripts/addon.py @@ -20,7 +20,7 @@ def configure(updated): event_log.append("scriptconfigure") -def load(l): +def load(loader): event_log.append("scriptload") diff --git a/test/mitmproxy/data/addonscripts/concurrent_decorator.py b/test/mitmproxy/data/addonscripts/concurrent_decorator.py index bf2628958a..0af96c486a 100644 --- a/test/mitmproxy/data/addonscripts/concurrent_decorator.py +++ b/test/mitmproxy/data/addonscripts/concurrent_decorator.py @@ -1,4 +1,5 @@ import time + from mitmproxy.script import concurrent diff --git a/test/mitmproxy/data/addonscripts/concurrent_decorator_class.py b/test/mitmproxy/data/addonscripts/concurrent_decorator_class.py index b4ef75292c..e08ca0cb13 100644 --- a/test/mitmproxy/data/addonscripts/concurrent_decorator_class.py +++ b/test/mitmproxy/data/addonscripts/concurrent_decorator_class.py @@ -1,4 +1,5 @@ import time + from mitmproxy.script import concurrent diff --git a/test/mitmproxy/data/addonscripts/import_error.py b/test/mitmproxy/data/addonscripts/import_error.py new file mode 100644 index 0000000000..1418bc5fd1 --- /dev/null +++ b/test/mitmproxy/data/addonscripts/import_error.py @@ -0,0 +1,3 @@ +import nonexistent + +nonexistent.foo() diff --git a/test/mitmproxy/data/corrupted_har/broken_headers.json b/test/mitmproxy/data/corrupted_har/broken_headers.json new file mode 100644 index 0000000000..7eec5846a7 --- /dev/null +++ b/test/mitmproxy/data/corrupted_har/broken_headers.json @@ -0,0 +1,7 @@ +{"headers": [ + [ + "Content-Type" + + ] + +]} \ No newline at end of file diff --git a/test/mitmproxy/data/corrupted_har/brokenfile.har b/test/mitmproxy/data/corrupted_har/brokenfile.har new file mode 100644 index 0000000000..175269c1a4 --- /dev/null +++ b/test/mitmproxy/data/corrupted_har/brokenfile.har @@ -0,0 +1,5 @@ +{"log": +[ + 2,1,3 +] +"request:"} \ No newline at end of file diff --git a/test/mitmproxy/data/dumpfile-19.mitm b/test/mitmproxy/data/dumpfile-19.mitm new file mode 100644 index 0000000000..480dc10305 --- /dev/null +++ b/test/mitmproxy/data/dumpfile-19.mitm @@ -0,0 +1,2252 @@ +131541:9:websocket;0:~8:response;126314:6:reason;0:,11:status_code;3:200#13:timestamp_end;17:1726927927.799734^15:timestamp_start;18:1726927927.7027874^8:trailers;0:~7:content;125959: + + QUIC | Cloudflare + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + +
    + + +
    + +
    + + Industries + + + + +
    + + + + + + +
    + +
    + +
    + +
    + +
    +
    + +
    + +
    + + Develop + + + + +
    + + + + + +
    + + Connect + + + + +
    + + +
    +
    +
    + + + + + +
    + +
    + + + + + +
    + + +
    +
    +
    +
    +
    +

    Does my browser support HTTP/3 & QUIC?

    + +

    When loading this page from Cloudflare's edge network, your browser used HTTP/3.

    +

    This page is HTTP/3 & QUIC enabled. Try reloading a few times to spring it into action.

    +

    Not all HTTP clients have HTTP/3 & QUIC support configured. See our documentation for more information about how to check and configure your favorite client such as Chrome, Firefox or curl.

    +
    + +
    + +
    + +
    +
    + +
    +
    +
    +
    +

    Available for all Cloudflare zones

    + +

    Cloudflare's QUIC & HTTP/3 is generally available to all zones. You can control whether it is enabled or disabled using a toggle on the Network tab of your dashboard.

    + +
    + +
    + +
    + +
    +
    + + +
    +
    +
    +
    + +
    + +
    +

    New to QUIC?

    + +

    QUIC is a new transport protocol being developed in the Internet Engineering Task Force (IETF). It offers reliability, security and multiplexing by default.

    +

    HTTP/3 is a new version of HTTP that sits on top of QUIC. It leverages the new transport features to fix performance problems such as Head-of-Line blocking. This enables web pages to load faster, especially over troublesome networks.

    +

    If you’re new to QUIC and need to learn more about the protocol, the following resources will help you gain a better understanding.

    + +
    +
    +
    +
    + + +
    +
    +
    +
    + +
    + +
    +

    Faster handshakes

    + +

    QUIC handshakes are faster by design when compared to the equivalent TCP & TLS.

    +

    Since QUIC uses TLS 1.3, it can benefit from zero roundtrip time (0-RTT) connection resumption. Check out our 0-RTT blogpost to understand more about this feature.

    + +
    +
    +
    +
    + +
    +
    +
    +
    +

    Powered by delicious quiche

    + +

    Quiche is Cloudflare's own open-source implementation of the QUIC and HTTP/3 protocols written in Rust.

    +

    The following articles provide some background.

    + + + +
    + +
    + +
    + +
    +
    + +
    +
    +
    +
    +

    Continuing developments

    + +

    Cloudflare will continue to make updates to its QUIC implementation as the IETF makes progress towards finalizing the protocol standard.

    +

    Since launching QUIC & HTTP/3 support we've continued to measure performance and deploy optimisations such as new Congestion Control algorithms.

    + +
    + +
    + +
    + +
    +
    + + +
    +
    +
    +
    +

    Get started

    + + + +
    +
    +
    +
    + + + + +
    + +
    + +
    + + + +,7:headers;171:40:4:date,29:Sat, 21 Sep 2024 14:12:09 GMT,]28:12:content-type,9:text/html,]27:14:content-length,6:125959,]23:6:server,10:cloudflare,]33:6:cf-ray,20:8c6aa6058e1730db-FRA,]]12:http_version;6:HTTP/3,}7:request;318:4:path;1:/,9:authority;19:cloudflare-quic.com,6:scheme;5:https,6:method;3:GET,4:port;3:443#4:host;19:cloudflare-quic.com;13:timestamp_end;18:1726927927.6633708^15:timestamp_start;18:1726927927.6628444^8:trailers;0:~7:content;0:,7:headers;51:28:10:user-agent,10:curl/8.9.1,]15:6:accept,3:*/*,]]12:http_version;6:HTTP/3,}6:backup;0:~17:timestamp_created;18:1726927927.6629763^7:comment;0:;8:metadata;0:}6:marked;0:;9:is_replay;0:~11:intercepted;5:false!11:server_conn;4129:3:via;0:~19:timestamp_tcp_setup;0:~7:address;29:19:cloudflare-quic.com;3:443#]19:timestamp_tls_setup;17:1726927927.652231^13:timestamp_end;0:~15:timestamp_start;18:1726927927.5914717^3:sni;19:cloudflare-quic.com;11:tls_version;4:QUIC;11:cipher_list;0:]6:cipher;18:AES_256_GCM_SHA384;11:alpn_offers;5:2:h3,]4:alpn;2:h3,16:certificate_list;3610:1359:-----BEGIN CERTIFICATE----- +MIIDvTCCA2OgAwIBAgIQfhh2L/oNtKINNqCqBxC8uTAKBggqhkjOPQQDAjA7MQsw +CQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMQwwCgYD +VQQDEwNXRTEwHhcNMjQwOTAzMDYwNzEyWhcNMjQxMjAyMDYwNzExWjAeMRwwGgYD +VQQDExNjbG91ZGZsYXJlLXF1aWMuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD +QgAEF2huuLoPM1zpmb7/VxTIoogVQlgutJM06dXnB8r7pvCmkAhuqyR/dkL/5cF8 +pxLW3OIVysPQQQpRwj1Lm6iC3KOCAmQwggJgMA4GA1UdDwEB/wQEAwIHgDATBgNV +HSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBSh9T+DF7Tm +xFzZEpIqjbqdWYaKRjAfBgNVHSMEGDAWgBSQd5I1Z8T/qMyp5nvZgHl7zJP5ODBe +BggrBgEFBQcBAQRSMFAwJwYIKwYBBQUHMAGGG2h0dHA6Ly9vLnBraS5nb29nL3Mv +d2UxL2ZoZzAlBggrBgEFBQcwAoYZaHR0cDovL2kucGtpLmdvb2cvd2UxLmNydDA1 +BgNVHREELjAsghNjbG91ZGZsYXJlLXF1aWMuY29tghUqLmNsb3VkZmxhcmUtcXVp +Yy5jb20wEwYDVR0gBAwwCjAIBgZngQwBAgEwNgYDVR0fBC8wLTAroCmgJ4YlaHR0 +cDovL2MucGtpLmdvb2cvd2UxL0pXVzNHajU2WmQ0LmNybDCCAQUGCisGAQQB1nkC +BAIEgfYEgfMA8QB2AHb/iD8KtvuVUcJhzPWHujS0pM27KdxoQgqf5mdMWjp0AAAB +kba1foQAAAQDAEcwRQIgHOTcOeZUJQc+kBVHwJHIqbvMnmx2D84nAFEfVv1x1EgC +IQDa0znSfzpHAIJ1/x8eWwUbgOK5e7Hn13R1DR057im4PgB3ANq2v2s/tbYin5vC +u1xr6HCRcWy7UYSFNL2kPTBI1/urAAABkba1fpEAAAQDAEgwRgIhAPjK86OWjZ/U +VrtJoGh2O00SgwW+4m13T1zuILLzYp0gAiEA075qC++r7e/KBafw69zsJ45JZF+i +N0LjWhF4WZ4yHYswCgYIKoZIzj0EAwIDSAAwRQIhALnaJ0FCYUhqQHSvCWgmBkXw +xqLr9zTDFVsHDm2jACBrAiAlovujZazaEb4n/GMSSL6BUM9VLeBZiUN0patcRTg2 +0A== +-----END CERTIFICATE----- +,969:-----BEGIN CERTIFICATE----- +MIICnzCCAiWgAwIBAgIQf/MZd5csIkp2FV0TttaF4zAKBggqhkjOPQQDAzBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjQwHhcNMjMxMjEzMDkwMDAwWhcNMjkwMjIwMTQw +MDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZp +Y2VzMQwwCgYDVQQDEwNXRTEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARvzTr+ +Z1dHTCEDhUDCR127WEcPQMFcF4XGGTfn1XzthkubgdnXGhOlCgP4mMTG6J7/EFmP +LCaY9eYmJbsPAvpWo4H+MIH7MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggr +BgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU +kHeSNWfE/6jMqeZ72YB5e8yT+TgwHwYDVR0jBBgwFoAUgEzW63T/STaj1dj8tT7F +avCUHYwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzAChhhodHRwOi8vaS5wa2ku +Z29vZy9yNC5jcnQwKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL2MucGtpLmdvb2cv +ci9yNC5jcmwwEwYDVR0gBAwwCjAIBgZngQwBAgEwCgYIKoZIzj0EAwMDaAAwZQIx +AOcCq1HW90OVznX+0RGU1cxAQXomvtgM8zItPZCuFQ8jSBJSjz5keROv9aYsAm5V +sQIwJonMaAFi54mrfhfoFNZEfuNMSQ6/bIBiNLiyoX46FohQvKeIoJ99cx7sUkFN +7uJW +-----END CERTIFICATE----- +,1265:-----BEGIN CERTIFICATE----- +MIIDejCCAmKgAwIBAgIQf+UwvzMTQ77dghYQST2KGzANBgkqhkiG9w0BAQsFADBX +MQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UE +CxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIzMTEx +NTAzNDMyMVoXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoT +GUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFI0 +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE83Rzp2iLYK5DuDXFgTB7S0md+8Fhzube +Rr1r1WEYNa5A3XP3iZEwWus87oV8okB2O6nGuEfYKueSkWpz6bFyOZ8pn6KY019e +WIZlD6GEZQbR3IvJx3PIjGov5cSr0R2Ko4H/MIH8MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUgEzW63T/STaj1dj8tT7FavCUHYwwHwYDVR0jBBgwFoAUYHtmGkUN +l8qJUC99BM00qP/8/UswNgYIKwYBBQUHAQEEKjAoMCYGCCsGAQUFBzAChhpodHRw +Oi8vaS5wa2kuZ29vZy9nc3IxLmNydDAtBgNVHR8EJjAkMCKgIKAehhxodHRwOi8v +Yy5wa2kuZ29vZy9yL2dzcjEuY3JsMBMGA1UdIAQMMAowCAYGZ4EMAQIBMA0GCSqG +SIb3DQEBCwUAA4IBAQAYQrsPBtYDh5bjP2OBDwmkoWhIDDkic574y04tfzHpn+cJ +odI2D4SseesQ6bDrarZ7C30ddLibZatoKiws3UL9xnELz4ct92vID24FfVbiI1hY ++SW6FoVHkNeWIP0GCbaM4C6uVdF5dTUsMVs/ZbzNnIdCp5Gxmx5ejvEau8otR/Cs +kGN+hr/W5GvT1tMBjgWKZ1i4//emhA1JG1BbPzoLJQvyEotc03lXjTaCzv8mEbep +8RqZ7a2CPsgRbuvTPBwcOMBBmuFeU88+FSBX6+7iP0il8b4Z0QFqIwwMHfs/L6K1 +vepuoxtGzi4CZ68zJpiq1UvSqTbFJjtbD4seiMHl +-----END CERTIFICATE----- +,]3:tls;4:true!5:error;0:~18:transport_protocol;3:udp;2:id;36:e47ee853-3b4f-4c26-9efe-2f0597c917ea;8:sockname;27:15:192.168.178.113;5:62652#]8:peername;21:11:104.22.8.38;3:443#]}11:client_conn;473:10:proxy_mode;36:reverse:https://cloudflare-quic.com/;8:mitmcert;0:~19:timestamp_tls_setup;18:1726927927.6583664^13:timestamp_end;0:~15:timestamp_start;17:1726927927.585847^3:sni;9:localhost;11:tls_version;4:QUIC;11:cipher_list;0:]6:cipher;18:AES_256_GCM_SHA384;11:alpn_offers;5:2:h3,]4:alpn;2:h3,16:certificate_list;0:]3:tls;4:true!5:error;0:~18:transport_protocol;3:udp;2:id;36:14d997f0-eb0b-4e39-958b-6ba688d9c705;8:sockname;12:2:::;4:8080#]8:peername;14:3:::1;5:62648#]}5:error;0:~2:id;36:82091aa1-7003-4aa6-9d00-d1e6ad69af03;4:type;4:http;7:version;2:20#} \ No newline at end of file diff --git a/test/mitmproxy/data/flows/compressed.zhar b/test/mitmproxy/data/flows/compressed.zhar new file mode 100644 index 0000000000..c2ee96258e Binary files /dev/null and b/test/mitmproxy/data/flows/compressed.zhar differ diff --git a/test/mitmproxy/data/flows/corrupted_gzip_body.har b/test/mitmproxy/data/flows/corrupted_gzip_body.har new file mode 100644 index 0000000000..b34195d2e8 --- /dev/null +++ b/test/mitmproxy/data/flows/corrupted_gzip_body.har @@ -0,0 +1,91 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "mitmproxy", + "version": "1.2.3", + "comment": "" + }, + "pages": [], + "entries": [ + { + "startedDateTime": "2024-11-14T14:59:42.210687+00:00", + "time": 18.944978713989258, + "request": { + "method": "GET", + "url": "http://127.0.0.1:5000/", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Host", + "value": "127.0.0.1:5000" + }, + { + "name": "User-Agent", + "value": "curl/8.11.0" + }, + { + "name": "Accept", + "value": "*/*" + } + ], + "queryString": [], + "headersSize": 91, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Server", + "value": "Werkzeug/3.0.4 Python/3.12.7" + }, + { + "name": "Date", + "value": "Thu, 14 Nov 2024 14:59:42 GMT" + }, + { + "name": "Content-Type", + "value": "text/html; charset=utf-8" + }, + { + "name": "Content-Length", + "value": "24" + }, + { + "name": "Content-Encoding", + "value": "gzip" + }, + { + "name": "Connection", + "value": "close" + } + ], + "content": { + "size": 24, + "compression": 0, + "mimeType": "text/html; charset=utf-8", + "text": "H4sIAAAAAAAAAyE+EjNBD/iT6u4EAAAA", + "encoding": "base64" + }, + "redirectURL": "", + "headersSize": 233, + "bodySize": 24 + }, + "cache": {}, + "timings": { + "connect": 1.5239715576171875, + "ssl": -1.0, + "send": 9.511947631835938, + "receive": 2.953767776489258, + "wait": 4.955291748046875 + }, + "serverIPAddress": "127.0.0.1" + } + ] + } +} diff --git a/test/mitmproxy/data/flows/corrupted_gzip_body.mitm b/test/mitmproxy/data/flows/corrupted_gzip_body.mitm new file mode 100644 index 0000000000..947f162d45 Binary files /dev/null and b/test/mitmproxy/data/flows/corrupted_gzip_body.mitm differ diff --git a/test/mitmproxy/data/flows/diff_data.har b/test/mitmproxy/data/flows/diff_data.har new file mode 100644 index 0000000000..8f6446018a --- /dev/null +++ b/test/mitmproxy/data/flows/diff_data.har @@ -0,0 +1,910 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "mitmproxy", + "version": "1.2.3", + "comment": "" + }, + "pages": [], + "entries": [ + { + "startedDateTime": "2023-09-10T04:38:02.923315+00:00", + "time": 85.77585220336914, + "request": { + "method": "POST", + "url": "https://jnn-pa.googleapis.com/$rpc/google.internal.waa.v1.Waa/GenerateIT", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "1020" + }, + { + "name": "sec-ch-ua", + "value": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"" + }, + { + "name": "x-user-agent", + "value": "grpc-web-javascript/0.1" + }, + { + "name": "dnt", + "value": "1" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "user-agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" + }, + { + "name": "content-type", + "value": "application/json+protobuf" + }, + { + "name": "x-goog-api-key", + "value": "AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + }, + { + "name": "accept", + "value": "*/*" + }, + { + "name": "origin", + "value": "https://www.youtube.com" + }, + { + "name": "x-client-data", + "value": "CKG1yQEIiLbJAQiltskBCKmdygEI0eXKAQiSocsBCIagzQEIh7LNAQjcvc0BCN/EzQEIucrNAQjzys0BCLjNzQEIk8/NAQjU0M0BCKrRzQEY642lFw==" + }, + { + "name": "sec-fetch-site", + "value": "cross-site" + }, + { + "name": "sec-fetch-mode", + "value": "cors" + }, + { + "name": "sec-fetch-dest", + "value": "empty" + }, + { + "name": "referer", + "value": "https://www.youtube.com/" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate, br" + }, + { + "name": "accept-language", + "value": "en-US,en;q=0.9" + } + ], + "queryString": [], + "headersSize": 934, + "bodySize": 1020, + "postData": { + "mimeType": "application/json+protobuf", + "text": "[\"O43z0dpjhgX20SCx4KAo\",\"$duk56bFRAAaRg-QUsYPeVnPkTVs5P02nADQBEArZ1Er31GfFWhS0dmxIS-FXKpqlUczdRU2sRD61QwvKWZqZDMpJ2miU0Ivw9QvhxBb3ngAAABrOAAAAAfQBB5YBRSRkCcYZ6FE4GijJAPpWsTg_WgXoYHnqXwaRZyZt7jpNUxcLv7006FgPfulvp3EOg_bDFy5wK23kfBF6YDT-ydnbbHo7NuaTFc89lW4TiWMBk6joHyDgaOuDgaRWKfn8Nmx61JdBSFsBIgjslokAxBUQcHvcrpgtLKEuU07WSuTSDRVlFwJQR1VVFW5eo2pkhs_woFF1FoXT96x--z9IcRN9dQhx8u7V2NOJQuZzUqNFAC51J1NYAHN74e5QwriNd4LN76WBuXgGYakgpUBKdqPwiBUgc7V03Ysbq0otrmgHTV1B9T6ONJZqAyplzOCo100Ks8NcTNv66Zlgufq0Q5EMzKX2wwLyatmsEmAlV5zSFtGLKg8utyw3Ng5sgd6azxOXBL214maf8n3O1ywU3LEm8LiNqQfg1WblE5pcDaYKQ7fS7iUFAULGuXP7fpK-0AZsonWcjgwi9s-LtNx4ZrkiBhUaoHoYrT06IX119UJCk-GLFYsgEbKpQqsJoBAi14oDYSL4mfnSmZ_0YSgBnIHF_TUDxSYL-08ZwHxAXcGc3znHc43C3DpOppj9XxtD420QJR3uI-pKafgYhf1PDovTF-NP-vFnuJ25gCPZAgZtC-1CcAHw5lXY_VI66vOn1e2rnkV569WQMipssgjb2c6au8kMpkMSudYbzeC4jzJKGS8cPrDi8bZD6zDtInVmqqCQTslGnSHWCVU_dENob6iMetgi8K19TcE7g9ugKQWTgSj67fAp-TBRuXXczBrBYaAlLba2Yo28RwWUj6dm4zJt8VVyNh_pGa1d1t1WEX4qmgTETmj-EHaa1j2WdPClLYP-BwZNhMDhbtgi41XdW4v1uZLs26pTPP0H\"]", + "params": [] + } + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "application/json+protobuf; charset=UTF-8" + }, + { + "name": "vary", + "value": "Origin" + }, + { + "name": "vary", + "value": "X-Origin" + }, + { + "name": "vary", + "value": "Referer" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "date", + "value": "Sun, 10 Sep 2023 04:38:03 GMT" + }, + { + "name": "server", + "value": "ESF" + }, + { + "name": "cache-control", + "value": "private" + }, + { + "name": "content-length", + "value": "110" + }, + { + "name": "x-xss-protection", + "value": "0" + }, + { + "name": "x-frame-options", + "value": "SAMEORIGIN" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "access-control-allow-origin", + "value": "https://www.youtube.com" + }, + { + "name": "access-control-allow-credentials", + "value": "true" + }, + { + "name": "access-control-expose-headers", + "value": "vary,vary,vary,content-encoding,date,server,content-length" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + } + ], + "content": { + "size": 110, + "compression": -20, + "mimeType": "application/json+protobuf; charset=UTF-8", + "text": "[\"JGcn43EwCMBu/X0WtJsKbhx8xATUqaWtf8faN4uaCX9hFbm3hzpeVf1+9cNOObeyo1BrAGgw/MM=\",43200,100]" + }, + "redirectURL": "", + "headersSize": 680, + "bodySize": 110 + }, + "cache": {}, + "timings": { + "connect": 22.59087562561035, + "ssl": 26.18408203125, + "send": 0.8690357208251953, + "receive": 0.7729530334472656, + "wait": 35.35890579223633 + }, + "serverIPAddress": "142.251.32.42" + }, + { + "startedDateTime": "2023-09-10T04:39:21.143838+00:00", + "time": 107.24520683288574, + "request": { + "method": "GET", + "url": "https://www.vskills.in/certification/tutorial/wp-content/plugins/wpforms-lite/assets/images/submit-spin.svg", + "httpVersion": "HTTP/2.0", + "cookies": [ + { + "name": "_ga", + "value": "GA1.2.1478153842.1693348042" + }, + { + "name": "__zlcmid", + "value": "1Ham5EWE334s94B" + }, + { + "name": "_ga_J3VKXFW3HD", + "value": "GS1.1.1693348041.1.0.1693348045.0.0.0" + } + ], + "headers": [ + { + "name": "sec-ch-ua", + "value": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"" + }, + { + "name": "dnt", + "value": "1" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "user-agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + }, + { + "name": "accept", + "value": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8" + }, + { + "name": "sec-fetch-site", + "value": "same-origin" + }, + { + "name": "sec-fetch-mode", + "value": "no-cors" + }, + { + "name": "sec-fetch-dest", + "value": "image" + }, + { + "name": "referer", + "value": "https://www.vskills.in/certification/tutorial/tcp-connection-establish-and-terminate/" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate, br" + }, + { + "name": "accept-language", + "value": "en-US,en;q=0.9" + }, + { + "name": "cookie", + "value": "_ga=GA1.2.1478153842.1693348042" + }, + { + "name": "cookie", + "value": "__zlcmid=1Ham5EWE334s94B" + }, + { + "name": "cookie", + "value": "_ga_J3VKXFW3HD=GS1.1.1693348041.1.0.1693348045.0.0.0" + } + ], + "queryString": [], + "headersSize": 848, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "date", + "value": "Sun, 10 Sep 2023 04:39:21 GMT" + }, + { + "name": "content-type", + "value": "image/svg+xml" + }, + { + "name": "last-modified", + "value": "Tue, 18 Jul 2023 23:15:31 GMT" + }, + { + "name": "vary", + "value": "Accept-Encoding" + }, + { + "name": "etag", + "value": "W/\"64b71d13-1fd\"" + }, + { + "name": "expires", + "value": "Thu, 31 Dec 2037 23:55:55 GMT" + }, + { + "name": "cache-control", + "value": "max-age=315360000" + }, + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "cf-cache-status", + "value": "HIT" + }, + { + "name": "age", + "value": "213871" + }, + { + "name": "report-to", + "value": "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=yIlVxwsDTDXaEWnWECABYKQ5cJi2MG9UTcbgpnb4ujsbPcvYlUka1LVJWLGI54bPETRJ%2FBgBWjg31hff6YwRHMMkimH50ezDVoQKRqNqJh3G0X2ZNPQxfJ2rP0Xh0fZ%2B\"}],\"group\":\"cf-nel\",\"max_age\":604800}" + }, + { + "name": "nel", + "value": "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "cf-ray", + "value": "8044fc9689fa2386-SJC" + }, + { + "name": "content-encoding", + "value": "br" + } + ], + "content": { + "size": 332, + "compression": 177, + "mimeType": "image/svg+xml", + "text": "" + }, + "redirectURL": "", + "headersSize": 820, + "bodySize": 332 + }, + "cache": {}, + "timings": { + "connect": 38.29193115234375, + "ssl": 25.390148162841797, + "send": 8.873939514160156, + "receive": 0.7030963897705078, + "wait": 33.98609161376953 + }, + "serverIPAddress": "172.67.71.94" + }, + { + "startedDateTime": "2023-09-10T04:39:35.804731+00:00", + "time": 127.532958984375, + "request": { + "method": "GET", + "url": "https://s0.2mdn.net/simgad/15064852172826283141", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "sec-ch-ua", + "value": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"" + }, + { + "name": "dnt", + "value": "1" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "user-agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + }, + { + "name": "accept", + "value": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8" + }, + { + "name": "sec-fetch-site", + "value": "cross-site" + }, + { + "name": "sec-fetch-mode", + "value": "no-cors" + }, + { + "name": "sec-fetch-dest", + "value": "image" + }, + { + "name": "referer", + "value": "https://ad.doubleclick.net/" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate, br" + }, + { + "name": "accept-language", + "value": "en-US,en;q=0.9" + } + ], + "queryString": [], + "headersSize": 628, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "accept-ranges", + "value": "bytes" + }, + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "cross-origin-resource-policy", + "value": "cross-origin" + }, + { + "name": "cross-origin-opener-policy-report-only", + "value": "same-origin; report-to=\"ads-doubleclick-media\"" + }, + { + "name": "report-to", + "value": "{\"group\":\"ads-doubleclick-media\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/ads-doubleclick-media\"}]}" + }, + { + "name": "timing-allow-origin", + "value": "*" + }, + { + "name": "content-length", + "value": "39198" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "x-dns-prefetch-control", + "value": "off" + }, + { + "name": "server", + "value": "sffe" + }, + { + "name": "x-xss-protection", + "value": "0" + }, + { + "name": "date", + "value": "Mon, 04 Sep 2023 21:11:07 GMT" + }, + { + "name": "expires", + "value": "Tue, 03 Sep 2024 21:11:07 GMT" + }, + { + "name": "cache-control", + "value": "public, max-age=31536000" + }, + { + "name": "age", + "value": "458909" + }, + { + "name": "last-modified", + "value": "Wed, 26 Jul 2023 21:25:21 GMT" + }, + { + "name": "content-type", + "value": "image/jpeg" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + } + ], + "content": { + "size": 39198, + "compression": 0, + "mimeType": "image/jpeg", + "text": "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", + "encoding": "base64" + }, + "redirectURL": "", + "headersSize": 892, + "bodySize": 39198 + }, + "cache": {}, + "timings": { + "connect": 41.954994201660156, + "ssl": 36.83590888977051, + "send": 9.533166885375977, + "receive": 13.534784317016602, + "wait": 25.674104690551758 + }, + "serverIPAddress": "142.251.214.134" + }, + { + "startedDateTime": "2023-09-10T04:39:45.261679+00:00", + "time": 117.12098121643066, + "request": { + "method": "POST", + "url": "https://clients4.google.com/chrome-sync/command/?client=Google+Chrome&client_id=PyIKPe4Z1cTqdvB7d%2BFPCQ%3D%3D", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "2379" + }, + { + "name": "pragma", + "value": "no-cache" + }, + { + "name": "cache-control", + "value": "no-cache" + }, + { + "name": "authorization", + "value": "Bearer ya29.a0AfB_byDg8-jTAQMiRXx-Dq3J1Pnb5NHe2LPWoocBcEIJrVmZt5QIZpy4SCYADq8LRThKz0OWYAO1Yw1ZPuc6b761TSLU7G5g8svYWhGHWWFAgcZRpzta-0hxbTzg6E4aidUv2LZNnqIvWsQM8UeSsNUu4yvIwPyp7pvu71fjnCd5Jujjp3MAQE9sOCi52V264frr-Xitashuo5x6EPtUUZ961EUeh6k2qJBxauS9qYia_9PLmLAGDMXe7fKfAU_Z6-hHeiITGxlHrGcaCgYKAaQSARESFQGOcNnCb-ua9Ga_HReXSlhJD7N_jA0278" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "user-agent", + "value": "Chrome MAC 116.0.5845.179 (17ff023f3eb4f6883321db9399bfc65560ef84a9-refs/branch-heads/5845@{#1745}) channel(stable)" + }, + { + "name": "content-type", + "value": "application/octet-stream" + }, + { + "name": "x-client-data", + "value": "CKG1yQEIiLbJAQiltskBCKmdygEI0eXKAQiSocsBCIagzQEIh7LNAQjcvc0BCN/EzQEIucrNAQjzys0BCLjNzQEIk8/NAQjU0M0BCKrRzQEY642lFw==" + }, + { + "name": "sec-fetch-site", + "value": "none" + }, + { + "name": "sec-fetch-mode", + "value": "no-cors" + }, + { + "name": "sec-fetch-dest", + "value": "empty" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate, br" + }, + { + "name": "accept-language", + "value": "en-US,en;q=0.9" + } + ], + "queryString": [ + { + "name": "client", + "value": "Google Chrome" + }, + { + "name": "client_id", + "value": "PyIKPe4Z1cTqdvB7d+FPCQ==" + } + ], + "headersSize": 990, + "bodySize": 5970, + "postData": { + "mimeType": "application/octet-stream", + "text": "\n\u0014stanleygvi@gmail.com\u0010c\u0018\u0001\"\u0088,\n\u00ae\u0012\nfZ:ADqtAZwoGEGO4Rxc1RHIZbNFXaOB6MkBUyZCQvU4oUZ5lSUjHseZ6QaZ9Zlbvq+gNumk3FGw1gapr32/inhTGYvBN+dv82tsAw== \u00f1\u00c9\u009c\u00de\u009c\u009f\u0081\u0003(\u00d9\u009d\u008d\u00eb\u00a710\u00d5\u00c3\u0087\u00eb\u00a71:\u000bMacBook-Pro\u0090\u0001\u0000\u00aa\u0001\u00fc\u0010\u00ba\u00bc\u0018\u00f7\u0010\n\u0018PyIKPe4Z1cTqdvB7d+FPCQ==\u001a\u00d8\u0010\b\u008f\u0097\u00c4\u00c0\u0007\u0010\u008e\u0097\u00c4\u00c0\u0007\u0018\u0000 \u0003(\u00002\u0000:d\u0012\u0010chrome://newtab/\u001a\u0000\"\u0007New Tab0\u0006@\u0011H\u00c4\u00b3\u0086\u00eb\u00a71P\u0000X\u0000`\u0000x\u00fd\u00d2\u009b\u00ee\u0080\u00f2\u00d8\u0017\u00a0\u0001\u00c8\u0001\u00b0\u0001\u0000\u00c8\u0001\u0006\u00d0\u0001\u0000\u00d8\u0001\u00fd\u00d2\u009b\u00ee\u0080\u00f2\u00d8\u0017\u00e0\u0001\u00fd\u00d2\u009b\u00ee\u0080\u00f2\u00d8\u0017\u00e0\u0001\u00ff\u00ff\u00ff\u00ff\u00ff\u00ff\u00ff\u00ff\u00ff\u0001:\u008f\u0004\u0012\u00ae\u0001https://www.google.com/search?q=how+to+start+txp+connection&oq=how+to+start+txp+connection+&aqs=chrome..69i57j0i13i30j0i22i30l3j0i390i650l3.10590j0j7&sourceid=chrome&ie=UTF-8\u001a\u0000\"+how to start txp connection - Google Search0\u0005@\u0016H\u00a6\u00c5\u0087\u00eb\u00a71P\u0000X\u0001`\u0000x\u0082\u00b3\u008e\u00f7\u0080\u00f2\u00d8\u0017\u008a\u0001\"https://www.google.com/favicon.ico\u00a0\u0001\u00c8\u0001\u00b0\u0001\u0000\u00c8\u0001\u0004\u00d0\u0001\u0000\u00d8\u0001\u00c5\u00e3\u0080\u00f7\u0080\u00f2\u00d8\u0017\u00e0\u0001\u00c5\u00e3\u0080\u00f7\u0080\u00f2\u00d8\u0017\u00e0\u0001\u00ff\u00ff\u00ff\u00ff\u00ff\u00ff\u00ff\u00ff\u00ff\u0001\u00ea\u0001\u00ba\u0001\n\u00ae\u0001https://www.google.com/search?q=how+to+start+txp+connection&oq=how+to+start+txp+connection+&aqs=chrome..69i57j0i13i30j0i22i30l3j0i390i650l3.10590j0j7&sourceid=chrome&ie=UTF-8\u0010\u00c7\u00c3\u0087\u00eb\u00a71\u0018\u0005\u00f2\u0001\u0002en:\u0084\t\u0012\u00de\u0003https://www.google.com/search?q=how+to+start+tcp+connection&sca_esv=564088781&sxsrf=AB5stBg6T1uwGI6y4Fe4qhRAiNDRQM-rJw%3A1694320680892&ei=KEj9ZMuDNszv0PEPxrGkmAQ&ved=0ahUKEwjLt8eynJ-BAxXMNzQIHcYYCUMQ4dUDCBA&uact=5&oq=how+to+start+tcp+connection&gs_lp=Egxnd3Mtd2l6LXNlcnAiG2hvdyB0byBzdGFydCB0Y3AgY29ubmVjdGlvbjIEEAAYHjIIEAAYigUYhgMyCBAAGIoFGIYDMggQABiKBRiGAzIIEAAYigUYhgNIiQ1QnApYhAtwAXgBkAEAmAGAAaAB3gGqAQMxLjG4AQPIAQD4AQHCAgoQABhHGNYEGLAD4gMEGAAgQYgGAZAGCA&sclient=gws-wiz-serp\u001a\u0017https://www.google.com/\"+how to start tcp connection - Google Search0\u0007@\u0018H\u00f4\u00f6\u008c\u00eb\u00a71P\u0001X\u0000`\u0000x\u00d6\u00fb\u0099\u00a1\u0081\u00f2\u00d8\u0017\u008a\u0001\"https://www.google.com/favicon.ico\u00a0\u0001\u00c8\u0001\u00b0\u0001\u0000\u00c8\u0001\u0004\u00d0\u0001\u0000\u00d8\u0001\u00ec\u00f4\u00cb\u00fa\u0080\u00f2\u00d8\u0017\u00e0\u0001\u00c5\u00e3\u0080\u00f7\u0080\u00f2\u00d8\u0017\u00e0\u0001\u00c5\u00e3\u0080\u00f7\u0080\u00f2\u00d8\u0017\u00ea\u0001\u00ea\u0003\n\u00de\u0003https://www.google.com/search?q=how+to+start+tcp+connection&sca_esv=564088781&sxsrf=AB5stBg6T1uwGI6y4Fe4qhRAiNDRQM-rJw%3A1694320680892&ei=KEj9ZMuDNszv0PEPxrGkmAQ&ved=0ahUKEwjLt8eynJ-BAxXMNzQIHcYYCUMQ4dUDCBA&uact=5&oq=how+to+start+tcp+connection&gs_lp=Egxnd3Mtd2l6LXNlcnAiG2hvdyB0byBzdGFydCB0Y3AgY29ubmVjdGlvbjIEEAAYHjIIEAAYigUYhgMyCBAAGIoFGIYDMggQABiKBRiGAzIIEAAYigUYhgNIiQ1QnApYhAtwAXgBkAEAmAGAAaAB3gGqAQMxLjG4AQPIAQD4AQHCAgoQABhHGNYEGLAD4gMEGAAgQYgGAZAGCA&sclient=gws-wiz-serp\u0010\u00a9\u00fe\u0087\u00eb\u00a71\u0018\u0007\u00f2\u0001\u0002en:\u00c0\u0002\u0012Phttps://stackoverflow.com/questions/31447099/starting-a-tcp-server-from-terminal\u001a\u0017https://www.google.com/\"YScz)QA/R`!9W&m0FqQX9+axfs\"m2sGK42H]bKk3*3,)Mr$|C+BXm}]_Cn1E00`O4!%:%z00000146-9b6d-1d2e-0000-0000539c866dRF\n\u000e\u0012\f8\u0000@\u0000R\u0004\b\u0000\u0010\u0001`\f\n\u0004\u0018\u0091\u00eb:\n\u0004\u0018\u0091\u00eb:\n\u0004\u0018\u00c7\u0087\u0003\n\u0004\u0018\u0091\u00eb:\n\u0004\u0018\u00c7\u0087\u0003\n\u0004\u0018\u0091\u00eb:\n\u0004\u0018\u00c7\u0087\u0003\n\u0004\u0018\u00c7\u0087\u0003\u0010\u0001\u0018\u0000 \u0000Z\u0081\u0001\n\u007f\u0012}Chrome MAC 116.0.5845.179 (17ff023f3eb4f6883321db9399bfc65560ef84a9-refs/branch-heads/5845@{#1745}) channel(stable),gzip(gfe)b'AIzaSyBOti4mM-6x9WDnZIjIeyEU21OpBXqWBgwj\u0002\u0010\u0001r\u000be27aus4bokQ", + "params": [] + } + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "content-type", + "value": "application/vnd.google.octet-stream-compressible" + }, + { + "name": "cache-control", + "value": "no-cache, no-store, max-age=0, must-revalidate" + }, + { + "name": "pragma", + "value": "no-cache" + }, + { + "name": "expires", + "value": "Mon, 01 Jan 1990 00:00:00 GMT" + }, + { + "name": "date", + "value": "Sun, 10 Sep 2023 04:39:45 GMT" + }, + { + "name": "content-disposition", + "value": "attachment; filename=\"response\"" + }, + { + "name": "cross-origin-opener-policy", + "value": "same-origin-allow-popups" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "server", + "value": "ESF" + }, + { + "name": "x-xss-protection", + "value": "0" + }, + { + "name": "x-frame-options", + "value": "SAMEORIGIN" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + } + ], + "content": { + "size": 409, + "compression": 20, + "mimeType": "application/vnd.google.octet-stream-compressible", + "text": "\n\u00f0\u0001\u000b\u0010\u0001\u001afZ:ADqtAZwoGEGO4Rxc1RHIZbNFXaOB6MkBUyZCQvU4oUZ5lSUjHseZ6QaZ9Zlbvq+gNumk3FGw1gapr32/inhTGYvBN+dv82tsAw==0\u00c1\u00a6\u00b7\u00e4\u009c\u009f\u0081\u0003P\u00c6\u00f0\u008d\u00eb\u00a71\f\u000b\u0010\u0001\u001a$97781583-edbd-4992-bc6d-41244fca980b0\u0083\u00a0\u00b7\u00e4\u009c\u009f\u0081\u0003P\u00a8\u0098\u008d\u00eb\u00a71\f\u000b\u0010\u0001\u001a$db37a49f-9fee-4129-9d97-2a68bac38af20\u0097\u00a0\u00b7\u00e4\u009c\u009f\u0081\u0003P\u00b1\u0098\u008d\u00eb\u00a71\f \u00002%z00000146-9b6d-1d2e-0000-0000539c866d:\u000b\b\u00c0p\u0010\u00e0\u00a8\u0001\u0018Z \u000br\u0081\u0001\n\u007f\u0012}Chrome MAC 116.0.5845.179 (17ff023f3eb4f6883321db9399bfc65560ef84a9-refs/branch-heads/5845@{#1745}) channel(stable),gzip(gfe)" + }, + "redirectURL": "", + "headersSize": 618, + "bodySize": 409 + }, + "cache": {}, + "timings": { + "connect": 15.475749969482422, + "ssl": 30.790328979492188, + "send": 1.6818046569824219, + "receive": 1.065969467163086, + "wait": 68.10712814331055 + }, + "serverIPAddress": "142.250.191.46" + }, + { + "startedDateTime": "2023-09-10T04:58:23.573645+00:00", + "time": 318.6910152435303, + "request": { + "method": "GET", + "url": "https://slack.com/beacon/timing?ver=1694215648&session_age=1446&session_id=1b55ded2-453f-414d-8356-9c024f271ca0&sub_app_name=client&data=rtm_queue_length_max%7Ccount%3A1", + "httpVersion": "HTTP/2.0", + "cookies": [ + { + "name": "b", + "value": "e096d4332e53962bca507efb1bdbeb00" + }, + { + "name": "ssb_instance_id", + "value": "20b0ab55-dcc5-4ef4-9143-694309c56c5a" + }, + { + "name": "lc", + "value": "1693313898" + }, + { + "name": "tz", + "value": "-420" + }, + { + "name": "d", + "value": "xoxd-ekKbs8IHIKMWUnFlBjwKmIViLmYdDaOfEF%2Bmne4M5sl1VdXeT41jFKO1PYOoeP%2B8SL8qB0WSBYXiXPceDLZXRJwk%2F9mBaKmGSMdEXnlxqAK%2Bi0Jg9TYTN5JSIlPTvSYVNMzVGKBiOYQrBGcqe9X%2BXm9mXqa0nch3RFGjFcafRjN7qESY7MAF3aRF3%2BLc%2BuQAT0I%2Baww%3D" + }, + { + "name": "d-s", + "value": "1694320458" + } + ], + "headers": [ + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "user-agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Slack/4.33.90 Chrome/114.0.5735.134 Electron/25.2.0 Safari/537.36 AppleSilicon Sonic Slack_SSB/4.33.90" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + }, + { + "name": "accept", + "value": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8" + }, + { + "name": "sec-fetch-site", + "value": "same-site" + }, + { + "name": "sec-fetch-mode", + "value": "no-cors" + }, + { + "name": "sec-fetch-dest", + "value": "image" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate, br" + }, + { + "name": "accept-language", + "value": "en-US" + }, + { + "name": "cookie", + "value": "b=e096d4332e53962bca507efb1bdbeb00" + }, + { + "name": "cookie", + "value": "ssb_instance_id=20b0ab55-dcc5-4ef4-9143-694309c56c5a" + }, + { + "name": "cookie", + "value": "lc=1693313898" + }, + { + "name": "cookie", + "value": "tz=-420" + }, + { + "name": "cookie", + "value": "d=xoxd-ekKbs8IHIKMWUnFlBjwKmIViLmYdDaOfEF%2Bmne4M5sl1VdXeT41jFKO1PYOoeP%2B8SL8qB0WSBYXiXPceDLZXRJwk%2F9mBaKmGSMdEXnlxqAK%2Bi0Jg9TYTN5JSIlPTvSYVNMzVGKBiOYQrBGcqe9X%2BXm9mXqa0nch3RFGjFcafRjN7qESY7MAF3aRF3%2BLc%2BuQAT0I%2Baww%3D" + }, + { + "name": "cookie", + "value": "d-s=1694320458" + } + ], + "queryString": [ + { + "name": "ver", + "value": "1694215648" + }, + { + "name": "session_age", + "value": "1446" + }, + { + "name": "session_id", + "value": "1b55ded2-453f-414d-8356-9c024f271ca0" + }, + { + "name": "sub_app_name", + "value": "client" + }, + { + "name": "data", + "value": "rtm_queue_length_max|count:1" + } + ], + "headersSize": 1054, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "date", + "value": "Sun, 10 Sep 2023 04:58:23 GMT" + }, + { + "name": "server", + "value": "Apache" + }, + { + "name": "content-type", + "value": "image/gif" + }, + { + "name": "cache-control", + "value": "no-cache, no-store" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "x-slack-unique-id", + "value": "ZP1M73-9mc8HoDSLrF5_vwAAEDo" + }, + { + "name": "x-slack-backend", + "value": "r" + }, + { + "name": "referrer-policy", + "value": "no-referrer" + }, + { + "name": "x-frame-options", + "value": "SAMEORIGIN" + }, + { + "name": "content-length", + "value": "29" + }, + { + "name": "via", + "value": "1.1 slack-prod.tinyspeck.com, envoy-www-iad-fhbwvxab, envoy-edge-pdx-nrvkuzcx" + }, + { + "name": "x-envoy-upstream-service-time", + "value": "70" + }, + { + "name": "x-backend", + "value": "beacons_normal beacons_canary_with_overflow beacons_control_with_overflow" + }, + { + "name": "x-server", + "value": "slack-www-hhvm-beacons-iad-uidr" + }, + { + "name": "x-slack-shared-secret-outcome", + "value": "no-match" + }, + { + "name": "x-edge-backend", + "value": "envoy-www" + }, + { + "name": "x-slack-edge-shared-secret-outcome", + "value": "no-match" + } + ], + "content": { + "size": 29, + "compression": 0, + "mimeType": "image/gif", + "text": "R0lGODlhAQABAAAAACwAAAAAAQABAAACAkwBADs=", + "encoding": "base64" + }, + "redirectURL": "", + "headersSize": 838, + "bodySize": 29 + }, + "cache": {}, + "timings": { + "connect": 69.93222236633301, + "ssl": 36.55195236206055, + "send": 4.504680633544922, + "receive": 1.310110092163086, + "wait": 206.3920497894287 + }, + "serverIPAddress": "54.188.33.22" + } + ] + } +} \ No newline at end of file diff --git a/test/mitmproxy/data/flows/diff_data.mitm b/test/mitmproxy/data/flows/diff_data.mitm new file mode 100644 index 0000000000..22884efdbd Binary files /dev/null and b/test/mitmproxy/data/flows/diff_data.mitm differ diff --git a/test/mitmproxy/data/flows/error_log.har b/test/mitmproxy/data/flows/error_log.har new file mode 100644 index 0000000000..11e4c48796 --- /dev/null +++ b/test/mitmproxy/data/flows/error_log.har @@ -0,0 +1,172 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "mitmproxy", + "version": "1.2.3", + "comment": "" + }, + "pages": [], + "entries": [ + { + "startedDateTime": "2023-07-25T11:03:15.216374+00:00", + "time": 2.851247787475586, + "request": { + "method": "GET", + "url": "http://163.com/", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Host", + "value": "163.com" + }, + { + "name": "User-Agent", + "value": "Wget/1.21.4" + }, + { + "name": "Accept", + "value": "*/*" + }, + { + "name": "Accept-Encoding", + "value": "identity" + }, + { + "name": "Connection", + "value": "Keep-Alive" + }, + { + "name": "Proxy-Connection", + "value": "Keep-Alive" + } + ], + "queryString": [], + "headersSize": 189, + "bodySize": 0 + }, + "response": { + "status": 0, + "statusText": "", + "httpVersion": "", + "headers": [], + "cookies": [], + "content": {}, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1, + "_transferSize": 0, + "_error": "Multiple exceptions: [Errno 60] Connect call failed ('123.58.180.8', 80), [Errno 60] Connect call failed ('123.58.180.7', 80)" + }, + "cache": {}, + "timings": { + "connect": -1.0, + "ssl": -1.0, + "send": 2.851247787475586, + "receive": 0, + "wait": 0 + } + }, + { + "startedDateTime": "2023-07-25T11:07:56.330959+00:00", + "time": 3712.937831878662, + "request": { + "method": "POST", + "url": "https://httpbin.org/get", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "user-agent", + "value": "curl/7.86.0" + }, + { + "name": "accept", + "value": "*/*" + }, + { + "name": "content-type", + "value": "application/x-www-form-urlencoded" + }, + { + "name": "content-length", + "value": "23" + } + ], + "queryString": [], + "headersSize": 146, + "bodySize": 23, + "postData": { + "mimeType": "application/x-www-form-urlencoded", + "text": "key1=value1&key2=value2", + "params": [ + { + "name": "key1", + "value": "value1" + }, + { + "name": "key2", + "value": "value2" + } + ] + } + }, + "response": { + "status": 405, + "statusText": "", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "date", + "value": "Tue, 25 Jul 2023 11:07:59 GMT" + }, + { + "name": "content-type", + "value": "text/html" + }, + { + "name": "content-length", + "value": "178" + }, + { + "name": "server", + "value": "gunicorn/19.9.0" + }, + { + "name": "allow", + "value": "GET, HEAD, OPTIONS" + }, + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "access-control-allow-credentials", + "value": "true" + } + ], + "content": { + "size": 178, + "compression": 0, + "mimeType": "text/html", + "text": "\n405 Method Not Allowed\n

    Method Not Allowed

    \n

    The method is not allowed for the requested URL.

    \n" + }, + "redirectURL": "", + "headersSize": 270, + "bodySize": 178 + }, + "cache": {}, + "timings": { + "connect": 309.07583236694336, + "ssl": 358.97302627563477, + "send": 2.0020008087158203, + "receive": 3.4279823303222656, + "wait": 3039.458990097046 + }, + "serverIPAddress": "100.26.90.23" + } + ] + } +} diff --git a/test/mitmproxy/data/flows/error_log.mitm b/test/mitmproxy/data/flows/error_log.mitm new file mode 100644 index 0000000000..c57a5adc74 --- /dev/null +++ b/test/mitmproxy/data/flows/error_log.mitm @@ -0,0 +1,118 @@ +1711:4:type;4:http;7:version;2:18#9:websocket;0:~8:response;0:~7:request;411:4:path;1:/,9:authority;0:,6:scheme;4:http,6:method;3:GET,4:port;2:80#4:host;7:163.com;13:timestamp_end;18:1690282995.2192252^15:timestamp_start;17:1690282995.216374^8:trailers;0:~7:content;0:,7:headers;177:17:4:Host,7:163.com,]29:10:User-Agent,11:Wget/1.21.4,]15:6:Accept,3:*/*,]30:15:Accept-Encoding,8:identity,]28:10:Connection,10:Keep-Alive,]34:16:Proxy-Connection,10:Keep-Alive,]]12:http_version;8:HTTP/1.1,}17:timestamp_created;17:1690282995.216589^7:comment;0:;8:metadata;0:}6:marked;0:;9:is_replay;0:~11:intercepted;5:false!11:server_conn;387:4:via2;0:~11:cipher_list;0:]11:cipher_name;0:~11:alpn_offers;0:]16:certificate_list;0:]3:tls;5:false!5:error;0:~5:state;1:0#3:via;0:~11:tls_version;0:~15:tls_established;5:false!19:timestamp_tls_setup;0:~19:timestamp_tcp_setup;0:~15:timestamp_start;0:~13:timestamp_end;0:~14:source_address;0:~3:sni;0:~10:ip_address;0:~2:id;36:e2a0c5dd-5877-447c-886b-265658665ca0;4:alpn;0:~7:address;0:~}11:client_conn;454:10:proxy_mode;7:regular;11:cipher_list;0:]11:alpn_offers;0:]16:certificate_list;0:]3:tls;5:false!5:error;0:~8:sockname;19:9:127.0.0.1;4:8080#]5:state;1:0#11:tls_version;0:~14:tls_extensions;0:]15:tls_established;5:false!19:timestamp_tls_setup;0:~15:timestamp_start;17:1690282995.214719^13:timestamp_end;17:1690283145.296892^3:sni;0:~8:mitmcert;0:~2:id;36:9cf45015-5dbb-4ab4-9dc3-e59d04625413;11:cipher_name;0:~4:alpn;0:~7:address;20:9:127.0.0.1;5:51741#]}5:error;169:9:timestamp;17:1690283145.289149^3:msg;125:Multiple exceptions: [Errno 60] Connect call failed ('123.58.180.8', 80), [Errno 60] Connect call failed ('123.58.180.7', 80);}2:id;36:a68db102-de4c-477d-9412-bf2a1b9a4859;}9382:4:type;4:http;7:version;2:18#9:websocket;0:~8:response;615:6:reason;0:,11:status_code;3:405#13:timestamp_end;17:1690283279.375848^15:timestamp_start;16:1690283279.37242^8:trailers;0:~7:content;178: +405 Method Not Allowed +

    Method Not Allowed

    +

    The method is not allowed for the requested URL.

    +,7:headers;256:40:4:date,29:Tue, 25 Jul 2023 11:07:59 GMT,]28:12:content-type,9:text/html,]24:14:content-length,3:178,]28:6:server,15:gunicorn/19.9.0,]30:5:allow,18:GET, HEAD, OPTIONS,]35:27:access-control-allow-origin,1:*,]43:32:access-control-allow-credentials,4:true,]]12:http_version;8:HTTP/2.0,}7:request;416:4:path;4:/get,9:authority;11:httpbin.org,6:scheme;5:https,6:method;4:POST,4:port;3:443#4:host;11:httpbin.org;13:timestamp_end;17:1690283276.332961^15:timestamp_start;17:1690283276.330959^8:trailers;0:~7:content;23:key1=value1&key2=value2,7:headers;136:29:10:user-agent,11:curl/7.86.0,]15:6:accept,3:*/*,]53:12:content-type,33:application/x-www-form-urlencoded,]23:14:content-length,2:23,]]12:http_version;8:HTTP/2.0,}17:timestamp_created;17:1690283276.331157^7:comment;0:;8:metadata;0:}6:marked;0:;9:is_replay;0:~11:intercepted;5:false!11:server_conn;7529:4:via2;0:~11:cipher_list;0:]11:cipher_name;27:ECDHE-RSA-AES128-GCM-SHA256;11:alpn_offers;16:2:h2,8:http/1.1,]16:certificate_list;6929:2078:-----BEGIN CERTIFICATE----- +MIIF0TCCBLmgAwIBAgIQBMaXROWeY5Qs9ibwxtesVDANBgkqhkiG9w0BAQsFADA8 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRwwGgYDVQQDExNBbWF6b24g +UlNBIDIwNDggTTAyMB4XDTIzMDMwMTAwMDAwMFoXDTIzMTExOTIzNTk1OVowFjEU +MBIGA1UEAxMLaHR0cGJpbi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCPE28yK7/fA5KcuE2U5qT4TwU2GUsXvss+y3EojNC0rQPwAVVp4+ID33r9 +Wr8LusvHgyqmPu7hNA17UUCvUVWrlYtzSSkxPqDpaRtF68laf9hPtpzxsAEcJ3Zj +QLg81JYVvgodPuKsAQ/j2s0b9Yd6O//g2NI2jl5Pu94Kveo5uedSbCGdGNgm0a04 +N9egCih4CumstTUjApVv566tNUILUbIQU6Zik2dn3AR/W6OEgk7818QCfYa1YlVV +y4Z3wZ+UucKd0c73Fy3kW3MhJcQ8YwuXpoH9D338UBDIeSy7Yd5J9nOZXaq9A9eR +0GiOh3DcDL71dPEkX80qBouCpHEhAgMBAAGjggLzMIIC7zAfBgNVHSMEGDAWgBTA +MVLNWlDDgnx0cc7L6Zz5euuC4jAdBgNVHQ4EFgQU8dXJczk/NQ+psnYfrKl/NN1E +2d4wJQYDVR0RBB4wHIILaHR0cGJpbi5vcmeCDSouaHR0cGJpbi5vcmcwDgYDVR0P +AQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjA7BgNVHR8E +NDAyMDCgLqAshipodHRwOi8vY3JsLnIybTAyLmFtYXpvbnRydXN0LmNvbS9yMm0w +Mi5jcmwwEwYDVR0gBAwwCjAIBgZngQwBAgEwdQYIKwYBBQUHAQEEaTBnMC0GCCsG +AQUFBzABhiFodHRwOi8vb2NzcC5yMm0wMi5hbWF6b250cnVzdC5jb20wNgYIKwYB +BQUHMAKGKmh0dHA6Ly9jcnQucjJtMDIuYW1hem9udHJ1c3QuY29tL3IybTAyLmNl +cjAMBgNVHRMBAf8EAjAAMIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdgCt9776 +fP8QyIudPZwePhhqtGcpXc+xDCTKhYY069yCigAAAYadUPKfAAAEAwBHMEUCIBtT +nUnstwdXAMX0ZV2qinUM7CBGmLsJGslKNZbDNQjiAiEA9LLXQMqBJEoqdg5UJcSi +c3LibKO877zTkemG3QlH9dYAdgCzc3cH4YRQ+GOG1gWp3BEJSnktsWcMC4fc8AMO +eTalmgAAAYadUPLWAAAEAwBHMEUCIChRxIknXNkZN7cIUKLcLErdkkKLzFBUV6d3 +85QOXQ2gAiEApG5R/+k6XGd5QrNDa9I6IgqzTxCbCs7Xqkl8MAb73H0AdgC3Pvsk +35xNunXyOcW6WPRsXfxCz3qfNcSeHQmBJe20mQAAAYadUPKCAAAEAwBHMEUCIQCq +Sut242xHZ/P2c/8n/0EiZ/CwtgmCXfz7NdB75dYtlAIgIE4TUU2JAyIRJlCKfatQ +aAOkpEP18yLmw9GIq3nnVAAwDQYJKoZIhvcNAQELBQADggEBACjTDO0NpDuWaZnw +6nHRFcYC+kWJ9dVD7y2LaZTaMQbrB24EDudhSJuZDOvFzkz5cdSc0KOjYPorMXQ3 +z31mBqFDNE1nVKAVhGT6Z2hgmBTCWn3cJG2E6lSsKVZLC3wW02BlU/eClE4cuxS/ +vtAbE8zJosU0V/+YJWNZe649AvF0cDSRsd37arNs+iJuHdCYKpd6tVgr8qSfjiYU +5XahqdcF3R328aVe5/vpBmFtyNNI4uCsBihrJIeXLOgFkt1xo+vrQVuAx5BDjgLG +2Jbx6D7eeSQmnhwZvkBXYuZhndyqb4yn5g7q/5u2dVUuEFyX6gUAJG1cdmJxOCJw +atJSKtI= +-----END CERTIFICATE----- +,1574:-----BEGIN CERTIFICATE----- +MIIEXjCCA0agAwIBAgITB3MSSkvL1E7HtTvq8ZSELToPoTANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTIyMDgyMzIyMjUzMFoXDTMwMDgyMzIyMjUzMFowPDEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEcMBoGA1UEAxMTQW1hem9uIFJT +QSAyMDQ4IE0wMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALtDGMZa +qHneKei1by6+pUPPLljTB143Si6VpEWPc6mSkFhZb/6qrkZyoHlQLbDYnI2D7hD0 +sdzEqfnuAjIsuXQLG3A8TvX6V3oFNBFVe8NlLJHvBseKY88saLwufxkZVwk74g4n +WlNMXzla9Y5F3wwRHwMVH443xGz6UtGSZSqQ94eFx5X7Tlqt8whi8qCaKdZ5rNak ++r9nUThOeClqFd4oXych//Rc7Y0eX1KNWHYSI1Nk31mYgiK3JvH063g+K9tHA63Z +eTgKgndlh+WI+zv7i44HepRZjA1FYwYZ9Vv/9UkC5Yz8/yU65fgjaE+wVHM4e/Yy +C2osrPWE7gJ+dXMCAwEAAaOCAVowggFWMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYD +VR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNV +HQ4EFgQUwDFSzVpQw4J8dHHOy+mc+XrrguIwHwYDVR0jBBgwFoAUhBjMhTTsvAyU +lC4IWZzHshBOCggwewYIKwYBBQUHAQEEbzBtMC8GCCsGAQUFBzABhiNodHRwOi8v +b2NzcC5yb290Y2ExLmFtYXpvbnRydXN0LmNvbTA6BggrBgEFBQcwAoYuaHR0cDov +L2NydC5yb290Y2ExLmFtYXpvbnRydXN0LmNvbS9yb290Y2ExLmNlcjA/BgNVHR8E +ODA2MDSgMqAwhi5odHRwOi8vY3JsLnJvb3RjYTEuYW1hem9udHJ1c3QuY29tL3Jv +b3RjYTEuY3JsMBMGA1UdIAQMMAowCAYGZ4EMAQIBMA0GCSqGSIb3DQEBCwUAA4IB +AQAtTi6Fs0Azfi+iwm7jrz+CSxHH+uHl7Law3MQSXVtR8RV53PtR6r/6gNpqlzdo +Zq4FKbADi1v9Bun8RY8D51uedRfjsbeodizeBB8nXmeyD33Ep7VATj4ozcd31YFV +fgRhvTSxNrrTlNpWkUk0m3BMPv8sg381HhA6uEYokE5q9uws/3YkKqRiEz3TsaWm +JqIRZhMbgAfp7O7FUwFIb7UIspogZSKxPIWJpxiPo3TcBambbVtQOcNRWz5qCQdD +slI2yayq0n2TXoHyNCLEH8rpsJRVILFsg0jc7BaFrMnF462+ajSehgj12IidNeRN +4zl+EoNaWdpnWndvSpAEkq2P +-----END CERTIFICATE----- +,1647:-----BEGIN CERTIFICATE----- +MIIEkjCCA3qgAwIBAgITBn+USionzfP6wq4rAfkI7rnExjANBgkqhkiG9w0BAQsF +ADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNj +b3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4x +OzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1 +dGhvcml0eSAtIEcyMB4XDTE1MDUyNTEyMDAwMFoXDTM3MTIzMTAxMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaOCATEwggEtMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBSEGMyFNOy8DJSULghZnMeyEE4KCDAfBgNVHSMEGDAW +gBScXwDfqgHXMCs4iKK4bUqc8hGRgzB4BggrBgEFBQcBAQRsMGowLgYIKwYBBQUH +MAGGImh0dHA6Ly9vY3NwLnJvb3RnMi5hbWF6b250cnVzdC5jb20wOAYIKwYBBQUH +MAKGLGh0dHA6Ly9jcnQucm9vdGcyLmFtYXpvbnRydXN0LmNvbS9yb290ZzIuY2Vy +MD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly9jcmwucm9vdGcyLmFtYXpvbnRydXN0 +LmNvbS9yb290ZzIuY3JsMBEGA1UdIAQKMAgwBgYEVR0gADANBgkqhkiG9w0BAQsF +AAOCAQEAYjdCXLwQtT6LLOkMm2xF4gcAevnFWAu5CIw+7bMlPLVvUOTNNWqnkzSW +MiGpSESrnO09tKpzbeR/FoCJbM8oAxiDR3mjEH4wW6w7sGDgd9QIpuEdfF7Au/ma +eyKdpwAJfqxGF4PcnCZXmTA5YpaP7dreqsXMGz7KQ2hsVxa81Q4gLv7/wmpdLqBK +bRRYh5TmOTFffHPLkIhqhBGWJ6bt2YFGpn6jcgAKUj6DiAdjd4lpFw85hdKrCEVN +0FE6/V1dN2RMfjCyVSRCnTawXZwXgWHxyvkQAiSr6w10kY17RSlQOYiypok1JR4U +akcjMS9cmvqtmg5iUaQqqcT5NJ0hGA== +-----END CERTIFICATE----- +,1606:-----BEGIN CERTIFICATE----- +MIIEdTCCA12gAwIBAgIJAKcOSkw0grd/MA0GCSqGSIb3DQEBCwUAMGgxCzAJBgNV +BAYTAlVTMSUwIwYDVQQKExxTdGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTIw +MAYDVQQLEylTdGFyZmllbGQgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eTAeFw0wOTA5MDIwMDAwMDBaFw0zNDA2MjgxNzM5MTZaMIGYMQswCQYDVQQGEwJV +UzEQMA4GA1UECBMHQXJpem9uYTETMBEGA1UEBxMKU2NvdHRzZGFsZTElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjE7MDkGA1UEAxMyU3RhcmZp +ZWxkIFNlcnZpY2VzIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVDDrEKvlO4vW+GZdfjohTsR8/ +y8+fIBNtKTrID30892t2OGPZNmCom15cAICyL1l/9of5JUOG52kbUpqQ4XHj2C0N +Tm/2yEnZtvMaVq4rtnQU68/7JuMauh2WLmo7WJSJR1b/JaCTcFOD2oR0FMNnngRo +Ot+OQFodSk7PQ5E751bWAHDLUu57fa4657wx+UX2wmDPE1kCK4DMNEffud6QZW0C +zyyRpqbn3oUYSXxmTqM6bam17jQuug0DuDPfR+uxa40l2ZvOgdFFRjKWcIfeAg5J +Q4W2bHO7ZOphQazJ1FTfhy/HIrImzJ9ZVGif/L4qL8RVHHVAYBeFAlU5i38FAgMB +AAGjgfAwge0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0O +BBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMB8GA1UdIwQYMBaAFL9ft9HO3R+G9FtV +rNzXEMIOqYjnME8GCCsGAQUFBwEBBEMwQTAcBggrBgEFBQcwAYYQaHR0cDovL28u +c3MyLnVzLzAhBggrBgEFBQcwAoYVaHR0cDovL3guc3MyLnVzL3guY2VyMCYGA1Ud +HwQfMB0wG6AZoBeGFWh0dHA6Ly9zLnNzMi51cy9yLmNybDARBgNVHSAECjAIMAYG +BFUdIAAwDQYJKoZIhvcNAQELBQADggEBACMd44pXyn3pF3lM8R5V/cxTbj5HD9/G +VfKyBDbtgB9TxF00KGu+x1X8Z+rLP3+QsjPNG1gQggL4+C/1E2DUBc7xgQjB3ad1 +l08YuW3e95ORCLp+QCztweq7dp4zBncdDQh/U90bZKuCJ/Fp1U1ervShw3WnWEQt +8jxwmKy6abaVd38PMV4s/KCHOkdp8Hlf9BRUpJVeEXgSYCfOn8J3/yNTd126/+pZ +59vPr5KW7ySaNRB6nJHGDn2Z9j8Z3/VyVOEVqQdZe4O/Ui5GjLIAZHYcSNPYeehu +VsyuLAOQ1xk4meTKCRlb/weWsKh/NEnfVqn3sF/tM+2MR7cwA130A4w= +-----END CERTIFICATE----- +,]3:tls;4:true!5:error;0:~5:state;1:0#3:via;0:~11:tls_version;7:TLSv1.2;15:tls_established;4:true!19:timestamp_tls_setup;17:1690283276.283815^19:timestamp_tcp_setup;17:1690283275.924842^15:timestamp_start;17:1690283275.615766^13:timestamp_end;18:1690283279.3815942^14:source_address;27:15:192.168.178.132;5:51858#]3:sni;11:httpbin.org;10:ip_address;22:12:100.26.90.23;3:443#]2:id;36:ded73dcb-b467-4bd8-a357-3984b6121b09;4:alpn;2:h2,7:address;21:11:httpbin.org;3:443#]}11:client_conn;531:10:proxy_mode;7:regular;11:cipher_list;0:]11:alpn_offers;16:2:h2,8:http/1.1,]16:certificate_list;0:]3:tls;4:true!5:error;0:~8:sockname;19:9:127.0.0.1;4:8080#]5:state;1:0#11:tls_version;7:TLSv1.3;14:tls_extensions;0:]15:tls_established;4:true!19:timestamp_tls_setup;17:1690283276.328037^15:timestamp_start;17:1690283275.612224^13:timestamp_end;17:1690283279.380243^3:sni;11:httpbin.org;8:mitmcert;0:~2:id;36:ea89783f-0427-49d5-8b9a-cc52338dd773;11:cipher_name;22:TLS_AES_256_GCM_SHA384;4:alpn;2:h2,7:address;20:9:127.0.0.1;5:51857#]}5:error;0:~2:id;36:bba69bfb-3a7e-420d-a301-faa9de7d658b;} \ No newline at end of file diff --git a/test/mitmproxy/data/flows/event_stream.har b/test/mitmproxy/data/flows/event_stream.har new file mode 100644 index 0000000000..b6c741f674 --- /dev/null +++ b/test/mitmproxy/data/flows/event_stream.har @@ -0,0 +1,191 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "mitmproxy", + "version": "1.2.3", + "comment": "" + }, + "pages": [], + "entries": [ + { + "startedDateTime": "2023-11-05T07:00:01.738167+00:00", + "time": 179.39209938049316, + "request": { + "method": "GET", + "url": "https://clientstream.launchdarkly.com/meval/eyJhbm9ueW1vdXMiOnRydWUsImtleSI6Im1pbnQtYW5kcm9pZCJ9", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "api_key mob-82856bdd-1e79-4d98-a55f-be43b733543b" + }, + { + "name": "user-agent", + "value": "AndroidClient/3.6.0" + }, + { + "name": "cache-control", + "value": "no-cache" + }, + { + "name": "accept", + "value": "text/event-stream" + }, + { + "name": "accept-encoding", + "value": "gzip" + } + ], + "queryString": [], + "headersSize": 220, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "date", + "value": "Sun, 05 Nov 2023 07:00:01 GMT" + }, + { + "name": "content-type", + "value": "text/event-stream; charset=utf-8" + }, + { + "name": "accept-ranges", + "value": "bytes" + }, + { + "name": "cache-control", + "value": "no-cache, no-store, must-revalidate" + }, + { + "name": "ld-region", + "value": "ap-southeast-1" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000" + }, + { + "name": "x-content-length", + "value": "" + } + ], + "content": { + "size": 0, + "compression": 0, + "mimeType": "text/event-stream; charset=utf-8", + "text": "" + }, + "redirectURL": "", + "headersSize": 314, + "bodySize": 0 + }, + "cache": {}, + "timings": { + "connect": 42.35076904296875, + "ssl": 83.31537246704102, + "send": 1.6655921936035156, + "receive": 0, + "wait": 52.06036567687988 + }, + "serverIPAddress": "3.33.235.18" + }, + { + "startedDateTime": "2023-11-05T07:00:33.458265+00:00", + "time": 234.92741584777832, + "request": { + "method": "GET", + "url": "https://clientstream.launchdarkly.com/meval/eyJhbm9ueW1vdXMiOnRydWUsImtleSI6Im1pbnQtYW5kcm9pZCIsImN1c3RvbSI6eyJvcyI6MzAsImRldmljZSI6Im1vdG9yb2xhIG9uZSA1RyBhY2Uga2lldl90In19", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "authorization", + "value": "api_key mob-82856bdd-1e79-4d98-a55f-be43b733543b" + }, + { + "name": "user-agent", + "value": "AndroidClient/3.6.0" + }, + { + "name": "cache-control", + "value": "no-cache" + }, + { + "name": "accept", + "value": "text/event-stream" + }, + { + "name": "accept-encoding", + "value": "gzip" + } + ], + "queryString": [], + "headersSize": 220, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "date", + "value": "Sun, 05 Nov 2023 07:00:33 GMT" + }, + { + "name": "content-type", + "value": "text/event-stream; charset=utf-8" + }, + { + "name": "accept-ranges", + "value": "bytes" + }, + { + "name": "cache-control", + "value": "no-cache, no-store, must-revalidate" + }, + { + "name": "ld-region", + "value": "ap-southeast-1" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000" + }, + { + "name": "x-content-length", + "value": "" + } + ], + "content": { + "size": 0, + "compression": 0, + "mimeType": "text/event-stream; charset=utf-8", + "text": "" + }, + "redirectURL": "", + "headersSize": 314, + "bodySize": 0 + }, + "cache": {}, + "timings": { + "connect": 71.06399536132812, + "ssl": 107.51891136169434, + "send": 3.0012130737304688, + "receive": 0, + "wait": 53.34329605102539 + }, + "serverIPAddress": "3.33.235.18" + } + ] + } +} diff --git a/test/mitmproxy/data/flows/event_stream.mitm b/test/mitmproxy/data/flows/event_stream.mitm new file mode 100644 index 0000000000..33e96c4f5c --- /dev/null +++ b/test/mitmproxy/data/flows/event_stream.mitm @@ -0,0 +1,229 @@ +9494:9:websocket;0:~8:response;466:6:reason;0:,11:status_code;3:200#13:timestamp_end;0:~15:timestamp_start;18:1699167601.7918932^8:trailers;0:~7:content;0:~7:headers;303:40:4:date,29:Sun, 05 Nov 2023 07:00:01 GMT,]52:12:content-type,32:text/event-stream; charset=utf-8,]25:13:accept-ranges,5:bytes,]56:13:cache-control,35:no-cache, no-store, must-revalidate,]30:9:ld-region,14:ap-southeast-1,]49:25:strict-transport-security,16:max-age=31536000,]23:16:x-content-length,0:,]]12:http_version;8:HTTP/2.0,}7:request;541:4:path;59:/meval/eyJhbm9ueW1vdXMiOnRydWUsImtleSI6Im1pbnQtYW5kcm9pZCJ9,9:authority;29:clientstream.launchdarkly.com,6:scheme;5:https,6:method;3:GET,4:port;3:443#4:host;11:3.33.235.18;13:timestamp_end;18:1699167601.7398329^15:timestamp_start;18:1699167601.7381673^8:trailers;0:~7:content;0:,7:headers;210:69:13:authorization,48:api_key mob-82856bdd-1e79-4d98-a55f-be43b733543b,]37:10:user-agent,19:AndroidClient/3.6.0,]28:13:cache-control,8:no-cache,]30:6:accept,17:text/event-stream,]26:15:accept-encoding,4:gzip,]]12:http_version;8:HTTP/2.0,}6:backup;0:~17:timestamp_created;18:1699167601.7383106^7:comment;0:;8:metadata;0:}6:marked;0:;9:is_replay;0:~11:intercepted;5:false!11:server_conn;7585:3:via;0:~19:timestamp_tcp_setup;18:1699167601.6371436^7:address;21:11:3.33.235.18;3:443#]19:timestamp_tls_setup;17:1699167601.720459^13:timestamp_end;17:1699167634.153953^15:timestamp_start;18:1699167601.5947928^3:sni;29:clientstream.launchdarkly.com;11:tls_version;7:TLSv1.2;11:cipher_list;0:]6:cipher;27:ECDHE-RSA-AES128-GCM-SHA256;11:alpn_offers;16:2:h2,8:http/1.1,]4:alpn;2:h2,16:certificate_list;7006:2155:-----BEGIN CERTIFICATE----- +MIIGCTCCBPGgAwIBAgIQAeDbDv9XbpH7ymPXOB5fSzANBgkqhkiG9w0BAQsFADA8 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRwwGgYDVQQDExNBbWF6b24g +UlNBIDIwNDggTTAyMB4XDTIzMDgxMDAwMDAwMFoXDTI0MDkwNzIzNTk1OVowKDEm +MCQGA1UEAxMdY2xpZW50c3RyZWFtLmxhdW5jaGRhcmtseS5jb20wggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCut88w5htLYomN3YjNau9j8dnzK6XsGYy4 +SpvGFuQavU+H+rVnSTF4lhZ9HL2kAVmeS/Rb/+ba7cf9NTLOcjY7zUt51Q83AI7G +/nt6fcnaEaWQBgNkC/+wWXgw1KlAkq0y73zy2E60VWa4wT9uGuMQ+1F8q4sQLbqJ +N3C2axoAVl6YmLUyjRgCeDGRE5XS1Qbza0cmNj26XO3gBVsOnrSUYx+4NnaFKheI +NIU4go59sszO6Ch4MmXR3Sf0L/AKQ1Nn4mcL06xom4KTTlznDD0jWk9AiGbWpXqO +q6LNzTqf/m94Sssu3nYNaUnRDTPGy2kAjBlx0PSfi8jYHSMDHrclAgMBAAGjggMZ +MIIDFTAfBgNVHSMEGDAWgBTAMVLNWlDDgnx0cc7L6Zz5euuC4jAdBgNVHQ4EFgQU +gi4py809RHPfJVgZx1TnRkPB3UcwTAYDVR0RBEUwQ4IdY2xpZW50c3RyZWFtLmxh +dW5jaGRhcmtseS5jb22CImNsaWVudHN0cmVhbS1hcGFjLmxhdW5jaGRhcmtseS5j +b20wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD +AjA7BgNVHR8ENDAyMDCgLqAshipodHRwOi8vY3JsLnIybTAyLmFtYXpvbnRydXN0 +LmNvbS9yMm0wMi5jcmwwEwYDVR0gBAwwCjAIBgZngQwBAgEwdQYIKwYBBQUHAQEE +aTBnMC0GCCsGAQUFBzABhiFodHRwOi8vb2NzcC5yMm0wMi5hbWF6b250cnVzdC5j +b20wNgYIKwYBBQUHMAKGKmh0dHA6Ly9jcnQucjJtMDIuYW1hem9udHJ1c3QuY29t +L3IybTAyLmNlcjAMBgNVHRMBAf8EAjAAMIIBfQYKKwYBBAHWeQIEAgSCAW0EggFp +AWcAdgDuzdBk1dsazsVct520zROiModGfLzs3sNRSFlGcR+1mwAAAYnc2rfaAAAE +AwBHMEUCIQDb9cdSARKuFj3FN5Bt9D2y7d2TSZGauiXl4piBFT0POAIgB8HUVsZy +sTJv1+qVL40RJB5IrkUZMpumGp6DPEWw0REAdQBIsONr2qZHNA/lagL6nTDrHFIB +y1bdLIHZu7+rOdiEcwAAAYnc2reaAAAEAwBGMEQCIC/Uv7DdZzaXR3kSqfhgGnlQ +AYmkeLXco54Ud/rr3sqnAiBjF5uxWhMrSy40JI/PetcXysH6bQRBfgEosu7wLlOm +zwB2ANq2v2s/tbYin5vCu1xr6HCRcWy7UYSFNL2kPTBI1/urAAABidzat1AAAAQD +AEcwRQIgALGkJe9WWmATZzzrxW1yYrjcqc1T9Zwzo1IrRNEkOVoCIQDYkadYMJnE +s04XmAEvLugCzlQr2jqgNEvRlalg3Pf08zANBgkqhkiG9w0BAQsFAAOCAQEASPho +9XBpJ+IMYGDOe3HO+a8st/wPgldClZv8wo15+fwEckIqoTQvdyDIpCqRtY1ivd4+ +rSVvqeOcYh+lFe4ubOFnI/2v7GTeEYavh6ddS2NtGnTkhC1FtalpKchRNC/+aZxF +xqUqtoGXrchsq8ux0qbIC8ksHVN0KCQ1LO6g1AHUvXazRhEYVtf/WuO6JSh39x6p +23ii3bMTwO/FmKfFGxYCCu9Wsb/9THgszuTgYVqmzTB9KzljSatP9qls8fx6M1Yl +mL/UhXWZDWUC1/fJ7jgKC3qsaaMiogIZoMwkP5cMbwfYabMis1b4zNtGm/C7ETqx +yqcie8qy4k5iTJxl4A== +-----END CERTIFICATE----- +,1574:-----BEGIN CERTIFICATE----- +MIIEXjCCA0agAwIBAgITB3MSSkvL1E7HtTvq8ZSELToPoTANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTIyMDgyMzIyMjUzMFoXDTMwMDgyMzIyMjUzMFowPDEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEcMBoGA1UEAxMTQW1hem9uIFJT +QSAyMDQ4IE0wMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALtDGMZa +qHneKei1by6+pUPPLljTB143Si6VpEWPc6mSkFhZb/6qrkZyoHlQLbDYnI2D7hD0 +sdzEqfnuAjIsuXQLG3A8TvX6V3oFNBFVe8NlLJHvBseKY88saLwufxkZVwk74g4n +WlNMXzla9Y5F3wwRHwMVH443xGz6UtGSZSqQ94eFx5X7Tlqt8whi8qCaKdZ5rNak ++r9nUThOeClqFd4oXych//Rc7Y0eX1KNWHYSI1Nk31mYgiK3JvH063g+K9tHA63Z +eTgKgndlh+WI+zv7i44HepRZjA1FYwYZ9Vv/9UkC5Yz8/yU65fgjaE+wVHM4e/Yy +C2osrPWE7gJ+dXMCAwEAAaOCAVowggFWMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYD +VR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNV +HQ4EFgQUwDFSzVpQw4J8dHHOy+mc+XrrguIwHwYDVR0jBBgwFoAUhBjMhTTsvAyU +lC4IWZzHshBOCggwewYIKwYBBQUHAQEEbzBtMC8GCCsGAQUFBzABhiNodHRwOi8v +b2NzcC5yb290Y2ExLmFtYXpvbnRydXN0LmNvbTA6BggrBgEFBQcwAoYuaHR0cDov +L2NydC5yb290Y2ExLmFtYXpvbnRydXN0LmNvbS9yb290Y2ExLmNlcjA/BgNVHR8E +ODA2MDSgMqAwhi5odHRwOi8vY3JsLnJvb3RjYTEuYW1hem9udHJ1c3QuY29tL3Jv +b3RjYTEuY3JsMBMGA1UdIAQMMAowCAYGZ4EMAQIBMA0GCSqGSIb3DQEBCwUAA4IB +AQAtTi6Fs0Azfi+iwm7jrz+CSxHH+uHl7Law3MQSXVtR8RV53PtR6r/6gNpqlzdo +Zq4FKbADi1v9Bun8RY8D51uedRfjsbeodizeBB8nXmeyD33Ep7VATj4ozcd31YFV +fgRhvTSxNrrTlNpWkUk0m3BMPv8sg381HhA6uEYokE5q9uws/3YkKqRiEz3TsaWm +JqIRZhMbgAfp7O7FUwFIb7UIspogZSKxPIWJpxiPo3TcBambbVtQOcNRWz5qCQdD +slI2yayq0n2TXoHyNCLEH8rpsJRVILFsg0jc7BaFrMnF462+ajSehgj12IidNeRN +4zl+EoNaWdpnWndvSpAEkq2P +-----END CERTIFICATE----- +,1647:-----BEGIN CERTIFICATE----- +MIIEkjCCA3qgAwIBAgITBn+USionzfP6wq4rAfkI7rnExjANBgkqhkiG9w0BAQsF +ADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNj +b3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4x +OzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1 +dGhvcml0eSAtIEcyMB4XDTE1MDUyNTEyMDAwMFoXDTM3MTIzMTAxMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaOCATEwggEtMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBSEGMyFNOy8DJSULghZnMeyEE4KCDAfBgNVHSMEGDAW +gBScXwDfqgHXMCs4iKK4bUqc8hGRgzB4BggrBgEFBQcBAQRsMGowLgYIKwYBBQUH +MAGGImh0dHA6Ly9vY3NwLnJvb3RnMi5hbWF6b250cnVzdC5jb20wOAYIKwYBBQUH +MAKGLGh0dHA6Ly9jcnQucm9vdGcyLmFtYXpvbnRydXN0LmNvbS9yb290ZzIuY2Vy +MD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly9jcmwucm9vdGcyLmFtYXpvbnRydXN0 +LmNvbS9yb290ZzIuY3JsMBEGA1UdIAQKMAgwBgYEVR0gADANBgkqhkiG9w0BAQsF +AAOCAQEAYjdCXLwQtT6LLOkMm2xF4gcAevnFWAu5CIw+7bMlPLVvUOTNNWqnkzSW +MiGpSESrnO09tKpzbeR/FoCJbM8oAxiDR3mjEH4wW6w7sGDgd9QIpuEdfF7Au/ma +eyKdpwAJfqxGF4PcnCZXmTA5YpaP7dreqsXMGz7KQ2hsVxa81Q4gLv7/wmpdLqBK +bRRYh5TmOTFffHPLkIhqhBGWJ6bt2YFGpn6jcgAKUj6DiAdjd4lpFw85hdKrCEVN +0FE6/V1dN2RMfjCyVSRCnTawXZwXgWHxyvkQAiSr6w10kY17RSlQOYiypok1JR4U +akcjMS9cmvqtmg5iUaQqqcT5NJ0hGA== +-----END CERTIFICATE----- +,1606:-----BEGIN CERTIFICATE----- +MIIEdTCCA12gAwIBAgIJAKcOSkw0grd/MA0GCSqGSIb3DQEBCwUAMGgxCzAJBgNV +BAYTAlVTMSUwIwYDVQQKExxTdGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTIw +MAYDVQQLEylTdGFyZmllbGQgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eTAeFw0wOTA5MDIwMDAwMDBaFw0zNDA2MjgxNzM5MTZaMIGYMQswCQYDVQQGEwJV +UzEQMA4GA1UECBMHQXJpem9uYTETMBEGA1UEBxMKU2NvdHRzZGFsZTElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjE7MDkGA1UEAxMyU3RhcmZp +ZWxkIFNlcnZpY2VzIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVDDrEKvlO4vW+GZdfjohTsR8/ +y8+fIBNtKTrID30892t2OGPZNmCom15cAICyL1l/9of5JUOG52kbUpqQ4XHj2C0N +Tm/2yEnZtvMaVq4rtnQU68/7JuMauh2WLmo7WJSJR1b/JaCTcFOD2oR0FMNnngRo +Ot+OQFodSk7PQ5E751bWAHDLUu57fa4657wx+UX2wmDPE1kCK4DMNEffud6QZW0C +zyyRpqbn3oUYSXxmTqM6bam17jQuug0DuDPfR+uxa40l2ZvOgdFFRjKWcIfeAg5J +Q4W2bHO7ZOphQazJ1FTfhy/HIrImzJ9ZVGif/L4qL8RVHHVAYBeFAlU5i38FAgMB +AAGjgfAwge0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0O +BBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMB8GA1UdIwQYMBaAFL9ft9HO3R+G9FtV +rNzXEMIOqYjnME8GCCsGAQUFBwEBBEMwQTAcBggrBgEFBQcwAYYQaHR0cDovL28u +c3MyLnVzLzAhBggrBgEFBQcwAoYVaHR0cDovL3guc3MyLnVzL3guY2VyMCYGA1Ud +HwQfMB0wG6AZoBeGFWh0dHA6Ly9zLnNzMi51cy9yLmNybDARBgNVHSAECjAIMAYG +BFUdIAAwDQYJKoZIhvcNAQELBQADggEBACMd44pXyn3pF3lM8R5V/cxTbj5HD9/G +VfKyBDbtgB9TxF00KGu+x1X8Z+rLP3+QsjPNG1gQggL4+C/1E2DUBc7xgQjB3ad1 +l08YuW3e95ORCLp+QCztweq7dp4zBncdDQh/U90bZKuCJ/Fp1U1ervShw3WnWEQt +8jxwmKy6abaVd38PMV4s/KCHOkdp8Hlf9BRUpJVeEXgSYCfOn8J3/yNTd126/+pZ +59vPr5KW7ySaNRB6nJHGDn2Z9j8Z3/VyVOEVqQdZe4O/Ui5GjLIAZHYcSNPYeehu +VsyuLAOQ1xk4meTKCRlb/weWsKh/NEnfVqn3sF/tM+2MR7cwA130A4w= +-----END CERTIFICATE----- +,]3:tls;4:true!5:error;0:~18:transport_protocol;3:tcp;2:id;36:4e471ff3-058f-44ff-bc8e-0e6602f62ebc;8:sockname;24:12:192.168.1.55;5:51442#]8:peername;21:11:3.33.235.18;3:443#]}11:client_conn;522:10:proxy_mode;9:wireguard;8:mitmcert;0:~19:timestamp_tls_setup;17:1699167601.730874^13:timestamp_end;18:1699167634.1526332^15:timestamp_start;17:1699167601.593318^3:sni;29:clientstream.launchdarkly.com;11:tls_version;7:TLSv1.2;11:cipher_list;0:]6:cipher;27:ECDHE-RSA-AES128-GCM-SHA256;11:alpn_offers;16:2:h2,8:http/1.1,]4:alpn;2:h2,16:certificate_list;0:]3:tls;4:true!5:error;0:~18:transport_protocol;3:tcp;2:id;36:d19eddc3-ebfe-4f6a-8847-df502419a55b;8:sockname;21:11:3.33.235.18;3:443#]8:peername;19:8:10.0.0.4;5:47050#]}5:error;75:9:timestamp;18:1699167633.1559772^3:msg;31:stream reset by client (CANCEL);}2:id;36:bd854f99-a155-4d5b-849c-a3619ab386b7;4:type;4:http;7:version;2:20#}9456:9:websocket;0:~8:response;466:6:reason;0:,11:status_code;3:200#13:timestamp_end;0:~15:timestamp_start;18:1699167633.5146096^8:trailers;0:~7:content;0:~7:headers;303:40:4:date,29:Sun, 05 Nov 2023 07:00:33 GMT,]52:12:content-type,32:text/event-stream; charset=utf-8,]25:13:accept-ranges,5:bytes,]56:13:cache-control,35:no-cache, no-store, must-revalidate,]30:9:ld-region,14:ap-southeast-1,]49:25:strict-transport-security,16:max-age=31536000,]23:16:x-content-length,0:,]]12:http_version;8:HTTP/2.0,}7:request;617:4:path;135:/meval/eyJhbm9ueW1vdXMiOnRydWUsImtleSI6Im1pbnQtYW5kcm9pZCIsImN1c3RvbSI6eyJvcyI6MzAsImRldmljZSI6Im1vdG9yb2xhIG9uZSA1RyBhY2Uga2lldl90In19,9:authority;29:clientstream.launchdarkly.com,6:scheme;5:https,6:method;3:GET,4:port;3:443#4:host;11:3.33.235.18;13:timestamp_end;18:1699167633.4612663^15:timestamp_start;17:1699167633.458265^8:trailers;0:~7:content;0:,7:headers;210:69:13:authorization,48:api_key mob-82856bdd-1e79-4d98-a55f-be43b733543b,]37:10:user-agent,19:AndroidClient/3.6.0,]28:13:cache-control,8:no-cache,]30:6:accept,17:text/event-stream,]26:15:accept-encoding,4:gzip,]]12:http_version;8:HTTP/2.0,}6:backup;0:~17:timestamp_created;18:1699167633.4584794^7:comment;0:;8:metadata;0:}6:marked;0:;9:is_replay;0:~11:intercepted;5:false!11:server_conn;7566:3:via;0:~19:timestamp_tcp_setup;17:1699167633.312949^7:address;21:11:3.33.235.18;3:443#]19:timestamp_tls_setup;18:1699167633.4204679^13:timestamp_end;0:~15:timestamp_start;17:1699167633.241885^3:sni;29:clientstream.launchdarkly.com;11:tls_version;7:TLSv1.2;11:cipher_list;0:]6:cipher;27:ECDHE-RSA-AES128-GCM-SHA256;11:alpn_offers;16:2:h2,8:http/1.1,]4:alpn;2:h2,16:certificate_list;7006:2155:-----BEGIN CERTIFICATE----- +MIIGCTCCBPGgAwIBAgIQAeDbDv9XbpH7ymPXOB5fSzANBgkqhkiG9w0BAQsFADA8 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRwwGgYDVQQDExNBbWF6b24g +UlNBIDIwNDggTTAyMB4XDTIzMDgxMDAwMDAwMFoXDTI0MDkwNzIzNTk1OVowKDEm +MCQGA1UEAxMdY2xpZW50c3RyZWFtLmxhdW5jaGRhcmtseS5jb20wggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCut88w5htLYomN3YjNau9j8dnzK6XsGYy4 +SpvGFuQavU+H+rVnSTF4lhZ9HL2kAVmeS/Rb/+ba7cf9NTLOcjY7zUt51Q83AI7G +/nt6fcnaEaWQBgNkC/+wWXgw1KlAkq0y73zy2E60VWa4wT9uGuMQ+1F8q4sQLbqJ +N3C2axoAVl6YmLUyjRgCeDGRE5XS1Qbza0cmNj26XO3gBVsOnrSUYx+4NnaFKheI +NIU4go59sszO6Ch4MmXR3Sf0L/AKQ1Nn4mcL06xom4KTTlznDD0jWk9AiGbWpXqO +q6LNzTqf/m94Sssu3nYNaUnRDTPGy2kAjBlx0PSfi8jYHSMDHrclAgMBAAGjggMZ +MIIDFTAfBgNVHSMEGDAWgBTAMVLNWlDDgnx0cc7L6Zz5euuC4jAdBgNVHQ4EFgQU +gi4py809RHPfJVgZx1TnRkPB3UcwTAYDVR0RBEUwQ4IdY2xpZW50c3RyZWFtLmxh +dW5jaGRhcmtseS5jb22CImNsaWVudHN0cmVhbS1hcGFjLmxhdW5jaGRhcmtseS5j +b20wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD +AjA7BgNVHR8ENDAyMDCgLqAshipodHRwOi8vY3JsLnIybTAyLmFtYXpvbnRydXN0 +LmNvbS9yMm0wMi5jcmwwEwYDVR0gBAwwCjAIBgZngQwBAgEwdQYIKwYBBQUHAQEE +aTBnMC0GCCsGAQUFBzABhiFodHRwOi8vb2NzcC5yMm0wMi5hbWF6b250cnVzdC5j +b20wNgYIKwYBBQUHMAKGKmh0dHA6Ly9jcnQucjJtMDIuYW1hem9udHJ1c3QuY29t +L3IybTAyLmNlcjAMBgNVHRMBAf8EAjAAMIIBfQYKKwYBBAHWeQIEAgSCAW0EggFp +AWcAdgDuzdBk1dsazsVct520zROiModGfLzs3sNRSFlGcR+1mwAAAYnc2rfaAAAE +AwBHMEUCIQDb9cdSARKuFj3FN5Bt9D2y7d2TSZGauiXl4piBFT0POAIgB8HUVsZy +sTJv1+qVL40RJB5IrkUZMpumGp6DPEWw0REAdQBIsONr2qZHNA/lagL6nTDrHFIB +y1bdLIHZu7+rOdiEcwAAAYnc2reaAAAEAwBGMEQCIC/Uv7DdZzaXR3kSqfhgGnlQ +AYmkeLXco54Ud/rr3sqnAiBjF5uxWhMrSy40JI/PetcXysH6bQRBfgEosu7wLlOm +zwB2ANq2v2s/tbYin5vCu1xr6HCRcWy7UYSFNL2kPTBI1/urAAABidzat1AAAAQD +AEcwRQIgALGkJe9WWmATZzzrxW1yYrjcqc1T9Zwzo1IrRNEkOVoCIQDYkadYMJnE +s04XmAEvLugCzlQr2jqgNEvRlalg3Pf08zANBgkqhkiG9w0BAQsFAAOCAQEASPho +9XBpJ+IMYGDOe3HO+a8st/wPgldClZv8wo15+fwEckIqoTQvdyDIpCqRtY1ivd4+ +rSVvqeOcYh+lFe4ubOFnI/2v7GTeEYavh6ddS2NtGnTkhC1FtalpKchRNC/+aZxF +xqUqtoGXrchsq8ux0qbIC8ksHVN0KCQ1LO6g1AHUvXazRhEYVtf/WuO6JSh39x6p +23ii3bMTwO/FmKfFGxYCCu9Wsb/9THgszuTgYVqmzTB9KzljSatP9qls8fx6M1Yl +mL/UhXWZDWUC1/fJ7jgKC3qsaaMiogIZoMwkP5cMbwfYabMis1b4zNtGm/C7ETqx +yqcie8qy4k5iTJxl4A== +-----END CERTIFICATE----- +,1574:-----BEGIN CERTIFICATE----- +MIIEXjCCA0agAwIBAgITB3MSSkvL1E7HtTvq8ZSELToPoTANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTIyMDgyMzIyMjUzMFoXDTMwMDgyMzIyMjUzMFowPDEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEcMBoGA1UEAxMTQW1hem9uIFJT +QSAyMDQ4IE0wMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALtDGMZa +qHneKei1by6+pUPPLljTB143Si6VpEWPc6mSkFhZb/6qrkZyoHlQLbDYnI2D7hD0 +sdzEqfnuAjIsuXQLG3A8TvX6V3oFNBFVe8NlLJHvBseKY88saLwufxkZVwk74g4n +WlNMXzla9Y5F3wwRHwMVH443xGz6UtGSZSqQ94eFx5X7Tlqt8whi8qCaKdZ5rNak ++r9nUThOeClqFd4oXych//Rc7Y0eX1KNWHYSI1Nk31mYgiK3JvH063g+K9tHA63Z +eTgKgndlh+WI+zv7i44HepRZjA1FYwYZ9Vv/9UkC5Yz8/yU65fgjaE+wVHM4e/Yy +C2osrPWE7gJ+dXMCAwEAAaOCAVowggFWMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYD +VR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNV +HQ4EFgQUwDFSzVpQw4J8dHHOy+mc+XrrguIwHwYDVR0jBBgwFoAUhBjMhTTsvAyU +lC4IWZzHshBOCggwewYIKwYBBQUHAQEEbzBtMC8GCCsGAQUFBzABhiNodHRwOi8v +b2NzcC5yb290Y2ExLmFtYXpvbnRydXN0LmNvbTA6BggrBgEFBQcwAoYuaHR0cDov +L2NydC5yb290Y2ExLmFtYXpvbnRydXN0LmNvbS9yb290Y2ExLmNlcjA/BgNVHR8E +ODA2MDSgMqAwhi5odHRwOi8vY3JsLnJvb3RjYTEuYW1hem9udHJ1c3QuY29tL3Jv +b3RjYTEuY3JsMBMGA1UdIAQMMAowCAYGZ4EMAQIBMA0GCSqGSIb3DQEBCwUAA4IB +AQAtTi6Fs0Azfi+iwm7jrz+CSxHH+uHl7Law3MQSXVtR8RV53PtR6r/6gNpqlzdo +Zq4FKbADi1v9Bun8RY8D51uedRfjsbeodizeBB8nXmeyD33Ep7VATj4ozcd31YFV +fgRhvTSxNrrTlNpWkUk0m3BMPv8sg381HhA6uEYokE5q9uws/3YkKqRiEz3TsaWm +JqIRZhMbgAfp7O7FUwFIb7UIspogZSKxPIWJpxiPo3TcBambbVtQOcNRWz5qCQdD +slI2yayq0n2TXoHyNCLEH8rpsJRVILFsg0jc7BaFrMnF462+ajSehgj12IidNeRN +4zl+EoNaWdpnWndvSpAEkq2P +-----END CERTIFICATE----- +,1647:-----BEGIN CERTIFICATE----- +MIIEkjCCA3qgAwIBAgITBn+USionzfP6wq4rAfkI7rnExjANBgkqhkiG9w0BAQsF +ADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNj +b3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4x +OzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1 +dGhvcml0eSAtIEcyMB4XDTE1MDUyNTEyMDAwMFoXDTM3MTIzMTAxMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaOCATEwggEtMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBSEGMyFNOy8DJSULghZnMeyEE4KCDAfBgNVHSMEGDAW +gBScXwDfqgHXMCs4iKK4bUqc8hGRgzB4BggrBgEFBQcBAQRsMGowLgYIKwYBBQUH +MAGGImh0dHA6Ly9vY3NwLnJvb3RnMi5hbWF6b250cnVzdC5jb20wOAYIKwYBBQUH +MAKGLGh0dHA6Ly9jcnQucm9vdGcyLmFtYXpvbnRydXN0LmNvbS9yb290ZzIuY2Vy +MD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly9jcmwucm9vdGcyLmFtYXpvbnRydXN0 +LmNvbS9yb290ZzIuY3JsMBEGA1UdIAQKMAgwBgYEVR0gADANBgkqhkiG9w0BAQsF +AAOCAQEAYjdCXLwQtT6LLOkMm2xF4gcAevnFWAu5CIw+7bMlPLVvUOTNNWqnkzSW +MiGpSESrnO09tKpzbeR/FoCJbM8oAxiDR3mjEH4wW6w7sGDgd9QIpuEdfF7Au/ma +eyKdpwAJfqxGF4PcnCZXmTA5YpaP7dreqsXMGz7KQ2hsVxa81Q4gLv7/wmpdLqBK +bRRYh5TmOTFffHPLkIhqhBGWJ6bt2YFGpn6jcgAKUj6DiAdjd4lpFw85hdKrCEVN +0FE6/V1dN2RMfjCyVSRCnTawXZwXgWHxyvkQAiSr6w10kY17RSlQOYiypok1JR4U +akcjMS9cmvqtmg5iUaQqqcT5NJ0hGA== +-----END CERTIFICATE----- +,1606:-----BEGIN CERTIFICATE----- +MIIEdTCCA12gAwIBAgIJAKcOSkw0grd/MA0GCSqGSIb3DQEBCwUAMGgxCzAJBgNV +BAYTAlVTMSUwIwYDVQQKExxTdGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTIw +MAYDVQQLEylTdGFyZmllbGQgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eTAeFw0wOTA5MDIwMDAwMDBaFw0zNDA2MjgxNzM5MTZaMIGYMQswCQYDVQQGEwJV +UzEQMA4GA1UECBMHQXJpem9uYTETMBEGA1UEBxMKU2NvdHRzZGFsZTElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjE7MDkGA1UEAxMyU3RhcmZp +ZWxkIFNlcnZpY2VzIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVDDrEKvlO4vW+GZdfjohTsR8/ +y8+fIBNtKTrID30892t2OGPZNmCom15cAICyL1l/9of5JUOG52kbUpqQ4XHj2C0N +Tm/2yEnZtvMaVq4rtnQU68/7JuMauh2WLmo7WJSJR1b/JaCTcFOD2oR0FMNnngRo +Ot+OQFodSk7PQ5E751bWAHDLUu57fa4657wx+UX2wmDPE1kCK4DMNEffud6QZW0C +zyyRpqbn3oUYSXxmTqM6bam17jQuug0DuDPfR+uxa40l2ZvOgdFFRjKWcIfeAg5J +Q4W2bHO7ZOphQazJ1FTfhy/HIrImzJ9ZVGif/L4qL8RVHHVAYBeFAlU5i38FAgMB +AAGjgfAwge0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0O +BBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMB8GA1UdIwQYMBaAFL9ft9HO3R+G9FtV +rNzXEMIOqYjnME8GCCsGAQUFBwEBBEMwQTAcBggrBgEFBQcwAYYQaHR0cDovL28u +c3MyLnVzLzAhBggrBgEFBQcwAoYVaHR0cDovL3guc3MyLnVzL3guY2VyMCYGA1Ud +HwQfMB0wG6AZoBeGFWh0dHA6Ly9zLnNzMi51cy9yLmNybDARBgNVHSAECjAIMAYG +BFUdIAAwDQYJKoZIhvcNAQELBQADggEBACMd44pXyn3pF3lM8R5V/cxTbj5HD9/G +VfKyBDbtgB9TxF00KGu+x1X8Z+rLP3+QsjPNG1gQggL4+C/1E2DUBc7xgQjB3ad1 +l08YuW3e95ORCLp+QCztweq7dp4zBncdDQh/U90bZKuCJ/Fp1U1ervShw3WnWEQt +8jxwmKy6abaVd38PMV4s/KCHOkdp8Hlf9BRUpJVeEXgSYCfOn8J3/yNTd126/+pZ +59vPr5KW7ySaNRB6nJHGDn2Z9j8Z3/VyVOEVqQdZe4O/Ui5GjLIAZHYcSNPYeehu +VsyuLAOQ1xk4meTKCRlb/weWsKh/NEnfVqn3sF/tM+2MR7cwA130A4w= +-----END CERTIFICATE----- +,]3:tls;4:true!5:error;0:~18:transport_protocol;3:tcp;2:id;36:0518ec85-b237-4122-8d11-44ad73578448;8:sockname;24:12:192.168.1.55;5:51444#]8:peername;21:11:3.33.235.18;3:443#]}11:client_conn;503:10:proxy_mode;9:wireguard;8:mitmcert;0:~19:timestamp_tls_setup;17:1699167633.445254^13:timestamp_end;0:~15:timestamp_start;17:1699167633.236526^3:sni;29:clientstream.launchdarkly.com;11:tls_version;7:TLSv1.2;11:cipher_list;0:]6:cipher;27:ECDHE-RSA-AES128-GCM-SHA256;11:alpn_offers;16:2:h2,8:http/1.1,]4:alpn;2:h2,16:certificate_list;0:]3:tls;4:true!5:error;0:~18:transport_protocol;3:tcp;2:id;36:66cbd513-8e07-4cd8-9a5c-7d65c92e0e06;8:sockname;21:11:3.33.235.18;3:443#]8:peername;19:8:10.0.0.4;5:47110#]}5:error;0:~2:id;36:343f9338-38a4-4c01-afb2-f8a3a64b69ee;4:type;4:http;7:version;2:20#} \ No newline at end of file diff --git a/test/mitmproxy/data/flows/incomplete_log.har b/test/mitmproxy/data/flows/incomplete_log.har new file mode 100644 index 0000000000..2d2c3709e3 --- /dev/null +++ b/test/mitmproxy/data/flows/incomplete_log.har @@ -0,0 +1,199 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "mitmproxy", + "version": "1.2.3", + "comment": "" + }, + "pages": [], + "entries": [ + { + "startedDateTime": "2023-07-17T13:04:44.056651+00:00", + "time": 0.0, + "request": { + "method": "GET", + "url": "https://example.com/", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "0" + }, + { + "name": "Host", + "value": "example.com" + } + ], + "queryString": [], + "headersSize": 61, + "bodySize": 0 + }, + "response": { + "status": 0, + "statusText": "", + "httpVersion": "", + "headers": [], + "cookies": [], + "content": {}, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1, + "_transferSize": 0, + "_error": null + }, + "cache": {}, + "timings": { + "connect": -1.0, + "ssl": -1.0, + "send": 0.0, + "receive": 0, + "wait": 0 + } + }, + { + "startedDateTime": "2023-07-17T14:35:39.872231+00:00", + "time": 0.00095367431640625, + "request": { + "method": "GET", + "url": "https://google.com/", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "0" + }, + { + "name": "Host", + "value": "google.com" + } + ], + "queryString": [], + "headersSize": 60, + "bodySize": 0 + }, + "response": { + "status": 0, + "statusText": "", + "httpVersion": "", + "headers": [], + "cookies": [], + "content": {}, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1, + "_transferSize": 0, + "_error": null + }, + "cache": {}, + "timings": { + "connect": -1.0, + "ssl": -1.0, + "send": 0.00095367431640625, + "receive": 0, + "wait": 0 + } + }, + { + "startedDateTime": "2023-07-17T14:35:51.329621+00:00", + "time": 0.0, + "request": { + "method": "POST", + "url": "https://google.com/", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "0" + }, + { + "name": "Host", + "value": "google.com" + } + ], + "queryString": [], + "headersSize": 60, + "bodySize": 0, + "postData": { + "mimeType": "", + "text": "", + "params": [] + } + }, + "response": { + "status": 0, + "statusText": "", + "httpVersion": "", + "headers": [], + "cookies": [], + "content": {}, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1, + "_transferSize": 0, + "_error": null + }, + "cache": {}, + "timings": { + "connect": -1.0, + "ssl": -1.0, + "send": 0.0, + "receive": 0, + "wait": 0 + } + }, + { + "startedDateTime": "2023-07-17T14:36:02.914265+00:00", + "time": 0.0, + "request": { + "method": "POST", + "url": "https://google.com/", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "0" + }, + { + "name": "Host", + "value": "google.com" + } + ], + "queryString": [], + "headersSize": 60, + "bodySize": 0, + "postData": { + "mimeType": "", + "text": "", + "params": [] + } + }, + "response": { + "status": 0, + "statusText": "", + "httpVersion": "", + "headers": [], + "cookies": [], + "content": {}, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1, + "_transferSize": 0, + "_error": null + }, + "cache": {}, + "timings": { + "connect": -1.0, + "ssl": -1.0, + "send": 0.0, + "receive": 0, + "wait": 0 + } + } + ] + } +} diff --git a/test/mitmproxy/data/flows/incomplete_log.mitm b/test/mitmproxy/data/flows/incomplete_log.mitm new file mode 100644 index 0000000000..037520c5b7 --- /dev/null +++ b/test/mitmproxy/data/flows/incomplete_log.mitm @@ -0,0 +1 @@ +1398:4:type;4:http;7:version;2:18#9:websocket;0:~8:response;0:~7:request;291:4:path;1:/,9:authority;0:,6:scheme;5:https,6:method;3:GET,4:port;3:443#4:host;11:example.com;13:timestamp_end;17:1689599084.056651^15:timestamp_start;17:1689599084.056651^8:trailers;0:~7:content;0:,7:headers;52:22:14:content-length,1:0,]22:4:Host,11:example.com,]]12:http_version;8:HTTP/1.1,}17:timestamp_created;17:1689599084.061592^7:comment;0:;8:metadata;0:}6:marked;0:;9:is_replay;0:~11:intercepted;5:false!11:server_conn;409:4:via2;0:~11:cipher_list;0:]11:cipher_name;0:~11:alpn_offers;0:]16:certificate_list;0:]3:tls;5:false!5:error;0:~5:state;1:0#3:via;0:~11:tls_version;0:~15:tls_established;5:false!19:timestamp_tls_setup;0:~19:timestamp_tcp_setup;0:~15:timestamp_start;0:~13:timestamp_end;0:~14:source_address;0:~3:sni;0:~10:ip_address;0:~2:id;36:5bcc6919-bec0-4201-b2a8-81d37aa04dc8;4:alpn;0:~7:address;21:11:example.com;3:443#]}11:client_conn;410:10:proxy_mode;7:regular;11:cipher_list;0:]11:alpn_offers;0:]16:certificate_list;0:]3:tls;5:false!5:error;0:~8:sockname;7:0:;1:0#]5:state;1:3#11:tls_version;0:~14:tls_extensions;0:]15:tls_established;5:false!19:timestamp_tls_setup;0:~15:timestamp_start;18:1689599084.0565512^13:timestamp_end;0:~3:sni;0:~8:mitmcert;0:~2:id;36:592af050-4844-4a8b-882c-b22fa2ddfc78;11:cipher_name;0:~4:alpn;0:~7:address;7:0:;1:0#]}5:error;0:~2:id;36:25e6bb05-b86a-49fb-b514-c5d515378744;}1394:4:type;4:http;7:version;2:18#9:websocket;0:~8:response;0:~7:request;289:4:path;1:/,9:authority;0:,6:scheme;5:https,6:method;3:GET,4:port;3:443#4:host;10:google.com;13:timestamp_end;17:1689604539.872232^15:timestamp_start;17:1689604539.872231^8:trailers;0:~7:content;0:,7:headers;51:22:14:content-length,1:0,]21:4:Host,10:google.com,]]12:http_version;8:HTTP/1.1,}17:timestamp_created;17:1689604539.873284^7:comment;0:;8:metadata;0:}6:marked;0:;9:is_replay;0:~11:intercepted;5:false!11:server_conn;408:4:via2;0:~11:cipher_list;0:]11:cipher_name;0:~11:alpn_offers;0:]16:certificate_list;0:]3:tls;5:false!5:error;0:~5:state;1:0#3:via;0:~11:tls_version;0:~15:tls_established;5:false!19:timestamp_tls_setup;0:~19:timestamp_tcp_setup;0:~15:timestamp_start;0:~13:timestamp_end;0:~14:source_address;0:~3:sni;0:~10:ip_address;0:~2:id;36:2390398b-e903-4cf2-aa39-37825231257d;4:alpn;0:~7:address;20:10:google.com;3:443#]}11:client_conn;409:10:proxy_mode;7:regular;11:cipher_list;0:]11:alpn_offers;0:]16:certificate_list;0:]3:tls;5:false!5:error;0:~8:sockname;7:0:;1:0#]5:state;1:3#11:tls_version;0:~14:tls_extensions;0:]15:tls_established;5:false!19:timestamp_tls_setup;0:~15:timestamp_start;17:1689604539.872131^13:timestamp_end;0:~3:sni;0:~8:mitmcert;0:~2:id;36:21ea5735-64d1-499d-9e93-a8edb451d807;11:cipher_name;0:~4:alpn;0:~7:address;7:0:;1:0#]}5:error;0:~2:id;36:6a0c192f-f4ef-481a-b35c-f0b78e323e28;}1396:4:type;4:http;7:version;2:18#9:websocket;0:~8:response;0:~7:request;290:4:path;1:/,9:authority;0:,6:scheme;5:https,6:method;4:POST,4:port;3:443#4:host;10:google.com;13:timestamp_end;17:1689604551.329621^15:timestamp_start;17:1689604551.329621^8:trailers;0:~7:content;0:,7:headers;51:22:14:content-length,1:0,]21:4:Host,10:google.com,]]12:http_version;8:HTTP/1.1,}17:timestamp_created;17:1689604551.330397^7:comment;0:;8:metadata;0:}6:marked;0:;9:is_replay;0:~11:intercepted;5:false!11:server_conn;408:4:via2;0:~11:cipher_list;0:]11:cipher_name;0:~11:alpn_offers;0:]16:certificate_list;0:]3:tls;5:false!5:error;0:~5:state;1:0#3:via;0:~11:tls_version;0:~15:tls_established;5:false!19:timestamp_tls_setup;0:~19:timestamp_tcp_setup;0:~15:timestamp_start;0:~13:timestamp_end;0:~14:source_address;0:~3:sni;0:~10:ip_address;0:~2:id;36:600f6313-534a-49bf-9bd5-b41562dbea78;4:alpn;0:~7:address;20:10:google.com;3:443#]}11:client_conn;410:10:proxy_mode;7:regular;11:cipher_list;0:]11:alpn_offers;0:]16:certificate_list;0:]3:tls;5:false!5:error;0:~8:sockname;7:0:;1:0#]5:state;1:3#11:tls_version;0:~14:tls_extensions;0:]15:tls_established;5:false!19:timestamp_tls_setup;0:~15:timestamp_start;18:1689604551.3295212^13:timestamp_end;0:~3:sni;0:~8:mitmcert;0:~2:id;36:77b8f50c-c5fe-45a6-9b9f-71b5b40256fe;11:cipher_name;0:~4:alpn;0:~7:address;7:0:;1:0#]}5:error;0:~2:id;36:ca39fbd9-7b44-4c13-859c-d2447c09d074;}1398:4:type;4:http;7:version;2:18#9:websocket;0:~8:response;0:~7:request;292:4:path;1:/,9:authority;0:,6:scheme;5:https,6:method;4:POST,4:port;3:443#4:host;10:google.com;13:timestamp_end;18:1689604562.9142652^15:timestamp_start;18:1689604562.9142652^8:trailers;0:~7:content;0:,7:headers;51:22:14:content-length,1:0,]21:4:Host,10:google.com,]]12:http_version;8:HTTP/1.1,}17:timestamp_created;17:1689604562.915139^7:comment;0:;8:metadata;0:}6:marked;0:;9:is_replay;0:~11:intercepted;5:false!11:server_conn;408:4:via2;0:~11:cipher_list;0:]11:cipher_name;0:~11:alpn_offers;0:]16:certificate_list;0:]3:tls;5:false!5:error;0:~5:state;1:0#3:via;0:~11:tls_version;0:~15:tls_established;5:false!19:timestamp_tls_setup;0:~19:timestamp_tcp_setup;0:~15:timestamp_start;0:~13:timestamp_end;0:~14:source_address;0:~3:sni;0:~10:ip_address;0:~2:id;36:ad48cb4c-ee61-4c22-bef5-c64dab14d678;4:alpn;0:~7:address;20:10:google.com;3:443#]}11:client_conn;410:10:proxy_mode;7:regular;11:cipher_list;0:]11:alpn_offers;0:]16:certificate_list;0:]3:tls;5:false!5:error;0:~8:sockname;7:0:;1:0#]5:state;1:3#11:tls_version;0:~14:tls_extensions;0:]15:tls_established;5:false!19:timestamp_tls_setup;0:~15:timestamp_start;18:1689604562.9141653^13:timestamp_end;0:~3:sni;0:~8:mitmcert;0:~2:id;36:e12f7a90-f8f5-4975-8292-638bb7b866ab;11:cipher_name;0:~4:alpn;0:~7:address;7:0:;1:0#]}5:error;0:~2:id;36:ab38d892-38fa-4b40-840e-466eb01629f1;} \ No newline at end of file diff --git a/test/mitmproxy/data/flows/successful_log.har b/test/mitmproxy/data/flows/successful_log.har new file mode 100644 index 0000000000..f3229fadef --- /dev/null +++ b/test/mitmproxy/data/flows/successful_log.har @@ -0,0 +1,201 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "mitmproxy", + "version": "1.2.3", + "comment": "" + }, + "pages": [], + "entries": [ + { + "startedDateTime": "2023-07-25T10:42:01.726810+00:00", + "time": 489.44091796875, + "request": { + "method": "GET", + "url": "https://example.com/", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "user-agent", + "value": "curl/7.86.0" + }, + { + "name": "accept", + "value": "*/*" + } + ], + "queryString": [], + "headersSize": 61, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "age", + "value": "504741" + }, + { + "name": "cache-control", + "value": "max-age=604800" + }, + { + "name": "content-type", + "value": "text/html; charset=UTF-8" + }, + { + "name": "date", + "value": "Tue, 25 Jul 2023 10:42:01 GMT" + }, + { + "name": "etag", + "value": "\"3147526947+gzip+ident\"" + }, + { + "name": "expires", + "value": "Tue, 01 Aug 2023 10:42:01 GMT" + }, + { + "name": "last-modified", + "value": "Thu, 17 Oct 2019 07:18:26 GMT" + }, + { + "name": "server", + "value": "ECS (bsa/EB24)" + }, + { + "name": "vary", + "value": "Accept-Encoding" + }, + { + "name": "x-cache", + "value": "HIT" + }, + { + "name": "content-length", + "value": "1256" + } + ], + "content": { + "size": 1256, + "compression": 0, + "mimeType": "text/html; charset=UTF-8", + "text": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
    \n

    Example Domain

    \n

    This domain is for use in illustrative examples in documents. You may use this\n domain in literature without prior coordination or asking for permission.

    \n

    More information...

    \n
    \n\n\n" + }, + "redirectURL": "", + "headersSize": 416, + "bodySize": 1256 + }, + "cache": {}, + "timings": { + "connect": 123.6569881439209, + "ssl": 246.7970848083496, + "send": 3.0448436737060547, + "receive": 3.8290023803710938, + "wait": 112.11299896240234 + }, + "serverIPAddress": "93.184.216.34" + }, + { + "startedDateTime": "2023-07-25T10:58:25.577252+00:00", + "time": 14989.961862564087, + "request": { + "method": "POST", + "url": "https://httpbin.org/post", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "user-agent", + "value": "curl/7.86.0" + }, + { + "name": "accept", + "value": "*/*" + }, + { + "name": "content-type", + "value": "application/x-www-form-urlencoded" + }, + { + "name": "content-length", + "value": "23" + } + ], + "queryString": [], + "headersSize": 146, + "bodySize": 23, + "postData": { + "mimeType": "application/x-www-form-urlencoded", + "text": "key1=value1&key2=value2", + "params": [ + { + "name": "key1", + "value": "value1" + }, + { + "name": "key2", + "value": "value2" + } + ] + } + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "date", + "value": "Tue, 25 Jul 2023 10:58:40 GMT" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "content-length", + "value": "453" + }, + { + "name": "server", + "value": "gunicorn/19.9.0" + }, + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "access-control-allow-credentials", + "value": "true" + } + ], + "content": { + "size": 453, + "compression": 0, + "mimeType": "application/json", + "text": "{\n \"args\": {}, \n \"data\": \"\", \n \"files\": {}, \n \"form\": {\n \"key1\": \"value1\", \n \"key2\": \"value2\"\n }, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Content-Length\": \"23\", \n \"Content-Type\": \"application/x-www-form-urlencoded\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\": \"curl/7.86.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-64bfaad1-5780a79a5bc60f5e7a38cafa\"\n }, \n \"json\": null, \n \"origin\": \"212.56.161.20\", \n \"url\": \"https://httpbin.org/post\"\n}\n" + }, + "redirectURL": "", + "headersSize": 242, + "bodySize": 453 + }, + "cache": {}, + "timings": { + "connect": 132.54094123840332, + "ssl": 279.72912788391113, + "send": 1.9299983978271484, + "receive": 3.267049789428711, + "wait": 14572.494745254517 + }, + "serverIPAddress": "54.211.216.104" + } + ] + } +} \ No newline at end of file diff --git a/test/mitmproxy/data/flows/successful_log.mitm b/test/mitmproxy/data/flows/successful_log.mitm new file mode 100644 index 0000000000..db5988d675 --- /dev/null +++ b/test/mitmproxy/data/flows/successful_log.mitm @@ -0,0 +1,249 @@ +7867:4:type;4:http;7:version;2:18#9:websocket;0:~8:response;1838:6:reason;0:,11:status_code;3:200#13:timestamp_end;17:1690281721.845797^15:timestamp_start;17:1690281721.841968^8:trailers;0:~7:content;1256: + + + Example Domain + + + + + + + + +
    +

    Example Domain

    +

    This domain is for use in illustrative examples in documents. You may use this + domain in literature without prior coordination or asking for permission.

    +

    More information...

    +
    + + +,7:headers;399:15:3:age,6:504741,]35:13:cache-control,14:max-age=604800,]44:12:content-type,24:text/html; charset=UTF-8,]40:4:date,29:Tue, 25 Jul 2023 10:42:01 GMT,]34:4:etag,23:"3147526947+gzip+ident",]43:7:expires,29:Tue, 01 Aug 2023 10:42:01 GMT,]50:13:last-modified,29:Thu, 17 Oct 2019 07:18:26 GMT,]27:6:server,14:ECS (bsa/EB24),]26:4:vary,15:Accept-Encoding,]16:7:x-cache,3:HIT,]25:14:content-length,4:1256,]]12:http_version;8:HTTP/2.0,}7:request;304:4:path;1:/,9:authority;11:example.com,6:scheme;5:https,6:method;3:GET,4:port;3:443#4:host;11:example.com;13:timestamp_end;17:1690281721.729855^15:timestamp_start;18:1690281721.7268102^8:trailers;0:~7:content;0:,7:headers;52:29:10:user-agent,11:curl/7.86.0,]15:6:accept,3:*/*,]]12:http_version;8:HTTP/2.0,}17:timestamp_created;17:1690281721.727008^7:comment;0:;8:metadata;0:}6:marked;0:;9:is_replay;0:~11:intercepted;5:false!11:server_conn;4901:4:via2;0:~11:cipher_list;0:]11:cipher_name;22:TLS_AES_256_GCM_SHA384;11:alpn_offers;16:2:h2,8:http/1.1,]16:certificate_list;4305:2589:-----BEGIN CERTIFICATE----- +MIIHSjCCBjKgAwIBAgIQDB/LGEUYx+OGZ0EjbWtz8TANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMSkwJwYDVQQDEyBE +aWdpQ2VydCBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTAeFw0yMzAxMTMwMDAwMDBa +Fw0yNDAyMTMyMzU5NTlaMIGWMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZv +cm5pYTEUMBIGA1UEBxMLTG9zIEFuZ2VsZXMxQjBABgNVBAoMOUludGVybmV0wqBD +b3Jwb3JhdGlvbsKgZm9ywqBBc3NpZ25lZMKgTmFtZXPCoGFuZMKgTnVtYmVyczEY +MBYGA1UEAxMPd3d3LmV4YW1wbGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAwoB3iVm4RW+6StkR+nutx1fQevu2+t0Fu6KBcbvhfyHSXy7w0nJO +dTT4jWLjStpRkNQBPZwMwHH35i+21gdnJtDe/xfO8IX9McFmyodlBUcqX8CruIzD +v9AXf2OjXPBG+4aq+03XKl5/muATl32++301Vw1dXoGYNeoWQqLTsHT3WS3tOOf+ +ehuzNuZ+rj+ephaD3lMBToEArrtC9R91KTTN6YSAOK48NxTA8CfOMFK5itxfIqB5 ++E9OSQTidXyqLyoeA+xxTKMqYfxvypEek1oueAhY9u67NCBdmuavxtfyvwp7+o6S +d+NsewxAhmRKFexw13KOYzDhC+9aMJcuJQIDAQABo4ID2DCCA9QwHwYDVR0jBBgw +FoAUt2ui6qiqhIx56rTaD5iyxZV2ufQwHQYDVR0OBBYEFLCTP+gXgv1ssrYXh8vj +gP6CmwGeMIGBBgNVHREEejB4gg93d3cuZXhhbXBsZS5vcmeCC2V4YW1wbGUubmV0 +ggtleGFtcGxlLmVkdYILZXhhbXBsZS5jb22CC2V4YW1wbGUub3Jngg93d3cuZXhh +bXBsZS5jb22CD3d3dy5leGFtcGxlLmVkdYIPd3d3LmV4YW1wbGUubmV0MA4GA1Ud +DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwgY8GA1Ud +HwSBhzCBhDBAoD6gPIY6aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0 +VExTUlNBU0hBMjU2MjAyMENBMS00LmNybDBAoD6gPIY6aHR0cDovL2NybDQuZGln +aWNlcnQuY29tL0RpZ2lDZXJ0VExTUlNBU0hBMjU2MjAyMENBMS00LmNybDA+BgNV +HSAENzA1MDMGBmeBDAECAjApMCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3LmRpZ2lj +ZXJ0LmNvbS9DUFMwfwYIKwYBBQUHAQEEczBxMCQGCCsGAQUFBzABhhhodHRwOi8v +b2NzcC5kaWdpY2VydC5jb20wSQYIKwYBBQUHMAKGPWh0dHA6Ly9jYWNlcnRzLmRp +Z2ljZXJ0LmNvbS9EaWdpQ2VydFRMU1JTQVNIQTI1NjIwMjBDQTEtMS5jcnQwCQYD +VR0TBAIwADCCAX8GCisGAQQB1nkCBAIEggFvBIIBawFpAHYA7s3QZNXbGs7FXLed +tM0TojKHRny87N7DUUhZRnEftZsAAAGFq0gFIwAABAMARzBFAiEAqt+fK6jFdGA6 +tv0EWt9rax0WYBV4re9jgZgq0zi42QUCIEBh1yKpPvgX1BreE0wBUmriOVUhJS77 +KgF193fT2877AHcAc9meiRtMlnigIH1HneayxhzQUV5xGSqMa4AQesF3crUAAAGF +q0gFnwAABAMASDBGAiEA12SUFK5rgLqRzvgcr7ZzV4nl+Zt9lloAzRLfPc7vSPAC +IQCXPbwScx1rE+BjFawZlVjLj/1PsM0KQQcsfHDZJUTLwAB2AEiw42vapkc0D+Vq +AvqdMOscUgHLVt0sgdm7v6s52IRzAAABhatIBV4AAAQDAEcwRQIhAN5bhHthoyWM +J3CQB/1iYFEhMgUVkFhHDM/nlE9ThCwhAiAPvPJXyp7a2kzwJX3P7fqH5Xko3rPh +CzRoXYd6W+QkCjANBgkqhkiG9w0BAQsFAAOCAQEAWeRK2KmCuppK8WMMbXYmdbM8 +dL7F9z2nkZL4zwYtWBDt87jW/Gz/E5YyzU/phySFC3SiwvYP9afYfXaKrunJWCtu +AG+5zSTuxELFTBaFnTRhOSO/xo6VyYSpsuVBD0R415W5z9l0v1hP5xb/fEAwxGxO +Ik3Lg2c6k78rxcWcGvJDoSU7hPb3U26oha7eFHSRMAYN8gfUxAi6Q2TF4j/arMVB +r6Q36EJ2dPcTu0p9NlmBm8dE34lzuTNC6GDCTWFdEloQ9u//M4kUUOjWn8a5XCs1 +263t3Ta2JfKViqxpP5r+GvgVKG3qGFrC0mIYr0B4tfpeCY9T+cz4I6GDMSP0xg== +-----END CERTIFICATE----- +,1704:-----BEGIN CERTIFICATE----- +MIIEvjCCA6agAwIBAgIQBtjZBNVYQ0b2ii+nVCJ+xDANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0yMTA0MTQwMDAwMDBaFw0zMTA0MTMyMzU5NTlaME8xCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxKTAnBgNVBAMTIERpZ2lDZXJ0IFRMUyBS +U0EgU0hBMjU2IDIwMjAgQ0ExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAwUuzZUdwvN1PWNvsnO3DZuUfMRNUrUpmRh8sCuxkB+Uu3Ny5CiDt3+PE0J6a +qXodgojlEVbbHp9YwlHnLDQNLtKS4VbL8Xlfs7uHyiUDe5pSQWYQYE9XE0nw6Ddn +g9/n00tnTCJRpt8OmRDtV1F0JuJ9x8piLhMbfyOIJVNvwTRYAIuE//i+p1hJInuW +raKImxW8oHzf6VGo1bDtN+I2tIJLYrVJmuzHZ9bjPvXj1hJeRPG/cUJ9WIQDgLGB +Afr5yjK7tI4nhyfFK3TUqNaX3sNk+crOU6JWvHgXjkkDKa77SU+kFbnO8lwZV21r +eacroicgE7XQPUDTITAHk+qZ9QIDAQABo4IBgjCCAX4wEgYDVR0TAQH/BAgwBgEB +/wIBADAdBgNVHQ4EFgQUt2ui6qiqhIx56rTaD5iyxZV2ufQwHwYDVR0jBBgwFoAU +A95QNVbRTLtm8KPiGxvDl7I90VUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQG +CCsGAQUFBwMBBggrBgEFBQcDAjB2BggrBgEFBQcBAQRqMGgwJAYIKwYBBQUHMAGG +GGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBABggrBgEFBQcwAoY0aHR0cDovL2Nh +Y2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNydDBCBgNV +HR8EOzA5MDegNaAzhjFodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRH +bG9iYWxSb290Q0EuY3JsMD0GA1UdIAQ2MDQwCwYJYIZIAYb9bAIBMAcGBWeBDAEB +MAgGBmeBDAECATAIBgZngQwBAgIwCAYGZ4EMAQIDMA0GCSqGSIb3DQEBCwUAA4IB +AQCAMs5eC91uWg0Kr+HWhMvAjvqFcO3aXbMM9yt1QP6FCvrzMXi3cEsaiVi6gL3z +ax3pfs8LulicWdSQ0/1s/dCYbbdxglvPbQtaCdB73sRD2Cqk3p5BJl+7j5nL3a7h +qG+fh/50tx8bIKuxT8b1Z11dmzzp/2n3YWzW2fP9NsarA4h20ksudYbj/NhVfSbC +EXffPgK2fPOre3qGNm+499iTcc+G33Mw+nur7SpZyEKEOxEXGlLzyQ4UfaJbcme6 +ce1XR2bFuAJKZTRei9AqPCCcUZlM51Ke92sRKw2Sfh3oius2FkOH6ipjv3U/697E +A7sKPPcw7+uvTPyLNhBzPvOk +-----END CERTIFICATE----- +,]3:tls;4:true!5:error;0:~5:state;1:0#3:via;0:~11:tls_version;7:TLSv1.3;15:tls_established;4:true!19:timestamp_tls_setup;18:1690281721.6761842^19:timestamp_tcp_setup;17:1690281721.429387^15:timestamp_start;16:1690281721.30573^13:timestamp_end;18:1690281721.8521862^14:source_address;27:15:192.168.178.132;5:51408#]3:sni;11:example.com;10:ip_address;23:13:93.184.216.34;3:443#]2:id;36:801c4fe1-4d07-4c4e-91e2-dc4f8a928fa7;4:alpn;2:h2,7:address;21:11:example.com;3:443#]}11:client_conn;532:10:proxy_mode;7:regular;11:cipher_list;0:]11:alpn_offers;16:2:h2,8:http/1.1,]16:certificate_list;0:]3:tls;4:true!5:error;0:~8:sockname;19:9:127.0.0.1;4:8080#]5:state;1:0#11:tls_version;7:TLSv1.3;14:tls_extensions;0:]15:tls_established;4:true!19:timestamp_tls_setup;17:1690281721.724515^15:timestamp_start;18:1690281721.3031292^13:timestamp_end;17:1690281721.850834^3:sni;11:example.com;8:mitmcert;0:~2:id;36:212d5061-f748-41cd-b54d-5502561bb30c;11:cipher_name;22:TLS_AES_256_GCM_SHA384;4:alpn;2:h2,7:address;20:9:127.0.0.1;5:51407#]}5:error;0:~2:id;36:8b751868-ff89-4844-b09f-0380bd9c8db6;}9635:4:type;4:http;7:version;2:18#9:websocket;0:~8:response;865:6:reason;0:,11:status_code;3:200#13:timestamp_end;17:1690282720.154944^15:timestamp_start;17:1690282720.151677^8:trailers;0:~7:content;453:{ + "args": {}, + "data": "", + "files": {}, + "form": { + "key1": "value1", + "key2": "value2" + }, + "headers": { + "Accept": "*/*", + "Content-Length": "23", + "Content-Type": "application/x-www-form-urlencoded", + "Host": "httpbin.org", + "User-Agent": "curl/7.86.0", + "X-Amzn-Trace-Id": "Root=1-64bfaad1-5780a79a5bc60f5e7a38cafa" + }, + "json": null, + "origin": "212.56.161.20", + "url": "https://httpbin.org/post" +} +,7:headers;230:40:4:date,29:Tue, 25 Jul 2023 10:58:40 GMT,]36:12:content-type,16:application/json,]24:14:content-length,3:453,]28:6:server,15:gunicorn/19.9.0,]35:27:access-control-allow-origin,1:*,]43:32:access-control-allow-credentials,4:true,]]12:http_version;8:HTTP/2.0,}7:request;419:4:path;5:/post,9:authority;11:httpbin.org,6:scheme;5:https,6:method;4:POST,4:port;3:443#4:host;11:httpbin.org;13:timestamp_end;18:1690282705.5791821^15:timestamp_start;18:1690282705.5772521^8:trailers;0:~7:content;23:key1=value1&key2=value2,7:headers;136:29:10:user-agent,11:curl/7.86.0,]15:6:accept,3:*/*,]53:12:content-type,33:application/x-www-form-urlencoded,]23:14:content-length,2:23,]]12:http_version;8:HTTP/2.0,}17:timestamp_created;17:1690282705.577479^7:comment;0:;8:metadata;0:}6:marked;0:;9:is_replay;0:~11:intercepted;5:false!11:server_conn;7530:4:via2;0:~11:cipher_list;0:]11:cipher_name;27:ECDHE-RSA-AES128-GCM-SHA256;11:alpn_offers;16:2:h2,8:http/1.1,]16:certificate_list;6929:2078:-----BEGIN CERTIFICATE----- +MIIF0TCCBLmgAwIBAgIQBMaXROWeY5Qs9ibwxtesVDANBgkqhkiG9w0BAQsFADA8 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRwwGgYDVQQDExNBbWF6b24g +UlNBIDIwNDggTTAyMB4XDTIzMDMwMTAwMDAwMFoXDTIzMTExOTIzNTk1OVowFjEU +MBIGA1UEAxMLaHR0cGJpbi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCPE28yK7/fA5KcuE2U5qT4TwU2GUsXvss+y3EojNC0rQPwAVVp4+ID33r9 +Wr8LusvHgyqmPu7hNA17UUCvUVWrlYtzSSkxPqDpaRtF68laf9hPtpzxsAEcJ3Zj +QLg81JYVvgodPuKsAQ/j2s0b9Yd6O//g2NI2jl5Pu94Kveo5uedSbCGdGNgm0a04 +N9egCih4CumstTUjApVv566tNUILUbIQU6Zik2dn3AR/W6OEgk7818QCfYa1YlVV +y4Z3wZ+UucKd0c73Fy3kW3MhJcQ8YwuXpoH9D338UBDIeSy7Yd5J9nOZXaq9A9eR +0GiOh3DcDL71dPEkX80qBouCpHEhAgMBAAGjggLzMIIC7zAfBgNVHSMEGDAWgBTA +MVLNWlDDgnx0cc7L6Zz5euuC4jAdBgNVHQ4EFgQU8dXJczk/NQ+psnYfrKl/NN1E +2d4wJQYDVR0RBB4wHIILaHR0cGJpbi5vcmeCDSouaHR0cGJpbi5vcmcwDgYDVR0P +AQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjA7BgNVHR8E +NDAyMDCgLqAshipodHRwOi8vY3JsLnIybTAyLmFtYXpvbnRydXN0LmNvbS9yMm0w +Mi5jcmwwEwYDVR0gBAwwCjAIBgZngQwBAgEwdQYIKwYBBQUHAQEEaTBnMC0GCCsG +AQUFBzABhiFodHRwOi8vb2NzcC5yMm0wMi5hbWF6b250cnVzdC5jb20wNgYIKwYB +BQUHMAKGKmh0dHA6Ly9jcnQucjJtMDIuYW1hem9udHJ1c3QuY29tL3IybTAyLmNl +cjAMBgNVHRMBAf8EAjAAMIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdgCt9776 +fP8QyIudPZwePhhqtGcpXc+xDCTKhYY069yCigAAAYadUPKfAAAEAwBHMEUCIBtT +nUnstwdXAMX0ZV2qinUM7CBGmLsJGslKNZbDNQjiAiEA9LLXQMqBJEoqdg5UJcSi +c3LibKO877zTkemG3QlH9dYAdgCzc3cH4YRQ+GOG1gWp3BEJSnktsWcMC4fc8AMO +eTalmgAAAYadUPLWAAAEAwBHMEUCIChRxIknXNkZN7cIUKLcLErdkkKLzFBUV6d3 +85QOXQ2gAiEApG5R/+k6XGd5QrNDa9I6IgqzTxCbCs7Xqkl8MAb73H0AdgC3Pvsk +35xNunXyOcW6WPRsXfxCz3qfNcSeHQmBJe20mQAAAYadUPKCAAAEAwBHMEUCIQCq +Sut242xHZ/P2c/8n/0EiZ/CwtgmCXfz7NdB75dYtlAIgIE4TUU2JAyIRJlCKfatQ +aAOkpEP18yLmw9GIq3nnVAAwDQYJKoZIhvcNAQELBQADggEBACjTDO0NpDuWaZnw +6nHRFcYC+kWJ9dVD7y2LaZTaMQbrB24EDudhSJuZDOvFzkz5cdSc0KOjYPorMXQ3 +z31mBqFDNE1nVKAVhGT6Z2hgmBTCWn3cJG2E6lSsKVZLC3wW02BlU/eClE4cuxS/ +vtAbE8zJosU0V/+YJWNZe649AvF0cDSRsd37arNs+iJuHdCYKpd6tVgr8qSfjiYU +5XahqdcF3R328aVe5/vpBmFtyNNI4uCsBihrJIeXLOgFkt1xo+vrQVuAx5BDjgLG +2Jbx6D7eeSQmnhwZvkBXYuZhndyqb4yn5g7q/5u2dVUuEFyX6gUAJG1cdmJxOCJw +atJSKtI= +-----END CERTIFICATE----- +,1574:-----BEGIN CERTIFICATE----- +MIIEXjCCA0agAwIBAgITB3MSSkvL1E7HtTvq8ZSELToPoTANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTIyMDgyMzIyMjUzMFoXDTMwMDgyMzIyMjUzMFowPDEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEcMBoGA1UEAxMTQW1hem9uIFJT +QSAyMDQ4IE0wMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALtDGMZa +qHneKei1by6+pUPPLljTB143Si6VpEWPc6mSkFhZb/6qrkZyoHlQLbDYnI2D7hD0 +sdzEqfnuAjIsuXQLG3A8TvX6V3oFNBFVe8NlLJHvBseKY88saLwufxkZVwk74g4n +WlNMXzla9Y5F3wwRHwMVH443xGz6UtGSZSqQ94eFx5X7Tlqt8whi8qCaKdZ5rNak ++r9nUThOeClqFd4oXych//Rc7Y0eX1KNWHYSI1Nk31mYgiK3JvH063g+K9tHA63Z +eTgKgndlh+WI+zv7i44HepRZjA1FYwYZ9Vv/9UkC5Yz8/yU65fgjaE+wVHM4e/Yy +C2osrPWE7gJ+dXMCAwEAAaOCAVowggFWMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYD +VR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNV +HQ4EFgQUwDFSzVpQw4J8dHHOy+mc+XrrguIwHwYDVR0jBBgwFoAUhBjMhTTsvAyU +lC4IWZzHshBOCggwewYIKwYBBQUHAQEEbzBtMC8GCCsGAQUFBzABhiNodHRwOi8v +b2NzcC5yb290Y2ExLmFtYXpvbnRydXN0LmNvbTA6BggrBgEFBQcwAoYuaHR0cDov +L2NydC5yb290Y2ExLmFtYXpvbnRydXN0LmNvbS9yb290Y2ExLmNlcjA/BgNVHR8E +ODA2MDSgMqAwhi5odHRwOi8vY3JsLnJvb3RjYTEuYW1hem9udHJ1c3QuY29tL3Jv +b3RjYTEuY3JsMBMGA1UdIAQMMAowCAYGZ4EMAQIBMA0GCSqGSIb3DQEBCwUAA4IB +AQAtTi6Fs0Azfi+iwm7jrz+CSxHH+uHl7Law3MQSXVtR8RV53PtR6r/6gNpqlzdo +Zq4FKbADi1v9Bun8RY8D51uedRfjsbeodizeBB8nXmeyD33Ep7VATj4ozcd31YFV +fgRhvTSxNrrTlNpWkUk0m3BMPv8sg381HhA6uEYokE5q9uws/3YkKqRiEz3TsaWm +JqIRZhMbgAfp7O7FUwFIb7UIspogZSKxPIWJpxiPo3TcBambbVtQOcNRWz5qCQdD +slI2yayq0n2TXoHyNCLEH8rpsJRVILFsg0jc7BaFrMnF462+ajSehgj12IidNeRN +4zl+EoNaWdpnWndvSpAEkq2P +-----END CERTIFICATE----- +,1647:-----BEGIN CERTIFICATE----- +MIIEkjCCA3qgAwIBAgITBn+USionzfP6wq4rAfkI7rnExjANBgkqhkiG9w0BAQsF +ADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNj +b3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4x +OzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1 +dGhvcml0eSAtIEcyMB4XDTE1MDUyNTEyMDAwMFoXDTM3MTIzMTAxMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaOCATEwggEtMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBSEGMyFNOy8DJSULghZnMeyEE4KCDAfBgNVHSMEGDAW +gBScXwDfqgHXMCs4iKK4bUqc8hGRgzB4BggrBgEFBQcBAQRsMGowLgYIKwYBBQUH +MAGGImh0dHA6Ly9vY3NwLnJvb3RnMi5hbWF6b250cnVzdC5jb20wOAYIKwYBBQUH +MAKGLGh0dHA6Ly9jcnQucm9vdGcyLmFtYXpvbnRydXN0LmNvbS9yb290ZzIuY2Vy +MD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly9jcmwucm9vdGcyLmFtYXpvbnRydXN0 +LmNvbS9yb290ZzIuY3JsMBEGA1UdIAQKMAgwBgYEVR0gADANBgkqhkiG9w0BAQsF +AAOCAQEAYjdCXLwQtT6LLOkMm2xF4gcAevnFWAu5CIw+7bMlPLVvUOTNNWqnkzSW +MiGpSESrnO09tKpzbeR/FoCJbM8oAxiDR3mjEH4wW6w7sGDgd9QIpuEdfF7Au/ma +eyKdpwAJfqxGF4PcnCZXmTA5YpaP7dreqsXMGz7KQ2hsVxa81Q4gLv7/wmpdLqBK +bRRYh5TmOTFffHPLkIhqhBGWJ6bt2YFGpn6jcgAKUj6DiAdjd4lpFw85hdKrCEVN +0FE6/V1dN2RMfjCyVSRCnTawXZwXgWHxyvkQAiSr6w10kY17RSlQOYiypok1JR4U +akcjMS9cmvqtmg5iUaQqqcT5NJ0hGA== +-----END CERTIFICATE----- +,1606:-----BEGIN CERTIFICATE----- +MIIEdTCCA12gAwIBAgIJAKcOSkw0grd/MA0GCSqGSIb3DQEBCwUAMGgxCzAJBgNV +BAYTAlVTMSUwIwYDVQQKExxTdGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTIw +MAYDVQQLEylTdGFyZmllbGQgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eTAeFw0wOTA5MDIwMDAwMDBaFw0zNDA2MjgxNzM5MTZaMIGYMQswCQYDVQQGEwJV +UzEQMA4GA1UECBMHQXJpem9uYTETMBEGA1UEBxMKU2NvdHRzZGFsZTElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjE7MDkGA1UEAxMyU3RhcmZp +ZWxkIFNlcnZpY2VzIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVDDrEKvlO4vW+GZdfjohTsR8/ +y8+fIBNtKTrID30892t2OGPZNmCom15cAICyL1l/9of5JUOG52kbUpqQ4XHj2C0N +Tm/2yEnZtvMaVq4rtnQU68/7JuMauh2WLmo7WJSJR1b/JaCTcFOD2oR0FMNnngRo +Ot+OQFodSk7PQ5E751bWAHDLUu57fa4657wx+UX2wmDPE1kCK4DMNEffud6QZW0C +zyyRpqbn3oUYSXxmTqM6bam17jQuug0DuDPfR+uxa40l2ZvOgdFFRjKWcIfeAg5J +Q4W2bHO7ZOphQazJ1FTfhy/HIrImzJ9ZVGif/L4qL8RVHHVAYBeFAlU5i38FAgMB +AAGjgfAwge0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0O +BBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMB8GA1UdIwQYMBaAFL9ft9HO3R+G9FtV +rNzXEMIOqYjnME8GCCsGAQUFBwEBBEMwQTAcBggrBgEFBQcwAYYQaHR0cDovL28u +c3MyLnVzLzAhBggrBgEFBQcwAoYVaHR0cDovL3guc3MyLnVzL3guY2VyMCYGA1Ud +HwQfMB0wG6AZoBeGFWh0dHA6Ly9zLnNzMi51cy9yLmNybDARBgNVHSAECjAIMAYG +BFUdIAAwDQYJKoZIhvcNAQELBQADggEBACMd44pXyn3pF3lM8R5V/cxTbj5HD9/G +VfKyBDbtgB9TxF00KGu+x1X8Z+rLP3+QsjPNG1gQggL4+C/1E2DUBc7xgQjB3ad1 +l08YuW3e95ORCLp+QCztweq7dp4zBncdDQh/U90bZKuCJ/Fp1U1ervShw3WnWEQt +8jxwmKy6abaVd38PMV4s/KCHOkdp8Hlf9BRUpJVeEXgSYCfOn8J3/yNTd126/+pZ +59vPr5KW7ySaNRB6nJHGDn2Z9j8Z3/VyVOEVqQdZe4O/Ui5GjLIAZHYcSNPYeehu +VsyuLAOQ1xk4meTKCRlb/weWsKh/NEnfVqn3sF/tM+2MR7cwA130A4w= +-----END CERTIFICATE----- +,]3:tls;4:true!5:error;0:~5:state;1:0#3:via;0:~11:tls_version;7:TLSv1.2;15:tls_established;4:true!19:timestamp_tls_setup;17:1690282705.526426^19:timestamp_tcp_setup;17:1690282705.246697^15:timestamp_start;17:1690282705.114156^13:timestamp_end;17:1690282720.165767^14:source_address;27:15:192.168.178.132;5:51728#]3:sni;11:httpbin.org;10:ip_address;24:14:54.211.216.104;3:443#]2:id;36:54e89ac2-f466-4652-a693-939adb20f804;4:alpn;2:h2,7:address;21:11:httpbin.org;3:443#]}11:client_conn;530:10:proxy_mode;7:regular;11:cipher_list;0:]11:alpn_offers;16:2:h2,8:http/1.1,]16:certificate_list;0:]3:tls;4:true!5:error;0:~8:sockname;19:9:127.0.0.1;4:8080#]5:state;1:0#11:tls_version;7:TLSv1.3;14:tls_extensions;0:]15:tls_established;4:true!19:timestamp_tls_setup;17:1690282705.574528^15:timestamp_start;16:1690282705.11064^13:timestamp_end;17:1690282720.164182^3:sni;11:httpbin.org;8:mitmcert;0:~2:id;36:12eb1fb0-4231-47ba-b282-a5548b0c9d05;11:cipher_name;22:TLS_AES_256_GCM_SHA384;4:alpn;2:h2,7:address;20:9:127.0.0.1;5:51727#]}5:error;0:~2:id;36:d9c85948-fb5b-49ff-bb81-5f454be2e6b7;} \ No newline at end of file diff --git a/test/mitmproxy/data/flows/websocket.har b/test/mitmproxy/data/flows/websocket.har new file mode 100644 index 0000000000..c03b34cdec --- /dev/null +++ b/test/mitmproxy/data/flows/websocket.har @@ -0,0 +1,460 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "mitmproxy", + "version": "1.2.3", + "comment": "" + }, + "pages": [], + "entries": [ + { + "startedDateTime": "2023-08-29T13:03:20.195591+00:00", + "time": 424.1049289703369, + "request": { + "method": "GET", + "url": "https://demo.piesocket.com/v3/channel_123?api_key=VCXCEuvhGcBDP7XhiJJUDvR1e1D3eiVjgZ9VRiaV¬ify_self", + "httpVersion": "HTTP/1.1", + "cookies": [ + { + "name": "_ga", + "value": "GA1.1.636013294.1693303610" + }, + { + "name": "_ga_MP49LGP298", + "value": "GS1.1.1693313101.3.1.1693314156.0.0.0" + } + ], + "headers": [ + { + "name": "Host", + "value": "demo.piesocket.com" + }, + { + "name": "Connection", + "value": "Upgrade" + }, + { + "name": "Pragma", + "value": "no-cache" + }, + { + "name": "Cache-Control", + "value": "no-cache" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "Upgrade", + "value": "websocket" + }, + { + "name": "Origin", + "value": "https://www.piesocket.com" + }, + { + "name": "Sec-WebSocket-Version", + "value": "13" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.9" + }, + { + "name": "Cookie", + "value": "_ga=GA1.1.636013294.1693303610; _ga_MP49LGP298=GS1.1.1693313101.3.1.1693314156.0.0.0" + }, + { + "name": "dnt", + "value": "1" + }, + { + "name": "Sec-WebSocket-Key", + "value": "aOpK58kffMOGmH4do+rCyw==" + }, + { + "name": "Sec-WebSocket-Extensions", + "value": "permessage-deflate; client_max_window_bits" + } + ], + "queryString": [ + { + "name": "api_key", + "value": "VCXCEuvhGcBDP7XhiJJUDvR1e1D3eiVjgZ9VRiaV" + }, + { + "name": "notify_self", + "value": "" + } + ], + "headersSize": 708, + "bodySize": 0 + }, + "response": { + "status": 101, + "statusText": "Switching Protocols", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Server", + "value": "nginx/1.22.0 (Ubuntu)" + }, + { + "name": "Date", + "value": "Tue, 29 Aug 2023 13:03:20 GMT" + }, + { + "name": "Connection", + "value": "upgrade" + }, + { + "name": "Upgrade", + "value": "websocket" + }, + { + "name": "Sec-WebSocket-Accept", + "value": "PKJZ27lf+8qSfJkNbj0Dn/cgI00=" + }, + { + "name": "X-Powered-By", + "value": "Ratchet/0.4.4" + } + ], + "content": { + "size": 0, + "compression": 0, + "mimeType": "", + "text": "" + }, + "redirectURL": "", + "headersSize": 245, + "bodySize": 0 + }, + "cache": {}, + "timings": { + "connect": 165.79508781433105, + "ssl": 126.24692916870117, + "send": 3.5181045532226562, + "receive": 5.8460235595703125, + "wait": 122.69878387451172 + }, + "serverIPAddress": "143.244.202.177", + "_resourceType": "websocket", + "_webSocketMessages": [ + { + "type": "receive", + "time": 1693314200.336985, + "opcode": 1, + "data": "{\"error\":\"Unknown api key\"}" + } + ] + }, + { + "startedDateTime": "2023-08-29T13:03:30.145184+00:00", + "time": 351.45115852355957, + "request": { + "method": "GET", + "url": "https://demo.piesocket.com/v3/channel_123", + "httpVersion": "HTTP/1.1", + "cookies": [ + { + "name": "_ga", + "value": "GA1.1.636013294.1693303610" + }, + { + "name": "_ga_MP49LGP298", + "value": "GS1.1.1693313101.3.1.1693314209.0.0.0" + } + ], + "headers": [ + { + "name": "Host", + "value": "demo.piesocket.com" + }, + { + "name": "Connection", + "value": "Upgrade" + }, + { + "name": "Pragma", + "value": "no-cache" + }, + { + "name": "Cache-Control", + "value": "no-cache" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "Upgrade", + "value": "websocket" + }, + { + "name": "Origin", + "value": "https://www.piesocket.com" + }, + { + "name": "Sec-WebSocket-Version", + "value": "13" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.9" + }, + { + "name": "Cookie", + "value": "_ga=GA1.1.636013294.1693303610; _ga_MP49LGP298=GS1.1.1693313101.3.1.1693314209.0.0.0" + }, + { + "name": "dnt", + "value": "1" + }, + { + "name": "Sec-WebSocket-Key", + "value": "0jHmrzSl0PCJeqxcc8C0Jg==" + }, + { + "name": "Sec-WebSocket-Extensions", + "value": "permessage-deflate; client_max_window_bits" + } + ], + "queryString": [], + "headersSize": 708, + "bodySize": 0 + }, + "response": { + "status": 101, + "statusText": "Switching Protocols", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Server", + "value": "nginx/1.22.0 (Ubuntu)" + }, + { + "name": "Date", + "value": "Tue, 29 Aug 2023 13:03:30 GMT" + }, + { + "name": "Connection", + "value": "upgrade" + }, + { + "name": "Upgrade", + "value": "websocket" + }, + { + "name": "Sec-WebSocket-Accept", + "value": "Wwhjg4OlFHvFrMi/px8nDtVomBs=" + }, + { + "name": "X-Powered-By", + "value": "Ratchet/0.4.4" + } + ], + "content": { + "size": 0, + "compression": 0, + "mimeType": "", + "text": "" + }, + "redirectURL": "", + "headersSize": 245, + "bodySize": 0 + }, + "cache": {}, + "timings": { + "connect": 108.87980461120605, + "ssl": 123.34632873535156, + "send": 2.778768539428711, + "receive": 2.521991729736328, + "wait": 113.92426490783691 + }, + "serverIPAddress": "143.244.202.177", + "_resourceType": "websocket", + "_webSocketMessages": [ + { + "type": "receive", + "time": 1693314210.2705212, + "opcode": 1, + "data": "{\"error\":\"Missing apiKey\"}" + } + ] + }, + { + "startedDateTime": "2023-08-29T13:03:52.266590+00:00", + "time": 508.44621658325195, + "request": { + "method": "GET", + "url": "https://demo.piesocket.com/v3/channel_123?api_key=VCXCEuvhGcBDP7XhiJJUDvR1e1D3eiVjgZ9VRiaV¬ify_self", + "httpVersion": "HTTP/1.1", + "cookies": [ + { + "name": "_ga", + "value": "GA1.1.636013294.1693303610" + }, + { + "name": "_ga_MP49LGP298", + "value": "GS1.1.1693313101.3.1.1693314221.0.0.0" + } + ], + "headers": [ + { + "name": "Host", + "value": "demo.piesocket.com" + }, + { + "name": "Connection", + "value": "Upgrade" + }, + { + "name": "Pragma", + "value": "no-cache" + }, + { + "name": "Cache-Control", + "value": "no-cache" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "Upgrade", + "value": "websocket" + }, + { + "name": "Origin", + "value": "https://www.piesocket.com" + }, + { + "name": "Sec-WebSocket-Version", + "value": "13" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.9" + }, + { + "name": "Cookie", + "value": "_ga=GA1.1.636013294.1693303610; _ga_MP49LGP298=GS1.1.1693313101.3.1.1693314221.0.0.0" + }, + { + "name": "dnt", + "value": "1" + }, + { + "name": "Sec-WebSocket-Key", + "value": "jYGzUBibPBT6k8EzhAjMwg==" + }, + { + "name": "Sec-WebSocket-Extensions", + "value": "permessage-deflate; client_max_window_bits" + } + ], + "queryString": [ + { + "name": "api_key", + "value": "VCXCEuvhGcBDP7XhiJJUDvR1e1D3eiVjgZ9VRiaV" + }, + { + "name": "notify_self", + "value": "" + } + ], + "headersSize": 708, + "bodySize": 0 + }, + "response": { + "status": 101, + "statusText": "Switching Protocols", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Server", + "value": "nginx/1.22.0 (Ubuntu)" + }, + { + "name": "Date", + "value": "Tue, 29 Aug 2023 13:03:52 GMT" + }, + { + "name": "Connection", + "value": "upgrade" + }, + { + "name": "Upgrade", + "value": "websocket" + }, + { + "name": "Sec-WebSocket-Accept", + "value": "XCjji+8Ziem+CRE5RLW6lJe6T6E=" + }, + { + "name": "X-Powered-By", + "value": "Ratchet/0.4.4" + } + ], + "content": { + "size": 0, + "compression": 0, + "mimeType": "", + "text": "" + }, + "redirectURL": "", + "headersSize": 245, + "bodySize": 0 + }, + "cache": {}, + "timings": { + "connect": 195.7991123199463, + "ssl": 143.0189609527588, + "send": 3.0930042266845703, + "receive": 4.317045211791992, + "wait": 162.2180938720703 + }, + "serverIPAddress": "143.244.202.177", + "_resourceType": "websocket", + "_webSocketMessages": [ + { + "type": "receive", + "time": 1693314232.444319, + "opcode": 1, + "data": "{\"error\":\"Unknown api key\"}" + }, + { + "type": "send", + "time": 1693314240.7948081, + "opcode": 1, + "data": "foo" + }, + { + "type": "send", + "time": 1693314244.039395, + "opcode": 1, + "data": "bar" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/test/mitmproxy/data/flows/websocket.mitm b/test/mitmproxy/data/flows/websocket.mitm new file mode 100644 index 0000000000..3d8d205a72 --- /dev/null +++ b/test/mitmproxy/data/flows/websocket.mitm @@ -0,0 +1,259 @@ +8290:9:websocket;201:13:timestamp_end;14:1693314203.128^12:close_reason;0:;10:close_code;4:1005#16:closed_by_client;4:true!8:messages;84:80:1:1#5:false!27:{"error":"Unknown api key"},17:1693314200.336985^5:false!5:false!]]}8:response;434:6:reason;19:Switching Protocols,11:status_code;3:101#13:timestamp_end;17:1693314200.327654^15:timestamp_start;18:1693314200.3218079^8:trailers;0:~7:content;0:,7:headers;233:34:6:Server,21:nginx/1.22.0 (Ubuntu),]40:4:Date,29:Tue, 29 Aug 2023 13:03:20 GMT,]24:10:Connection,7:upgrade,]22:7:Upgrade,9:websocket,]56:20:Sec-WebSocket-Accept,28:PKJZ27lf+8qSfJkNbj0Dn/cgI00=,]33:12:X-Powered-By,13:Ratchet/0.4.4,]]12:http_version;8:HTTP/1.1,}7:request;1014:4:path;76:/v3/channel_123?api_key=VCXCEuvhGcBDP7XhiJJUDvR1e1D3eiVjgZ9VRiaV¬ify_self,9:authority;0:,6:scheme;5:https,6:method;3:GET,4:port;3:443#4:host;18:demo.piesocket.com;13:timestamp_end;17:1693314200.199109^15:timestamp_start;17:1693314200.195591^8:trailers;0:~7:content;0:,7:headers;691:29:4:Host,18:demo.piesocket.com,]24:10:Connection,7:Upgrade,]20:6:Pragma,8:no-cache,]28:13:Cache-Control,8:no-cache,]136:10:User-Agent,117:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36,]22:7:Upgrade,9:websocket,]38:6:Origin,25:https://www.piesocket.com,]30:21:Sec-WebSocket-Version,2:13,]40:15:Accept-Encoding,17:gzip, deflate, br,]37:15:Accept-Language,14:en-US,en;q=0.9,]97:6:Cookie,84:_ga=GA1.1.636013294.1693303610; _ga_MP49LGP298=GS1.1.1693313101.3.1.1693314156.0.0.0,]10:3:dnt,1:1,]49:17:Sec-WebSocket-Key,24:aOpK58kffMOGmH4do+rCyw==,]74:24:Sec-WebSocket-Extensions,42:permessage-deflate; client_max_window_bits,]]12:http_version;8:HTTP/1.1,}6:backup;0:~17:timestamp_created;17:1693314200.195851^7:comment;0:;8:metadata;0:}6:marked;0:;9:is_replay;0:~11:intercepted;5:false!11:server_conn;5831:3:via;0:~19:timestamp_tcp_setup;16:1693314200.04639^7:address;28:18:demo.piesocket.com;3:443#]19:timestamp_tls_setup;17:1693314200.172637^13:timestamp_end;17:1693314203.133634^15:timestamp_start;17:1693314199.880595^3:sni;18:demo.piesocket.com;11:tls_version;7:TLSv1.3;11:cipher_list;0:]6:cipher;22:TLS_AES_256_GCM_SHA384;11:alpn_offers;11:8:http/1.1,]4:alpn;8:http/1.1,16:certificate_list;5256:1489:-----BEGIN CERTIFICATE----- +MIIEHjCCAwagAwIBAgISAw9WcI3AoYkkGUVT7RHS1DczMA0GCSqGSIb3DQEBCwUA +MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD +EwJSMzAeFw0yMzA3MTIxMTMzMDFaFw0yMzEwMTAxMTMzMDBaMBoxGDAWBgNVBAMM +DyoucGllc29ja2V0LmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLqZHOAR +5SouUzBM01KTHBe+1q3lBuexffT7w9god02D/B17UVQZ/mWQ+1ZnGQQN1DYxatY9 +6y9d+TByJ533wgyjggIPMIICCzAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYI +KwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFIC4DERR +AGAF4urMZyNMaUvcT/BaMB8GA1UdIwQYMBaAFBQusxe3WFbLrlAJQOYfr52LFMLG +MFUGCCsGAQUFBwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0cDovL3IzLm8ubGVuY3Iu +b3JnMCIGCCsGAQUFBzAChhZodHRwOi8vcjMuaS5sZW5jci5vcmcvMBoGA1UdEQQT +MBGCDyoucGllc29ja2V0LmNvbTATBgNVHSAEDDAKMAgGBmeBDAECATCCAQIGCisG +AQQB1nkCBAIEgfMEgfAA7gB1AHoyjFTYty22IOo44FIe6YQWcDIThU070ivBOlej +UutSAAABiUoXMnQAAAQDAEYwRAIgF7y//yrAcNvsbWXCLDvIttIDV1nrrnETGIWz +Kcj0OY8CIGVaR8bOFXVybyeg2kFVQAqHmJGC+KLJTg0hZclTSLoVAHUA6D7Q2j71 +BjUy51covIlryQPTy9ERa+zraeF3fW0GvW4AAAGJShcyTAAABAMARjBEAiA9TFF+ +YZJbso2w8wyxW/F1ibV1KJRWTCGe6mRr40HVkwIgbiNDl6u5auGGtFyMc6IfGK2k +TpUvSsChyQQJSSZe2bswDQYJKoZIhvcNAQELBQADggEBAIUQKUu0s94lOlXl6CTc +gOjWZ/pEXE8/ND9OhAaUbcjyLryFBH7X3xdggAa2xcgXPKQeZsMtyilAVeYVy+2G +ULx0PVQqqCDykZqTsQp166lZf21nx74EwjJt61tgLzCn3YI8f5F/ELXYdoX3o4om +hXMQFfUtCtKSKV5IiRQ/76HclP1+LAzIv3dMBNvi1CZKesvB5oALEXRSC78HmgJM +31F59wzsov5iVv8fdxpmm9DuWkW+S7hmrUw23etWHWUwVU67g/rD6Fwi4xJLw/jj +qEr4rARGaj8iR9tdDI01qQDJ+nOZ6TA77OekZzBc5EKBAxinp+O1mTpXgiBb4c42 +EHc= +-----END CERTIFICATE----- +,1826:-----BEGIN CERTIFICATE----- +MIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw +WhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg +RW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP +R5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx +sxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm +NHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg +Z3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG +/kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB +Af8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA +FHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw +AoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw +Oi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB +gt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W +PTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl +ikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz +CkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm +lJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4 +avAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2 +yJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O +yK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids +hCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+ +HlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv +MldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX +nLRbwHOoq7hHwg== +-----END CERTIFICATE----- +,1923:-----BEGIN CERTIFICATE----- +MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTIxMDEyMDE5MTQwM1oXDTI0MDkzMDE4MTQwM1ow +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCt6CRz9BQ385ueK1coHIe+3LffOJCMbjzmV6B493XC +ov71am72AE8o295ohmxEk7axY/0UEmu/H9LqMZshftEzPLpI9d1537O4/xLxIZpL +wYqGcWlKZmZsj348cL+tKSIG8+TA5oCu4kuPt5l+lAOf00eXfJlII1PoOK5PCm+D +LtFJV4yAdLbaL9A4jXsDcCEbdfIwPPqPrt3aY6vrFk/CjhFLfs8L6P+1dy70sntK +4EwSJQxwjQMpoOFTJOwT2e4ZvxCzSow/iaNhUd6shweU9GNx7C7ib1uYgeGJXDR5 +bHbvO5BieebbpJovJsXQEOEO3tkQjhb7t/eo98flAgeYjzYIlefiN5YNNnWe+w5y +sR2bvAP5SQXYgd0FtCrWQemsAXaVCg/Y39W9Eh81LygXbNKYwagJZHduRze6zqxZ +Xmidf3LWicUGQSk+WT7dJvUkyRGnWqNMQB9GoZm1pzpRboY7nn1ypxIFeFntPlF4 +FQsDj43QLwWyPntKHEtzBRL8xurgUBN8Q5N0s8p0544fAQjQMNRbcTa0B7rBMDBc +SLeCO5imfWCKoqMpgsy6vYMEG6KDA0Gh1gXxG8K28Kh8hjtGqEgqiNx2mna/H2ql +PRmP6zjzZN7IKw0KKP/32+IVQtQi0Cdd4Xn+GOdwiK1O5tmLOsbdJ1Fu/7xk9TND +TwIDAQABo4IBRjCCAUIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +SwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5pZGVudHJ1 +c3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTEp7Gkeyxx ++tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEB +ATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQu +b3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0LmNvbS9E +U1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFHm0WeZ7tuXkAXOACIjIGlj26Ztu +MA0GCSqGSIb3DQEBCwUAA4IBAQAKcwBslm7/DlLQrt2M51oGrS+o44+/yQoDFVDC +5WxCu2+b9LRPwkSICHXM6webFGJueN7sJ7o5XPWioW5WlHAQU7G75K/QosMrAdSW +9MUgNTP52GE24HGNtLi1qoJFlcDyqSMo59ahy2cI2qBDLKobkx/J3vWraV0T9VuG +WCLKTVXkcGdtwlfFRjlBz4pYg1htmf5X6DYO8A4jqv2Il9DjXA6USbW1FzXSLr9O +he8Y4IWS6wY7bCkjCWDcRQJMEhg76fsO3txE+FiYruq9RUWhiF1myv4Q6W+CyBFC +Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5 +-----END CERTIFICATE----- +,]3:tls;4:true!5:error;0:~18:transport_protocol;3:tcp;2:id;36:1e1bf646-978a-4032-9f02-4af87dff921e;8:sockname;27:15:192.168.178.132;5:58247#]8:peername;25:15:143.244.202.177;3:443#]}11:client_conn;504:10:proxy_mode;7:regular;8:mitmcert;0:~19:timestamp_tls_setup;17:1693314200.194439^13:timestamp_end;18:1693314203.1340852^15:timestamp_start;17:1693314199.872163^3:sni;18:demo.piesocket.com;11:tls_version;7:TLSv1.3;11:cipher_list;0:]6:cipher;22:TLS_AES_256_GCM_SHA384;11:alpn_offers;11:8:http/1.1,]4:alpn;8:http/1.1,16:certificate_list;0:]3:tls;4:true!5:error;0:~18:transport_protocol;3:tcp;2:id;36:4b822dcb-1a36-4c62-9c74-492fe4ffd540;8:sockname;19:9:127.0.0.1;4:8080#]8:peername;20:9:127.0.0.1;5:58246#]}5:error;0:~2:id;36:048beb47-5242-4486-8634-165f6fef572a;4:type;4:http;7:version;2:20#}8235:9:websocket;205:13:timestamp_end;17:1693314210.272316^12:close_reason;0:;10:close_code;4:1000#16:closed_by_client;5:false!8:messages;84:80:1:1#5:false!26:{"error":"Missing apiKey"},18:1693314210.2705212^5:false!5:false!]]}8:response;433:6:reason;19:Switching Protocols,11:status_code;3:101#13:timestamp_end;17:1693314210.264409^15:timestamp_start;17:1693314210.261887^8:trailers;0:~7:content;0:,7:headers;233:34:6:Server,21:nginx/1.22.0 (Ubuntu),]40:4:Date,29:Tue, 29 Aug 2023 13:03:30 GMT,]24:10:Connection,7:upgrade,]22:7:Upgrade,9:websocket,]56:20:Sec-WebSocket-Accept,28:Wwhjg4OlFHvFrMi/px8nDtVomBs=,]33:12:X-Powered-By,13:Ratchet/0.4.4,]]12:http_version;8:HTTP/1.1,}7:request;954:4:path;15:/v3/channel_123,9:authority;0:,6:scheme;5:https,6:method;3:GET,4:port;3:443#4:host;18:demo.piesocket.com;13:timestamp_end;18:1693314210.1479628^15:timestamp_start;17:1693314210.145184^8:trailers;0:~7:content;0:,7:headers;691:29:4:Host,18:demo.piesocket.com,]24:10:Connection,7:Upgrade,]20:6:Pragma,8:no-cache,]28:13:Cache-Control,8:no-cache,]136:10:User-Agent,117:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36,]22:7:Upgrade,9:websocket,]38:6:Origin,25:https://www.piesocket.com,]30:21:Sec-WebSocket-Version,2:13,]40:15:Accept-Encoding,17:gzip, deflate, br,]37:15:Accept-Language,14:en-US,en;q=0.9,]97:6:Cookie,84:_ga=GA1.1.636013294.1693303610; _ga_MP49LGP298=GS1.1.1693313101.3.1.1693314209.0.0.0,]10:3:dnt,1:1,]49:17:Sec-WebSocket-Key,24:0jHmrzSl0PCJeqxcc8C0Jg==,]74:24:Sec-WebSocket-Extensions,42:permessage-deflate; client_max_window_bits,]]12:http_version;8:HTTP/1.1,}6:backup;0:~17:timestamp_created;17:1693314210.145588^7:comment;0:;8:metadata;0:}6:marked;0:;9:is_replay;0:~11:intercepted;5:false!11:server_conn;5835:3:via;0:~19:timestamp_tcp_setup;18:1693314210.0084548^7:address;28:18:demo.piesocket.com;3:443#]19:timestamp_tls_setup;18:1693314210.1318011^13:timestamp_end;18:1693314210.2760131^15:timestamp_start;17:1693314209.899575^3:sni;18:demo.piesocket.com;11:tls_version;7:TLSv1.3;11:cipher_list;0:]6:cipher;22:TLS_AES_256_GCM_SHA384;11:alpn_offers;11:8:http/1.1,]4:alpn;8:http/1.1,16:certificate_list;5256:1489:-----BEGIN CERTIFICATE----- +MIIEHjCCAwagAwIBAgISAw9WcI3AoYkkGUVT7RHS1DczMA0GCSqGSIb3DQEBCwUA +MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD +EwJSMzAeFw0yMzA3MTIxMTMzMDFaFw0yMzEwMTAxMTMzMDBaMBoxGDAWBgNVBAMM +DyoucGllc29ja2V0LmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLqZHOAR +5SouUzBM01KTHBe+1q3lBuexffT7w9god02D/B17UVQZ/mWQ+1ZnGQQN1DYxatY9 +6y9d+TByJ533wgyjggIPMIICCzAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYI +KwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFIC4DERR +AGAF4urMZyNMaUvcT/BaMB8GA1UdIwQYMBaAFBQusxe3WFbLrlAJQOYfr52LFMLG +MFUGCCsGAQUFBwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0cDovL3IzLm8ubGVuY3Iu +b3JnMCIGCCsGAQUFBzAChhZodHRwOi8vcjMuaS5sZW5jci5vcmcvMBoGA1UdEQQT +MBGCDyoucGllc29ja2V0LmNvbTATBgNVHSAEDDAKMAgGBmeBDAECATCCAQIGCisG +AQQB1nkCBAIEgfMEgfAA7gB1AHoyjFTYty22IOo44FIe6YQWcDIThU070ivBOlej +UutSAAABiUoXMnQAAAQDAEYwRAIgF7y//yrAcNvsbWXCLDvIttIDV1nrrnETGIWz +Kcj0OY8CIGVaR8bOFXVybyeg2kFVQAqHmJGC+KLJTg0hZclTSLoVAHUA6D7Q2j71 +BjUy51covIlryQPTy9ERa+zraeF3fW0GvW4AAAGJShcyTAAABAMARjBEAiA9TFF+ +YZJbso2w8wyxW/F1ibV1KJRWTCGe6mRr40HVkwIgbiNDl6u5auGGtFyMc6IfGK2k +TpUvSsChyQQJSSZe2bswDQYJKoZIhvcNAQELBQADggEBAIUQKUu0s94lOlXl6CTc +gOjWZ/pEXE8/ND9OhAaUbcjyLryFBH7X3xdggAa2xcgXPKQeZsMtyilAVeYVy+2G +ULx0PVQqqCDykZqTsQp166lZf21nx74EwjJt61tgLzCn3YI8f5F/ELXYdoX3o4om +hXMQFfUtCtKSKV5IiRQ/76HclP1+LAzIv3dMBNvi1CZKesvB5oALEXRSC78HmgJM +31F59wzsov5iVv8fdxpmm9DuWkW+S7hmrUw23etWHWUwVU67g/rD6Fwi4xJLw/jj +qEr4rARGaj8iR9tdDI01qQDJ+nOZ6TA77OekZzBc5EKBAxinp+O1mTpXgiBb4c42 +EHc= +-----END CERTIFICATE----- +,1826:-----BEGIN CERTIFICATE----- +MIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw +WhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg +RW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP +R5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx +sxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm +NHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg +Z3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG +/kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB +Af8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA +FHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw +AoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw +Oi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB +gt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W +PTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl +ikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz +CkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm +lJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4 +avAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2 +yJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O +yK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids +hCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+ +HlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv +MldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX +nLRbwHOoq7hHwg== +-----END CERTIFICATE----- +,1923:-----BEGIN CERTIFICATE----- +MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTIxMDEyMDE5MTQwM1oXDTI0MDkzMDE4MTQwM1ow +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCt6CRz9BQ385ueK1coHIe+3LffOJCMbjzmV6B493XC +ov71am72AE8o295ohmxEk7axY/0UEmu/H9LqMZshftEzPLpI9d1537O4/xLxIZpL +wYqGcWlKZmZsj348cL+tKSIG8+TA5oCu4kuPt5l+lAOf00eXfJlII1PoOK5PCm+D +LtFJV4yAdLbaL9A4jXsDcCEbdfIwPPqPrt3aY6vrFk/CjhFLfs8L6P+1dy70sntK +4EwSJQxwjQMpoOFTJOwT2e4ZvxCzSow/iaNhUd6shweU9GNx7C7ib1uYgeGJXDR5 +bHbvO5BieebbpJovJsXQEOEO3tkQjhb7t/eo98flAgeYjzYIlefiN5YNNnWe+w5y +sR2bvAP5SQXYgd0FtCrWQemsAXaVCg/Y39W9Eh81LygXbNKYwagJZHduRze6zqxZ +Xmidf3LWicUGQSk+WT7dJvUkyRGnWqNMQB9GoZm1pzpRboY7nn1ypxIFeFntPlF4 +FQsDj43QLwWyPntKHEtzBRL8xurgUBN8Q5N0s8p0544fAQjQMNRbcTa0B7rBMDBc +SLeCO5imfWCKoqMpgsy6vYMEG6KDA0Gh1gXxG8K28Kh8hjtGqEgqiNx2mna/H2ql +PRmP6zjzZN7IKw0KKP/32+IVQtQi0Cdd4Xn+GOdwiK1O5tmLOsbdJ1Fu/7xk9TND +TwIDAQABo4IBRjCCAUIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +SwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5pZGVudHJ1 +c3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTEp7Gkeyxx ++tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEB +ATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQu +b3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0LmNvbS9E +U1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFHm0WeZ7tuXkAXOACIjIGlj26Ztu +MA0GCSqGSIb3DQEBCwUAA4IBAQAKcwBslm7/DlLQrt2M51oGrS+o44+/yQoDFVDC +5WxCu2+b9LRPwkSICHXM6webFGJueN7sJ7o5XPWioW5WlHAQU7G75K/QosMrAdSW +9MUgNTP52GE24HGNtLi1qoJFlcDyqSMo59ahy2cI2qBDLKobkx/J3vWraV0T9VuG +WCLKTVXkcGdtwlfFRjlBz4pYg1htmf5X6DYO8A4jqv2Il9DjXA6USbW1FzXSLr9O +he8Y4IWS6wY7bCkjCWDcRQJMEhg76fsO3txE+FiYruq9RUWhiF1myv4Q6W+CyBFC +Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5 +-----END CERTIFICATE----- +,]3:tls;4:true!5:error;0:~18:transport_protocol;3:tcp;2:id;36:99d8c8c1-d3da-45d7-991f-e119fc80cf96;8:sockname;27:15:192.168.178.132;5:58291#]8:peername;25:15:143.244.202.177;3:443#]}11:client_conn;503:10:proxy_mode;7:regular;8:mitmcert;0:~19:timestamp_tls_setup;17:1693314210.143479^13:timestamp_end;17:1693314210.276394^15:timestamp_start;17:1693314209.894985^3:sni;18:demo.piesocket.com;11:tls_version;7:TLSv1.3;11:cipher_list;0:]6:cipher;22:TLS_AES_256_GCM_SHA384;11:alpn_offers;11:8:http/1.1,]4:alpn;8:http/1.1,16:certificate_list;0:]3:tls;4:true!5:error;0:~18:transport_protocol;3:tcp;2:id;36:f98594b3-81e8-499c-834b-7cf15569cea1;8:sockname;19:9:127.0.0.1;4:8080#]8:peername;20:9:127.0.0.1;5:58290#]}5:error;0:~2:id;36:0474f9fe-0973-4ff1-afd5-01013d04ba1f;4:type;4:http;7:version;2:20#}8412:9:websocket;323:13:timestamp_end;18:1693314246.2275898^12:close_reason;0:;10:close_code;4:1005#16:closed_by_client;4:true!8:messages;201:80:1:1#5:false!27:{"error":"Unknown api key"},17:1693314232.444319^5:false!5:false!]55:1:1#4:true!3:foo,18:1693314240.7948081^5:false!5:false!]54:1:1#4:true!3:bar,17:1693314244.039395^5:false!5:false!]]}8:response;433:6:reason;19:Switching Protocols,11:status_code;3:101#13:timestamp_end;17:1693314232.436218^15:timestamp_start;17:1693314232.431901^8:trailers;0:~7:content;0:,7:headers;233:34:6:Server,21:nginx/1.22.0 (Ubuntu),]40:4:Date,29:Tue, 29 Aug 2023 13:03:52 GMT,]24:10:Connection,7:upgrade,]22:7:Upgrade,9:websocket,]56:20:Sec-WebSocket-Accept,28:XCjji+8Ziem+CRE5RLW6lJe6T6E=,]33:12:X-Powered-By,13:Ratchet/0.4.4,]]12:http_version;8:HTTP/1.1,}7:request;1015:4:path;76:/v3/channel_123?api_key=VCXCEuvhGcBDP7XhiJJUDvR1e1D3eiVjgZ9VRiaV¬ify_self,9:authority;0:,6:scheme;5:https,6:method;3:GET,4:port;3:443#4:host;18:demo.piesocket.com;13:timestamp_end;17:1693314232.269683^15:timestamp_start;18:1693314232.2665899^8:trailers;0:~7:content;0:,7:headers;691:29:4:Host,18:demo.piesocket.com,]24:10:Connection,7:Upgrade,]20:6:Pragma,8:no-cache,]28:13:Cache-Control,8:no-cache,]136:10:User-Agent,117:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36,]22:7:Upgrade,9:websocket,]38:6:Origin,25:https://www.piesocket.com,]30:21:Sec-WebSocket-Version,2:13,]40:15:Accept-Encoding,17:gzip, deflate, br,]37:15:Accept-Language,14:en-US,en;q=0.9,]97:6:Cookie,84:_ga=GA1.1.636013294.1693303610; _ga_MP49LGP298=GS1.1.1693313101.3.1.1693314221.0.0.0,]10:3:dnt,1:1,]49:17:Sec-WebSocket-Key,24:jYGzUBibPBT6k8EzhAjMwg==,]74:24:Sec-WebSocket-Extensions,42:permessage-deflate; client_max_window_bits,]]12:http_version;8:HTTP/1.1,}6:backup;0:~17:timestamp_created;17:1693314232.266952^7:comment;0:;8:metadata;0:}6:marked;0:;9:is_replay;0:~11:intercepted;5:false!11:server_conn;5833:3:via;0:~19:timestamp_tcp_setup;17:1693314232.112502^7:address;28:18:demo.piesocket.com;3:443#]19:timestamp_tls_setup;17:1693314232.255521^13:timestamp_end;18:1693314246.2324648^15:timestamp_start;17:1693314231.916703^3:sni;18:demo.piesocket.com;11:tls_version;7:TLSv1.3;11:cipher_list;0:]6:cipher;22:TLS_AES_256_GCM_SHA384;11:alpn_offers;11:8:http/1.1,]4:alpn;8:http/1.1,16:certificate_list;5256:1489:-----BEGIN CERTIFICATE----- +MIIEHjCCAwagAwIBAgISAw9WcI3AoYkkGUVT7RHS1DczMA0GCSqGSIb3DQEBCwUA +MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD +EwJSMzAeFw0yMzA3MTIxMTMzMDFaFw0yMzEwMTAxMTMzMDBaMBoxGDAWBgNVBAMM +DyoucGllc29ja2V0LmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLqZHOAR +5SouUzBM01KTHBe+1q3lBuexffT7w9god02D/B17UVQZ/mWQ+1ZnGQQN1DYxatY9 +6y9d+TByJ533wgyjggIPMIICCzAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYI +KwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFIC4DERR +AGAF4urMZyNMaUvcT/BaMB8GA1UdIwQYMBaAFBQusxe3WFbLrlAJQOYfr52LFMLG +MFUGCCsGAQUFBwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0cDovL3IzLm8ubGVuY3Iu +b3JnMCIGCCsGAQUFBzAChhZodHRwOi8vcjMuaS5sZW5jci5vcmcvMBoGA1UdEQQT +MBGCDyoucGllc29ja2V0LmNvbTATBgNVHSAEDDAKMAgGBmeBDAECATCCAQIGCisG +AQQB1nkCBAIEgfMEgfAA7gB1AHoyjFTYty22IOo44FIe6YQWcDIThU070ivBOlej +UutSAAABiUoXMnQAAAQDAEYwRAIgF7y//yrAcNvsbWXCLDvIttIDV1nrrnETGIWz +Kcj0OY8CIGVaR8bOFXVybyeg2kFVQAqHmJGC+KLJTg0hZclTSLoVAHUA6D7Q2j71 +BjUy51covIlryQPTy9ERa+zraeF3fW0GvW4AAAGJShcyTAAABAMARjBEAiA9TFF+ +YZJbso2w8wyxW/F1ibV1KJRWTCGe6mRr40HVkwIgbiNDl6u5auGGtFyMc6IfGK2k +TpUvSsChyQQJSSZe2bswDQYJKoZIhvcNAQELBQADggEBAIUQKUu0s94lOlXl6CTc +gOjWZ/pEXE8/ND9OhAaUbcjyLryFBH7X3xdggAa2xcgXPKQeZsMtyilAVeYVy+2G +ULx0PVQqqCDykZqTsQp166lZf21nx74EwjJt61tgLzCn3YI8f5F/ELXYdoX3o4om +hXMQFfUtCtKSKV5IiRQ/76HclP1+LAzIv3dMBNvi1CZKesvB5oALEXRSC78HmgJM +31F59wzsov5iVv8fdxpmm9DuWkW+S7hmrUw23etWHWUwVU67g/rD6Fwi4xJLw/jj +qEr4rARGaj8iR9tdDI01qQDJ+nOZ6TA77OekZzBc5EKBAxinp+O1mTpXgiBb4c42 +EHc= +-----END CERTIFICATE----- +,1826:-----BEGIN CERTIFICATE----- +MIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw +WhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg +RW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP +R5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx +sxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm +NHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg +Z3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG +/kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB +Af8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA +FHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw +AoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw +Oi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB +gt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W +PTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl +ikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz +CkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm +lJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4 +avAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2 +yJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O +yK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids +hCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+ +HlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv +MldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX +nLRbwHOoq7hHwg== +-----END CERTIFICATE----- +,1923:-----BEGIN CERTIFICATE----- +MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTIxMDEyMDE5MTQwM1oXDTI0MDkzMDE4MTQwM1ow +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCt6CRz9BQ385ueK1coHIe+3LffOJCMbjzmV6B493XC +ov71am72AE8o295ohmxEk7axY/0UEmu/H9LqMZshftEzPLpI9d1537O4/xLxIZpL +wYqGcWlKZmZsj348cL+tKSIG8+TA5oCu4kuPt5l+lAOf00eXfJlII1PoOK5PCm+D +LtFJV4yAdLbaL9A4jXsDcCEbdfIwPPqPrt3aY6vrFk/CjhFLfs8L6P+1dy70sntK +4EwSJQxwjQMpoOFTJOwT2e4ZvxCzSow/iaNhUd6shweU9GNx7C7ib1uYgeGJXDR5 +bHbvO5BieebbpJovJsXQEOEO3tkQjhb7t/eo98flAgeYjzYIlefiN5YNNnWe+w5y +sR2bvAP5SQXYgd0FtCrWQemsAXaVCg/Y39W9Eh81LygXbNKYwagJZHduRze6zqxZ +Xmidf3LWicUGQSk+WT7dJvUkyRGnWqNMQB9GoZm1pzpRboY7nn1ypxIFeFntPlF4 +FQsDj43QLwWyPntKHEtzBRL8xurgUBN8Q5N0s8p0544fAQjQMNRbcTa0B7rBMDBc +SLeCO5imfWCKoqMpgsy6vYMEG6KDA0Gh1gXxG8K28Kh8hjtGqEgqiNx2mna/H2ql +PRmP6zjzZN7IKw0KKP/32+IVQtQi0Cdd4Xn+GOdwiK1O5tmLOsbdJ1Fu/7xk9TND +TwIDAQABo4IBRjCCAUIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +SwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5pZGVudHJ1 +c3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTEp7Gkeyxx ++tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEB +ATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQu +b3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0LmNvbS9E +U1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFHm0WeZ7tuXkAXOACIjIGlj26Ztu +MA0GCSqGSIb3DQEBCwUAA4IBAQAKcwBslm7/DlLQrt2M51oGrS+o44+/yQoDFVDC +5WxCu2+b9LRPwkSICHXM6webFGJueN7sJ7o5XPWioW5WlHAQU7G75K/QosMrAdSW +9MUgNTP52GE24HGNtLi1qoJFlcDyqSMo59ahy2cI2qBDLKobkx/J3vWraV0T9VuG +WCLKTVXkcGdtwlfFRjlBz4pYg1htmf5X6DYO8A4jqv2Il9DjXA6USbW1FzXSLr9O +he8Y4IWS6wY7bCkjCWDcRQJMEhg76fsO3txE+FiYruq9RUWhiF1myv4Q6W+CyBFC +Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5 +-----END CERTIFICATE----- +,]3:tls;4:true!5:error;0:~18:transport_protocol;3:tcp;2:id;36:63e5a61e-ed54-46fc-bcc6-4e4c0b59b5fb;8:sockname;27:15:192.168.178.132;5:58387#]8:peername;25:15:143.244.202.177;3:443#]}11:client_conn;502:10:proxy_mode;7:regular;8:mitmcert;0:~19:timestamp_tls_setup;16:1693314232.26438^13:timestamp_end;17:1693314246.232969^15:timestamp_start;17:1693314231.911616^3:sni;18:demo.piesocket.com;11:tls_version;7:TLSv1.3;11:cipher_list;0:]6:cipher;22:TLS_AES_256_GCM_SHA384;11:alpn_offers;11:8:http/1.1,]4:alpn;8:http/1.1,16:certificate_list;0:]3:tls;4:true!5:error;0:~18:transport_protocol;3:tcp;2:id;36:67c4acac-3970-456c-9b91-35b5289eb553;8:sockname;19:9:127.0.0.1;4:8080#]8:peername;20:9:127.0.0.1;5:58386#]}5:error;0:~2:id;36:5f4f7d1a-64bc-479a-ad5d-b364d6ad3538;4:type;4:http;7:version;2:20#} \ No newline at end of file diff --git a/test/mitmproxy/data/har_files/charles.har b/test/mitmproxy/data/har_files/charles.har new file mode 100644 index 0000000000..c74723b995 --- /dev/null +++ b/test/mitmproxy/data/har_files/charles.har @@ -0,0 +1,122 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Charles Proxy", + "version": "4.6.3" + }, + "entries": [ + { + "startedDateTime": "2023-03-29T17:37:42.482-07:00", + "time": 107, + "request": { + "method": "GET", + "url": "https://mitmproxy.org/?=", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Host", + "value": "mitmproxy.org" + }, + { + "name": "User-Agent", + "value": "Charles/4.6.3" + } + ], + "queryString": [ + { + "name": "", + "value": "" + } + ], + "headersSize": 68, + "bodySize": 0 + }, + "response": { + "_charlesStatus": "COMPLETE", + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "text/html" + }, + { + "name": "Content-Length", + "value": "23866" + }, + { + "name": "Last-Modified", + "value": "Sat, 04 Mar 2023 18:02:08 GMT" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "Date", + "value": "Wed, 29 Mar 2023 08:30:06 GMT" + }, + { + "name": "ETag", + "value": "\"a1550c2bd25c5bcfef789d730f5bbddf\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "X-Cache", + "value": "Hit from cloudfront" + }, + { + "name": "Via", + "value": "1.1 85331abd84b5669394785900a34f7b14.cloudfront.net (CloudFront)" + }, + { + "name": "X-Amz-Cf-Pop", + "value": "SFO5-C1" + }, + { + "name": "Alt-Svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "X-Amz-Cf-Id", + "value": "Kk1li6yk9uTkdA-9qj0AGGjPrNRPabTH_kq4rm91I5Xq3rtCARbzFg==" + }, + { + "name": "Age", + "value": "58057" + }, + { + "name": "Connection", + "value": "keep-alive" + } + ], + "content": { + "size": 23866, + "mimeType": "text/html", + "text": "\n\n\n \n \n \n\n \n mitmproxy - an interactive HTTPS proxy<\/title>\n \n \n \n <link rel=\"stylesheet\" href=\"./style.min.css\">\n <link rel=\"alternate\" type=\"application/rss+xml\" href=\"https://mitmproxy.org/index.xml\" title=\"mitmproxy.org\" />\n <meta name=\"generator\" content=\"Hugo 0.104.3\" />\n<\/head>\n<body>\n<nav class=\"navbar is-dark\" role=\"navigation\" aria-label=\"main navigation\">\n <div class=\"container\">\n <div class=\"navbar-brand\">\n <a class=\"navbar-item\" href=\"./\">\n <img src=\"./logo-navbar.png\" alt=\"mitmproxy\">\n <\/a>\n <button class=\"button navbar-burger is-dark\" data-target=\"topnav\">\n <span><\/span>\n <span><\/span>\n <span><\/span>\n <\/button>\n <\/div>\n <div id=\"topnav\" class=\"navbar-menu\">\n <div class=\"navbar-start\">\n <a class=\"navbar-item\" href=\"./posts\">\n Blog\n <\/a>\n <div class=\"navbar-item has-dropdown is-hoverable\">\n <a class=\"navbar-link\" href=\"https://docs.mitmproxy.org/stable\">\n Docs\n <\/a>\n <div class=\"navbar-dropdown is-boxed\">\n <a class=\"navbar-item\" target=\"_blank\" href=\"https://docs.mitmproxy.org/stable\">\n v9 (latest release) <\/a>\n <a class=\"navbar-item\" target=\"_blank\" href=\"https://docs.mitmproxy.org/archive/v8\">\n v8<\/a>\n <a class=\"navbar-item\" target=\"_blank\" href=\"https://docs.mitmproxy.org/archive/v7\">\n v7<\/a>\n <a class=\"navbar-item\" target=\"_blank\" href=\"https://docs.mitmproxy.org/archive/v6\">\n v6<\/a>\n <a class=\"navbar-item\" target=\"_blank\" href=\"https://docs.mitmproxy.org/archive/v5\">\n v5<\/a>\n <a class=\"navbar-item\" target=\"_blank\" href=\"https://docs.mitmproxy.org/archive/v4\">\n v4<\/a>\n <a class=\"navbar-item\" target=\"_blank\" href=\"https://docs.mitmproxy.org/archive/v3\">\n v3<\/a>\n <a class=\"navbar-item\" target=\"_blank\" href=\"https://docs.mitmproxy.org/archive/v2\">\n v2<\/a>\n <a class=\"navbar-item\" target=\"_blank\" href=\"https://docs.mitmproxy.org/archive/v1\">\n v1<\/a>\n\n <hr class=\"navbar-divider\">\n\n <a class=\"navbar-item\" target=\"_blank\" href=\"https://docs.mitmproxy.org/dev\">\n dev<\/a>\n <\/div>\n <\/div>\n <a class=\"navbar-item\" href=\"./publications\">\n Publications\n <\/a>\n <\/div>\n <div class=\"navbar-end\">\n <div class=\"navbar-item is-hidden-touch\">\n <iframe src=\"./github-btn.html?user=mhils&type=sponsor&size=large\"\n frameborder=\"0\"\n scrolling=\"0\"\n width=\"115\"\n height=\"30\"><\/iframe>\n <iframe src=\"./github-btn.html?user=mitmproxy&repo=mitmproxy&type=star&count=true&size=large\"\n frameborder=\"0\"\n scrolling=\"0\"\n width=\"160\"\n height=\"30\"><\/iframe>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n<\/nav>\n<script>\n document.addEventListener('DOMContentLoaded', function () {\n\n \n var $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);\n\n \n if ($navbarBurgers.length > 0) {\n\n \n $navbarBurgers.forEach(function ($el) {\n $el.addEventListener('click', function () {\n\n \n var target = $el.dataset.target;\n var $target = document.getElementById(target);\n\n \n $el.classList.toggle('is-active');\n $target.classList.toggle('is-active');\n\n });\n });\n }\n\n });\n<\/script>\n\n<div id=\"home\">\n <section id=\"intro\">\n <div class=\"hero-body\">\n <div class=\"container\">\n <div class=\"columns\">\n <div class=\"column is-7\">\n <img src=\"./screenshot.png\" alt=\"screenshot of mitmproxy's interface\">\n <\/div>\n <div class=\"column is-hidden-mobile\"><\/div>\n <div class=\"column is-two-fifths\">\n <div>\n <h1 class=\"is-size-3\">\n <dfn id=\"definition\">mitmproxy<\/dfn> is a free\n and open source\n interactive HTTPS proxy.\n <\/h1>\n <br>\n <div id=\"install\" role=\"navigation\" aria-label=\"Installation\">\n <a id=\"install-windows-classic\"\n class=\"button is-hidden is-primary is-medium\"\n href=\"./downloads\">\n Download Windows Installer\n <\/a>\n <div>\n <a id=\"install-windows-store\" class=\"button is-hidden is-black\" href=\"ms-windows-store://pdp/?ProductId=9NWNDLQMNZD7\">\n <span class=\"icon\">\n <svg width=20 height=20 xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 23 23\">\n <path fill=\"#f35325\" d=\"M1 1h10v10H1z\"/>\n <path fill=\"#81bc06\" d=\"M12 1h10v10H12z\"/>\n <path fill=\"#05a6f0\" d=\"M1 12h10v10H1z\"/>\n <path fill=\"#ffba08\" d=\"M12 12h10v10H12z\"/>\n <\/svg>\n <\/span>\n <span>Get from Microsoft Store<\/span>\n <\/a>\n <\/div>\n <a id=\"install-linux\"\n class=\"button is-hidden is-primary is-medium\"\n href=\"./downloads\">\n Download Linux Binaries\n <\/a>\n <pre id=\"install-macos\" class=\"shell-command is-hidden\">\n <code>brew install mitmproxy<\/code>\n <code class=\"copy is-hidden-touch\"\n data-clipboard-text=\"brew install mitmproxy\">copy<\/code>\n <\/pre>\n <a id=\"install-docker\"\n class=\"button is-hidden is-link is-medium\"\n href=\"https://hub.docker.com/r/mitmproxy/mitmproxy/\">\n Docker Hub\n <\/a>\n <a id=\"install-more\"\n class=\"button is-hidden is-info is-medium\"\n href=\"./downloads\">\n More Downloads\n <\/a>\n <a id=\"install-default\"\n class=\"button is-primary is-medium\"\n href=\"./downloads\">\n Download\n <\/a>\n <\/div>\n <p>\n <a class=\"has-text-dark\" href=\"./tags/releases/\">Release Notes (v9.0)<\/a>\n <span>\n \u2013\n <a id=\"other-downloads\" class=\"has-text-dark\"\n href=\"./downloads\">\n Other Downloads\n <\/a>\n <\/span>\n <\/p>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n <\/section>\n\n <section id=\"features\">\n <div class=\"hero-body\">\n <div class=\"container\">\n <div class=\"columns\" role=\"navigation\" aria-label=\"Features\">\n <a href=\"#mitmproxy\" class=\"column has-text-centered has-text-primary\">\n <i class=\"fas fa-2x fa-terminal\"><\/i>\n <div class=\"title\">Command Line<\/div>\n <\/a>\n <a href=\"#mitmweb\" class=\"column has-text-centered has-text-primary\">\n <i class=\"fab fa-2x fa-firefox-browser\"><\/i>\n <div class=\"title\">Web Interface<\/div>\n <\/a>\n <a href=\"#mitmdump\" class=\"column has-text-centered has-text-primary\">\n <i class=\"fas fa-2x fa-code\"><\/i>\n <div class=\"title\">Python API<\/div>\n <\/a>\n <\/div>\n <\/div>\n <\/div>\n <\/section>\n\n <section id=\"mitmproxy\" class=\"feature\">\n <div class=\"hero-body\">\n <div class=\"container\">\n <div class=\"columns is-centered\">\n <div class=\"column\">\n <img src=\"./screenshot.png\" alt=\"screenshot of mitmproxy's interface\">\n <\/div>\n <div class=\"column\">\n <h2 class=\"title\">Command Line<\/h2>\n <p>\n <kbd>mitmproxy<\/kbd> is your swiss-army knife for debugging, testing,\n privacy measurements, and penetration testing.\n It can be used to intercept, inspect, modify and replay web traffic such\n as\n HTTP/1, HTTP/2, WebSockets, or any other SSL/TLS-protected protocols.\n You can prettify and decode a variety of message types ranging from HTML\n to\n Protobuf,\n intercept specific messages on-the-fly,\n modify them before they reach their destination, and replay them\n to a client or server later on.\n <\/p>\n <\/div>\n\n <\/div>\n <\/div>\n <\/div>\n <\/section>\n\n <section id=\"mitmweb\" class=\"feature\">\n <div class=\"hero-body\">\n <div class=\"container\">\n <div class=\"columns is-centered\">\n <div class=\"column\">\n <img src=\"./mitmweb.png\"\n alt=\"screenshot of mitmweb's interface\"\n style=\"border: solid #93a1a1 1px;\">\n <\/div>\n <div class=\"column\">\n <h2 class=\"title\">Web Interface<\/h2>\n <p>\n Use mitmproxy's main features in a graphical interface with\n <kbd>mitmweb<\/kbd>. Do you like Chrome's DevTools? <kbd>mitmweb<\/kbd>\n gives\n you a similar experience for any other application or device,\n plus additional features such as request interception and replay.\n\n <\/p>\n <!--\n <a class=\"button is-info is-outlined\"\n href=\"http://share.mitmproxy.org/honeynet-demo/#/flows/b07c553b-1ffd-4414-924f-ca4412804a6d/response\">\n View Static Demo\n <\/a>-->\n <\/div>\n\n <\/div>\n <\/div>\n <\/div>\n <\/section>\n\n <section id=\"mitmdump\" class=\"feature\">\n <div class=\"hero-body\">\n <div class=\"container\">\n <div class=\"columns is-centered\">\n <div class=\"column\">\n <div class=\"sample-code\">\n <div class=\"filename\">addon.py<\/div>\n <pre><span\n class=\"import\">from<\/span> mitmproxy <span\n class=\"import\">import<\/span> http\n\n<span class=\"keyword\">def<\/span> <span class=\"method\">request<\/span>(flow: http.HTTPFlow):\n <span class=\"comment\"># redirect to different host<\/span>\n <span class=\"keyword\">if<\/span> flow.request.pretty_host == <span\n class=\"str\">\"example.com\"<\/span>:\n flow.request.host = <span class=\"str\">\"mitmproxy.org\"<\/span>\n <span class=\"comment\"># answer from proxy<\/span>\n <span class=\"keyword\">elif<\/span> flow.request.path.endswith(<span class=\"str\">\"/brew\"<\/span>):\n \tflow.response = http.Response.make(\n <span class=\"num\">418<\/span>, <strong>b<\/strong><span class=\"str\">\"I'm a teapot\"<\/span>,\n )<\/pre>\n <\/div>\n <\/div>\n <div class=\"column\">\n <h2 class=\"title\">\n Python API\n <\/h2>\n <p>\n Write powerful addons and script mitmproxy with <kbd>mitmdump<\/kbd>.\n The scripting API offers full control over mitmproxy and makes it\n possible\n to automatically modify messages, redirect traffic, visualize messages,\n or\n implement custom commands.\n <\/p>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n <\/section>\n\n <section id=\"ecosystem\" class=\"feature\">\n <div class=\"hero-body\">\n <div class=\"container\">\n <div class=\"columns is-centered\">\n <div class=\"column\">\n <div style=\"margin: 0 auto\">\n <p class=\"is-size-5\"><i class=\"fab fa-twitter has-text-info\"><\/i> Latest\n Tweets<\/p>\n <div style=\"height: 500px; overflow-y: scroll\">\n <a href=\"https://twitter.com/mitmproxy/likes\" target=\"_blank\" aria-label=\"Open latest liked tweets by mitmproxy in new window.\">\n <img style=\"width: 100%; max-width: 430px\" alt=\"A screenshot of the latest liked tweets by @mitmproxy.\" src=\"./data/twitter-timeline.png\" loading=\"lazy\" title=\"🖼️ This is a screenshot to protect your privacy. ✨\">\n <\/a>\n <\/div>\n <\/div>\n <\/div>\n <div class=\"column\">\n <h2 class=\"title\">Powerful Ecosystem<\/h2>\n <div class=\"content\" role=\"navigation\" aria-label=\"Ecosystem\">\n <p>\n Mitmproxy has a vibrant ecosystem of addons and tools building on it:\n <\/p>\n <ul>\n <li>\n <a href=\"https://github.com/mitmproxy/mitmproxy/tree/main/examples/contrib\" class=\"has-text-link\">\n mitmproxy/examples/contrib<\/a>, a collection of\n community-contributed mitmproxy addons.\n <\/li>\n <li>\n <a href=\"https://github.com/alufers/mitmproxy2swagger\" class=\"has-text-link\">\n mitmproxy2swagger<\/a>, a tool for automatically converting mitmproxy captures to\n OpenAPI 3.0 specifications.\n <\/li>\n <li>\n <a href=\"https://github.com/soluble-ai/kubetap\" class=\"has-text-link\">\n kubetap<\/a>, a kubectl plugin to interactively proxy Kubernetes Services.\n <\/li>\n <\/ul>\n <\/div>\n <h1 class=\"title is-4\">Sponsored By<\/h1>\n <div class=\"sponsors\">\n <a href=\"https://proxyman.io/\"><img alt=\"Proxyman\" src=\"./sponsors/proxyman.png\"><\/a>\n <a href=\"https://netograph.io/\"><img alt=\"Netograph.io\" src=\"./sponsors/netograph.svg\"><\/a>\n <span style=\"margin-left: .4em\">...and many <a href=\"https://github.com/sponsors/mhils\" class=\"has-text-link\">individual supporters!<\/a> ❤️<\/span>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n <\/section>\n\n <section id=\"opensource\" class=\"feature\">\n <div class=\"hero-body\">\n <div class=\"container\">\n <div class=\"columns is-centered\">\n <div class=\"column has-text-centered\">\n <i class=\"fas fa-code fa-8x has-text-danger\"><\/i>\n <\/div>\n <div class=\"column\">\n <h2 class=\"title\">Open Source<\/h2>\n <p>\n Mitmproxy is free and open source. Be part of the mitmproxy community\n and\n help improve your favorite HTTPS proxy.\n <\/p>\n <br>\n <div class=\"buttons\" role=\"navigation\" aria-label=\"Open Source Cummunity\">\n <a class=\"button is-dark\"\n href=\"https://github.com/mitmproxy/mitmproxy\">\n <span class=\"icon\">\n <i class=\"fab fa-github\"><\/i>\n <\/span>\n <span>GitHub<\/span>\n <\/a>\n <a class=\"button is-info\" href=\"https://github.com/mitmproxy/mitmproxy/discussions\">\n <span class=\"icon\">\n <i class=\"far fa-comments\"><\/i>\n <\/span>\n <span>Ask Questions<\/span>\n <\/a>\n <a class=\"button is-danger\" href=\"https://slack.mitmproxy.org\">\n <span class=\"icon\">\n <i class=\"fab fa-slack-hash\"><\/i>\n <\/span>\n <span>Developer Chat<\/span>\n <\/a>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n <\/section>\n<\/div>\n<script src=\"./polyfills.js\"><\/script>\n<script src=\"./clipboard.min.js\"><\/script>\n<script src=\"./snapshots.js\"><\/script>\n<script>\n document.getElementsByClassName(\"copy\")\n // Copy\n let clipboard = new ClipboardJS('.copy');\n clipboard.on('success', function (e) {\n let node = e.trigger;\n node.classList.add(\"has-text-success\");\n node.textContent = \"copied!\";\n window.setTimeout(function(){\n node.classList.remove(\"has-text-success\")\n node.textContent = \"copy\"\n }, 1000)\n });\n\n getLatestRelease(\"-windows-x64-installer.exe\").then(function (url) {\n document.getElementById(\"install-windows-classic\").href = url;\n })\n getLatestRelease(\"-linux.tar.gz\").then(function (url) {\n document.getElementById(\"install-linux\").href = url;\n })\n\n // OS Detection\n let platform = window.navigator.platform.toLowerCase();\n if (platform.includes(\"win\")) {\n document.getElementById(\"install-default\").classList.add(\"is-hidden\");\n document.getElementById(\"install-windows-classic\").classList.remove(\"is-hidden\");\n document.getElementById(\"install-windows-store\").classList.remove(\"is-hidden\");\n } else if (platform.includes(\"linux\") && !platform.includes(\"android\")) {\n document.getElementById(\"install-default\").classList.add(\"is-hidden\");\n document.getElementById(\"install-linux\").classList.remove(\"is-hidden\");\n } else if (platform.includes(\"mac\") && !platform.includes(\"iPhone\") && !platform.includes(\"iPad\")) {\n document.getElementById(\"install-default\").classList.add(\"is-hidden\");\n document.getElementById(\"install-macos\").classList.remove(\"is-hidden\");\n }\n\n let install = document.querySelector(\"#install\");\n install.style.height = install.clientHeight + \"px\";\n document.getElementById(\"other-downloads\").addEventListener(\"click\", function (e) {\n e.preventDefault();\n document.getElementById(\"other-downloads\").parentElement.classList.add(\"is-hidden\");\n install.style.height = \"0px\";\n window.setTimeout(function () {\n document.querySelectorAll(\"#install > *\").forEach(function (x) {\n x.classList.remove(\"is-hidden\");\n })\n document.getElementById(\"install-default\").classList.add(\"is-hidden\");\n install.style.height = install.scrollHeight + \"px\";\n }, 300);\n })\n<\/script>\n\n<footer class=\"footer\">\n <div class=\"container\">\n <div class=\"content\">\n <div class=\"level\">\n <div class=\"level-left\">\n <p>\n <strong>mitmproxy<\/strong>, a project by\n <a href=\"https://twitter.com/cortesi\">@cortesi<\/a>,\n <a href=\"https://twitter.com/maximilianhils\">@maximilianhils<\/a>, and\n <a href=\"https://twitter.com/raumfresser\">@raumfresser<\/a>.<br>\n Maintained by the <a href=\"https://github.com/orgs/mitmproxy/people\">core\n team<\/a>\n with the help of <a\n href=\"https://github.com/mitmproxy/mitmproxy/graphs/contributors\">our fantastic\n contributors<\/a>.<br>\n Code licensed <a\n href=\"https://github.com/mitmproxy/mitmproxy/blob/main/LICENSE\">MIT<\/a>,\n website © 2023 Mitmproxy Project.\n <br><br>\n <small>Also checkout <a href=\"https://pdoc.dev\">pdoc<\/a>, a Python API documentation generator built by the mitmproxy developers.<\/small>\n <\/p>\n <\/div>\n <div class=\"level-right\">\n <a class=\"button is-outlined is-info\" href=\"https://twitter.com/mitmproxy\">\n <span class=\"icon\">\n <i class=\"fab fa-twitter\"><\/i>\n <\/span>\n <span>Follow @mitmproxy<\/span>\n <\/a>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n<\/footer>\n\n<\/body>\n<\/html>\n\n" + }, + "redirectURL": null, + "headersSize": 0, + "bodySize": 23866 + }, + "serverIPAddress": "13.35.121.50", + "cache": {}, + "timings": { + "dns": 43, + "connect": 37, + "ssl": 26, + "send": 1, + "wait": 24, + "receive": 2 + } + } + ] + } +} \ No newline at end of file diff --git a/test/mitmproxy/data/har_files/charles.json b/test/mitmproxy/data/har_files/charles.json new file mode 100644 index 0000000000..8077a35474 --- /dev/null +++ b/test/mitmproxy/data/har_files/charles.json @@ -0,0 +1,145 @@ +[ + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680136662.482, + "timestamp_tls_setup": null, + "timestamp_end": 1680136662.5890002 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": [ + "13.35.121.50", + 443 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/?=", + "http_version": "HTTP/1.1", + "headers": [ + [ + "Host", + "mitmproxy.org" + ], + [ + "User-Agent", + "Charles/4.6.3" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680136662.482, + "timestamp_end": 1680136662.5890002, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "text/html" + ], + [ + "Content-Length", + "23866" + ], + [ + "Last-Modified", + "Sat, 04 Mar 2023 18:02:08 GMT" + ], + [ + "Server", + "AmazonS3" + ], + [ + "Date", + "Wed, 29 Mar 2023 08:30:06 GMT" + ], + [ + "ETag", + "\"a1550c2bd25c5bcfef789d730f5bbddf\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "X-Cache", + "Hit from cloudfront" + ], + [ + "Via", + "1.1 85331abd84b5669394785900a34f7b14.cloudfront.net (CloudFront)" + ], + [ + "X-Amz-Cf-Pop", + "SFO5-C1" + ], + [ + "Alt-Svc", + "h3=\":443\"; ma=86400" + ], + [ + "X-Amz-Cf-Id", + "Kk1li6yk9uTkdA-9qj0AGGjPrNRPabTH_kq4rm91I5Xq3rtCARbzFg==" + ], + [ + "Age", + "58057" + ], + [ + "Connection", + "keep-alive" + ] + ], + "contentLength": 23866, + "contentHash": "7fd5f643a86976f5711df86ae2d5f9f8137a47c705dee31ccc550215564a5364", + "timestamp_start": 1680136662.482, + "timestamp_end": 1680136662.5890002 + } + } +] \ No newline at end of file diff --git a/test/mitmproxy/data/har_files/chrome.har b/test/mitmproxy/data/har_files/chrome.har new file mode 100644 index 0000000000..ce4462c48a --- /dev/null +++ b/test/mitmproxy/data/har_files/chrome.har @@ -0,0 +1,715 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "WebInspector", + "version": "537.36" + }, + "pages": [ + { + "startedDateTime": "2023-03-30T00:00:54.253Z", + "id": "page_1", + "title": "https://mitmproxy.org/", + "pageTimings": { + "onContentLoad": 63.44699999317527, + "onLoad": 447.70199997583404 + } + } + ], + "entries": [ + { + "_initiator": { + "type": "other" + }, + "_priority": "VeryHigh", + "_resourceType": "document", + "cache": {}, + "pageref": "page_1", + "request": { + "method": "GET", + "url": "https://mitmproxy.org/", + "httpVersion": "http/2.0", + "headers": [ + { + "name": ":authority", + "value": "mitmproxy.org" + }, + { + "name": ":method", + "value": "GET" + }, + { + "name": ":path", + "value": "/" + }, + { + "name": ":scheme", + "value": "https" + }, + { + "name": "accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate, br" + }, + { + "name": "accept-language", + "value": "en-US,en;q=0.9" + }, + { + "name": "cache-control", + "value": "max-age=0" + }, + { + "name": "dnt", + "value": "1" + }, + { + "name": "if-modified-since", + "value": "Sat, 04 Mar 2023 18:02:08 GMT" + }, + { + "name": "if-none-match", + "value": "W/\"a1550c2bd25c5bcfef789d730f5bbddf\"" + }, + { + "name": "referer", + "value": "https://www.google.com/" + }, + { + "name": "sec-ch-ua", + "value": "\"Chromium\";v=\"110\", \"Not A(Brand\";v=\"24\", \"Google Chrome\";v=\"110\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + }, + { + "name": "sec-fetch-dest", + "value": "document" + }, + { + "name": "sec-fetch-mode", + "value": "navigate" + }, + { + "name": "sec-fetch-site", + "value": "cross-site" + }, + { + "name": "sec-fetch-user", + "value": "?1" + }, + { + "name": "upgrade-insecure-requests", + "value": "1" + }, + { + "name": "user-agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36" + } + ], + "queryString": [], + "cookies": [], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 304, + "statusText": "", + "httpVersion": "http/2.0", + "headers": [ + { + "name": "age", + "value": "11391" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "date", + "value": "Thu, 30 Mar 2023 00:00:54 GMT" + }, + { + "name": "etag", + "value": "W/\"a1550c2bd25c5bcfef789d730f5bbddf\"" + }, + { + "name": "server", + "value": "AmazonS3" + }, + { + "name": "vary", + "value": "Accept-Encoding" + }, + { + "name": "via", + "value": "1.1 e758e6512b4c08d28af121962cc722ce.cloudfront.net (CloudFront)" + }, + { + "name": "x-amz-cf-id", + "value": "PaWnOEEyng3O4PBX2lab_qyD2DQ60VajC413wiIbt93eiNkaSKvpkQ==" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-P1" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + } + ], + "cookies": [], + "content": { + "size": 23866, + "mimeType": "text/html", + "text": "PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ImVuLXVzIj4KPGhlYWQ+CiAgICA8bWV0YSBjaGFyc2V0PSJVVEYtOCI+CiAgICA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEuMCI+CiAgICA8bWV0YSBodHRwLWVxdWl2PSJYLVVBLUNvbXBhdGlibGUiIGNvbnRlbnQ9IklFPWVkZ2UiPgoKICAgIDxsaW5rIHJlbD0iaWNvbiIgdHlwZT0iaW1hZ2UvcG5nIiBocmVmPSIuL2Zhdmljb24uaWNvIj4KICAgIDx0aXRsZT5taXRtcHJveHkgLSBhbiBpbnRlcmFjdGl2ZSBIVFRQUyBwcm94eTwvdGl0bGU+CiAgICAKICAgIAogICAgCiAgICA8bGluayByZWw9InN0eWxlc2hlZXQiIGhyZWY9Ii4vc3R5bGUubWluLmNzcyI+CiAgICA8bGluayByZWw9ImFsdGVybmF0ZSIgdHlwZT0iYXBwbGljYXRpb24vcnNzK3htbCIgaHJlZj0iaHR0cHM6Ly9taXRtcHJveHkub3JnL2luZGV4LnhtbCIgdGl0bGU9Im1pdG1wcm94eS5vcmciIC8+CiAgICA8bWV0YSBuYW1lPSJnZW5lcmF0b3IiIGNvbnRlbnQ9Ikh1Z28gMC4xMDQuMyIgLz4KPC9oZWFkPgo8Ym9keT4KPG5hdiBjbGFzcz0ibmF2YmFyIGlzLWRhcmsiIHJvbGU9Im5hdmlnYXRpb24iIGFyaWEtbGFiZWw9Im1haW4gbmF2aWdhdGlvbiI+CiAgICA8ZGl2IGNsYXNzPSJjb250YWluZXIiPgogICAgICAgIDxkaXYgY2xhc3M9Im5hdmJhci1icmFuZCI+CiAgICAgICAgICAgIDxhIGNsYXNzPSJuYXZiYXItaXRlbSIgaHJlZj0iLi8iPgogICAgICAgICAgICAgICAgPGltZyBzcmM9Ii4vbG9nby1uYXZiYXIucG5nIiBhbHQ9Im1pdG1wcm94eSI+CiAgICAgICAgICAgIDwvYT4KICAgICAgICAgICAgPGJ1dHRvbiBjbGFzcz0iYnV0dG9uIG5hdmJhci1idXJnZXIgaXMtZGFyayIgZGF0YS10YXJnZXQ9InRvcG5hdiI+CiAgICAgICAgICAgICAgICA8c3Bhbj48L3NwYW4+CiAgICAgICAgICAgICAgICA8c3Bhbj48L3NwYW4+CiAgICAgICAgICAgICAgICA8c3Bhbj48L3NwYW4+CiAgICAgICAgICAgIDwvYnV0dG9uPgogICAgICAgIDwvZGl2PgogICAgICAgIDxkaXYgaWQ9InRvcG5hdiIgY2xhc3M9Im5hdmJhci1tZW51Ij4KICAgICAgICAgICAgPGRpdiBjbGFzcz0ibmF2YmFyLXN0YXJ0Ij4KICAgICAgICAgICAgICAgIDxhIGNsYXNzPSJuYXZiYXItaXRlbSIgaHJlZj0iLi9wb3N0cyI+CiAgICAgICAgICAgICAgICAgICAgQmxvZwogICAgICAgICAgICAgICAgPC9hPgogICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0ibmF2YmFyLWl0ZW0gaGFzLWRyb3Bkb3duIGlzLWhvdmVyYWJsZSI+CiAgICAgICAgICAgICAgICAgICAgPGEgY2xhc3M9Im5hdmJhci1saW5rIiBocmVmPSJodHRwczovL2RvY3MubWl0bXByb3h5Lm9yZy9zdGFibGUiPgogICAgICAgICAgICAgICAgICAgICAgICBEb2NzCiAgICAgICAgICAgICAgICAgICAgPC9hPgogICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9Im5hdmJhci1kcm9wZG93biBpcy1ib3hlZCI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxhIGNsYXNzPSJuYXZiYXItaXRlbSIgdGFyZ2V0PSJfYmxhbmsiIGhyZWY9Imh0dHBzOi8vZG9jcy5taXRtcHJveHkub3JnL3N0YWJsZSI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB2OSAobGF0ZXN0IHJlbGVhc2UpIDwvYT4KICAgICAgICAgICAgICAgICAgICAgICAgPGEgY2xhc3M9Im5hdmJhci1pdGVtIiB0YXJnZXQ9Il9ibGFuayIgaHJlZj0iaHR0cHM6Ly9kb2NzLm1pdG1wcm94eS5vcmcvYXJjaGl2ZS92OCI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB2ODwvYT4KICAgICAgICAgICAgICAgICAgICAgICAgPGEgY2xhc3M9Im5hdmJhci1pdGVtIiB0YXJnZXQ9Il9ibGFuayIgaHJlZj0iaHR0cHM6Ly9kb2NzLm1pdG1wcm94eS5vcmcvYXJjaGl2ZS92NyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB2NzwvYT4KICAgICAgICAgICAgICAgICAgICAgICAgPGEgY2xhc3M9Im5hdmJhci1pdGVtIiB0YXJnZXQ9Il9ibGFuayIgaHJlZj0iaHR0cHM6Ly9kb2NzLm1pdG1wcm94eS5vcmcvYXJjaGl2ZS92NiI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB2NjwvYT4KICAgICAgICAgICAgICAgICAgICAgICAgPGEgY2xhc3M9Im5hdmJhci1pdGVtIiB0YXJnZXQ9Il9ibGFuayIgaHJlZj0iaHR0cHM6Ly9kb2NzLm1pdG1wcm94eS5vcmcvYXJjaGl2ZS92NSI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB2NTwvYT4KICAgICAgICAgICAgICAgICAgICAgICAgPGEgY2xhc3M9Im5hdmJhci1pdGVtIiB0YXJnZXQ9Il9ibGFuayIgaHJlZj0iaHR0cHM6Ly9kb2NzLm1pdG1wcm94eS5vcmcvYXJjaGl2ZS92NCI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB2NDwvYT4KICAgICAgICAgICAgICAgICAgICAgICAgPGEgY2xhc3M9Im5hdmJhci1pdGVtIiB0YXJnZXQ9Il9ibGFuayIgaHJlZj0iaHR0cHM6Ly9kb2NzLm1pdG1wcm94eS5vcmcvYXJjaGl2ZS92MyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB2MzwvYT4KICAgICAgICAgICAgICAgICAgICAgICAgPGEgY2xhc3M9Im5hdmJhci1pdGVtIiB0YXJnZXQ9Il9ibGFuayIgaHJlZj0iaHR0cHM6Ly9kb2NzLm1pdG1wcm94eS5vcmcvYXJjaGl2ZS92MiI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB2MjwvYT4KICAgICAgICAgICAgICAgICAgICAgICAgPGEgY2xhc3M9Im5hdmJhci1pdGVtIiB0YXJnZXQ9Il9ibGFuayIgaHJlZj0iaHR0cHM6Ly9kb2NzLm1pdG1wcm94eS5vcmcvYXJjaGl2ZS92MSI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB2MTwvYT4KCiAgICAgICAgICAgICAgICAgICAgICAgIDxociBjbGFzcz0ibmF2YmFyLWRpdmlkZXIiPgoKICAgICAgICAgICAgICAgICAgICAgICAgPGEgY2xhc3M9Im5hdmJhci1pdGVtIiB0YXJnZXQ9Il9ibGFuayIgaHJlZj0iaHR0cHM6Ly9kb2NzLm1pdG1wcm94eS5vcmcvZGV2Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRldjwvYT4KICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgPGEgY2xhc3M9Im5hdmJhci1pdGVtIiBocmVmPSIuL3B1YmxpY2F0aW9ucyI+CiAgICAgICAgICAgICAgICAgICAgUHVibGljYXRpb25zCiAgICAgICAgICAgICAgICA8L2E+CiAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8ZGl2IGNsYXNzPSJuYXZiYXItZW5kIj4KICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9Im5hdmJhci1pdGVtIGlzLWhpZGRlbi10b3VjaCI+CiAgICAgICAgICAgICAgICAgICAgPGlmcmFtZSBzcmM9Ii4vZ2l0aHViLWJ0bi5odG1sP3VzZXI9bWhpbHMmdHlwZT1zcG9uc29yJnNpemU9bGFyZ2UiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBmcmFtZWJvcmRlcj0iMCIKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNjcm9sbGluZz0iMCIKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHdpZHRoPSIxMTUiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBoZWlnaHQ9IjMwIj48L2lmcmFtZT4KICAgICAgICAgICAgICAgICAgICA8aWZyYW1lIHNyYz0iLi9naXRodWItYnRuLmh0bWw/dXNlcj1taXRtcHJveHkmcmVwbz1taXRtcHJveHkmdHlwZT1zdGFyJmNvdW50PXRydWUmc2l6ZT1sYXJnZSIKICAgICAgICAgICAgICAgICAgICAgICAgICAgIGZyYW1lYm9yZGVyPSIwIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgc2Nyb2xsaW5nPSIwIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgd2lkdGg9IjE2MCIKICAgICAgICAgICAgICAgICAgICAgICAgICAgIGhlaWdodD0iMzAiPjwvaWZyYW1lPgogICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgIDwvZGl2PgogICAgICAgIDwvZGl2PgogICAgPC9kaXY+CjwvbmF2Pgo8c2NyaXB0PgogICAgZG9jdW1lbnQuYWRkRXZlbnRMaXN0ZW5lcignRE9NQ29udGVudExvYWRlZCcsIGZ1bmN0aW9uICgpIHsKCiAgICAgICAgCiAgICAgICAgdmFyICRuYXZiYXJCdXJnZXJzID0gQXJyYXkucHJvdG90eXBlLnNsaWNlLmNhbGwoZG9jdW1lbnQucXVlcnlTZWxlY3RvckFsbCgnLm5hdmJhci1idXJnZXInKSwgMCk7CgogICAgICAgIAogICAgICAgIGlmICgkbmF2YmFyQnVyZ2Vycy5sZW5ndGggPiAwKSB7CgogICAgICAgICAgICAKICAgICAgICAgICAgJG5hdmJhckJ1cmdlcnMuZm9yRWFjaChmdW5jdGlvbiAoJGVsKSB7CiAgICAgICAgICAgICAgICAkZWwuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCBmdW5jdGlvbiAoKSB7CgogICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgIHZhciB0YXJnZXQgPSAkZWwuZGF0YXNldC50YXJnZXQ7CiAgICAgICAgICAgICAgICAgICAgdmFyICR0YXJnZXQgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCh0YXJnZXQpOwoKICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAkZWwuY2xhc3NMaXN0LnRvZ2dsZSgnaXMtYWN0aXZlJyk7CiAgICAgICAgICAgICAgICAgICAgJHRhcmdldC5jbGFzc0xpc3QudG9nZ2xlKCdpcy1hY3RpdmUnKTsKCiAgICAgICAgICAgICAgICB9KTsKICAgICAgICAgICAgfSk7CiAgICAgICAgfQoKICAgIH0pOwo8L3NjcmlwdD4KCjxkaXYgaWQ9ImhvbWUiPgogICAgPHNlY3Rpb24gaWQ9ImludHJvIj4KICAgICAgICA8ZGl2IGNsYXNzPSJoZXJvLWJvZHkiPgogICAgICAgICAgICA8ZGl2IGNsYXNzPSJjb250YWluZXIiPgogICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY29sdW1ucyI+CiAgICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY29sdW1uIGlzLTciPgogICAgICAgICAgICAgICAgICAgICAgICA8aW1nIHNyYz0iLi9zY3JlZW5zaG90LnBuZyIgYWx0PSJzY3JlZW5zaG90IG9mIG1pdG1wcm94eSdzIGludGVyZmFjZSI+CiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY29sdW1uIGlzLWhpZGRlbi1tb2JpbGUiPjwvZGl2PgogICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbHVtbiBpcy10d28tZmlmdGhzIj4KICAgICAgICAgICAgICAgICAgICAgICAgPGRpdj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxoMSBjbGFzcz0iaXMtc2l6ZS0zIj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGZuIGlkPSJkZWZpbml0aW9uIj5taXRtcHJveHk8L2Rmbj4gaXMgYSBmcmVlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYW5kIG9wZW4gc291cmNlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaW50ZXJhY3RpdmUgSFRUUFMgcHJveHkuCiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2gxPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPGJyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdiBpZD0iaW5zdGFsbCIgcm9sZT0ibmF2aWdhdGlvbiIgYXJpYS1sYWJlbD0iSW5zdGFsbGF0aW9uIj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YSBpZD0iaW5zdGFsbC13aW5kb3dzLWNsYXNzaWMiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3M9ImJ1dHRvbiBpcy1oaWRkZW4gaXMtcHJpbWFyeSBpcy1tZWRpdW0iCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaHJlZj0iLi9kb3dubG9hZHMiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBEb3dubG9hZCBXaW5kb3dzIEluc3RhbGxlcgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvYT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxhIGlkPSJpbnN0YWxsLXdpbmRvd3Mtc3RvcmUiIGNsYXNzPSJidXR0b24gaXMtaGlkZGVuIGlzLWJsYWNrIiBocmVmPSJtcy13aW5kb3dzLXN0b3JlOi8vcGRwLz9Qcm9kdWN0SWQ9OU5XTkRMUU1OWkQ3Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHNwYW4gY2xhc3M9Imljb24iPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8c3ZnIHdpZHRoPTIwIGhlaWdodD0yMCB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMyAyMyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cGF0aCBmaWxsPSIjZjM1MzI1IiBkPSJNMSAxaDEwdjEwSDF6Ii8+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cGF0aCBmaWxsPSIjODFiYzA2IiBkPSJNMTIgMWgxMHYxMEgxMnoiLz4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxwYXRoIGZpbGw9IiMwNWE2ZjAiIGQ9Ik0xIDEyaDEwdjEwSDF6Ii8+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cGF0aCBmaWxsPSIjZmZiYTA4IiBkPSJNMTIgMTJoMTB2MTBIMTJ6Ii8+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvc3ZnPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3NwYW4+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxzcGFuPkdldCBmcm9tIE1pY3Jvc29mdCBTdG9yZTwvc3Bhbj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2E+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGEgaWQ9Imluc3RhbGwtbGludXgiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3M9ImJ1dHRvbiBpcy1oaWRkZW4gaXMtcHJpbWFyeSBpcy1tZWRpdW0iCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaHJlZj0iLi9kb3dubG9hZHMiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBEb3dubG9hZCBMaW51eCBCaW5hcmllcwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvYT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cHJlIGlkPSJpbnN0YWxsLW1hY29zIiBjbGFzcz0ic2hlbGwtY29tbWFuZCBpcy1oaWRkZW4iPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8Y29kZT5icmV3IGluc3RhbGwgbWl0bXByb3h5PC9jb2RlPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8Y29kZSBjbGFzcz0iY29weSBpcy1oaWRkZW4tdG91Y2giCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRhdGEtY2xpcGJvYXJkLXRleHQ9ImJyZXcgaW5zdGFsbCBtaXRtcHJveHkiPmNvcHk8L2NvZGU+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9wcmU+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGEgaWQ9Imluc3RhbGwtZG9ja2VyIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsYXNzPSJidXR0b24gaXMtaGlkZGVuIGlzLWxpbmsgaXMtbWVkaXVtIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGhyZWY9Imh0dHBzOi8vaHViLmRvY2tlci5jb20vci9taXRtcHJveHkvbWl0bXByb3h5LyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIERvY2tlciBIdWIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2E+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGEgaWQ9Imluc3RhbGwtbW9yZSIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzcz0iYnV0dG9uIGlzLWhpZGRlbiBpcy1pbmZvIGlzLW1lZGl1bSIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBocmVmPSIuL2Rvd25sb2FkcyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIE1vcmUgRG93bmxvYWRzCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxhIGlkPSJpbnN0YWxsLWRlZmF1bHQiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3M9ImJ1dHRvbiBpcy1wcmltYXJ5IGlzLW1lZGl1bSIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBocmVmPSIuL2Rvd25sb2FkcyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIERvd25sb2FkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YSBjbGFzcz0iaGFzLXRleHQtZGFyayIgaHJlZj0iLi90YWdzL3JlbGVhc2VzLyI+UmVsZWFzZSBOb3RlcyAodjkuMCk8L2E+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHNwYW4+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg4oCTCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGEgaWQ9Im90aGVyLWRvd25sb2FkcyIgY2xhc3M9Imhhcy10ZXh0LWRhcmsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaHJlZj0iLi9kb3dubG9hZHMiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBPdGhlciBEb3dubG9hZHMKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2E+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9zcGFuPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9wPgogICAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L2Rpdj4KICAgIDwvc2VjdGlvbj4KCiAgICA8c2VjdGlvbiBpZD0iZmVhdHVyZXMiPgogICAgICAgIDxkaXYgY2xhc3M9Imhlcm8tYm9keSI+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbnRhaW5lciI+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjb2x1bW5zIiByb2xlPSJuYXZpZ2F0aW9uIiBhcmlhLWxhYmVsPSJGZWF0dXJlcyI+CiAgICAgICAgICAgICAgICAgICAgPGEgaHJlZj0iI21pdG1wcm94eSIgY2xhc3M9ImNvbHVtbiBoYXMtdGV4dC1jZW50ZXJlZCBoYXMtdGV4dC1wcmltYXJ5Ij4KICAgICAgICAgICAgICAgICAgICAgICAgPGkgY2xhc3M9ImZhcyBmYS0yeCBmYS10ZXJtaW5hbCI+PC9pPgogICAgICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJ0aXRsZSI+Q29tbWFuZCBMaW5lPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPC9hPgogICAgICAgICAgICAgICAgICAgIDxhIGhyZWY9IiNtaXRtd2ViIiBjbGFzcz0iY29sdW1uIGhhcy10ZXh0LWNlbnRlcmVkIGhhcy10ZXh0LXByaW1hcnkiPgogICAgICAgICAgICAgICAgICAgICAgICA8aSBjbGFzcz0iZmFiIGZhLTJ4IGZhLWZpcmVmb3gtYnJvd3NlciI+PC9pPgogICAgICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJ0aXRsZSI+V2ViIEludGVyZmFjZTwvZGl2PgogICAgICAgICAgICAgICAgICAgIDwvYT4KICAgICAgICAgICAgICAgICAgICA8YSBocmVmPSIjbWl0bWR1bXAiIGNsYXNzPSJjb2x1bW4gaGFzLXRleHQtY2VudGVyZWQgaGFzLXRleHQtcHJpbWFyeSI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxpIGNsYXNzPSJmYXMgZmEtMnggZmEtY29kZSI+PC9pPgogICAgICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJ0aXRsZSI+UHl0aG9uIEFQSTwvZGl2PgogICAgICAgICAgICAgICAgICAgIDwvYT4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L2Rpdj4KICAgIDwvc2VjdGlvbj4KCiAgICA8c2VjdGlvbiBpZD0ibWl0bXByb3h5IiBjbGFzcz0iZmVhdHVyZSI+CiAgICAgICAgPGRpdiBjbGFzcz0iaGVyby1ib2R5Ij4KICAgICAgICAgICAgPGRpdiBjbGFzcz0iY29udGFpbmVyIj4KICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbHVtbnMgaXMtY2VudGVyZWQiPgogICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbHVtbiI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxpbWcgc3JjPSIuL3NjcmVlbnNob3QucG5nIiBhbHQ9InNjcmVlbnNob3Qgb2YgbWl0bXByb3h5J3MgaW50ZXJmYWNlIj4KICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjb2x1bW4iPgogICAgICAgICAgICAgICAgICAgICAgICA8aDIgY2xhc3M9InRpdGxlIj5Db21tYW5kIExpbmU8L2gyPgogICAgICAgICAgICAgICAgICAgICAgICA8cD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxrYmQ+bWl0bXByb3h5PC9rYmQ+IGlzIHlvdXIgc3dpc3MtYXJteSBrbmlmZSBmb3IgZGVidWdnaW5nLCB0ZXN0aW5nLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgcHJpdmFjeSBtZWFzdXJlbWVudHMsIGFuZCBwZW5ldHJhdGlvbiB0ZXN0aW5nLgogICAgICAgICAgICAgICAgICAgICAgICAgICAgSXQgY2FuIGJlIHVzZWQgdG8gaW50ZXJjZXB0LCBpbnNwZWN0LCBtb2RpZnkgYW5kIHJlcGxheSB3ZWIgdHJhZmZpYyBzdWNoCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBhcwogICAgICAgICAgICAgICAgICAgICAgICAgICAgSFRUUC8xLCBIVFRQLzIsIFdlYlNvY2tldHMsIG9yIGFueSBvdGhlciBTU0wvVExTLXByb3RlY3RlZCBwcm90b2NvbHMuCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBZb3UgY2FuIHByZXR0aWZ5IGFuZCBkZWNvZGUgYSB2YXJpZXR5IG9mIG1lc3NhZ2UgdHlwZXMgcmFuZ2luZyBmcm9tIEhUTUwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRvCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBQcm90b2J1ZiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIGludGVyY2VwdCBzcGVjaWZpYyBtZXNzYWdlcyBvbi10aGUtZmx5LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgbW9kaWZ5IHRoZW0gYmVmb3JlIHRoZXkgcmVhY2ggdGhlaXIgZGVzdGluYXRpb24sIGFuZCByZXBsYXkgdGhlbQogICAgICAgICAgICAgICAgICAgICAgICAgICAgdG8gYSBjbGllbnQgb3Igc2VydmVyIGxhdGVyIG9uLgogICAgICAgICAgICAgICAgICAgICAgICA8L3A+CiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CgogICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgIDwvZGl2PgogICAgICAgIDwvZGl2PgogICAgPC9zZWN0aW9uPgoKICAgIDxzZWN0aW9uIGlkPSJtaXRtd2ViIiBjbGFzcz0iZmVhdHVyZSI+CiAgICAgICAgPGRpdiBjbGFzcz0iaGVyby1ib2R5Ij4KICAgICAgICAgICAgPGRpdiBjbGFzcz0iY29udGFpbmVyIj4KICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbHVtbnMgaXMtY2VudGVyZWQiPgogICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbHVtbiI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxpbWcgc3JjPSIuL21pdG13ZWIucG5nIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsdD0ic2NyZWVuc2hvdCBvZiBtaXRtd2ViJ3MgaW50ZXJmYWNlIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0eWxlPSJib3JkZXI6IHNvbGlkICM5M2ExYTEgMXB4OyI+CiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY29sdW1uIj4KICAgICAgICAgICAgICAgICAgICAgICAgPGgyIGNsYXNzPSJ0aXRsZSI+V2ViIEludGVyZmFjZTwvaDI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxwPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgVXNlIG1pdG1wcm94eSdzIG1haW4gZmVhdHVyZXMgaW4gYSBncmFwaGljYWwgaW50ZXJmYWNlIHdpdGgKICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxrYmQ+bWl0bXdlYjwva2JkPi4gRG8geW91IGxpa2UgQ2hyb21lJ3MgRGV2VG9vbHM/IDxrYmQ+bWl0bXdlYjwva2JkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgZ2l2ZXMKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHlvdSBhIHNpbWlsYXIgZXhwZXJpZW5jZSBmb3IgYW55IG90aGVyIGFwcGxpY2F0aW9uIG9yIGRldmljZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBsdXMgYWRkaXRpb25hbCBmZWF0dXJlcyBzdWNoIGFzIHJlcXVlc3QgaW50ZXJjZXB0aW9uIGFuZCByZXBsYXkuCgogICAgICAgICAgICAgICAgICAgICAgICA8L3A+CiAgICAgICAgICAgICAgICAgICAgICAgIDwhLS0KICAgICAgICAgICAgICAgICAgICAgICAgPGEgY2xhc3M9ImJ1dHRvbiBpcy1pbmZvIGlzLW91dGxpbmVkIgogICAgICAgICAgICAgICAgICAgICAgICAgICBocmVmPSJodHRwOi8vc2hhcmUubWl0bXByb3h5Lm9yZy9ob25leW5ldC1kZW1vLyMvZmxvd3MvYjA3YzU1M2ItMWZmZC00NDE0LTkyNGYtY2E0NDEyODA0YTZkL3Jlc3BvbnNlIj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIFZpZXcgU3RhdGljIERlbW8KICAgICAgICAgICAgICAgICAgICAgICAgPC9hPi0tPgogICAgICAgICAgICAgICAgICAgIDwvZGl2PgoKICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L2Rpdj4KICAgIDwvc2VjdGlvbj4KCiAgICA8c2VjdGlvbiBpZD0ibWl0bWR1bXAiIGNsYXNzPSJmZWF0dXJlIj4KICAgICAgICA8ZGl2IGNsYXNzPSJoZXJvLWJvZHkiPgogICAgICAgICAgICA8ZGl2IGNsYXNzPSJjb250YWluZXIiPgogICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY29sdW1ucyBpcy1jZW50ZXJlZCI+CiAgICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY29sdW1uIj4KICAgICAgICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0ic2FtcGxlLWNvZGUiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iZmlsZW5hbWUiPmFkZG9uLnB5PC9kaXY+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cHJlPjxzcGFuCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsYXNzPSJpbXBvcnQiPmZyb208L3NwYW4+IG1pdG1wcm94eSA8c3BhbgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzcz0iaW1wb3J0Ij5pbXBvcnQ8L3NwYW4+IGh0dHAKCjxzcGFuIGNsYXNzPSJrZXl3b3JkIj5kZWY8L3NwYW4+IDxzcGFuIGNsYXNzPSJtZXRob2QiPnJlcXVlc3Q8L3NwYW4+KGZsb3c6IGh0dHAuSFRUUEZsb3cpOgogICAgPHNwYW4gY2xhc3M9ImNvbW1lbnQiPiMgcmVkaXJlY3QgdG8gZGlmZmVyZW50IGhvc3Q8L3NwYW4+CiAgICA8c3BhbiBjbGFzcz0ia2V5d29yZCI+aWY8L3NwYW4+IGZsb3cucmVxdWVzdC5wcmV0dHlfaG9zdCA9PSA8c3BhbgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3M9InN0ciI+ImV4YW1wbGUuY29tIjwvc3Bhbj46CiAgICAgICAgZmxvdy5yZXF1ZXN0Lmhvc3QgPSA8c3BhbiBjbGFzcz0ic3RyIj4ibWl0bXByb3h5Lm9yZyI8L3NwYW4+CiAgICA8c3BhbiBjbGFzcz0iY29tbWVudCI+IyBhbnN3ZXIgZnJvbSBwcm94eTwvc3Bhbj4KICAgIDxzcGFuIGNsYXNzPSJrZXl3b3JkIj5lbGlmPC9zcGFuPiBmbG93LnJlcXVlc3QucGF0aC5lbmRzd2l0aCg8c3BhbiBjbGFzcz0ic3RyIj4iL2JyZXciPC9zcGFuPik6CiAgICAJZmxvdy5yZXNwb25zZSA9IGh0dHAuUmVzcG9uc2UubWFrZSgKICAgICAgICAgICAgPHNwYW4gY2xhc3M9Im51bSI+NDE4PC9zcGFuPiwgPHN0cm9uZz5iPC9zdHJvbmc+PHNwYW4gY2xhc3M9InN0ciI+IkknbSBhIHRlYXBvdCI8L3NwYW4+LAogICAgICAgICk8L3ByZT4KICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY29sdW1uIj4KICAgICAgICAgICAgICAgICAgICAgICAgPGgyIGNsYXNzPSJ0aXRsZSI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBQeXRob24gQVBJCiAgICAgICAgICAgICAgICAgICAgICAgIDwvaDI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxwPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgV3JpdGUgcG93ZXJmdWwgYWRkb25zIGFuZCBzY3JpcHQgbWl0bXByb3h5IHdpdGggPGtiZD5taXRtZHVtcDwva2JkPi4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIFRoZSBzY3JpcHRpbmcgQVBJIG9mZmVycyBmdWxsIGNvbnRyb2wgb3ZlciBtaXRtcHJveHkgYW5kIG1ha2VzIGl0CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBwb3NzaWJsZQogICAgICAgICAgICAgICAgICAgICAgICAgICAgdG8gYXV0b21hdGljYWxseSBtb2RpZnkgbWVzc2FnZXMsIHJlZGlyZWN0IHRyYWZmaWMsIHZpc3VhbGl6ZSBtZXNzYWdlcywKICAgICAgICAgICAgICAgICAgICAgICAgICAgIG9yCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBpbXBsZW1lbnQgY3VzdG9tIGNvbW1hbmRzLgogICAgICAgICAgICAgICAgICAgICAgICA8L3A+CiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CiAgICA8L3NlY3Rpb24+CgogICAgPHNlY3Rpb24gaWQ9ImVjb3N5c3RlbSIgY2xhc3M9ImZlYXR1cmUiPgogICAgICAgIDxkaXYgY2xhc3M9Imhlcm8tYm9keSI+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbnRhaW5lciI+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjb2x1bW5zIGlzLWNlbnRlcmVkIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjb2x1bW4iPgogICAgICAgICAgICAgICAgICAgICAgICA8ZGl2IHN0eWxlPSJtYXJnaW46IDAgYXV0byI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cCBjbGFzcz0iaXMtc2l6ZS01Ij48aSBjbGFzcz0iZmFiIGZhLXR3aXR0ZXIgaGFzLXRleHQtaW5mbyI+PC9pPiBMYXRlc3QKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBUd2VldHM8L3A+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2IHN0eWxlPSJoZWlnaHQ6IDUwMHB4OyBvdmVyZmxvdy15OiBzY3JvbGwiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxhIGhyZWY9Imh0dHBzOi8vdHdpdHRlci5jb20vbWl0bXByb3h5L2xpa2VzIiB0YXJnZXQ9Il9ibGFuayIgYXJpYS1sYWJlbD0iT3BlbiBsYXRlc3QgbGlrZWQgdHdlZXRzIGJ5IG1pdG1wcm94eSBpbiBuZXcgd2luZG93LiI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGltZyBzdHlsZT0id2lkdGg6IDEwMCU7IG1heC13aWR0aDogNDMwcHgiIGFsdD0iQSBzY3JlZW5zaG90IG9mIHRoZSBsYXRlc3QgbGlrZWQgdHdlZXRzIGJ5IEBtaXRtcHJveHkuIiBzcmM9Ii4vZGF0YS90d2l0dGVyLXRpbWVsaW5lLnBuZyIgbG9hZGluZz0ibGF6eSIgdGl0bGU9IvCflrzvuI8gVGhpcyBpcyBhIHNjcmVlbnNob3QgdG8gcHJvdGVjdCB5b3VyIHByaXZhY3kuIOKcqCI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbHVtbiI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxoMiBjbGFzcz0idGl0bGUiPlBvd2VyZnVsIEVjb3N5c3RlbTwvaDI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbnRlbnQiIHJvbGU9Im5hdmlnYXRpb24iIGFyaWEtbGFiZWw9IkVjb3N5c3RlbSI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBNaXRtcHJveHkgaGFzIGEgdmlicmFudCBlY29zeXN0ZW0gb2YgYWRkb25zIGFuZCB0b29scyBidWlsZGluZyBvbiBpdDoKICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvcD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx1bD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8bGk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxhIGhyZWY9Imh0dHBzOi8vZ2l0aHViLmNvbS9taXRtcHJveHkvbWl0bXByb3h5L3RyZWUvbWFpbi9leGFtcGxlcy9jb250cmliIiBjbGFzcz0iaGFzLXRleHQtbGluayI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1pdG1wcm94eS9leGFtcGxlcy9jb250cmliPC9hPiwgYSBjb2xsZWN0aW9uIG9mCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNvbW11bml0eS1jb250cmlidXRlZCBtaXRtcHJveHkgYWRkb25zLgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvbGk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGxpPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YSBocmVmPSJodHRwczovL2dpdGh1Yi5jb20vYWx1ZmVycy9taXRtcHJveHkyc3dhZ2dlciIgY2xhc3M9Imhhcy10ZXh0LWxpbmsiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtaXRtcHJveHkyc3dhZ2dlcjwvYT4sIGEgdG9vbCBmb3IgYXV0b21hdGljYWxseSBjb252ZXJ0aW5nIG1pdG1wcm94eSBjYXB0dXJlcyB0bwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBPcGVuQVBJIDMuMCBzcGVjaWZpY2F0aW9ucy4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2xpPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxsaT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGEgaHJlZj0iaHR0cHM6Ly9naXRodWIuY29tL3NvbHVibGUtYWkva3ViZXRhcCIgY2xhc3M9Imhhcy10ZXh0LWxpbmsiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBrdWJldGFwPC9hPiwgYSBrdWJlY3RsIHBsdWdpbiB0byBpbnRlcmFjdGl2ZWx5IHByb3h5IEt1YmVybmV0ZXMgU2VydmljZXMuCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9saT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdWw+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICAgICAgICA8aDEgY2xhc3M9InRpdGxlIGlzLTQiPlNwb25zb3JlZCBCeTwvaDE+CiAgICAgICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9InNwb25zb3JzIj4KICAgICAgICAgICAgICAgICAgICAgICAgPGEgaHJlZj0iaHR0cHM6Ly9wcm94eW1hbi5pby8iPjxpbWcgYWx0PSJQcm94eW1hbiIgc3JjPSIuL3Nwb25zb3JzL3Byb3h5bWFuLnBuZyI+PC9hPgogICAgICAgICAgICAgICAgICAgICAgICA8YSBocmVmPSJodHRwczovL25ldG9ncmFwaC5pby8iPjxpbWcgYWx0PSJOZXRvZ3JhcGguaW8iIHNyYz0iLi9zcG9uc29ycy9uZXRvZ3JhcGguc3ZnIj48L2E+CiAgICAgICAgICAgICAgICAgICAgICAgIDxzcGFuIHN0eWxlPSJtYXJnaW4tbGVmdDogLjRlbSI+Li4uYW5kIG1hbnkgPGEgaHJlZj0iaHR0cHM6Ly9naXRodWIuY29tL3Nwb25zb3JzL21oaWxzIiBjbGFzcz0iaGFzLXRleHQtbGluayI+aW5kaXZpZHVhbCBzdXBwb3J0ZXJzITwvYT4g4p2k77iPPC9zcGFuPgogICAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L2Rpdj4KICAgIDwvc2VjdGlvbj4KCiAgICA8c2VjdGlvbiBpZD0ib3BlbnNvdXJjZSIgY2xhc3M9ImZlYXR1cmUiPgogICAgICAgIDxkaXYgY2xhc3M9Imhlcm8tYm9keSI+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbnRhaW5lciI+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjb2x1bW5zIGlzLWNlbnRlcmVkIj4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjb2x1bW4gaGFzLXRleHQtY2VudGVyZWQiPgogICAgICAgICAgICAgICAgICAgICAgICA8aSBjbGFzcz0iZmFzIGZhLWNvZGUgZmEtOHggaGFzLXRleHQtZGFuZ2VyIj48L2k+CiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY29sdW1uIj4KICAgICAgICAgICAgICAgICAgICAgICAgPGgyIGNsYXNzPSJ0aXRsZSI+T3BlbiBTb3VyY2U8L2gyPgogICAgICAgICAgICAgICAgICAgICAgICA8cD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIE1pdG1wcm94eSBpcyBmcmVlIGFuZCBvcGVuIHNvdXJjZS4gQmUgcGFydCBvZiB0aGUgbWl0bXByb3h5IGNvbW11bml0eQogICAgICAgICAgICAgICAgICAgICAgICAgICAgYW5kCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBoZWxwIGltcHJvdmUgeW91ciBmYXZvcml0ZSBIVFRQUyBwcm94eS4KICAgICAgICAgICAgICAgICAgICAgICAgPC9wPgogICAgICAgICAgICAgICAgICAgICAgICA8YnI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImJ1dHRvbnMiIHJvbGU9Im5hdmlnYXRpb24iIGFyaWEtbGFiZWw9Ik9wZW4gU291cmNlIEN1bW11bml0eSI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YSBjbGFzcz0iYnV0dG9uIGlzLWRhcmsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBocmVmPSJodHRwczovL2dpdGh1Yi5jb20vbWl0bXByb3h5L21pdG1wcm94eSI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8c3BhbiBjbGFzcz0iaWNvbiI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGkgY2xhc3M9ImZhYiBmYS1naXRodWIiPjwvaT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvc3Bhbj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8c3Bhbj5HaXRIdWI8L3NwYW4+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2E+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YSBjbGFzcz0iYnV0dG9uIGlzLWluZm8iIGhyZWY9Imh0dHBzOi8vZ2l0aHViLmNvbS9taXRtcHJveHkvbWl0bXByb3h5L2Rpc2N1c3Npb25zIj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxzcGFuIGNsYXNzPSJpY29uIj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8aSBjbGFzcz0iZmFyIGZhLWNvbW1lbnRzIj48L2k+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3NwYW4+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHNwYW4+QXNrIFF1ZXN0aW9uczwvc3Bhbj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvYT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxhIGNsYXNzPSJidXR0b24gaXMtZGFuZ2VyIiBocmVmPSJodHRwczovL3NsYWNrLm1pdG1wcm94eS5vcmciPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPHNwYW4gY2xhc3M9Imljb24iPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxpIGNsYXNzPSJmYWIgZmEtc2xhY2staGFzaCI+PC9pPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9zcGFuPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxzcGFuPkRldmVsb3BlciBDaGF0PC9zcGFuPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hPgogICAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L2Rpdj4KICAgIDwvc2VjdGlvbj4KPC9kaXY+CjxzY3JpcHQgc3JjPSIuL3BvbHlmaWxscy5qcyI+PC9zY3JpcHQ+CjxzY3JpcHQgc3JjPSIuL2NsaXBib2FyZC5taW4uanMiPjwvc2NyaXB0Pgo8c2NyaXB0IHNyYz0iLi9zbmFwc2hvdHMuanMiPjwvc2NyaXB0Pgo8c2NyaXB0PgogICAgZG9jdW1lbnQuZ2V0RWxlbWVudHNCeUNsYXNzTmFtZSgiY29weSIpCiAgICAvLyBDb3B5CiAgICBsZXQgY2xpcGJvYXJkID0gbmV3IENsaXBib2FyZEpTKCcuY29weScpOwogICAgY2xpcGJvYXJkLm9uKCdzdWNjZXNzJywgZnVuY3Rpb24gKGUpIHsKICAgICAgICBsZXQgbm9kZSA9IGUudHJpZ2dlcjsKICAgICAgICBub2RlLmNsYXNzTGlzdC5hZGQoImhhcy10ZXh0LXN1Y2Nlc3MiKTsKICAgICAgICBub2RlLnRleHRDb250ZW50ID0gImNvcGllZCEiOwogICAgICAgIHdpbmRvdy5zZXRUaW1lb3V0KGZ1bmN0aW9uKCl7CiAgICAgICAgICAgIG5vZGUuY2xhc3NMaXN0LnJlbW92ZSgiaGFzLXRleHQtc3VjY2VzcyIpCiAgICAgICAgICAgIG5vZGUudGV4dENvbnRlbnQgPSAiY29weSIKICAgICAgICB9LCAxMDAwKQogICAgfSk7CgogICAgZ2V0TGF0ZXN0UmVsZWFzZSgiLXdpbmRvd3MteDY0LWluc3RhbGxlci5leGUiKS50aGVuKGZ1bmN0aW9uICh1cmwpIHsKICAgICAgICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiaW5zdGFsbC13aW5kb3dzLWNsYXNzaWMiKS5ocmVmID0gdXJsOwogICAgfSkKICAgIGdldExhdGVzdFJlbGVhc2UoIi1saW51eC50YXIuZ3oiKS50aGVuKGZ1bmN0aW9uICh1cmwpIHsKICAgICAgICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiaW5zdGFsbC1saW51eCIpLmhyZWYgPSB1cmw7CiAgICB9KQoKICAgIC8vIE9TIERldGVjdGlvbgogICAgbGV0IHBsYXRmb3JtID0gd2luZG93Lm5hdmlnYXRvci5wbGF0Zm9ybS50b0xvd2VyQ2FzZSgpOwogICAgaWYgKHBsYXRmb3JtLmluY2x1ZGVzKCJ3aW4iKSkgewogICAgICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJpbnN0YWxsLWRlZmF1bHQiKS5jbGFzc0xpc3QuYWRkKCJpcy1oaWRkZW4iKTsKICAgICAgICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiaW5zdGFsbC13aW5kb3dzLWNsYXNzaWMiKS5jbGFzc0xpc3QucmVtb3ZlKCJpcy1oaWRkZW4iKTsKICAgICAgICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiaW5zdGFsbC13aW5kb3dzLXN0b3JlIikuY2xhc3NMaXN0LnJlbW92ZSgiaXMtaGlkZGVuIik7CiAgICB9IGVsc2UgaWYgKHBsYXRmb3JtLmluY2x1ZGVzKCJsaW51eCIpICYmICFwbGF0Zm9ybS5pbmNsdWRlcygiYW5kcm9pZCIpKSB7CiAgICAgICAgZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImluc3RhbGwtZGVmYXVsdCIpLmNsYXNzTGlzdC5hZGQoImlzLWhpZGRlbiIpOwogICAgICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJpbnN0YWxsLWxpbnV4IikuY2xhc3NMaXN0LnJlbW92ZSgiaXMtaGlkZGVuIik7CiAgICB9IGVsc2UgaWYgKHBsYXRmb3JtLmluY2x1ZGVzKCJtYWMiKSAmJiAhcGxhdGZvcm0uaW5jbHVkZXMoImlQaG9uZSIpICYmICFwbGF0Zm9ybS5pbmNsdWRlcygiaVBhZCIpKSB7CiAgICAgICAgZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImluc3RhbGwtZGVmYXVsdCIpLmNsYXNzTGlzdC5hZGQoImlzLWhpZGRlbiIpOwogICAgICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJpbnN0YWxsLW1hY29zIikuY2xhc3NMaXN0LnJlbW92ZSgiaXMtaGlkZGVuIik7CiAgICB9CgogICAgbGV0IGluc3RhbGwgPSBkb2N1bWVudC5xdWVyeVNlbGVjdG9yKCIjaW5zdGFsbCIpOwogICAgaW5zdGFsbC5zdHlsZS5oZWlnaHQgPSBpbnN0YWxsLmNsaWVudEhlaWdodCArICJweCI7CiAgICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgib3RoZXItZG93bmxvYWRzIikuYWRkRXZlbnRMaXN0ZW5lcigiY2xpY2siLCBmdW5jdGlvbiAoZSkgewogICAgICAgIGUucHJldmVudERlZmF1bHQoKTsKICAgICAgICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgib3RoZXItZG93bmxvYWRzIikucGFyZW50RWxlbWVudC5jbGFzc0xpc3QuYWRkKCJpcy1oaWRkZW4iKTsKICAgICAgICBpbnN0YWxsLnN0eWxlLmhlaWdodCA9ICIwcHgiOwogICAgICAgIHdpbmRvdy5zZXRUaW1lb3V0KGZ1bmN0aW9uICgpIHsKICAgICAgICAgICAgZG9jdW1lbnQucXVlcnlTZWxlY3RvckFsbCgiI2luc3RhbGwgPiAqIikuZm9yRWFjaChmdW5jdGlvbiAoeCkgewogICAgICAgICAgICAgICAgeC5jbGFzc0xpc3QucmVtb3ZlKCJpcy1oaWRkZW4iKTsKICAgICAgICAgICAgfSkKICAgICAgICAgICAgZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImluc3RhbGwtZGVmYXVsdCIpLmNsYXNzTGlzdC5hZGQoImlzLWhpZGRlbiIpOwogICAgICAgICAgICBpbnN0YWxsLnN0eWxlLmhlaWdodCA9IGluc3RhbGwuc2Nyb2xsSGVpZ2h0ICsgInB4IjsKICAgICAgICB9LCAzMDApOwogICAgfSkKPC9zY3JpcHQ+Cgo8Zm9vdGVyIGNsYXNzPSJmb290ZXIiPgogICAgPGRpdiBjbGFzcz0iY29udGFpbmVyIj4KICAgICAgICA8ZGl2IGNsYXNzPSJjb250ZW50Ij4KICAgICAgICAgICAgPGRpdiBjbGFzcz0ibGV2ZWwiPgogICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0ibGV2ZWwtbGVmdCI+CiAgICAgICAgICAgICAgICAgICAgPHA+CiAgICAgICAgICAgICAgICAgICAgICAgIDxzdHJvbmc+bWl0bXByb3h5PC9zdHJvbmc+LCBhIHByb2plY3QgYnkKICAgICAgICAgICAgICAgICAgICAgICAgPGEgaHJlZj0iaHR0cHM6Ly90d2l0dGVyLmNvbS9jb3J0ZXNpIj5AY29ydGVzaTwvYT4sCiAgICAgICAgICAgICAgICAgICAgICAgIDxhIGhyZWY9Imh0dHBzOi8vdHdpdHRlci5jb20vbWF4aW1pbGlhbmhpbHMiPkBtYXhpbWlsaWFuaGlsczwvYT4sIGFuZAogICAgICAgICAgICAgICAgICAgICAgICA8YSBocmVmPSJodHRwczovL3R3aXR0ZXIuY29tL3JhdW1mcmVzc2VyIj5AcmF1bWZyZXNzZXI8L2E+Ljxicj4KICAgICAgICAgICAgICAgICAgICAgICAgTWFpbnRhaW5lZCBieSB0aGUgPGEgaHJlZj0iaHR0cHM6Ly9naXRodWIuY29tL29yZ3MvbWl0bXByb3h5L3Blb3BsZSI+Y29yZQogICAgICAgICAgICAgICAgICAgICAgICB0ZWFtPC9hPgogICAgICAgICAgICAgICAgICAgICAgICB3aXRoIHRoZSBoZWxwIG9mIDxhCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBocmVmPSJodHRwczovL2dpdGh1Yi5jb20vbWl0bXByb3h5L21pdG1wcm94eS9ncmFwaHMvY29udHJpYnV0b3JzIj5vdXIgZmFudGFzdGljCiAgICAgICAgICAgICAgICAgICAgICAgIGNvbnRyaWJ1dG9yczwvYT4uPGJyPgogICAgICAgICAgICAgICAgICAgICAgICBDb2RlIGxpY2Vuc2VkIDxhCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBocmVmPSJodHRwczovL2dpdGh1Yi5jb20vbWl0bXByb3h5L21pdG1wcm94eS9ibG9iL21haW4vTElDRU5TRSI+TUlUPC9hPiwKICAgICAgICAgICAgICAgICAgICAgICAgd2Vic2l0ZSDCqSAyMDIzIE1pdG1wcm94eSBQcm9qZWN0LgogICAgICAgICAgICAgICAgICAgICAgICA8YnI+PGJyPgogICAgICAgICAgICAgICAgICAgICAgICA8c21hbGw+QWxzbyBjaGVja291dCA8YSBocmVmPSJodHRwczovL3Bkb2MuZGV2Ij5wZG9jPC9hPiwgYSBQeXRob24gQVBJIGRvY3VtZW50YXRpb24gZ2VuZXJhdG9yIGJ1aWx0IGJ5IHRoZSBtaXRtcHJveHkgZGV2ZWxvcGVycy48L3NtYWxsPgogICAgICAgICAgICAgICAgICAgIDwvcD4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0ibGV2ZWwtcmlnaHQiPgogICAgICAgICAgICAgICAgICAgIDxhIGNsYXNzPSJidXR0b24gaXMtb3V0bGluZWQgaXMtaW5mbyIgaHJlZj0iaHR0cHM6Ly90d2l0dGVyLmNvbS9taXRtcHJveHkiPgogICAgICAgICAgICAgICAgICAgICAgICA8c3BhbiBjbGFzcz0iaWNvbiI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8aSBjbGFzcz0iZmFiIGZhLXR3aXR0ZXIiPjwvaT4KICAgICAgICAgICAgICAgICAgICAgICAgPC9zcGFuPgogICAgICAgICAgICAgICAgICAgICAgICA8c3Bhbj5Gb2xsb3cgQG1pdG1wcm94eTwvc3Bhbj4KICAgICAgICAgICAgICAgICAgICA8L2E+CiAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CiAgICA8L2Rpdj4KPC9mb290ZXI+Cgo8L2JvZHk+CjwvaHRtbD4KCg==", + "encoding": "base64" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": 0, + "_transferSize": 253, + "_error": null + }, + "serverIPAddress": "108.138.246.80", + "startedDateTime": "2023-03-30T00:00:54.250Z", + "time": 14.666000031866133, + "timings": { + "blocked": 5.397000045232475, + "dns": -1, + "ssl": -1, + "connect": -1, + "send": 0.2370000000000001, + "wait": 7.330000007074326, + "receive": 1.7019999795593321, + "_blocked_queueing": 3.293000045232475 + } + }, + { + "_initiator": { + "type": "other" + }, + "_priority": "VeryHigh", + "_resourceType": "document", + "cache": {}, + "pageref": "page_1", + "request": { + "method": "GET", + "url": "https://www.google.com/", + "httpVersion": "", + "headers": [ + { + "name": "DNT", + "value": "1" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Google Chrome\";v=\"114\"" + }, + { + "name": "sec-ch-ua-arch", + "value": "\"arm\"" + }, + { + "name": "sec-ch-ua-bitness", + "value": "\"64\"" + }, + { + "name": "sec-ch-ua-full-version", + "value": "\"114.0.5735.198\"" + }, + { + "name": "sec-ch-ua-full-version-list", + "value": "\"Not.A/Brand\";v=\"8.0.0.0\", \"Chromium\";v=\"114.0.5735.198\", \"Google Chrome\";v=\"114.0.5735.198\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-model", + "value": "\"\"" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + }, + { + "name": "sec-ch-ua-platform-version", + "value": "\"13.2.1\"" + }, + { + "name": "sec-ch-ua-wow64", + "value": "?0" + } + ], + "queryString": [], + "cookies": [], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 0, + "statusText": "", + "httpVersion": "", + "headers": [], + "cookies": [], + "content": { + "size": 0, + "mimeType": "x-unknown" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1, + "_transferSize": 0, + "_error": "net::ERR_INTERNET_DISCONNECTED" + }, + "serverIPAddress": "", + "startedDateTime": "2023-07-13T12:32:32.676Z", + "time": 4.119000000173401, + "timings": { + "blocked": 4.119000000173401, + "dns": -1, + "ssl": -1, + "connect": -1, + "send": 0, + "wait": 0, + "receive": 0, + "_blocked_queueing": -1 + } + }, + { + "_initiator": { + "type": "parser", + "url": "https://www.google.com/", + "lineNumber": 113 + }, + "_priority": "High", + "_resourceType": "image", + "cache": {}, + "pageref": "page_2", + "request": { + "method": "GET", + "url": "https://www.google.com/images/branding/googlelogo/2x/googlelogo_light_color_272x92dp.png", + "httpVersion": "h3", + "headers": [ + { + "name": ":authority", + "value": "www.google.com" + }, + { + "name": ":method", + "value": "GET" + }, + { + "name": ":path", + "value": "/images/branding/googlelogo/2x/googlelogo_light_color_272x92dp.png" + }, + { + "name": ":scheme", + "value": "https" + }, + { + "name": "accept", + "value": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate, br" + }, + { + "name": "accept-language", + "value": "en-US,en;q=0.9" + }, + { + "name": "cache-control", + "value": "no-cache" + }, + { + "name": "cookie", + "value": "SID=YAieF18TdLas-8SKhGKNYvnieZbB1wJ1jf2tW44mAWYKcppPKLvK_Ok7NklKqsURxU82bg.; __Secure-1PSID=YAieF18TdLas-8SKhGKNYvnieZbB1wJ1jf2tW44mAWYKcppP1MvyqChs4flonGDrhu0qfw.; __Secure-3PSID=YAieF18TdLas-8SKhGKNYvnieZbB1wJ1jf2tW44mAWYKcppPqnnM4L77WRxhbrutGn6uEw.; HSID=AOiDlkphfGSlvWKsu; SSID=A_mrp5cpjfkd72x04; APISID=GtMYjIVXfk0omvEX/A1mpBXxfb2x4uc-qq; SAPISID=lTu95MmvfwxU5H_h/Af79riHHI6PPtD3q3; __Secure-1PAPISID=lTu95MmvfwxU5H_h/Af79riHHI6PPtD3q3; __Secure-3PAPISID=lTu95MmvfwxU5H_h/Af79riHHI6PPtD3q3; OGPC=19022552-1:19031986-1:; SEARCH_SAMESITE=CgQI5pgB; AEC=Ad49MVEbr-A-zp-lPUibpASs-qNYts6opSVc67g4tFxAiIcO79t6fv7tSw; OTZ=7131795_48_52_123900_48_436380; 1P_JAR=2023-7-25-11; __Secure-ENID=13.SE=o3Bl7qRCyr6NPgYD4acm8JRn0rHVFaHBm2GJK2uTs7tHS0gwr5E_QfusAwogWPHDkJWCTosdnQbrw5mZIk4a0GdDMLmUgp1c63gYrMAoNoAJYN7hQ-HNT6ztr32WVkImvjtzfI7XxVRC4uRovzZP5X1CLbq5eifYkMGyuQWB4vKok5OBwvnbAJoETbBQ7yaG_vBp3v2Mx7RLCXMHzmb_55e6fogLMGMRw_ilgfQjayO31xAlXhPfFrPsx-R7p9uEu1ka; NID=511=nguIsOmSJcCSka0RAZK37__ia-Rm4RXhcxFnfwfMxyS3IIKf7KKi8QPxqH8MgLl4hFljOeweA_8t_o6JYklI6f6TpenXMrjXP9rFfaflNOp7fwNQX3hDPqOnHANLFsQBpc2Yh56SsDfzVDnjJcGw1rPV3a6N-N4r8e0bdLseNy0OgT8RdVSqoCREsbvn6Q-lmadCfM35a8FToMgl8fP7WXXZRutJKIJBly6os_NDIxJO25qntdadaEhYmujyFCTkGHcbTqtR3VhiEO3L4KZOmmWd9eVnVuqJDPvRt20aNy4c-_tctcvq48qW5SwUE1eiDoN0SFeTFlIIK1P1kchv6Nno4wNWoadtqvIIxMBzrp2ueN5AufcVUP4GcfT3glpuphaMEjcWYi9dN-qdvoIZTDoY2UbjYW5Mv3V7jF1UyZaR7q8BVQTFrHeGoeaHpVYsQP3uKGT5KdmLMWLLyLQ; __Secure-1PSIDTS=sidts-CjEBPu3jIZnNVFcJFxoLH-VBGrYwtBzLlYvkamouuPMziP33rF7t30l_WylizU_03CxbEAA; __Secure-3PSIDTS=sidts-CjEBPu3jIZnNVFcJFxoLH-VBGrYwtBzLlYvkamouuPMziP33rF7t30l_WylizU_03CxbEAA; SIDCC=APoG2W_9UqmUgcCg-einZHGu5g355CBjyEUdjxqwA_6rEuwN_gn337dUXNMjvB3mCSnLWrSgnTA; __Secure-1PSIDCC=APoG2W8WMUYq99_nTjsvPQwHnvITj9AqaBQWvoJrzTikBF9ct4-xVUD6NwEvj-eXsXpvjuHCg2I; __Secure-3PSIDCC=APoG2W-9cTkSJnEPYb1qMKif0ohMbB5jrVPirjfWRynrxpgbjD29BKtUFSbH48Vi2XGqPylzJYs0" + }, + { + "name": "dnt", + "value": "1" + }, + { + "name": "pragma", + "value": "no-cache" + }, + { + "name": "referer", + "value": "https://www.google.com/" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Google Chrome\";v=\"114\"" + }, + { + "name": "sec-ch-ua-arch", + "value": "\"arm\"" + }, + { + "name": "sec-ch-ua-bitness", + "value": "\"64\"" + }, + { + "name": "sec-ch-ua-full-version", + "value": "\"114.0.5735.198\"" + }, + { + "name": "sec-ch-ua-full-version-list", + "value": "\"Not.A/Brand\";v=\"8.0.0.0\", \"Chromium\";v=\"114.0.5735.198\", \"Google Chrome\";v=\"114.0.5735.198\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-model", + "value": "\"\"" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + }, + { + "name": "sec-ch-ua-platform-version", + "value": "\"13.2.1\"" + }, + { + "name": "sec-ch-ua-wow64", + "value": "?0" + }, + { + "name": "sec-fetch-dest", + "value": "image" + }, + { + "name": "sec-fetch-mode", + "value": "no-cors" + }, + { + "name": "sec-fetch-site", + "value": "same-origin" + }, + { + "name": "user-agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + { + "name": "x-client-data", + "value": "CKG1yQEIiLbJAQiltskBCKmdygEI0eXKAQiVocsBCIagzQEI2rTNAQjLtc0BCIa9zQEI3L3NAQi8vs0BCKS/zQEI/r/NAQjnwc0BCLLDzQEY06DNAQ==" + } + ], + "queryString": [], + "cookies": [ + { + "name": "SID", + "value": "YAieF18TdLas-8SKhGKNYvnieZbB1wJ1jf2tW44mAWYKcppPKLvK_Ok7NklKqsURxU82bg.", + "path": "/", + "domain": ".google.com", + "expires": "2024-08-28T10:15:59.461Z", + "httpOnly": false, + "secure": false + }, + { + "name": "__Secure-1PSID", + "value": "YAieF18TdLas-8SKhGKNYvnieZbB1wJ1jf2tW44mAWYKcppP1MvyqChs4flonGDrhu0qfw.", + "path": "/", + "domain": ".google.com", + "expires": "2024-07-31T01:07:02.843Z", + "httpOnly": true, + "secure": true + }, + { + "name": "__Secure-3PSID", + "value": "YAieF18TdLas-8SKhGKNYvnieZbB1wJ1jf2tW44mAWYKcppPqnnM4L77WRxhbrutGn6uEw.", + "path": "/", + "domain": ".google.com", + "expires": "2024-07-31T01:07:02.843Z", + "httpOnly": true, + "secure": true, + "sameSite": "None" + }, + { + "name": "HSID", + "value": "AOiDlkphfGSlvWKsu", + "path": "/", + "domain": ".google.com", + "expires": "2024-07-31T01:07:02.843Z", + "httpOnly": true, + "secure": false + }, + { + "name": "SSID", + "value": "A_mrp5cpjfkd72x04", + "path": "/", + "domain": ".google.com", + "expires": "2024-07-31T01:07:02.843Z", + "httpOnly": true, + "secure": true + }, + { + "name": "APISID", + "value": "GtMYjIVXfk0omvEX/A1mpBXxfb2x4uc-qq", + "path": "/", + "domain": ".google.com", + "expires": "2024-07-31T01:07:02.843Z", + "httpOnly": false, + "secure": false + }, + { + "name": "SAPISID", + "value": "lTu95MmvfwxU5H_h/Af79riHHI6PPtD3q3", + "path": "/", + "domain": ".google.com", + "expires": "2024-07-31T01:07:02.843Z", + "httpOnly": false, + "secure": true + }, + { + "name": "__Secure-1PAPISID", + "value": "lTu95MmvfwxU5H_h/Af79riHHI6PPtD3q3", + "path": "/", + "domain": ".google.com", + "expires": "2024-07-31T01:07:02.844Z", + "httpOnly": false, + "secure": true + }, + { + "name": "__Secure-3PAPISID", + "value": "lTu95MmvfwxU5H_h/Af79riHHI6PPtD3q3", + "path": "/", + "domain": ".google.com", + "expires": "2024-07-31T01:07:02.844Z", + "httpOnly": false, + "secure": true, + "sameSite": "None" + }, + { + "name": "OGPC", + "value": "19022552-1:19031986-1:", + "path": "/", + "domain": ".google.com", + "expires": "2023-07-28T04:30:33.000Z", + "httpOnly": false, + "secure": false + }, + { + "name": "SEARCH_SAMESITE", + "value": "CgQI5pgB", + "path": "/", + "domain": ".google.com", + "expires": "2024-01-16T15:11:10.871Z", + "httpOnly": false, + "secure": false, + "sameSite": "Strict" + }, + { + "name": "AEC", + "value": "Ad49MVEbr-A-zp-lPUibpASs-qNYts6opSVc67g4tFxAiIcO79t6fv7tSw", + "path": "/", + "domain": ".google.com", + "expires": "2024-01-16T14:15:33.684Z", + "httpOnly": true, + "secure": true, + "sameSite": "Lax" + }, + { + "name": "OTZ", + "value": "7131795_48_52_123900_48_436380", + "path": "/", + "domain": "www.google.com", + "expires": "2023-08-23T15:14:55.000Z", + "httpOnly": false, + "secure": true + }, + { + "name": "1P_JAR", + "value": "2023-7-25-11", + "path": "/", + "domain": ".google.com", + "expires": "2023-08-24T11:34:32.000Z", + "httpOnly": false, + "secure": false + }, + { + "name": "__Secure-ENID", + "value": "13.SE=o3Bl7qRCyr6NPgYD4acm8JRn0rHVFaHBm2GJK2uTs7tHS0gwr5E_QfusAwogWPHDkJWCTosdnQbrw5mZIk4a0GdDMLmUgp1c63gYrMAoNoAJYN7hQ-HNT6ztr32WVkImvjtzfI7XxVRC4uRovzZP5X1CLbq5eifYkMGyuQWB4vKok5OBwvnbAJoETbBQ7yaG_vBp3v2Mx7RLCXMHzmb_55e6fogLMGMRw_ilgfQjayO31xAlXhPfFrPsx-R7p9uEu1ka", + "path": "/", + "domain": ".google.com", + "expires": "2024-08-03T21:56:27.103Z", + "httpOnly": true, + "secure": true, + "sameSite": "Lax" + }, + { + "name": "NID", + "value": "511=nguIsOmSJcCSka0RAZK37__ia-Rm4RXhcxFnfwfMxyS3IIKf7KKi8QPxqH8MgLl4hFljOeweA_8t_o6JYklI6f6TpenXMrjXP9rFfaflNOp7fwNQX3hDPqOnHANLFsQBpc2Yh56SsDfzVDnjJcGw1rPV3a6N-N4r8e0bdLseNy0OgT8RdVSqoCREsbvn6Q-lmadCfM35a8FToMgl8fP7WXXZRutJKIJBly6os_NDIxJO25qntdadaEhYmujyFCTkGHcbTqtR3VhiEO3L4KZOmmWd9eVnVuqJDPvRt20aNy4c-_tctcvq48qW5SwUE1eiDoN0SFeTFlIIK1P1kchv6Nno4wNWoadtqvIIxMBzrp2ueN5AufcVUP4GcfT3glpuphaMEjcWYi9dN-qdvoIZTDoY2UbjYW5Mv3V7jF1UyZaR7q8BVQTFrHeGoeaHpVYsQP3uKGT5KdmLMWLLyLQ", + "path": "/", + "domain": ".google.com", + "expires": "2024-01-24T12:11:13.525Z", + "httpOnly": true, + "secure": true, + "sameSite": "None" + }, + { + "name": "__Secure-1PSIDTS", + "value": "sidts-CjEBPu3jIZnNVFcJFxoLH-VBGrYwtBzLlYvkamouuPMziP33rF7t30l_WylizU_03CxbEAA", + "path": "/", + "domain": ".google.com", + "expires": "2024-07-24T12:11:14.623Z", + "httpOnly": true, + "secure": true + }, + { + "name": "__Secure-3PSIDTS", + "value": "sidts-CjEBPu3jIZnNVFcJFxoLH-VBGrYwtBzLlYvkamouuPMziP33rF7t30l_WylizU_03CxbEAA", + "path": "/", + "domain": ".google.com", + "expires": "2024-07-24T12:11:14.623Z", + "httpOnly": true, + "secure": true, + "sameSite": "None" + }, + { + "name": "SIDCC", + "value": "APoG2W_9UqmUgcCg-einZHGu5g355CBjyEUdjxqwA_6rEuwN_gn337dUXNMjvB3mCSnLWrSgnTA", + "path": "/", + "domain": ".google.com", + "expires": "2024-07-24T12:58:46.153Z", + "httpOnly": false, + "secure": false + }, + { + "name": "__Secure-1PSIDCC", + "value": "APoG2W8WMUYq99_nTjsvPQwHnvITj9AqaBQWvoJrzTikBF9ct4-xVUD6NwEvj-eXsXpvjuHCg2I", + "path": "/", + "domain": ".google.com", + "expires": "2024-07-24T12:58:46.153Z", + "httpOnly": true, + "secure": true + }, + { + "name": "__Secure-3PSIDCC", + "value": "APoG2W-9cTkSJnEPYb1qMKif0ohMbB5jrVPirjfWRynrxpgbjD29BKtUFSbH48Vi2XGqPylzJYs0", + "path": "/", + "domain": ".google.com", + "expires": "2024-07-24T12:58:46.153Z", + "httpOnly": true, + "secure": true, + "sameSite": "None" + } + ], + "headersSize": -1, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "h3", + "headers": [ + { + "name": "accept-ranges", + "value": "bytes" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + }, + { + "name": "cache-control", + "value": "private, max-age=31536000" + }, + { + "name": "content-length", + "value": "7108" + }, + { + "name": "content-type", + "value": "image/png" + }, + { + "name": "cross-origin-opener-policy-report-only", + "value": "same-origin; report-to=\"static-on-bigtable\"" + }, + { + "name": "cross-origin-resource-policy", + "value": "cross-origin" + }, + { + "name": "date", + "value": "Tue, 25 Jul 2023 12:58:46 GMT" + }, + { + "name": "expires", + "value": "Tue, 25 Jul 2023 12:58:46 GMT" + }, + { + "name": "last-modified", + "value": "Tue, 22 Oct 2019 18:30:00 GMT" + }, + { + "name": "report-to", + "value": "{\"group\":\"static-on-bigtable\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/static-on-bigtable\"}]}" + }, + { + "name": "server", + "value": "sffe" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "x-xss-protection", + "value": "0" + } + ], + "cookies": [], + "content": { + "size": 7108, + "mimeType": "image/png", + "text": "iVBORw0KGgoAAAANSUhEUgAAAiAAAAC4CAMAAADt/acpAAADAFBMVEUAAAD////////////////////////+/v7////+/v7////+/v7////+/v7+/v7////////////+/v7+/v7////+/v7+/v7+/v7////+/v7+/v7+/v7+/v7+/v7////+/v7////+/v7////+/v7+/v7+/v7+/v7+/v7////+/v7+/v7+/v7+/v7+/v7+/v7+/v7////+/v7+/v7////+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7////+/v7+/v7+/v7////+/v7+/v7+/v7////+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7////+/v7+/v7+/v7+/v7////+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7////+/v7+/v7+/v7+/v7+/v7////+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7////+/v7+/v7+/v7+/v7+/v7+/v7+/v7////9/f39/f39/f39/f39/f39/f39/f3////9/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f3////9/f39/f39/f39/f39/f39/f39/f39/f39/f3////9/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f3////9/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f3////9/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f3////9/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/f3///9Rwuv5AAAA/3RSTlMAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1+f4CBgoOEhYaHiImKi4yNjo+QkZKTlJWWl5iZmpucnZ6foKGio6SlpqeoqaqrrK2ur7CxsrO0tba3uLm6u7y9vr/AwcLDxMXGx8jJysvMzc7P0NHS09TV1tfY2drb3N3e3+Dh4uPk5ebn6Onq6+zt7u/w8fLz9PX29/j5+vv8/f7rCNk1AAAXdElEQVR42uzdiWMV1dkG8GfuvVnIJSGREFZFdpViyy5LBQHBEoOyKYJEUTbr8ili+4ESK2D5BAQVZRWpVkAFUT4pRRShWgsIsgUMUFEEJBhDAgSaQJL7tmW5BOY9M+fMzL2Ey/z+gJdw55k5Z842cIIvrpI/SoPLVVZU4z4jX1u2fs+RUjqt8OC21QsnDOtay43KFc/TeMi8LSdJIG/NlF5V4bpS1Rj0Xh6Z2vVy1xi4rjjXPLmeZB1fkBYN1xUkLv1zUpP38g1wXSHqv3KELFjTwwOXU0gP5UPbDwJk0Y50H1yRHZA2K8mOrN7uq28kB+T6ZWTXupZwRWhAKr9aQvYFXk+GKwIDoqXnkjNy+sIVcQGp+wk5Z1FluCIqINoDBeSkAx3hiqCAVP6AHBbIcAdFIicgLfaS8z5KhCsyAjL4JIXCznpwRUBAvFMoRHLbw3XZB8S/lEKmqAdcl3lAktaStOPZuzd98eWGHdmnSFLJQLgu64BU3UoSTv3j9ZFpDaNwjpbc4q7nPjhApk64b7uXd0Cq7SQzpesndPWDo9UeOD/XzUckByRlB5nY9HgKjHg7zTnq5iNSA5Jk0r4cmdwE5vyDNrn5iMiAxP6NjOSOrgQ5WufP3HxEXkA875KBnBF+KOi0wc1HpAVkHIkFZiSpxm1QjpuPiApIbxLb3BrqKr/l5iOCAtL4OImUjvbBkp65bj4iJSAVtpNIdgdYVeMLNx8REpDpJLKqKqyLesnNR0QEJI1Epnthy9ASNx+Xf0CSDpLAOA02dc9x83HZB2QeCTwO++LguswDcgsJDIHLDQh824n3DFxuQIDHiDfD3VbrBgRAYh6xPvTC5QYEGE+sXRXhcgMCVD1OnJNN4XIDAuAFYj0GlxsQAJWOEmeZ20F1A3La74hz4mq43IAA8O0nzii43ID8Vw/i/NM9CdcNyBl/JU53uNyA/FetADH+DpcbkNOeJM7tcLkBOe1rYmx3zwJyA3JGPeK4O/AjNCBadLQGJf9DjENRKJ/8bdLHvbM+a39+cXH+/qx1C8cOvMkfmvqBovw9X70/ZVi7inBOUpeRc1Zl/VwYoKKc7ctferCJN1wB8TYZOPH9jftP0H+c2PfVe+PvqqdBykpivIRyKCF14rpi0ile+0JqPBwQn/rCWqZ+6bbX7kyAfdFdXtpGF8tfdG9C6APSaMSKY6ST/Xb/BJjynyRGC5Q3FXovLiKhwkW9YmFLbK/FhSR0clm6H3ZorWccJl7hW61wjudOnevtBqT6qG0kUrS4mwfGOhNjl4bypdHsY2Ti6MyGsKzhzKNkomB2E1gVNfBrMrKmA87wkc4f7AWkzaISMrRzcDSM/IEYGShX2sh9jCTwfmtYctOSAMlY+WtY4Ru8l8wsrROSgLRfTea+H6BBbBUxmqMcuf5jkra8EZRdt4KkrfwllN22kySceERzPCB1l5CcfzSBiOcY97T2otyoNKWYFBRPSlCsP5mp7+ARB9WXkKSllZwNiO/3RSSr+BmvKGTE+AjlRtpPpCi7OxSkHiJFh+6AgrvzSNo31zgZkHrrScXnNcDqSYwnUU7ETiMLpsZI13+ZLHjDD0lxc0nFvnrOBaR3gWrw24CTQYyWKB+u20aWbGkIKQ23kCXbG0FK7U2k5rvGDgVEG0fKivqA8SdixKFc+HU+WXS4HSS0O0wW5XeGhOaHSJkzAYn+M1kQeBB63Jl1+1Eu9Ckiywp7wlTPQrKsZABMdTlOlyggMUvJmiHQ2Ud6q1AePBQgGwJDYWJogOx4CCZuK6JLFJCopWRR4B5cRDvFbbdEOTCQbOoPQwPIpiEwdEsRXaKAeP5Mlp3qhAslOPIS8wjZkwKdHiVkU3F3GLjddv1AHxhoVkCXKiBjyYb8BhLDIIPCHZCO+v5pEdlW2AZC7QrJtqJ2EKr1I12qgNxFtmT6UVZzYvQJY0D4p3W1bHLAjykQqHqQHHCoFgRiN9ClCkjDArJnHspqS4zbwh2Q8biQ9zMydHD1nEkZGZNeX21ynT/xguX9lAztXzFr/NNPj5/18QEytDYKvJlkJLDrw2nPjc6YvGBjkeMBiTKM5s7Zwzs3rlu/eY+RC8Q/XV+U0ZEY7cMdkDcUWtHCd9KvRtA1971bSGIZYD1LYgXz+1VDUPV7FhwnsXFg3UliubPSEnFOdPsJe50NyBgS2jumAc7TWrxyhFi5VXFeV2L8KtwB+Qsu0DJAIruHJuAiicP3kEhpMzCal5LIjsF+XKTi0Cxx/VZgpPxMIpv6R+NCnt+sdjAgDU+SQGZfLy6SkHGCOAtxXhditAh3QNaiLO9GEtg/wAuGL13YJVzvYRqYr0hgbz8PGN4B+0ggMxp680ng254aGF13OhUQ7WPi5Q31glGbb8s7I+hmYnQMd0C+QVkPEy8wNR4CCa+QwFBmBE5Uf1IFCPinksAT3Bo9XmCiqH7MRIcC0o14y6uB553Mxt6Hc1oTIy3cAfkBZaSImsZuMJCaR6y8yrhIlXxi/XQLDNyaS6wjybiIL5NYP3eBWFqBEwHRNhFrjAdCY4nxAM65kRj3hDsgP6GM/yPWzmtgqM4/JfuRfyTWdpP61+4k1iRc5H5i7a4LI01zHAjI7RbmjbS3uXUHwZazBjGGhjsgeTgv6RhxNqfARLWtxDlSCRdIPEqcr6+CieQtxPlXVVwg6ju+Fa0KY40P2w/I34lR2huG4n8wGpmKYf+kcAck32yBCu1Ohqmq/KUZjQs8Q5wdlWGqyi7ijJWZQ9pbA2baFNkNSFNrByEP4H7vYKPE3a/zwx2QHARVOMy23/UgoVEeMXJjUUaFXGLkXAsJ9dn6hyugDG0zMQoaw9wguwGZSYxpMKG1LSa9VJz1DeltCHdADiKoH3FSIaUncfqYzlQEukBKKnEGoIx2xOkHGQvtBaQC17vfGgNDcQ9sJM4ynLWMW9SuhTkgexC0jL8LJM0mxlKU8RExptr6os5npiv0FkJKcq6tgPThOiBNYaTBi3nEK62FM6YRo1qYA7IZ56SUcA+YeEhK5MYwi5MRVKWY67NXhKQEbgYjUA1BcSe4LlYK5AyzERB+fG4mxLw9VpDYs0ZntN8e5oB8jnMeJUY6pA0lxkMmf2l/SHuAGA8jqC8xRkCSb7eNgPiYFqaoBkRSRv1ARvb7cFoHYkwOc0CWGLYwOz2Q5uPeZD40bmF2KNT3cqMtyxG0gPSyYyFrkI2A3ER6c4Qd07dPEoMZLk0kxkao6bf5vE1lfH3exqAfDdY4+o7ZG5bhH0H53mB9bhDkfigYxg2FBBPg5d7Bfg9pMTnWAzKK9JqB4x+ymTh8N/V7rodSCaEzVdzeoSV3eStAQcUC/ncS18+LhQI/F+H2RuuvTiZD3mTrAVnKPBvBuO7loyRh92NGU49pCJ3lBvfwU6Q3Fwr4/88Io/rToeQN0vsdDHp0i6GgmfWAZJsvxAJ8vT4lCaVLugTb3eHEeBOhs89g+vg90utq/9ud7xjV7wwl3UlvkVEX5G4o0PZbDUgV0uuAC1Ufc4AkHBp7Nc77BTEK4hAqV5FebZzFTHcURkNJXDEz0WJQ/7hi/QrMiHgWztrB3IyJUDHXakA6MP+0H2VoN79TTBL+dnc0yvJkExv7UOnGXCMPzvAwowhroGgdk3dNXH8lVPB7EUuicFpMie0O/yCrARnMvP7hvPiHMknCsVd/wWSW8f8IlWcNhvZrkd4foehF0qsurj8eiiaR3rU4rYH9Lk4TqwEZa3QRG79WQBIyh8dDrxcximsgRD4zWLPc0YkDWweT3s3i+v2g6D7xGrxbSW8YlESXWgzIXOHoQVTfNSTh1IL2GjgVC4kxEaHhZ5rw4UaTbW2d+ADwHeL6LaConXinQDrpdYKavRYD8iHpPA8ANZ87SBL2PV0NIu8RoyAJIZHGn4gm/oFrQlEd0rtXXL86FNUmYcSfIL0GUPOlakDEXaNnoHVaXEISPr7DpzxJPjpsH4A/EY2zfkt68VBUmbuA4voxUFSR9EaKT4xMgpqPLAZkA9MKPJpFEvJeNAlxbB4xcvwIgZh8/j1C/Gk0LxTFkN5TwvolUOXl7ibxctdoqFloMSBbyJoNg+IkBr854+E4fs3CKKPlhsVQpXHLuoX1T0FZgHQyxC84GtS8ZTEgmWRB4byWkNCIOCfrw3kruS6I4ZdroqAojtu9Iqwf0KDIR3r/i9MmXMInyCZS9u2TlSFnlWA6z3HXc3vwNcPVHIlQlMIt3BXXr+jEUPCIS98HWUdqSpd280DWb4iVBmfxC/JmIag/6dVzIoT9xPXrQNF13EajS/8Ws4JU5DxfGwq0rcTJToGzGnLvXF2MP795K5Twu4dSxfU7QFFX8ThLuv25QPxgMSDzSd4X98RATX9iLffAUUu4BadeBLVi1wsqelw8GtbK/i4xfkq/lXgkdTiUxAQsBmQySTo+40Yo824n1gg46VZiTMF5SXwLZL8ZSxDXnw5Fc8WHrNVnlw0r+RVZDMhjJGXHwwmw4k5inWoF5/i/I8YNKCOH2/mviPlXsg3qb4aiLPFscTQzn74FSgZbDUgqmSt+t4MGa7S1xPqpLhwzhxirUdYX7N2pgJ+xXWNQP5AMJTWNzjfJtF3/LasBuZbMHBhTHda1It7uKqE9+LSvaYaGQMmjRs3UHLtrlvkVeLNx1tvspg0FnkNWA+I5SoY+6emDLXOJt94Paep7k7/1mU6lr7L8mhh0r1H9FVCyxug4jYfZ87UUtLW+JvUTEjsytRHsSskj3kpHEnJDLnGGmM+UltaBgkbEqGlYvzYUNCBG8Nf/JemV1IK86dYDMpZENj3oD+nZ12uvciAf2cTZF8P0MG19onU210wa158EBa9ym9A0roUIeh7S4o9aD0gHYhW92VqDI7S/kEBmDdjU/Gdi3SfT0B1PhrTqRVwXxLh+QWWF+oWk97rxO/aRRMh6gqwHJLqAm8p/KhmOqXmYBL5vAlu6FRBrq5cZBrU1VDGPGLeZ1J8KabNMDqdII8YESIrLthEQvMtt+qsBWZoGM3eQyL/uh3XaIyXE64SLReVwvYQbIaklMQ75TOqXSOe/RYB7QkQjKOYINy/eAHIyyE5A7iLGnyDr3k9rw8w0EnojDhbFvUksfnvWK8TYEgMpcVnEmGJaf2MUpMRkEmOW6Qkin3sho36hvQNkjhLjZshpWEAFwzQYi11HQpmtYUnTHSSQmyz7DKApkDKDOM3M60+U7aFyWsL0nLCnIcH3ZSiOoNoTDxnx2+k/TB8iNbMtfzGWF5NxikR6gaH9u717ccqqzOMA/n0OLwjIJVAplRx0TVtLxBvbkpYpmIZRsIkXakwdL1jSmOu2y3rJ1k10NUedzQs1mYaJGkuk5mWFWU0l2bzhmopakjppraiQcnnf97ezztiK73Pu5wyzh+fzB3zhnfPl4bwvh9/vIJHRT7MyiaeUacgfZXg4CJUx3G0PcXiHQt18MlmQWOLJY1Dn2ka3qR4iCbUk7/JLEvQZfIJk5eq5EWp4Gqqe9RBPspb8uiSoSnYTTzoaSSGemr5Q85L5MZjFxDMDfPwlVX+PgaLhXlJwfJQftOu5heSdaAkuqZx4alOgYng98RxmmvJvqTYwpY54TrruyT9CPFVqDUlzmy9IIpGhCU3+6+h/qiczlccpFJ1+2R+asH6FpOBqZ8gYRVzuiQwK2FQPcQ3XmN8wDkrYFJn8kT5FIq6aZCgZ47ZiFPc+4vFOgKLw7dTI7hgomUfKLi/pyaAmMvMQKfEkgkN5GcMHwZAVsp749kua81cp5a8hvoM++ayEuLxzXZDjWmDNMP/HiW+hC/LiTlEjaocIW0hqyn/XXYK8qHGf1pGyseBQW+dybghkPFtJfO5YHfkVAyHj6W9Ixq/ho4eb+Eq7g69bqVX7YjYS374ukNFiVj35UD5E2GJSdyV/0i85tZQeSl92lFRNM/hxTFFvcMRvIznv6MsvjNV5L7USHLI/Ye6V0fDVenE9WVWQ9jeIry4nEhx+o84Qh8ohwmaTJg1fFy6YmD60X49fdI3tO2jEK29vOFhNWsyBovALJGvnyCA0Ejx6N8mqDNObvz2tBRoJfGEXyfouHBzBp0lO/dr+Ehp5dFmNlSvJMklO9bI4hsbazThLcg4FQMFrZKfZUNHfTfJuFE6ND8dt4b/KKqpWanCCgfzrGzN7B+O24D5TNt8ged4B4OrbQPIurM542IX/Yu2SFx63eKkh207yKpaN6OqP21j00Lf2e0lWTVcoSrtJtsmGqjdIxZVTZWWnrpCK1w3nf19eWlp+mVTMhYzppMxdefhA2eka69eiqm+U9VwqL93/1blbpGwkVPS5RPbwTII6aQtZoJDZm79Fggy2scn25uKJBjJvAVS13Ut2uPU8tAg/Qqb9M8ze/KPhkNWyrMkKgglk2iYJ6vwXkfW+6w1t7q8gk061sTf/fHsoeOBskxUE88mk4kBoMuwyWWzv/dAq5qLJKnawN/9SZyjqeKHJCsJWkCn7QqFR1KdkJe/b/tCuyzky4cxD9uaf7wIVnSubqiCQ3iUTSkKgGRth4SFyKQm6PPAVGVYWZW/+8WioijnZVAUBm0+GfRIIPSJXe8ka790HnUK3k0FbW9qb/3kYNGi1R//VsaYgwGQ3GbPYDzr1+gdZ4GQi9HPN8ZAB7j/62ZrvfUtjfoDeo36By6qC4ElDZ//NF6EfSy0nk65N84ch/SpJt28T7M2/kATNMm6QDkuZdQVB2x2k27FHYIiUfoJMqJnfCkZFrCGd3r/P3vw1EdChUwlpls1gYUEgTakmXdwLWsAoaVgxGVSVEwUzHisjHb6Mtzf/2ADoI038N2lyNYU/IW+O0YIAD+aTDvt6wpSeK66TfqdeDYFJ0vhvSaNvxkq25p8f7wfdWi2vJ3WfR4NfkGzjBQH67SWNKtIZzGo5Zoeb9LiRm8BgAf+MY6TBkVEuW/NPTgiAITGr69Sanc4gU5AZZgoClriTNPjXyy5YovWEbTdJmyu5z7SAVdiQDSpf96ePBzM78+s3D5FgWDvFkfrnMlvIj4nOUimIqu7Lq0hRw9+SJFgncPCiA/Wk7IfPpsVKsFboi1tlr+HNLaND7Myv3Ta+FczxH7qOf53qCob54Y5Q3j+4mxaYsu5H2eLvmtwGlgvqP3V16XVuG08U/HlERwZbBDyevcPnlf64/Q8JATbmV+36U1IQrOD/5JvF16mRivfSw3CXtuQjFVaQ+kzffJ7uca0kZ2go7BPRK3Vi9uLctRsKNuWtWZnz+0lpfdv6wW4R8RmzFq3MKyrKW7loVkZ8hF35Wwvyls0eO7Atg5VYpyGvzFuV90nB+hVzx/f3+eYf4Q1ytUxY/PDf5uTmbSrIX7N05rinOjAI/2cG8UY1C4LCbvYQCIL8sz5XIQg/20r3OghBuINdoXuthyDc0Y23q0hwsi7vtIN2r/Mm8AmO5ffcTqIcaPcF+WgDwaGiss8TEVVHQavOvP37giOxhI/qdG+MWUI+3ofgQC0nHKafeR+DNm1+4k1AExyny5JrjX9NBEOTpeTDEwHBWVzP7fSdQsygQZybfBRDcJgviONVqAs6Rr4mQXCYN4jD8wLUsLXkq168yXWc8GvE0TAcytg84vgYguPMJh5vFoMC6S/E8wQExwn5nrg+DIGsyCLi+VI88uVEY4nvXBL42PMXiCsVggNJe0jGZ73giz1VQnxl4gBxpq63SE7x6FA00nH6UZIzEIIzTSV5dbvnpnZvHeAXFNVtSNYHFSQvH4JDsSIyr6o9BKeKOEOmjYbgXN2uk0kfijtURxtUT6YcDYbgaL/xkAmXOkBwuBFuMuxqHATHS6klg6r6QGgGEn4gQy4+CqFZiDlEBpRFQ2gmAt8l3VYFQWg+ki+SLpfTIDQrYUvdpJl3dSSE5qZbIWm0tQeE5qj3Zg+pcuf3hdBcxcy/SIrOzHwQQnPmN2D5WeLz7H8zTvxpTgDrNOavB6robrVfb5yZGAZBuIO1jnsmI/O16VmTRyf3aS9BEARBEARBEARd/gMKzDOWvyQ0KQAAAABJRU5ErkJggg==", + "encoding": "base64" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1, + "_transferSize": 7132, + "_error": null + }, + "serverIPAddress": "142.250.185.164", + "startedDateTime": "2023-07-25T12:58:46.182Z", + "time": 58.204999993904494, + "timings": { + "blocked": 6.350000002189539, + "dns": -1, + "ssl": -1, + "connect": -1, + "send": 0.18300000000000027, + "wait": 51.0799999950286, + "receive": 0.5919999966863543, + "_blocked_queueing": 4.176000002189539 + } + } + ] + } +} \ No newline at end of file diff --git a/test/mitmproxy/data/har_files/chrome.json b/test/mitmproxy/data/har_files/chrome.json new file mode 100644 index 0000000000..240c99c3ec --- /dev/null +++ b/test/mitmproxy/data/har_files/chrome.json @@ -0,0 +1,575 @@ +[ + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680134454.25, + "timestamp_tls_setup": null, + "timestamp_end": 1680134454.264666 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": [ + "108.138.246.80", + 443 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/", + "http_version": "HTTP/2", + "headers": [ + [ + ":authority", + "mitmproxy.org" + ], + [ + ":method", + "GET" + ], + [ + ":path", + "/" + ], + [ + ":scheme", + "https" + ], + [ + "accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" + ], + [ + "accept-encoding", + "gzip, deflate, br" + ], + [ + "accept-language", + "en-US,en;q=0.9" + ], + [ + "cache-control", + "max-age=0" + ], + [ + "dnt", + "1" + ], + [ + "if-modified-since", + "Sat, 04 Mar 2023 18:02:08 GMT" + ], + [ + "if-none-match", + "W/\"a1550c2bd25c5bcfef789d730f5bbddf\"" + ], + [ + "referer", + "https://www.google.com/" + ], + [ + "sec-ch-ua", + "\"Chromium\";v=\"110\", \"Not A(Brand\";v=\"24\", \"Google Chrome\";v=\"110\"" + ], + [ + "sec-ch-ua-mobile", + "?0" + ], + [ + "sec-ch-ua-platform", + "\"macOS\"" + ], + [ + "sec-fetch-dest", + "document" + ], + [ + "sec-fetch-mode", + "navigate" + ], + [ + "sec-fetch-site", + "cross-site" + ], + [ + "sec-fetch-user", + "?1" + ], + [ + "upgrade-insecure-requests", + "1" + ], + [ + "user-agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134454.25, + "timestamp_end": 1680134454.264666, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/2", + "status_code": 304, + "reason": "Not Modified", + "headers": [ + [ + "age", + "11391" + ], + [ + "alt-svc", + "h3=\":443\"; ma=86400" + ], + [ + "date", + "Thu, 30 Mar 2023 00:00:54 GMT" + ], + [ + "etag", + "W/\"a1550c2bd25c5bcfef789d730f5bbddf\"" + ], + [ + "server", + "AmazonS3" + ], + [ + "vary", + "Accept-Encoding" + ], + [ + "via", + "1.1 e758e6512b4c08d28af121962cc722ce.cloudfront.net (CloudFront)" + ], + [ + "x-amz-cf-id", + "PaWnOEEyng3O4PBX2lab_qyD2DQ60VajC413wiIbt93eiNkaSKvpkQ==" + ], + [ + "x-amz-cf-pop", + "SFO5-P1" + ], + [ + "x-cache", + "Hit from cloudfront" + ] + ], + "contentLength": 23866, + "contentHash": "7fd5f643a86976f5711df86ae2d5f9f8137a47c705dee31ccc550215564a5364", + "timestamp_start": 1680134454.25, + "timestamp_end": 1680134454.264666 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1689251552.676, + "timestamp_tls_setup": null, + "timestamp_end": 1689251552.680119 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "www.google.com", + "port": 443, + "path": "/", + "http_version": "HTTP/1.1", + "headers": [ + [ + "DNT", + "1" + ], + [ + "Upgrade-Insecure-Requests", + "1" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + ], + [ + "sec-ch-ua", + "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Google Chrome\";v=\"114\"" + ], + [ + "sec-ch-ua-arch", + "\"arm\"" + ], + [ + "sec-ch-ua-bitness", + "\"64\"" + ], + [ + "sec-ch-ua-full-version", + "\"114.0.5735.198\"" + ], + [ + "sec-ch-ua-full-version-list", + "\"Not.A/Brand\";v=\"8.0.0.0\", \"Chromium\";v=\"114.0.5735.198\", \"Google Chrome\";v=\"114.0.5735.198\"" + ], + [ + "sec-ch-ua-mobile", + "?0" + ], + [ + "sec-ch-ua-model", + "\"\"" + ], + [ + "sec-ch-ua-platform", + "\"macOS\"" + ], + [ + "sec-ch-ua-platform-version", + "\"13.2.1\"" + ], + [ + "sec-ch-ua-wow64", + "?0" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1689251552.676, + "timestamp_end": 1689251552.680119, + "pretty_host": "www.google.com" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 0, + "reason": "", + "headers": [], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1689251552.676, + "timestamp_end": 1689251552.680119 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1690289926.182, + "timestamp_tls_setup": null, + "timestamp_end": 1690289926.2402048 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": [ + "142.250.185.164", + 443 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "www.google.com", + "port": 443, + "path": "/images/branding/googlelogo/2x/googlelogo_light_color_272x92dp.png", + "http_version": "HTTP/1.1", + "headers": [ + [ + ":authority", + "www.google.com" + ], + [ + ":method", + "GET" + ], + [ + ":path", + "/images/branding/googlelogo/2x/googlelogo_light_color_272x92dp.png" + ], + [ + ":scheme", + "https" + ], + [ + "accept", + "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8" + ], + [ + "accept-encoding", + "gzip, deflate, br" + ], + [ + "accept-language", + "en-US,en;q=0.9" + ], + [ + "cache-control", + "no-cache" + ], + [ + "cookie", + "SID=YAieF18TdLas-8SKhGKNYvnieZbB1wJ1jf2tW44mAWYKcppPKLvK_Ok7NklKqsURxU82bg.; __Secure-1PSID=YAieF18TdLas-8SKhGKNYvnieZbB1wJ1jf2tW44mAWYKcppP1MvyqChs4flonGDrhu0qfw.; __Secure-3PSID=YAieF18TdLas-8SKhGKNYvnieZbB1wJ1jf2tW44mAWYKcppPqnnM4L77WRxhbrutGn6uEw.; HSID=AOiDlkphfGSlvWKsu; SSID=A_mrp5cpjfkd72x04; APISID=GtMYjIVXfk0omvEX/A1mpBXxfb2x4uc-qq; SAPISID=lTu95MmvfwxU5H_h/Af79riHHI6PPtD3q3; __Secure-1PAPISID=lTu95MmvfwxU5H_h/Af79riHHI6PPtD3q3; __Secure-3PAPISID=lTu95MmvfwxU5H_h/Af79riHHI6PPtD3q3; OGPC=19022552-1:19031986-1:; SEARCH_SAMESITE=CgQI5pgB; AEC=Ad49MVEbr-A-zp-lPUibpASs-qNYts6opSVc67g4tFxAiIcO79t6fv7tSw; OTZ=7131795_48_52_123900_48_436380; 1P_JAR=2023-7-25-11; __Secure-ENID=13.SE=o3Bl7qRCyr6NPgYD4acm8JRn0rHVFaHBm2GJK2uTs7tHS0gwr5E_QfusAwogWPHDkJWCTosdnQbrw5mZIk4a0GdDMLmUgp1c63gYrMAoNoAJYN7hQ-HNT6ztr32WVkImvjtzfI7XxVRC4uRovzZP5X1CLbq5eifYkMGyuQWB4vKok5OBwvnbAJoETbBQ7yaG_vBp3v2Mx7RLCXMHzmb_55e6fogLMGMRw_ilgfQjayO31xAlXhPfFrPsx-R7p9uEu1ka; NID=511=nguIsOmSJcCSka0RAZK37__ia-Rm4RXhcxFnfwfMxyS3IIKf7KKi8QPxqH8MgLl4hFljOeweA_8t_o6JYklI6f6TpenXMrjXP9rFfaflNOp7fwNQX3hDPqOnHANLFsQBpc2Yh56SsDfzVDnjJcGw1rPV3a6N-N4r8e0bdLseNy0OgT8RdVSqoCREsbvn6Q-lmadCfM35a8FToMgl8fP7WXXZRutJKIJBly6os_NDIxJO25qntdadaEhYmujyFCTkGHcbTqtR3VhiEO3L4KZOmmWd9eVnVuqJDPvRt20aNy4c-_tctcvq48qW5SwUE1eiDoN0SFeTFlIIK1P1kchv6Nno4wNWoadtqvIIxMBzrp2ueN5AufcVUP4GcfT3glpuphaMEjcWYi9dN-qdvoIZTDoY2UbjYW5Mv3V7jF1UyZaR7q8BVQTFrHeGoeaHpVYsQP3uKGT5KdmLMWLLyLQ; __Secure-1PSIDTS=sidts-CjEBPu3jIZnNVFcJFxoLH-VBGrYwtBzLlYvkamouuPMziP33rF7t30l_WylizU_03CxbEAA; __Secure-3PSIDTS=sidts-CjEBPu3jIZnNVFcJFxoLH-VBGrYwtBzLlYvkamouuPMziP33rF7t30l_WylizU_03CxbEAA; SIDCC=APoG2W_9UqmUgcCg-einZHGu5g355CBjyEUdjxqwA_6rEuwN_gn337dUXNMjvB3mCSnLWrSgnTA; __Secure-1PSIDCC=APoG2W8WMUYq99_nTjsvPQwHnvITj9AqaBQWvoJrzTikBF9ct4-xVUD6NwEvj-eXsXpvjuHCg2I; __Secure-3PSIDCC=APoG2W-9cTkSJnEPYb1qMKif0ohMbB5jrVPirjfWRynrxpgbjD29BKtUFSbH48Vi2XGqPylzJYs0" + ], + [ + "dnt", + "1" + ], + [ + "pragma", + "no-cache" + ], + [ + "referer", + "https://www.google.com/" + ], + [ + "sec-ch-ua", + "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Google Chrome\";v=\"114\"" + ], + [ + "sec-ch-ua-arch", + "\"arm\"" + ], + [ + "sec-ch-ua-bitness", + "\"64\"" + ], + [ + "sec-ch-ua-full-version", + "\"114.0.5735.198\"" + ], + [ + "sec-ch-ua-full-version-list", + "\"Not.A/Brand\";v=\"8.0.0.0\", \"Chromium\";v=\"114.0.5735.198\", \"Google Chrome\";v=\"114.0.5735.198\"" + ], + [ + "sec-ch-ua-mobile", + "?0" + ], + [ + "sec-ch-ua-model", + "\"\"" + ], + [ + "sec-ch-ua-platform", + "\"macOS\"" + ], + [ + "sec-ch-ua-platform-version", + "\"13.2.1\"" + ], + [ + "sec-ch-ua-wow64", + "?0" + ], + [ + "sec-fetch-dest", + "image" + ], + [ + "sec-fetch-mode", + "no-cors" + ], + [ + "sec-fetch-site", + "same-origin" + ], + [ + "user-agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + ], + [ + "x-client-data", + "CKG1yQEIiLbJAQiltskBCKmdygEI0eXKAQiVocsBCIagzQEI2rTNAQjLtc0BCIa9zQEI3L3NAQi8vs0BCKS/zQEI/r/NAQjnwc0BCLLDzQEY06DNAQ==" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1690289926.182, + "timestamp_end": 1690289926.2402048, + "pretty_host": "www.google.com" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "accept-ranges", + "bytes" + ], + [ + "alt-svc", + "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000" + ], + [ + "cache-control", + "private, max-age=31536000" + ], + [ + "content-length", + "7108" + ], + [ + "content-type", + "image/png" + ], + [ + "cross-origin-opener-policy-report-only", + "same-origin; report-to=\"static-on-bigtable\"" + ], + [ + "cross-origin-resource-policy", + "cross-origin" + ], + [ + "date", + "Tue, 25 Jul 2023 12:58:46 GMT" + ], + [ + "expires", + "Tue, 25 Jul 2023 12:58:46 GMT" + ], + [ + "last-modified", + "Tue, 22 Oct 2019 18:30:00 GMT" + ], + [ + "report-to", + "{\"group\":\"static-on-bigtable\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/static-on-bigtable\"}]}" + ], + [ + "server", + "sffe" + ], + [ + "x-content-type-options", + "nosniff" + ], + [ + "x-xss-protection", + "0" + ] + ], + "contentLength": 7108, + "contentHash": "d2c8a5c554b741fab4a622552e5f89d8a75b09baa3bc5b37819a4279217d6cec", + "timestamp_start": 1690289926.182, + "timestamp_end": 1690289926.2402048 + } + } +] \ No newline at end of file diff --git a/test/mitmproxy/data/har_files/firefox.har b/test/mitmproxy/data/har_files/firefox.har new file mode 100644 index 0000000000..df88c172d2 --- /dev/null +++ b/test/mitmproxy/data/har_files/firefox.har @@ -0,0 +1,1674 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Firefox", + "version": "111.0.1" + }, + "browser": { + "name": "Firefox", + "version": "111.0.1" + }, + "pages": [ + { + "startedDateTime": "2023-03-29T16:58:59.303-07:00", + "id": "page_1", + "title": "mitmproxy - an interactive HTTPS proxy", + "pageTimings": { + "onContentLoad": 208, + "onLoad": 270 + } + } + ], + "entries": [ + { + "pageref": "page_1", + "startedDateTime": "2023-03-29T16:58:59.303-07:00", + "request": { + "bodySize": 0, + "method": "GET", + "url": "https://mitmproxy.org/", + "httpVersion": "HTTP/3", + "headers": [ + { + "name": "Host", + "value": "mitmproxy.org" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + }, + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.5" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Referer", + "value": "https://www.google.com/" + }, + { + "name": "DNT", + "value": "1" + }, + { + "name": "Alt-Used", + "value": "mitmproxy.org" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "cross-site" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "If-Modified-Since", + "value": "Sat, 04 Mar 2023 18:02:08 GMT" + }, + { + "name": "If-None-Match", + "value": "W/\"a1550c2bd25c5bcfef789d730f5bbddf\"" + } + ], + "cookies": [], + "queryString": [], + "headersSize": 625 + }, + "response": { + "status": 304, + "statusText": "Not Modified", + "httpVersion": "HTTP/3", + "headers": [ + { + "name": "age", + "value": "32345" + }, + { + "name": "date", + "value": "Wed, 29 Mar 2023 23:58:59 GMT" + }, + { + "name": "server", + "value": "AmazonS3" + }, + { + "name": "etag", + "value": "W/\"a1550c2bd25c5bcfef789d730f5bbddf\"" + }, + { + "name": "vary", + "value": "Accept-Encoding" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + }, + { + "name": "via", + "value": "1.1 5fa120f79d5713714191c32768eca58c.cloudfront.net (CloudFront)" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "x-amz-cf-id", + "value": "DPEkuUbeK1ZXMsRuUHqgk6iE4l7ShgyrJntkqIbLaSJ5646Ptc2Xew==" + } + ], + "cookies": [], + "content": { + "mimeType": "text/html", + "size": 23866, + "text": "<!DOCTYPE html>\n<html lang=\"en-us\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\n <link rel=\"icon\" type=\"image/png\" href=\"./favicon.ico\">\n <title>mitmproxy - an interactive HTTPS proxy\n \n \n \n \n \n \n\n\n\n\n\n
    \n
    \n
    \n
    \n
    \n
    \n \"screenshot\n
    \n
    \n
    \n
    \n

    \n mitmproxy is a free\n and open source\n interactive HTTPS proxy.\n

    \n
    \n \n

    \n Release Notes (v9.0)\n \n –\n \n Other Downloads\n \n \n

    \n
    \n
    \n
    \n
    \n
    \n
    \n\n
    \n \n
    \n\n
    \n
    \n
    \n
    \n
    \n \"screenshot\n
    \n
    \n

    Command Line

    \n

    \n mitmproxy is your swiss-army knife for debugging, testing,\n privacy measurements, and penetration testing.\n It can be used to intercept, inspect, modify and replay web traffic such\n as\n HTTP/1, HTTP/2, WebSockets, or any other SSL/TLS-protected protocols.\n You can prettify and decode a variety of message types ranging from HTML\n to\n Protobuf,\n intercept specific messages on-the-fly,\n modify them before they reach their destination, and replay them\n to a client or server later on.\n

    \n
    \n\n
    \n
    \n
    \n
    \n\n
    \n
    \n
    \n
    \n
    \n \"screenshot\n
    \n
    \n

    Web Interface

    \n

    \n Use mitmproxy's main features in a graphical interface with\n mitmweb. Do you like Chrome's DevTools? mitmweb\n gives\n you a similar experience for any other application or device,\n plus additional features such as request interception and replay.\n\n

    \n \n
    \n\n
    \n
    \n
    \n
    \n\n
    \n
    \n
    \n
    \n
    \n
    \n
    addon.py
    \n
    from mitmproxy import http\n\ndef request(flow: http.HTTPFlow):\n    # redirect to different host\n    if flow.request.pretty_host == \"example.com\":\n        flow.request.host = \"mitmproxy.org\"\n    # answer from proxy\n    elif flow.request.path.endswith(\"/brew\"):\n    \tflow.response = http.Response.make(\n            418, b\"I'm a teapot\",\n        )
    \n
    \n
    \n
    \n

    \n Python API\n

    \n

    \n Write powerful addons and script mitmproxy with mitmdump.\n The scripting API offers full control over mitmproxy and makes it\n possible\n to automatically modify messages, redirect traffic, visualize messages,\n or\n implement custom commands.\n

    \n
    \n
    \n
    \n
    \n
    \n\n
    \n
    \n
    \n
    \n
    \n
    \n

    Latest\n Tweets

    \n
    \n \n \"A\n \n
    \n
    \n
    \n
    \n

    Powerful Ecosystem

    \n
    \n

    \n Mitmproxy has a vibrant ecosystem of addons and tools building on it:\n

    \n
      \n
    • \n \n mitmproxy/examples/contrib, a collection of\n community-contributed mitmproxy addons.\n
    • \n
    • \n \n mitmproxy2swagger, a tool for automatically converting mitmproxy captures to\n OpenAPI 3.0 specifications.\n
    • \n
    • \n \n kubetap, a kubectl plugin to interactively proxy Kubernetes Services.\n
    • \n
    \n
    \n

    Sponsored By

    \n
    \n \"Proxyman\"\n \"Netograph.io\"\n ...and many individual supporters! ❤️\n
    \n
    \n
    \n
    \n
    \n
    \n\n
    \n
    \n
    \n
    \n
    \n \n
    \n
    \n

    Open Source

    \n

    \n Mitmproxy is free and open source. Be part of the mitmproxy community\n and\n help improve your favorite HTTPS proxy.\n

    \n
    \n \n
    \n
    \n
    \n
    \n
    \n
    \n\n\n\n\n\n\n\n\n\n\n" + }, + "redirectURL": "", + "headersSize": 386, + "bodySize": 5509 + }, + "cache": { + "afterRequest": { + "expires": "4294967295", + "lastFetched": "", + "fetchCount": "", + "_dataSize": "", + "_lastModified": "1680134301", + "_device": "" + } + }, + "timings": { + "blocked": 17, + "dns": 0, + "connect": 0, + "ssl": 0, + "send": 0, + "wait": 6, + "receive": 0 + }, + "time": 23, + "_securityState": "secure", + "serverIPAddress": "13.35.121.55", + "connection": "443" + }, + { + "pageref": "page_1", + "startedDateTime": "2023-03-29T16:58:59.418-07:00", + "request": { + "bodySize": 0, + "method": "GET", + "url": "https://mitmproxy.org/logo-navbar.png", + "httpVersion": "", + "headers": [ + { + "name": "Host", + "value": "mitmproxy.org" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + }, + { + "name": "Accept", + "value": "image/avif,image/webp,*/*" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.5" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Referer", + "value": "https://mitmproxy.org/" + } + ], + "cookies": [], + "queryString": [], + "headersSize": null + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "", + "headers": [], + "cookies": [], + "content": { + "mimeType": "image/png", + "comment": "Response bodies are not included." + }, + "redirectURL": "", + "bodySize": -1 + }, + "cache": {}, + "timings": {}, + "time": 0 + }, + { + "pageref": "page_1", + "startedDateTime": "2023-03-29T16:58:59.456-07:00", + "request": { + "bodySize": 0, + "method": "GET", + "url": "https://mitmproxy.org/screenshot.png", + "httpVersion": "", + "headers": [ + { + "name": "Host", + "value": "mitmproxy.org" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + }, + { + "name": "Accept", + "value": "image/avif,image/webp,*/*" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.5" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Referer", + "value": "https://mitmproxy.org/" + } + ], + "cookies": [], + "queryString": [], + "headersSize": null + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "", + "headers": [], + "cookies": [], + "content": { + "mimeType": "image/png", + "comment": "Response bodies are not included." + }, + "redirectURL": "", + "bodySize": -1 + }, + "cache": {}, + "timings": {}, + "time": 0 + }, + { + "pageref": "page_1", + "startedDateTime": "2023-03-29T16:58:59.457-07:00", + "request": { + "bodySize": 0, + "method": "GET", + "url": "https://mitmproxy.org/mitmweb.png", + "httpVersion": "", + "headers": [ + { + "name": "Host", + "value": "mitmproxy.org" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + }, + { + "name": "Accept", + "value": "image/avif,image/webp,*/*" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.5" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Referer", + "value": "https://mitmproxy.org/" + } + ], + "cookies": [], + "queryString": [], + "headersSize": null + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "", + "headers": [], + "cookies": [], + "content": { + "mimeType": "image/png", + "comment": "Response bodies are not included." + }, + "redirectURL": "", + "bodySize": -1 + }, + "cache": {}, + "timings": {}, + "time": 0 + }, + { + "pageref": "page_1", + "startedDateTime": "2023-03-29T16:58:59.457-07:00", + "request": { + "bodySize": 0, + "method": "GET", + "url": "https://mitmproxy.org/sponsors/proxyman.png", + "httpVersion": "", + "headers": [ + { + "name": "Host", + "value": "mitmproxy.org" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + }, + { + "name": "Accept", + "value": "image/avif,image/webp,*/*" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.5" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Referer", + "value": "https://mitmproxy.org/" + } + ], + "cookies": [], + "queryString": [], + "headersSize": null + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "", + "headers": [], + "cookies": [], + "content": { + "mimeType": "image/png", + "comment": "Response bodies are not included." + }, + "redirectURL": "", + "bodySize": -1 + }, + "cache": {}, + "timings": {}, + "time": 0 + }, + { + "pageref": "page_1", + "startedDateTime": "2023-03-29T16:58:59.458-07:00", + "request": { + "bodySize": 0, + "method": "GET", + "url": "https://mitmproxy.org/sponsors/netograph.svg", + "httpVersion": "", + "headers": [ + { + "name": "Host", + "value": "mitmproxy.org" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + }, + { + "name": "Accept", + "value": "image/avif,image/webp,*/*" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.5" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Referer", + "value": "https://mitmproxy.org/" + } + ], + "cookies": [], + "queryString": [], + "headersSize": null + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "", + "headers": [], + "cookies": [], + "content": { + "mimeType": "image/svg+xml", + "comment": "Response bodies are not included." + }, + "redirectURL": "", + "bodySize": -1 + }, + "cache": {}, + "timings": {}, + "time": 0 + }, + { + "pageref": "page_1", + "startedDateTime": "2023-03-29T16:58:59.495-07:00", + "request": { + "bodySize": 0, + "method": "GET", + "url": "https://mitmproxy.org/polyfills.js", + "httpVersion": "HTTP/3", + "headers": [ + { + "name": "Host", + "value": "mitmproxy.org" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + }, + { + "name": "Accept", + "value": "*/*" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.5" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "DNT", + "value": "1" + }, + { + "name": "Alt-Used", + "value": "mitmproxy.org" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Referer", + "value": "https://mitmproxy.org/" + }, + { + "name": "Sec-Fetch-Dest", + "value": "script" + }, + { + "name": "Sec-Fetch-Mode", + "value": "no-cors" + }, + { + "name": "Sec-Fetch-Site", + "value": "same-origin" + } + ], + "cookies": [], + "queryString": [], + "headersSize": null + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/3", + "headers": [ + { + "name": "content-type", + "value": "text/javascript" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "age", + "value": "14777" + }, + { + "name": "last-modified", + "value": "Sat, 04 Mar 2023 16:01:12 GMT" + }, + { + "name": "server", + "value": "AmazonS3" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "date", + "value": "Wed, 29 Mar 2023 19:52:06 GMT" + }, + { + "name": "etag", + "value": "W/\"542d62f852e229d44f16469475b7500b\"" + }, + { + "name": "vary", + "value": "Accept-Encoding" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + }, + { + "name": "via", + "value": "1.1 28663e5849ed20a9d037ca8066957990.cloudfront.net (CloudFront)" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "x-amz-cf-id", + "value": "EufbwzXQheESUTNil_OCjKK8cvVL51cQpYfmF7iZSHloCTvVhfZ8yQ==" + } + ], + "cookies": [], + "content": { + "mimeType": "text/javascript", + "size": 11800, + "text": "// https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js\n!function(e,n){\"object\"==typeof exports&&\"undefined\"!=typeof module?n():\"function\"==typeof define&&define.amd?define(n):n()}(0,function(){\"use strict\";function e(e){var n=this.constructor;return this.then(function(t){return n.resolve(e()).then(function(){return t})},function(t){return n.resolve(e()).then(function(){return n.reject(t)})})}function n(e){return!(!e||\"undefined\"==typeof e.length)}function t(){}function o(e){if(!(this instanceof o))throw new TypeError(\"Promises must be constructed via new\");if(\"function\"!=typeof e)throw new TypeError(\"not a function\");this._state=0,this._handled=!1,this._value=undefined,this._deferreds=[],c(e,this)}function r(e,n){for(;3===e._state;)e=e._value;0!==e._state?(e._handled=!0,o._immediateFn(function(){var t=1===e._state?n.onFulfilled:n.onRejected;if(null!==t){var o;try{o=t(e._value)}catch(r){return void f(n.promise,r)}i(n.promise,o)}else(1===e._state?i:f)(n.promise,e._value)})):e._deferreds.push(n)}function i(e,n){try{if(n===e)throw new TypeError(\"A promise cannot be resolved with itself.\");if(n&&(\"object\"==typeof n||\"function\"==typeof n)){var t=n.then;if(n instanceof o)return e._state=3,e._value=n,void u(e);if(\"function\"==typeof t)return void c(function(e,n){return function(){e.apply(n,arguments)}}(t,n),e)}e._state=1,e._value=n,u(e)}catch(r){f(e,r)}}function f(e,n){e._state=2,e._value=n,u(e)}function u(e){2===e._state&&0===e._deferreds.length&&o._immediateFn(function(){e._handled||o._unhandledRejectionFn(e._value)});for(var n=0,t=e._deferreds.length;t>n;n++)r(e,e._deferreds[n]);e._deferreds=null}function c(e,n){var t=!1;try{e(function(e){t||(t=!0,i(n,e))},function(e){t||(t=!0,f(n,e))})}catch(o){if(t)return;t=!0,f(n,o)}}var a=setTimeout;o.prototype[\"catch\"]=function(e){return this.then(null,e)},o.prototype.then=function(e,n){var o=new this.constructor(t);return r(this,new function(e,n,t){this.onFulfilled=\"function\"==typeof e?e:null,this.onRejected=\"function\"==typeof n?n:null,this.promise=t}(e,n,o)),o},o.prototype[\"finally\"]=e,o.all=function(e){return new o(function(t,o){function r(e,n){try{if(n&&(\"object\"==typeof n||\"function\"==typeof n)){var u=n.then;if(\"function\"==typeof u)return void u.call(n,function(n){r(e,n)},o)}i[e]=n,0==--f&&t(i)}catch(c){o(c)}}if(!n(e))return o(new TypeError(\"Promise.all accepts an array\"));var i=Array.prototype.slice.call(e);if(0===i.length)return t([]);for(var f=i.length,u=0;i.length>u;u++)r(u,i[u])})},o.resolve=function(e){return e&&\"object\"==typeof e&&e.constructor===o?e:new o(function(n){n(e)})},o.reject=function(e){return new o(function(n,t){t(e)})},o.race=function(e){return new o(function(t,r){if(!n(e))return r(new TypeError(\"Promise.race accepts an array\"));for(var i=0,f=e.length;f>i;i++)o.resolve(e[i]).then(t,r)})},o._immediateFn=\"function\"==typeof setImmediate&&function(e){setImmediate(e)}||function(e){a(e,0)},o._unhandledRejectionFn=function(e){void 0!==console&&console&&console.warn(\"Possible Unhandled Promise Rejection:\",e)};var l=function(){if(\"undefined\"!=typeof self)return self;if(\"undefined\"!=typeof window)return window;if(\"undefined\"!=typeof global)return global;throw Error(\"unable to locate global object\")}();\"Promise\"in l?l.Promise.prototype[\"finally\"]||(l.Promise.prototype[\"finally\"]=e):l.Promise=o});\n\n// https://cdn.jsdelivr.net/npm/whatwg-fetch@3.0.0/dist/fetch.umd.min.js\n!function(t,e){\"object\"==typeof exports&&\"undefined\"!=typeof module?e(exports):\"function\"==typeof define&&define.amd?define([\"exports\"],e):e(t.WHATWGFetch={})}(this,function(a){\"use strict\";var r=\"URLSearchParams\"in self,o=\"Symbol\"in self&&\"iterator\"in Symbol,h=\"FileReader\"in self&&\"Blob\"in self&&function(){try{return new Blob,!0}catch(t){return!1}}(),n=\"FormData\"in self,i=\"ArrayBuffer\"in self;if(i)var e=[\"[object Int8Array]\",\"[object Uint8Array]\",\"[object Uint8ClampedArray]\",\"[object Int16Array]\",\"[object Uint16Array]\",\"[object Int32Array]\",\"[object Uint32Array]\",\"[object Float32Array]\",\"[object Float64Array]\"],s=ArrayBuffer.isView||function(t){return t&&-1this.length)&&-1!==this.indexOf(t,n)});\n" + }, + "redirectURL": "", + "headersSize": 0, + "bodySize": 4002 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": 0, + "ssl": 0, + "connect": 0, + "send": 0, + "wait": 0, + "receive": 0 + }, + "time": 0, + "_securityState": "secure" + }, + { + "pageref": "page_1", + "startedDateTime": "2023-03-29T16:58:59.498-07:00", + "request": { + "bodySize": 0, + "method": "GET", + "url": "https://mitmproxy.org/clipboard.min.js", + "httpVersion": "HTTP/3", + "headers": [ + { + "name": "Host", + "value": "mitmproxy.org" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + }, + { + "name": "Accept", + "value": "*/*" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.5" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "DNT", + "value": "1" + }, + { + "name": "Alt-Used", + "value": "mitmproxy.org" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Referer", + "value": "https://mitmproxy.org/" + }, + { + "name": "Sec-Fetch-Dest", + "value": "script" + }, + { + "name": "Sec-Fetch-Mode", + "value": "no-cors" + }, + { + "name": "Sec-Fetch-Site", + "value": "same-origin" + } + ], + "cookies": [], + "queryString": [], + "headersSize": null + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/3", + "headers": [ + { + "name": "content-type", + "value": "text/javascript" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "age", + "value": "28518" + }, + { + "name": "last-modified", + "value": "Sat, 04 Mar 2023 16:01:11 GMT" + }, + { + "name": "server", + "value": "AmazonS3" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "date", + "value": "Wed, 29 Mar 2023 16:03:05 GMT" + }, + { + "name": "etag", + "value": "W/\"af8ab36589315582ccdd82f22e84bffb\"" + }, + { + "name": "vary", + "value": "Accept-Encoding" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + }, + { + "name": "via", + "value": "1.1 28663e5849ed20a9d037ca8066957990.cloudfront.net (CloudFront)" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "x-amz-cf-id", + "value": "UuHHTyGhTqqTO07G_oZCw7rxjb9RJUGTN3OW0EUS77RH4GiQ-LkAvw==" + } + ], + "cookies": [], + "content": { + "mimeType": "text/javascript", + "size": 10453, + "text": "/*!\n * clipboard.js v2.0.6\n * https://clipboardjs.com/\n * \n * Licensed MIT © Zeno Rocha\n */\n!function(t,e){\"object\"==typeof exports&&\"object\"==typeof module?module.exports=e():\"function\"==typeof define&&define.amd?define([],e):\"object\"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return o={},r.m=n=[function(t,e){t.exports=function(t){var e;if(\"SELECT\"===t.nodeName)t.focus(),e=t.value;else if(\"INPUT\"===t.nodeName||\"TEXTAREA\"===t.nodeName){var n=t.hasAttribute(\"readonly\");n||t.setAttribute(\"readonly\",\"\"),t.select(),t.setSelectionRange(0,t.value.length),n||t.removeAttribute(\"readonly\"),e=t.value}else{t.hasAttribute(\"contenteditable\")&&t.focus();var o=window.getSelection(),r=document.createRange();r.selectNodeContents(t),o.removeAllRanges(),o.addRange(r),e=o.toString()}return e}},function(t,e){function n(){}n.prototype={on:function(t,e,n){var o=this.e||(this.e={});return(o[t]||(o[t]=[])).push({fn:e,ctx:n}),this},once:function(t,e,n){var o=this;function r(){o.off(t,r),e.apply(n,arguments)}return r._=e,this.on(t,r,n)},emit:function(t){for(var e=[].slice.call(arguments,1),n=((this.e||(this.e={}))[t]||[]).slice(),o=0,r=n.length;o 4.0 > 1.0 > a > b > z\n let invert = (/^[0-9]/.test(a.name) && /^[0-9]/.test(b.name)) ? -1 : 1;\n if (a.name > b.name) return invert;\n if (a.name < b.name) return -invert;\n return 0;\n}\n\nlet s3cache = {};\nfunction fetchS3(directory) {\n let url = BUCKET_URL + \"?delimiter=/&prefix=\" + directory;\n s3cache[url] = s3cache[url] || (fetch(url)\n .then(function (response) {\n return response.text()\n })\n .then(function (data) {\n let s3 = (new DOMParser()).parseFromString(data, \"text/xml\");\n let files = [];\n s3.querySelectorAll(\"Contents\").forEach(function (item) {\n if (item.querySelector(\"Key\").textContent in EXCLUDE) {\n return;\n }\n files.push({\n name: item.querySelector(\"Key\").textContent.replace(directory, \"\"),\n time: new Date(item.querySelector(\"LastModified\").textContent),\n size: parseInt(item.querySelector(\"Size\").textContent)\n });\n })\n files.sort(sortByName);\n\n let directories = [];\n s3.querySelectorAll(\"CommonPrefixes\").forEach(function (item) {\n directories.push({\n name: item.querySelector(\"Prefix\").textContent.replace(directory, \"\"),\n });\n })\n directories.sort(sortByName);\n\n return { directory: directory, files: files, directories: directories };\n }));\n return s3cache[url];\n}\n\n\nfunction getLatestRelease(suffix) {\n return fetchS3(\"\").then(function (data) {\n let latestVersion = data.directories\n .map(function (x) {\n return x.name.replace(\"/\", \"\")\n })\n .filter(function (x) {\n return /^[\\d.]+$/.test(x);\n })[0]\n return WEB_ROOT + latestVersion + \"/mitmproxy-\" + latestVersion + suffix;\n })\n}\n" + }, + "redirectURL": "", + "headersSize": 0, + "bodySize": 831 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": 0, + "ssl": 0, + "connect": 0, + "send": 0, + "wait": 0, + "receive": 0 + }, + "time": 0, + "_securityState": "secure" + }, + { + "pageref": "page_1", + "startedDateTime": "2023-03-29T16:58:59.527-07:00", + "request": { + "bodySize": 0, + "method": "GET", + "url": "https://mitmproxy.org/github-btn.html?user=mhils&type=sponsor&size=large", + "httpVersion": "HTTP/3", + "headers": [ + { + "name": "Host", + "value": "mitmproxy.org" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + }, + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.5" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Referer", + "value": "https://mitmproxy.org/" + }, + { + "name": "DNT", + "value": "1" + }, + { + "name": "Alt-Used", + "value": "mitmproxy.org" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "Sec-Fetch-Dest", + "value": "iframe" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "same-origin" + }, + { + "name": "If-Modified-Since", + "value": "Sat, 04 Mar 2023 16:01:11 GMT" + }, + { + "name": "If-None-Match", + "value": "W/\"8d3963829b6394c8c198172e36049e5e\"" + }, + { + "name": "TE", + "value": "trailers" + } + ], + "cookies": [], + "queryString": [ + { + "name": "user", + "value": "mhils" + }, + { + "name": "type", + "value": "sponsor" + }, + { + "name": "size", + "value": "large" + } + ], + "headersSize": 653 + }, + "response": { + "status": 304, + "statusText": "Not Modified", + "httpVersion": "HTTP/3", + "headers": [ + { + "name": "age", + "value": "13417" + }, + { + "name": "date", + "value": "Wed, 29 Mar 2023 23:58:59 GMT" + }, + { + "name": "server", + "value": "AmazonS3" + }, + { + "name": "etag", + "value": "W/\"8d3963829b6394c8c198172e36049e5e\"" + }, + { + "name": "vary", + "value": "Accept-Encoding" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + }, + { + "name": "via", + "value": "1.1 5fa120f79d5713714191c32768eca58c.cloudfront.net (CloudFront)" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "x-amz-cf-id", + "value": "nnIrWtgAMt42ua4HYBtNAao6m_iD9WjIzLAFyURb8mjOr5MriSQXRA==" + } + ], + "cookies": [], + "content": { + "mimeType": "text/html", + "size": 9689, + "text": "\n\n\n \n \n \n \n \n \n\n\n \n \n \n \n \n \n \n \n\n\n" + }, + "redirectURL": "", + "headersSize": 386, + "bodySize": 3756 + }, + "cache": { + "afterRequest": { + "expires": "4294967295", + "lastFetched": "", + "fetchCount": "", + "_dataSize": "", + "_lastModified": "1680134302", + "_device": "" + } + }, + "timings": { + "blocked": -1, + "dns": 0, + "connect": 0, + "ssl": 0, + "send": 0, + "wait": 6, + "receive": 0 + }, + "time": 6, + "_securityState": "secure", + "serverIPAddress": "13.35.121.55", + "connection": "443" + }, + { + "pageref": "page_1", + "startedDateTime": "2023-03-29T16:58:59.528-07:00", + "request": { + "bodySize": 0, + "method": "GET", + "url": "https://mitmproxy.org/github-btn.html?user=mitmproxy&repo=mitmproxy&type=star&count=true&size=large", + "httpVersion": "HTTP/3", + "headers": [ + { + "name": "Host", + "value": "mitmproxy.org" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + }, + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.5" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Referer", + "value": "https://mitmproxy.org/" + }, + { + "name": "DNT", + "value": "1" + }, + { + "name": "Alt-Used", + "value": "mitmproxy.org" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "Sec-Fetch-Dest", + "value": "iframe" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "same-origin" + }, + { + "name": "If-Modified-Since", + "value": "Sat, 04 Mar 2023 16:01:11 GMT" + }, + { + "name": "If-None-Match", + "value": "W/\"8d3963829b6394c8c198172e36049e5e\"" + }, + { + "name": "TE", + "value": "trailers" + } + ], + "cookies": [], + "queryString": [ + { + "name": "user", + "value": "mitmproxy" + }, + { + "name": "repo", + "value": "mitmproxy" + }, + { + "name": "type", + "value": "star" + }, + { + "name": "count", + "value": "true" + }, + { + "name": "size", + "value": "large" + } + ], + "headersSize": 680 + }, + "response": { + "status": 304, + "statusText": "Not Modified", + "httpVersion": "HTTP/3", + "headers": [ + { + "name": "age", + "value": "13417" + }, + { + "name": "date", + "value": "Wed, 29 Mar 2023 23:58:59 GMT" + }, + { + "name": "server", + "value": "AmazonS3" + }, + { + "name": "etag", + "value": "W/\"8d3963829b6394c8c198172e36049e5e\"" + }, + { + "name": "vary", + "value": "Accept-Encoding" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + }, + { + "name": "via", + "value": "1.1 5fa120f79d5713714191c32768eca58c.cloudfront.net (CloudFront)" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "x-amz-cf-id", + "value": "PMTLvP_yUCVocnhd1i1ir7_FRAJRw0ayMhK3KaZKELDO3pxxoqLWjg==" + } + ], + "cookies": [], + "content": { + "mimeType": "text/html", + "size": 9689, + "text": "\n\n\n \n \n \n \n \n \n\n\n \n \n \n \n \n \n \n \n\n\n" + }, + "redirectURL": "", + "headersSize": 386, + "bodySize": 3756 + }, + "cache": { + "afterRequest": { + "expires": "4294967295", + "lastFetched": "", + "fetchCount": "", + "_dataSize": "", + "_lastModified": "1680134302", + "_device": "" + } + }, + "timings": { + "blocked": -1, + "dns": 0, + "connect": 0, + "ssl": 0, + "send": 0, + "wait": 7, + "receive": 0 + }, + "time": 7, + "_securityState": "secure", + "serverIPAddress": "13.35.121.55", + "connection": "443" + }, + { + "pageref": "page_1", + "startedDateTime": "2023-03-29T16:58:59.543-07:00", + "request": { + "bodySize": 0, + "method": "GET", + "url": "https://s3-us-west-2.amazonaws.com/snapshots.mitmproxy.org?delimiter=/&prefix=", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Host", + "value": "s3-us-west-2.amazonaws.com" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + }, + { + "name": "Accept", + "value": "*/*" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.5" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Referer", + "value": "https://mitmproxy.org/" + }, + { + "name": "Origin", + "value": "https://mitmproxy.org" + }, + { + "name": "DNT", + "value": "1" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Sec-Fetch-Dest", + "value": "empty" + }, + { + "name": "Sec-Fetch-Mode", + "value": "cors" + }, + { + "name": "Sec-Fetch-Site", + "value": "cross-site" + } + ], + "cookies": [], + "queryString": [ + { + "name": "delimiter", + "value": "/" + }, + { + "name": "prefix", + "value": "" + } + ], + "headersSize": 444 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "x-amz-id-2", + "value": "Wea5PituXz4XdzoNJ+QC6XDgoU/3V7l0magzJrS+LUtCBaNXHc1HwMDB2zvhYirPY8+6SfU08Bk=" + }, + { + "name": "x-amz-request-id", + "value": "299PK1CFMV40HV7M" + }, + { + "name": "Date", + "value": "Wed, 29 Mar 2023 23:59:00 GMT" + }, + { + "name": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "name": "Access-Control-Allow-Methods", + "value": "GET" + }, + { + "name": "Access-Control-Max-Age", + "value": "3000" + }, + { + "name": "Vary", + "value": "Origin, Access-Control-Request-Headers, Access-Control-Request-Method" + }, + { + "name": "x-amz-bucket-region", + "value": "us-west-2" + }, + { + "name": "Content-Type", + "value": "application/xml" + }, + { + "name": "Transfer-Encoding", + "value": "chunked" + }, + { + "name": "Server", + "value": "AmazonS3" + } + ], + "cookies": [], + "content": { + "mimeType": "application/xml", + "size": 3406, + "text": "\nsnapshots.mitmproxy.org1000/falseerror.html2018-03-07T22:33:17.000Z"f5abfbae6b5f0fbf3002d22a00804488"426STANDARDindex.html2018-03-07T22:33:11.000Z"f5abfbae6b5f0fbf3002d22a00804488"426STANDARDlist.js2018-03-07T22:33:14.000Z"2662f70064b002b56bd6768e017efaa9"6790STANDARD0.15/0.16/0.17.1/0.17/0.18.1/0.18.2/0.18.3/0.18/0.19/1.0.0/1.0.1/1.0.2/2.0.0/2.0.1/2.0.2/3.0.0/3.0.1/3.0.2/3.0.3/3.0.4/4.0.0/4.0.1/4.0.2/4.0.3/4.0.4/5.0.0/5.0.1/5.1.0/5.1.1/5.2/5.3.0/6.0.0/6.0.1/6.0.2/7.0.0/7.0.1/7.0.2/7.0.3/7.0.4/8.0.0/8.1.0/8.1.1/9.0.0/9.0.1/branches/" + }, + "redirectURL": "", + "headersSize": 465, + "bodySize": 3871 + }, + "cache": {}, + "timings": { + "blocked": 89, + "dns": 0, + "connect": 32, + "ssl": 66, + "send": 0, + "wait": 60, + "receive": 0 + }, + "time": 247, + "_securityState": "secure", + "serverIPAddress": "52.218.234.208", + "connection": "443" + }, + { + "pageref": "page_1", + "startedDateTime": "2023-03-29T16:58:59.639-07:00", + "request": { + "bodySize": 0, + "method": "GET", + "url": "https://mitmproxy.org/data/github-stats.json", + "httpVersion": "HTTP/3", + "headers": [ + { + "name": "Host", + "value": "mitmproxy.org" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + }, + { + "name": "Accept", + "value": "*/*" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.5" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "DNT", + "value": "1" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Sec-Fetch-Dest", + "value": "empty" + }, + { + "name": "Sec-Fetch-Mode", + "value": "cors" + }, + { + "name": "Sec-Fetch-Site", + "value": "same-origin" + }, + { + "name": "If-Modified-Since", + "value": "Wed, 29 Mar 2023 23:55:21 GMT" + }, + { + "name": "If-None-Match", + "value": "W/\"07201abd774cb0523be31d94fffe67a3\"" + }, + { + "name": "TE", + "value": "trailers" + } + ], + "cookies": [], + "queryString": [], + "headersSize": 450 + }, + "response": { + "status": 304, + "statusText": "Not Modified", + "httpVersion": "HTTP/3", + "headers": [ + { + "name": "age", + "value": "205" + }, + { + "name": "date", + "value": "Wed, 29 Mar 2023 23:58:59 GMT" + }, + { + "name": "etag", + "value": "W/\"07201abd774cb0523be31d94fffe67a3\"" + }, + { + "name": "server", + "value": "AmazonS3" + }, + { + "name": "vary", + "value": "Accept-Encoding" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + }, + { + "name": "via", + "value": "1.1 5fa120f79d5713714191c32768eca58c.cloudfront.net (CloudFront)" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "x-amz-cf-id", + "value": "0okGWJw6nYo7R-4egQWE-WfonThN2EXyRSLO9MlCNKyMfD-2v1AU0Q==" + } + ], + "cookies": [], + "content": { + "mimeType": "application/json", + "size": 6986, + "text": "{\n \"id\": 519832,\n \"node_id\": \"MDEwOlJlcG9zaXRvcnk1MTk4MzI=\",\n \"name\": \"mitmproxy\",\n \"full_name\": \"mitmproxy/mitmproxy\",\n \"private\": false,\n \"owner\": {\n \"login\": \"mitmproxy\",\n \"id\": 4652787,\n \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjQ2NTI3ODc=\",\n \"avatar_url\": \"https://avatars.githubusercontent.com/u/4652787?v=4\",\n \"gravatar_id\": \"\",\n \"url\": \"https://api.github.com/users/mitmproxy\",\n \"html_url\": \"https://github.com/mitmproxy\",\n \"followers_url\": \"https://api.github.com/users/mitmproxy/followers\",\n \"following_url\": \"https://api.github.com/users/mitmproxy/following{/other_user}\",\n \"gists_url\": \"https://api.github.com/users/mitmproxy/gists{/gist_id}\",\n \"starred_url\": \"https://api.github.com/users/mitmproxy/starred{/owner}{/repo}\",\n \"subscriptions_url\": \"https://api.github.com/users/mitmproxy/subscriptions\",\n \"organizations_url\": \"https://api.github.com/users/mitmproxy/orgs\",\n \"repos_url\": \"https://api.github.com/users/mitmproxy/repos\",\n \"events_url\": \"https://api.github.com/users/mitmproxy/events{/privacy}\",\n \"received_events_url\": \"https://api.github.com/users/mitmproxy/received_events\",\n \"type\": \"Organization\",\n \"site_admin\": false\n },\n \"html_url\": \"https://github.com/mitmproxy/mitmproxy\",\n \"description\": \"An interactive TLS-capable intercepting HTTP proxy for penetration testers and software developers.\",\n \"fork\": false,\n \"url\": \"https://api.github.com/repos/mitmproxy/mitmproxy\",\n \"forks_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/forks\",\n \"keys_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/keys{/key_id}\",\n \"collaborators_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/collaborators{/collaborator}\",\n \"teams_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/teams\",\n \"hooks_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/hooks\",\n \"issue_events_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/issues/events{/number}\",\n \"events_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/events\",\n \"assignees_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/assignees{/user}\",\n \"branches_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/branches{/branch}\",\n \"tags_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/tags\",\n \"blobs_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/git/blobs{/sha}\",\n \"git_tags_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/git/tags{/sha}\",\n \"git_refs_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/git/refs{/sha}\",\n \"trees_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/git/trees{/sha}\",\n \"statuses_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/statuses/{sha}\",\n \"languages_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/languages\",\n \"stargazers_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/stargazers\",\n \"contributors_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/contributors\",\n \"subscribers_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/subscribers\",\n \"subscription_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/subscription\",\n \"commits_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/commits{/sha}\",\n \"git_commits_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/git/commits{/sha}\",\n \"comments_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/comments{/number}\",\n \"issue_comment_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/issues/comments{/number}\",\n \"contents_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/contents/{+path}\",\n \"compare_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/compare/{base}...{head}\",\n \"merges_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/merges\",\n \"archive_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/{archive_format}{/ref}\",\n \"downloads_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/downloads\",\n \"issues_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/issues{/number}\",\n \"pulls_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/pulls{/number}\",\n \"milestones_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/milestones{/number}\",\n \"notifications_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/notifications{?since,all,participating}\",\n \"labels_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/labels{/name}\",\n \"releases_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/releases{/id}\",\n \"deployments_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/deployments\",\n \"created_at\": \"2010-02-16T04:10:13Z\",\n \"updated_at\": \"2023-03-29T21:46:17Z\",\n \"pushed_at\": \"2023-03-29T11:13:54Z\",\n \"git_url\": \"git://github.com/mitmproxy/mitmproxy.git\",\n \"ssh_url\": \"git@github.com:mitmproxy/mitmproxy.git\",\n \"clone_url\": \"https://github.com/mitmproxy/mitmproxy.git\",\n \"svn_url\": \"https://github.com/mitmproxy/mitmproxy\",\n \"homepage\": \"https://mitmproxy.org\",\n \"size\": 57388,\n \"stargazers_count\": 30554,\n \"watchers_count\": 30554,\n \"language\": \"Python\",\n \"has_issues\": true,\n \"has_projects\": true,\n \"has_downloads\": true,\n \"has_wiki\": false,\n \"has_pages\": false,\n \"has_discussions\": true,\n \"forks_count\": 3658,\n \"mirror_url\": null,\n \"archived\": false,\n \"disabled\": false,\n \"open_issues_count\": 260,\n \"license\": {\n \"key\": \"mit\",\n \"name\": \"MIT License\",\n \"spdx_id\": \"MIT\",\n \"url\": \"https://api.github.com/licenses/mit\",\n \"node_id\": \"MDc6TGljZW5zZTEz\"\n },\n \"allow_forking\": true,\n \"is_template\": false,\n \"web_commit_signoff_required\": false,\n \"topics\": [\n \"debugging\",\n \"http\",\n \"http2\",\n \"man-in-the-middle\",\n \"mitmproxy\",\n \"proxy\",\n \"python\",\n \"security\",\n \"ssl\",\n \"tls\",\n \"websocket\"\n ],\n \"visibility\": \"public\",\n \"forks\": 3658,\n \"open_issues\": 260,\n \"watchers\": 30554,\n \"default_branch\": \"main\",\n \"temp_clone_token\": null,\n \"organization\": {\n \"login\": \"mitmproxy\",\n \"id\": 4652787,\n \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjQ2NTI3ODc=\",\n \"avatar_url\": \"https://avatars.githubusercontent.com/u/4652787?v=4\",\n \"gravatar_id\": \"\",\n \"url\": \"https://api.github.com/users/mitmproxy\",\n \"html_url\": \"https://github.com/mitmproxy\",\n \"followers_url\": \"https://api.github.com/users/mitmproxy/followers\",\n \"following_url\": \"https://api.github.com/users/mitmproxy/following{/other_user}\",\n \"gists_url\": \"https://api.github.com/users/mitmproxy/gists{/gist_id}\",\n \"starred_url\": \"https://api.github.com/users/mitmproxy/starred{/owner}{/repo}\",\n \"subscriptions_url\": \"https://api.github.com/users/mitmproxy/subscriptions\",\n \"organizations_url\": \"https://api.github.com/users/mitmproxy/orgs\",\n \"repos_url\": \"https://api.github.com/users/mitmproxy/repos\",\n \"events_url\": \"https://api.github.com/users/mitmproxy/events{/privacy}\",\n \"received_events_url\": \"https://api.github.com/users/mitmproxy/received_events\",\n \"type\": \"Organization\",\n \"site_admin\": false\n },\n \"network_count\": 3658,\n \"subscribers_count\": 619\n}\n" + }, + "redirectURL": "", + "headersSize": 384, + "bodySize": 1821 + }, + "cache": { + "afterRequest": { + "expires": "4294967295", + "lastFetched": "", + "fetchCount": "", + "_dataSize": "", + "_lastModified": "1680134339", + "_device": "" + } + }, + "timings": { + "blocked": -1, + "dns": 0, + "connect": 0, + "ssl": 0, + "send": 0, + "wait": 7, + "receive": 0 + }, + "time": 7, + "_securityState": "secure", + "serverIPAddress": "13.35.121.55", + "connection": "443" + }, + { + "pageref": "page_1", + "startedDateTime": "2023-03-29T16:58:59.643-07:00", + "request": { + "bodySize": 0, + "method": "GET", + "url": "https://mitmproxy.org/favicon.ico", + "httpVersion": "HTTP/3", + "headers": [ + { + "name": "Host", + "value": "mitmproxy.org" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + }, + { + "name": "Accept", + "value": "image/avif,image/webp,*/*" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.5" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "DNT", + "value": "1" + }, + { + "name": "Alt-Used", + "value": "mitmproxy.org" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Referer", + "value": "https://mitmproxy.org/" + }, + { + "name": "Sec-Fetch-Dest", + "value": "image" + }, + { + "name": "Sec-Fetch-Mode", + "value": "no-cors" + }, + { + "name": "Sec-Fetch-Site", + "value": "same-origin" + } + ], + "cookies": [], + "queryString": [], + "headersSize": null + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/3", + "headers": [ + { + "name": "content-type", + "value": "image/vnd.microsoft.icon" + }, + { + "name": "content-length", + "value": "98065" + }, + { + "name": "age", + "value": "62116" + }, + { + "name": "last-modified", + "value": "Sat, 04 Mar 2023 16:01:11 GMT" + }, + { + "name": "server", + "value": "AmazonS3" + }, + { + "name": "date", + "value": "Wed, 29 Mar 2023 06:43:07 GMT" + }, + { + "name": "etag", + "value": "\"0f8a699781bb4a5a204a467db88dd555\"" + }, + { + "name": "vary", + "value": "Accept-Encoding" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + }, + { + "name": "via", + "value": "1.1 28663e5849ed20a9d037ca8066957990.cloudfront.net (CloudFront)" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "x-amz-cf-id", + "value": "dkVeTGCIfZ6m6k9VkhQJKLdxjemd8oSJPwnCEX6Tdimqsa5n2CJybg==" + } + ], + "cookies": [], + "content": { + "mimeType": "image/vnd.microsoft.icon", + "size": 98065, + "encoding": "base64", + "text": "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/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/h4eH/////////////////////////////////////////////////////////////f39/3d3d/8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/BAQE/zw8PP+dnZ3/3d3d//b29v/9/f3//f39//b29v/c3Nz/np6e/z09Pf8EBAT/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/f35+/296g/8Ra7b/G5H0/xFlrf99hIr/cnJx/wAAAP8AAAD/AAAA/xsbG//U1NT//////9HR0f8XFxf/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/CgoK/76+vv//////5ubm/5CQkP+IiIj/iYmJ/4mJif+JiYn/iYmJ/4mJif+JiYn/ioqK/3p6ev8WFhb/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/EBAQ/4KCgv/s7Oz////////////////////+/////v/////////////////s7Oz/goKC/xAQEP8AAAD/AAAA/wAAAP8AAAD/AAAA/4+Ojf9Xann/F4Pe/x+Y/v8WftX/Y3J+/4OCgv8AAAD/AAAA/wAAAP8AAAD/jIyM///////39/f/TExM/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/yIiIv/i4uL//////62trf8EBAT/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/EBAQ/5ycnP/8/Pz///////j39//CwcD/goGB/2JjZP9iY2T/g4GA/8LBwP/39/f///////z8/P+dnZ3/EBAQ/wAAAP8AAAD/AAAA/wAAAP9gYF//mp2g/xpSgf8Tbrv/HlF8/6Okpv9TU1P/AAAA/wAAAP8AAAD/AAAA/0ZGRv/29vb//////4uLi/8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP9AQED/9fX1//////+AgID/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AwMD/4SEhP/9/f3//////97d3f9iY2T/FjBH/w1MgP8QX6L/EF+i/w1MgP8VMEb/YmNk/97d3f///////f39/4ODg/8DAwP/AAAA/wAAAP8AAAD/Dg4O/42Njf+nqKj/hYqP/6qrq/+FhIT/CgoK/wAAAP8AAAD/AAAA/wAAAP8ZGRn/1tbW///////AwMD/CwsL/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/XV5e//7+/v/+/v7/XV1d/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/z4+Pv/s7Oz//////93d3P9ARkr/DEl8/xd3x/8RWJH/DT1n/w09Z/8SWJL/F3fI/wxJfP9BRkr/3t3d///////r6+v/Pj4+/wAAAP8AAAD/AAAA/wAAAP8KCgr/QUFB/2BfX/88PDz/BwcH/wAAAP8AAAD/AAAA/wAAAP8AAAD/BAQE/6qqqv//////4+Pj/yQkJP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/3Z2dv//////9/f3/0RERP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wMDA/+enp7///////j4+P9gYWL/DEl8/xdxu/8JJUT/ESRi/xo4mv8aOJr/ESVi/wklRP8Xcbz/DEl8/2FiY//4+Pj//////52dnf8DAwP/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP9/f3////////b29v9CQkL/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP+Ghob///////Dw8P80NDT/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8eHh7/3Nzc///////CwcD/FjFI/xd3yP8JJUT/GjiZ/yla8P8qXPX/Klz1/yla8P8aOJn/CSVE/xd3x/8XMUj/w8HA///////c3Nz/Hh4e/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/W1tb//39/f/+/v7/X19f/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/j4+P///////z8/P/eXl5/1lZWf9bW1v/W1tb/1tbW/9bW1v/W1tb/1tbW/9bW1v/W1tb/1tbW/9bW1v/W1tb/1tbW/9YWFj/gICA//f39///////g4KB/w1MgP8SWJL/ESVj/yla8P8pWvL/KVrx/yla8f8pWvL/KVrw/xElY/8SWJL/DUyA/4OCgf//////9/f3/4CAgP9YWFj/W1tb/1paWv9bW1v/W1tb/1paWv9bW1v/W1tb/1paWv9bW1v/W1tb/1paWv9bW1v/WFhY/4iIiP/7+/v//////3Z2dv8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/4+Pj/////////////7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v///////////2FjZP8QX6H/DT5o/xo5mv8qXPX/KVrx/yla8f8pWvH/KVrx/ypc9f8aOZr/DT5o/xBfof9iY2T////////////+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7///////////+Hh4f/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP+Hh4f////////////+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7///////////9hY2T/EF+h/w09Z/8aOZr/Klz1/yla8f8pWvH/KVrx/yla8f8qXPX/Gjma/w0+aP8QX6H/YmNk/////////////v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+//7+/v/+/v7//v7+////////////jo6O/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/d3d3///////7+/v/iYmJ/1paWv9cXFz/XFxc/1xcXP9cXFz/XFxc/1xcXP9cXFz/XFxc/1xcXP9cXFz/XFxc/1xcXP9aWlr/gYGB//f39///////goGA/w1Mgf8RWJH/ESVk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/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAADaAAAATwAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAAnwAAAOwAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAOwAAACfAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAABBAAAAlQAAANMAAADzAAAA/gAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP4AAADzAAAA0wAAAJUAAABBAAAABwAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAABAwAAAAAAAADEAAAAAAAAACwAAAAAAAAAOAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAABwAAAAAAAAANAAAAAAAAAAjAAAAAAAAAMCAAAAAAAABAoAAAAMAAAAGAAAAABACAAAAAAAAAkAADXDQAA1w0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAEgAAACnAAAA4wAAAPsAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAPsAAADjAAAApwAAAEgAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAkQAAAO4AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAO4AAACRAAAAGAAAAAAAAAAAAAAAAAAAABgAAACwAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAsAAAABgAAAAAAAAABgAAAJEAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA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/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAsQAAABgAAAAAAAAAAAAAAAAAAAAYAAAAkgAAAO8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAO8AAACSAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAEkAAACoAAAA5AAAAPsAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAPsAAADkAAAAqAAAAEkAAAAHAAAAAAAAAAAAAAAAIAAAAAAEAABAAAAAAAIAAIAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAABAABAAAAAAAIAACAAAAAABAAAKAAAACAAAABAAAAAAQAgAAAAAAAAEAAA1w0A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" + }, + "redirectURL": "", + "headersSize": 0, + "bodySize": 98065 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": 0, + "ssl": 0, + "connect": 0, + "send": 0, + "wait": 0, + "receive": 0 + }, + "time": 0, + "_securityState": "secure" + } + ] + } + } \ No newline at end of file diff --git a/test/mitmproxy/data/har_files/firefox.json b/test/mitmproxy/data/har_files/firefox.json new file mode 100644 index 0000000000..e3b701447f --- /dev/null +++ b/test/mitmproxy/data/har_files/firefox.json @@ -0,0 +1,2092 @@ +[ + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680134339.303, + "timestamp_tls_setup": null, + "timestamp_end": 1680134339.326 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": [ + "13.35.121.55", + 443 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/", + "http_version": "HTTP/3", + "headers": [ + [ + "Host", + "mitmproxy.org" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + ], + [ + "Accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" + ], + [ + "Accept-Language", + "en-US,en;q=0.5" + ], + [ + "Accept-Encoding", + "gzip, deflate, br" + ], + [ + "Referer", + "https://www.google.com/" + ], + [ + "DNT", + "1" + ], + [ + "Alt-Used", + "mitmproxy.org" + ], + [ + "Connection", + "keep-alive" + ], + [ + "Upgrade-Insecure-Requests", + "1" + ], + [ + "Sec-Fetch-Dest", + "document" + ], + [ + "Sec-Fetch-Mode", + "navigate" + ], + [ + "Sec-Fetch-Site", + "cross-site" + ], + [ + "Sec-Fetch-User", + "?1" + ], + [ + "If-Modified-Since", + "Sat, 04 Mar 2023 18:02:08 GMT" + ], + [ + "If-None-Match", + "W/\"a1550c2bd25c5bcfef789d730f5bbddf\"" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134339.303, + "timestamp_end": 1680134339.326, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/3", + "status_code": 304, + "reason": "Not Modified", + "headers": [ + [ + "age", + "32345" + ], + [ + "date", + "Wed, 29 Mar 2023 23:58:59 GMT" + ], + [ + "server", + "AmazonS3" + ], + [ + "etag", + "W/\"a1550c2bd25c5bcfef789d730f5bbddf\"" + ], + [ + "vary", + "Accept-Encoding" + ], + [ + "x-cache", + "Hit from cloudfront" + ], + [ + "via", + "1.1 5fa120f79d5713714191c32768eca58c.cloudfront.net (CloudFront)" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "alt-svc", + "h3=\":443\"; ma=86400" + ], + [ + "x-amz-cf-id", + "DPEkuUbeK1ZXMsRuUHqgk6iE4l7ShgyrJntkqIbLaSJ5646Ptc2Xew==" + ] + ], + "contentLength": 23866, + "contentHash": "7fd5f643a86976f5711df86ae2d5f9f8137a47c705dee31ccc550215564a5364", + "timestamp_start": 1680134339.303, + "timestamp_end": 1680134339.326 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680134339.418, + "timestamp_tls_setup": null, + "timestamp_end": 1680134339.418 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/logo-navbar.png", + "http_version": "HTTP/1.1", + "headers": [ + [ + "Host", + "mitmproxy.org" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + ], + [ + "Accept", + "image/avif,image/webp,*/*" + ], + [ + "Accept-Language", + "en-US,en;q=0.5" + ], + [ + "Accept-Encoding", + "gzip, deflate, br" + ], + [ + "Referer", + "https://mitmproxy.org/" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134339.418, + "timestamp_end": 1680134339.418, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134339.418, + "timestamp_end": 1680134339.418 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680134339.456, + "timestamp_tls_setup": null, + "timestamp_end": 1680134339.456 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/screenshot.png", + "http_version": "HTTP/1.1", + "headers": [ + [ + "Host", + "mitmproxy.org" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + ], + [ + "Accept", + "image/avif,image/webp,*/*" + ], + [ + "Accept-Language", + "en-US,en;q=0.5" + ], + [ + "Accept-Encoding", + "gzip, deflate, br" + ], + [ + "Referer", + "https://mitmproxy.org/" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134339.456, + "timestamp_end": 1680134339.456, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134339.456, + "timestamp_end": 1680134339.456 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680134339.457, + "timestamp_tls_setup": null, + "timestamp_end": 1680134339.457 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/mitmweb.png", + "http_version": "HTTP/1.1", + "headers": [ + [ + "Host", + "mitmproxy.org" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + ], + [ + "Accept", + "image/avif,image/webp,*/*" + ], + [ + "Accept-Language", + "en-US,en;q=0.5" + ], + [ + "Accept-Encoding", + "gzip, deflate, br" + ], + [ + "Referer", + "https://mitmproxy.org/" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134339.457, + "timestamp_end": 1680134339.457, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134339.457, + "timestamp_end": 1680134339.457 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680134339.457, + "timestamp_tls_setup": null, + "timestamp_end": 1680134339.457 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/sponsors/proxyman.png", + "http_version": "HTTP/1.1", + "headers": [ + [ + "Host", + "mitmproxy.org" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + ], + [ + "Accept", + "image/avif,image/webp,*/*" + ], + [ + "Accept-Language", + "en-US,en;q=0.5" + ], + [ + "Accept-Encoding", + "gzip, deflate, br" + ], + [ + "Referer", + "https://mitmproxy.org/" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134339.457, + "timestamp_end": 1680134339.457, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134339.457, + "timestamp_end": 1680134339.457 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680134339.458, + "timestamp_tls_setup": null, + "timestamp_end": 1680134339.458 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/sponsors/netograph.svg", + "http_version": "HTTP/1.1", + "headers": [ + [ + "Host", + "mitmproxy.org" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + ], + [ + "Accept", + "image/avif,image/webp,*/*" + ], + [ + "Accept-Language", + "en-US,en;q=0.5" + ], + [ + "Accept-Encoding", + "gzip, deflate, br" + ], + [ + "Referer", + "https://mitmproxy.org/" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134339.458, + "timestamp_end": 1680134339.458, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134339.458, + "timestamp_end": 1680134339.458 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680134339.495, + "timestamp_tls_setup": null, + "timestamp_end": 1680134339.495 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/polyfills.js", + "http_version": "HTTP/3", + "headers": [ + [ + "Host", + "mitmproxy.org" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Language", + "en-US,en;q=0.5" + ], + [ + "Accept-Encoding", + "gzip, deflate, br" + ], + [ + "DNT", + "1" + ], + [ + "Alt-Used", + "mitmproxy.org" + ], + [ + "Connection", + "keep-alive" + ], + [ + "Referer", + "https://mitmproxy.org/" + ], + [ + "Sec-Fetch-Dest", + "script" + ], + [ + "Sec-Fetch-Mode", + "no-cors" + ], + [ + "Sec-Fetch-Site", + "same-origin" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134339.495, + "timestamp_end": 1680134339.495, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/3", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "content-type", + "text/javascript" + ], + [ + "alt-svc", + "h3=\":443\"; ma=86400" + ], + [ + "age", + "14777" + ], + [ + "last-modified", + "Sat, 04 Mar 2023 16:01:12 GMT" + ], + [ + "server", + "AmazonS3" + ], + [ + "content-encoding", + "gzip" + ], + [ + "date", + "Wed, 29 Mar 2023 19:52:06 GMT" + ], + [ + "etag", + "W/\"542d62f852e229d44f16469475b7500b\"" + ], + [ + "vary", + "Accept-Encoding" + ], + [ + "x-cache", + "Hit from cloudfront" + ], + [ + "via", + "1.1 28663e5849ed20a9d037ca8066957990.cloudfront.net (CloudFront)" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-amz-cf-id", + "EufbwzXQheESUTNil_OCjKK8cvVL51cQpYfmF7iZSHloCTvVhfZ8yQ==" + ] + ], + "contentLength": 3969, + "contentHash": "3c4880b4b424071aa5e5c5f652b934179099ee8786ea67520b2fadbc4305e5a8", + "timestamp_start": 1680134339.495, + "timestamp_end": 1680134339.495 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680134339.498, + "timestamp_tls_setup": null, + "timestamp_end": 1680134339.498 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/clipboard.min.js", + "http_version": "HTTP/3", + "headers": [ + [ + "Host", + "mitmproxy.org" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Language", + "en-US,en;q=0.5" + ], + [ + "Accept-Encoding", + "gzip, deflate, br" + ], + [ + "DNT", + "1" + ], + [ + "Alt-Used", + "mitmproxy.org" + ], + [ + "Connection", + "keep-alive" + ], + [ + "Referer", + "https://mitmproxy.org/" + ], + [ + "Sec-Fetch-Dest", + "script" + ], + [ + "Sec-Fetch-Mode", + "no-cors" + ], + [ + "Sec-Fetch-Site", + "same-origin" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134339.498, + "timestamp_end": 1680134339.498, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/3", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "content-type", + "text/javascript" + ], + [ + "alt-svc", + "h3=\":443\"; ma=86400" + ], + [ + "age", + "28518" + ], + [ + "last-modified", + "Sat, 04 Mar 2023 16:01:11 GMT" + ], + [ + "server", + "AmazonS3" + ], + [ + "content-encoding", + "gzip" + ], + [ + "date", + "Wed, 29 Mar 2023 16:03:05 GMT" + ], + [ + "etag", + "W/\"af8ab36589315582ccdd82f22e84bffb\"" + ], + [ + "vary", + "Accept-Encoding" + ], + [ + "x-cache", + "Hit from cloudfront" + ], + [ + "via", + "1.1 28663e5849ed20a9d037ca8066957990.cloudfront.net (CloudFront)" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-amz-cf-id", + "UuHHTyGhTqqTO07G_oZCw7rxjb9RJUGTN3OW0EUS77RH4GiQ-LkAvw==" + ] + ], + "contentLength": 3346, + "contentHash": "e6c21f88023539515a971172a00e500b7e4444fbf9506e47ceee126ace246808", + "timestamp_start": 1680134339.498, + "timestamp_end": 1680134339.498 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680134339.5, + "timestamp_tls_setup": null, + "timestamp_end": 1680134339.5 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/snapshots.js", + "http_version": "HTTP/3", + "headers": [ + [ + "Host", + "mitmproxy.org" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Language", + "en-US,en;q=0.5" + ], + [ + "Accept-Encoding", + "gzip, deflate, br" + ], + [ + "DNT", + "1" + ], + [ + "Alt-Used", + "mitmproxy.org" + ], + [ + "Connection", + "keep-alive" + ], + [ + "Referer", + "https://mitmproxy.org/" + ], + [ + "Sec-Fetch-Dest", + "script" + ], + [ + "Sec-Fetch-Mode", + "no-cors" + ], + [ + "Sec-Fetch-Site", + "same-origin" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134339.5, + "timestamp_end": 1680134339.5, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/3", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "content-type", + "text/javascript" + ], + [ + "alt-svc", + "h3=\":443\"; ma=86400" + ], + [ + "age", + "76473" + ], + [ + "last-modified", + "Sat, 04 Mar 2023 16:01:14 GMT" + ], + [ + "server", + "AmazonS3" + ], + [ + "content-encoding", + "gzip" + ], + [ + "date", + "Wed, 29 Mar 2023 02:43:50 GMT" + ], + [ + "etag", + "W/\"20a8c9dba8b59dc27b96998053650836\"" + ], + [ + "vary", + "Accept-Encoding" + ], + [ + "x-cache", + "Hit from cloudfront" + ], + [ + "via", + "1.1 28663e5849ed20a9d037ca8066957990.cloudfront.net (CloudFront)" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-amz-cf-id", + "TOLqHpQWMFHQDWnv2yHHFWI5xkA3R13TTJQDJe1ARViKrgihxZdhxA==" + ] + ], + "contentLength": 794, + "contentHash": "43cb84ef784bafbab5472abc7c396d95fc4468973d6501c83709e40963b2a953", + "timestamp_start": 1680134339.5, + "timestamp_end": 1680134339.5 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680134339.527, + "timestamp_tls_setup": null, + "timestamp_end": 1680134339.533 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": [ + "13.35.121.55", + 443 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/github-btn.html?user=mhils&type=sponsor&size=large", + "http_version": "HTTP/3", + "headers": [ + [ + "Host", + "mitmproxy.org" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + ], + [ + "Accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" + ], + [ + "Accept-Language", + "en-US,en;q=0.5" + ], + [ + "Accept-Encoding", + "gzip, deflate, br" + ], + [ + "Referer", + "https://mitmproxy.org/" + ], + [ + "DNT", + "1" + ], + [ + "Alt-Used", + "mitmproxy.org" + ], + [ + "Connection", + "keep-alive" + ], + [ + "Upgrade-Insecure-Requests", + "1" + ], + [ + "Sec-Fetch-Dest", + "iframe" + ], + [ + "Sec-Fetch-Mode", + "navigate" + ], + [ + "Sec-Fetch-Site", + "same-origin" + ], + [ + "If-Modified-Since", + "Sat, 04 Mar 2023 16:01:11 GMT" + ], + [ + "If-None-Match", + "W/\"8d3963829b6394c8c198172e36049e5e\"" + ], + [ + "TE", + "trailers" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134339.527, + "timestamp_end": 1680134339.533, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/3", + "status_code": 304, + "reason": "Not Modified", + "headers": [ + [ + "age", + "13417" + ], + [ + "date", + "Wed, 29 Mar 2023 23:58:59 GMT" + ], + [ + "server", + "AmazonS3" + ], + [ + "etag", + "W/\"8d3963829b6394c8c198172e36049e5e\"" + ], + [ + "vary", + "Accept-Encoding" + ], + [ + "x-cache", + "Hit from cloudfront" + ], + [ + "via", + "1.1 5fa120f79d5713714191c32768eca58c.cloudfront.net (CloudFront)" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "alt-svc", + "h3=\":443\"; ma=86400" + ], + [ + "x-amz-cf-id", + "nnIrWtgAMt42ua4HYBtNAao6m_iD9WjIzLAFyURb8mjOr5MriSQXRA==" + ] + ], + "contentLength": 9689, + "contentHash": "a4dcfad01ab92fbd09cad3477fb26184fbb26f164d1302ee79489519b280e22a", + "timestamp_start": 1680134339.527, + "timestamp_end": 1680134339.533 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680134339.528, + "timestamp_tls_setup": null, + "timestamp_end": 1680134339.535 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": [ + "13.35.121.55", + 443 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/github-btn.html?user=mitmproxy&repo=mitmproxy&type=star&count=true&size=large", + "http_version": "HTTP/3", + "headers": [ + [ + "Host", + "mitmproxy.org" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + ], + [ + "Accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" + ], + [ + "Accept-Language", + "en-US,en;q=0.5" + ], + [ + "Accept-Encoding", + "gzip, deflate, br" + ], + [ + "Referer", + "https://mitmproxy.org/" + ], + [ + "DNT", + "1" + ], + [ + "Alt-Used", + "mitmproxy.org" + ], + [ + "Connection", + "keep-alive" + ], + [ + "Upgrade-Insecure-Requests", + "1" + ], + [ + "Sec-Fetch-Dest", + "iframe" + ], + [ + "Sec-Fetch-Mode", + "navigate" + ], + [ + "Sec-Fetch-Site", + "same-origin" + ], + [ + "If-Modified-Since", + "Sat, 04 Mar 2023 16:01:11 GMT" + ], + [ + "If-None-Match", + "W/\"8d3963829b6394c8c198172e36049e5e\"" + ], + [ + "TE", + "trailers" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134339.528, + "timestamp_end": 1680134339.535, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/3", + "status_code": 304, + "reason": "Not Modified", + "headers": [ + [ + "age", + "13417" + ], + [ + "date", + "Wed, 29 Mar 2023 23:58:59 GMT" + ], + [ + "server", + "AmazonS3" + ], + [ + "etag", + "W/\"8d3963829b6394c8c198172e36049e5e\"" + ], + [ + "vary", + "Accept-Encoding" + ], + [ + "x-cache", + "Hit from cloudfront" + ], + [ + "via", + "1.1 5fa120f79d5713714191c32768eca58c.cloudfront.net (CloudFront)" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "alt-svc", + "h3=\":443\"; ma=86400" + ], + [ + "x-amz-cf-id", + "PMTLvP_yUCVocnhd1i1ir7_FRAJRw0ayMhK3KaZKELDO3pxxoqLWjg==" + ] + ], + "contentLength": 9689, + "contentHash": "a4dcfad01ab92fbd09cad3477fb26184fbb26f164d1302ee79489519b280e22a", + "timestamp_start": 1680134339.528, + "timestamp_end": 1680134339.535 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680134339.543, + "timestamp_tls_setup": null, + "timestamp_end": 1680134339.79 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": [ + "52.218.234.208", + 443 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "s3-us-west-2.amazonaws.com", + "port": 443, + "path": "/snapshots.mitmproxy.org?delimiter=/&prefix=", + "http_version": "HTTP/1.1", + "headers": [ + [ + "Host", + "s3-us-west-2.amazonaws.com" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Language", + "en-US,en;q=0.5" + ], + [ + "Accept-Encoding", + "gzip, deflate, br" + ], + [ + "Referer", + "https://mitmproxy.org/" + ], + [ + "Origin", + "https://mitmproxy.org" + ], + [ + "DNT", + "1" + ], + [ + "Connection", + "keep-alive" + ], + [ + "Sec-Fetch-Dest", + "empty" + ], + [ + "Sec-Fetch-Mode", + "cors" + ], + [ + "Sec-Fetch-Site", + "cross-site" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134339.543, + "timestamp_end": 1680134339.79, + "pretty_host": "s3-us-west-2.amazonaws.com" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "x-amz-id-2", + "Wea5PituXz4XdzoNJ+QC6XDgoU/3V7l0magzJrS+LUtCBaNXHc1HwMDB2zvhYirPY8+6SfU08Bk=" + ], + [ + "x-amz-request-id", + "299PK1CFMV40HV7M" + ], + [ + "Date", + "Wed, 29 Mar 2023 23:59:00 GMT" + ], + [ + "Access-Control-Allow-Origin", + "*" + ], + [ + "Access-Control-Allow-Methods", + "GET" + ], + [ + "Access-Control-Max-Age", + "3000" + ], + [ + "Vary", + "Origin, Access-Control-Request-Headers, Access-Control-Request-Method" + ], + [ + "x-amz-bucket-region", + "us-west-2" + ], + [ + "Content-Type", + "application/xml" + ], + [ + "Transfer-Encoding", + "chunked" + ], + [ + "Server", + "AmazonS3" + ] + ], + "contentLength": 3406, + "contentHash": "1463cf2c4e430b2373b9cd16548f263d3335bc245fdca8019d56a4c9e6ae3b14", + "timestamp_start": 1680134339.543, + "timestamp_end": 1680134339.79 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680134339.639, + "timestamp_tls_setup": null, + "timestamp_end": 1680134339.646 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": [ + "13.35.121.55", + 443 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/data/github-stats.json", + "http_version": "HTTP/3", + "headers": [ + [ + "Host", + "mitmproxy.org" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Language", + "en-US,en;q=0.5" + ], + [ + "Accept-Encoding", + "gzip, deflate, br" + ], + [ + "DNT", + "1" + ], + [ + "Connection", + "keep-alive" + ], + [ + "Sec-Fetch-Dest", + "empty" + ], + [ + "Sec-Fetch-Mode", + "cors" + ], + [ + "Sec-Fetch-Site", + "same-origin" + ], + [ + "If-Modified-Since", + "Wed, 29 Mar 2023 23:55:21 GMT" + ], + [ + "If-None-Match", + "W/\"07201abd774cb0523be31d94fffe67a3\"" + ], + [ + "TE", + "trailers" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134339.639, + "timestamp_end": 1680134339.646, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/3", + "status_code": 304, + "reason": "Not Modified", + "headers": [ + [ + "age", + "205" + ], + [ + "date", + "Wed, 29 Mar 2023 23:58:59 GMT" + ], + [ + "etag", + "W/\"07201abd774cb0523be31d94fffe67a3\"" + ], + [ + "server", + "AmazonS3" + ], + [ + "vary", + "Accept-Encoding" + ], + [ + "x-cache", + "Hit from cloudfront" + ], + [ + "via", + "1.1 5fa120f79d5713714191c32768eca58c.cloudfront.net (CloudFront)" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "alt-svc", + "h3=\":443\"; ma=86400" + ], + [ + "x-amz-cf-id", + "0okGWJw6nYo7R-4egQWE-WfonThN2EXyRSLO9MlCNKyMfD-2v1AU0Q==" + ] + ], + "contentLength": 6986, + "contentHash": "ebb5ca702c6b7f09fe1c10e8992602bad67989e25151f0cb6928ea51299bf4e8", + "timestamp_start": 1680134339.639, + "timestamp_end": 1680134339.646 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680134339.643, + "timestamp_tls_setup": null, + "timestamp_end": 1680134339.643 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/favicon.ico", + "http_version": "HTTP/3", + "headers": [ + [ + "Host", + "mitmproxy.org" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0" + ], + [ + "Accept", + "image/avif,image/webp,*/*" + ], + [ + "Accept-Language", + "en-US,en;q=0.5" + ], + [ + "Accept-Encoding", + "gzip, deflate, br" + ], + [ + "DNT", + "1" + ], + [ + "Alt-Used", + "mitmproxy.org" + ], + [ + "Connection", + "keep-alive" + ], + [ + "Referer", + "https://mitmproxy.org/" + ], + [ + "Sec-Fetch-Dest", + "image" + ], + [ + "Sec-Fetch-Mode", + "no-cors" + ], + [ + "Sec-Fetch-Site", + "same-origin" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680134339.643, + "timestamp_end": 1680134339.643, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/3", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "content-type", + "image/vnd.microsoft.icon" + ], + [ + "content-length", + "98065" + ], + [ + "age", + "62116" + ], + [ + "last-modified", + "Sat, 04 Mar 2023 16:01:11 GMT" + ], + [ + "server", + "AmazonS3" + ], + [ + "date", + "Wed, 29 Mar 2023 06:43:07 GMT" + ], + [ + "etag", + "\"0f8a699781bb4a5a204a467db88dd555\"" + ], + [ + "vary", + "Accept-Encoding" + ], + [ + "x-cache", + "Hit from cloudfront" + ], + [ + "via", + "1.1 28663e5849ed20a9d037ca8066957990.cloudfront.net (CloudFront)" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "alt-svc", + "h3=\":443\"; ma=86400" + ], + [ + "x-amz-cf-id", + "dkVeTGCIfZ6m6k9VkhQJKLdxjemd8oSJPwnCEX6Tdimqsa5n2CJybg==" + ] + ], + "contentLength": 98065, + "contentHash": "ed040187e112545848bb115eb5fd16a85c2a0c89864bea5d930481518d05614d", + "timestamp_start": 1680134339.643, + "timestamp_end": 1680134339.643 + } + } +] \ No newline at end of file diff --git a/test/mitmproxy/data/har_files/head-content-length.har b/test/mitmproxy/data/har_files/head-content-length.har new file mode 100644 index 0000000000..ec549cbe9d --- /dev/null +++ b/test/mitmproxy/data/har_files/head-content-length.har @@ -0,0 +1,179 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Firefox", + "version": "111.0.1" + }, + "browser": { + "name": "Firefox", + "version": "111.0.1" + }, + "pages": [ + { + "startedDateTime": "2023-03-29T16:58:59.303-07:00", + "id": "page_1", + "title": "mitmproxy - an interactive HTTPS proxy", + "pageTimings": { + "onContentLoad": 208, + "onLoad": 270 + } + } + ], + "entries": [ + { + "startedDateTime": "2023-12-12T16:23:16.067544+00:00", + "time": 101.56393051147461, + "request": { + "method": "HEAD", + "url": "https://files.pythonhosted.org/packages/00/e5/f12a80907d0884e6dff9c16d0c0114d81b8cd07dc3ae54c5e962cc83037e/tqdm-4.66.1-py3-none-any.whl", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "accept", + "value": "*/*" + }, + { + "name": "user-agent", + "value": "puffin" + }, + { + "name": "accept-encoding", + "value": "gzip, br" + } + ], + "queryString": [], + "headersSize": 91, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": "last-modified", + "value": "Thu, 10 Aug 2023 11:39:00 GMT" + }, + { + "name": "etag", + "value": "\"a296c6e224c118b0d08cd77e8c08f4b1\"" + }, + { + "name": "x-amz-request-id", + "value": "aeb4d3335548af85" + }, + { + "name": "x-amz-id-2", + "value": "aN65jxTFgNrlm8zEJMNdk7mYLYwUwTzh0" + }, + { + "name": "x-amz-version-id", + "value": "4_z179c51e67f11a0ad8f6c0018_f10789ff3151435c8_d20230810_m113900_c005_v0501001_t0045_u01691667540984" + }, + { + "name": "content-type", + "value": "application/octet-stream" + }, + { + "name": "cache-control", + "value": "max-age=365000000, immutable, public" + }, + { + "name": "accept-ranges", + "value": "bytes" + }, + { + "name": "date", + "value": "Tue, 12 Dec 2023 16:23:16 GMT" + }, + { + "name": "age", + "value": "2335275" + }, + { + "name": "x-served-by", + "value": "cache-iad-kcgs7200038-IAD, cache-lck10926-LCK" + }, + { + "name": "x-cache", + "value": "HIT, HIT" + }, + { + "name": "x-cache-hits", + "value": "21459, 139679" + }, + { + "name": "x-timer", + "value": "S1702398196.107663,VS0,VE0" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "x-frame-options", + "value": "deny" + }, + { + "name": "x-xss-protection", + "value": "1; mode=block" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "x-permitted-cross-domain-policies", + "value": "none" + }, + { + "name": "x-robots-header", + "value": "noindex" + }, + { + "name": "x-pypi-file-python-version", + "value": "py3" + }, + { + "name": "x-pypi-file-version", + "value": "4.66.1" + }, + { + "name": "x-pypi-file-package-type", + "value": "bdist_wheel" + }, + { + "name": "x-pypi-file-project", + "value": "tqdm" + }, + { + "name": "content-length", + "value": "78258" + } + ], + "content": { + "size": 0, + "compression": 0, + "mimeType": "application/octet-stream", + "text": "" + }, + "redirectURL": "", + "headersSize": 1188, + "bodySize": 0 + }, + "cache": {}, + "timings": { + "connect": 31.686782836914062, + "ssl": 35.33315658569336, + "send": 0.2961158752441406, + "receive": 0.6310939788818359, + "wait": 33.61678123474121 + }, + "serverIPAddress": "199.232.96.223" + } + ] + } +} \ No newline at end of file diff --git a/test/mitmproxy/data/har_files/head-content-length.json b/test/mitmproxy/data/har_files/head-content-length.json new file mode 100644 index 0000000000..a3248bb4e9 --- /dev/null +++ b/test/mitmproxy/data/har_files/head-content-length.json @@ -0,0 +1,193 @@ +[ + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1702398196.067544, + "timestamp_tls_setup": null, + "timestamp_end": 1702398196.169108 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": [ + "199.232.96.223", + 443 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "HEAD", + "scheme": "https", + "host": "files.pythonhosted.org", + "port": 443, + "path": "/packages/00/e5/f12a80907d0884e6dff9c16d0c0114d81b8cd07dc3ae54c5e962cc83037e/tqdm-4.66.1-py3-none-any.whl", + "http_version": "HTTP/1.1", + "headers": [ + [ + "accept", + "*/*" + ], + [ + "user-agent", + "puffin" + ], + [ + "accept-encoding", + "gzip, br" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1702398196.067544, + "timestamp_end": 1702398196.169108, + "pretty_host": "files.pythonhosted.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "last-modified", + "Thu, 10 Aug 2023 11:39:00 GMT" + ], + [ + "etag", + "\"a296c6e224c118b0d08cd77e8c08f4b1\"" + ], + [ + "x-amz-request-id", + "aeb4d3335548af85" + ], + [ + "x-amz-id-2", + "aN65jxTFgNrlm8zEJMNdk7mYLYwUwTzh0" + ], + [ + "x-amz-version-id", + "4_z179c51e67f11a0ad8f6c0018_f10789ff3151435c8_d20230810_m113900_c005_v0501001_t0045_u01691667540984" + ], + [ + "content-type", + "application/octet-stream" + ], + [ + "cache-control", + "max-age=365000000, immutable, public" + ], + [ + "accept-ranges", + "bytes" + ], + [ + "date", + "Tue, 12 Dec 2023 16:23:16 GMT" + ], + [ + "age", + "2335275" + ], + [ + "x-served-by", + "cache-iad-kcgs7200038-IAD, cache-lck10926-LCK" + ], + [ + "x-cache", + "HIT, HIT" + ], + [ + "x-cache-hits", + "21459, 139679" + ], + [ + "x-timer", + "S1702398196.107663,VS0,VE0" + ], + [ + "strict-transport-security", + "max-age=31536000; includeSubDomains; preload" + ], + [ + "x-frame-options", + "deny" + ], + [ + "x-xss-protection", + "1; mode=block" + ], + [ + "x-content-type-options", + "nosniff" + ], + [ + "x-permitted-cross-domain-policies", + "none" + ], + [ + "x-robots-header", + "noindex" + ], + [ + "x-pypi-file-python-version", + "py3" + ], + [ + "x-pypi-file-version", + "4.66.1" + ], + [ + "x-pypi-file-package-type", + "bdist_wheel" + ], + [ + "x-pypi-file-project", + "tqdm" + ], + [ + "content-length", + "78258" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1702398196.067544, + "timestamp_end": 1702398196.169108 + } + } +] \ No newline at end of file diff --git a/test/mitmproxy/data/har_files/insomnia.har b/test/mitmproxy/data/har_files/insomnia.har new file mode 100644 index 0000000000..b738971e65 --- /dev/null +++ b/test/mitmproxy/data/har_files/insomnia.har @@ -0,0 +1,118 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Insomnia REST Client", + "version": "insomnia.desktop.app:v2022.1.1" + }, + "entries": [ + { + "startedDateTime": "2023-03-30T04:39:18.981Z", + "time": 70.402, + "request": { + "method": "GET", + "url": "http://mitm.it/", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [], + "queryString": [], + "postData": { + "mimeType": "", + "text": "", + "params": [] + }, + "headersSize": -1, + "bodySize": -1, + "settingEncodeUrl": true + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "text/html" + }, + { + "name": "Content-Length", + "value": "250" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Last-Modified", + "value": "Mon, 26 Dec 2016 20:04:24 GMT" + }, + { + "name": "Accept-Ranges", + "value": "bytes" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "Date", + "value": "Thu, 30 Mar 2023 04:36:02 GMT" + }, + { + "name": "Etag", + "value": "\"b17c505e5c0969bb21b89e6116530dde\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "Via", + "value": "1.1 6354bde44a975facce9c0ed03828827e.cloudfront.net (CloudFront)" + }, + { + "name": "Age", + "value": "45929" + }, + { + "name": "Cache-Control", + "value": "no-store, must-revalidate" + }, + { + "name": "X-Cache", + "value": "Hit from cloudfront" + }, + { + "name": "X-Amz-Cf-Pop", + "value": "SFO53-P1" + }, + { + "name": "X-Amz-Cf-Id", + "value": "uGf_zmZcmVZRcJ8PJQvKXtwrdSIzIupupc51RIAQMdov8FQh755mSQ==" + } + ], + "content": { + "size": 250, + "mimeType": "text/html", + "text": "
    \n

    If you can see this, traffic is not passing through mitmproxy.

    \n\n

    \n \n Visit the Documentation\n

    \n\n\n" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": -1, + "connect": -1, + "send": 0, + "wait": 70.402, + "receive": 0, + "ssl": -1 + }, + "comment": "mitmproxy" + } + ] + } +} \ No newline at end of file diff --git a/test/mitmproxy/data/har_files/insomnia.json b/test/mitmproxy/data/har_files/insomnia.json new file mode 100644 index 0000000000..62630b0003 --- /dev/null +++ b/test/mitmproxy/data/har_files/insomnia.json @@ -0,0 +1,138 @@ +[ + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680151158.981, + "timestamp_tls_setup": null, + "timestamp_end": 1680151159.0514019 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "http", + "host": "mitm.it", + "port": 80, + "path": "/", + "http_version": "HTTP/1.1", + "headers": [ + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680151158.981, + "timestamp_end": 1680151159.0514019, + "pretty_host": "mitm.it" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "text/html" + ], + [ + "Content-Length", + "250" + ], + [ + "Connection", + "keep-alive" + ], + [ + "Last-Modified", + "Mon, 26 Dec 2016 20:04:24 GMT" + ], + [ + "Accept-Ranges", + "bytes" + ], + [ + "Server", + "AmazonS3" + ], + [ + "Date", + "Thu, 30 Mar 2023 04:36:02 GMT" + ], + [ + "Etag", + "\"b17c505e5c0969bb21b89e6116530dde\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "Via", + "1.1 6354bde44a975facce9c0ed03828827e.cloudfront.net (CloudFront)" + ], + [ + "Age", + "45929" + ], + [ + "Cache-Control", + "no-store, must-revalidate" + ], + [ + "X-Cache", + "Hit from cloudfront" + ], + [ + "X-Amz-Cf-Pop", + "SFO53-P1" + ], + [ + "X-Amz-Cf-Id", + "uGf_zmZcmVZRcJ8PJQvKXtwrdSIzIupupc51RIAQMdov8FQh755mSQ==" + ] + ], + "contentLength": 250, + "contentHash": "ad5724ee351ebc53212702f448c0136f3892e52036fb9e5918192a130bde38bd", + "timestamp_start": 1680151158.981, + "timestamp_end": 1680151159.0514019 + } + } +] \ No newline at end of file diff --git a/test/mitmproxy/data/har_files/postdata.har b/test/mitmproxy/data/har_files/postdata.har new file mode 100644 index 0000000000..21932bc87f --- /dev/null +++ b/test/mitmproxy/data/har_files/postdata.har @@ -0,0 +1,189 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "WebInspector", + "version": "537.36" + }, + "pages": [], + "entries": [ + { + "_initiator": { + "type": "script", + "stack": { + "callFrames": [ + { + "functionName": "send", + "scriptId": "108", + "url": "https://signal-beacon.s-onetag.com/beacon.min.js", + "lineNumber": 21, + "columnNumber": 529 + }, + { + "functionName": "X", + "scriptId": "108", + "url": "https://signal-beacon.s-onetag.com/beacon.min.js", + "lineNumber": 33, + "columnNumber": 422 + }, + { + "functionName": "", + "scriptId": "108", + "url": "https://signal-beacon.s-onetag.com/beacon.min.js", + "lineNumber": 34, + "columnNumber": 343 + } + ] + } + }, + "_priority": "VeryLow", + "_resourceType": "ping", + "cache": {}, + "connection": "159834", + "request": { + "method": "POST", + "url": "https://signal-metrics-collector-beta.s-onetag.com/metrics", + "httpVersion": "http/2.0", + "headers": [ + { + "name": ":authority", + "value": "signal-metrics-collector-beta.s-onetag.com" + }, + { + "name": ":method", + "value": "POST" + }, + { + "name": ":path", + "value": "/metrics" + }, + { + "name": ":scheme", + "value": "https" + }, + { + "name": "accept", + "value": "*/*" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate, br" + }, + { + "name": "accept-language", + "value": "en-US,en;q=0.9" + }, + { + "name": "cache-control", + "value": "no-cache" + }, + { + "name": "content-length", + "value": "1310" + }, + { + "name": "content-type", + "value": "text/plain;charset=UTF-8" + }, + { + "name": "dnt", + "value": "1" + }, + { + "name": "origin", + "value": "https://reqbin.com" + }, + { + "name": "pragma", + "value": "no-cache" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Google Chrome\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + }, + { + "name": "sec-fetch-dest", + "value": "empty" + }, + { + "name": "sec-fetch-mode", + "value": "no-cors" + }, + { + "name": "sec-fetch-site", + "value": "cross-site" + }, + { + "name": "user-agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + } + ], + "queryString": [], + "cookies": [], + "headersSize": -1, + "bodySize": 1310, + "postData": { + "mimeType": "text/plain;charset=UTF-8", + "text": "{\"metadata\":{\"pageViewId\":1689428126308,\"affiliateId\":62299,\"domain\":\"reqbin.com\",\"path\":\"/post-online\",\"isCollectable\":true,\"consentString\":\"\",\"gppString\":\"\",\"location\":\"AT\",\"ljtReader\":\"\",\"query\":\"\",\"referrer\":\"https://www.google.com/\",\"canCollectIp\":false},\"payloads\":[{\"scrollDepth\":856,\"dwellTime\":76,\"engagedDwellTime\":5,\"documentHeight\":2958,\"type\":\"page\",\"userEids\":[]},{\"impressionId\":1689428184383,\"zoneId\":null,\"transactionId\":null,\"secondsInView\":0,\"gamAdUnitPath\":\"/1254144,21910826702/reqbin_com-box-4\",\"clicks\":0,\"viewableEngagedSeconds\":0,\"isSovrnReload\":false,\"isAboveTheFold\":false,\"adSize\":\"970x250\",\"adElementId\":\"#div-gpt-ad-reqbin_com-box-4-0\",\"type\":\"impression\"},{\"impressionId\":1689428230692,\"zoneId\":null,\"transactionId\":null,\"secondsInView\":0,\"gamAdUnitPath\":\"/1254144,21910826702/reqbin_com-box-1\",\"clicks\":0,\"viewableEngagedSeconds\":0,\"isSovrnReload\":false,\"isAboveTheFold\":true,\"adSize\":\"300x250\",\"adElementId\":\"#div-gpt-ad-reqbin_com-box-1-0\",\"type\":\"impression\"},{\"impressionId\":1689428203420,\"zoneId\":null,\"transactionId\":null,\"secondsInView\":0,\"gamAdUnitPath\":\"/1254144,21910826702/reqbin_com-banner-2\",\"clicks\":0,\"viewableEngagedSeconds\":0,\"isSovrnReload\":false,\"isAboveTheFold\":true,\"adSize\":\"300x250\",\"adElementId\":\"#div-gpt-ad-reqbin_com-banner-2-0\",\"type\":\"impression\"}]}" + } + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "http/2.0", + "headers": [ + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "content-length", + "value": "0" + }, + { + "name": "date", + "value": "Sat, 15 Jul 2023 13:37:26 GMT" + }, + { + "name": "vary", + "value": "Origin" + } + ], + "cookies": [], + "content": { + "size": 0, + "mimeType": "text/plain", + "text": "" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1, + "_transferSize": 73, + "_error": null + }, + "serverIPAddress": "99.83.181.31", + "startedDateTime": "2023-07-15T13:37:26.093Z", + "time": 169.79599999582302, + "timings": { + "blocked": 0.6539999998793937, + "dns": 18.781000000000002, + "ssl": 55.379, + "connect": 96.754, + "send": 0.3190000000000026, + "wait": 53.01799999912642, + "receive": 0.2699999968172051, + "_blocked_queueing": 0.4079999998793937 + } + } + + ] + } +} \ No newline at end of file diff --git a/test/mitmproxy/data/har_files/postdata.json b/test/mitmproxy/data/har_files/postdata.json new file mode 100644 index 0000000000..08c8b8abbf --- /dev/null +++ b/test/mitmproxy/data/har_files/postdata.json @@ -0,0 +1,173 @@ +[ + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1689428246.093, + "timestamp_tls_setup": null, + "timestamp_end": 1689428246.262796 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": [ + "99.83.181.31", + 443 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "POST", + "scheme": "https", + "host": "signal-metrics-collector-beta.s-onetag.com", + "port": 443, + "path": "/metrics", + "http_version": "HTTP/2", + "headers": [ + [ + ":authority", + "signal-metrics-collector-beta.s-onetag.com" + ], + [ + ":method", + "POST" + ], + [ + ":path", + "/metrics" + ], + [ + ":scheme", + "https" + ], + [ + "accept", + "*/*" + ], + [ + "accept-encoding", + "gzip, deflate, br" + ], + [ + "accept-language", + "en-US,en;q=0.9" + ], + [ + "cache-control", + "no-cache" + ], + [ + "content-length", + "1310" + ], + [ + "content-type", + "text/plain;charset=UTF-8" + ], + [ + "dnt", + "1" + ], + [ + "origin", + "https://reqbin.com" + ], + [ + "pragma", + "no-cache" + ], + [ + "sec-ch-ua", + "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Google Chrome\";v=\"114\"" + ], + [ + "sec-ch-ua-mobile", + "?0" + ], + [ + "sec-ch-ua-platform", + "\"macOS\"" + ], + [ + "sec-fetch-dest", + "empty" + ], + [ + "sec-fetch-mode", + "no-cors" + ], + [ + "sec-fetch-site", + "cross-site" + ], + [ + "user-agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + ] + ], + "contentLength": 1310, + "contentHash": "94c0d23b4e9f828b4b9062885ba0b785ce53fc374aef106b01fa62ff9f15c35b", + "timestamp_start": 1689428246.093, + "timestamp_end": 1689428246.262796, + "pretty_host": "signal-metrics-collector-beta.s-onetag.com" + }, + "response": { + "http_version": "HTTP/2", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "access-control-allow-origin", + "*" + ], + [ + "content-length", + "0" + ], + [ + "date", + "Sat, 15 Jul 2023 13:37:26 GMT" + ], + [ + "vary", + "Origin" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1689428246.093, + "timestamp_end": 1689428246.262796 + } + } +] \ No newline at end of file diff --git a/test/mitmproxy/data/har_files/safari.har b/test/mitmproxy/data/har_files/safari.har new file mode 100644 index 0000000000..1b6049f865 --- /dev/null +++ b/test/mitmproxy/data/har_files/safari.har @@ -0,0 +1,2057 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "WebKit Web Inspector", + "version": "1.0" + }, + "pages": [ + { + "startedDateTime": "2023-03-30T00:13:32.418Z", + "id": "page_0", + "title": "https://mitmproxy.org/", + "pageTimings": { + "onContentLoad": 15788.239001994953, + "onLoad": 15799.182686023414 + } + } + ], + "entries": [ + { + "pageref": "page_0", + "startedDateTime": "2023-03-30T00:13:32.418Z", + "time": 111.34259781101719, + "request": { + "method": "GET", + "url": "https://mitmproxy.org/", + "httpVersion": "HTTP/2", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Host", + "value": "mitmproxy.org" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.9" + }, + { + "name": "Referer", + "value": "https://www.google.com/" + }, + { + "name": "Connection", + "value": "keep-alive" + } + ], + "queryString": [], + "headersSize": 216, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/2", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "text/html" + }, + { + "name": "Last-Modified", + "value": "Sat, 04 Mar 2023 18:02:08 GMT" + }, + { + "name": "Age", + "value": "33218" + }, + { + "name": "Via", + "value": "1.1 39464b01f314ad3cb531f46c3049bf58.cloudfront.net (CloudFront)" + }, + { + "name": "Content-Encoding", + "value": "gzip" + }, + { + "name": "Date", + "value": "Wed, 29 Mar 2023 14:59:54 GMT" + }, + { + "name": "ETag", + "value": "W/\"a1550c2bd25c5bcfef789d730f5bbddf\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "x-amz-cf-id", + "value": "Qd8Jlu_8NgzlCi_CafxkuPQE_pdS4nei9rjPK9088Zhdsz4j7tcVcA==" + }, + { + "name": "Alt-Svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + } + ], + "content": { + "size": 23866, + "compression": 18716, + "mimeType": "text/html", + "text": "\n\n\n \n \n \n\n \n mitmproxy - an interactive HTTPS proxy\n \n \n \n \n \n \n\n\n
    \n\n\n
    \n
    \n
    \n
    \n
    \n
    \n \"screenshot\n
    \n
    \n
    \n
    \n

    \n mitmproxy is a free\n and open source\n interactive HTTPS proxy.\n

    \n
    \n \n

    \n Release Notes (v9.0)\n \n –\n \n Other Downloads\n \n \n

    \n
    \n
    \n
    \n
    \n
    \n
    \n\n
    \n \n
    \n\n
    \n
    \n
    \n
    \n
    \n \"screenshot\n
    \n
    \n

    Command Line

    \n

    \n mitmproxy is your swiss-army knife for debugging, testing,\n privacy measurements, and penetration testing.\n It can be used to intercept, inspect, modify and replay web traffic such\n as\n HTTP/1, HTTP/2, WebSockets, or any other SSL/TLS-protected protocols.\n You can prettify and decode a variety of message types ranging from HTML\n to\n Protobuf,\n intercept specific messages on-the-fly,\n modify them before they reach their destination, and replay them\n to a client or server later on.\n

    \n
    \n\n
    \n
    \n
    \n
    \n\n
    \n
    \n
    \n
    \n
    \n \"screenshot\n
    \n
    \n

    Web Interface

    \n

    \n Use mitmproxy's main features in a graphical interface with\n mitmweb. Do you like Chrome's DevTools? mitmweb\n gives\n you a similar experience for any other application or device,\n plus additional features such as request interception and replay.\n\n

    \n \n
    \n\n
    \n
    \n
    \n
    \n\n
    \n
    \n
    \n
    \n
    \n
    \n
    addon.py
    \n
    from mitmproxy import http\n\ndef request(flow: http.HTTPFlow):\n    # redirect to different host\n    if flow.request.pretty_host == \"example.com\":\n        flow.request.host = \"mitmproxy.org\"\n    # answer from proxy\n    elif flow.request.path.endswith(\"/brew\"):\n    \tflow.response = http.Response.make(\n            418, b\"I'm a teapot\",\n        )
    \n
    \n
    \n
    \n

    \n Python API\n

    \n

    \n Write powerful addons and script mitmproxy with mitmdump.\n The scripting API offers full control over mitmproxy and makes it\n possible\n to automatically modify messages, redirect traffic, visualize messages,\n or\n implement custom commands.\n

    \n
    \n
    \n
    \n
    \n
    \n\n
    \n
    \n
    \n
    \n
    \n
    \n

    Latest\n Tweets

    \n
    \n \n \"A\n \n
    \n
    \n
    \n
    \n

    Powerful Ecosystem

    \n
    \n

    \n Mitmproxy has a vibrant ecosystem of addons and tools building on it:\n

    \n
      \n
    • \n \n mitmproxy/examples/contrib, a collection of\n community-contributed mitmproxy addons.\n
    • \n
    • \n \n mitmproxy2swagger, a tool for automatically converting mitmproxy captures to\n OpenAPI 3.0 specifications.\n
    • \n
    • \n \n kubetap, a kubectl plugin to interactively proxy Kubernetes Services.\n
    • \n
    \n
    \n

    Sponsored By

    \n
    \n \"Proxyman\"\n \"Netograph.io\"\n ...and many individual supporters! ❤️\n
    \n
    \n
    \n
    \n
    \n
    \n\n
    \n
    \n
    \n
    \n
    \n \n
    \n
    \n

    Open Source

    \n

    \n Mitmproxy is free and open source. Be part of the mitmproxy community\n and\n help improve your favorite HTTPS proxy.\n

    \n
    \n \n
    \n
    \n
    \n
    \n
    \n
    \n\n\n\n\n\n\n\n\n\n\n" + }, + "redirectURL": "", + "headersSize": 345, + "bodySize": 5150, + "_transferSize": 5495 + }, + "cache": {}, + "timings": { + "blocked": 2.400923112872988, + "dns": 50.99903920199722, + "connect": 29.000284848734736, + "ssl": 19.000165397301316, + "send": 0.7859446341171861, + "wait": 6.00014696829021, + "receive": 21.15597511874512 + }, + "serverIPAddress": "13.35.121.29", + "_serverPort": 443, + "connection": "1", + "_fetchType": "Network Load", + "_priority": "high" + }, + { + "pageref": "page_0", + "startedDateTime": "2023-03-30T00:13:32.542Z", + "time": 0.06406899774447083, + "request": { + "method": "GET", + "url": "https://mitmproxy.org/style.min.css", + "httpVersion": "", + "cookies": [], + "headers": [], + "queryString": [], + "headersSize": -1, + "bodySize": -1 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "text/css" + }, + { + "name": "Last-Modified", + "value": "Sat, 04 Mar 2023 18:02:10 GMT" + }, + { + "name": "Age", + "value": "53840" + }, + { + "name": "Via", + "value": "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + }, + { + "name": "Content-Encoding", + "value": "gzip" + }, + { + "name": "Date", + "value": "Wed, 29 Mar 2023 09:06:44 GMT" + }, + { + "name": "ETag", + "value": "W/\"98a27a6d8538067b552f3a85c757dd25\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "x-amz-cf-id", + "value": "X2i3z35vQWE0vwoa9PeqSCeJU2nusZQGFA5JWaEOH-RHLOF8bzILtw==" + }, + { + "name": "Alt-Svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + } + ], + "content": { + "size": 0, + "compression": 0, + "mimeType": "text/css", + "text": "/*!* Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com\n* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)*/@font-face{font-family:'font awesome 5 brands';font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix)format(\"embedded-opentype\"),url(../webfonts/fa-brands-400.woff2)format(\"woff2\"),url(../webfonts/fa-brands-400.woff)format(\"woff\"),url(../webfonts/fa-brands-400.ttf)format(\"truetype\"),url(../webfonts/fa-brands-400.svg#fontawesome)format(\"svg\")}.fab{font-family:'font awesome 5 brands';font-weight:400}/*!* Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com\n* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)*/@font-face{font-family:'font awesome 5 free';font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix)format(\"embedded-opentype\"),url(../webfonts/fa-regular-400.woff2)format(\"woff2\"),url(../webfonts/fa-regular-400.woff)format(\"woff\"),url(../webfonts/fa-regular-400.ttf)format(\"truetype\"),url(../webfonts/fa-regular-400.svg#fontawesome)format(\"svg\")}.far{font-family:'font awesome 5 free';font-weight:400}/*!* Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com\n* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)*/@font-face{font-family:'font awesome 5 free';font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix)format(\"embedded-opentype\"),url(../webfonts/fa-solid-900.woff2)format(\"woff2\"),url(../webfonts/fa-solid-900.woff)format(\"woff\"),url(../webfonts/fa-solid-900.ttf)format(\"truetype\"),url(../webfonts/fa-solid-900.svg#fontawesome)format(\"svg\")}.fa,.fas{font-family:'font awesome 5 free';font-weight:900}/*!* Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com\n* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)*/.fa,.fas,.far,.fal,.fad,.fab{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:solid .08em #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fas.fa-pull-left,.far.fa-pull-left,.fal.fa-pull-left,.fab.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fas.fa-pull-right,.far.fa-pull-right,.fal.fa-pull-right,.fab.fa-pull-right{margin-left:.3em}.fa-spin{animation:fa-spin 2s infinite linear}.fa-pulse{animation:fa-spin 1s infinite steps(8)}@keyframes fa-spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.fa-rotate-90{-ms-filter:\"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)\";transform:rotate(90deg)}.fa-rotate-180{-ms-filter:\"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)\";transform:rotate(180deg)}.fa-rotate-270{-ms-filter:\"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)\";transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:\"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)\";transform:scale(-1,1)}.fa-flip-vertical{-ms-filter:\"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)\";transform:scale(1,-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-ms-filter:\"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)\";transform:scale(-1,-1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-flip-both{filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:\"\\f26e\"}.fa-accessible-icon:before{content:\"\\f368\"}.fa-accusoft:before{content:\"\\f369\"}.fa-acquisitions-incorporated:before{content:\"\\f6af\"}.fa-ad:before{content:\"\\f641\"}.fa-address-book:before{content:\"\\f2b9\"}.fa-address-card:before{content:\"\\f2bb\"}.fa-adjust:before{content:\"\\f042\"}.fa-adn:before{content:\"\\f170\"}.fa-adversal:before{content:\"\\f36a\"}.fa-affiliatetheme:before{content:\"\\f36b\"}.fa-air-freshener:before{content:\"\\f5d0\"}.fa-airbnb:before{content:\"\\f834\"}.fa-algolia:before{content:\"\\f36c\"}.fa-align-center:before{content:\"\\f037\"}.fa-align-justify:before{content:\"\\f039\"}.fa-align-left:before{content:\"\\f036\"}.fa-align-right:before{content:\"\\f038\"}.fa-alipay:before{content:\"\\f642\"}.fa-allergies:before{content:\"\\f461\"}.fa-amazon:before{content:\"\\f270\"}.fa-amazon-pay:before{content:\"\\f42c\"}.fa-ambulance:before{content:\"\\f0f9\"}.fa-american-sign-language-interpreting:before{content:\"\\f2a3\"}.fa-amilia:before{content:\"\\f36d\"}.fa-anchor:before{content:\"\\f13d\"}.fa-android:before{content:\"\\f17b\"}.fa-angellist:before{content:\"\\f209\"}.fa-angle-double-down:before{content:\"\\f103\"}.fa-angle-double-left:before{content:\"\\f100\"}.fa-angle-double-right:before{content:\"\\f101\"}.fa-angle-double-up:before{content:\"\\f102\"}.fa-angle-down:before{content:\"\\f107\"}.fa-angle-left:before{content:\"\\f104\"}.fa-angle-right:before{content:\"\\f105\"}.fa-angle-up:before{content:\"\\f106\"}.fa-angry:before{content:\"\\f556\"}.fa-angrycreative:before{content:\"\\f36e\"}.fa-angular:before{content:\"\\f420\"}.fa-ankh:before{content:\"\\f644\"}.fa-app-store:before{content:\"\\f36f\"}.fa-app-store-ios:before{content:\"\\f370\"}.fa-apper:before{content:\"\\f371\"}.fa-apple:before{content:\"\\f179\"}.fa-apple-alt:before{content:\"\\f5d1\"}.fa-apple-pay:before{content:\"\\f415\"}.fa-archive:before{content:\"\\f187\"}.fa-archway:before{content:\"\\f557\"}.fa-arrow-alt-circle-down:before{content:\"\\f358\"}.fa-arrow-alt-circle-left:before{content:\"\\f359\"}.fa-arrow-alt-circle-right:before{content:\"\\f35a\"}.fa-arrow-alt-circle-up:before{content:\"\\f35b\"}.fa-arrow-circle-down:before{content:\"\\f0ab\"}.fa-arrow-circle-left:before{content:\"\\f0a8\"}.fa-arrow-circle-right:before{content:\"\\f0a9\"}.fa-arrow-circle-up:before{content:\"\\f0aa\"}.fa-arrow-down:before{content:\"\\f063\"}.fa-arrow-left:before{content:\"\\f060\"}.fa-arrow-right:before{content:\"\\f061\"}.fa-arrow-up:before{content:\"\\f062\"}.fa-arrows-alt:before{content:\"\\f0b2\"}.fa-arrows-alt-h:before{content:\"\\f337\"}.fa-arrows-alt-v:before{content:\"\\f338\"}.fa-artstation:before{content:\"\\f77a\"}.fa-assistive-listening-systems:before{content:\"\\f2a2\"}.fa-asterisk:before{content:\"\\f069\"}.fa-asymmetrik:before{content:\"\\f372\"}.fa-at:before{content:\"\\f1fa\"}.fa-atlas:before{content:\"\\f558\"}.fa-atlassian:before{content:\"\\f77b\"}.fa-atom:before{content:\"\\f5d2\"}.fa-audible:before{content:\"\\f373\"}.fa-audio-description:before{content:\"\\f29e\"}.fa-autoprefixer:before{content:\"\\f41c\"}.fa-avianex:before{content:\"\\f374\"}.fa-aviato:before{content:\"\\f421\"}.fa-award:before{content:\"\\f559\"}.fa-aws:before{content:\"\\f375\"}.fa-baby:before{content:\"\\f77c\"}.fa-baby-carriage:before{content:\"\\f77d\"}.fa-backspace:before{content:\"\\f55a\"}.fa-backward:before{content:\"\\f04a\"}.fa-bacon:before{content:\"\\f7e5\"}.fa-bacteria:before{content:\"\\e059\"}.fa-bacterium:before{content:\"\\e05a\"}.fa-bahai:before{content:\"\\f666\"}.fa-balance-scale:before{content:\"\\f24e\"}.fa-balance-scale-left:before{content:\"\\f515\"}.fa-balance-scale-right:before{content:\"\\f516\"}.fa-ban:before{content:\"\\f05e\"}.fa-band-aid:before{content:\"\\f462\"}.fa-bandcamp:before{content:\"\\f2d5\"}.fa-barcode:before{content:\"\\f02a\"}.fa-bars:before{content:\"\\f0c9\"}.fa-baseball-ball:before{content:\"\\f433\"}.fa-basketball-ball:before{content:\"\\f434\"}.fa-bath:before{content:\"\\f2cd\"}.fa-battery-empty:before{content:\"\\f244\"}.fa-battery-full:before{content:\"\\f240\"}.fa-battery-half:before{content:\"\\f242\"}.fa-battery-quarter:before{content:\"\\f243\"}.fa-battery-three-quarters:before{content:\"\\f241\"}.fa-battle-net:before{content:\"\\f835\"}.fa-bed:before{content:\"\\f236\"}.fa-beer:before{content:\"\\f0fc\"}.fa-behance:before{content:\"\\f1b4\"}.fa-behance-square:before{content:\"\\f1b5\"}.fa-bell:before{content:\"\\f0f3\"}.fa-bell-slash:before{content:\"\\f1f6\"}.fa-bezier-curve:before{content:\"\\f55b\"}.fa-bible:before{content:\"\\f647\"}.fa-bicycle:before{content:\"\\f206\"}.fa-biking:before{content:\"\\f84a\"}.fa-bimobject:before{content:\"\\f378\"}.fa-binoculars:before{content:\"\\f1e5\"}.fa-biohazard:before{content:\"\\f780\"}.fa-birthday-cake:before{content:\"\\f1fd\"}.fa-bitbucket:before{content:\"\\f171\"}.fa-bitcoin:before{content:\"\\f379\"}.fa-bity:before{content:\"\\f37a\"}.fa-black-tie:before{content:\"\\f27e\"}.fa-blackberry:before{content:\"\\f37b\"}.fa-blender:before{content:\"\\f517\"}.fa-blender-phone:before{content:\"\\f6b6\"}.fa-blind:before{content:\"\\f29d\"}.fa-blog:before{content:\"\\f781\"}.fa-blogger:before{content:\"\\f37c\"}.fa-blogger-b:before{content:\"\\f37d\"}.fa-bluetooth:before{content:\"\\f293\"}.fa-bluetooth-b:before{content:\"\\f294\"}.fa-bold:before{content:\"\\f032\"}.fa-bolt:before{content:\"\\f0e7\"}.fa-bomb:before{content:\"\\f1e2\"}.fa-bone:before{content:\"\\f5d7\"}.fa-bong:before{content:\"\\f55c\"}.fa-book:before{content:\"\\f02d\"}.fa-book-dead:before{content:\"\\f6b7\"}.fa-book-medical:before{content:\"\\f7e6\"}.fa-book-open:before{content:\"\\f518\"}.fa-book-reader:before{content:\"\\f5da\"}.fa-bookmark:before{content:\"\\f02e\"}.fa-bootstrap:before{content:\"\\f836\"}.fa-border-all:before{content:\"\\f84c\"}.fa-border-none:before{content:\"\\f850\"}.fa-border-style:before{content:\"\\f853\"}.fa-bowling-ball:before{content:\"\\f436\"}.fa-box:before{content:\"\\f466\"}.fa-box-open:before{content:\"\\f49e\"}.fa-box-tissue:before{content:\"\\e05b\"}.fa-boxes:before{content:\"\\f468\"}.fa-braille:before{content:\"\\f2a1\"}.fa-brain:before{content:\"\\f5dc\"}.fa-bread-slice:before{content:\"\\f7ec\"}.fa-briefcase:before{content:\"\\f0b1\"}.fa-briefcase-medical:before{content:\"\\f469\"}.fa-broadcast-tower:before{content:\"\\f519\"}.fa-broom:before{content:\"\\f51a\"}.fa-brush:before{content:\"\\f55d\"}.fa-btc:before{content:\"\\f15a\"}.fa-buffer:before{content:\"\\f837\"}.fa-bug:before{content:\"\\f188\"}.fa-building:before{content:\"\\f1ad\"}.fa-bullhorn:before{content:\"\\f0a1\"}.fa-bullseye:before{content:\"\\f140\"}.fa-burn:before{content:\"\\f46a\"}.fa-buromobelexperte:before{content:\"\\f37f\"}.fa-bus:before{content:\"\\f207\"}.fa-bus-alt:before{content:\"\\f55e\"}.fa-business-time:before{content:\"\\f64a\"}.fa-buy-n-large:before{content:\"\\f8a6\"}.fa-buysellads:before{content:\"\\f20d\"}.fa-calculator:before{content:\"\\f1ec\"}.fa-calendar:before{content:\"\\f133\"}.fa-calendar-alt:before{content:\"\\f073\"}.fa-calendar-check:before{content:\"\\f274\"}.fa-calendar-day:before{content:\"\\f783\"}.fa-calendar-minus:before{content:\"\\f272\"}.fa-calendar-plus:before{content:\"\\f271\"}.fa-calendar-times:before{content:\"\\f273\"}.fa-calendar-week:before{content:\"\\f784\"}.fa-camera:before{content:\"\\f030\"}.fa-camera-retro:before{content:\"\\f083\"}.fa-campground:before{content:\"\\f6bb\"}.fa-canadian-maple-leaf:before{content:\"\\f785\"}.fa-candy-cane:before{content:\"\\f786\"}.fa-cannabis:before{content:\"\\f55f\"}.fa-capsules:before{content:\"\\f46b\"}.fa-car:before{content:\"\\f1b9\"}.fa-car-alt:before{content:\"\\f5de\"}.fa-car-battery:before{content:\"\\f5df\"}.fa-car-crash:before{content:\"\\f5e1\"}.fa-car-side:before{content:\"\\f5e4\"}.fa-caravan:before{content:\"\\f8ff\"}.fa-caret-down:before{content:\"\\f0d7\"}.fa-caret-left:before{content:\"\\f0d9\"}.fa-caret-right:before{content:\"\\f0da\"}.fa-caret-square-down:before{content:\"\\f150\"}.fa-caret-square-left:before{content:\"\\f191\"}.fa-caret-square-right:before{content:\"\\f152\"}.fa-caret-square-up:before{content:\"\\f151\"}.fa-caret-up:before{content:\"\\f0d8\"}.fa-carrot:before{content:\"\\f787\"}.fa-cart-arrow-down:before{content:\"\\f218\"}.fa-cart-plus:before{content:\"\\f217\"}.fa-cash-register:before{content:\"\\f788\"}.fa-cat:before{content:\"\\f6be\"}.fa-cc-amazon-pay:before{content:\"\\f42d\"}.fa-cc-amex:before{content:\"\\f1f3\"}.fa-cc-apple-pay:before{content:\"\\f416\"}.fa-cc-diners-club:before{content:\"\\f24c\"}.fa-cc-discover:before{content:\"\\f1f2\"}.fa-cc-jcb:before{content:\"\\f24b\"}.fa-cc-mastercard:before{content:\"\\f1f1\"}.fa-cc-paypal:before{content:\"\\f1f4\"}.fa-cc-stripe:before{content:\"\\f1f5\"}.fa-cc-visa:before{content:\"\\f1f0\"}.fa-centercode:before{content:\"\\f380\"}.fa-centos:before{content:\"\\f789\"}.fa-certificate:before{content:\"\\f0a3\"}.fa-chair:before{content:\"\\f6c0\"}.fa-chalkboard:before{content:\"\\f51b\"}.fa-chalkboard-teacher:before{content:\"\\f51c\"}.fa-charging-station:before{content:\"\\f5e7\"}.fa-chart-area:before{content:\"\\f1fe\"}.fa-chart-bar:before{content:\"\\f080\"}.fa-chart-line:before{content:\"\\f201\"}.fa-chart-pie:before{content:\"\\f200\"}.fa-check:before{content:\"\\f00c\"}.fa-check-circle:before{content:\"\\f058\"}.fa-check-double:before{content:\"\\f560\"}.fa-check-square:before{content:\"\\f14a\"}.fa-cheese:before{content:\"\\f7ef\"}.fa-chess:before{content:\"\\f439\"}.fa-chess-bishop:before{content:\"\\f43a\"}.fa-chess-board:before{content:\"\\f43c\"}.fa-chess-king:before{content:\"\\f43f\"}.fa-chess-knight:before{content:\"\\f441\"}.fa-chess-pawn:before{content:\"\\f443\"}.fa-chess-queen:before{content:\"\\f445\"}.fa-chess-rook:before{content:\"\\f447\"}.fa-chevron-circle-down:before{content:\"\\f13a\"}.fa-chevron-circle-left:before{content:\"\\f137\"}.fa-chevron-circle-right:before{content:\"\\f138\"}.fa-chevron-circle-up:before{content:\"\\f139\"}.fa-chevron-down:before{content:\"\\f078\"}.fa-chevron-left:before{content:\"\\f053\"}.fa-chevron-right:before{content:\"\\f054\"}.fa-chevron-up:before{content:\"\\f077\"}.fa-child:before{content:\"\\f1ae\"}.fa-chrome:before{content:\"\\f268\"}.fa-chromecast:before{content:\"\\f838\"}.fa-church:before{content:\"\\f51d\"}.fa-circle:before{content:\"\\f111\"}.fa-circle-notch:before{content:\"\\f1ce\"}.fa-city:before{content:\"\\f64f\"}.fa-clinic-medical:before{content:\"\\f7f2\"}.fa-clipboard:before{content:\"\\f328\"}.fa-clipboard-check:before{content:\"\\f46c\"}.fa-clipboard-list:before{content:\"\\f46d\"}.fa-clock:before{content:\"\\f017\"}.fa-clone:before{content:\"\\f24d\"}.fa-closed-captioning:before{content:\"\\f20a\"}.fa-cloud:before{content:\"\\f0c2\"}.fa-cloud-download-alt:before{content:\"\\f381\"}.fa-cloud-meatball:before{content:\"\\f73b\"}.fa-cloud-moon:before{content:\"\\f6c3\"}.fa-cloud-moon-rain:before{content:\"\\f73c\"}.fa-cloud-rain:before{content:\"\\f73d\"}.fa-cloud-showers-heavy:before{content:\"\\f740\"}.fa-cloud-sun:before{content:\"\\f6c4\"}.fa-cloud-sun-rain:before{content:\"\\f743\"}.fa-cloud-upload-alt:before{content:\"\\f382\"}.fa-cloudflare:before{content:\"\\e07d\"}.fa-cloudscale:before{content:\"\\f383\"}.fa-cloudsmith:before{content:\"\\f384\"}.fa-cloudversify:before{content:\"\\f385\"}.fa-cocktail:before{content:\"\\f561\"}.fa-code:before{content:\"\\f121\"}.fa-code-branch:before{content:\"\\f126\"}.fa-codepen:before{content:\"\\f1cb\"}.fa-codiepie:before{content:\"\\f284\"}.fa-coffee:before{content:\"\\f0f4\"}.fa-cog:before{content:\"\\f013\"}.fa-cogs:before{content:\"\\f085\"}.fa-coins:before{content:\"\\f51e\"}.fa-columns:before{content:\"\\f0db\"}.fa-comment:before{content:\"\\f075\"}.fa-comment-alt:before{content:\"\\f27a\"}.fa-comment-dollar:before{content:\"\\f651\"}.fa-comment-dots:before{content:\"\\f4ad\"}.fa-comment-medical:before{content:\"\\f7f5\"}.fa-comment-slash:before{content:\"\\f4b3\"}.fa-comments:before{content:\"\\f086\"}.fa-comments-dollar:before{content:\"\\f653\"}.fa-compact-disc:before{content:\"\\f51f\"}.fa-compass:before{content:\"\\f14e\"}.fa-compress:before{content:\"\\f066\"}.fa-compress-alt:before{content:\"\\f422\"}.fa-compress-arrows-alt:before{content:\"\\f78c\"}.fa-concierge-bell:before{content:\"\\f562\"}.fa-confluence:before{content:\"\\f78d\"}.fa-connectdevelop:before{content:\"\\f20e\"}.fa-contao:before{content:\"\\f26d\"}.fa-cookie:before{content:\"\\f563\"}.fa-cookie-bite:before{content:\"\\f564\"}.fa-copy:before{content:\"\\f0c5\"}.fa-copyright:before{content:\"\\f1f9\"}.fa-cotton-bureau:before{content:\"\\f89e\"}.fa-couch:before{content:\"\\f4b8\"}.fa-cpanel:before{content:\"\\f388\"}.fa-creative-commons:before{content:\"\\f25e\"}.fa-creative-commons-by:before{content:\"\\f4e7\"}.fa-creative-commons-nc:before{content:\"\\f4e8\"}.fa-creative-commons-nc-eu:before{content:\"\\f4e9\"}.fa-creative-commons-nc-jp:before{content:\"\\f4ea\"}.fa-creative-commons-nd:before{content:\"\\f4eb\"}.fa-creative-commons-pd:before{content:\"\\f4ec\"}.fa-creative-commons-pd-alt:before{content:\"\\f4ed\"}.fa-creative-commons-remix:before{content:\"\\f4ee\"}.fa-creative-commons-sa:before{content:\"\\f4ef\"}.fa-creative-commons-sampling:before{content:\"\\f4f0\"}.fa-creative-commons-sampling-plus:before{content:\"\\f4f1\"}.fa-creative-commons-share:before{content:\"\\f4f2\"}.fa-creative-commons-zero:before{content:\"\\f4f3\"}.fa-credit-card:before{content:\"\\f09d\"}.fa-critical-role:before{content:\"\\f6c9\"}.fa-crop:before{content:\"\\f125\"}.fa-crop-alt:before{content:\"\\f565\"}.fa-cross:before{content:\"\\f654\"}.fa-crosshairs:before{content:\"\\f05b\"}.fa-crow:before{content:\"\\f520\"}.fa-crown:before{content:\"\\f521\"}.fa-crutch:before{content:\"\\f7f7\"}.fa-css3:before{content:\"\\f13c\"}.fa-css3-alt:before{content:\"\\f38b\"}.fa-cube:before{content:\"\\f1b2\"}.fa-cubes:before{content:\"\\f1b3\"}.fa-cut:before{content:\"\\f0c4\"}.fa-cuttlefish:before{content:\"\\f38c\"}.fa-d-and-d:before{content:\"\\f38d\"}.fa-d-and-d-beyond:before{content:\"\\f6ca\"}.fa-dailymotion:before{content:\"\\e052\"}.fa-dashcube:before{content:\"\\f210\"}.fa-database:before{content:\"\\f1c0\"}.fa-deaf:before{content:\"\\f2a4\"}.fa-deezer:before{content:\"\\e077\"}.fa-delicious:before{content:\"\\f1a5\"}.fa-democrat:before{content:\"\\f747\"}.fa-deploydog:before{content:\"\\f38e\"}.fa-deskpro:before{content:\"\\f38f\"}.fa-desktop:before{content:\"\\f108\"}.fa-dev:before{content:\"\\f6cc\"}.fa-deviantart:before{content:\"\\f1bd\"}.fa-dharmachakra:before{content:\"\\f655\"}.fa-dhl:before{content:\"\\f790\"}.fa-diagnoses:before{content:\"\\f470\"}.fa-diaspora:before{content:\"\\f791\"}.fa-dice:before{content:\"\\f522\"}.fa-dice-d20:before{content:\"\\f6cf\"}.fa-dice-d6:before{content:\"\\f6d1\"}.fa-dice-five:before{content:\"\\f523\"}.fa-dice-four:before{content:\"\\f524\"}.fa-dice-one:before{content:\"\\f525\"}.fa-dice-six:before{content:\"\\f526\"}.fa-dice-three:before{content:\"\\f527\"}.fa-dice-two:before{content:\"\\f528\"}.fa-digg:before{content:\"\\f1a6\"}.fa-digital-ocean:before{content:\"\\f391\"}.fa-digital-tachograph:before{content:\"\\f566\"}.fa-directions:before{content:\"\\f5eb\"}.fa-discord:before{content:\"\\f392\"}.fa-discourse:before{content:\"\\f393\"}.fa-disease:before{content:\"\\f7fa\"}.fa-divide:before{content:\"\\f529\"}.fa-dizzy:before{content:\"\\f567\"}.fa-dna:before{content:\"\\f471\"}.fa-dochub:before{content:\"\\f394\"}.fa-docker:before{content:\"\\f395\"}.fa-dog:before{content:\"\\f6d3\"}.fa-dollar-sign:before{content:\"\\f155\"}.fa-dolly:before{content:\"\\f472\"}.fa-dolly-flatbed:before{content:\"\\f474\"}.fa-donate:before{content:\"\\f4b9\"}.fa-door-closed:before{content:\"\\f52a\"}.fa-door-open:before{content:\"\\f52b\"}.fa-dot-circle:before{content:\"\\f192\"}.fa-dove:before{content:\"\\f4ba\"}.fa-download:before{content:\"\\f019\"}.fa-draft2digital:before{content:\"\\f396\"}.fa-drafting-compass:before{content:\"\\f568\"}.fa-dragon:before{content:\"\\f6d5\"}.fa-draw-polygon:before{content:\"\\f5ee\"}.fa-dribbble:before{content:\"\\f17d\"}.fa-dribbble-square:before{content:\"\\f397\"}.fa-dropbox:before{content:\"\\f16b\"}.fa-drum:before{content:\"\\f569\"}.fa-drum-steelpan:before{content:\"\\f56a\"}.fa-drumstick-bite:before{content:\"\\f6d7\"}.fa-drupal:before{content:\"\\f1a9\"}.fa-dumbbell:before{content:\"\\f44b\"}.fa-dumpster:before{content:\"\\f793\"}.fa-dumpster-fire:before{content:\"\\f794\"}.fa-dungeon:before{content:\"\\f6d9\"}.fa-dyalog:before{content:\"\\f399\"}.fa-earlybirds:before{content:\"\\f39a\"}.fa-ebay:before{content:\"\\f4f4\"}.fa-edge:before{content:\"\\f282\"}.fa-edge-legacy:before{content:\"\\e078\"}.fa-edit:before{content:\"\\f044\"}.fa-egg:before{content:\"\\f7fb\"}.fa-eject:before{content:\"\\f052\"}.fa-elementor:before{content:\"\\f430\"}.fa-ellipsis-h:before{content:\"\\f141\"}.fa-ellipsis-v:before{content:\"\\f142\"}.fa-ello:before{content:\"\\f5f1\"}.fa-ember:before{content:\"\\f423\"}.fa-empire:before{content:\"\\f1d1\"}.fa-envelope:before{content:\"\\f0e0\"}.fa-envelope-open:before{content:\"\\f2b6\"}.fa-envelope-open-text:before{content:\"\\f658\"}.fa-envelope-square:before{content:\"\\f199\"}.fa-envira:before{content:\"\\f299\"}.fa-equals:before{content:\"\\f52c\"}.fa-eraser:before{content:\"\\f12d\"}.fa-erlang:before{content:\"\\f39d\"}.fa-ethereum:before{content:\"\\f42e\"}.fa-ethernet:before{content:\"\\f796\"}.fa-etsy:before{content:\"\\f2d7\"}.fa-euro-sign:before{content:\"\\f153\"}.fa-evernote:before{content:\"\\f839\"}.fa-exchange-alt:before{content:\"\\f362\"}.fa-exclamation:before{content:\"\\f12a\"}.fa-exclamation-circle:before{content:\"\\f06a\"}.fa-exclamation-triangle:before{content:\"\\f071\"}.fa-expand:before{content:\"\\f065\"}.fa-expand-alt:before{content:\"\\f424\"}.fa-expand-arrows-alt:before{content:\"\\f31e\"}.fa-expeditedssl:before{content:\"\\f23e\"}.fa-external-link-alt:before{content:\"\\f35d\"}.fa-external-link-square-alt:before{content:\"\\f360\"}.fa-eye:before{content:\"\\f06e\"}.fa-eye-dropper:before{content:\"\\f1fb\"}.fa-eye-slash:before{content:\"\\f070\"}.fa-facebook:before{content:\"\\f09a\"}.fa-facebook-f:before{content:\"\\f39e\"}.fa-facebook-messenger:before{content:\"\\f39f\"}.fa-facebook-square:before{content:\"\\f082\"}.fa-fan:before{content:\"\\f863\"}.fa-fantasy-flight-games:before{content:\"\\f6dc\"}.fa-fast-backward:before{content:\"\\f049\"}.fa-fast-forward:before{content:\"\\f050\"}.fa-faucet:before{content:\"\\e005\"}.fa-fax:before{content:\"\\f1ac\"}.fa-feather:before{content:\"\\f52d\"}.fa-feather-alt:before{content:\"\\f56b\"}.fa-fedex:before{content:\"\\f797\"}.fa-fedora:before{content:\"\\f798\"}.fa-female:before{content:\"\\f182\"}.fa-fighter-jet:before{content:\"\\f0fb\"}.fa-figma:before{content:\"\\f799\"}.fa-file:before{content:\"\\f15b\"}.fa-file-alt:before{content:\"\\f15c\"}.fa-file-archive:before{content:\"\\f1c6\"}.fa-file-audio:before{content:\"\\f1c7\"}.fa-file-code:before{content:\"\\f1c9\"}.fa-file-contract:before{content:\"\\f56c\"}.fa-file-csv:before{content:\"\\f6dd\"}.fa-file-download:before{content:\"\\f56d\"}.fa-file-excel:before{content:\"\\f1c3\"}.fa-file-export:before{content:\"\\f56e\"}.fa-file-image:before{content:\"\\f1c5\"}.fa-file-import:before{content:\"\\f56f\"}.fa-file-invoice:before{content:\"\\f570\"}.fa-file-invoice-dollar:before{content:\"\\f571\"}.fa-file-medical:before{content:\"\\f477\"}.fa-file-medical-alt:before{content:\"\\f478\"}.fa-file-pdf:before{content:\"\\f1c1\"}.fa-file-powerpoint:before{content:\"\\f1c4\"}.fa-file-prescription:before{content:\"\\f572\"}.fa-file-signature:before{content:\"\\f573\"}.fa-file-upload:before{content:\"\\f574\"}.fa-file-video:before{content:\"\\f1c8\"}.fa-file-word:before{content:\"\\f1c2\"}.fa-fill:before{content:\"\\f575\"}.fa-fill-drip:before{content:\"\\f576\"}.fa-film:before{content:\"\\f008\"}.fa-filter:before{content:\"\\f0b0\"}.fa-fingerprint:before{content:\"\\f577\"}.fa-fire:before{content:\"\\f06d\"}.fa-fire-alt:before{content:\"\\f7e4\"}.fa-fire-extinguisher:before{content:\"\\f134\"}.fa-firefox:before{content:\"\\f269\"}.fa-firefox-browser:before{content:\"\\e007\"}.fa-first-aid:before{content:\"\\f479\"}.fa-first-order:before{content:\"\\f2b0\"}.fa-first-order-alt:before{content:\"\\f50a\"}.fa-firstdraft:before{content:\"\\f3a1\"}.fa-fish:before{content:\"\\f578\"}.fa-fist-raised:before{content:\"\\f6de\"}.fa-flag:before{content:\"\\f024\"}.fa-flag-checkered:before{content:\"\\f11e\"}.fa-flag-usa:before{content:\"\\f74d\"}.fa-flask:before{content:\"\\f0c3\"}.fa-flickr:before{content:\"\\f16e\"}.fa-flipboard:before{content:\"\\f44d\"}.fa-flushed:before{content:\"\\f579\"}.fa-fly:before{content:\"\\f417\"}.fa-folder:before{content:\"\\f07b\"}.fa-folder-minus:before{content:\"\\f65d\"}.fa-folder-open:before{content:\"\\f07c\"}.fa-folder-plus:before{content:\"\\f65e\"}.fa-font:before{content:\"\\f031\"}.fa-font-awesome:before{content:\"\\f2b4\"}.fa-font-awesome-alt:before{content:\"\\f35c\"}.fa-font-awesome-flag:before{content:\"\\f425\"}.fa-font-awesome-logo-full:before{content:\"\\f4e6\"}.fa-fonticons:before{content:\"\\f280\"}.fa-fonticons-fi:before{content:\"\\f3a2\"}.fa-football-ball:before{content:\"\\f44e\"}.fa-fort-awesome:before{content:\"\\f286\"}.fa-fort-awesome-alt:before{content:\"\\f3a3\"}.fa-forumbee:before{content:\"\\f211\"}.fa-forward:before{content:\"\\f04e\"}.fa-foursquare:before{content:\"\\f180\"}.fa-free-code-camp:before{content:\"\\f2c5\"}.fa-freebsd:before{content:\"\\f3a4\"}.fa-frog:before{content:\"\\f52e\"}.fa-frown:before{content:\"\\f119\"}.fa-frown-open:before{content:\"\\f57a\"}.fa-fulcrum:before{content:\"\\f50b\"}.fa-funnel-dollar:before{content:\"\\f662\"}.fa-futbol:before{content:\"\\f1e3\"}.fa-galactic-republic:before{content:\"\\f50c\"}.fa-galactic-senate:before{content:\"\\f50d\"}.fa-gamepad:before{content:\"\\f11b\"}.fa-gas-pump:before{content:\"\\f52f\"}.fa-gavel:before{content:\"\\f0e3\"}.fa-gem:before{content:\"\\f3a5\"}.fa-genderless:before{content:\"\\f22d\"}.fa-get-pocket:before{content:\"\\f265\"}.fa-gg:before{content:\"\\f260\"}.fa-gg-circle:before{content:\"\\f261\"}.fa-ghost:before{content:\"\\f6e2\"}.fa-gift:before{content:\"\\f06b\"}.fa-gifts:before{content:\"\\f79c\"}.fa-git:before{content:\"\\f1d3\"}.fa-git-alt:before{content:\"\\f841\"}.fa-git-square:before{content:\"\\f1d2\"}.fa-github:before{content:\"\\f09b\"}.fa-github-alt:before{content:\"\\f113\"}.fa-github-square:before{content:\"\\f092\"}.fa-gitkraken:before{content:\"\\f3a6\"}.fa-gitlab:before{content:\"\\f296\"}.fa-gitter:before{content:\"\\f426\"}.fa-glass-cheers:before{content:\"\\f79f\"}.fa-glass-martini:before{content:\"\\f000\"}.fa-glass-martini-alt:before{content:\"\\f57b\"}.fa-glass-whiskey:before{content:\"\\f7a0\"}.fa-glasses:before{content:\"\\f530\"}.fa-glide:before{content:\"\\f2a5\"}.fa-glide-g:before{content:\"\\f2a6\"}.fa-globe:before{content:\"\\f0ac\"}.fa-globe-africa:before{content:\"\\f57c\"}.fa-globe-americas:before{content:\"\\f57d\"}.fa-globe-asia:before{content:\"\\f57e\"}.fa-globe-europe:before{content:\"\\f7a2\"}.fa-gofore:before{content:\"\\f3a7\"}.fa-golf-ball:before{content:\"\\f450\"}.fa-goodreads:before{content:\"\\f3a8\"}.fa-goodreads-g:before{content:\"\\f3a9\"}.fa-google:before{content:\"\\f1a0\"}.fa-google-drive:before{content:\"\\f3aa\"}.fa-google-pay:before{content:\"\\e079\"}.fa-google-play:before{content:\"\\f3ab\"}.fa-google-plus:before{content:\"\\f2b3\"}.fa-google-plus-g:before{content:\"\\f0d5\"}.fa-google-plus-square:before{content:\"\\f0d4\"}.fa-google-wallet:before{content:\"\\f1ee\"}.fa-gopuram:before{content:\"\\f664\"}.fa-graduation-cap:before{content:\"\\f19d\"}.fa-gratipay:before{content:\"\\f184\"}.fa-grav:before{content:\"\\f2d6\"}.fa-greater-than:before{content:\"\\f531\"}.fa-greater-than-equal:before{content:\"\\f532\"}.fa-grimace:before{content:\"\\f57f\"}.fa-grin:before{content:\"\\f580\"}.fa-grin-alt:before{content:\"\\f581\"}.fa-grin-beam:before{content:\"\\f582\"}.fa-grin-beam-sweat:before{content:\"\\f583\"}.fa-grin-hearts:before{content:\"\\f584\"}.fa-grin-squint:before{content:\"\\f585\"}.fa-grin-squint-tears:before{content:\"\\f586\"}.fa-grin-stars:before{content:\"\\f587\"}.fa-grin-tears:before{content:\"\\f588\"}.fa-grin-tongue:before{content:\"\\f589\"}.fa-grin-tongue-squint:before{content:\"\\f58a\"}.fa-grin-tongue-wink:before{content:\"\\f58b\"}.fa-grin-wink:before{content:\"\\f58c\"}.fa-grip-horizontal:before{content:\"\\f58d\"}.fa-grip-lines:before{content:\"\\f7a4\"}.fa-grip-lines-vertical:before{content:\"\\f7a5\"}.fa-grip-vertical:before{content:\"\\f58e\"}.fa-gripfire:before{content:\"\\f3ac\"}.fa-grunt:before{content:\"\\f3ad\"}.fa-guilded:before{content:\"\\e07e\"}.fa-guitar:before{content:\"\\f7a6\"}.fa-gulp:before{content:\"\\f3ae\"}.fa-h-square:before{content:\"\\f0fd\"}.fa-hacker-news:before{content:\"\\f1d4\"}.fa-hacker-news-square:before{content:\"\\f3af\"}.fa-hackerrank:before{content:\"\\f5f7\"}.fa-hamburger:before{content:\"\\f805\"}.fa-hammer:before{content:\"\\f6e3\"}.fa-hamsa:before{content:\"\\f665\"}.fa-hand-holding:before{content:\"\\f4bd\"}.fa-hand-holding-heart:before{content:\"\\f4be\"}.fa-hand-holding-medical:before{content:\"\\e05c\"}.fa-hand-holding-usd:before{content:\"\\f4c0\"}.fa-hand-holding-water:before{content:\"\\f4c1\"}.fa-hand-lizard:before{content:\"\\f258\"}.fa-hand-middle-finger:before{content:\"\\f806\"}.fa-hand-paper:before{content:\"\\f256\"}.fa-hand-peace:before{content:\"\\f25b\"}.fa-hand-point-down:before{content:\"\\f0a7\"}.fa-hand-point-left:before{content:\"\\f0a5\"}.fa-hand-point-right:before{content:\"\\f0a4\"}.fa-hand-point-up:before{content:\"\\f0a6\"}.fa-hand-pointer:before{content:\"\\f25a\"}.fa-hand-rock:before{content:\"\\f255\"}.fa-hand-scissors:before{content:\"\\f257\"}.fa-hand-sparkles:before{content:\"\\e05d\"}.fa-hand-spock:before{content:\"\\f259\"}.fa-hands:before{content:\"\\f4c2\"}.fa-hands-helping:before{content:\"\\f4c4\"}.fa-hands-wash:before{content:\"\\e05e\"}.fa-handshake:before{content:\"\\f2b5\"}.fa-handshake-alt-slash:before{content:\"\\e05f\"}.fa-handshake-slash:before{content:\"\\e060\"}.fa-hanukiah:before{content:\"\\f6e6\"}.fa-hard-hat:before{content:\"\\f807\"}.fa-hashtag:before{content:\"\\f292\"}.fa-hat-cowboy:before{content:\"\\f8c0\"}.fa-hat-cowboy-side:before{content:\"\\f8c1\"}.fa-hat-wizard:before{content:\"\\f6e8\"}.fa-hdd:before{content:\"\\f0a0\"}.fa-head-side-cough:before{content:\"\\e061\"}.fa-head-side-cough-slash:before{content:\"\\e062\"}.fa-head-side-mask:before{content:\"\\e063\"}.fa-head-side-virus:before{content:\"\\e064\"}.fa-heading:before{content:\"\\f1dc\"}.fa-headphones:before{content:\"\\f025\"}.fa-headphones-alt:before{content:\"\\f58f\"}.fa-headset:before{content:\"\\f590\"}.fa-heart:before{content:\"\\f004\"}.fa-heart-broken:before{content:\"\\f7a9\"}.fa-heartbeat:before{content:\"\\f21e\"}.fa-helicopter:before{content:\"\\f533\"}.fa-highlighter:before{content:\"\\f591\"}.fa-hiking:before{content:\"\\f6ec\"}.fa-hippo:before{content:\"\\f6ed\"}.fa-hips:before{content:\"\\f452\"}.fa-hire-a-helper:before{content:\"\\f3b0\"}.fa-history:before{content:\"\\f1da\"}.fa-hive:before{content:\"\\e07f\"}.fa-hockey-puck:before{content:\"\\f453\"}.fa-holly-berry:before{content:\"\\f7aa\"}.fa-home:before{content:\"\\f015\"}.fa-hooli:before{content:\"\\f427\"}.fa-hornbill:before{content:\"\\f592\"}.fa-horse:before{content:\"\\f6f0\"}.fa-horse-head:before{content:\"\\f7ab\"}.fa-hospital:before{content:\"\\f0f8\"}.fa-hospital-alt:before{content:\"\\f47d\"}.fa-hospital-symbol:before{content:\"\\f47e\"}.fa-hospital-user:before{content:\"\\f80d\"}.fa-hot-tub:before{content:\"\\f593\"}.fa-hotdog:before{content:\"\\f80f\"}.fa-hotel:before{content:\"\\f594\"}.fa-hotjar:before{content:\"\\f3b1\"}.fa-hourglass:before{content:\"\\f254\"}.fa-hourglass-end:before{content:\"\\f253\"}.fa-hourglass-half:before{content:\"\\f252\"}.fa-hourglass-start:before{content:\"\\f251\"}.fa-house-damage:before{content:\"\\f6f1\"}.fa-house-user:before{content:\"\\e065\"}.fa-houzz:before{content:\"\\f27c\"}.fa-hryvnia:before{content:\"\\f6f2\"}.fa-html5:before{content:\"\\f13b\"}.fa-hubspot:before{content:\"\\f3b2\"}.fa-i-cursor:before{content:\"\\f246\"}.fa-ice-cream:before{content:\"\\f810\"}.fa-icicles:before{content:\"\\f7ad\"}.fa-icons:before{content:\"\\f86d\"}.fa-id-badge:before{content:\"\\f2c1\"}.fa-id-card:before{content:\"\\f2c2\"}.fa-id-card-alt:before{content:\"\\f47f\"}.fa-ideal:before{content:\"\\e013\"}.fa-igloo:before{content:\"\\f7ae\"}.fa-image:before{content:\"\\f03e\"}.fa-images:before{content:\"\\f302\"}.fa-imdb:before{content:\"\\f2d8\"}.fa-inbox:before{content:\"\\f01c\"}.fa-indent:before{content:\"\\f03c\"}.fa-industry:before{content:\"\\f275\"}.fa-infinity:before{content:\"\\f534\"}.fa-info:before{content:\"\\f129\"}.fa-info-circle:before{content:\"\\f05a\"}.fa-innosoft:before{content:\"\\e080\"}.fa-instagram:before{content:\"\\f16d\"}.fa-instagram-square:before{content:\"\\e055\"}.fa-instalod:before{content:\"\\e081\"}.fa-intercom:before{content:\"\\f7af\"}.fa-internet-explorer:before{content:\"\\f26b\"}.fa-invision:before{content:\"\\f7b0\"}.fa-ioxhost:before{content:\"\\f208\"}.fa-italic:before{content:\"\\f033\"}.fa-itch-io:before{content:\"\\f83a\"}.fa-itunes:before{content:\"\\f3b4\"}.fa-itunes-note:before{content:\"\\f3b5\"}.fa-java:before{content:\"\\f4e4\"}.fa-jedi:before{content:\"\\f669\"}.fa-jedi-order:before{content:\"\\f50e\"}.fa-jenkins:before{content:\"\\f3b6\"}.fa-jira:before{content:\"\\f7b1\"}.fa-joget:before{content:\"\\f3b7\"}.fa-joint:before{content:\"\\f595\"}.fa-joomla:before{content:\"\\f1aa\"}.fa-journal-whills:before{content:\"\\f66a\"}.fa-js:before{content:\"\\f3b8\"}.fa-js-square:before{content:\"\\f3b9\"}.fa-jsfiddle:before{content:\"\\f1cc\"}.fa-kaaba:before{content:\"\\f66b\"}.fa-kaggle:before{content:\"\\f5fa\"}.fa-key:before{content:\"\\f084\"}.fa-keybase:before{content:\"\\f4f5\"}.fa-keyboard:before{content:\"\\f11c\"}.fa-keycdn:before{content:\"\\f3ba\"}.fa-khanda:before{content:\"\\f66d\"}.fa-kickstarter:before{content:\"\\f3bb\"}.fa-kickstarter-k:before{content:\"\\f3bc\"}.fa-kiss:before{content:\"\\f596\"}.fa-kiss-beam:before{content:\"\\f597\"}.fa-kiss-wink-heart:before{content:\"\\f598\"}.fa-kiwi-bird:before{content:\"\\f535\"}.fa-korvue:before{content:\"\\f42f\"}.fa-landmark:before{content:\"\\f66f\"}.fa-language:before{content:\"\\f1ab\"}.fa-laptop:before{content:\"\\f109\"}.fa-laptop-code:before{content:\"\\f5fc\"}.fa-laptop-house:before{content:\"\\e066\"}.fa-laptop-medical:before{content:\"\\f812\"}.fa-laravel:before{content:\"\\f3bd\"}.fa-lastfm:before{content:\"\\f202\"}.fa-lastfm-square:before{content:\"\\f203\"}.fa-laugh:before{content:\"\\f599\"}.fa-laugh-beam:before{content:\"\\f59a\"}.fa-laugh-squint:before{content:\"\\f59b\"}.fa-laugh-wink:before{content:\"\\f59c\"}.fa-layer-group:before{content:\"\\f5fd\"}.fa-leaf:before{content:\"\\f06c\"}.fa-leanpub:before{content:\"\\f212\"}.fa-lemon:before{content:\"\\f094\"}.fa-less:before{content:\"\\f41d\"}.fa-less-than:before{content:\"\\f536\"}.fa-less-than-equal:before{content:\"\\f537\"}.fa-level-down-alt:before{content:\"\\f3be\"}.fa-level-up-alt:before{content:\"\\f3bf\"}.fa-life-ring:before{content:\"\\f1cd\"}.fa-lightbulb:before{content:\"\\f0eb\"}.fa-line:before{content:\"\\f3c0\"}.fa-link:before{content:\"\\f0c1\"}.fa-linkedin:before{content:\"\\f08c\"}.fa-linkedin-in:before{content:\"\\f0e1\"}.fa-linode:before{content:\"\\f2b8\"}.fa-linux:before{content:\"\\f17c\"}.fa-lira-sign:before{content:\"\\f195\"}.fa-list:before{content:\"\\f03a\"}.fa-list-alt:before{content:\"\\f022\"}.fa-list-ol:before{content:\"\\f0cb\"}.fa-list-ul:before{content:\"\\f0ca\"}.fa-location-arrow:before{content:\"\\f124\"}.fa-lock:before{content:\"\\f023\"}.fa-lock-open:before{content:\"\\f3c1\"}.fa-long-arrow-alt-down:before{content:\"\\f309\"}.fa-long-arrow-alt-left:before{content:\"\\f30a\"}.fa-long-arrow-alt-right:before{content:\"\\f30b\"}.fa-long-arrow-alt-up:before{content:\"\\f30c\"}.fa-low-vision:before{content:\"\\f2a8\"}.fa-luggage-cart:before{content:\"\\f59d\"}.fa-lungs:before{content:\"\\f604\"}.fa-lungs-virus:before{content:\"\\e067\"}.fa-lyft:before{content:\"\\f3c3\"}.fa-magento:before{content:\"\\f3c4\"}.fa-magic:before{content:\"\\f0d0\"}.fa-magnet:before{content:\"\\f076\"}.fa-mail-bulk:before{content:\"\\f674\"}.fa-mailchimp:before{content:\"\\f59e\"}.fa-male:before{content:\"\\f183\"}.fa-mandalorian:before{content:\"\\f50f\"}.fa-map:before{content:\"\\f279\"}.fa-map-marked:before{content:\"\\f59f\"}.fa-map-marked-alt:before{content:\"\\f5a0\"}.fa-map-marker:before{content:\"\\f041\"}.fa-map-marker-alt:before{content:\"\\f3c5\"}.fa-map-pin:before{content:\"\\f276\"}.fa-map-signs:before{content:\"\\f277\"}.fa-markdown:before{content:\"\\f60f\"}.fa-marker:before{content:\"\\f5a1\"}.fa-mars:before{content:\"\\f222\"}.fa-mars-double:before{content:\"\\f227\"}.fa-mars-stroke:before{content:\"\\f229\"}.fa-mars-stroke-h:before{content:\"\\f22b\"}.fa-mars-stroke-v:before{content:\"\\f22a\"}.fa-mask:before{content:\"\\f6fa\"}.fa-mastodon:before{content:\"\\f4f6\"}.fa-maxcdn:before{content:\"\\f136\"}.fa-mdb:before{content:\"\\f8ca\"}.fa-medal:before{content:\"\\f5a2\"}.fa-medapps:before{content:\"\\f3c6\"}.fa-medium:before{content:\"\\f23a\"}.fa-medium-m:before{content:\"\\f3c7\"}.fa-medkit:before{content:\"\\f0fa\"}.fa-medrt:before{content:\"\\f3c8\"}.fa-meetup:before{content:\"\\f2e0\"}.fa-megaport:before{content:\"\\f5a3\"}.fa-meh:before{content:\"\\f11a\"}.fa-meh-blank:before{content:\"\\f5a4\"}.fa-meh-rolling-eyes:before{content:\"\\f5a5\"}.fa-memory:before{content:\"\\f538\"}.fa-mendeley:before{content:\"\\f7b3\"}.fa-menorah:before{content:\"\\f676\"}.fa-mercury:before{content:\"\\f223\"}.fa-meteor:before{content:\"\\f753\"}.fa-microblog:before{content:\"\\e01a\"}.fa-microchip:before{content:\"\\f2db\"}.fa-microphone:before{content:\"\\f130\"}.fa-microphone-alt:before{content:\"\\f3c9\"}.fa-microphone-alt-slash:before{content:\"\\f539\"}.fa-microphone-slash:before{content:\"\\f131\"}.fa-microscope:before{content:\"\\f610\"}.fa-microsoft:before{content:\"\\f3ca\"}.fa-minus:before{content:\"\\f068\"}.fa-minus-circle:before{content:\"\\f056\"}.fa-minus-square:before{content:\"\\f146\"}.fa-mitten:before{content:\"\\f7b5\"}.fa-mix:before{content:\"\\f3cb\"}.fa-mixcloud:before{content:\"\\f289\"}.fa-mixer:before{content:\"\\e056\"}.fa-mizuni:before{content:\"\\f3cc\"}.fa-mobile:before{content:\"\\f10b\"}.fa-mobile-alt:before{content:\"\\f3cd\"}.fa-modx:before{content:\"\\f285\"}.fa-monero:before{content:\"\\f3d0\"}.fa-money-bill:before{content:\"\\f0d6\"}.fa-money-bill-alt:before{content:\"\\f3d1\"}.fa-money-bill-wave:before{content:\"\\f53a\"}.fa-money-bill-wave-alt:before{content:\"\\f53b\"}.fa-money-check:before{content:\"\\f53c\"}.fa-money-check-alt:before{content:\"\\f53d\"}.fa-monument:before{content:\"\\f5a6\"}.fa-moon:before{content:\"\\f186\"}.fa-mortar-pestle:before{content:\"\\f5a7\"}.fa-mosque:before{content:\"\\f678\"}.fa-motorcycle:before{content:\"\\f21c\"}.fa-mountain:before{content:\"\\f6fc\"}.fa-mouse:before{content:\"\\f8cc\"}.fa-mouse-pointer:before{content:\"\\f245\"}.fa-mug-hot:before{content:\"\\f7b6\"}.fa-music:before{content:\"\\f001\"}.fa-napster:before{content:\"\\f3d2\"}.fa-neos:before{content:\"\\f612\"}.fa-network-wired:before{content:\"\\f6ff\"}.fa-neuter:before{content:\"\\f22c\"}.fa-newspaper:before{content:\"\\f1ea\"}.fa-nimblr:before{content:\"\\f5a8\"}.fa-node:before{content:\"\\f419\"}.fa-node-js:before{content:\"\\f3d3\"}.fa-not-equal:before{content:\"\\f53e\"}.fa-notes-medical:before{content:\"\\f481\"}.fa-npm:before{content:\"\\f3d4\"}.fa-ns8:before{content:\"\\f3d5\"}.fa-nutritionix:before{content:\"\\f3d6\"}.fa-object-group:before{content:\"\\f247\"}.fa-object-ungroup:before{content:\"\\f248\"}.fa-octopus-deploy:before{content:\"\\e082\"}.fa-odnoklassniki:before{content:\"\\f263\"}.fa-odnoklassniki-square:before{content:\"\\f264\"}.fa-oil-can:before{content:\"\\f613\"}.fa-old-republic:before{content:\"\\f510\"}.fa-om:before{content:\"\\f679\"}.fa-opencart:before{content:\"\\f23d\"}.fa-openid:before{content:\"\\f19b\"}.fa-opera:before{content:\"\\f26a\"}.fa-optin-monster:before{content:\"\\f23c\"}.fa-orcid:before{content:\"\\f8d2\"}.fa-osi:before{content:\"\\f41a\"}.fa-otter:before{content:\"\\f700\"}.fa-outdent:before{content:\"\\f03b\"}.fa-page4:before{content:\"\\f3d7\"}.fa-pagelines:before{content:\"\\f18c\"}.fa-pager:before{content:\"\\f815\"}.fa-paint-brush:before{content:\"\\f1fc\"}.fa-paint-roller:before{content:\"\\f5aa\"}.fa-palette:before{content:\"\\f53f\"}.fa-palfed:before{content:\"\\f3d8\"}.fa-pallet:before{content:\"\\f482\"}.fa-paper-plane:before{content:\"\\f1d8\"}.fa-paperclip:before{content:\"\\f0c6\"}.fa-parachute-box:before{content:\"\\f4cd\"}.fa-paragraph:before{content:\"\\f1dd\"}.fa-parking:before{content:\"\\f540\"}.fa-passport:before{content:\"\\f5ab\"}.fa-pastafarianism:before{content:\"\\f67b\"}.fa-paste:before{content:\"\\f0ea\"}.fa-patreon:before{content:\"\\f3d9\"}.fa-pause:before{content:\"\\f04c\"}.fa-pause-circle:before{content:\"\\f28b\"}.fa-paw:before{content:\"\\f1b0\"}.fa-paypal:before{content:\"\\f1ed\"}.fa-peace:before{content:\"\\f67c\"}.fa-pen:before{content:\"\\f304\"}.fa-pen-alt:before{content:\"\\f305\"}.fa-pen-fancy:before{content:\"\\f5ac\"}.fa-pen-nib:before{content:\"\\f5ad\"}.fa-pen-square:before{content:\"\\f14b\"}.fa-pencil-alt:before{content:\"\\f303\"}.fa-pencil-ruler:before{content:\"\\f5ae\"}.fa-penny-arcade:before{content:\"\\f704\"}.fa-people-arrows:before{content:\"\\e068\"}.fa-people-carry:before{content:\"\\f4ce\"}.fa-pepper-hot:before{content:\"\\f816\"}.fa-perbyte:before{content:\"\\e083\"}.fa-percent:before{content:\"\\f295\"}.fa-percentage:before{content:\"\\f541\"}.fa-periscope:before{content:\"\\f3da\"}.fa-person-booth:before{content:\"\\f756\"}.fa-phabricator:before{content:\"\\f3db\"}.fa-phoenix-framework:before{content:\"\\f3dc\"}.fa-phoenix-squadron:before{content:\"\\f511\"}.fa-phone:before{content:\"\\f095\"}.fa-phone-alt:before{content:\"\\f879\"}.fa-phone-slash:before{content:\"\\f3dd\"}.fa-phone-square:before{content:\"\\f098\"}.fa-phone-square-alt:before{content:\"\\f87b\"}.fa-phone-volume:before{content:\"\\f2a0\"}.fa-photo-video:before{content:\"\\f87c\"}.fa-php:before{content:\"\\f457\"}.fa-pied-piper:before{content:\"\\f2ae\"}.fa-pied-piper-alt:before{content:\"\\f1a8\"}.fa-pied-piper-hat:before{content:\"\\f4e5\"}.fa-pied-piper-pp:before{content:\"\\f1a7\"}.fa-pied-piper-square:before{content:\"\\e01e\"}.fa-piggy-bank:before{content:\"\\f4d3\"}.fa-pills:before{content:\"\\f484\"}.fa-pinterest:before{content:\"\\f0d2\"}.fa-pinterest-p:before{content:\"\\f231\"}.fa-pinterest-square:before{content:\"\\f0d3\"}.fa-pizza-slice:before{content:\"\\f818\"}.fa-place-of-worship:before{content:\"\\f67f\"}.fa-plane:before{content:\"\\f072\"}.fa-plane-arrival:before{content:\"\\f5af\"}.fa-plane-departure:before{content:\"\\f5b0\"}.fa-plane-slash:before{content:\"\\e069\"}.fa-play:before{content:\"\\f04b\"}.fa-play-circle:before{content:\"\\f144\"}.fa-playstation:before{content:\"\\f3df\"}.fa-plug:before{content:\"\\f1e6\"}.fa-plus:before{content:\"\\f067\"}.fa-plus-circle:before{content:\"\\f055\"}.fa-plus-square:before{content:\"\\f0fe\"}.fa-podcast:before{content:\"\\f2ce\"}.fa-poll:before{content:\"\\f681\"}.fa-poll-h:before{content:\"\\f682\"}.fa-poo:before{content:\"\\f2fe\"}.fa-poo-storm:before{content:\"\\f75a\"}.fa-poop:before{content:\"\\f619\"}.fa-portrait:before{content:\"\\f3e0\"}.fa-pound-sign:before{content:\"\\f154\"}.fa-power-off:before{content:\"\\f011\"}.fa-pray:before{content:\"\\f683\"}.fa-praying-hands:before{content:\"\\f684\"}.fa-prescription:before{content:\"\\f5b1\"}.fa-prescription-bottle:before{content:\"\\f485\"}.fa-prescription-bottle-alt:before{content:\"\\f486\"}.fa-print:before{content:\"\\f02f\"}.fa-procedures:before{content:\"\\f487\"}.fa-product-hunt:before{content:\"\\f288\"}.fa-project-diagram:before{content:\"\\f542\"}.fa-pump-medical:before{content:\"\\e06a\"}.fa-pump-soap:before{content:\"\\e06b\"}.fa-pushed:before{content:\"\\f3e1\"}.fa-puzzle-piece:before{content:\"\\f12e\"}.fa-python:before{content:\"\\f3e2\"}.fa-qq:before{content:\"\\f1d6\"}.fa-qrcode:before{content:\"\\f029\"}.fa-question:before{content:\"\\f128\"}.fa-question-circle:before{content:\"\\f059\"}.fa-quidditch:before{content:\"\\f458\"}.fa-quinscape:before{content:\"\\f459\"}.fa-quora:before{content:\"\\f2c4\"}.fa-quote-left:before{content:\"\\f10d\"}.fa-quote-right:before{content:\"\\f10e\"}.fa-quran:before{content:\"\\f687\"}.fa-r-project:before{content:\"\\f4f7\"}.fa-radiation:before{content:\"\\f7b9\"}.fa-radiation-alt:before{content:\"\\f7ba\"}.fa-rainbow:before{content:\"\\f75b\"}.fa-random:before{content:\"\\f074\"}.fa-raspberry-pi:before{content:\"\\f7bb\"}.fa-ravelry:before{content:\"\\f2d9\"}.fa-react:before{content:\"\\f41b\"}.fa-reacteurope:before{content:\"\\f75d\"}.fa-readme:before{content:\"\\f4d5\"}.fa-rebel:before{content:\"\\f1d0\"}.fa-receipt:before{content:\"\\f543\"}.fa-record-vinyl:before{content:\"\\f8d9\"}.fa-recycle:before{content:\"\\f1b8\"}.fa-red-river:before{content:\"\\f3e3\"}.fa-reddit:before{content:\"\\f1a1\"}.fa-reddit-alien:before{content:\"\\f281\"}.fa-reddit-square:before{content:\"\\f1a2\"}.fa-redhat:before{content:\"\\f7bc\"}.fa-redo:before{content:\"\\f01e\"}.fa-redo-alt:before{content:\"\\f2f9\"}.fa-registered:before{content:\"\\f25d\"}.fa-remove-format:before{content:\"\\f87d\"}.fa-renren:before{content:\"\\f18b\"}.fa-reply:before{content:\"\\f3e5\"}.fa-reply-all:before{content:\"\\f122\"}.fa-replyd:before{content:\"\\f3e6\"}.fa-republican:before{content:\"\\f75e\"}.fa-researchgate:before{content:\"\\f4f8\"}.fa-resolving:before{content:\"\\f3e7\"}.fa-restroom:before{content:\"\\f7bd\"}.fa-retweet:before{content:\"\\f079\"}.fa-rev:before{content:\"\\f5b2\"}.fa-ribbon:before{content:\"\\f4d6\"}.fa-ring:before{content:\"\\f70b\"}.fa-road:before{content:\"\\f018\"}.fa-robot:before{content:\"\\f544\"}.fa-rocket:before{content:\"\\f135\"}.fa-rocketchat:before{content:\"\\f3e8\"}.fa-rockrms:before{content:\"\\f3e9\"}.fa-route:before{content:\"\\f4d7\"}.fa-rss:before{content:\"\\f09e\"}.fa-rss-square:before{content:\"\\f143\"}.fa-ruble-sign:before{content:\"\\f158\"}.fa-ruler:before{content:\"\\f545\"}.fa-ruler-combined:before{content:\"\\f546\"}.fa-ruler-horizontal:before{content:\"\\f547\"}.fa-ruler-vertical:before{content:\"\\f548\"}.fa-running:before{content:\"\\f70c\"}.fa-rupee-sign:before{content:\"\\f156\"}.fa-rust:before{content:\"\\e07a\"}.fa-sad-cry:before{content:\"\\f5b3\"}.fa-sad-tear:before{content:\"\\f5b4\"}.fa-safari:before{content:\"\\f267\"}.fa-salesforce:before{content:\"\\f83b\"}.fa-sass:before{content:\"\\f41e\"}.fa-satellite:before{content:\"\\f7bf\"}.fa-satellite-dish:before{content:\"\\f7c0\"}.fa-save:before{content:\"\\f0c7\"}.fa-schlix:before{content:\"\\f3ea\"}.fa-school:before{content:\"\\f549\"}.fa-screwdriver:before{content:\"\\f54a\"}.fa-scribd:before{content:\"\\f28a\"}.fa-scroll:before{content:\"\\f70e\"}.fa-sd-card:before{content:\"\\f7c2\"}.fa-search:before{content:\"\\f002\"}.fa-search-dollar:before{content:\"\\f688\"}.fa-search-location:before{content:\"\\f689\"}.fa-search-minus:before{content:\"\\f010\"}.fa-search-plus:before{content:\"\\f00e\"}.fa-searchengin:before{content:\"\\f3eb\"}.fa-seedling:before{content:\"\\f4d8\"}.fa-sellcast:before{content:\"\\f2da\"}.fa-sellsy:before{content:\"\\f213\"}.fa-server:before{content:\"\\f233\"}.fa-servicestack:before{content:\"\\f3ec\"}.fa-shapes:before{content:\"\\f61f\"}.fa-share:before{content:\"\\f064\"}.fa-share-alt:before{content:\"\\f1e0\"}.fa-share-alt-square:before{content:\"\\f1e1\"}.fa-share-square:before{content:\"\\f14d\"}.fa-shekel-sign:before{content:\"\\f20b\"}.fa-shield-alt:before{content:\"\\f3ed\"}.fa-shield-virus:before{content:\"\\e06c\"}.fa-ship:before{content:\"\\f21a\"}.fa-shipping-fast:before{content:\"\\f48b\"}.fa-shirtsinbulk:before{content:\"\\f214\"}.fa-shoe-prints:before{content:\"\\f54b\"}.fa-shopify:before{content:\"\\e057\"}.fa-shopping-bag:before{content:\"\\f290\"}.fa-shopping-basket:before{content:\"\\f291\"}.fa-shopping-cart:before{content:\"\\f07a\"}.fa-shopware:before{content:\"\\f5b5\"}.fa-shower:before{content:\"\\f2cc\"}.fa-shuttle-van:before{content:\"\\f5b6\"}.fa-sign:before{content:\"\\f4d9\"}.fa-sign-in-alt:before{content:\"\\f2f6\"}.fa-sign-language:before{content:\"\\f2a7\"}.fa-sign-out-alt:before{content:\"\\f2f5\"}.fa-signal:before{content:\"\\f012\"}.fa-signature:before{content:\"\\f5b7\"}.fa-sim-card:before{content:\"\\f7c4\"}.fa-simplybuilt:before{content:\"\\f215\"}.fa-sink:before{content:\"\\e06d\"}.fa-sistrix:before{content:\"\\f3ee\"}.fa-sitemap:before{content:\"\\f0e8\"}.fa-sith:before{content:\"\\f512\"}.fa-skating:before{content:\"\\f7c5\"}.fa-sketch:before{content:\"\\f7c6\"}.fa-skiing:before{content:\"\\f7c9\"}.fa-skiing-nordic:before{content:\"\\f7ca\"}.fa-skull:before{content:\"\\f54c\"}.fa-skull-crossbones:before{content:\"\\f714\"}.fa-skyatlas:before{content:\"\\f216\"}.fa-skype:before{content:\"\\f17e\"}.fa-slack:before{content:\"\\f198\"}.fa-slack-hash:before{content:\"\\f3ef\"}.fa-slash:before{content:\"\\f715\"}.fa-sleigh:before{content:\"\\f7cc\"}.fa-sliders-h:before{content:\"\\f1de\"}.fa-slideshare:before{content:\"\\f1e7\"}.fa-smile:before{content:\"\\f118\"}.fa-smile-beam:before{content:\"\\f5b8\"}.fa-smile-wink:before{content:\"\\f4da\"}.fa-smog:before{content:\"\\f75f\"}.fa-smoking:before{content:\"\\f48d\"}.fa-smoking-ban:before{content:\"\\f54d\"}.fa-sms:before{content:\"\\f7cd\"}.fa-snapchat:before{content:\"\\f2ab\"}.fa-snapchat-ghost:before{content:\"\\f2ac\"}.fa-snapchat-square:before{content:\"\\f2ad\"}.fa-snowboarding:before{content:\"\\f7ce\"}.fa-snowflake:before{content:\"\\f2dc\"}.fa-snowman:before{content:\"\\f7d0\"}.fa-snowplow:before{content:\"\\f7d2\"}.fa-soap:before{content:\"\\e06e\"}.fa-socks:before{content:\"\\f696\"}.fa-solar-panel:before{content:\"\\f5ba\"}.fa-sort:before{content:\"\\f0dc\"}.fa-sort-alpha-down:before{content:\"\\f15d\"}.fa-sort-alpha-down-alt:before{content:\"\\f881\"}.fa-sort-alpha-up:before{content:\"\\f15e\"}.fa-sort-alpha-up-alt:before{content:\"\\f882\"}.fa-sort-amount-down:before{content:\"\\f160\"}.fa-sort-amount-down-alt:before{content:\"\\f884\"}.fa-sort-amount-up:before{content:\"\\f161\"}.fa-sort-amount-up-alt:before{content:\"\\f885\"}.fa-sort-down:before{content:\"\\f0dd\"}.fa-sort-numeric-down:before{content:\"\\f162\"}.fa-sort-numeric-down-alt:before{content:\"\\f886\"}.fa-sort-numeric-up:before{content:\"\\f163\"}.fa-sort-numeric-up-alt:before{content:\"\\f887\"}.fa-sort-up:before{content:\"\\f0de\"}.fa-soundcloud:before{content:\"\\f1be\"}.fa-sourcetree:before{content:\"\\f7d3\"}.fa-spa:before{content:\"\\f5bb\"}.fa-space-shuttle:before{content:\"\\f197\"}.fa-speakap:before{content:\"\\f3f3\"}.fa-speaker-deck:before{content:\"\\f83c\"}.fa-spell-check:before{content:\"\\f891\"}.fa-spider:before{content:\"\\f717\"}.fa-spinner:before{content:\"\\f110\"}.fa-splotch:before{content:\"\\f5bc\"}.fa-spotify:before{content:\"\\f1bc\"}.fa-spray-can:before{content:\"\\f5bd\"}.fa-square:before{content:\"\\f0c8\"}.fa-square-full:before{content:\"\\f45c\"}.fa-square-root-alt:before{content:\"\\f698\"}.fa-squarespace:before{content:\"\\f5be\"}.fa-stack-exchange:before{content:\"\\f18d\"}.fa-stack-overflow:before{content:\"\\f16c\"}.fa-stackpath:before{content:\"\\f842\"}.fa-stamp:before{content:\"\\f5bf\"}.fa-star:before{content:\"\\f005\"}.fa-star-and-crescent:before{content:\"\\f699\"}.fa-star-half:before{content:\"\\f089\"}.fa-star-half-alt:before{content:\"\\f5c0\"}.fa-star-of-david:before{content:\"\\f69a\"}.fa-star-of-life:before{content:\"\\f621\"}.fa-staylinked:before{content:\"\\f3f5\"}.fa-steam:before{content:\"\\f1b6\"}.fa-steam-square:before{content:\"\\f1b7\"}.fa-steam-symbol:before{content:\"\\f3f6\"}.fa-step-backward:before{content:\"\\f048\"}.fa-step-forward:before{content:\"\\f051\"}.fa-stethoscope:before{content:\"\\f0f1\"}.fa-sticker-mule:before{content:\"\\f3f7\"}.fa-sticky-note:before{content:\"\\f249\"}.fa-stop:before{content:\"\\f04d\"}.fa-stop-circle:before{content:\"\\f28d\"}.fa-stopwatch:before{content:\"\\f2f2\"}.fa-stopwatch-20:before{content:\"\\e06f\"}.fa-store:before{content:\"\\f54e\"}.fa-store-alt:before{content:\"\\f54f\"}.fa-store-alt-slash:before{content:\"\\e070\"}.fa-store-slash:before{content:\"\\e071\"}.fa-strava:before{content:\"\\f428\"}.fa-stream:before{content:\"\\f550\"}.fa-street-view:before{content:\"\\f21d\"}.fa-strikethrough:before{content:\"\\f0cc\"}.fa-stripe:before{content:\"\\f429\"}.fa-stripe-s:before{content:\"\\f42a\"}.fa-stroopwafel:before{content:\"\\f551\"}.fa-studiovinari:before{content:\"\\f3f8\"}.fa-stumbleupon:before{content:\"\\f1a4\"}.fa-stumbleupon-circle:before{content:\"\\f1a3\"}.fa-subscript:before{content:\"\\f12c\"}.fa-subway:before{content:\"\\f239\"}.fa-suitcase:before{content:\"\\f0f2\"}.fa-suitcase-rolling:before{content:\"\\f5c1\"}.fa-sun:before{content:\"\\f185\"}.fa-superpowers:before{content:\"\\f2dd\"}.fa-superscript:before{content:\"\\f12b\"}.fa-supple:before{content:\"\\f3f9\"}.fa-surprise:before{content:\"\\f5c2\"}.fa-suse:before{content:\"\\f7d6\"}.fa-swatchbook:before{content:\"\\f5c3\"}.fa-swift:before{content:\"\\f8e1\"}.fa-swimmer:before{content:\"\\f5c4\"}.fa-swimming-pool:before{content:\"\\f5c5\"}.fa-symfony:before{content:\"\\f83d\"}.fa-synagogue:before{content:\"\\f69b\"}.fa-sync:before{content:\"\\f021\"}.fa-sync-alt:before{content:\"\\f2f1\"}.fa-syringe:before{content:\"\\f48e\"}.fa-table:before{content:\"\\f0ce\"}.fa-table-tennis:before{content:\"\\f45d\"}.fa-tablet:before{content:\"\\f10a\"}.fa-tablet-alt:before{content:\"\\f3fa\"}.fa-tablets:before{content:\"\\f490\"}.fa-tachometer-alt:before{content:\"\\f3fd\"}.fa-tag:before{content:\"\\f02b\"}.fa-tags:before{content:\"\\f02c\"}.fa-tape:before{content:\"\\f4db\"}.fa-tasks:before{content:\"\\f0ae\"}.fa-taxi:before{content:\"\\f1ba\"}.fa-teamspeak:before{content:\"\\f4f9\"}.fa-teeth:before{content:\"\\f62e\"}.fa-teeth-open:before{content:\"\\f62f\"}.fa-telegram:before{content:\"\\f2c6\"}.fa-telegram-plane:before{content:\"\\f3fe\"}.fa-temperature-high:before{content:\"\\f769\"}.fa-temperature-low:before{content:\"\\f76b\"}.fa-tencent-weibo:before{content:\"\\f1d5\"}.fa-tenge:before{content:\"\\f7d7\"}.fa-terminal:before{content:\"\\f120\"}.fa-text-height:before{content:\"\\f034\"}.fa-text-width:before{content:\"\\f035\"}.fa-th:before{content:\"\\f00a\"}.fa-th-large:before{content:\"\\f009\"}.fa-th-list:before{content:\"\\f00b\"}.fa-the-red-yeti:before{content:\"\\f69d\"}.fa-theater-masks:before{content:\"\\f630\"}.fa-themeco:before{content:\"\\f5c6\"}.fa-themeisle:before{content:\"\\f2b2\"}.fa-thermometer:before{content:\"\\f491\"}.fa-thermometer-empty:before{content:\"\\f2cb\"}.fa-thermometer-full:before{content:\"\\f2c7\"}.fa-thermometer-half:before{content:\"\\f2c9\"}.fa-thermometer-quarter:before{content:\"\\f2ca\"}.fa-thermometer-three-quarters:before{content:\"\\f2c8\"}.fa-think-peaks:before{content:\"\\f731\"}.fa-thumbs-down:before{content:\"\\f165\"}.fa-thumbs-up:before{content:\"\\f164\"}.fa-thumbtack:before{content:\"\\f08d\"}.fa-ticket-alt:before{content:\"\\f3ff\"}.fa-tiktok:before{content:\"\\e07b\"}.fa-times:before{content:\"\\f00d\"}.fa-times-circle:before{content:\"\\f057\"}.fa-tint:before{content:\"\\f043\"}.fa-tint-slash:before{content:\"\\f5c7\"}.fa-tired:before{content:\"\\f5c8\"}.fa-toggle-off:before{content:\"\\f204\"}.fa-toggle-on:before{content:\"\\f205\"}.fa-toilet:before{content:\"\\f7d8\"}.fa-toilet-paper:before{content:\"\\f71e\"}.fa-toilet-paper-slash:before{content:\"\\e072\"}.fa-toolbox:before{content:\"\\f552\"}.fa-tools:before{content:\"\\f7d9\"}.fa-tooth:before{content:\"\\f5c9\"}.fa-torah:before{content:\"\\f6a0\"}.fa-torii-gate:before{content:\"\\f6a1\"}.fa-tractor:before{content:\"\\f722\"}.fa-trade-federation:before{content:\"\\f513\"}.fa-trademark:before{content:\"\\f25c\"}.fa-traffic-light:before{content:\"\\f637\"}.fa-trailer:before{content:\"\\e041\"}.fa-train:before{content:\"\\f238\"}.fa-tram:before{content:\"\\f7da\"}.fa-transgender:before{content:\"\\f224\"}.fa-transgender-alt:before{content:\"\\f225\"}.fa-trash:before{content:\"\\f1f8\"}.fa-trash-alt:before{content:\"\\f2ed\"}.fa-trash-restore:before{content:\"\\f829\"}.fa-trash-restore-alt:before{content:\"\\f82a\"}.fa-tree:before{content:\"\\f1bb\"}.fa-trello:before{content:\"\\f181\"}.fa-trophy:before{content:\"\\f091\"}.fa-truck:before{content:\"\\f0d1\"}.fa-truck-loading:before{content:\"\\f4de\"}.fa-truck-monster:before{content:\"\\f63b\"}.fa-truck-moving:before{content:\"\\f4df\"}.fa-truck-pickup:before{content:\"\\f63c\"}.fa-tshirt:before{content:\"\\f553\"}.fa-tty:before{content:\"\\f1e4\"}.fa-tumblr:before{content:\"\\f173\"}.fa-tumblr-square:before{content:\"\\f174\"}.fa-tv:before{content:\"\\f26c\"}.fa-twitch:before{content:\"\\f1e8\"}.fa-twitter:before{content:\"\\f099\"}.fa-twitter-square:before{content:\"\\f081\"}.fa-typo3:before{content:\"\\f42b\"}.fa-uber:before{content:\"\\f402\"}.fa-ubuntu:before{content:\"\\f7df\"}.fa-uikit:before{content:\"\\f403\"}.fa-umbraco:before{content:\"\\f8e8\"}.fa-umbrella:before{content:\"\\f0e9\"}.fa-umbrella-beach:before{content:\"\\f5ca\"}.fa-uncharted:before{content:\"\\e084\"}.fa-underline:before{content:\"\\f0cd\"}.fa-undo:before{content:\"\\f0e2\"}.fa-undo-alt:before{content:\"\\f2ea\"}.fa-uniregistry:before{content:\"\\f404\"}.fa-unity:before{content:\"\\e049\"}.fa-universal-access:before{content:\"\\f29a\"}.fa-university:before{content:\"\\f19c\"}.fa-unlink:before{content:\"\\f127\"}.fa-unlock:before{content:\"\\f09c\"}.fa-unlock-alt:before{content:\"\\f13e\"}.fa-unsplash:before{content:\"\\e07c\"}.fa-untappd:before{content:\"\\f405\"}.fa-upload:before{content:\"\\f093\"}.fa-ups:before{content:\"\\f7e0\"}.fa-usb:before{content:\"\\f287\"}.fa-user:before{content:\"\\f007\"}.fa-user-alt:before{content:\"\\f406\"}.fa-user-alt-slash:before{content:\"\\f4fa\"}.fa-user-astronaut:before{content:\"\\f4fb\"}.fa-user-check:before{content:\"\\f4fc\"}.fa-user-circle:before{content:\"\\f2bd\"}.fa-user-clock:before{content:\"\\f4fd\"}.fa-user-cog:before{content:\"\\f4fe\"}.fa-user-edit:before{content:\"\\f4ff\"}.fa-user-friends:before{content:\"\\f500\"}.fa-user-graduate:before{content:\"\\f501\"}.fa-user-injured:before{content:\"\\f728\"}.fa-user-lock:before{content:\"\\f502\"}.fa-user-md:before{content:\"\\f0f0\"}.fa-user-minus:before{content:\"\\f503\"}.fa-user-ninja:before{content:\"\\f504\"}.fa-user-nurse:before{content:\"\\f82f\"}.fa-user-plus:before{content:\"\\f234\"}.fa-user-secret:before{content:\"\\f21b\"}.fa-user-shield:before{content:\"\\f505\"}.fa-user-slash:before{content:\"\\f506\"}.fa-user-tag:before{content:\"\\f507\"}.fa-user-tie:before{content:\"\\f508\"}.fa-user-times:before{content:\"\\f235\"}.fa-users:before{content:\"\\f0c0\"}.fa-users-cog:before{content:\"\\f509\"}.fa-users-slash:before{content:\"\\e073\"}.fa-usps:before{content:\"\\f7e1\"}.fa-ussunnah:before{content:\"\\f407\"}.fa-utensil-spoon:before{content:\"\\f2e5\"}.fa-utensils:before{content:\"\\f2e7\"}.fa-vaadin:before{content:\"\\f408\"}.fa-vector-square:before{content:\"\\f5cb\"}.fa-venus:before{content:\"\\f221\"}.fa-venus-double:before{content:\"\\f226\"}.fa-venus-mars:before{content:\"\\f228\"}.fa-vest:before{content:\"\\e085\"}.fa-vest-patches:before{content:\"\\e086\"}.fa-viacoin:before{content:\"\\f237\"}.fa-viadeo:before{content:\"\\f2a9\"}.fa-viadeo-square:before{content:\"\\f2aa\"}.fa-vial:before{content:\"\\f492\"}.fa-vials:before{content:\"\\f493\"}.fa-viber:before{content:\"\\f409\"}.fa-video:before{content:\"\\f03d\"}.fa-video-slash:before{content:\"\\f4e2\"}.fa-vihara:before{content:\"\\f6a7\"}.fa-vimeo:before{content:\"\\f40a\"}.fa-vimeo-square:before{content:\"\\f194\"}.fa-vimeo-v:before{content:\"\\f27d\"}.fa-vine:before{content:\"\\f1ca\"}.fa-virus:before{content:\"\\e074\"}.fa-virus-slash:before{content:\"\\e075\"}.fa-viruses:before{content:\"\\e076\"}.fa-vk:before{content:\"\\f189\"}.fa-vnv:before{content:\"\\f40b\"}.fa-voicemail:before{content:\"\\f897\"}.fa-volleyball-ball:before{content:\"\\f45f\"}.fa-volume-down:before{content:\"\\f027\"}.fa-volume-mute:before{content:\"\\f6a9\"}.fa-volume-off:before{content:\"\\f026\"}.fa-volume-up:before{content:\"\\f028\"}.fa-vote-yea:before{content:\"\\f772\"}.fa-vr-cardboard:before{content:\"\\f729\"}.fa-vuejs:before{content:\"\\f41f\"}.fa-walking:before{content:\"\\f554\"}.fa-wallet:before{content:\"\\f555\"}.fa-warehouse:before{content:\"\\f494\"}.fa-watchman-monitoring:before{content:\"\\e087\"}.fa-water:before{content:\"\\f773\"}.fa-wave-square:before{content:\"\\f83e\"}.fa-waze:before{content:\"\\f83f\"}.fa-weebly:before{content:\"\\f5cc\"}.fa-weibo:before{content:\"\\f18a\"}.fa-weight:before{content:\"\\f496\"}.fa-weight-hanging:before{content:\"\\f5cd\"}.fa-weixin:before{content:\"\\f1d7\"}.fa-whatsapp:before{content:\"\\f232\"}.fa-whatsapp-square:before{content:\"\\f40c\"}.fa-wheelchair:before{content:\"\\f193\"}.fa-whmcs:before{content:\"\\f40d\"}.fa-wifi:before{content:\"\\f1eb\"}.fa-wikipedia-w:before{content:\"\\f266\"}.fa-wind:before{content:\"\\f72e\"}.fa-window-close:before{content:\"\\f410\"}.fa-window-maximize:before{content:\"\\f2d0\"}.fa-window-minimize:before{content:\"\\f2d1\"}.fa-window-restore:before{content:\"\\f2d2\"}.fa-windows:before{content:\"\\f17a\"}.fa-wine-bottle:before{content:\"\\f72f\"}.fa-wine-glass:before{content:\"\\f4e3\"}.fa-wine-glass-alt:before{content:\"\\f5ce\"}.fa-wix:before{content:\"\\f5cf\"}.fa-wizards-of-the-coast:before{content:\"\\f730\"}.fa-wodu:before{content:\"\\e088\"}.fa-wolf-pack-battalion:before{content:\"\\f514\"}.fa-won-sign:before{content:\"\\f159\"}.fa-wordpress:before{content:\"\\f19a\"}.fa-wordpress-simple:before{content:\"\\f411\"}.fa-wpbeginner:before{content:\"\\f297\"}.fa-wpexplorer:before{content:\"\\f2de\"}.fa-wpforms:before{content:\"\\f298\"}.fa-wpressr:before{content:\"\\f3e4\"}.fa-wrench:before{content:\"\\f0ad\"}.fa-x-ray:before{content:\"\\f497\"}.fa-xbox:before{content:\"\\f412\"}.fa-xing:before{content:\"\\f168\"}.fa-xing-square:before{content:\"\\f169\"}.fa-y-combinator:before{content:\"\\f23b\"}.fa-yahoo:before{content:\"\\f19e\"}.fa-yammer:before{content:\"\\f840\"}.fa-yandex:before{content:\"\\f413\"}.fa-yandex-international:before{content:\"\\f414\"}.fa-yarn:before{content:\"\\f7e3\"}.fa-yelp:before{content:\"\\f1e9\"}.fa-yen-sign:before{content:\"\\f157\"}.fa-yin-yang:before{content:\"\\f6ad\"}.fa-yoast:before{content:\"\\f2b1\"}.fa-youtube:before{content:\"\\f167\"}.fa-youtube-square:before{content:\"\\f431\"}.fa-zhihu:before{content:\"\\f63f\"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}/*!*\nbulma.io v0.8.0 | MIT License | github.com/jgthms/bulma*/@keyframes spinAround{from{transform:rotate(0)}to{transform:rotate(359deg)}}.tabs,.pagination-previous,.pagination-next,.pagination-link,.pagination-ellipsis,.breadcrumb,.button,.is-unselectable,.modal-close,.delete{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.navbar-link:not(.is-arrowless)::after{border:3px solid transparent;border-radius:2px;border-right:0;border-top:0;content:\" \";display:block;height:.625em;margin-top:-.4375em;pointer-events:none;position:absolute;top:50%;transform:rotate(-45deg);transform-origin:center;width:.625em}.tabs:not(:last-child),.pagination:not(:last-child),.message:not(:last-child),.list:not(:last-child),.level:not(:last-child),.breadcrumb:not(:last-child),.highlight:not(:last-child),.block:not(:last-child),.title:not(:last-child),.subtitle:not(:last-child),.table-container:not(:last-child),.table:not(:last-child),.progress:not(:last-child),.notification:not(:last-child),.content:not(:last-child),.box:not(:last-child){margin-bottom:1.5rem}.modal-close,.delete{-moz-appearance:none;-webkit-appearance:none;background-color:rgba(10,10,10,.2);border:none;border-radius:290486px;cursor:pointer;pointer-events:auto;display:inline-block;flex-grow:0;flex-shrink:0;font-size:0;height:20px;max-height:20px;max-width:20px;min-height:20px;min-width:20px;outline:none;position:relative;vertical-align:top;width:20px}.modal-close::before,.delete::before,.modal-close::after,.delete::after{background-color:#fff;content:\"\";display:block;left:50%;position:absolute;top:50%;transform:translateX(-50%)translateY(-50%)rotate(45deg);transform-origin:center center}.modal-close::before,.delete::before{height:2px;width:50%}.modal-close::after,.delete::after{height:50%;width:2px}.modal-close:hover,.delete:hover,.modal-close:focus,.delete:focus{background-color:rgba(10,10,10,.3)}.modal-close:active,.delete:active{background-color:rgba(10,10,10,.4)}.is-small.modal-close,.is-small.delete{height:16px;max-height:16px;max-width:16px;min-height:16px;min-width:16px;width:16px}.is-medium.modal-close,.is-medium.delete{height:24px;max-height:24px;max-width:24px;min-height:24px;min-width:24px;width:24px}.is-large.modal-close,.is-large.delete{height:32px;max-height:32px;max-width:32px;min-height:32px;min-width:32px;width:32px}.loader,.button.is-loading::after{animation:spinAround 500ms infinite linear;border:2px solid #dbdbdb;border-radius:290486px;border-right-color:transparent;border-top-color:transparent;content:\"\";display:block;height:1em;position:relative;width:1em}.hero-video,.modal-background,.modal,.image.is-square img,.image.is-square .has-ratio,.image.is-1by1 img,.image.is-1by1 .has-ratio,.image.is-5by4 img,.image.is-5by4 .has-ratio,.image.is-4by3 img,.image.is-4by3 .has-ratio,.image.is-3by2 img,.image.is-3by2 .has-ratio,.image.is-5by3 img,.image.is-5by3 .has-ratio,.image.is-16by9 img,.image.is-16by9 .has-ratio,.image.is-2by1 img,.image.is-2by1 .has-ratio,.image.is-3by1 img,.image.is-3by1 .has-ratio,.image.is-4by5 img,.image.is-4by5 .has-ratio,.image.is-3by4 img,.image.is-3by4 .has-ratio,.image.is-2by3 img,.image.is-2by3 .has-ratio,.image.is-3by5 img,.image.is-3by5 .has-ratio,.image.is-9by16 img,.image.is-9by16 .has-ratio,.image.is-1by2 img,.image.is-1by2 .has-ratio,.image.is-1by3 img,.image.is-1by3 .has-ratio,.is-overlay{bottom:0;left:0;position:absolute;right:0;top:0}.pagination-previous,.pagination-next,.pagination-link,.pagination-ellipsis,.button{-moz-appearance:none;-webkit-appearance:none;align-items:center;border:1px solid transparent;border-radius:4px;box-shadow:none;display:inline-flex;font-size:1rem;height:2.5em;justify-content:flex-start;line-height:1.5;padding-bottom:calc(.5em - 1px);padding-left:calc(.75em - 1px);padding-right:calc(.75em - 1px);padding-top:calc(.5em - 1px);position:relative;vertical-align:top}.pagination-previous:focus,.pagination-next:focus,.pagination-link:focus,.pagination-ellipsis:focus,.button:focus,.is-focused.pagination-previous,.is-focused.pagination-next,.is-focused.pagination-link,.is-focused.pagination-ellipsis,.is-focused.button,.pagination-previous:active,.pagination-next:active,.pagination-link:active,.pagination-ellipsis:active,.button:active,.is-active.pagination-previous,.is-active.pagination-next,.is-active.pagination-link,.is-active.pagination-ellipsis,.is-active.button{outline:none}[disabled].pagination-previous,[disabled].pagination-next,[disabled].pagination-link,[disabled].pagination-ellipsis,[disabled].button,fieldset[disabled] .pagination-previous,fieldset[disabled] .pagination-next,fieldset[disabled] .pagination-link,fieldset[disabled] .pagination-ellipsis,fieldset[disabled] .button{cursor:not-allowed}/*!minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css*/html,body,p,ol,ul,li,dl,dt,dd,blockquote,figure,fieldset,legend,textarea,pre,iframe,hr,h1,h2,h3,h4,h5,h6{margin:0;padding:0}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:400}ul{list-style:none}button,input,select,textarea{margin:0}html{box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}img,video{height:auto;max-width:100%}iframe{border:0}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}td:not([align]),th:not([align]){text-align:left}html{background-color:#fff;font-size:16px;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;min-width:300px;overflow-x:hidden;overflow-y:scroll;text-rendering:optimizeLegibility;text-size-adjust:100%}article,aside,figure,footer,header,hgroup,section{display:block}body,button,input,select,textarea{font-family:BlinkMacSystemFont,-apple-system,segoe ui,roboto,oxygen,ubuntu,cantarell,fira sans,droid sans,helvetica neue,helvetica,arial,sans-serif}code,pre{-moz-osx-font-smoothing:auto;-webkit-font-smoothing:auto;font-family:monospace}body{color:#4a4a4a;font-size:1em;font-weight:400;line-height:1.5}a{color:#3273dc;cursor:pointer;text-decoration:none}a strong{color:currentColor}a:hover{color:#363636}code{background-color:#f5f5f5;color:#f14668;font-size:.875em;font-weight:400;padding:.25em .5em}hr{background-color:#f5f5f5;border:none;display:block;height:2px;margin:1.5rem 0}img{height:auto;max-width:100%}input[type=checkbox],input[type=radio]{vertical-align:baseline}small{font-size:.875em}span{font-style:inherit;font-weight:inherit}strong{color:#363636;font-weight:700}fieldset{border:none}pre{-webkit-overflow-scrolling:touch;background-color:#f5f5f5;color:#4a4a4a;font-size:.875em;overflow-x:auto;padding:1.25rem 1.5rem;white-space:pre;word-wrap:normal}pre code{background-color:transparent;color:currentColor;font-size:1em;padding:0}table td,table th{vertical-align:top}table td:not([align]),table th:not([align]){text-align:left}table th{color:#363636}.is-clearfix::after{clear:both;content:\" \";display:table}.is-pulled-left{float:left!important}.is-pulled-right{float:right!important}.is-clipped{overflow:hidden!important}.is-size-1{font-size:3rem!important}.is-size-2{font-size:2.5rem!important}.is-size-3{font-size:2rem!important}.is-size-4{font-size:1.5rem!important}.is-size-5{font-size:1.25rem!important}.is-size-6{font-size:1rem!important}.is-size-7{font-size:.75rem!important}@media screen and (max-width:768px){.is-size-1-mobile{font-size:3rem!important}.is-size-2-mobile{font-size:2.5rem!important}.is-size-3-mobile{font-size:2rem!important}.is-size-4-mobile{font-size:1.5rem!important}.is-size-5-mobile{font-size:1.25rem!important}.is-size-6-mobile{font-size:1rem!important}.is-size-7-mobile{font-size:.75rem!important}}@media screen and (min-width:769px),print{.is-size-1-tablet{font-size:3rem!important}.is-size-2-tablet{font-size:2.5rem!important}.is-size-3-tablet{font-size:2rem!important}.is-size-4-tablet{font-size:1.5rem!important}.is-size-5-tablet{font-size:1.25rem!important}.is-size-6-tablet{font-size:1rem!important}.is-size-7-tablet{font-size:.75rem!important}}@media screen and (max-width:1023px){.is-size-1-touch{font-size:3rem!important}.is-size-2-touch{font-size:2.5rem!important}.is-size-3-touch{font-size:2rem!important}.is-size-4-touch{font-size:1.5rem!important}.is-size-5-touch{font-size:1.25rem!important}.is-size-6-touch{font-size:1rem!important}.is-size-7-touch{font-size:.75rem!important}}@media screen and (min-width:1024px){.is-size-1-desktop{font-size:3rem!important}.is-size-2-desktop{font-size:2.5rem!important}.is-size-3-desktop{font-size:2rem!important}.is-size-4-desktop{font-size:1.5rem!important}.is-size-5-desktop{font-size:1.25rem!important}.is-size-6-desktop{font-size:1rem!important}.is-size-7-desktop{font-size:.75rem!important}}@media screen and (min-width:1216px){.is-size-1-widescreen{font-size:3rem!important}.is-size-2-widescreen{font-size:2.5rem!important}.is-size-3-widescreen{font-size:2rem!important}.is-size-4-widescreen{font-size:1.5rem!important}.is-size-5-widescreen{font-size:1.25rem!important}.is-size-6-widescreen{font-size:1rem!important}.is-size-7-widescreen{font-size:.75rem!important}}@media screen and (min-width:1408px){.is-size-1-fullhd{font-size:3rem!important}.is-size-2-fullhd{font-size:2.5rem!important}.is-size-3-fullhd{font-size:2rem!important}.is-size-4-fullhd{font-size:1.5rem!important}.is-size-5-fullhd{font-size:1.25rem!important}.is-size-6-fullhd{font-size:1rem!important}.is-size-7-fullhd{font-size:.75rem!important}}.has-text-centered{text-align:center!important}.has-text-justified{text-align:justify!important}.has-text-left{text-align:left!important}.has-text-right{text-align:right!important}@media screen and (max-width:768px){.has-text-centered-mobile{text-align:center!important}}@media screen and (min-width:769px),print{.has-text-centered-tablet{text-align:center!important}}@media screen and (min-width:769px) and (max-width:1023px){.has-text-centered-tablet-only{text-align:center!important}}@media screen and (max-width:1023px){.has-text-centered-touch{text-align:center!important}}@media screen and (min-width:1024px){.has-text-centered-desktop{text-align:center!important}}@media screen and (min-width:1024px) and (max-width:1215px){.has-text-centered-desktop-only{text-align:center!important}}@media screen and (min-width:1216px){.has-text-centered-widescreen{text-align:center!important}}@media screen and (min-width:1216px) and (max-width:1407px){.has-text-centered-widescreen-only{text-align:center!important}}@media screen and (min-width:1408px){.has-text-centered-fullhd{text-align:center!important}}@media screen and (max-width:768px){.has-text-justified-mobile{text-align:justify!important}}@media screen and (min-width:769px),print{.has-text-justified-tablet{text-align:justify!important}}@media screen and (min-width:769px) and (max-width:1023px){.has-text-justified-tablet-only{text-align:justify!important}}@media screen and (max-width:1023px){.has-text-justified-touch{text-align:justify!important}}@media screen and (min-width:1024px){.has-text-justified-desktop{text-align:justify!important}}@media screen and (min-width:1024px) and (max-width:1215px){.has-text-justified-desktop-only{text-align:justify!important}}@media screen and (min-width:1216px){.has-text-justified-widescreen{text-align:justify!important}}@media screen and (min-width:1216px) and (max-width:1407px){.has-text-justified-widescreen-only{text-align:justify!important}}@media screen and (min-width:1408px){.has-text-justified-fullhd{text-align:justify!important}}@media screen and (max-width:768px){.has-text-left-mobile{text-align:left!important}}@media screen and (min-width:769px),print{.has-text-left-tablet{text-align:left!important}}@media screen and (min-width:769px) and (max-width:1023px){.has-text-left-tablet-only{text-align:left!important}}@media screen and (max-width:1023px){.has-text-left-touch{text-align:left!important}}@media screen and (min-width:1024px){.has-text-left-desktop{text-align:left!important}}@media screen and (min-width:1024px) and (max-width:1215px){.has-text-left-desktop-only{text-align:left!important}}@media screen and (min-width:1216px){.has-text-left-widescreen{text-align:left!important}}@media screen and (min-width:1216px) and (max-width:1407px){.has-text-left-widescreen-only{text-align:left!important}}@media screen and (min-width:1408px){.has-text-left-fullhd{text-align:left!important}}@media screen and (max-width:768px){.has-text-right-mobile{text-align:right!important}}@media screen and (min-width:769px),print{.has-text-right-tablet{text-align:right!important}}@media screen and (min-width:769px) and (max-width:1023px){.has-text-right-tablet-only{text-align:right!important}}@media screen and (max-width:1023px){.has-text-right-touch{text-align:right!important}}@media screen and (min-width:1024px){.has-text-right-desktop{text-align:right!important}}@media screen and (min-width:1024px) and (max-width:1215px){.has-text-right-desktop-only{text-align:right!important}}@media screen and (min-width:1216px){.has-text-right-widescreen{text-align:right!important}}@media screen and (min-width:1216px) and (max-width:1407px){.has-text-right-widescreen-only{text-align:right!important}}@media screen and (min-width:1408px){.has-text-right-fullhd{text-align:right!important}}.is-capitalized{text-transform:capitalize!important}.is-lowercase{text-transform:lowercase!important}.is-uppercase{text-transform:uppercase!important}.is-italic{font-style:italic!important}.has-text-white{color:#fff!important}a.has-text-white:hover,a.has-text-white:focus{color:#e6e6e6!important}.has-background-white{background-color:#fff!important}.has-text-black{color:#0a0a0a!important}a.has-text-black:hover,a.has-text-black:focus{color:#000!important}.has-background-black{background-color:#0a0a0a!important}.has-text-light{color:#f5f5f5!important}a.has-text-light:hover,a.has-text-light:focus{color:#dbdbdb!important}.has-background-light{background-color:#f5f5f5!important}.has-text-dark{color:#363636!important}a.has-text-dark:hover,a.has-text-dark:focus{color:#1c1c1c!important}.has-background-dark{background-color:#363636!important}.has-text-primary{color:#c93312!important}a.has-text-primary:hover,a.has-text-primary:focus{color:#9a270e!important}.has-background-primary{background-color:#c93312!important}.has-text-link{color:#3273dc!important}a.has-text-link:hover,a.has-text-link:focus{color:#205bbc!important}.has-background-link{background-color:#3273dc!important}.has-text-info{color:#3298dc!important}a.has-text-info:hover,a.has-text-info:focus{color:#207dbc!important}.has-background-info{background-color:#3298dc!important}.has-text-success{color:#48c774!important}a.has-text-success:hover,a.has-text-success:focus{color:#34a85c!important}.has-background-success{background-color:#48c774!important}.has-text-warning{color:#ffdd57!important}a.has-text-warning:hover,a.has-text-warning:focus{color:#ffd324!important}.has-background-warning{background-color:#ffdd57!important}.has-text-danger{color:#f14668!important}a.has-text-danger:hover,a.has-text-danger:focus{color:#ee1742!important}.has-background-danger{background-color:#f14668!important}.has-text-black-bis{color:#121212!important}.has-background-black-bis{background-color:#121212!important}.has-text-black-ter{color:#242424!important}.has-background-black-ter{background-color:#242424!important}.has-text-grey-darker{color:#363636!important}.has-background-grey-darker{background-color:#363636!important}.has-text-grey-dark{color:#4a4a4a!important}.has-background-grey-dark{background-color:#4a4a4a!important}.has-text-grey{color:#7a7a7a!important}.has-background-grey{background-color:#7a7a7a!important}.has-text-grey-light{color:#b5b5b5!important}.has-background-grey-light{background-color:#b5b5b5!important}.has-text-grey-lighter{color:#dbdbdb!important}.has-background-grey-lighter{background-color:#dbdbdb!important}.has-text-white-ter{color:#f5f5f5!important}.has-background-white-ter{background-color:#f5f5f5!important}.has-text-white-bis{color:#fafafa!important}.has-background-white-bis{background-color:#fafafa!important}.has-text-weight-light{font-weight:300!important}.has-text-weight-normal{font-weight:400!important}.has-text-weight-medium{font-weight:500!important}.has-text-weight-semibold{font-weight:600!important}.has-text-weight-bold{font-weight:700!important}.is-family-primary{font-family:BlinkMacSystemFont,-apple-system,segoe ui,roboto,oxygen,ubuntu,cantarell,fira sans,droid sans,helvetica neue,helvetica,arial,sans-serif!important}.is-family-secondary{font-family:BlinkMacSystemFont,-apple-system,segoe ui,roboto,oxygen,ubuntu,cantarell,fira sans,droid sans,helvetica neue,helvetica,arial,sans-serif!important}.is-family-sans-serif{font-family:BlinkMacSystemFont,-apple-system,segoe ui,roboto,oxygen,ubuntu,cantarell,fira sans,droid sans,helvetica neue,helvetica,arial,sans-serif!important}.is-family-monospace{font-family:monospace!important}.is-family-code{font-family:monospace!important}.is-block{display:block!important}@media screen and (max-width:768px){.is-block-mobile{display:block!important}}@media screen and (min-width:769px),print{.is-block-tablet{display:block!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-block-tablet-only{display:block!important}}@media screen and (max-width:1023px){.is-block-touch{display:block!important}}@media screen and (min-width:1024px){.is-block-desktop{display:block!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-block-desktop-only{display:block!important}}@media screen and (min-width:1216px){.is-block-widescreen{display:block!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-block-widescreen-only{display:block!important}}@media screen and (min-width:1408px){.is-block-fullhd{display:block!important}}.is-flex{display:flex!important}@media screen and (max-width:768px){.is-flex-mobile{display:flex!important}}@media screen and (min-width:769px),print{.is-flex-tablet{display:flex!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-flex-tablet-only{display:flex!important}}@media screen and (max-width:1023px){.is-flex-touch{display:flex!important}}@media screen and (min-width:1024px){.is-flex-desktop{display:flex!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-flex-desktop-only{display:flex!important}}@media screen and (min-width:1216px){.is-flex-widescreen{display:flex!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-flex-widescreen-only{display:flex!important}}@media screen and (min-width:1408px){.is-flex-fullhd{display:flex!important}}.is-inline{display:inline!important}@media screen and (max-width:768px){.is-inline-mobile{display:inline!important}}@media screen and (min-width:769px),print{.is-inline-tablet{display:inline!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-inline-tablet-only{display:inline!important}}@media screen and (max-width:1023px){.is-inline-touch{display:inline!important}}@media screen and (min-width:1024px){.is-inline-desktop{display:inline!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-inline-desktop-only{display:inline!important}}@media screen and (min-width:1216px){.is-inline-widescreen{display:inline!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-inline-widescreen-only{display:inline!important}}@media screen and (min-width:1408px){.is-inline-fullhd{display:inline!important}}.is-inline-block{display:inline-block!important}@media screen and (max-width:768px){.is-inline-block-mobile{display:inline-block!important}}@media screen and (min-width:769px),print{.is-inline-block-tablet{display:inline-block!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-inline-block-tablet-only{display:inline-block!important}}@media screen and (max-width:1023px){.is-inline-block-touch{display:inline-block!important}}@media screen and (min-width:1024px){.is-inline-block-desktop{display:inline-block!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-inline-block-desktop-only{display:inline-block!important}}@media screen and (min-width:1216px){.is-inline-block-widescreen{display:inline-block!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-inline-block-widescreen-only{display:inline-block!important}}@media screen and (min-width:1408px){.is-inline-block-fullhd{display:inline-block!important}}.is-inline-flex{display:inline-flex!important}@media screen and (max-width:768px){.is-inline-flex-mobile{display:inline-flex!important}}@media screen and (min-width:769px),print{.is-inline-flex-tablet{display:inline-flex!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-inline-flex-tablet-only{display:inline-flex!important}}@media screen and (max-width:1023px){.is-inline-flex-touch{display:inline-flex!important}}@media screen and (min-width:1024px){.is-inline-flex-desktop{display:inline-flex!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-inline-flex-desktop-only{display:inline-flex!important}}@media screen and (min-width:1216px){.is-inline-flex-widescreen{display:inline-flex!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-inline-flex-widescreen-only{display:inline-flex!important}}@media screen and (min-width:1408px){.is-inline-flex-fullhd{display:inline-flex!important}}.is-hidden{display:none!important}.is-sr-only{border:none!important;clip:rect(0,0,0,0)!important;height:.01em!important;overflow:hidden!important;padding:0!important;position:absolute!important;white-space:nowrap!important;width:.01em!important}@media screen and (max-width:768px){.is-hidden-mobile{display:none!important}}@media screen and (min-width:769px),print{.is-hidden-tablet{display:none!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-hidden-tablet-only{display:none!important}}@media screen and (max-width:1023px){.is-hidden-touch{display:none!important}}@media screen and (min-width:1024px){.is-hidden-desktop{display:none!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-hidden-desktop-only{display:none!important}}@media screen and (min-width:1216px){.is-hidden-widescreen{display:none!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-hidden-widescreen-only{display:none!important}}@media screen and (min-width:1408px){.is-hidden-fullhd{display:none!important}}.is-invisible{visibility:hidden!important}@media screen and (max-width:768px){.is-invisible-mobile{visibility:hidden!important}}@media screen and (min-width:769px),print{.is-invisible-tablet{visibility:hidden!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-invisible-tablet-only{visibility:hidden!important}}@media screen and (max-width:1023px){.is-invisible-touch{visibility:hidden!important}}@media screen and (min-width:1024px){.is-invisible-desktop{visibility:hidden!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-invisible-desktop-only{visibility:hidden!important}}@media screen and (min-width:1216px){.is-invisible-widescreen{visibility:hidden!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-invisible-widescreen-only{visibility:hidden!important}}@media screen and (min-width:1408px){.is-invisible-fullhd{visibility:hidden!important}}.is-marginless{margin:0!important}.is-paddingless{padding:0!important}.is-radiusless{border-radius:0!important}.is-shadowless{box-shadow:none!important}.is-relative{position:relative!important}.column{display:block;flex-basis:0;flex-grow:1;flex-shrink:1;padding:.75rem}.columns.is-mobile>.column.is-narrow{flex:none}.columns.is-mobile>.column.is-full{flex:none;width:100%}.columns.is-mobile>.column.is-three-quarters{flex:none;width:75%}.columns.is-mobile>.column.is-two-thirds{flex:none;width:66.6666%}.columns.is-mobile>.column.is-half{flex:none;width:50%}.columns.is-mobile>.column.is-one-third{flex:none;width:33.3333%}.columns.is-mobile>.column.is-one-quarter{flex:none;width:25%}.columns.is-mobile>.column.is-one-fifth{flex:none;width:20%}.columns.is-mobile>.column.is-two-fifths{flex:none;width:40%}.columns.is-mobile>.column.is-three-fifths{flex:none;width:60%}.columns.is-mobile>.column.is-four-fifths{flex:none;width:80%}.columns.is-mobile>.column.is-offset-three-quarters{margin-left:75%}.columns.is-mobile>.column.is-offset-two-thirds{margin-left:66.6666%}.columns.is-mobile>.column.is-offset-half{margin-left:50%}.columns.is-mobile>.column.is-offset-one-third{margin-left:33.3333%}.columns.is-mobile>.column.is-offset-one-quarter{margin-left:25%}.columns.is-mobile>.column.is-offset-one-fifth{margin-left:20%}.columns.is-mobile>.column.is-offset-two-fifths{margin-left:40%}.columns.is-mobile>.column.is-offset-three-fifths{margin-left:60%}.columns.is-mobile>.column.is-offset-four-fifths{margin-left:80%}.columns.is-mobile>.column.is-0{flex:none;width:0%}.columns.is-mobile>.column.is-offset-0{margin-left:0%}.columns.is-mobile>.column.is-1{flex:none;width:8.33333333%}.columns.is-mobile>.column.is-offset-1{margin-left:8.33333333%}.columns.is-mobile>.column.is-2{flex:none;width:16.66666667%}.columns.is-mobile>.column.is-offset-2{margin-left:16.66666667%}.columns.is-mobile>.column.is-3{flex:none;width:25%}.columns.is-mobile>.column.is-offset-3{margin-left:25%}.columns.is-mobile>.column.is-4{flex:none;width:33.33333333%}.columns.is-mobile>.column.is-offset-4{margin-left:33.33333333%}.columns.is-mobile>.column.is-5{flex:none;width:41.66666667%}.columns.is-mobile>.column.is-offset-5{margin-left:41.66666667%}.columns.is-mobile>.column.is-6{flex:none;width:50%}.columns.is-mobile>.column.is-offset-6{margin-left:50%}.columns.is-mobile>.column.is-7{flex:none;width:58.33333333%}.columns.is-mobile>.column.is-offset-7{margin-left:58.33333333%}.columns.is-mobile>.column.is-8{flex:none;width:66.66666667%}.columns.is-mobile>.column.is-offset-8{margin-left:66.66666667%}.columns.is-mobile>.column.is-9{flex:none;width:75%}.columns.is-mobile>.column.is-offset-9{margin-left:75%}.columns.is-mobile>.column.is-10{flex:none;width:83.33333333%}.columns.is-mobile>.column.is-offset-10{margin-left:83.33333333%}.columns.is-mobile>.column.is-11{flex:none;width:91.66666667%}.columns.is-mobile>.column.is-offset-11{margin-left:91.66666667%}.columns.is-mobile>.column.is-12{flex:none;width:100%}.columns.is-mobile>.column.is-offset-12{margin-left:100%}@media screen and (max-width:768px){.column.is-narrow-mobile{flex:none}.column.is-full-mobile{flex:none;width:100%}.column.is-three-quarters-mobile{flex:none;width:75%}.column.is-two-thirds-mobile{flex:none;width:66.6666%}.column.is-half-mobile{flex:none;width:50%}.column.is-one-third-mobile{flex:none;width:33.3333%}.column.is-one-quarter-mobile{flex:none;width:25%}.column.is-one-fifth-mobile{flex:none;width:20%}.column.is-two-fifths-mobile{flex:none;width:40%}.column.is-three-fifths-mobile{flex:none;width:60%}.column.is-four-fifths-mobile{flex:none;width:80%}.column.is-offset-three-quarters-mobile{margin-left:75%}.column.is-offset-two-thirds-mobile{margin-left:66.6666%}.column.is-offset-half-mobile{margin-left:50%}.column.is-offset-one-third-mobile{margin-left:33.3333%}.column.is-offset-one-quarter-mobile{margin-left:25%}.column.is-offset-one-fifth-mobile{margin-left:20%}.column.is-offset-two-fifths-mobile{margin-left:40%}.column.is-offset-three-fifths-mobile{margin-left:60%}.column.is-offset-four-fifths-mobile{margin-left:80%}.column.is-0-mobile{flex:none;width:0%}.column.is-offset-0-mobile{margin-left:0%}.column.is-1-mobile{flex:none;width:8.33333333%}.column.is-offset-1-mobile{margin-left:8.33333333%}.column.is-2-mobile{flex:none;width:16.66666667%}.column.is-offset-2-mobile{margin-left:16.66666667%}.column.is-3-mobile{flex:none;width:25%}.column.is-offset-3-mobile{margin-left:25%}.column.is-4-mobile{flex:none;width:33.33333333%}.column.is-offset-4-mobile{margin-left:33.33333333%}.column.is-5-mobile{flex:none;width:41.66666667%}.column.is-offset-5-mobile{margin-left:41.66666667%}.column.is-6-mobile{flex:none;width:50%}.column.is-offset-6-mobile{margin-left:50%}.column.is-7-mobile{flex:none;width:58.33333333%}.column.is-offset-7-mobile{margin-left:58.33333333%}.column.is-8-mobile{flex:none;width:66.66666667%}.column.is-offset-8-mobile{margin-left:66.66666667%}.column.is-9-mobile{flex:none;width:75%}.column.is-offset-9-mobile{margin-left:75%}.column.is-10-mobile{flex:none;width:83.33333333%}.column.is-offset-10-mobile{margin-left:83.33333333%}.column.is-11-mobile{flex:none;width:91.66666667%}.column.is-offset-11-mobile{margin-left:91.66666667%}.column.is-12-mobile{flex:none;width:100%}.column.is-offset-12-mobile{margin-left:100%}}@media screen and (min-width:769px),print{.column.is-narrow,.column.is-narrow-tablet{flex:none}.column.is-full,.column.is-full-tablet{flex:none;width:100%}.column.is-three-quarters,.column.is-three-quarters-tablet{flex:none;width:75%}.column.is-two-thirds,.column.is-two-thirds-tablet{flex:none;width:66.6666%}.column.is-half,.column.is-half-tablet{flex:none;width:50%}.column.is-one-third,.column.is-one-third-tablet{flex:none;width:33.3333%}.column.is-one-quarter,.column.is-one-quarter-tablet{flex:none;width:25%}.column.is-one-fifth,.column.is-one-fifth-tablet{flex:none;width:20%}.column.is-two-fifths,.column.is-two-fifths-tablet{flex:none;width:40%}.column.is-three-fifths,.column.is-three-fifths-tablet{flex:none;width:60%}.column.is-four-fifths,.column.is-four-fifths-tablet{flex:none;width:80%}.column.is-offset-three-quarters,.column.is-offset-three-quarters-tablet{margin-left:75%}.column.is-offset-two-thirds,.column.is-offset-two-thirds-tablet{margin-left:66.6666%}.column.is-offset-half,.column.is-offset-half-tablet{margin-left:50%}.column.is-offset-one-third,.column.is-offset-one-third-tablet{margin-left:33.3333%}.column.is-offset-one-quarter,.column.is-offset-one-quarter-tablet{margin-left:25%}.column.is-offset-one-fifth,.column.is-offset-one-fifth-tablet{margin-left:20%}.column.is-offset-two-fifths,.column.is-offset-two-fifths-tablet{margin-left:40%}.column.is-offset-three-fifths,.column.is-offset-three-fifths-tablet{margin-left:60%}.column.is-offset-four-fifths,.column.is-offset-four-fifths-tablet{margin-left:80%}.column.is-0,.column.is-0-tablet{flex:none;width:0%}.column.is-offset-0,.column.is-offset-0-tablet{margin-left:0%}.column.is-1,.column.is-1-tablet{flex:none;width:8.33333333%}.column.is-offset-1,.column.is-offset-1-tablet{margin-left:8.33333333%}.column.is-2,.column.is-2-tablet{flex:none;width:16.66666667%}.column.is-offset-2,.column.is-offset-2-tablet{margin-left:16.66666667%}.column.is-3,.column.is-3-tablet{flex:none;width:25%}.column.is-offset-3,.column.is-offset-3-tablet{margin-left:25%}.column.is-4,.column.is-4-tablet{flex:none;width:33.33333333%}.column.is-offset-4,.column.is-offset-4-tablet{margin-left:33.33333333%}.column.is-5,.column.is-5-tablet{flex:none;width:41.66666667%}.column.is-offset-5,.column.is-offset-5-tablet{margin-left:41.66666667%}.column.is-6,.column.is-6-tablet{flex:none;width:50%}.column.is-offset-6,.column.is-offset-6-tablet{margin-left:50%}.column.is-7,.column.is-7-tablet{flex:none;width:58.33333333%}.column.is-offset-7,.column.is-offset-7-tablet{margin-left:58.33333333%}.column.is-8,.column.is-8-tablet{flex:none;width:66.66666667%}.column.is-offset-8,.column.is-offset-8-tablet{margin-left:66.66666667%}.column.is-9,.column.is-9-tablet{flex:none;width:75%}.column.is-offset-9,.column.is-offset-9-tablet{margin-left:75%}.column.is-10,.column.is-10-tablet{flex:none;width:83.33333333%}.column.is-offset-10,.column.is-offset-10-tablet{margin-left:83.33333333%}.column.is-11,.column.is-11-tablet{flex:none;width:91.66666667%}.column.is-offset-11,.column.is-offset-11-tablet{margin-left:91.66666667%}.column.is-12,.column.is-12-tablet{flex:none;width:100%}.column.is-offset-12,.column.is-offset-12-tablet{margin-left:100%}}@media screen and (max-width:1023px){.column.is-narrow-touch{flex:none}.column.is-full-touch{flex:none;width:100%}.column.is-three-quarters-touch{flex:none;width:75%}.column.is-two-thirds-touch{flex:none;width:66.6666%}.column.is-half-touch{flex:none;width:50%}.column.is-one-third-touch{flex:none;width:33.3333%}.column.is-one-quarter-touch{flex:none;width:25%}.column.is-one-fifth-touch{flex:none;width:20%}.column.is-two-fifths-touch{flex:none;width:40%}.column.is-three-fifths-touch{flex:none;width:60%}.column.is-four-fifths-touch{flex:none;width:80%}.column.is-offset-three-quarters-touch{margin-left:75%}.column.is-offset-two-thirds-touch{margin-left:66.6666%}.column.is-offset-half-touch{margin-left:50%}.column.is-offset-one-third-touch{margin-left:33.3333%}.column.is-offset-one-quarter-touch{margin-left:25%}.column.is-offset-one-fifth-touch{margin-left:20%}.column.is-offset-two-fifths-touch{margin-left:40%}.column.is-offset-three-fifths-touch{margin-left:60%}.column.is-offset-four-fifths-touch{margin-left:80%}.column.is-0-touch{flex:none;width:0%}.column.is-offset-0-touch{margin-left:0%}.column.is-1-touch{flex:none;width:8.33333333%}.column.is-offset-1-touch{margin-left:8.33333333%}.column.is-2-touch{flex:none;width:16.66666667%}.column.is-offset-2-touch{margin-left:16.66666667%}.column.is-3-touch{flex:none;width:25%}.column.is-offset-3-touch{margin-left:25%}.column.is-4-touch{flex:none;width:33.33333333%}.column.is-offset-4-touch{margin-left:33.33333333%}.column.is-5-touch{flex:none;width:41.66666667%}.column.is-offset-5-touch{margin-left:41.66666667%}.column.is-6-touch{flex:none;width:50%}.column.is-offset-6-touch{margin-left:50%}.column.is-7-touch{flex:none;width:58.33333333%}.column.is-offset-7-touch{margin-left:58.33333333%}.column.is-8-touch{flex:none;width:66.66666667%}.column.is-offset-8-touch{margin-left:66.66666667%}.column.is-9-touch{flex:none;width:75%}.column.is-offset-9-touch{margin-left:75%}.column.is-10-touch{flex:none;width:83.33333333%}.column.is-offset-10-touch{margin-left:83.33333333%}.column.is-11-touch{flex:none;width:91.66666667%}.column.is-offset-11-touch{margin-left:91.66666667%}.column.is-12-touch{flex:none;width:100%}.column.is-offset-12-touch{margin-left:100%}}@media screen and (min-width:1024px){.column.is-narrow-desktop{flex:none}.column.is-full-desktop{flex:none;width:100%}.column.is-three-quarters-desktop{flex:none;width:75%}.column.is-two-thirds-desktop{flex:none;width:66.6666%}.column.is-half-desktop{flex:none;width:50%}.column.is-one-third-desktop{flex:none;width:33.3333%}.column.is-one-quarter-desktop{flex:none;width:25%}.column.is-one-fifth-desktop{flex:none;width:20%}.column.is-two-fifths-desktop{flex:none;width:40%}.column.is-three-fifths-desktop{flex:none;width:60%}.column.is-four-fifths-desktop{flex:none;width:80%}.column.is-offset-three-quarters-desktop{margin-left:75%}.column.is-offset-two-thirds-desktop{margin-left:66.6666%}.column.is-offset-half-desktop{margin-left:50%}.column.is-offset-one-third-desktop{margin-left:33.3333%}.column.is-offset-one-quarter-desktop{margin-left:25%}.column.is-offset-one-fifth-desktop{margin-left:20%}.column.is-offset-two-fifths-desktop{margin-left:40%}.column.is-offset-three-fifths-desktop{margin-left:60%}.column.is-offset-four-fifths-desktop{margin-left:80%}.column.is-0-desktop{flex:none;width:0%}.column.is-offset-0-desktop{margin-left:0%}.column.is-1-desktop{flex:none;width:8.33333333%}.column.is-offset-1-desktop{margin-left:8.33333333%}.column.is-2-desktop{flex:none;width:16.66666667%}.column.is-offset-2-desktop{margin-left:16.66666667%}.column.is-3-desktop{flex:none;width:25%}.column.is-offset-3-desktop{margin-left:25%}.column.is-4-desktop{flex:none;width:33.33333333%}.column.is-offset-4-desktop{margin-left:33.33333333%}.column.is-5-desktop{flex:none;width:41.66666667%}.column.is-offset-5-desktop{margin-left:41.66666667%}.column.is-6-desktop{flex:none;width:50%}.column.is-offset-6-desktop{margin-left:50%}.column.is-7-desktop{flex:none;width:58.33333333%}.column.is-offset-7-desktop{margin-left:58.33333333%}.column.is-8-desktop{flex:none;width:66.66666667%}.column.is-offset-8-desktop{margin-left:66.66666667%}.column.is-9-desktop{flex:none;width:75%}.column.is-offset-9-desktop{margin-left:75%}.column.is-10-desktop{flex:none;width:83.33333333%}.column.is-offset-10-desktop{margin-left:83.33333333%}.column.is-11-desktop{flex:none;width:91.66666667%}.column.is-offset-11-desktop{margin-left:91.66666667%}.column.is-12-desktop{flex:none;width:100%}.column.is-offset-12-desktop{margin-left:100%}}@media screen and (min-width:1216px){.column.is-narrow-widescreen{flex:none}.column.is-full-widescreen{flex:none;width:100%}.column.is-three-quarters-widescreen{flex:none;width:75%}.column.is-two-thirds-widescreen{flex:none;width:66.6666%}.column.is-half-widescreen{flex:none;width:50%}.column.is-one-third-widescreen{flex:none;width:33.3333%}.column.is-one-quarter-widescreen{flex:none;width:25%}.column.is-one-fifth-widescreen{flex:none;width:20%}.column.is-two-fifths-widescreen{flex:none;width:40%}.column.is-three-fifths-widescreen{flex:none;width:60%}.column.is-four-fifths-widescreen{flex:none;width:80%}.column.is-offset-three-quarters-widescreen{margin-left:75%}.column.is-offset-two-thirds-widescreen{margin-left:66.6666%}.column.is-offset-half-widescreen{margin-left:50%}.column.is-offset-one-third-widescreen{margin-left:33.3333%}.column.is-offset-one-quarter-widescreen{margin-left:25%}.column.is-offset-one-fifth-widescreen{margin-left:20%}.column.is-offset-two-fifths-widescreen{margin-left:40%}.column.is-offset-three-fifths-widescreen{margin-left:60%}.column.is-offset-four-fifths-widescreen{margin-left:80%}.column.is-0-widescreen{flex:none;width:0%}.column.is-offset-0-widescreen{margin-left:0%}.column.is-1-widescreen{flex:none;width:8.33333333%}.column.is-offset-1-widescreen{margin-left:8.33333333%}.column.is-2-widescreen{flex:none;width:16.66666667%}.column.is-offset-2-widescreen{margin-left:16.66666667%}.column.is-3-widescreen{flex:none;width:25%}.column.is-offset-3-widescreen{margin-left:25%}.column.is-4-widescreen{flex:none;width:33.33333333%}.column.is-offset-4-widescreen{margin-left:33.33333333%}.column.is-5-widescreen{flex:none;width:41.66666667%}.column.is-offset-5-widescreen{margin-left:41.66666667%}.column.is-6-widescreen{flex:none;width:50%}.column.is-offset-6-widescreen{margin-left:50%}.column.is-7-widescreen{flex:none;width:58.33333333%}.column.is-offset-7-widescreen{margin-left:58.33333333%}.column.is-8-widescreen{flex:none;width:66.66666667%}.column.is-offset-8-widescreen{margin-left:66.66666667%}.column.is-9-widescreen{flex:none;width:75%}.column.is-offset-9-widescreen{margin-left:75%}.column.is-10-widescreen{flex:none;width:83.33333333%}.column.is-offset-10-widescreen{margin-left:83.33333333%}.column.is-11-widescreen{flex:none;width:91.66666667%}.column.is-offset-11-widescreen{margin-left:91.66666667%}.column.is-12-widescreen{flex:none;width:100%}.column.is-offset-12-widescreen{margin-left:100%}}@media screen and (min-width:1408px){.column.is-narrow-fullhd{flex:none}.column.is-full-fullhd{flex:none;width:100%}.column.is-three-quarters-fullhd{flex:none;width:75%}.column.is-two-thirds-fullhd{flex:none;width:66.6666%}.column.is-half-fullhd{flex:none;width:50%}.column.is-one-third-fullhd{flex:none;width:33.3333%}.column.is-one-quarter-fullhd{flex:none;width:25%}.column.is-one-fifth-fullhd{flex:none;width:20%}.column.is-two-fifths-fullhd{flex:none;width:40%}.column.is-three-fifths-fullhd{flex:none;width:60%}.column.is-four-fifths-fullhd{flex:none;width:80%}.column.is-offset-three-quarters-fullhd{margin-left:75%}.column.is-offset-two-thirds-fullhd{margin-left:66.6666%}.column.is-offset-half-fullhd{margin-left:50%}.column.is-offset-one-third-fullhd{margin-left:33.3333%}.column.is-offset-one-quarter-fullhd{margin-left:25%}.column.is-offset-one-fifth-fullhd{margin-left:20%}.column.is-offset-two-fifths-fullhd{margin-left:40%}.column.is-offset-three-fifths-fullhd{margin-left:60%}.column.is-offset-four-fifths-fullhd{margin-left:80%}.column.is-0-fullhd{flex:none;width:0%}.column.is-offset-0-fullhd{margin-left:0%}.column.is-1-fullhd{flex:none;width:8.33333333%}.column.is-offset-1-fullhd{margin-left:8.33333333%}.column.is-2-fullhd{flex:none;width:16.66666667%}.column.is-offset-2-fullhd{margin-left:16.66666667%}.column.is-3-fullhd{flex:none;width:25%}.column.is-offset-3-fullhd{margin-left:25%}.column.is-4-fullhd{flex:none;width:33.33333333%}.column.is-offset-4-fullhd{margin-left:33.33333333%}.column.is-5-fullhd{flex:none;width:41.66666667%}.column.is-offset-5-fullhd{margin-left:41.66666667%}.column.is-6-fullhd{flex:none;width:50%}.column.is-offset-6-fullhd{margin-left:50%}.column.is-7-fullhd{flex:none;width:58.33333333%}.column.is-offset-7-fullhd{margin-left:58.33333333%}.column.is-8-fullhd{flex:none;width:66.66666667%}.column.is-offset-8-fullhd{margin-left:66.66666667%}.column.is-9-fullhd{flex:none;width:75%}.column.is-offset-9-fullhd{margin-left:75%}.column.is-10-fullhd{flex:none;width:83.33333333%}.column.is-offset-10-fullhd{margin-left:83.33333333%}.column.is-11-fullhd{flex:none;width:91.66666667%}.column.is-offset-11-fullhd{margin-left:91.66666667%}.column.is-12-fullhd{flex:none;width:100%}.column.is-offset-12-fullhd{margin-left:100%}}.columns{margin-left:-.75rem;margin-right:-.75rem;margin-top:-.75rem}.columns:last-child{margin-bottom:-.75rem}.columns:not(:last-child){margin-bottom:calc(1.5rem - .75rem)}.columns.is-centered{justify-content:center}.columns.is-gapless{margin-left:0;margin-right:0;margin-top:0}.columns.is-gapless>.column{margin:0;padding:0!important}.columns.is-gapless:not(:last-child){margin-bottom:1.5rem}.columns.is-gapless:last-child{margin-bottom:0}.columns.is-mobile{display:flex}.columns.is-multiline{flex-wrap:wrap}.columns.is-vcentered{align-items:center}@media screen and (min-width:769px),print{.columns:not(.is-desktop){display:flex}}@media screen and (min-width:1024px){.columns.is-desktop{display:flex}}.columns.is-variable{--columnGap:0.75rem;margin-left:calc(-1 * var(--columnGap));margin-right:calc(-1 * var(--columnGap))}.columns.is-variable .column{padding-left:var(--columnGap);padding-right:var(--columnGap)}.columns.is-variable.is-0{--columnGap:0rem}@media screen and (max-width:768px){.columns.is-variable.is-0-mobile{--columnGap:0rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-0-tablet{--columnGap:0rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-0-tablet-only{--columnGap:0rem}}@media screen and (max-width:1023px){.columns.is-variable.is-0-touch{--columnGap:0rem}}@media screen and (min-width:1024px){.columns.is-variable.is-0-desktop{--columnGap:0rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-0-desktop-only{--columnGap:0rem}}@media screen and (min-width:1216px){.columns.is-variable.is-0-widescreen{--columnGap:0rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-0-widescreen-only{--columnGap:0rem}}@media screen and (min-width:1408px){.columns.is-variable.is-0-fullhd{--columnGap:0rem}}.columns.is-variable.is-1{--columnGap:0.25rem}@media screen and (max-width:768px){.columns.is-variable.is-1-mobile{--columnGap:0.25rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-1-tablet{--columnGap:0.25rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-1-tablet-only{--columnGap:0.25rem}}@media screen and (max-width:1023px){.columns.is-variable.is-1-touch{--columnGap:0.25rem}}@media screen and (min-width:1024px){.columns.is-variable.is-1-desktop{--columnGap:0.25rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-1-desktop-only{--columnGap:0.25rem}}@media screen and (min-width:1216px){.columns.is-variable.is-1-widescreen{--columnGap:0.25rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-1-widescreen-only{--columnGap:0.25rem}}@media screen and (min-width:1408px){.columns.is-variable.is-1-fullhd{--columnGap:0.25rem}}.columns.is-variable.is-2{--columnGap:0.5rem}@media screen and (max-width:768px){.columns.is-variable.is-2-mobile{--columnGap:0.5rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-2-tablet{--columnGap:0.5rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-2-tablet-only{--columnGap:0.5rem}}@media screen and (max-width:1023px){.columns.is-variable.is-2-touch{--columnGap:0.5rem}}@media screen and (min-width:1024px){.columns.is-variable.is-2-desktop{--columnGap:0.5rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-2-desktop-only{--columnGap:0.5rem}}@media screen and (min-width:1216px){.columns.is-variable.is-2-widescreen{--columnGap:0.5rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-2-widescreen-only{--columnGap:0.5rem}}@media screen and (min-width:1408px){.columns.is-variable.is-2-fullhd{--columnGap:0.5rem}}.columns.is-variable.is-3{--columnGap:0.75rem}@media screen and (max-width:768px){.columns.is-variable.is-3-mobile{--columnGap:0.75rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-3-tablet{--columnGap:0.75rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-3-tablet-only{--columnGap:0.75rem}}@media screen and (max-width:1023px){.columns.is-variable.is-3-touch{--columnGap:0.75rem}}@media screen and (min-width:1024px){.columns.is-variable.is-3-desktop{--columnGap:0.75rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-3-desktop-only{--columnGap:0.75rem}}@media screen and (min-width:1216px){.columns.is-variable.is-3-widescreen{--columnGap:0.75rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-3-widescreen-only{--columnGap:0.75rem}}@media screen and (min-width:1408px){.columns.is-variable.is-3-fullhd{--columnGap:0.75rem}}.columns.is-variable.is-4{--columnGap:1rem}@media screen and (max-width:768px){.columns.is-variable.is-4-mobile{--columnGap:1rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-4-tablet{--columnGap:1rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-4-tablet-only{--columnGap:1rem}}@media screen and (max-width:1023px){.columns.is-variable.is-4-touch{--columnGap:1rem}}@media screen and (min-width:1024px){.columns.is-variable.is-4-desktop{--columnGap:1rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-4-desktop-only{--columnGap:1rem}}@media screen and (min-width:1216px){.columns.is-variable.is-4-widescreen{--columnGap:1rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-4-widescreen-only{--columnGap:1rem}}@media screen and (min-width:1408px){.columns.is-variable.is-4-fullhd{--columnGap:1rem}}.columns.is-variable.is-5{--columnGap:1.25rem}@media screen and (max-width:768px){.columns.is-variable.is-5-mobile{--columnGap:1.25rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-5-tablet{--columnGap:1.25rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-5-tablet-only{--columnGap:1.25rem}}@media screen and (max-width:1023px){.columns.is-variable.is-5-touch{--columnGap:1.25rem}}@media screen and (min-width:1024px){.columns.is-variable.is-5-desktop{--columnGap:1.25rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-5-desktop-only{--columnGap:1.25rem}}@media screen and (min-width:1216px){.columns.is-variable.is-5-widescreen{--columnGap:1.25rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-5-widescreen-only{--columnGap:1.25rem}}@media screen and (min-width:1408px){.columns.is-variable.is-5-fullhd{--columnGap:1.25rem}}.columns.is-variable.is-6{--columnGap:1.5rem}@media screen and (max-width:768px){.columns.is-variable.is-6-mobile{--columnGap:1.5rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-6-tablet{--columnGap:1.5rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-6-tablet-only{--columnGap:1.5rem}}@media screen and (max-width:1023px){.columns.is-variable.is-6-touch{--columnGap:1.5rem}}@media screen and (min-width:1024px){.columns.is-variable.is-6-desktop{--columnGap:1.5rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-6-desktop-only{--columnGap:1.5rem}}@media screen and (min-width:1216px){.columns.is-variable.is-6-widescreen{--columnGap:1.5rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-6-widescreen-only{--columnGap:1.5rem}}@media screen and (min-width:1408px){.columns.is-variable.is-6-fullhd{--columnGap:1.5rem}}.columns.is-variable.is-7{--columnGap:1.75rem}@media screen and (max-width:768px){.columns.is-variable.is-7-mobile{--columnGap:1.75rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-7-tablet{--columnGap:1.75rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-7-tablet-only{--columnGap:1.75rem}}@media screen and (max-width:1023px){.columns.is-variable.is-7-touch{--columnGap:1.75rem}}@media screen and (min-width:1024px){.columns.is-variable.is-7-desktop{--columnGap:1.75rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-7-desktop-only{--columnGap:1.75rem}}@media screen and (min-width:1216px){.columns.is-variable.is-7-widescreen{--columnGap:1.75rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-7-widescreen-only{--columnGap:1.75rem}}@media screen and (min-width:1408px){.columns.is-variable.is-7-fullhd{--columnGap:1.75rem}}.columns.is-variable.is-8{--columnGap:2rem}@media screen and (max-width:768px){.columns.is-variable.is-8-mobile{--columnGap:2rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-8-tablet{--columnGap:2rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-8-tablet-only{--columnGap:2rem}}@media screen and (max-width:1023px){.columns.is-variable.is-8-touch{--columnGap:2rem}}@media screen and (min-width:1024px){.columns.is-variable.is-8-desktop{--columnGap:2rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-8-desktop-only{--columnGap:2rem}}@media screen and (min-width:1216px){.columns.is-variable.is-8-widescreen{--columnGap:2rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-8-widescreen-only{--columnGap:2rem}}@media screen and (min-width:1408px){.columns.is-variable.is-8-fullhd{--columnGap:2rem}}.tile{align-items:stretch;display:block;flex-basis:0;flex-grow:1;flex-shrink:1;min-height:min-content}.tile.is-ancestor{margin-left:-.75rem;margin-right:-.75rem;margin-top:-.75rem}.tile.is-ancestor:last-child{margin-bottom:-.75rem}.tile.is-ancestor:not(:last-child){margin-bottom:.75rem}.tile.is-child{margin:0!important}.tile.is-parent{padding:.75rem}.tile.is-vertical{flex-direction:column}.tile.is-vertical>.tile.is-child:not(:last-child){margin-bottom:1.5rem!important}@media screen and (min-width:769px),print{.tile:not(.is-child){display:flex}.tile.is-1{flex:none;width:8.33333333%}.tile.is-2{flex:none;width:16.66666667%}.tile.is-3{flex:none;width:25%}.tile.is-4{flex:none;width:33.33333333%}.tile.is-5{flex:none;width:41.66666667%}.tile.is-6{flex:none;width:50%}.tile.is-7{flex:none;width:58.33333333%}.tile.is-8{flex:none;width:66.66666667%}.tile.is-9{flex:none;width:75%}.tile.is-10{flex:none;width:83.33333333%}.tile.is-11{flex:none;width:91.66666667%}.tile.is-12{flex:none;width:100%}}.box{background-color:#fff;border-radius:6px;box-shadow:0 .5em 1em -.125em rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,2%);color:#4a4a4a;display:block;padding:1.25rem}a.box:hover,a.box:focus{box-shadow:0 .5em 1em -.125em rgba(10,10,10,.1),0 0 0 1px #3273dc}a.box:active{box-shadow:inset 0 1px 2px rgba(10,10,10,.2),0 0 0 1px #3273dc}.button{background-color:#fff;border-color:#dbdbdb;border-width:1px;color:#363636;cursor:pointer;justify-content:center;padding-bottom:calc(.5em - 1px);padding-left:1em;padding-right:1em;padding-top:calc(.5em - 1px);text-align:center;white-space:nowrap}.button strong{color:inherit}.button .icon,.button .icon.is-small,.button .icon.is-medium,.button .icon.is-large{height:1.5em;width:1.5em}.button .icon:first-child:not(:last-child){margin-left:calc(-.5em - 1px);margin-right:.25em}.button .icon:last-child:not(:first-child){margin-left:.25em;margin-right:calc(-.5em - 1px)}.button .icon:first-child:last-child{margin-left:calc(-.5em - 1px);margin-right:calc(-.5em - 1px)}.button:hover,.button.is-hovered{border-color:#b5b5b5;color:#363636}.button:focus,.button.is-focused{border-color:#3273dc;color:#363636}.button:focus:not(:active),.button.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.button:active,.button.is-active{border-color:#4a4a4a;color:#363636}.button.is-text{background-color:transparent;border-color:transparent;color:#4a4a4a;text-decoration:underline}.button.is-text:hover,.button.is-text.is-hovered,.button.is-text:focus,.button.is-text.is-focused{background-color:#f5f5f5;color:#363636}.button.is-text:active,.button.is-text.is-active{background-color:#e8e8e8;color:#363636}.button.is-text[disabled],fieldset[disabled] .button.is-text{background-color:transparent;border-color:transparent;box-shadow:none}.button.is-white{background-color:#fff;border-color:transparent;color:#0a0a0a}.button.is-white:hover,.button.is-white.is-hovered{background-color:#f9f9f9;border-color:transparent;color:#0a0a0a}.button.is-white:focus,.button.is-white.is-focused{border-color:transparent;color:#0a0a0a}.button.is-white:focus:not(:active),.button.is-white.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(255,255,255,.25)}.button.is-white:active,.button.is-white.is-active{background-color:#f2f2f2;border-color:transparent;color:#0a0a0a}.button.is-white[disabled],fieldset[disabled] .button.is-white{background-color:#fff;border-color:transparent;box-shadow:none}.button.is-white.is-inverted{background-color:#0a0a0a;color:#fff}.button.is-white.is-inverted:hover,.button.is-white.is-inverted.is-hovered{background-color:#000}.button.is-white.is-inverted[disabled],fieldset[disabled] .button.is-white.is-inverted{background-color:#0a0a0a;border-color:transparent;box-shadow:none;color:#fff}.button.is-white.is-loading::after{border-color:transparent transparent #0a0a0a #0a0a0a!important}.button.is-white.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-white.is-outlined:hover,.button.is-white.is-outlined.is-hovered,.button.is-white.is-outlined:focus,.button.is-white.is-outlined.is-focused{background-color:#fff;border-color:#fff;color:#0a0a0a}.button.is-white.is-outlined.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-white.is-outlined.is-loading:hover::after,.button.is-white.is-outlined.is-loading.is-hovered::after,.button.is-white.is-outlined.is-loading:focus::after,.button.is-white.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #0a0a0a #0a0a0a!important}.button.is-white.is-outlined[disabled],fieldset[disabled] .button.is-white.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-white.is-inverted.is-outlined{background-color:transparent;border-color:#0a0a0a;color:#0a0a0a}.button.is-white.is-inverted.is-outlined:hover,.button.is-white.is-inverted.is-outlined.is-hovered,.button.is-white.is-inverted.is-outlined:focus,.button.is-white.is-inverted.is-outlined.is-focused{background-color:#0a0a0a;color:#fff}.button.is-white.is-inverted.is-outlined.is-loading:hover::after,.button.is-white.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-white.is-inverted.is-outlined.is-loading:focus::after,.button.is-white.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff!important}.button.is-white.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-white.is-inverted.is-outlined{background-color:transparent;border-color:#0a0a0a;box-shadow:none;color:#0a0a0a}.button.is-black{background-color:#0a0a0a;border-color:transparent;color:#fff}.button.is-black:hover,.button.is-black.is-hovered{background-color:#040404;border-color:transparent;color:#fff}.button.is-black:focus,.button.is-black.is-focused{border-color:transparent;color:#fff}.button.is-black:focus:not(:active),.button.is-black.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(10,10,10,.25)}.button.is-black:active,.button.is-black.is-active{background-color:#000;border-color:transparent;color:#fff}.button.is-black[disabled],fieldset[disabled] .button.is-black{background-color:#0a0a0a;border-color:transparent;box-shadow:none}.button.is-black.is-inverted{background-color:#fff;color:#0a0a0a}.button.is-black.is-inverted:hover,.button.is-black.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-black.is-inverted[disabled],fieldset[disabled] .button.is-black.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#0a0a0a}.button.is-black.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-black.is-outlined{background-color:transparent;border-color:#0a0a0a;color:#0a0a0a}.button.is-black.is-outlined:hover,.button.is-black.is-outlined.is-hovered,.button.is-black.is-outlined:focus,.button.is-black.is-outlined.is-focused{background-color:#0a0a0a;border-color:#0a0a0a;color:#fff}.button.is-black.is-outlined.is-loading::after{border-color:transparent transparent #0a0a0a #0a0a0a!important}.button.is-black.is-outlined.is-loading:hover::after,.button.is-black.is-outlined.is-loading.is-hovered::after,.button.is-black.is-outlined.is-loading:focus::after,.button.is-black.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff!important}.button.is-black.is-outlined[disabled],fieldset[disabled] .button.is-black.is-outlined{background-color:transparent;border-color:#0a0a0a;box-shadow:none;color:#0a0a0a}.button.is-black.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-black.is-inverted.is-outlined:hover,.button.is-black.is-inverted.is-outlined.is-hovered,.button.is-black.is-inverted.is-outlined:focus,.button.is-black.is-inverted.is-outlined.is-focused{background-color:#fff;color:#0a0a0a}.button.is-black.is-inverted.is-outlined.is-loading:hover::after,.button.is-black.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-black.is-inverted.is-outlined.is-loading:focus::after,.button.is-black.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #0a0a0a #0a0a0a!important}.button.is-black.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-black.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-light,#home>section.button:nth-child(even){background-color:#f5f5f5;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-light:hover,#home>section.button:hover:nth-child(even),.button.is-light.is-hovered,#home>section.button.is-hovered:nth-child(even){background-color:#eee;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-light:focus,#home>section.button:focus:nth-child(even),.button.is-light.is-focused,#home>section.button.is-focused:nth-child(even){border-color:transparent;color:rgba(0,0,0,.7)}.button.is-light:focus:not(:active),#home>section.button:focus:not(:active):nth-child(even),.button.is-light.is-focused:not(:active),#home>section.button.is-focused:not(:active):nth-child(even){box-shadow:0 0 0 .125em rgba(245,245,245,.25)}.button.is-light:active,#home>section.button:active:nth-child(even),.button.is-light.is-active,#home>section.button.is-active:nth-child(even){background-color:#e8e8e8;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-light[disabled],#home>section.button[disabled]:nth-child(even),fieldset[disabled] .button.is-light,fieldset[disabled] #home>section.button:nth-child(even){background-color:#f5f5f5;border-color:transparent;box-shadow:none}.button.is-light.is-inverted,#home>section.button.is-inverted:nth-child(even){background-color:rgba(0,0,0,.7);color:#f5f5f5}.button.is-light.is-inverted:hover,#home>section.button.is-inverted:hover:nth-child(even),.button.is-light.is-inverted.is-hovered,#home>section.button.is-inverted.is-hovered:nth-child(even){background-color:rgba(0,0,0,.7)}.button.is-light.is-inverted[disabled],#home>section.button.is-inverted[disabled]:nth-child(even),fieldset[disabled] .button.is-light.is-inverted,fieldset[disabled] #home>section.button.is-inverted:nth-child(even){background-color:rgba(0,0,0,.7);border-color:transparent;box-shadow:none;color:#f5f5f5}.button.is-light.is-loading::after,#home>section.button.is-loading:nth-child(even)::after{border-color:transparent transparent rgba(0,0,0,.7)rgba(0,0,0,.7)!important}.button.is-light.is-outlined,#home>section.button.is-outlined:nth-child(even){background-color:transparent;border-color:#f5f5f5;color:#f5f5f5}.button.is-light.is-outlined:hover,#home>section.button.is-outlined:hover:nth-child(even),.button.is-light.is-outlined.is-hovered,#home>section.button.is-outlined.is-hovered:nth-child(even),.button.is-light.is-outlined:focus,#home>section.button.is-outlined:focus:nth-child(even),.button.is-light.is-outlined.is-focused,#home>section.button.is-outlined.is-focused:nth-child(even){background-color:#f5f5f5;border-color:#f5f5f5;color:rgba(0,0,0,.7)}.button.is-light.is-outlined.is-loading::after,#home>section.button.is-outlined.is-loading:nth-child(even)::after{border-color:transparent transparent #f5f5f5 #f5f5f5!important}.button.is-light.is-outlined.is-loading:hover::after,#home>section.button.is-outlined.is-loading:hover:nth-child(even)::after,.button.is-light.is-outlined.is-loading.is-hovered::after,#home>section.button.is-outlined.is-loading.is-hovered:nth-child(even)::after,.button.is-light.is-outlined.is-loading:focus::after,#home>section.button.is-outlined.is-loading:focus:nth-child(even)::after,.button.is-light.is-outlined.is-loading.is-focused::after,#home>section.button.is-outlined.is-loading.is-focused:nth-child(even)::after{border-color:transparent transparent rgba(0,0,0,.7)rgba(0,0,0,.7)!important}.button.is-light.is-outlined[disabled],#home>section.button.is-outlined[disabled]:nth-child(even),fieldset[disabled] .button.is-light.is-outlined,fieldset[disabled] #home>section.button.is-outlined:nth-child(even){background-color:transparent;border-color:#f5f5f5;box-shadow:none;color:#f5f5f5}.button.is-light.is-inverted.is-outlined,#home>section.button.is-inverted.is-outlined:nth-child(even){background-color:transparent;border-color:rgba(0,0,0,.7);color:rgba(0,0,0,.7)}.button.is-light.is-inverted.is-outlined:hover,#home>section.button.is-inverted.is-outlined:hover:nth-child(even),.button.is-light.is-inverted.is-outlined.is-hovered,#home>section.button.is-inverted.is-outlined.is-hovered:nth-child(even),.button.is-light.is-inverted.is-outlined:focus,#home>section.button.is-inverted.is-outlined:focus:nth-child(even),.button.is-light.is-inverted.is-outlined.is-focused,#home>section.button.is-inverted.is-outlined.is-focused:nth-child(even){background-color:rgba(0,0,0,.7);color:#f5f5f5}.button.is-light.is-inverted.is-outlined.is-loading:hover::after,#home>section.button.is-inverted.is-outlined.is-loading:hover:nth-child(even)::after,.button.is-light.is-inverted.is-outlined.is-loading.is-hovered::after,#home>section.button.is-inverted.is-outlined.is-loading.is-hovered:nth-child(even)::after,.button.is-light.is-inverted.is-outlined.is-loading:focus::after,#home>section.button.is-inverted.is-outlined.is-loading:focus:nth-child(even)::after,.button.is-light.is-inverted.is-outlined.is-loading.is-focused::after,#home>section.button.is-inverted.is-outlined.is-loading.is-focused:nth-child(even)::after{border-color:transparent transparent #f5f5f5 #f5f5f5!important}.button.is-light.is-inverted.is-outlined[disabled],#home>section.button.is-inverted.is-outlined[disabled]:nth-child(even),fieldset[disabled] .button.is-light.is-inverted.is-outlined,fieldset[disabled] #home>section.button.is-inverted.is-outlined:nth-child(even){background-color:transparent;border-color:rgba(0,0,0,.7);box-shadow:none;color:rgba(0,0,0,.7)}.button.is-dark{background-color:#363636;border-color:transparent;color:#fff}.button.is-dark:hover,.button.is-dark.is-hovered{background-color:#2f2f2f;border-color:transparent;color:#fff}.button.is-dark:focus,.button.is-dark.is-focused{border-color:transparent;color:#fff}.button.is-dark:focus:not(:active),.button.is-dark.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(54,54,54,.25)}.button.is-dark:active,.button.is-dark.is-active{background-color:#292929;border-color:transparent;color:#fff}.button.is-dark[disabled],fieldset[disabled] .button.is-dark{background-color:#363636;border-color:transparent;box-shadow:none}.button.is-dark.is-inverted{background-color:#fff;color:#363636}.button.is-dark.is-inverted:hover,.button.is-dark.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-dark.is-inverted[disabled],fieldset[disabled] .button.is-dark.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#363636}.button.is-dark.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-dark.is-outlined{background-color:transparent;border-color:#363636;color:#363636}.button.is-dark.is-outlined:hover,.button.is-dark.is-outlined.is-hovered,.button.is-dark.is-outlined:focus,.button.is-dark.is-outlined.is-focused{background-color:#363636;border-color:#363636;color:#fff}.button.is-dark.is-outlined.is-loading::after{border-color:transparent transparent #363636 #363636!important}.button.is-dark.is-outlined.is-loading:hover::after,.button.is-dark.is-outlined.is-loading.is-hovered::after,.button.is-dark.is-outlined.is-loading:focus::after,.button.is-dark.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff!important}.button.is-dark.is-outlined[disabled],fieldset[disabled] .button.is-dark.is-outlined{background-color:transparent;border-color:#363636;box-shadow:none;color:#363636}.button.is-dark.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-dark.is-inverted.is-outlined:hover,.button.is-dark.is-inverted.is-outlined.is-hovered,.button.is-dark.is-inverted.is-outlined:focus,.button.is-dark.is-inverted.is-outlined.is-focused{background-color:#fff;color:#363636}.button.is-dark.is-inverted.is-outlined.is-loading:hover::after,.button.is-dark.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-dark.is-inverted.is-outlined.is-loading:focus::after,.button.is-dark.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #363636 #363636!important}.button.is-dark.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-dark.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-primary{background-color:#c93312;border-color:transparent;color:#fff}.button.is-primary:hover,.button.is-primary.is-hovered{background-color:#bd3011;border-color:transparent;color:#fff}.button.is-primary:focus,.button.is-primary.is-focused{border-color:transparent;color:#fff}.button.is-primary:focus:not(:active),.button.is-primary.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(201,51,18,.25)}.button.is-primary:active,.button.is-primary.is-active{background-color:#b22d10;border-color:transparent;color:#fff}.button.is-primary[disabled],fieldset[disabled] .button.is-primary{background-color:#c93312;border-color:transparent;box-shadow:none}.button.is-primary.is-inverted{background-color:#fff;color:#c93312}.button.is-primary.is-inverted:hover,.button.is-primary.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-primary.is-inverted[disabled],fieldset[disabled] .button.is-primary.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#c93312}.button.is-primary.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-primary.is-outlined{background-color:transparent;border-color:#c93312;color:#c93312}.button.is-primary.is-outlined:hover,.button.is-primary.is-outlined.is-hovered,.button.is-primary.is-outlined:focus,.button.is-primary.is-outlined.is-focused{background-color:#c93312;border-color:#c93312;color:#fff}.button.is-primary.is-outlined.is-loading::after{border-color:transparent transparent #c93312 #c93312!important}.button.is-primary.is-outlined.is-loading:hover::after,.button.is-primary.is-outlined.is-loading.is-hovered::after,.button.is-primary.is-outlined.is-loading:focus::after,.button.is-primary.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff!important}.button.is-primary.is-outlined[disabled],fieldset[disabled] .button.is-primary.is-outlined{background-color:transparent;border-color:#c93312;box-shadow:none;color:#c93312}.button.is-primary.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-primary.is-inverted.is-outlined:hover,.button.is-primary.is-inverted.is-outlined.is-hovered,.button.is-primary.is-inverted.is-outlined:focus,.button.is-primary.is-inverted.is-outlined.is-focused{background-color:#fff;color:#c93312}.button.is-primary.is-inverted.is-outlined.is-loading:hover::after,.button.is-primary.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-primary.is-inverted.is-outlined.is-loading:focus::after,.button.is-primary.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #c93312 #c93312!important}.button.is-primary.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-primary.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-primary.is-light,#home>section.is-primary:nth-child(even){background-color:#fdefec;color:#e13914}.button.is-primary.is-light:hover,#home>section.is-primary:hover:nth-child(even),.button.is-primary.is-light.is-hovered,#home>section.is-primary.is-hovered:nth-child(even){background-color:#fce6e1;border-color:transparent;color:#e13914}.button.is-primary.is-light:active,#home>section.is-primary:active:nth-child(even),.button.is-primary.is-light.is-active,#home>section.is-primary.is-active:nth-child(even){background-color:#fbdcd5;border-color:transparent;color:#e13914}.button.is-link{background-color:#3273dc;border-color:transparent;color:#fff}.button.is-link:hover,.button.is-link.is-hovered{background-color:#276cda;border-color:transparent;color:#fff}.button.is-link:focus,.button.is-link.is-focused{border-color:transparent;color:#fff}.button.is-link:focus:not(:active),.button.is-link.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.button.is-link:active,.button.is-link.is-active{background-color:#2366d1;border-color:transparent;color:#fff}.button.is-link[disabled],fieldset[disabled] .button.is-link{background-color:#3273dc;border-color:transparent;box-shadow:none}.button.is-link.is-inverted{background-color:#fff;color:#3273dc}.button.is-link.is-inverted:hover,.button.is-link.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-link.is-inverted[disabled],fieldset[disabled] .button.is-link.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#3273dc}.button.is-link.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-link.is-outlined{background-color:transparent;border-color:#3273dc;color:#3273dc}.button.is-link.is-outlined:hover,.button.is-link.is-outlined.is-hovered,.button.is-link.is-outlined:focus,.button.is-link.is-outlined.is-focused{background-color:#3273dc;border-color:#3273dc;color:#fff}.button.is-link.is-outlined.is-loading::after{border-color:transparent transparent #3273dc #3273dc!important}.button.is-link.is-outlined.is-loading:hover::after,.button.is-link.is-outlined.is-loading.is-hovered::after,.button.is-link.is-outlined.is-loading:focus::after,.button.is-link.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff!important}.button.is-link.is-outlined[disabled],fieldset[disabled] .button.is-link.is-outlined{background-color:transparent;border-color:#3273dc;box-shadow:none;color:#3273dc}.button.is-link.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-link.is-inverted.is-outlined:hover,.button.is-link.is-inverted.is-outlined.is-hovered,.button.is-link.is-inverted.is-outlined:focus,.button.is-link.is-inverted.is-outlined.is-focused{background-color:#fff;color:#3273dc}.button.is-link.is-inverted.is-outlined.is-loading:hover::after,.button.is-link.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-link.is-inverted.is-outlined.is-loading:focus::after,.button.is-link.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #3273dc #3273dc!important}.button.is-link.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-link.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-link.is-light,#home>section.is-link:nth-child(even){background-color:#eef3fc;color:#2160c4}.button.is-link.is-light:hover,#home>section.is-link:hover:nth-child(even),.button.is-link.is-light.is-hovered,#home>section.is-link.is-hovered:nth-child(even){background-color:#e3ecfa;border-color:transparent;color:#2160c4}.button.is-link.is-light:active,#home>section.is-link:active:nth-child(even),.button.is-link.is-light.is-active,#home>section.is-link.is-active:nth-child(even){background-color:#d8e4f8;border-color:transparent;color:#2160c4}.button.is-info{background-color:#3298dc;border-color:transparent;color:#fff}.button.is-info:hover,.button.is-info.is-hovered{background-color:#2793da;border-color:transparent;color:#fff}.button.is-info:focus,.button.is-info.is-focused{border-color:transparent;color:#fff}.button.is-info:focus:not(:active),.button.is-info.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(50,152,220,.25)}.button.is-info:active,.button.is-info.is-active{background-color:#238cd1;border-color:transparent;color:#fff}.button.is-info[disabled],fieldset[disabled] .button.is-info{background-color:#3298dc;border-color:transparent;box-shadow:none}.button.is-info.is-inverted{background-color:#fff;color:#3298dc}.button.is-info.is-inverted:hover,.button.is-info.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-info.is-inverted[disabled],fieldset[disabled] .button.is-info.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#3298dc}.button.is-info.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-info.is-outlined{background-color:transparent;border-color:#3298dc;color:#3298dc}.button.is-info.is-outlined:hover,.button.is-info.is-outlined.is-hovered,.button.is-info.is-outlined:focus,.button.is-info.is-outlined.is-focused{background-color:#3298dc;border-color:#3298dc;color:#fff}.button.is-info.is-outlined.is-loading::after{border-color:transparent transparent #3298dc #3298dc!important}.button.is-info.is-outlined.is-loading:hover::after,.button.is-info.is-outlined.is-loading.is-hovered::after,.button.is-info.is-outlined.is-loading:focus::after,.button.is-info.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff!important}.button.is-info.is-outlined[disabled],fieldset[disabled] .button.is-info.is-outlined{background-color:transparent;border-color:#3298dc;box-shadow:none;color:#3298dc}.button.is-info.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-info.is-inverted.is-outlined:hover,.button.is-info.is-inverted.is-outlined.is-hovered,.button.is-info.is-inverted.is-outlined:focus,.button.is-info.is-inverted.is-outlined.is-focused{background-color:#fff;color:#3298dc}.button.is-info.is-inverted.is-outlined.is-loading:hover::after,.button.is-info.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-info.is-inverted.is-outlined.is-loading:focus::after,.button.is-info.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #3298dc #3298dc!important}.button.is-info.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-info.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-info.is-light,#home>section.is-info:nth-child(even){background-color:#eef6fc;color:#1d72aa}.button.is-info.is-light:hover,#home>section.is-info:hover:nth-child(even),.button.is-info.is-light.is-hovered,#home>section.is-info.is-hovered:nth-child(even){background-color:#e3f1fa;border-color:transparent;color:#1d72aa}.button.is-info.is-light:active,#home>section.is-info:active:nth-child(even),.button.is-info.is-light.is-active,#home>section.is-info.is-active:nth-child(even){background-color:#d8ebf8;border-color:transparent;color:#1d72aa}.button.is-success{background-color:#48c774;border-color:transparent;color:#fff}.button.is-success:hover,.button.is-success.is-hovered{background-color:#3ec46d;border-color:transparent;color:#fff}.button.is-success:focus,.button.is-success.is-focused{border-color:transparent;color:#fff}.button.is-success:focus:not(:active),.button.is-success.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(72,199,116,.25)}.button.is-success:active,.button.is-success.is-active{background-color:#3abb67;border-color:transparent;color:#fff}.button.is-success[disabled],fieldset[disabled] .button.is-success{background-color:#48c774;border-color:transparent;box-shadow:none}.button.is-success.is-inverted{background-color:#fff;color:#48c774}.button.is-success.is-inverted:hover,.button.is-success.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-success.is-inverted[disabled],fieldset[disabled] .button.is-success.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#48c774}.button.is-success.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-success.is-outlined{background-color:transparent;border-color:#48c774;color:#48c774}.button.is-success.is-outlined:hover,.button.is-success.is-outlined.is-hovered,.button.is-success.is-outlined:focus,.button.is-success.is-outlined.is-focused{background-color:#48c774;border-color:#48c774;color:#fff}.button.is-success.is-outlined.is-loading::after{border-color:transparent transparent #48c774 #48c774!important}.button.is-success.is-outlined.is-loading:hover::after,.button.is-success.is-outlined.is-loading.is-hovered::after,.button.is-success.is-outlined.is-loading:focus::after,.button.is-success.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff!important}.button.is-success.is-outlined[disabled],fieldset[disabled] .button.is-success.is-outlined{background-color:transparent;border-color:#48c774;box-shadow:none;color:#48c774}.button.is-success.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-success.is-inverted.is-outlined:hover,.button.is-success.is-inverted.is-outlined.is-hovered,.button.is-success.is-inverted.is-outlined:focus,.button.is-success.is-inverted.is-outlined.is-focused{background-color:#fff;color:#48c774}.button.is-success.is-inverted.is-outlined.is-loading:hover::after,.button.is-success.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-success.is-inverted.is-outlined.is-loading:focus::after,.button.is-success.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #48c774 #48c774!important}.button.is-success.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-success.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-success.is-light,#home>section.is-success:nth-child(even){background-color:#effaf3;color:#257942}.button.is-success.is-light:hover,#home>section.is-success:hover:nth-child(even),.button.is-success.is-light.is-hovered,#home>section.is-success.is-hovered:nth-child(even){background-color:#e6f7ec;border-color:transparent;color:#257942}.button.is-success.is-light:active,#home>section.is-success:active:nth-child(even),.button.is-success.is-light.is-active,#home>section.is-success.is-active:nth-child(even){background-color:#dcf4e4;border-color:transparent;color:#257942}.button.is-warning{background-color:#ffdd57;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning:hover,.button.is-warning.is-hovered{background-color:#ffdb4a;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning:focus,.button.is-warning.is-focused{border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning:focus:not(:active),.button.is-warning.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(255,221,87,.25)}.button.is-warning:active,.button.is-warning.is-active{background-color:#ffd83d;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning[disabled],fieldset[disabled] .button.is-warning{background-color:#ffdd57;border-color:transparent;box-shadow:none}.button.is-warning.is-inverted{background-color:rgba(0,0,0,.7);color:#ffdd57}.button.is-warning.is-inverted:hover,.button.is-warning.is-inverted.is-hovered{background-color:rgba(0,0,0,.7)}.button.is-warning.is-inverted[disabled],fieldset[disabled] .button.is-warning.is-inverted{background-color:rgba(0,0,0,.7);border-color:transparent;box-shadow:none;color:#ffdd57}.button.is-warning.is-loading::after{border-color:transparent transparent rgba(0,0,0,.7)rgba(0,0,0,.7)!important}.button.is-warning.is-outlined{background-color:transparent;border-color:#ffdd57;color:#ffdd57}.button.is-warning.is-outlined:hover,.button.is-warning.is-outlined.is-hovered,.button.is-warning.is-outlined:focus,.button.is-warning.is-outlined.is-focused{background-color:#ffdd57;border-color:#ffdd57;color:rgba(0,0,0,.7)}.button.is-warning.is-outlined.is-loading::after{border-color:transparent transparent #ffdd57 #ffdd57!important}.button.is-warning.is-outlined.is-loading:hover::after,.button.is-warning.is-outlined.is-loading.is-hovered::after,.button.is-warning.is-outlined.is-loading:focus::after,.button.is-warning.is-outlined.is-loading.is-focused::after{border-color:transparent transparent rgba(0,0,0,.7)rgba(0,0,0,.7)!important}.button.is-warning.is-outlined[disabled],fieldset[disabled] .button.is-warning.is-outlined{background-color:transparent;border-color:#ffdd57;box-shadow:none;color:#ffdd57}.button.is-warning.is-inverted.is-outlined{background-color:transparent;border-color:rgba(0,0,0,.7);color:rgba(0,0,0,.7)}.button.is-warning.is-inverted.is-outlined:hover,.button.is-warning.is-inverted.is-outlined.is-hovered,.button.is-warning.is-inverted.is-outlined:focus,.button.is-warning.is-inverted.is-outlined.is-focused{background-color:rgba(0,0,0,.7);color:#ffdd57}.button.is-warning.is-inverted.is-outlined.is-loading:hover::after,.button.is-warning.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-warning.is-inverted.is-outlined.is-loading:focus::after,.button.is-warning.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #ffdd57 #ffdd57!important}.button.is-warning.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-warning.is-inverted.is-outlined{background-color:transparent;border-color:rgba(0,0,0,.7);box-shadow:none;color:rgba(0,0,0,.7)}.button.is-warning.is-light,#home>section.is-warning:nth-child(even){background-color:#fffbeb;color:#947600}.button.is-warning.is-light:hover,#home>section.is-warning:hover:nth-child(even),.button.is-warning.is-light.is-hovered,#home>section.is-warning.is-hovered:nth-child(even){background-color:#fff8de;border-color:transparent;color:#947600}.button.is-warning.is-light:active,#home>section.is-warning:active:nth-child(even),.button.is-warning.is-light.is-active,#home>section.is-warning.is-active:nth-child(even){background-color:#fff6d1;border-color:transparent;color:#947600}.button.is-danger{background-color:#f14668;border-color:transparent;color:#fff}.button.is-danger:hover,.button.is-danger.is-hovered{background-color:#f03a5f;border-color:transparent;color:#fff}.button.is-danger:focus,.button.is-danger.is-focused{border-color:transparent;color:#fff}.button.is-danger:focus:not(:active),.button.is-danger.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(241,70,104,.25)}.button.is-danger:active,.button.is-danger.is-active{background-color:#ef2e55;border-color:transparent;color:#fff}.button.is-danger[disabled],fieldset[disabled] .button.is-danger{background-color:#f14668;border-color:transparent;box-shadow:none}.button.is-danger.is-inverted{background-color:#fff;color:#f14668}.button.is-danger.is-inverted:hover,.button.is-danger.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-danger.is-inverted[disabled],fieldset[disabled] .button.is-danger.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#f14668}.button.is-danger.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-danger.is-outlined{background-color:transparent;border-color:#f14668;color:#f14668}.button.is-danger.is-outlined:hover,.button.is-danger.is-outlined.is-hovered,.button.is-danger.is-outlined:focus,.button.is-danger.is-outlined.is-focused{background-color:#f14668;border-color:#f14668;color:#fff}.button.is-danger.is-outlined.is-loading::after{border-color:transparent transparent #f14668 #f14668!important}.button.is-danger.is-outlined.is-loading:hover::after,.button.is-danger.is-outlined.is-loading.is-hovered::after,.button.is-danger.is-outlined.is-loading:focus::after,.button.is-danger.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff!important}.button.is-danger.is-outlined[disabled],fieldset[disabled] .button.is-danger.is-outlined{background-color:transparent;border-color:#f14668;box-shadow:none;color:#f14668}.button.is-danger.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-danger.is-inverted.is-outlined:hover,.button.is-danger.is-inverted.is-outlined.is-hovered,.button.is-danger.is-inverted.is-outlined:focus,.button.is-danger.is-inverted.is-outlined.is-focused{background-color:#fff;color:#f14668}.button.is-danger.is-inverted.is-outlined.is-loading:hover::after,.button.is-danger.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-danger.is-inverted.is-outlined.is-loading:focus::after,.button.is-danger.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #f14668 #f14668!important}.button.is-danger.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-danger.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-danger.is-light,#home>section.is-danger:nth-child(even){background-color:#feecf0;color:#cc0f35}.button.is-danger.is-light:hover,#home>section.is-danger:hover:nth-child(even),.button.is-danger.is-light.is-hovered,#home>section.is-danger.is-hovered:nth-child(even){background-color:#fde0e6;border-color:transparent;color:#cc0f35}.button.is-danger.is-light:active,#home>section.is-danger:active:nth-child(even),.button.is-danger.is-light.is-active,#home>section.is-danger.is-active:nth-child(even){background-color:#fcd4dc;border-color:transparent;color:#cc0f35}.button.is-small{border-radius:2px;font-size:.75rem}.button.is-normal{font-size:1rem}.button.is-medium{font-size:1.25rem}.button.is-large{font-size:1.5rem}.button[disabled],fieldset[disabled] .button{background-color:#fff;border-color:#dbdbdb;box-shadow:none;opacity:.5}.button.is-fullwidth{display:flex;width:100%}.button.is-loading{color:transparent!important;pointer-events:none}.button.is-loading::after{position:absolute;left:calc(50% - (1em/2));top:calc(50% - (1em/2));position:absolute!important}.button.is-static{background-color:#f5f5f5;border-color:#dbdbdb;color:#7a7a7a;box-shadow:none;pointer-events:none}.button.is-rounded{border-radius:290486px;padding-left:calc(1em + .25em);padding-right:calc(1em + .25em)}.buttons{align-items:center;display:flex;flex-wrap:wrap;justify-content:flex-start}.buttons .button{margin-bottom:.5rem}.buttons .button:not(:last-child):not(.is-fullwidth){margin-right:.5rem}.buttons:last-child{margin-bottom:-.5rem}.buttons:not(:last-child){margin-bottom:1rem}.buttons.are-small .button:not(.is-normal):not(.is-medium):not(.is-large){border-radius:2px;font-size:.75rem}.buttons.are-medium .button:not(.is-small):not(.is-normal):not(.is-large){font-size:1.25rem}.buttons.are-large .button:not(.is-small):not(.is-normal):not(.is-medium){font-size:1.5rem}.buttons.has-addons .button:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.buttons.has-addons .button:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0;margin-right:-1px}.buttons.has-addons .button:last-child{margin-right:0}.buttons.has-addons .button:hover,.buttons.has-addons .button.is-hovered{z-index:2}.buttons.has-addons .button:focus,.buttons.has-addons .button.is-focused,.buttons.has-addons .button:active,.buttons.has-addons .button.is-active,.buttons.has-addons .button.is-selected{z-index:3}.buttons.has-addons .button:focus:hover,.buttons.has-addons .button.is-focused:hover,.buttons.has-addons .button:active:hover,.buttons.has-addons .button.is-active:hover,.buttons.has-addons .button.is-selected:hover{z-index:4}.buttons.has-addons .button.is-expanded{flex-grow:1;flex-shrink:1}.buttons.is-centered{justify-content:center}.buttons.is-centered:not(.has-addons) .button:not(.is-fullwidth){margin-left:.25rem;margin-right:.25rem}.buttons.is-right{justify-content:flex-end}.buttons.is-right:not(.has-addons) .button:not(.is-fullwidth){margin-left:.25rem;margin-right:.25rem}.container{flex-grow:1;margin:0 auto;position:relative;width:auto}.container.is-fluid{max-width:none;padding-left:32px;padding-right:32px;width:100%}@media screen and (min-width:1024px){.container{max-width:960px}}@media screen and (max-width:1215px){.container.is-widescreen{max-width:1152px}}@media screen and (max-width:1407px){.container.is-fullhd{max-width:1344px}}@media screen and (min-width:1216px){.container{max-width:1152px}}@media screen and (min-width:1408px){.container{max-width:1344px}}.content li+li{margin-top:.25em}.content p:not(:last-child),.content dl:not(:last-child),.content ol:not(:last-child),.content ul:not(:last-child),.content blockquote:not(:last-child),.content pre:not(:last-child),.content table:not(:last-child){margin-bottom:1em}.content h1,.content h2,.content h3,.content h4,.content h5,.content h6{color:#363636;font-weight:600;line-height:1.125}.content h1{font-size:2em;margin-bottom:.5em}.content h1:not(:first-child){margin-top:1em}.content h2{font-size:1.75em;margin-bottom:.5714em}.content h2:not(:first-child){margin-top:1.1428em}.content h3{font-size:1.5em;margin-bottom:.6666em}.content h3:not(:first-child){margin-top:1.3333em}.content h4{font-size:1.25em;margin-bottom:.8em}.content h5{font-size:1.125em;margin-bottom:.8888em}.content h6{font-size:1em;margin-bottom:1em}.content blockquote{background-color:#f5f5f5;border-left:5px solid #dbdbdb;padding:1.25em 1.5em}.content ol{list-style-position:outside;margin-left:2em;margin-top:1em}.content ol:not([type]){list-style-type:decimal}.content ol:not([type]).is-lower-alpha{list-style-type:lower-alpha}.content ol:not([type]).is-lower-roman{list-style-type:lower-roman}.content ol:not([type]).is-upper-alpha{list-style-type:upper-alpha}.content ol:not([type]).is-upper-roman{list-style-type:upper-roman}.content ul{list-style:disc outside;margin-left:2em;margin-top:1em}.content ul ul{list-style-type:circle;margin-top:.5em}.content ul ul ul{list-style-type:square}.content dd{margin-left:2em}.content figure{margin-left:2em;margin-right:2em;text-align:center}.content figure:not(:first-child){margin-top:2em}.content figure:not(:last-child){margin-bottom:2em}.content figure img{display:inline-block}.content figure figcaption{font-style:italic}.content pre{-webkit-overflow-scrolling:touch;overflow-x:auto;padding:1.25em 1.5em;white-space:pre;word-wrap:normal}.content sup,.content sub{font-size:75%}.content table{width:100%}.content table td,.content table th{border:1px solid #dbdbdb;border-width:0 0 1px;padding:.5em .75em;vertical-align:top}.content table th{color:#363636}.content table th:not([align]){text-align:left}.content table thead td,.content table thead th{border-width:0 0 2px;color:#363636}.content table tfoot td,.content table tfoot th{border-width:2px 0 0;color:#363636}.content table tbody tr:last-child td,.content table tbody tr:last-child th{border-bottom-width:0}.content .tabs li+li{margin-top:0}.content.is-small{font-size:.75rem}.content.is-medium{font-size:1.25rem}.content.is-large{font-size:1.5rem}.icon{align-items:center;display:inline-flex;justify-content:center;height:1.5rem;width:1.5rem}.icon.is-small{height:1rem;width:1rem}.icon.is-medium{height:2rem;width:2rem}.icon.is-large{height:3rem;width:3rem}.image{display:block;position:relative}.image img{display:block;height:auto;width:100%}.image img.is-rounded{border-radius:290486px}.image.is-fullwidth{width:100%}.image.is-square img,.image.is-square .has-ratio,.image.is-1by1 img,.image.is-1by1 .has-ratio,.image.is-5by4 img,.image.is-5by4 .has-ratio,.image.is-4by3 img,.image.is-4by3 .has-ratio,.image.is-3by2 img,.image.is-3by2 .has-ratio,.image.is-5by3 img,.image.is-5by3 .has-ratio,.image.is-16by9 img,.image.is-16by9 .has-ratio,.image.is-2by1 img,.image.is-2by1 .has-ratio,.image.is-3by1 img,.image.is-3by1 .has-ratio,.image.is-4by5 img,.image.is-4by5 .has-ratio,.image.is-3by4 img,.image.is-3by4 .has-ratio,.image.is-2by3 img,.image.is-2by3 .has-ratio,.image.is-3by5 img,.image.is-3by5 .has-ratio,.image.is-9by16 img,.image.is-9by16 .has-ratio,.image.is-1by2 img,.image.is-1by2 .has-ratio,.image.is-1by3 img,.image.is-1by3 .has-ratio{height:100%;width:100%}.image.is-square,.image.is-1by1{padding-top:100%}.image.is-5by4{padding-top:80%}.image.is-4by3{padding-top:75%}.image.is-3by2{padding-top:66.6666%}.image.is-5by3{padding-top:60%}.image.is-16by9{padding-top:56.25%}.image.is-2by1{padding-top:50%}.image.is-3by1{padding-top:33.3333%}.image.is-4by5{padding-top:125%}.image.is-3by4{padding-top:133.3333%}.image.is-2by3{padding-top:150%}.image.is-3by5{padding-top:166.6666%}.image.is-9by16{padding-top:177.7777%}.image.is-1by2{padding-top:200%}.image.is-1by3{padding-top:300%}.image.is-16x16{height:16px;width:16px}.image.is-24x24{height:24px;width:24px}.image.is-32x32{height:32px;width:32px}.image.is-48x48{height:48px;width:48px}.image.is-64x64{height:64px;width:64px}.image.is-96x96{height:96px;width:96px}.image.is-128x128{height:128px;width:128px}.notification{background-color:#f5f5f5;border-radius:4px;padding:1.25rem 2.5rem 1.25rem 1.5rem;position:relative}.notification a:not(.button):not(.dropdown-item){color:currentColor;text-decoration:underline}.notification strong{color:currentColor}.notification code,.notification pre{background:#fff}.notification pre code{background:0 0}.notification>.delete{position:absolute;right:.5rem;top:.5rem}.notification .title,.notification .subtitle,.notification .content{color:currentColor}.notification.is-white{background-color:#fff;color:#0a0a0a}.notification.is-black{background-color:#0a0a0a;color:#fff}.notification.is-light,#home>section.notification:nth-child(even){background-color:#f5f5f5;color:rgba(0,0,0,.7)}.notification.is-dark{background-color:#363636;color:#fff}.notification.is-primary{background-color:#c93312;color:#fff}.notification.is-link{background-color:#3273dc;color:#fff}.notification.is-info{background-color:#3298dc;color:#fff}.notification.is-success{background-color:#48c774;color:#fff}.notification.is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.notification.is-danger{background-color:#f14668;color:#fff}.progress{-moz-appearance:none;-webkit-appearance:none;border:none;border-radius:290486px;display:block;height:1rem;overflow:hidden;padding:0;width:100%}.progress::-webkit-progress-bar{background-color:#ededed}.progress::-webkit-progress-value{background-color:#4a4a4a}.progress::-moz-progress-bar{background-color:#4a4a4a}.progress::-ms-fill{background-color:#4a4a4a;border:none}.progress.is-white::-webkit-progress-value{background-color:#fff}.progress.is-white::-moz-progress-bar{background-color:#fff}.progress.is-white::-ms-fill{background-color:#fff}.progress.is-white:indeterminate{background-image:linear-gradient(to right,white 30%,#ededed 30%)}.progress.is-black::-webkit-progress-value{background-color:#0a0a0a}.progress.is-black::-moz-progress-bar{background-color:#0a0a0a}.progress.is-black::-ms-fill{background-color:#0a0a0a}.progress.is-black:indeterminate{background-image:linear-gradient(to right,#0a0a0a 30%,#ededed 30%)}.progress.is-light::-webkit-progress-value,#home>section.progress:nth-child(even)::-webkit-progress-value{background-color:#f5f5f5}.progress.is-light::-moz-progress-bar,#home>section.progress:nth-child(even)::-moz-progress-bar{background-color:#f5f5f5}.progress.is-light::-ms-fill,#home>section.progress:nth-child(even)::-ms-fill{background-color:#f5f5f5}.progress.is-light:indeterminate,#home>section.progress:indeterminate:nth-child(even){background-image:linear-gradient(to right,whitesmoke 30%,#ededed 30%)}.progress.is-dark::-webkit-progress-value{background-color:#363636}.progress.is-dark::-moz-progress-bar{background-color:#363636}.progress.is-dark::-ms-fill{background-color:#363636}.progress.is-dark:indeterminate{background-image:linear-gradient(to right,#363636 30%,#ededed 30%)}.progress.is-primary::-webkit-progress-value{background-color:#c93312}.progress.is-primary::-moz-progress-bar{background-color:#c93312}.progress.is-primary::-ms-fill{background-color:#c93312}.progress.is-primary:indeterminate{background-image:linear-gradient(to right,#C93312 30%,#ededed 30%)}.progress.is-link::-webkit-progress-value{background-color:#3273dc}.progress.is-link::-moz-progress-bar{background-color:#3273dc}.progress.is-link::-ms-fill{background-color:#3273dc}.progress.is-link:indeterminate{background-image:linear-gradient(to right,#3273dc 30%,#ededed 30%)}.progress.is-info::-webkit-progress-value{background-color:#3298dc}.progress.is-info::-moz-progress-bar{background-color:#3298dc}.progress.is-info::-ms-fill{background-color:#3298dc}.progress.is-info:indeterminate{background-image:linear-gradient(to right,#3298dc 30%,#ededed 30%)}.progress.is-success::-webkit-progress-value{background-color:#48c774}.progress.is-success::-moz-progress-bar{background-color:#48c774}.progress.is-success::-ms-fill{background-color:#48c774}.progress.is-success:indeterminate{background-image:linear-gradient(to right,#48c774 30%,#ededed 30%)}.progress.is-warning::-webkit-progress-value{background-color:#ffdd57}.progress.is-warning::-moz-progress-bar{background-color:#ffdd57}.progress.is-warning::-ms-fill{background-color:#ffdd57}.progress.is-warning:indeterminate{background-image:linear-gradient(to right,#ffdd57 30%,#ededed 30%)}.progress.is-danger::-webkit-progress-value{background-color:#f14668}.progress.is-danger::-moz-progress-bar{background-color:#f14668}.progress.is-danger::-ms-fill{background-color:#f14668}.progress.is-danger:indeterminate{background-image:linear-gradient(to right,#f14668 30%,#ededed 30%)}.progress:indeterminate{animation-duration:1.5s;animation-iteration-count:infinite;animation-name:moveIndeterminate;animation-timing-function:linear;background-color:#ededed;background-image:linear-gradient(to right,#4a4a4a 30%,#ededed 30%);background-position:0 0;background-repeat:no-repeat;background-size:150% 150%}.progress:indeterminate::-webkit-progress-bar{background-color:transparent}.progress:indeterminate::-moz-progress-bar{background-color:transparent}.progress.is-small{height:.75rem}.progress.is-medium{height:1.25rem}.progress.is-large{height:1.5rem}@keyframes moveIndeterminate{from{background-position:200% 0}to{background-position:-200% 0}}.table{background-color:#fff;color:#363636}.table td,.table th{border:1px solid #dbdbdb;border-width:0 0 1px;padding:.5em .75em;vertical-align:top}.table td.is-white,.table th.is-white{background-color:#fff;border-color:#fff;color:#0a0a0a}.table td.is-black,.table th.is-black{background-color:#0a0a0a;border-color:#0a0a0a;color:#fff}.table td.is-light,.table th.is-light{background-color:#f5f5f5;border-color:#f5f5f5;color:rgba(0,0,0,.7)}.table td.is-dark,.table th.is-dark{background-color:#363636;border-color:#363636;color:#fff}.table td.is-primary,.table th.is-primary{background-color:#c93312;border-color:#c93312;color:#fff}.table td.is-link,.table th.is-link{background-color:#3273dc;border-color:#3273dc;color:#fff}.table td.is-info,.table th.is-info{background-color:#3298dc;border-color:#3298dc;color:#fff}.table td.is-success,.table th.is-success{background-color:#48c774;border-color:#48c774;color:#fff}.table td.is-warning,.table th.is-warning{background-color:#ffdd57;border-color:#ffdd57;color:rgba(0,0,0,.7)}.table td.is-danger,.table th.is-danger{background-color:#f14668;border-color:#f14668;color:#fff}.table td.is-narrow,.table th.is-narrow{white-space:nowrap;width:1%}.table td.is-selected,.table th.is-selected{background-color:#c93312;color:#fff}.table td.is-selected a,.table td.is-selected strong,.table th.is-selected a,.table th.is-selected strong{color:currentColor}.table th{color:#363636}.table th:not([align]){text-align:left}.table tr.is-selected{background-color:#c93312;color:#fff}.table tr.is-selected a,.table tr.is-selected strong{color:currentColor}.table tr.is-selected td,.table tr.is-selected th{border-color:#fff;color:currentColor}.table thead{background-color:transparent}.table thead td,.table thead th{border-width:0 0 2px;color:#363636}.table tfoot{background-color:transparent}.table tfoot td,.table tfoot th{border-width:2px 0 0;color:#363636}.table tbody{background-color:transparent}.table tbody tr:last-child td,.table tbody tr:last-child th{border-bottom-width:0}.table.is-bordered td,.table.is-bordered th{border-width:1px}.table.is-bordered tr:last-child td,.table.is-bordered tr:last-child th{border-bottom-width:1px}.table.is-fullwidth{width:100%}.table.is-hoverable tbody tr:not(.is-selected):hover{background-color:#fafafa}.table.is-hoverable.is-striped tbody tr:not(.is-selected):hover{background-color:#fafafa}.table.is-hoverable.is-striped tbody tr:not(.is-selected):hover:nth-child(even){background-color:#f5f5f5}.table.is-narrow td,.table.is-narrow th{padding:.25em .5em}.table.is-striped tbody tr:not(.is-selected):nth-child(even){background-color:#fafafa}.table-container{-webkit-overflow-scrolling:touch;overflow:auto;overflow-y:hidden;max-width:100%}.tags{align-items:center;display:flex;flex-wrap:wrap;justify-content:flex-start}.tags .tag{margin-bottom:.5rem}.tags .tag:not(:last-child){margin-right:.5rem}.tags:last-child{margin-bottom:-.5rem}.tags:not(:last-child){margin-bottom:1rem}.tags.are-medium .tag:not(.is-normal):not(.is-large){font-size:1rem}.tags.are-large .tag:not(.is-normal):not(.is-medium){font-size:1.25rem}.tags.is-centered{justify-content:center}.tags.is-centered .tag{margin-right:.25rem;margin-left:.25rem}.tags.is-right{justify-content:flex-end}.tags.is-right .tag:not(:first-child){margin-left:.5rem}.tags.is-right .tag:not(:last-child){margin-right:0}.tags.has-addons .tag{margin-right:0}.tags.has-addons .tag:not(:first-child){margin-left:0;border-bottom-left-radius:0;border-top-left-radius:0}.tags.has-addons .tag:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.tag:not(body){align-items:center;background-color:#f5f5f5;border-radius:4px;color:#4a4a4a;display:inline-flex;font-size:.75rem;height:2em;justify-content:center;line-height:1.5;padding-left:.75em;padding-right:.75em;white-space:nowrap}.tag:not(body) .delete{margin-left:.25rem;margin-right:-.375rem}.tag:not(body).is-white{background-color:#fff;color:#0a0a0a}.tag:not(body).is-black{background-color:#0a0a0a;color:#fff}.tag:not(body).is-light,#home>section:not(body):nth-child(even){background-color:#f5f5f5;color:rgba(0,0,0,.7)}.tag:not(body).is-dark{background-color:#363636;color:#fff}.tag:not(body).is-primary{background-color:#c93312;color:#fff}.tag:not(body).is-primary.is-light,#home>section.is-primary:nth-child(even){background-color:#fdefec;color:#e13914}.tag:not(body).is-link{background-color:#3273dc;color:#fff}.tag:not(body).is-link.is-light,#home>section.is-link:nth-child(even){background-color:#eef3fc;color:#2160c4}.tag:not(body).is-info{background-color:#3298dc;color:#fff}.tag:not(body).is-info.is-light,#home>section.is-info:nth-child(even){background-color:#eef6fc;color:#1d72aa}.tag:not(body).is-success{background-color:#48c774;color:#fff}.tag:not(body).is-success.is-light,#home>section.is-success:nth-child(even){background-color:#effaf3;color:#257942}.tag:not(body).is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.tag:not(body).is-warning.is-light,#home>section.is-warning:nth-child(even){background-color:#fffbeb;color:#947600}.tag:not(body).is-danger{background-color:#f14668;color:#fff}.tag:not(body).is-danger.is-light,#home>section.is-danger:nth-child(even){background-color:#feecf0;color:#cc0f35}.tag:not(body).is-normal{font-size:.75rem}.tag:not(body).is-medium{font-size:1rem}.tag:not(body).is-large{font-size:1.25rem}.tag:not(body) .icon:first-child:not(:last-child){margin-left:-.375em;margin-right:.1875em}.tag:not(body) .icon:last-child:not(:first-child){margin-left:.1875em;margin-right:-.375em}.tag:not(body) .icon:first-child:last-child{margin-left:-.375em;margin-right:-.375em}.tag:not(body).is-delete{margin-left:1px;padding:0;position:relative;width:2em}.tag:not(body).is-delete::before,.tag:not(body).is-delete::after{background-color:currentColor;content:\"\";display:block;left:50%;position:absolute;top:50%;transform:translateX(-50%)translateY(-50%)rotate(45deg);transform-origin:center center}.tag:not(body).is-delete::before{height:1px;width:50%}.tag:not(body).is-delete::after{height:50%;width:1px}.tag:not(body).is-delete:hover,.tag:not(body).is-delete:focus{background-color:#e8e8e8}.tag:not(body).is-delete:active{background-color:#dbdbdb}.tag:not(body).is-rounded{border-radius:290486px}a.tag:hover{text-decoration:underline}.title,.subtitle{word-break:break-word}.title em,.title span,.subtitle em,.subtitle span{font-weight:inherit}.title sub,.subtitle sub{font-size:.75em}.title sup,.subtitle sup{font-size:.75em}.title .tag,.subtitle .tag{vertical-align:middle}.title{color:#363636;font-size:2rem;font-weight:600;line-height:1.125}.title strong{color:inherit;font-weight:inherit}.title+.highlight{margin-top:-.75rem}.title:not(.is-spaced)+.subtitle{margin-top:-1.25rem}.title.is-1{font-size:3rem}.title.is-2{font-size:2.5rem}.title.is-3{font-size:2rem}.title.is-4{font-size:1.5rem}.title.is-5{font-size:1.25rem}.title.is-6{font-size:1rem}.title.is-7{font-size:.75rem}.subtitle{color:#4a4a4a;font-size:1.25rem;font-weight:400;line-height:1.25}.subtitle strong{color:#363636;font-weight:600}.subtitle:not(.is-spaced)+.title{margin-top:-1.25rem}.subtitle.is-1{font-size:3rem}.subtitle.is-2{font-size:2.5rem}.subtitle.is-3{font-size:2rem}.subtitle.is-4{font-size:1.5rem}.subtitle.is-5{font-size:1.25rem}.subtitle.is-6{font-size:1rem}.subtitle.is-7{font-size:.75rem}.heading{display:block;font-size:11px;letter-spacing:1px;margin-bottom:5px;text-transform:uppercase}.highlight{font-weight:400;max-width:100%;overflow:hidden;padding:0}.highlight pre{overflow:auto;max-width:100%}.number{align-items:center;background-color:#f5f5f5;border-radius:290486px;display:inline-flex;font-size:1.25rem;height:2em;justify-content:center;margin-right:1.5rem;min-width:2.5em;padding:.25rem .5rem;text-align:center;vertical-align:top}.breadcrumb{font-size:1rem;white-space:nowrap}.breadcrumb a{align-items:center;color:#3273dc;display:flex;justify-content:center;padding:0 .75em}.breadcrumb a:hover{color:#363636}.breadcrumb li{align-items:center;display:flex}.breadcrumb li:first-child a{padding-left:0}.breadcrumb li.is-active a{color:#363636;cursor:default;pointer-events:none}.breadcrumb li+li::before{color:#b5b5b5;content:\"\\0002f\"}.breadcrumb ul,.breadcrumb ol{align-items:flex-start;display:flex;flex-wrap:wrap;justify-content:flex-start}.breadcrumb .icon:first-child{margin-right:.5em}.breadcrumb .icon:last-child{margin-left:.5em}.breadcrumb.is-centered ol,.breadcrumb.is-centered ul{justify-content:center}.breadcrumb.is-right ol,.breadcrumb.is-right ul{justify-content:flex-end}.breadcrumb.is-small{font-size:.75rem}.breadcrumb.is-medium{font-size:1.25rem}.breadcrumb.is-large{font-size:1.5rem}.breadcrumb.has-arrow-separator li+li::before{content:\"\\02192\"}.breadcrumb.has-bullet-separator li+li::before{content:\"\\02022\"}.breadcrumb.has-dot-separator li+li::before{content:\"\\000b7\"}.breadcrumb.has-succeeds-separator li+li::before{content:\"\\0227B\"}.card{background-color:#fff;box-shadow:0 .5em 1em -.125em rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,2%);color:#4a4a4a;max-width:100%;position:relative}.card-header{background-color:transparent;align-items:stretch;box-shadow:0 .125em .25em rgba(10,10,10,.1);display:flex}.card-header-title{align-items:center;color:#363636;display:flex;flex-grow:1;font-weight:700;padding:.75rem 1rem}.card-header-title.is-centered{justify-content:center}.card-header-icon{align-items:center;cursor:pointer;display:flex;justify-content:center;padding:.75rem 1rem}.card-image{display:block;position:relative}.card-content{background-color:transparent;padding:1.5rem}.card-footer{background-color:transparent;border-top:1px solid #ededed;align-items:stretch;display:flex}.card-footer-item{align-items:center;display:flex;flex-basis:0;flex-grow:1;flex-shrink:0;justify-content:center;padding:.75rem}.card-footer-item:not(:last-child){border-right:1px solid #ededed}.card .media:not(:last-child){margin-bottom:1.5rem}.dropdown{display:inline-flex;position:relative;vertical-align:top}.dropdown.is-active .dropdown-menu,.dropdown.is-hoverable:hover .dropdown-menu{display:block}.dropdown.is-right .dropdown-menu{left:auto;right:0}.dropdown.is-up .dropdown-menu{bottom:100%;padding-bottom:4px;padding-top:initial;top:auto}.dropdown-menu{display:none;left:0;min-width:12rem;padding-top:4px;position:absolute;top:100%;z-index:20}.dropdown-content{background-color:#fff;border-radius:4px;box-shadow:0 .5em 1em -.125em rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,2%);padding-bottom:.5rem;padding-top:.5rem}.dropdown-item{color:#4a4a4a;display:block;font-size:.875rem;line-height:1.5;padding:.375rem 1rem;position:relative}a.dropdown-item,button.dropdown-item{padding-right:3rem;text-align:left;white-space:nowrap;width:100%}a.dropdown-item:hover,button.dropdown-item:hover{background-color:#f5f5f5;color:#0a0a0a}a.dropdown-item.is-active,button.dropdown-item.is-active{background-color:#3273dc;color:#fff}.dropdown-divider{background-color:#ededed;border:none;display:block;height:1px;margin:.5rem 0}.level{align-items:center;justify-content:space-between}.level code{border-radius:4px}.level img{display:inline-block;vertical-align:top}.level.is-mobile{display:flex}.level.is-mobile .level-left,.level.is-mobile .level-right{display:flex}.level.is-mobile .level-left+.level-right{margin-top:0}.level.is-mobile .level-item:not(:last-child){margin-bottom:0;margin-right:.75rem}.level.is-mobile .level-item:not(.is-narrow){flex-grow:1}@media screen and (min-width:769px),print{.level{display:flex}.level>.level-item:not(.is-narrow){flex-grow:1}}.level-item{align-items:center;display:flex;flex-basis:auto;flex-grow:0;flex-shrink:0;justify-content:center}.level-item .title,.level-item .subtitle{margin-bottom:0}@media screen and (max-width:768px){.level-item:not(:last-child){margin-bottom:.75rem}}.level-left,.level-right{flex-basis:auto;flex-grow:0;flex-shrink:0}.level-left .level-item.is-flexible,.level-right .level-item.is-flexible{flex-grow:1}@media screen and (min-width:769px),print{.level-left .level-item:not(:last-child),.level-right .level-item:not(:last-child){margin-right:.75rem}}.level-left{align-items:center;justify-content:flex-start}@media screen and (max-width:768px){.level-left+.level-right{margin-top:1.5rem}}@media screen and (min-width:769px),print{.level-left{display:flex}}.level-right{align-items:center;justify-content:flex-end}@media screen and (min-width:769px),print{.level-right{display:flex}}.list{background-color:#fff;border-radius:4px;box-shadow:0 2px 3px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1)}.list-item{display:block;padding:.5em 1em}.list-item:not(a){color:#4a4a4a}.list-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-item:last-child{border-bottom-left-radius:4px;border-bottom-right-radius:4px}.list-item:not(:last-child){border-bottom:1px solid #dbdbdb}.list-item.is-active{background-color:#3273dc;color:#fff}a.list-item{background-color:#f5f5f5;cursor:pointer}.media{align-items:flex-start;display:flex;text-align:left}.media .content:not(:last-child){margin-bottom:.75rem}.media .media{border-top:1px solid rgba(219,219,219,.5);display:flex;padding-top:.75rem}.media .media .content:not(:last-child),.media .media .control:not(:last-child){margin-bottom:.5rem}.media .media .media{padding-top:.5rem}.media .media .media+.media{margin-top:.5rem}.media+.media{border-top:1px solid rgba(219,219,219,.5);margin-top:1rem;padding-top:1rem}.media.is-large+.media{margin-top:1.5rem;padding-top:1.5rem}.media-left,.media-right{flex-basis:auto;flex-grow:0;flex-shrink:0}.media-left{margin-right:1rem}.media-right{margin-left:1rem}.media-content{flex-basis:auto;flex-grow:1;flex-shrink:1;text-align:left}@media screen and (max-width:768px){.media-content{overflow-x:auto}}.menu{font-size:1rem}.menu.is-small{font-size:.75rem}.menu.is-medium{font-size:1.25rem}.menu.is-large{font-size:1.5rem}.menu-list{line-height:1.25}.menu-list a{border-radius:2px;color:#4a4a4a;display:block;padding:.5em .75em}.menu-list a:hover{background-color:#f5f5f5;color:#363636}.menu-list a.is-active{background-color:#3273dc;color:#fff}.menu-list li ul{border-left:1px solid #dbdbdb;margin:.75em;padding-left:.75em}.menu-label{color:#7a7a7a;font-size:.75em;letter-spacing:.1em;text-transform:uppercase}.menu-label:not(:first-child){margin-top:1em}.menu-label:not(:last-child){margin-bottom:1em}.message{background-color:#f5f5f5;border-radius:4px;font-size:1rem}.message strong{color:currentColor}.message a:not(.button):not(.tag):not(.dropdown-item){color:currentColor;text-decoration:underline}.message.is-small{font-size:.75rem}.message.is-medium{font-size:1.25rem}.message.is-large{font-size:1.5rem}.message.is-white{background-color:#fff}.message.is-white .message-header{background-color:#fff;color:#0a0a0a}.message.is-white .message-body{border-color:#fff}.message.is-black{background-color:#fafafa}.message.is-black .message-header{background-color:#0a0a0a;color:#fff}.message.is-black .message-body{border-color:#0a0a0a}.message.is-light,#home>section.message:nth-child(even){background-color:#fafafa}.message.is-light .message-header,#home>section.message:nth-child(even) .message-header{background-color:#f5f5f5;color:rgba(0,0,0,.7)}.message.is-light .message-body,#home>section.message:nth-child(even) .message-body{border-color:#f5f5f5}.message.is-dark{background-color:#fafafa}.message.is-dark .message-header{background-color:#363636;color:#fff}.message.is-dark .message-body{border-color:#363636}.message.is-primary{background-color:#fdefec}.message.is-primary .message-header{background-color:#c93312;color:#fff}.message.is-primary .message-body{border-color:#c93312;color:#e13914}.message.is-link{background-color:#eef3fc}.message.is-link .message-header{background-color:#3273dc;color:#fff}.message.is-link .message-body{border-color:#3273dc;color:#2160c4}.message.is-info{background-color:#eef6fc}.message.is-info .message-header{background-color:#3298dc;color:#fff}.message.is-info .message-body{border-color:#3298dc;color:#1d72aa}.message.is-success{background-color:#effaf3}.message.is-success .message-header{background-color:#48c774;color:#fff}.message.is-success .message-body{border-color:#48c774;color:#257942}.message.is-warning{background-color:#fffbeb}.message.is-warning .message-header{background-color:#ffdd57;color:rgba(0,0,0,.7)}.message.is-warning .message-body{border-color:#ffdd57;color:#947600}.message.is-danger{background-color:#feecf0}.message.is-danger .message-header{background-color:#f14668;color:#fff}.message.is-danger .message-body{border-color:#f14668;color:#cc0f35}.message-header{align-items:center;background-color:#4a4a4a;border-radius:4px 4px 0 0;color:#fff;display:flex;font-weight:700;justify-content:space-between;line-height:1.25;padding:.75em 1em;position:relative}.message-header .delete{flex-grow:0;flex-shrink:0;margin-left:.75em}.message-header+.message-body{border-width:0;border-top-left-radius:0;border-top-right-radius:0}.message-body{border-color:#dbdbdb;border-radius:4px;border-style:solid;border-width:0 0 0 4px;color:#4a4a4a;padding:1.25em 1.5em}.message-body code,.message-body pre{background-color:#fff}.message-body pre code{background-color:transparent}.modal{align-items:center;display:none;flex-direction:column;justify-content:center;overflow:hidden;position:fixed;z-index:40}.modal.is-active{display:flex}.modal-background{background-color:rgba(10,10,10,.86)}.modal-content,.modal-card{margin:0 20px;max-height:calc(100vh - 160px);overflow:auto;position:relative;width:100%}@media screen and (min-width:769px),print{.modal-content,.modal-card{margin:0 auto;max-height:calc(100vh - 40px);width:640px}}.modal-close{background:0 0;height:40px;position:fixed;right:20px;top:20px;width:40px}.modal-card{display:flex;flex-direction:column;max-height:calc(100vh - 40px);overflow:hidden;-ms-overflow-y:visible}.modal-card-head,.modal-card-foot{align-items:center;background-color:#f5f5f5;display:flex;flex-shrink:0;justify-content:flex-start;padding:20px;position:relative}.modal-card-head{border-bottom:1px solid #dbdbdb;border-top-left-radius:6px;border-top-right-radius:6px}.modal-card-title{color:#363636;flex-grow:1;flex-shrink:0;font-size:1.5rem;line-height:1}.modal-card-foot{border-bottom-left-radius:6px;border-bottom-right-radius:6px;border-top:1px solid #dbdbdb}.modal-card-foot .button:not(:last-child){margin-right:.5em}.modal-card-body{-webkit-overflow-scrolling:touch;background-color:#fff;flex-grow:1;flex-shrink:1;overflow:auto;padding:20px}.navbar{background-color:#fff;min-height:3.25rem;position:relative;z-index:30}.navbar.is-white{background-color:#fff;color:#0a0a0a}.navbar.is-white .navbar-brand>.navbar-item,.navbar.is-white .navbar-brand .navbar-link{color:#0a0a0a}.navbar.is-white .navbar-brand>a.navbar-item:focus,.navbar.is-white .navbar-brand>a.navbar-item:hover,.navbar.is-white .navbar-brand>a.navbar-item.is-active,.navbar.is-white .navbar-brand .navbar-link:focus,.navbar.is-white .navbar-brand .navbar-link:hover,.navbar.is-white .navbar-brand .navbar-link.is-active{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-brand .navbar-link::after{border-color:#0a0a0a}.navbar.is-white .navbar-burger{color:#0a0a0a}@media screen and (min-width:1024px){.navbar.is-white .navbar-start>.navbar-item,.navbar.is-white .navbar-start .navbar-link,.navbar.is-white .navbar-end>.navbar-item,.navbar.is-white .navbar-end .navbar-link{color:#0a0a0a}.navbar.is-white .navbar-start>a.navbar-item:focus,.navbar.is-white .navbar-start>a.navbar-item:hover,.navbar.is-white .navbar-start>a.navbar-item.is-active,.navbar.is-white .navbar-start .navbar-link:focus,.navbar.is-white .navbar-start .navbar-link:hover,.navbar.is-white .navbar-start .navbar-link.is-active,.navbar.is-white .navbar-end>a.navbar-item:focus,.navbar.is-white .navbar-end>a.navbar-item:hover,.navbar.is-white .navbar-end>a.navbar-item.is-active,.navbar.is-white .navbar-end .navbar-link:focus,.navbar.is-white .navbar-end .navbar-link:hover,.navbar.is-white .navbar-end .navbar-link.is-active{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-start .navbar-link::after,.navbar.is-white .navbar-end .navbar-link::after{border-color:#0a0a0a}.navbar.is-white .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-white .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-white .navbar-item.has-dropdown.is-active .navbar-link{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-dropdown a.navbar-item.is-active{background-color:#fff;color:#0a0a0a}}.navbar.is-black{background-color:#0a0a0a;color:#fff}.navbar.is-black .navbar-brand>.navbar-item,.navbar.is-black .navbar-brand .navbar-link{color:#fff}.navbar.is-black .navbar-brand>a.navbar-item:focus,.navbar.is-black .navbar-brand>a.navbar-item:hover,.navbar.is-black .navbar-brand>a.navbar-item.is-active,.navbar.is-black .navbar-brand .navbar-link:focus,.navbar.is-black .navbar-brand .navbar-link:hover,.navbar.is-black .navbar-brand .navbar-link.is-active{background-color:#000;color:#fff}.navbar.is-black .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-black .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-black .navbar-start>.navbar-item,.navbar.is-black .navbar-start .navbar-link,.navbar.is-black .navbar-end>.navbar-item,.navbar.is-black .navbar-end .navbar-link{color:#fff}.navbar.is-black .navbar-start>a.navbar-item:focus,.navbar.is-black .navbar-start>a.navbar-item:hover,.navbar.is-black .navbar-start>a.navbar-item.is-active,.navbar.is-black .navbar-start .navbar-link:focus,.navbar.is-black .navbar-start .navbar-link:hover,.navbar.is-black .navbar-start .navbar-link.is-active,.navbar.is-black .navbar-end>a.navbar-item:focus,.navbar.is-black .navbar-end>a.navbar-item:hover,.navbar.is-black .navbar-end>a.navbar-item.is-active,.navbar.is-black .navbar-end .navbar-link:focus,.navbar.is-black .navbar-end .navbar-link:hover,.navbar.is-black .navbar-end .navbar-link.is-active{background-color:#000;color:#fff}.navbar.is-black .navbar-start .navbar-link::after,.navbar.is-black .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-black .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-black .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-black .navbar-item.has-dropdown.is-active .navbar-link{background-color:#000;color:#fff}.navbar.is-black .navbar-dropdown a.navbar-item.is-active{background-color:#0a0a0a;color:#fff}}.navbar.is-light,#home>section.navbar:nth-child(even){background-color:#f5f5f5;color:rgba(0,0,0,.7)}.navbar.is-light .navbar-brand>.navbar-item,#home>section.navbar:nth-child(even) .navbar-brand>.navbar-item,.navbar.is-light .navbar-brand .navbar-link,#home>section.navbar:nth-child(even) .navbar-brand .navbar-link{color:rgba(0,0,0,.7)}.navbar.is-light .navbar-brand>a.navbar-item:focus,#home>section.navbar:nth-child(even) .navbar-brand>a.navbar-item:focus,.navbar.is-light .navbar-brand>a.navbar-item:hover,#home>section.navbar:nth-child(even) .navbar-brand>a.navbar-item:hover,.navbar.is-light .navbar-brand>a.navbar-item.is-active,#home>section.navbar:nth-child(even) .navbar-brand>a.navbar-item.is-active,.navbar.is-light .navbar-brand .navbar-link:focus,#home>section.navbar:nth-child(even) .navbar-brand .navbar-link:focus,.navbar.is-light .navbar-brand .navbar-link:hover,#home>section.navbar:nth-child(even) .navbar-brand .navbar-link:hover,.navbar.is-light .navbar-brand .navbar-link.is-active,#home>section.navbar:nth-child(even) .navbar-brand .navbar-link.is-active{background-color:#e8e8e8;color:rgba(0,0,0,.7)}.navbar.is-light .navbar-brand .navbar-link::after,#home>section.navbar:nth-child(even) .navbar-brand .navbar-link::after{border-color:rgba(0,0,0,.7)}.navbar.is-light .navbar-burger,#home>section.navbar:nth-child(even) .navbar-burger{color:rgba(0,0,0,.7)}@media screen and (min-width:1024px){.navbar.is-light .navbar-start>.navbar-item,#home>section.navbar:nth-child(even) .navbar-start>.navbar-item,.navbar.is-light .navbar-start .navbar-link,#home>section.navbar:nth-child(even) .navbar-start .navbar-link,.navbar.is-light .navbar-end>.navbar-item,#home>section.navbar:nth-child(even) .navbar-end>.navbar-item,.navbar.is-light .navbar-end .navbar-link,#home>section.navbar:nth-child(even) .navbar-end .navbar-link{color:rgba(0,0,0,.7)}.navbar.is-light .navbar-start>a.navbar-item:focus,#home>section.navbar:nth-child(even) .navbar-start>a.navbar-item:focus,.navbar.is-light .navbar-start>a.navbar-item:hover,#home>section.navbar:nth-child(even) .navbar-start>a.navbar-item:hover,.navbar.is-light .navbar-start>a.navbar-item.is-active,#home>section.navbar:nth-child(even) .navbar-start>a.navbar-item.is-active,.navbar.is-light .navbar-start .navbar-link:focus,#home>section.navbar:nth-child(even) .navbar-start .navbar-link:focus,.navbar.is-light .navbar-start .navbar-link:hover,#home>section.navbar:nth-child(even) .navbar-start .navbar-link:hover,.navbar.is-light .navbar-start .navbar-link.is-active,#home>section.navbar:nth-child(even) .navbar-start .navbar-link.is-active,.navbar.is-light .navbar-end>a.navbar-item:focus,#home>section.navbar:nth-child(even) .navbar-end>a.navbar-item:focus,.navbar.is-light .navbar-end>a.navbar-item:hover,#home>section.navbar:nth-child(even) .navbar-end>a.navbar-item:hover,.navbar.is-light .navbar-end>a.navbar-item.is-active,#home>section.navbar:nth-child(even) .navbar-end>a.navbar-item.is-active,.navbar.is-light .navbar-end .navbar-link:focus,#home>section.navbar:nth-child(even) .navbar-end .navbar-link:focus,.navbar.is-light .navbar-end .navbar-link:hover,#home>section.navbar:nth-child(even) .navbar-end .navbar-link:hover,.navbar.is-light .navbar-end .navbar-link.is-active,#home>section.navbar:nth-child(even) .navbar-end .navbar-link.is-active{background-color:#e8e8e8;color:rgba(0,0,0,.7)}.navbar.is-light .navbar-start .navbar-link::after,#home>section.navbar:nth-child(even) .navbar-start .navbar-link::after,.navbar.is-light .navbar-end .navbar-link::after,#home>section.navbar:nth-child(even) .navbar-end .navbar-link::after{border-color:rgba(0,0,0,.7)}.navbar.is-light .navbar-item.has-dropdown:focus .navbar-link,#home>section.navbar:nth-child(even) .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-light .navbar-item.has-dropdown:hover .navbar-link,#home>section.navbar:nth-child(even) .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-light .navbar-item.has-dropdown.is-active .navbar-link,#home>section.navbar:nth-child(even) .navbar-item.has-dropdown.is-active .navbar-link{background-color:#e8e8e8;color:rgba(0,0,0,.7)}.navbar.is-light .navbar-dropdown a.navbar-item.is-active,#home>section.navbar:nth-child(even) .navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:rgba(0,0,0,.7)}}.navbar.is-dark{background-color:#363636;color:#fff}.navbar.is-dark .navbar-brand>.navbar-item,.navbar.is-dark .navbar-brand .navbar-link{color:#fff}.navbar.is-dark .navbar-brand>a.navbar-item:focus,.navbar.is-dark .navbar-brand>a.navbar-item:hover,.navbar.is-dark .navbar-brand>a.navbar-item.is-active,.navbar.is-dark .navbar-brand .navbar-link:focus,.navbar.is-dark .navbar-brand .navbar-link:hover,.navbar.is-dark .navbar-brand .navbar-link.is-active{background-color:#292929;color:#fff}.navbar.is-dark .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-dark .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-dark .navbar-start>.navbar-item,.navbar.is-dark .navbar-start .navbar-link,.navbar.is-dark .navbar-end>.navbar-item,.navbar.is-dark .navbar-end .navbar-link{color:#fff}.navbar.is-dark .navbar-start>a.navbar-item:focus,.navbar.is-dark .navbar-start>a.navbar-item:hover,.navbar.is-dark .navbar-start>a.navbar-item.is-active,.navbar.is-dark .navbar-start .navbar-link:focus,.navbar.is-dark .navbar-start .navbar-link:hover,.navbar.is-dark .navbar-start .navbar-link.is-active,.navbar.is-dark .navbar-end>a.navbar-item:focus,.navbar.is-dark .navbar-end>a.navbar-item:hover,.navbar.is-dark .navbar-end>a.navbar-item.is-active,.navbar.is-dark .navbar-end .navbar-link:focus,.navbar.is-dark .navbar-end .navbar-link:hover,.navbar.is-dark .navbar-end .navbar-link.is-active{background-color:#292929;color:#fff}.navbar.is-dark .navbar-start .navbar-link::after,.navbar.is-dark .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-dark .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-dark .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-dark .navbar-item.has-dropdown.is-active .navbar-link{background-color:#292929;color:#fff}.navbar.is-dark .navbar-dropdown a.navbar-item.is-active{background-color:#363636;color:#fff}}.navbar.is-primary{background-color:#c93312;color:#fff}.navbar.is-primary .navbar-brand>.navbar-item,.navbar.is-primary .navbar-brand .navbar-link{color:#fff}.navbar.is-primary .navbar-brand>a.navbar-item:focus,.navbar.is-primary .navbar-brand>a.navbar-item:hover,.navbar.is-primary .navbar-brand>a.navbar-item.is-active,.navbar.is-primary .navbar-brand .navbar-link:focus,.navbar.is-primary .navbar-brand .navbar-link:hover,.navbar.is-primary .navbar-brand .navbar-link.is-active{background-color:#b22d10;color:#fff}.navbar.is-primary .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-primary .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-primary .navbar-start>.navbar-item,.navbar.is-primary .navbar-start .navbar-link,.navbar.is-primary .navbar-end>.navbar-item,.navbar.is-primary .navbar-end .navbar-link{color:#fff}.navbar.is-primary .navbar-start>a.navbar-item:focus,.navbar.is-primary .navbar-start>a.navbar-item:hover,.navbar.is-primary .navbar-start>a.navbar-item.is-active,.navbar.is-primary .navbar-start .navbar-link:focus,.navbar.is-primary .navbar-start .navbar-link:hover,.navbar.is-primary .navbar-start .navbar-link.is-active,.navbar.is-primary .navbar-end>a.navbar-item:focus,.navbar.is-primary .navbar-end>a.navbar-item:hover,.navbar.is-primary .navbar-end>a.navbar-item.is-active,.navbar.is-primary .navbar-end .navbar-link:focus,.navbar.is-primary .navbar-end .navbar-link:hover,.navbar.is-primary .navbar-end .navbar-link.is-active{background-color:#b22d10;color:#fff}.navbar.is-primary .navbar-start .navbar-link::after,.navbar.is-primary .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-primary .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-primary .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-primary .navbar-item.has-dropdown.is-active .navbar-link{background-color:#b22d10;color:#fff}.navbar.is-primary .navbar-dropdown a.navbar-item.is-active{background-color:#c93312;color:#fff}}.navbar.is-link{background-color:#3273dc;color:#fff}.navbar.is-link .navbar-brand>.navbar-item,.navbar.is-link .navbar-brand .navbar-link{color:#fff}.navbar.is-link .navbar-brand>a.navbar-item:focus,.navbar.is-link .navbar-brand>a.navbar-item:hover,.navbar.is-link .navbar-brand>a.navbar-item.is-active,.navbar.is-link .navbar-brand .navbar-link:focus,.navbar.is-link .navbar-brand .navbar-link:hover,.navbar.is-link .navbar-brand .navbar-link.is-active{background-color:#2366d1;color:#fff}.navbar.is-link .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-link .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-link .navbar-start>.navbar-item,.navbar.is-link .navbar-start .navbar-link,.navbar.is-link .navbar-end>.navbar-item,.navbar.is-link .navbar-end .navbar-link{color:#fff}.navbar.is-link .navbar-start>a.navbar-item:focus,.navbar.is-link .navbar-start>a.navbar-item:hover,.navbar.is-link .navbar-start>a.navbar-item.is-active,.navbar.is-link .navbar-start .navbar-link:focus,.navbar.is-link .navbar-start .navbar-link:hover,.navbar.is-link .navbar-start .navbar-link.is-active,.navbar.is-link .navbar-end>a.navbar-item:focus,.navbar.is-link .navbar-end>a.navbar-item:hover,.navbar.is-link .navbar-end>a.navbar-item.is-active,.navbar.is-link .navbar-end .navbar-link:focus,.navbar.is-link .navbar-end .navbar-link:hover,.navbar.is-link .navbar-end .navbar-link.is-active{background-color:#2366d1;color:#fff}.navbar.is-link .navbar-start .navbar-link::after,.navbar.is-link .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-link .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-link .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-link .navbar-item.has-dropdown.is-active .navbar-link{background-color:#2366d1;color:#fff}.navbar.is-link .navbar-dropdown a.navbar-item.is-active{background-color:#3273dc;color:#fff}}.navbar.is-info{background-color:#3298dc;color:#fff}.navbar.is-info .navbar-brand>.navbar-item,.navbar.is-info .navbar-brand .navbar-link{color:#fff}.navbar.is-info .navbar-brand>a.navbar-item:focus,.navbar.is-info .navbar-brand>a.navbar-item:hover,.navbar.is-info .navbar-brand>a.navbar-item.is-active,.navbar.is-info .navbar-brand .navbar-link:focus,.navbar.is-info .navbar-brand .navbar-link:hover,.navbar.is-info .navbar-brand .navbar-link.is-active{background-color:#238cd1;color:#fff}.navbar.is-info .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-info .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-info .navbar-start>.navbar-item,.navbar.is-info .navbar-start .navbar-link,.navbar.is-info .navbar-end>.navbar-item,.navbar.is-info .navbar-end .navbar-link{color:#fff}.navbar.is-info .navbar-start>a.navbar-item:focus,.navbar.is-info .navbar-start>a.navbar-item:hover,.navbar.is-info .navbar-start>a.navbar-item.is-active,.navbar.is-info .navbar-start .navbar-link:focus,.navbar.is-info .navbar-start .navbar-link:hover,.navbar.is-info .navbar-start .navbar-link.is-active,.navbar.is-info .navbar-end>a.navbar-item:focus,.navbar.is-info .navbar-end>a.navbar-item:hover,.navbar.is-info .navbar-end>a.navbar-item.is-active,.navbar.is-info .navbar-end .navbar-link:focus,.navbar.is-info .navbar-end .navbar-link:hover,.navbar.is-info .navbar-end .navbar-link.is-active{background-color:#238cd1;color:#fff}.navbar.is-info .navbar-start .navbar-link::after,.navbar.is-info .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-info .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-info .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-info .navbar-item.has-dropdown.is-active .navbar-link{background-color:#238cd1;color:#fff}.navbar.is-info .navbar-dropdown a.navbar-item.is-active{background-color:#3298dc;color:#fff}}.navbar.is-success{background-color:#48c774;color:#fff}.navbar.is-success .navbar-brand>.navbar-item,.navbar.is-success .navbar-brand .navbar-link{color:#fff}.navbar.is-success .navbar-brand>a.navbar-item:focus,.navbar.is-success .navbar-brand>a.navbar-item:hover,.navbar.is-success .navbar-brand>a.navbar-item.is-active,.navbar.is-success .navbar-brand .navbar-link:focus,.navbar.is-success .navbar-brand .navbar-link:hover,.navbar.is-success .navbar-brand .navbar-link.is-active{background-color:#3abb67;color:#fff}.navbar.is-success .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-success .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-success .navbar-start>.navbar-item,.navbar.is-success .navbar-start .navbar-link,.navbar.is-success .navbar-end>.navbar-item,.navbar.is-success .navbar-end .navbar-link{color:#fff}.navbar.is-success .navbar-start>a.navbar-item:focus,.navbar.is-success .navbar-start>a.navbar-item:hover,.navbar.is-success .navbar-start>a.navbar-item.is-active,.navbar.is-success .navbar-start .navbar-link:focus,.navbar.is-success .navbar-start .navbar-link:hover,.navbar.is-success .navbar-start .navbar-link.is-active,.navbar.is-success .navbar-end>a.navbar-item:focus,.navbar.is-success .navbar-end>a.navbar-item:hover,.navbar.is-success .navbar-end>a.navbar-item.is-active,.navbar.is-success .navbar-end .navbar-link:focus,.navbar.is-success .navbar-end .navbar-link:hover,.navbar.is-success .navbar-end .navbar-link.is-active{background-color:#3abb67;color:#fff}.navbar.is-success .navbar-start .navbar-link::after,.navbar.is-success .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-success .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-success .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-success .navbar-item.has-dropdown.is-active .navbar-link{background-color:#3abb67;color:#fff}.navbar.is-success .navbar-dropdown a.navbar-item.is-active{background-color:#48c774;color:#fff}}.navbar.is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-brand>.navbar-item,.navbar.is-warning .navbar-brand .navbar-link{color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-brand>a.navbar-item:focus,.navbar.is-warning .navbar-brand>a.navbar-item:hover,.navbar.is-warning .navbar-brand>a.navbar-item.is-active,.navbar.is-warning .navbar-brand .navbar-link:focus,.navbar.is-warning .navbar-brand .navbar-link:hover,.navbar.is-warning .navbar-brand .navbar-link.is-active{background-color:#ffd83d;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-brand .navbar-link::after{border-color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-burger{color:rgba(0,0,0,.7)}@media screen and (min-width:1024px){.navbar.is-warning .navbar-start>.navbar-item,.navbar.is-warning .navbar-start .navbar-link,.navbar.is-warning .navbar-end>.navbar-item,.navbar.is-warning .navbar-end .navbar-link{color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-start>a.navbar-item:focus,.navbar.is-warning .navbar-start>a.navbar-item:hover,.navbar.is-warning .navbar-start>a.navbar-item.is-active,.navbar.is-warning .navbar-start .navbar-link:focus,.navbar.is-warning .navbar-start .navbar-link:hover,.navbar.is-warning .navbar-start .navbar-link.is-active,.navbar.is-warning .navbar-end>a.navbar-item:focus,.navbar.is-warning .navbar-end>a.navbar-item:hover,.navbar.is-warning .navbar-end>a.navbar-item.is-active,.navbar.is-warning .navbar-end .navbar-link:focus,.navbar.is-warning .navbar-end .navbar-link:hover,.navbar.is-warning .navbar-end .navbar-link.is-active{background-color:#ffd83d;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-start .navbar-link::after,.navbar.is-warning .navbar-end .navbar-link::after{border-color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-warning .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-warning .navbar-item.has-dropdown.is-active .navbar-link{background-color:#ffd83d;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-dropdown a.navbar-item.is-active{background-color:#ffdd57;color:rgba(0,0,0,.7)}}.navbar.is-danger{background-color:#f14668;color:#fff}.navbar.is-danger .navbar-brand>.navbar-item,.navbar.is-danger .navbar-brand .navbar-link{color:#fff}.navbar.is-danger .navbar-brand>a.navbar-item:focus,.navbar.is-danger .navbar-brand>a.navbar-item:hover,.navbar.is-danger .navbar-brand>a.navbar-item.is-active,.navbar.is-danger .navbar-brand .navbar-link:focus,.navbar.is-danger .navbar-brand .navbar-link:hover,.navbar.is-danger .navbar-brand .navbar-link.is-active{background-color:#ef2e55;color:#fff}.navbar.is-danger .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-danger .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-danger .navbar-start>.navbar-item,.navbar.is-danger .navbar-start .navbar-link,.navbar.is-danger .navbar-end>.navbar-item,.navbar.is-danger .navbar-end .navbar-link{color:#fff}.navbar.is-danger .navbar-start>a.navbar-item:focus,.navbar.is-danger .navbar-start>a.navbar-item:hover,.navbar.is-danger .navbar-start>a.navbar-item.is-active,.navbar.is-danger .navbar-start .navbar-link:focus,.navbar.is-danger .navbar-start .navbar-link:hover,.navbar.is-danger .navbar-start .navbar-link.is-active,.navbar.is-danger .navbar-end>a.navbar-item:focus,.navbar.is-danger .navbar-end>a.navbar-item:hover,.navbar.is-danger .navbar-end>a.navbar-item.is-active,.navbar.is-danger .navbar-end .navbar-link:focus,.navbar.is-danger .navbar-end .navbar-link:hover,.navbar.is-danger .navbar-end .navbar-link.is-active{background-color:#ef2e55;color:#fff}.navbar.is-danger .navbar-start .navbar-link::after,.navbar.is-danger .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-danger .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-danger .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-danger .navbar-item.has-dropdown.is-active .navbar-link{background-color:#ef2e55;color:#fff}.navbar.is-danger .navbar-dropdown a.navbar-item.is-active{background-color:#f14668;color:#fff}}.navbar>.container{align-items:stretch;display:flex;min-height:3.25rem;width:100%}.navbar.has-shadow{box-shadow:0 2px whitesmoke}.navbar.is-fixed-bottom,.navbar.is-fixed-top{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom{bottom:0}.navbar.is-fixed-bottom.has-shadow{box-shadow:0 -2px whitesmoke}.navbar.is-fixed-top{top:0}html.has-navbar-fixed-top,body.has-navbar-fixed-top{padding-top:3.25rem}html.has-navbar-fixed-bottom,body.has-navbar-fixed-bottom{padding-bottom:3.25rem}.navbar-brand,.navbar-tabs{align-items:stretch;display:flex;flex-shrink:0;min-height:3.25rem}.navbar-brand a.navbar-item:focus,.navbar-brand a.navbar-item:hover{background-color:transparent}.navbar-tabs{-webkit-overflow-scrolling:touch;max-width:100vw;overflow-x:auto;overflow-y:hidden}.navbar-burger{color:#4a4a4a;cursor:pointer;display:block;height:3.25rem;position:relative;width:3.25rem;margin-left:auto}.navbar-burger span{background-color:currentColor;display:block;height:1px;left:calc(50% - 8px);position:absolute;transform-origin:center;transition-duration:86ms;transition-property:background-color,opacity,transform;transition-timing-function:ease-out;width:16px}.navbar-burger span:nth-child(1){top:calc(50% - 6px)}.navbar-burger span:nth-child(2){top:calc(50% - 1px)}.navbar-burger span:nth-child(3){top:calc(50% + 4px)}.navbar-burger:hover{background-color:rgba(0,0,0,5%)}.navbar-burger.is-active span:nth-child(1){transform:translateY(5px)rotate(45deg)}.navbar-burger.is-active span:nth-child(2){opacity:0}.navbar-burger.is-active span:nth-child(3){transform:translateY(-5px)rotate(-45deg)}.navbar-menu{display:none}.navbar-item,.navbar-link{color:#4a4a4a;display:block;line-height:1.5;padding:.5rem .75rem;position:relative}.navbar-item .icon:only-child,.navbar-link .icon:only-child{margin-left:-.25rem;margin-right:-.25rem}a.navbar-item,.navbar-link{cursor:pointer}a.navbar-item:focus,a.navbar-item:focus-within,a.navbar-item:hover,a.navbar-item.is-active,.navbar-link:focus,.navbar-link:focus-within,.navbar-link:hover,.navbar-link.is-active{background-color:#fafafa;color:#3273dc}.navbar-item{display:block;flex-grow:0;flex-shrink:0}.navbar-item img{max-height:1.75rem}.navbar-item.has-dropdown{padding:0}.navbar-item.is-expanded{flex-grow:1;flex-shrink:1}.navbar-item.is-tab{border-bottom:1px solid transparent;min-height:3.25rem;padding-bottom:calc(.5rem - 1px)}.navbar-item.is-tab:focus,.navbar-item.is-tab:hover{background-color:transparent;border-bottom-color:#3273dc}.navbar-item.is-tab.is-active{background-color:transparent;border-bottom-color:#3273dc;border-bottom-style:solid;border-bottom-width:3px;color:#3273dc;padding-bottom:calc(.5rem - 3px)}.navbar-content{flex-grow:1;flex-shrink:1}.navbar-link:not(.is-arrowless){padding-right:2.5em}.navbar-link:not(.is-arrowless)::after{border-color:#3273dc;margin-top:-.375em;right:1.125em}.navbar-dropdown{font-size:.875rem;padding-bottom:.5rem;padding-top:.5rem}.navbar-dropdown .navbar-item{padding-left:1.5rem;padding-right:1.5rem}.navbar-divider{background-color:#f5f5f5;border:none;display:none;height:2px;margin:.5rem 0}@media screen and (max-width:1023px){.navbar>.container{display:block}.navbar-brand .navbar-item,.navbar-tabs .navbar-item{align-items:center;display:flex}.navbar-link::after{display:none}.navbar-menu{background-color:#fff;box-shadow:0 8px 16px rgba(10,10,10,.1);padding:.5rem 0}.navbar-menu.is-active{display:block}.navbar.is-fixed-bottom-touch,.navbar.is-fixed-top-touch{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom-touch{bottom:0}.navbar.is-fixed-bottom-touch.has-shadow{box-shadow:0 -2px 3px rgba(10,10,10,.1)}.navbar.is-fixed-top-touch{top:0}.navbar.is-fixed-top .navbar-menu,.navbar.is-fixed-top-touch .navbar-menu{-webkit-overflow-scrolling:touch;max-height:calc(100vh - 3.25rem);overflow:auto}html.has-navbar-fixed-top-touch,body.has-navbar-fixed-top-touch{padding-top:3.25rem}html.has-navbar-fixed-bottom-touch,body.has-navbar-fixed-bottom-touch{padding-bottom:3.25rem}}@media screen and (min-width:1024px){.navbar,.navbar-menu,.navbar-start,.navbar-end{align-items:stretch;display:flex}.navbar{min-height:3.25rem}.navbar.is-spaced{padding:1rem 2rem}.navbar.is-spaced .navbar-start,.navbar.is-spaced .navbar-end{align-items:center}.navbar.is-spaced a.navbar-item,.navbar.is-spaced .navbar-link{border-radius:4px}.navbar.is-transparent a.navbar-item:focus,.navbar.is-transparent a.navbar-item:hover,.navbar.is-transparent a.navbar-item.is-active,.navbar.is-transparent .navbar-link:focus,.navbar.is-transparent .navbar-link:hover,.navbar.is-transparent .navbar-link.is-active{background-color:transparent!important}.navbar.is-transparent .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-transparent .navbar-item.has-dropdown.is-hoverable:focus .navbar-link,.navbar.is-transparent .navbar-item.has-dropdown.is-hoverable:focus-within .navbar-link,.navbar.is-transparent .navbar-item.has-dropdown.is-hoverable:hover .navbar-link{background-color:transparent!important}.navbar.is-transparent .navbar-dropdown a.navbar-item:focus,.navbar.is-transparent .navbar-dropdown a.navbar-item:hover{background-color:#f5f5f5;color:#0a0a0a}.navbar.is-transparent .navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:#3273dc}.navbar-burger{display:none}.navbar-item,.navbar-link{align-items:center;display:flex}.navbar-item{display:flex}.navbar-item.has-dropdown{align-items:stretch}.navbar-item.has-dropdown-up .navbar-link::after{transform:rotate(135deg)translate(.25em,-.25em)}.navbar-item.has-dropdown-up .navbar-dropdown{border-bottom:2px solid #dbdbdb;border-radius:6px 6px 0 0;border-top:none;bottom:100%;box-shadow:0 -8px 8px rgba(10,10,10,.1);top:auto}.navbar-item.is-active .navbar-dropdown,.navbar-item.is-hoverable:focus .navbar-dropdown,.navbar-item.is-hoverable:focus-within .navbar-dropdown,.navbar-item.is-hoverable:hover .navbar-dropdown{display:block}.navbar.is-spaced .navbar-item.is-active .navbar-dropdown,.navbar-item.is-active .navbar-dropdown.is-boxed,.navbar.is-spaced .navbar-item.is-hoverable:focus .navbar-dropdown,.navbar-item.is-hoverable:focus .navbar-dropdown.is-boxed,.navbar.is-spaced .navbar-item.is-hoverable:focus-within .navbar-dropdown,.navbar-item.is-hoverable:focus-within .navbar-dropdown.is-boxed,.navbar.is-spaced .navbar-item.is-hoverable:hover .navbar-dropdown,.navbar-item.is-hoverable:hover .navbar-dropdown.is-boxed{opacity:1;pointer-events:auto;transform:translateY(0)}.navbar-menu{flex-grow:1;flex-shrink:0}.navbar-start{justify-content:flex-start;margin-right:auto}.navbar-end{justify-content:flex-end;margin-left:auto}.navbar-dropdown{background-color:#fff;border-bottom-left-radius:6px;border-bottom-right-radius:6px;border-top:2px solid #dbdbdb;box-shadow:0 8px 8px rgba(10,10,10,.1);display:none;font-size:.875rem;left:0;min-width:100%;position:absolute;top:100%;z-index:20}.navbar-dropdown .navbar-item{padding:.375rem 1rem;white-space:nowrap}.navbar-dropdown a.navbar-item{padding-right:3rem}.navbar-dropdown a.navbar-item:focus,.navbar-dropdown a.navbar-item:hover{background-color:#f5f5f5;color:#0a0a0a}.navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:#3273dc}.navbar.is-spaced .navbar-dropdown,.navbar-dropdown.is-boxed{border-radius:6px;border-top:none;box-shadow:0 8px 8px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1);display:block;opacity:0;pointer-events:none;top:calc(100% + (-4px));transform:translateY(-5px);transition-duration:86ms;transition-property:opacity,transform}.navbar-dropdown.is-right{left:auto;right:0}.navbar-divider{display:block}.navbar>.container .navbar-brand,.container>.navbar .navbar-brand{margin-left:-.75rem}.navbar>.container .navbar-menu,.container>.navbar .navbar-menu{margin-right:-.75rem}.navbar.is-fixed-bottom-desktop,.navbar.is-fixed-top-desktop{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom-desktop{bottom:0}.navbar.is-fixed-bottom-desktop.has-shadow{box-shadow:0 -2px 3px rgba(10,10,10,.1)}.navbar.is-fixed-top-desktop{top:0}html.has-navbar-fixed-top-desktop,body.has-navbar-fixed-top-desktop{padding-top:3.25rem}html.has-navbar-fixed-bottom-desktop,body.has-navbar-fixed-bottom-desktop{padding-bottom:3.25rem}html.has-spaced-navbar-fixed-top,body.has-spaced-navbar-fixed-top{padding-top:5.25rem}html.has-spaced-navbar-fixed-bottom,body.has-spaced-navbar-fixed-bottom{padding-bottom:5.25rem}a.navbar-item.is-active,.navbar-link.is-active{color:#0a0a0a}a.navbar-item.is-active:not(:focus):not(:hover),.navbar-link.is-active:not(:focus):not(:hover){background-color:transparent}.navbar-item.has-dropdown:focus .navbar-link,.navbar-item.has-dropdown:hover .navbar-link,.navbar-item.has-dropdown.is-active .navbar-link{background-color:#fafafa}}.hero.is-fullheight-with-navbar,#home>section.is-fullheight-with-navbar{min-height:calc(100vh - 3.25rem)}.pagination{font-size:1rem;margin:-.25rem}.pagination.is-small{font-size:.75rem}.pagination.is-medium{font-size:1.25rem}.pagination.is-large{font-size:1.5rem}.pagination.is-rounded .pagination-previous,.pagination.is-rounded .pagination-next{padding-left:1em;padding-right:1em;border-radius:290486px}.pagination.is-rounded .pagination-link{border-radius:290486px}.pagination,.pagination-list{align-items:center;display:flex;justify-content:center;text-align:center}.pagination-previous,.pagination-next,.pagination-link,.pagination-ellipsis{font-size:1em;justify-content:center;margin:.25rem;padding-left:.5em;padding-right:.5em;text-align:center}.pagination-previous,.pagination-next,.pagination-link{border-color:#dbdbdb;color:#363636;min-width:2.5em}.pagination-previous:hover,.pagination-next:hover,.pagination-link:hover{border-color:#b5b5b5;color:#363636}.pagination-previous:focus,.pagination-next:focus,.pagination-link:focus{border-color:#3273dc}.pagination-previous:active,.pagination-next:active,.pagination-link:active{box-shadow:inset 0 1px 2px rgba(10,10,10,.2)}.pagination-previous[disabled],.pagination-next[disabled],.pagination-link[disabled]{background-color:#dbdbdb;border-color:#dbdbdb;box-shadow:none;color:#7a7a7a;opacity:.5}.pagination-previous,.pagination-next{padding-left:.75em;padding-right:.75em;white-space:nowrap}.pagination-link.is-current{background-color:#3273dc;border-color:#3273dc;color:#fff}.pagination-ellipsis{color:#b5b5b5;pointer-events:none}.pagination-list{flex-wrap:wrap}@media screen and (max-width:768px){.pagination{flex-wrap:wrap}.pagination-previous,.pagination-next{flex-grow:1;flex-shrink:1}.pagination-list li{flex-grow:1;flex-shrink:1}}@media screen and (min-width:769px),print{.pagination-list{flex-grow:1;flex-shrink:1;justify-content:flex-start;order:1}.pagination-previous{order:2}.pagination-next{order:3}.pagination{justify-content:space-between}.pagination.is-centered .pagination-previous{order:1}.pagination.is-centered .pagination-list{justify-content:center;order:2}.pagination.is-centered .pagination-next{order:3}.pagination.is-right .pagination-previous{order:1}.pagination.is-right .pagination-next{order:2}.pagination.is-right .pagination-list{justify-content:flex-end;order:3}}.panel{border-radius:6px;box-shadow:0 .5em 1em -.125em rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,2%);font-size:1rem}.panel:not(:last-child){margin-bottom:1.5rem}.panel.is-white .panel-heading{background-color:#fff;color:#0a0a0a}.panel.is-white .panel-tabs a.is-active{border-bottom-color:#fff}.panel.is-white .panel-block.is-active .panel-icon{color:#fff}.panel.is-black .panel-heading{background-color:#0a0a0a;color:#fff}.panel.is-black .panel-tabs a.is-active{border-bottom-color:#0a0a0a}.panel.is-black .panel-block.is-active .panel-icon{color:#0a0a0a}.panel.is-light .panel-heading,#home>section.panel:nth-child(even) .panel-heading{background-color:#f5f5f5;color:rgba(0,0,0,.7)}.panel.is-light .panel-tabs a.is-active,#home>section.panel:nth-child(even) .panel-tabs a.is-active{border-bottom-color:#f5f5f5}.panel.is-light .panel-block.is-active .panel-icon,#home>section.panel:nth-child(even) .panel-block.is-active .panel-icon{color:#f5f5f5}.panel.is-dark .panel-heading{background-color:#363636;color:#fff}.panel.is-dark .panel-tabs a.is-active{border-bottom-color:#363636}.panel.is-dark .panel-block.is-active .panel-icon{color:#363636}.panel.is-primary .panel-heading{background-color:#c93312;color:#fff}.panel.is-primary .panel-tabs a.is-active{border-bottom-color:#c93312}.panel.is-primary .panel-block.is-active .panel-icon{color:#c93312}.panel.is-link .panel-heading{background-color:#3273dc;color:#fff}.panel.is-link .panel-tabs a.is-active{border-bottom-color:#3273dc}.panel.is-link .panel-block.is-active .panel-icon{color:#3273dc}.panel.is-info .panel-heading{background-color:#3298dc;color:#fff}.panel.is-info .panel-tabs a.is-active{border-bottom-color:#3298dc}.panel.is-info .panel-block.is-active .panel-icon{color:#3298dc}.panel.is-success .panel-heading{background-color:#48c774;color:#fff}.panel.is-success .panel-tabs a.is-active{border-bottom-color:#48c774}.panel.is-success .panel-block.is-active .panel-icon{color:#48c774}.panel.is-warning .panel-heading{background-color:#ffdd57;color:rgba(0,0,0,.7)}.panel.is-warning .panel-tabs a.is-active{border-bottom-color:#ffdd57}.panel.is-warning .panel-block.is-active .panel-icon{color:#ffdd57}.panel.is-danger .panel-heading{background-color:#f14668;color:#fff}.panel.is-danger .panel-tabs a.is-active{border-bottom-color:#f14668}.panel.is-danger .panel-block.is-active .panel-icon{color:#f14668}.panel-tabs:not(:last-child),.panel-block:not(:last-child){border-bottom:1px solid #ededed}.panel-heading{background-color:#ededed;border-radius:6px 6px 0 0;color:#363636;font-size:1.25em;font-weight:700;line-height:1.25;padding:.75em 1em}.panel-tabs{align-items:flex-end;display:flex;font-size:.875em;justify-content:center}.panel-tabs a{border-bottom:1px solid #dbdbdb;margin-bottom:-1px;padding:.5em}.panel-tabs a.is-active{border-bottom-color:#4a4a4a;color:#363636}.panel-list a{color:#4a4a4a}.panel-list a:hover{color:#3273dc}.panel-block{align-items:center;color:#363636;display:flex;justify-content:flex-start;padding:.5em .75em}.panel-block input[type=checkbox]{margin-right:.75em}.panel-block>.control{flex-grow:1;flex-shrink:1;width:100%}.panel-block.is-wrapped{flex-wrap:wrap}.panel-block.is-active{border-left-color:#3273dc;color:#363636}.panel-block.is-active .panel-icon{color:#3273dc}.panel-block:last-child{border-bottom-left-radius:6px;border-bottom-right-radius:6px}a.panel-block,label.panel-block{cursor:pointer}a.panel-block:hover,label.panel-block:hover{background-color:#f5f5f5}.panel-icon{display:inline-block;font-size:14px;height:1em;line-height:1em;text-align:center;vertical-align:top;width:1em;color:#7a7a7a;margin-right:.75em}.panel-icon .fa{font-size:inherit;line-height:inherit}.tabs{-webkit-overflow-scrolling:touch;align-items:stretch;display:flex;font-size:1rem;justify-content:space-between;overflow:hidden;overflow-x:auto;white-space:nowrap}.tabs a{align-items:center;border-bottom-color:#dbdbdb;border-bottom-style:solid;border-bottom-width:1px;color:#4a4a4a;display:flex;justify-content:center;margin-bottom:-1px;padding:.5em 1em;vertical-align:top}.tabs a:hover{border-bottom-color:#363636;color:#363636}.tabs li{display:block}.tabs li.is-active a{border-bottom-color:#3273dc;color:#3273dc}.tabs ul{align-items:center;border-bottom-color:#dbdbdb;border-bottom-style:solid;border-bottom-width:1px;display:flex;flex-grow:1;flex-shrink:0;justify-content:flex-start}.tabs ul.is-left{padding-right:.75em}.tabs ul.is-center{flex:none;justify-content:center;padding-left:.75em;padding-right:.75em}.tabs ul.is-right{justify-content:flex-end;padding-left:.75em}.tabs .icon:first-child{margin-right:.5em}.tabs .icon:last-child{margin-left:.5em}.tabs.is-centered ul{justify-content:center}.tabs.is-right ul{justify-content:flex-end}.tabs.is-boxed a{border:1px solid transparent;border-radius:4px 4px 0 0}.tabs.is-boxed a:hover{background-color:#f5f5f5;border-bottom-color:#dbdbdb}.tabs.is-boxed li.is-active a{background-color:#fff;border-color:#dbdbdb;border-bottom-color:transparent!important}.tabs.is-fullwidth li{flex-grow:1;flex-shrink:0}.tabs.is-toggle a{border-color:#dbdbdb;border-style:solid;border-width:1px;margin-bottom:0;position:relative}.tabs.is-toggle a:hover{background-color:#f5f5f5;border-color:#b5b5b5;z-index:2}.tabs.is-toggle li+li{margin-left:-1px}.tabs.is-toggle li:first-child a{border-radius:4px 0 0 4px}.tabs.is-toggle li:last-child a{border-radius:0 4px 4px 0}.tabs.is-toggle li.is-active a{background-color:#3273dc;border-color:#3273dc;color:#fff;z-index:1}.tabs.is-toggle ul{border-bottom:none}.tabs.is-toggle.is-toggle-rounded li:first-child a{border-bottom-left-radius:290486px;border-top-left-radius:290486px;padding-left:1.25em}.tabs.is-toggle.is-toggle-rounded li:last-child a{border-bottom-right-radius:290486px;border-top-right-radius:290486px;padding-right:1.25em}.tabs.is-small{font-size:.75rem}.tabs.is-medium{font-size:1.25rem}.tabs.is-large{font-size:1.5rem}.hero,#home>section{align-items:stretch;display:flex;flex-direction:column;justify-content:space-between}.hero .navbar,#home>section .navbar{background:0 0}.hero .tabs ul,#home>section .tabs ul{border-bottom:none}.hero.is-white,#home>section.is-white{background-color:#fff;color:#0a0a0a}.hero.is-white a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),#home>section.is-white a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-white strong,#home>section.is-white strong{color:inherit}.hero.is-white .title,#home>section.is-white .title{color:#0a0a0a}.hero.is-white .subtitle,#home>section.is-white .subtitle{color:rgba(10,10,10,.9)}.hero.is-white .subtitle a:not(.button),#home>section.is-white .subtitle a:not(.button),.hero.is-white .subtitle strong,#home>section.is-white .subtitle strong{color:#0a0a0a}@media screen and (max-width:1023px){.hero.is-white .navbar-menu,#home>section.is-white .navbar-menu{background-color:#fff}}.hero.is-white .navbar-item,#home>section.is-white .navbar-item,.hero.is-white .navbar-link,#home>section.is-white .navbar-link{color:rgba(10,10,10,.7)}.hero.is-white a.navbar-item:hover,#home>section.is-white a.navbar-item:hover,.hero.is-white a.navbar-item.is-active,#home>section.is-white a.navbar-item.is-active,.hero.is-white .navbar-link:hover,#home>section.is-white .navbar-link:hover,.hero.is-white .navbar-link.is-active,#home>section.is-white .navbar-link.is-active{background-color:#f2f2f2;color:#0a0a0a}.hero.is-white .tabs a,#home>section.is-white .tabs a{color:#0a0a0a;opacity:.9}.hero.is-white .tabs a:hover,#home>section.is-white .tabs a:hover{opacity:1}.hero.is-white .tabs li.is-active a,#home>section.is-white .tabs li.is-active a{opacity:1}.hero.is-white .tabs.is-boxed a,#home>section.is-white .tabs.is-boxed a,.hero.is-white .tabs.is-toggle a,#home>section.is-white .tabs.is-toggle a{color:#0a0a0a}.hero.is-white .tabs.is-boxed a:hover,#home>section.is-white .tabs.is-boxed a:hover,.hero.is-white .tabs.is-toggle a:hover,#home>section.is-white .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-white .tabs.is-boxed li.is-active a,#home>section.is-white .tabs.is-boxed li.is-active a,.hero.is-white .tabs.is-boxed li.is-active a:hover,.hero.is-white .tabs.is-toggle li.is-active a,#home>section.is-white .tabs.is-toggle li.is-active a,.hero.is-white .tabs.is-toggle li.is-active a:hover{background-color:#0a0a0a;border-color:#0a0a0a;color:#fff}.hero.is-white.is-bold,#home>section.is-white.is-bold{background-image:linear-gradient(141deg,#e8e3e4 0%,white 71%,white 100%)}@media screen and (max-width:768px){.hero.is-white.is-bold .navbar-menu,#home>section.is-white.is-bold .navbar-menu{background-image:linear-gradient(141deg,#e8e3e4 0%,white 71%,white 100%)}}.hero.is-black,#home>section.is-black{background-color:#0a0a0a;color:#fff}.hero.is-black a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),#home>section.is-black a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-black strong,#home>section.is-black strong{color:inherit}.hero.is-black .title,#home>section.is-black .title{color:#fff}.hero.is-black .subtitle,#home>section.is-black .subtitle{color:rgba(255,255,255,.9)}.hero.is-black .subtitle a:not(.button),#home>section.is-black .subtitle a:not(.button),.hero.is-black .subtitle strong,#home>section.is-black .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-black .navbar-menu,#home>section.is-black .navbar-menu{background-color:#0a0a0a}}.hero.is-black .navbar-item,#home>section.is-black .navbar-item,.hero.is-black .navbar-link,#home>section.is-black .navbar-link{color:rgba(255,255,255,.7)}.hero.is-black a.navbar-item:hover,#home>section.is-black a.navbar-item:hover,.hero.is-black a.navbar-item.is-active,#home>section.is-black a.navbar-item.is-active,.hero.is-black .navbar-link:hover,#home>section.is-black .navbar-link:hover,.hero.is-black .navbar-link.is-active,#home>section.is-black .navbar-link.is-active{background-color:#000;color:#fff}.hero.is-black .tabs a,#home>section.is-black .tabs a{color:#fff;opacity:.9}.hero.is-black .tabs a:hover,#home>section.is-black .tabs a:hover{opacity:1}.hero.is-black .tabs li.is-active a,#home>section.is-black .tabs li.is-active a{opacity:1}.hero.is-black .tabs.is-boxed a,#home>section.is-black .tabs.is-boxed a,.hero.is-black .tabs.is-toggle a,#home>section.is-black .tabs.is-toggle a{color:#fff}.hero.is-black .tabs.is-boxed a:hover,#home>section.is-black .tabs.is-boxed a:hover,.hero.is-black .tabs.is-toggle a:hover,#home>section.is-black .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-black .tabs.is-boxed li.is-active a,#home>section.is-black .tabs.is-boxed li.is-active a,.hero.is-black .tabs.is-boxed li.is-active a:hover,.hero.is-black .tabs.is-toggle li.is-active a,#home>section.is-black .tabs.is-toggle li.is-active a,.hero.is-black .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#0a0a0a}.hero.is-black.is-bold,#home>section.is-black.is-bold{background-image:linear-gradient(141deg,black 0%,#0a0a0a 71%,#181616 100%)}@media screen and (max-width:768px){.hero.is-black.is-bold .navbar-menu,#home>section.is-black.is-bold .navbar-menu{background-image:linear-gradient(141deg,black 0%,#0a0a0a 71%,#181616 100%)}}.hero.is-light,#home>section.is-light,#home>section:nth-child(even){background-color:#f5f5f5;color:rgba(0,0,0,.7)}.hero.is-light a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),#home>section.is-light a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),#home>section:nth-child(even) a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-light strong,#home>section.is-light strong,#home>section:nth-child(even) strong{color:inherit}.hero.is-light .title,#home>section.is-light .title,#home>section:nth-child(even) .title{color:rgba(0,0,0,.7)}.hero.is-light .subtitle,#home>section.is-light .subtitle,#home>section:nth-child(even) .subtitle{color:rgba(0,0,0,.9)}.hero.is-light .subtitle a:not(.button),#home>section.is-light .subtitle a:not(.button),#home>section:nth-child(even) .subtitle a:not(.button),.hero.is-light .subtitle strong,#home>section.is-light .subtitle strong,#home>section:nth-child(even) .subtitle strong{color:rgba(0,0,0,.7)}@media screen and (max-width:1023px){.hero.is-light .navbar-menu,#home>section.is-light .navbar-menu,#home>section:nth-child(even) .navbar-menu{background-color:#f5f5f5}}.hero.is-light .navbar-item,#home>section.is-light .navbar-item,#home>section:nth-child(even) .navbar-item,.hero.is-light .navbar-link,#home>section.is-light .navbar-link,#home>section:nth-child(even) .navbar-link{color:rgba(0,0,0,.7)}.hero.is-light a.navbar-item:hover,#home>section.is-light a.navbar-item:hover,#home>section:nth-child(even) a.navbar-item:hover,.hero.is-light a.navbar-item.is-active,#home>section.is-light a.navbar-item.is-active,#home>section:nth-child(even) a.navbar-item.is-active,.hero.is-light .navbar-link:hover,#home>section.is-light .navbar-link:hover,#home>section:nth-child(even) .navbar-link:hover,.hero.is-light .navbar-link.is-active,#home>section.is-light .navbar-link.is-active,#home>section:nth-child(even) .navbar-link.is-active{background-color:#e8e8e8;color:rgba(0,0,0,.7)}.hero.is-light .tabs a,#home>section.is-light .tabs a,#home>section:nth-child(even) .tabs a{color:rgba(0,0,0,.7);opacity:.9}.hero.is-light .tabs a:hover,#home>section.is-light .tabs a:hover,#home>section:nth-child(even) .tabs a:hover{opacity:1}.hero.is-light .tabs li.is-active a,#home>section.is-light .tabs li.is-active a,#home>section:nth-child(even) .tabs li.is-active a{opacity:1}.hero.is-light .tabs.is-boxed a,#home>section.is-light .tabs.is-boxed a,#home>section:nth-child(even) .tabs.is-boxed a,.hero.is-light .tabs.is-toggle a,#home>section.is-light .tabs.is-toggle a,#home>section:nth-child(even) .tabs.is-toggle a{color:rgba(0,0,0,.7)}.hero.is-light .tabs.is-boxed a:hover,#home>section.is-light .tabs.is-boxed a:hover,#home>section:nth-child(even) .tabs.is-boxed a:hover,.hero.is-light .tabs.is-toggle a:hover,#home>section.is-light .tabs.is-toggle a:hover,#home>section:nth-child(even) .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-light .tabs.is-boxed li.is-active a,#home>section.is-light .tabs.is-boxed li.is-active a,#home>section:nth-child(even) .tabs.is-boxed li.is-active a,.hero.is-light .tabs.is-boxed li.is-active a:hover,.hero.is-light .tabs.is-toggle li.is-active a,#home>section.is-light .tabs.is-toggle li.is-active a,#home>section:nth-child(even) .tabs.is-toggle li.is-active a,.hero.is-light .tabs.is-toggle li.is-active a:hover{background-color:rgba(0,0,0,.7);border-color:rgba(0,0,0,.7);color:#f5f5f5}.hero.is-light.is-bold,#home>section.is-light.is-bold,#home>section.is-bold:nth-child(even){background-image:linear-gradient(141deg,#dfd8d9 0%,whitesmoke 71%,white 100%)}@media screen and (max-width:768px){.hero.is-light.is-bold .navbar-menu,#home>section.is-light.is-bold .navbar-menu,#home>section.is-bold:nth-child(even) .navbar-menu{background-image:linear-gradient(141deg,#dfd8d9 0%,whitesmoke 71%,white 100%)}}.hero.is-dark,#home>section.is-dark{background-color:#363636;color:#fff}.hero.is-dark a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),#home>section.is-dark a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-dark strong,#home>section.is-dark strong{color:inherit}.hero.is-dark .title,#home>section.is-dark .title{color:#fff}.hero.is-dark .subtitle,#home>section.is-dark .subtitle{color:rgba(255,255,255,.9)}.hero.is-dark .subtitle a:not(.button),#home>section.is-dark .subtitle a:not(.button),.hero.is-dark .subtitle strong,#home>section.is-dark .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-dark .navbar-menu,#home>section.is-dark .navbar-menu{background-color:#363636}}.hero.is-dark .navbar-item,#home>section.is-dark .navbar-item,.hero.is-dark .navbar-link,#home>section.is-dark .navbar-link{color:rgba(255,255,255,.7)}.hero.is-dark a.navbar-item:hover,#home>section.is-dark a.navbar-item:hover,.hero.is-dark a.navbar-item.is-active,#home>section.is-dark a.navbar-item.is-active,.hero.is-dark .navbar-link:hover,#home>section.is-dark .navbar-link:hover,.hero.is-dark .navbar-link.is-active,#home>section.is-dark .navbar-link.is-active{background-color:#292929;color:#fff}.hero.is-dark .tabs a,#home>section.is-dark .tabs a{color:#fff;opacity:.9}.hero.is-dark .tabs a:hover,#home>section.is-dark .tabs a:hover{opacity:1}.hero.is-dark .tabs li.is-active a,#home>section.is-dark .tabs li.is-active a{opacity:1}.hero.is-dark .tabs.is-boxed a,#home>section.is-dark .tabs.is-boxed a,.hero.is-dark .tabs.is-toggle a,#home>section.is-dark .tabs.is-toggle a{color:#fff}.hero.is-dark .tabs.is-boxed a:hover,#home>section.is-dark .tabs.is-boxed a:hover,.hero.is-dark .tabs.is-toggle a:hover,#home>section.is-dark .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-dark .tabs.is-boxed li.is-active a,#home>section.is-dark .tabs.is-boxed li.is-active a,.hero.is-dark .tabs.is-boxed li.is-active a:hover,.hero.is-dark .tabs.is-toggle li.is-active a,#home>section.is-dark .tabs.is-toggle li.is-active a,.hero.is-dark .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#363636}.hero.is-dark.is-bold,#home>section.is-dark.is-bold{background-image:linear-gradient(141deg,#1f191a 0%,#363636 71%,#46403f 100%)}@media screen and (max-width:768px){.hero.is-dark.is-bold .navbar-menu,#home>section.is-dark.is-bold .navbar-menu{background-image:linear-gradient(141deg,#1f191a 0%,#363636 71%,#46403f 100%)}}.hero.is-primary,#home>section.is-primary{background-color:#c93312;color:#fff}.hero.is-primary a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),#home>section.is-primary a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-primary strong,#home>section.is-primary strong{color:inherit}.hero.is-primary .title,#home>section.is-primary .title{color:#fff}.hero.is-primary .subtitle,#home>section.is-primary .subtitle{color:rgba(255,255,255,.9)}.hero.is-primary .subtitle a:not(.button),#home>section.is-primary .subtitle a:not(.button),.hero.is-primary .subtitle strong,#home>section.is-primary .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-primary .navbar-menu,#home>section.is-primary .navbar-menu{background-color:#c93312}}.hero.is-primary .navbar-item,#home>section.is-primary .navbar-item,.hero.is-primary .navbar-link,#home>section.is-primary .navbar-link{color:rgba(255,255,255,.7)}.hero.is-primary a.navbar-item:hover,#home>section.is-primary a.navbar-item:hover,.hero.is-primary a.navbar-item.is-active,#home>section.is-primary a.navbar-item.is-active,.hero.is-primary .navbar-link:hover,#home>section.is-primary .navbar-link:hover,.hero.is-primary .navbar-link.is-active,#home>section.is-primary .navbar-link.is-active{background-color:#b22d10;color:#fff}.hero.is-primary .tabs a,#home>section.is-primary .tabs a{color:#fff;opacity:.9}.hero.is-primary .tabs a:hover,#home>section.is-primary .tabs a:hover{opacity:1}.hero.is-primary .tabs li.is-active a,#home>section.is-primary .tabs li.is-active a{opacity:1}.hero.is-primary .tabs.is-boxed a,#home>section.is-primary .tabs.is-boxed a,.hero.is-primary .tabs.is-toggle a,#home>section.is-primary .tabs.is-toggle a{color:#fff}.hero.is-primary .tabs.is-boxed a:hover,#home>section.is-primary .tabs.is-boxed a:hover,.hero.is-primary .tabs.is-toggle a:hover,#home>section.is-primary .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-primary .tabs.is-boxed li.is-active a,#home>section.is-primary .tabs.is-boxed li.is-active a,.hero.is-primary .tabs.is-boxed li.is-active a:hover,.hero.is-primary .tabs.is-toggle li.is-active a,#home>section.is-primary .tabs.is-toggle li.is-active a,.hero.is-primary .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#c93312}.hero.is-primary.is-bold,#home>section.is-primary.is-bold{background-image:linear-gradient(141deg,#a30805 0%,#C93312 71%,#e7590e 100%)}@media screen and (max-width:768px){.hero.is-primary.is-bold .navbar-menu,#home>section.is-primary.is-bold .navbar-menu{background-image:linear-gradient(141deg,#a30805 0%,#C93312 71%,#e7590e 100%)}}.hero.is-link,#home>section.is-link{background-color:#3273dc;color:#fff}.hero.is-link a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),#home>section.is-link a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-link strong,#home>section.is-link strong{color:inherit}.hero.is-link .title,#home>section.is-link .title{color:#fff}.hero.is-link .subtitle,#home>section.is-link .subtitle{color:rgba(255,255,255,.9)}.hero.is-link .subtitle a:not(.button),#home>section.is-link .subtitle a:not(.button),.hero.is-link .subtitle strong,#home>section.is-link .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-link .navbar-menu,#home>section.is-link .navbar-menu{background-color:#3273dc}}.hero.is-link .navbar-item,#home>section.is-link .navbar-item,.hero.is-link .navbar-link,#home>section.is-link .navbar-link{color:rgba(255,255,255,.7)}.hero.is-link a.navbar-item:hover,#home>section.is-link a.navbar-item:hover,.hero.is-link a.navbar-item.is-active,#home>section.is-link a.navbar-item.is-active,.hero.is-link .navbar-link:hover,#home>section.is-link .navbar-link:hover,.hero.is-link .navbar-link.is-active,#home>section.is-link .navbar-link.is-active{background-color:#2366d1;color:#fff}.hero.is-link .tabs a,#home>section.is-link .tabs a{color:#fff;opacity:.9}.hero.is-link .tabs a:hover,#home>section.is-link .tabs a:hover{opacity:1}.hero.is-link .tabs li.is-active a,#home>section.is-link .tabs li.is-active a{opacity:1}.hero.is-link .tabs.is-boxed a,#home>section.is-link .tabs.is-boxed a,.hero.is-link .tabs.is-toggle a,#home>section.is-link .tabs.is-toggle a{color:#fff}.hero.is-link .tabs.is-boxed a:hover,#home>section.is-link .tabs.is-boxed a:hover,.hero.is-link .tabs.is-toggle a:hover,#home>section.is-link .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-link .tabs.is-boxed li.is-active a,#home>section.is-link .tabs.is-boxed li.is-active a,.hero.is-link .tabs.is-boxed li.is-active a:hover,.hero.is-link .tabs.is-toggle li.is-active a,#home>section.is-link .tabs.is-toggle li.is-active a,.hero.is-link .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#3273dc}.hero.is-link.is-bold,#home>section.is-link.is-bold{background-image:linear-gradient(141deg,#1577c6 0%,#3273dc 71%,#4366e5 100%)}@media screen and (max-width:768px){.hero.is-link.is-bold .navbar-menu,#home>section.is-link.is-bold .navbar-menu{background-image:linear-gradient(141deg,#1577c6 0%,#3273dc 71%,#4366e5 100%)}}.hero.is-info,#home>section.is-info{background-color:#3298dc;color:#fff}.hero.is-info a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),#home>section.is-info a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-info strong,#home>section.is-info strong{color:inherit}.hero.is-info .title,#home>section.is-info .title{color:#fff}.hero.is-info .subtitle,#home>section.is-info .subtitle{color:rgba(255,255,255,.9)}.hero.is-info .subtitle a:not(.button),#home>section.is-info .subtitle a:not(.button),.hero.is-info .subtitle strong,#home>section.is-info .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-info .navbar-menu,#home>section.is-info .navbar-menu{background-color:#3298dc}}.hero.is-info .navbar-item,#home>section.is-info .navbar-item,.hero.is-info .navbar-link,#home>section.is-info .navbar-link{color:rgba(255,255,255,.7)}.hero.is-info a.navbar-item:hover,#home>section.is-info a.navbar-item:hover,.hero.is-info a.navbar-item.is-active,#home>section.is-info a.navbar-item.is-active,.hero.is-info .navbar-link:hover,#home>section.is-info .navbar-link:hover,.hero.is-info .navbar-link.is-active,#home>section.is-info .navbar-link.is-active{background-color:#238cd1;color:#fff}.hero.is-info .tabs a,#home>section.is-info .tabs a{color:#fff;opacity:.9}.hero.is-info .tabs a:hover,#home>section.is-info .tabs a:hover{opacity:1}.hero.is-info .tabs li.is-active a,#home>section.is-info .tabs li.is-active a{opacity:1}.hero.is-info .tabs.is-boxed a,#home>section.is-info .tabs.is-boxed a,.hero.is-info .tabs.is-toggle a,#home>section.is-info .tabs.is-toggle a{color:#fff}.hero.is-info .tabs.is-boxed a:hover,#home>section.is-info .tabs.is-boxed a:hover,.hero.is-info .tabs.is-toggle a:hover,#home>section.is-info .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-info .tabs.is-boxed li.is-active a,#home>section.is-info .tabs.is-boxed li.is-active a,.hero.is-info .tabs.is-boxed li.is-active a:hover,.hero.is-info .tabs.is-toggle li.is-active a,#home>section.is-info .tabs.is-toggle li.is-active a,.hero.is-info .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#3298dc}.hero.is-info.is-bold,#home>section.is-info.is-bold{background-image:linear-gradient(141deg,#159dc6 0%,#3298dc 71%,#4389e5 100%)}@media screen and (max-width:768px){.hero.is-info.is-bold .navbar-menu,#home>section.is-info.is-bold .navbar-menu{background-image:linear-gradient(141deg,#159dc6 0%,#3298dc 71%,#4389e5 100%)}}.hero.is-success,#home>section.is-success{background-color:#48c774;color:#fff}.hero.is-success a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),#home>section.is-success a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-success strong,#home>section.is-success strong{color:inherit}.hero.is-success .title,#home>section.is-success .title{color:#fff}.hero.is-success .subtitle,#home>section.is-success .subtitle{color:rgba(255,255,255,.9)}.hero.is-success .subtitle a:not(.button),#home>section.is-success .subtitle a:not(.button),.hero.is-success .subtitle strong,#home>section.is-success .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-success .navbar-menu,#home>section.is-success .navbar-menu{background-color:#48c774}}.hero.is-success .navbar-item,#home>section.is-success .navbar-item,.hero.is-success .navbar-link,#home>section.is-success .navbar-link{color:rgba(255,255,255,.7)}.hero.is-success a.navbar-item:hover,#home>section.is-success a.navbar-item:hover,.hero.is-success a.navbar-item.is-active,#home>section.is-success a.navbar-item.is-active,.hero.is-success .navbar-link:hover,#home>section.is-success .navbar-link:hover,.hero.is-success .navbar-link.is-active,#home>section.is-success .navbar-link.is-active{background-color:#3abb67;color:#fff}.hero.is-success .tabs a,#home>section.is-success .tabs a{color:#fff;opacity:.9}.hero.is-success .tabs a:hover,#home>section.is-success .tabs a:hover{opacity:1}.hero.is-success .tabs li.is-active a,#home>section.is-success .tabs li.is-active a{opacity:1}.hero.is-success .tabs.is-boxed a,#home>section.is-success .tabs.is-boxed a,.hero.is-success .tabs.is-toggle a,#home>section.is-success .tabs.is-toggle a{color:#fff}.hero.is-success .tabs.is-boxed a:hover,#home>section.is-success .tabs.is-boxed a:hover,.hero.is-success .tabs.is-toggle a:hover,#home>section.is-success .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-success .tabs.is-boxed li.is-active a,#home>section.is-success .tabs.is-boxed li.is-active a,.hero.is-success .tabs.is-boxed li.is-active a:hover,.hero.is-success .tabs.is-toggle li.is-active a,#home>section.is-success .tabs.is-toggle li.is-active a,.hero.is-success .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#48c774}.hero.is-success.is-bold,#home>section.is-success.is-bold{background-image:linear-gradient(141deg,#29b342 0%,#48c774 71%,#56d296 100%)}@media screen and (max-width:768px){.hero.is-success.is-bold .navbar-menu,#home>section.is-success.is-bold .navbar-menu{background-image:linear-gradient(141deg,#29b342 0%,#48c774 71%,#56d296 100%)}}.hero.is-warning,#home>section.is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.hero.is-warning a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),#home>section.is-warning a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-warning strong,#home>section.is-warning strong{color:inherit}.hero.is-warning .title,#home>section.is-warning .title{color:rgba(0,0,0,.7)}.hero.is-warning .subtitle,#home>section.is-warning .subtitle{color:rgba(0,0,0,.9)}.hero.is-warning .subtitle a:not(.button),#home>section.is-warning .subtitle a:not(.button),.hero.is-warning .subtitle strong,#home>section.is-warning .subtitle strong{color:rgba(0,0,0,.7)}@media screen and (max-width:1023px){.hero.is-warning .navbar-menu,#home>section.is-warning .navbar-menu{background-color:#ffdd57}}.hero.is-warning .navbar-item,#home>section.is-warning .navbar-item,.hero.is-warning .navbar-link,#home>section.is-warning .navbar-link{color:rgba(0,0,0,.7)}.hero.is-warning a.navbar-item:hover,#home>section.is-warning a.navbar-item:hover,.hero.is-warning a.navbar-item.is-active,#home>section.is-warning a.navbar-item.is-active,.hero.is-warning .navbar-link:hover,#home>section.is-warning .navbar-link:hover,.hero.is-warning .navbar-link.is-active,#home>section.is-warning .navbar-link.is-active{background-color:#ffd83d;color:rgba(0,0,0,.7)}.hero.is-warning .tabs a,#home>section.is-warning .tabs a{color:rgba(0,0,0,.7);opacity:.9}.hero.is-warning .tabs a:hover,#home>section.is-warning .tabs a:hover{opacity:1}.hero.is-warning .tabs li.is-active a,#home>section.is-warning .tabs li.is-active a{opacity:1}.hero.is-warning .tabs.is-boxed a,#home>section.is-warning .tabs.is-boxed a,.hero.is-warning .tabs.is-toggle a,#home>section.is-warning .tabs.is-toggle a{color:rgba(0,0,0,.7)}.hero.is-warning .tabs.is-boxed a:hover,#home>section.is-warning .tabs.is-boxed a:hover,.hero.is-warning .tabs.is-toggle a:hover,#home>section.is-warning .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-warning .tabs.is-boxed li.is-active a,#home>section.is-warning .tabs.is-boxed li.is-active a,.hero.is-warning .tabs.is-boxed li.is-active a:hover,.hero.is-warning .tabs.is-toggle li.is-active a,#home>section.is-warning .tabs.is-toggle li.is-active a,.hero.is-warning .tabs.is-toggle li.is-active a:hover{background-color:rgba(0,0,0,.7);border-color:rgba(0,0,0,.7);color:#ffdd57}.hero.is-warning.is-bold,#home>section.is-warning.is-bold{background-image:linear-gradient(141deg,#ffaf24 0%,#ffdd57 71%,#fffa70 100%)}@media screen and (max-width:768px){.hero.is-warning.is-bold .navbar-menu,#home>section.is-warning.is-bold .navbar-menu{background-image:linear-gradient(141deg,#ffaf24 0%,#ffdd57 71%,#fffa70 100%)}}.hero.is-danger,#home>section.is-danger{background-color:#f14668;color:#fff}.hero.is-danger a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),#home>section.is-danger a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-danger strong,#home>section.is-danger strong{color:inherit}.hero.is-danger .title,#home>section.is-danger .title{color:#fff}.hero.is-danger .subtitle,#home>section.is-danger .subtitle{color:rgba(255,255,255,.9)}.hero.is-danger .subtitle a:not(.button),#home>section.is-danger .subtitle a:not(.button),.hero.is-danger .subtitle strong,#home>section.is-danger .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-danger .navbar-menu,#home>section.is-danger .navbar-menu{background-color:#f14668}}.hero.is-danger .navbar-item,#home>section.is-danger .navbar-item,.hero.is-danger .navbar-link,#home>section.is-danger .navbar-link{color:rgba(255,255,255,.7)}.hero.is-danger a.navbar-item:hover,#home>section.is-danger a.navbar-item:hover,.hero.is-danger a.navbar-item.is-active,#home>section.is-danger a.navbar-item.is-active,.hero.is-danger .navbar-link:hover,#home>section.is-danger .navbar-link:hover,.hero.is-danger .navbar-link.is-active,#home>section.is-danger .navbar-link.is-active{background-color:#ef2e55;color:#fff}.hero.is-danger .tabs a,#home>section.is-danger .tabs a{color:#fff;opacity:.9}.hero.is-danger .tabs a:hover,#home>section.is-danger .tabs a:hover{opacity:1}.hero.is-danger .tabs li.is-active a,#home>section.is-danger .tabs li.is-active a{opacity:1}.hero.is-danger .tabs.is-boxed a,#home>section.is-danger .tabs.is-boxed a,.hero.is-danger .tabs.is-toggle a,#home>section.is-danger .tabs.is-toggle a{color:#fff}.hero.is-danger .tabs.is-boxed a:hover,#home>section.is-danger .tabs.is-boxed a:hover,.hero.is-danger .tabs.is-toggle a:hover,#home>section.is-danger .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-danger .tabs.is-boxed li.is-active a,#home>section.is-danger .tabs.is-boxed li.is-active a,.hero.is-danger .tabs.is-boxed li.is-active a:hover,.hero.is-danger .tabs.is-toggle li.is-active a,#home>section.is-danger .tabs.is-toggle li.is-active a,.hero.is-danger .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#f14668}.hero.is-danger.is-bold,#home>section.is-danger.is-bold{background-image:linear-gradient(141deg,#fa0a62 0%,#f14668 71%,#f7595f 100%)}@media screen and (max-width:768px){.hero.is-danger.is-bold .navbar-menu,#home>section.is-danger.is-bold .navbar-menu{background-image:linear-gradient(141deg,#fa0a62 0%,#f14668 71%,#f7595f 100%)}}.hero.is-small .hero-body,#home>section.is-small .hero-body{padding-bottom:1.5rem;padding-top:1.5rem}@media screen and (min-width:769px),print{.hero.is-medium .hero-body,#home>section.is-medium .hero-body{padding-bottom:9rem;padding-top:9rem}}@media screen and (min-width:769px),print{.hero.is-large .hero-body,#home>section.is-large .hero-body{padding-bottom:18rem;padding-top:18rem}}.hero.is-halfheight .hero-body,#home>section.is-halfheight .hero-body,.hero.is-fullheight .hero-body,#home>section.is-fullheight .hero-body,.hero.is-fullheight-with-navbar .hero-body,#home>section.is-fullheight-with-navbar .hero-body{align-items:center;display:flex}.hero.is-halfheight .hero-body>.container,#home>section.is-halfheight .hero-body>.container,.hero.is-fullheight .hero-body>.container,#home>section.is-fullheight .hero-body>.container,.hero.is-fullheight-with-navbar .hero-body>.container,#home>section.is-fullheight-with-navbar .hero-body>.container{flex-grow:1;flex-shrink:1}.hero.is-halfheight,#home>section.is-halfheight{min-height:50vh}.hero.is-fullheight,#home>section.is-fullheight{min-height:100vh}.hero-video{overflow:hidden}.hero-video video{left:50%;min-height:100%;min-width:100%;position:absolute;top:50%;transform:translate3d(-50%,-50%,0)}.hero-video.is-transparent{opacity:.3}@media screen and (max-width:768px){.hero-video{display:none}}.hero-buttons{margin-top:1.5rem}@media screen and (max-width:768px){.hero-buttons .button{display:flex}.hero-buttons .button:not(:last-child){margin-bottom:.75rem}}@media screen and (min-width:769px),print{.hero-buttons{display:flex;justify-content:center}.hero-buttons .button:not(:last-child){margin-right:1.5rem}}.hero-head,.hero-foot{flex-grow:0;flex-shrink:0}.hero-body{flex-grow:1;flex-shrink:0;padding:3rem 1.5rem}.section{padding:3rem 1.5rem}@media screen and (min-width:1024px){.section.is-medium{padding:9rem 1.5rem}.section.is-large{padding:18rem 1.5rem}}.footer{background-color:#fafafa;padding:3rem 1.5rem 6rem}/*!*\nmitmproxy website | MIT License | github.com/mitmproxy/www*/.has-vcenter-contents,#home .column{display:flex;flex-direction:column;justify-content:center}html{scroll-behavior:smooth}@media screen and (min-width:769px),print{#intro .columns{flex-direction:row-reverse}}#intro #definition{font-style:normal;font-weight:700}#intro #install{transition:all 250ms ease-in-out;overflow:hidden}#intro #install>*{margin-bottom:.5rem}.feature h2{margin:0 0 1.5rem!important}@media screen and (min-width:769px),print{.feature .column{flex:none;margin:0 2%;width:35%}.feature:nth-child(even) .columns{flex-direction:row-reverse}}.sample-code>.filename{padding-left:5px;font-size:.75rem;background-color:#586e75;color:#fdf6e3;border-top-left-radius:3px;border-top-right-radius:3px}.sample-code>.filename+pre{border-top-width:0;border-top-left-radius:0;border-top-right-radius:0}.sample-code>pre{background-color:#fdf6e3;border-radius:3px;border:solid #93a1a1 1px}.sample-code .import{color:#dc322f}.sample-code .keyword{color:#859900}.sample-code .method{color:#268bd2}.sample-code .str{color:#2aa198}.sample-code .num{color:#d33682}.sample-code .comment{color:#93a1a1}.shell-command{border-radius:3px;font-size:1.25rem;white-space:initial;background:#363636;color:#fff!important;border-width:0;padding:.25rem .75rem}.shell-command code{padding:.3rem;display:inline-block}.shell-command .copy{padding:.3rem 1.5rem;float:right;border-radius:2px;cursor:pointer;color:#c93312}.shell-command .copy:hover{background-color:#c93312;color:#fff!important}.shell-command .copy:hover.has-text-success{background-color:#48c774}.sponsors img{margin-right:.4em;vertical-align:middle;max-width:48px;max-height:48px;transition:all 100ms ease-in-out}.sponsors img:hover{transform:scale(1.1)}.blog{max-width:700px}.blog article{margin:4rem auto 2rem;font-size:1.1rem}.blog article h1>a{color:#363636}.blog article>section{margin-bottom:1rem}.blog article .subtitle{color:gray;margin-top:-1rem!important}.blog article figure{margin:1rem 2rem!important}" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 0.03475300036370754, + "receive": 0.029315997380763292 + }, + "_fetchType": "Memory Cache" + }, + { + "pageref": "page_0", + "startedDateTime": "2023-03-30T00:13:32.552Z", + "time": 0.0736030051484704, + "request": { + "method": "GET", + "url": "https://mitmproxy.org/logo-navbar.png", + "httpVersion": "", + "cookies": [], + "headers": [], + "queryString": [], + "headersSize": -1, + "bodySize": -1 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "image/png" + }, + { + "name": "Last-Modified", + "value": "Sat, 04 Mar 2023 16:01:11 GMT" + }, + { + "name": "Age", + "value": "53839" + }, + { + "name": "Via", + "value": "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + }, + { + "name": "Date", + "value": "Wed, 29 Mar 2023 09:06:44 GMT" + }, + { + "name": "Content-Length", + "value": "2145" + }, + { + "name": "ETag", + "value": "\"fae2ae3cb7832bd9fbd0f12e08e185ee\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "x-amz-cf-id", + "value": "IW86a_cg24ojMavKL5JYn8hlQ2M4VVVCXzsSkL-HobMhQ9ihGmRiVQ==" + }, + { + "name": "Alt-Svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + } + ], + "content": { + "size": 0, + "compression": 0, + "mimeType": "image/png", + "text": "iVBORw0KGgoAAAANSUhEUgAAANwAAAAyCAMAAAAAykVBAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAJcEhZcwAAAZAAAAGQAFbw2+gAAACcUExURUdwTP///////////////////////////////////////////////////////////////v////////////////////////////////////////////////////////////////////////////icKf7ozf3ZrPeOF/JdI/ifL/u6avFXJPeSDveTEfidKfzGhv3gu/////FaJPeTExh1PR4AAAAxdFJOUwDk+jgEEMQkCYf+F/bwdKd+HtbO55S4L+tW3UlPwLFfnW5DZ5iTkXt+fmrF4tCufmV322glAAAHEUlEQVRo3u2aB3OjOhCAQRQhigXY9A5OvTKnd///v71VodhJ3ktyuTsnk53JBNTQx65Wy8qa9imf8k7E8XCWZZFlfjQwExdJGh+CINf3bVh5H0lnVV8yxvxcL/UcMYZiYnwQ/ZlV6jPfTjoDR1GUjUMfuyzvs4/AhhOf6clobYq8ooWynfXu2cY9C5IHWnLGFrk08963cXYHtq9mBNNxFhpr0FmahO9Ze13g0kjqytj1qW23yZQpwDo9fr+e3i9bnbuJ0I01NQH4Sxf+WE4rh5cZV99+Hsm7ZctK1gu2sXGZ3oZFVXcEKH3KF6GXHL9fda81CUpp9TfZrJalfLt2hpyVIVbW6BhJzmI+MYMkw8vWnBcSMgi1J2ACu78JNyCda8gMEerxtsJo2KHgnC91JxnY9t66BLgoRoNg9P3QOdMAZfr4Gju/GLiQ2TAPPOgofLCdWZTtsSXFeQFc7roXAeftUcfjk+vrVNmklVVVpiwR71kZS3nBHK26KEbzAuAKFIM3ma6/frsSbs2B3cB1A3uSeEXuC0HMPlOdo+T8mt+YICIY6AEunGOCJ3uoyEGJ+aDokXpz7Wyej7Ve9yzhLnuG8yhium2XTO3qpiGlyPOts3Em2thC0t4Yl2tamBpOxXXvaDvbPgBcadsh9OiorLCb3jD6pUcn5lH1qhKqqXyvRpKqRsSEQGl+ht2kPfiBiEJnES46CZRNcNsYytpgKLm1WXtUc6+SXF8RKHAoO4T3d3f3oc7arZN0bLfe3E6ILVLq63VQaZkvrmDNJUsxBRPw11Z6uV773B8bB7YRxA0Zr21aUwvdbQPw7iaB/70pTA8eFlG4VZHGxGuUY9OFRqKi5iwdOvw4Hm9ujscfpXSis1C2uTVb9oQkwlU+Atc/1QPqtN1pUQra7NgGzrFPG0zcycOrBBV6tnhBHSDG4uPaBE5UyMjL3a4lp3HD483tly+3N8cd2m9VlzCy1SPXks5FhGu5uPTFVLNYz10BR2QRtEvk6/BFM16rOvPqFF4/V4N/WMbjD+a8SBb1mhcvXQ7cZkJlPKml7WA06ghWYYNaBCovZaA8sBSvUgX6/fH2H5Db433p15uqRKzN5SXAUJPskjN2GMVlKODMCNdyn/MwFtaCsSfg0CCajWDGgRx6QCtcIp/T+SsclUWegFNdcK/gLD6HjlvvwVB+WZhpDWNSU+1y/mGVnNl3N1843Jebu5Tlm6rgHM6X6zeCqZYy2VK40sge28QFnIwy+UwP0juNG7jdaQCwWxfRSRctVHBaBS1jGNYVzSp4KSWWj3RVKEyYn68SnMIFmyr/xCxXOKwvVvByuOohnPFsOFMt4ziSnhGgJvlfxzNcv7G9+tQsi00VlSNeEJyWCTeNurW8dUT/1pyL+lOH/4RDMVPWXRqcFnLnMu9YvGOe8X7u7Ne7BVPtX/qyFYg2VlWLmXulgrkkON52IXFSXgFK4IgqMYRi6zRS1sUmvivFK7HI1XXCRx390rs0OJPwTaWZ5z/AnW3ocptUcwsKGWBloiiiLiubBhxsy2c8Xn37KhIo4exeLwjOyMWam1WHIcwJerSJ1EGZSIbGuVxT1mTD8L4tP74F3MB9kLvNNFwEnNPKdE+pKnhowmDAYF0/g/qi0Vk8f/IYdW0oXXvp9TVPOhdIAfw+uOGl+xyPVm0OlCib6mTAa1ubQx0p2Gbtw4OPwc93wGal7CT/9SK4tq6zZ8D1tRDuAR+F80PZIFVwHmxoEEPBAsqVqiIZaz+WqYOlSKOz44MhQKF8Jzp+FRwRqwLRZ8C5SAhjT8DxceYGfFKhDCp5bNk6c/zLY9TzxIj4uKsPrDmpiBIfJbwGx26ovQpumCP//4dj28+AMzjh5jfSyR2cB5Xqq0CbQzC2P01nOVNC+EyrmOWJsfjRYc8CkTKC/aE5tVj+VYCMxUfNcOwBHN7PcDA9V8FxS1Jw7mNweaGdwWldsG1gR5r4xifz0tvL6fHQaxsCi+qr78eEzy+jPgtS0lVjvaMwBbs2xaeuqxtn9joREkoia0eIOgjKCCHFnLec5EvC0FKUdtBDEjnD0gNDw05Zb0OU7MShoAFXazrXNHZzNQkneLIH96EnVQNl2WKXqD7Lfx1/fr0y5PEjzYX5M7EbyM4EBa9NOD9XyNvkkbjtx2desQPNtcpszWzqU3vf0FAd1Xk98gfzfcDx9d+fZ+LCpHX1NWVubpKUY8Py6bcfz70RXIHmBMNWnV5GXbcdH+RdMclZXP/+/OIbwSWLbzvT3k5nQVtsDdYxSKlOet4HHHeW9HEry/qcobifRhxBvGIU/BALNYWj/Qk42JyHXx7FyJH/lOszDRLzH2jkZVmKvNSBFn/ouBiP4xj9+skAjPIfE/Yq0vLf2AR52SQd/ng/IrJwZmTYc7RP+ZRPeWP5F3t9ThKVLBLWAAAAAElFTkSuQmCC", + "encoding": "base64" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 0.04431704292073846, + "receive": 0.029285962227731943 + }, + "_fetchType": "Memory Cache" + }, + { + "pageref": "page_0", + "startedDateTime": "2023-03-30T00:13:32.554Z", + "time": 0.050728966016322374, + "request": { + "method": "GET", + "url": "https://mitmproxy.org/screenshot.png", + "httpVersion": "", + "cookies": [], + "headers": [], + "queryString": [], + "headersSize": -1, + "bodySize": -1 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "image/png" + }, + { + "name": "Last-Modified", + "value": "Sat, 04 Mar 2023 16:01:13 GMT" + }, + { + "name": "Age", + "value": "28469" + }, + { + "name": "Via", + "value": "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + }, + { + "name": "Date", + "value": "Wed, 29 Mar 2023 16:09:35 GMT" + }, + { + "name": "Content-Length", + "value": "117218" + }, + { + "name": "ETag", + "value": "\"4f98eb11153c35af50112da2abe191b9\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "x-amz-cf-id", + "value": "hZNJFIRL_Rh_dwh-8yZ4qQg9WelLhTw2JMmGrq9IAy3GTBSi8uFMOg==" + }, + { + "name": "Alt-Svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + } + ], + "content": { + "size": 0, + "compression": 0, + "mimeType": "image/png", + "text": "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", + "encoding": "base64" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 0.024640990886837244, + "receive": 0.02608797512948513 + }, + "_fetchType": "Memory Cache" + }, + { + "pageref": "page_0", + "startedDateTime": "2023-03-30T00:13:32.555Z", + "time": 0.05478603998199105, + "request": { + "method": "GET", + "url": "https://mitmproxy.org/mitmweb.png", + "httpVersion": "", + "cookies": [], + "headers": [], + "queryString": [], + "headersSize": -1, + "bodySize": -1 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "image/png" + }, + { + "name": "Last-Modified", + "value": "Sat, 04 Mar 2023 16:01:12 GMT" + }, + { + "name": "Age", + "value": "15118" + }, + { + "name": "Via", + "value": "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + }, + { + "name": "Date", + "value": "Wed, 29 Mar 2023 19:52:06 GMT" + }, + { + "name": "Content-Length", + "value": "26548" + }, + { + "name": "ETag", + "value": "\"e2209c09538257b5222ab66de75471c9\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "x-amz-cf-id", + "value": "OOEfB2ge39p2xq2ulC-JusFaIsZnAoWIPu0PEwHcQ9137i2fYbl4fg==" + }, + { + "name": "Alt-Svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + } + ], + "content": { + "size": 0, + "compression": 0, + "mimeType": "image/png", + "text": "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", + "encoding": "base64" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 0.028695038054138422, + "receive": 0.02609100192785263 + }, + "_fetchType": "Memory Cache" + }, + { + "pageref": "page_0", + "startedDateTime": "2023-03-30T00:13:32.555Z", + "time": 0.30104396864771843, + "request": { + "method": "GET", + "url": "https://mitmproxy.org/sponsors/proxyman.png", + "httpVersion": "", + "cookies": [], + "headers": [], + "queryString": [], + "headersSize": -1, + "bodySize": -1 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "image/png" + }, + { + "name": "Last-Modified", + "value": "Sat, 04 Mar 2023 18:02:10 GMT" + }, + { + "name": "Age", + "value": "15118" + }, + { + "name": "Via", + "value": "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + }, + { + "name": "Date", + "value": "Wed, 29 Mar 2023 19:52:06 GMT" + }, + { + "name": "Content-Length", + "value": "10780" + }, + { + "name": "ETag", + "value": "\"addeb8eedd4a1a33a6ac74921cfe7674\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "x-amz-cf-id", + "value": "zPDSuhbRpFby5s1HSS4Yx7BXLfxmFabIzQ-YoVqwsx56vin_-g4zsg==" + }, + { + "name": "Alt-Svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + } + ], + "content": { + "size": 0, + "compression": 0, + "mimeType": "image/png", + "text": "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", + "encoding": "base64" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 0.15787500888109207, + "receive": 0.14316895976662636 + }, + "_fetchType": "Memory Cache" + }, + { + "pageref": "page_0", + "startedDateTime": "2023-03-30T00:13:32.557Z", + "time": 0.31256803777068853, + "request": { + "method": "GET", + "url": "https://mitmproxy.org/sponsors/netograph.svg", + "httpVersion": "", + "cookies": [], + "headers": [], + "queryString": [], + "headersSize": -1, + "bodySize": -1 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "image/svg+xml" + }, + { + "name": "Last-Modified", + "value": "Sat, 04 Mar 2023 18:02:10 GMT" + }, + { + "name": "Age", + "value": "5762" + }, + { + "name": "Via", + "value": "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + }, + { + "name": "Content-Encoding", + "value": "gzip" + }, + { + "name": "Date", + "value": "Wed, 29 Mar 2023 22:44:19 GMT" + }, + { + "name": "ETag", + "value": "W/\"bf618d63750cd4028045db92584a9c1b\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "x-amz-cf-id", + "value": "EnEfhuTUZNfB8XbGj6LKFRv260yMl4vs6tC2IgCnDZ2zZ6DzC_DOQA==" + }, + { + "name": "Alt-Svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + } + ], + "content": { + "size": 0, + "compression": 0, + "mimeType": "image/svg+xml", + "text": "\n\n \n logox\n Created with Sketch.\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 0.16857503214851022, + "receive": 0.14399300562217832 + }, + "_fetchType": "Memory Cache" + }, + { + "pageref": "page_0", + "startedDateTime": "2023-03-30T00:13:32.559Z", + "time": 0.0724269775673747, + "request": { + "method": "GET", + "url": "https://mitmproxy.org/clipboard.min.js", + "httpVersion": "", + "cookies": [], + "headers": [], + "queryString": [], + "headersSize": -1, + "bodySize": -1 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "text/javascript" + }, + { + "name": "Last-Modified", + "value": "Sat, 04 Mar 2023 16:01:11 GMT" + }, + { + "name": "Age", + "value": "28859" + }, + { + "name": "Via", + "value": "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + }, + { + "name": "Content-Encoding", + "value": "gzip" + }, + { + "name": "Date", + "value": "Wed, 29 Mar 2023 16:03:05 GMT" + }, + { + "name": "ETag", + "value": "W/\"af8ab36589315582ccdd82f22e84bffb\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "x-amz-cf-id", + "value": "_rRb3fewwMatT3geo3A2Ou669UeSLF5K8O_N_r6US4rrCBkg-11r5g==" + }, + { + "name": "Alt-Svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + } + ], + "content": { + "size": 0, + "compression": 0, + "mimeType": "text/javascript", + "text": "/*!\n * clipboard.js v2.0.6\n * https://clipboardjs.com/\n * \n * Licensed MIT © Zeno Rocha\n */\n!function(t,e){\"object\"==typeof exports&&\"object\"==typeof module?module.exports=e():\"function\"==typeof define&&define.amd?define([],e):\"object\"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return o={},r.m=n=[function(t,e){t.exports=function(t){var e;if(\"SELECT\"===t.nodeName)t.focus(),e=t.value;else if(\"INPUT\"===t.nodeName||\"TEXTAREA\"===t.nodeName){var n=t.hasAttribute(\"readonly\");n||t.setAttribute(\"readonly\",\"\"),t.select(),t.setSelectionRange(0,t.value.length),n||t.removeAttribute(\"readonly\"),e=t.value}else{t.hasAttribute(\"contenteditable\")&&t.focus();var o=window.getSelection(),r=document.createRange();r.selectNodeContents(t),o.removeAllRanges(),o.addRange(r),e=o.toString()}return e}},function(t,e){function n(){}n.prototype={on:function(t,e,n){var o=this.e||(this.e={});return(o[t]||(o[t]=[])).push({fn:e,ctx:n}),this},once:function(t,e,n){var o=this;function r(){o.off(t,r),e.apply(n,arguments)}return r._=e,this.on(t,r,n)},emit:function(t){for(var e=[].slice.call(arguments,1),n=((this.e||(this.e={}))[t]||[]).slice(),o=0,r=n.length;o 4.0 > 1.0 > a > b > z\n let invert = (/^[0-9]/.test(a.name) && /^[0-9]/.test(b.name)) ? -1 : 1;\n if (a.name > b.name) return invert;\n if (a.name < b.name) return -invert;\n return 0;\n}\n\nlet s3cache = {};\nfunction fetchS3(directory) {\n let url = BUCKET_URL + \"?delimiter=/&prefix=\" + directory;\n s3cache[url] = s3cache[url] || (fetch(url)\n .then(function (response) {\n return response.text()\n })\n .then(function (data) {\n let s3 = (new DOMParser()).parseFromString(data, \"text/xml\");\n let files = [];\n s3.querySelectorAll(\"Contents\").forEach(function (item) {\n if (item.querySelector(\"Key\").textContent in EXCLUDE) {\n return;\n }\n files.push({\n name: item.querySelector(\"Key\").textContent.replace(directory, \"\"),\n time: new Date(item.querySelector(\"LastModified\").textContent),\n size: parseInt(item.querySelector(\"Size\").textContent)\n });\n })\n files.sort(sortByName);\n\n let directories = [];\n s3.querySelectorAll(\"CommonPrefixes\").forEach(function (item) {\n directories.push({\n name: item.querySelector(\"Prefix\").textContent.replace(directory, \"\"),\n });\n })\n directories.sort(sortByName);\n\n return { directory: directory, files: files, directories: directories };\n }));\n return s3cache[url];\n}\n\n\nfunction getLatestRelease(suffix) {\n return fetchS3(\"\").then(function (data) {\n let latestVersion = data.directories\n .map(function (x) {\n return x.name.replace(\"/\", \"\")\n })\n .filter(function (x) {\n return /^[\\d.]+$/.test(x);\n })[0]\n return WEB_ROOT + latestVersion + \"/mitmproxy-\" + latestVersion + suffix;\n })\n}\n" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 0.02172501990571618, + "receive": 0.03967998782172799 + }, + "_fetchType": "Memory Cache" + }, + { + "pageref": "page_0", + "startedDateTime": "2023-03-30T00:13:32.560Z", + "time": 34.83466396573931, + "request": { + "method": "GET", + "url": "https://mitmproxy.org/github-btn.html?user=mhils&type=sponsor&size=large", + "httpVersion": "", + "cookies": [], + "headers": [ + { + "name": "Referer", + "value": "https://mitmproxy.org/" + }, + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15" + } + ], + "queryString": [ + { + "name": "user", + "value": "mhils" + }, + { + "name": "type", + "value": "sponsor" + }, + { + "name": "size", + "value": "large" + } + ], + "headersSize": -1, + "bodySize": -1 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "text/html" + }, + { + "name": "Last-Modified", + "value": "Sat, 04 Mar 2023 16:01:11 GMT" + }, + { + "name": "Age", + "value": "13721" + }, + { + "name": "Via", + "value": "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + }, + { + "name": "Content-Encoding", + "value": "gzip" + }, + { + "name": "Date", + "value": "Wed, 29 Mar 2023 20:15:23 GMT" + }, + { + "name": "ETag", + "value": "W/\"8d3963829b6394c8c198172e36049e5e\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "x-amz-cf-id", + "value": "V4OiAmBuPQryEdGywm_hx7I_Mlg9qjOGfujiFqdU838H6tGDJHqJSg==" + }, + { + "name": "Alt-Svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + } + ], + "content": { + "size": 9689, + "compression": 9689, + "mimeType": "text/html", + "text": "\n\n\n \n \n \n \n \n \n\n\n \n \n \n \n \n \n \n \n\n\n" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 30.74104496045038, + "receive": 4.093619005288929 + }, + "_fetchType": "Memory Cache" + }, + { + "pageref": "page_0", + "startedDateTime": "2023-03-30T00:13:32.560Z", + "time": 34.83466396573931, + "request": { + "method": "GET", + "url": "https://mitmproxy.org/github-btn.html?user=mhils&type=sponsor&size=large", + "httpVersion": "", + "cookies": [], + "headers": [ + { + "name": "Referer", + "value": "https://mitmproxy.org/" + }, + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15" + } + ], + "queryString": [ + { + "name": "user", + "value": "mhils" + }, + { + "name": "type", + "value": "sponsor" + }, + { + "name": "size", + "value": "large" + } + ], + "headersSize": -1, + "bodySize": -1 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "text/html" + }, + { + "name": "Last-Modified", + "value": "Sat, 04 Mar 2023 16:01:11 GMT" + }, + { + "name": "Age", + "value": "13721" + }, + { + "name": "Via", + "value": "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + }, + { + "name": "Content-Encoding", + "value": "gzip" + }, + { + "name": "Date", + "value": "Wed, 29 Mar 2023 20:15:23 GMT" + }, + { + "name": "ETag", + "value": "W/\"8d3963829b6394c8c198172e36049e5e\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "x-amz-cf-id", + "value": "V4OiAmBuPQryEdGywm_hx7I_Mlg9qjOGfujiFqdU838H6tGDJHqJSg==" + }, + { + "name": "Alt-Svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + } + ], + "content": { + "size": 9689, + "compression": 9689, + "mimeType": "text/html", + "text": "\n\n\n \n \n \n \n \n \n\n\n \n \n \n \n \n \n \n \n\n\n" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 30.74104496045038, + "receive": 4.093619005288929 + }, + "_fetchType": "Memory Cache" + }, + { + "pageref": "page_0", + "startedDateTime": "2023-03-30T00:13:32.560Z", + "time": 39.96881702914834, + "request": { + "method": "GET", + "url": "https://mitmproxy.org/github-btn.html?user=mitmproxy&repo=mitmproxy&type=star&count=true&size=large", + "httpVersion": "", + "cookies": [], + "headers": [ + { + "name": "Referer", + "value": "https://mitmproxy.org/" + }, + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15" + } + ], + "queryString": [ + { + "name": "user", + "value": "mitmproxy" + }, + { + "name": "repo", + "value": "mitmproxy" + }, + { + "name": "type", + "value": "star" + }, + { + "name": "count", + "value": "true" + }, + { + "name": "size", + "value": "large" + } + ], + "headersSize": -1, + "bodySize": -1 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "text/html" + }, + { + "name": "Last-Modified", + "value": "Sat, 04 Mar 2023 16:01:11 GMT" + }, + { + "name": "Age", + "value": "13721" + }, + { + "name": "Via", + "value": "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + }, + { + "name": "Content-Encoding", + "value": "gzip" + }, + { + "name": "Date", + "value": "Wed, 29 Mar 2023 20:15:23 GMT" + }, + { + "name": "ETag", + "value": "W/\"8d3963829b6394c8c198172e36049e5e\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "x-amz-cf-id", + "value": "LbvTezgDPwlIbqKzJ8mAb6KjFaV2RZ5ypt0yP_Iosb6GvAo7RxmQnA==" + }, + { + "name": "Alt-Svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + } + ], + "content": { + "size": 9689, + "compression": 9689, + "mimeType": "text/html", + "text": "\n\n\n \n \n \n \n \n \n\n\n \n \n \n \n \n \n \n \n\n\n" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 36.46425798069686, + "receive": 3.5045590484514832 + }, + "_fetchType": "Memory Cache" + }, + { + "pageref": "page_0", + "startedDateTime": "2023-03-30T00:13:32.560Z", + "time": 39.96881702914834, + "request": { + "method": "GET", + "url": "https://mitmproxy.org/github-btn.html?user=mitmproxy&repo=mitmproxy&type=star&count=true&size=large", + "httpVersion": "", + "cookies": [], + "headers": [ + { + "name": "Referer", + "value": "https://mitmproxy.org/" + }, + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15" + } + ], + "queryString": [ + { + "name": "user", + "value": "mitmproxy" + }, + { + "name": "repo", + "value": "mitmproxy" + }, + { + "name": "type", + "value": "star" + }, + { + "name": "count", + "value": "true" + }, + { + "name": "size", + "value": "large" + } + ], + "headersSize": -1, + "bodySize": -1 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "text/html" + }, + { + "name": "Last-Modified", + "value": "Sat, 04 Mar 2023 16:01:11 GMT" + }, + { + "name": "Age", + "value": "13721" + }, + { + "name": "Via", + "value": "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + }, + { + "name": "Content-Encoding", + "value": "gzip" + }, + { + "name": "Date", + "value": "Wed, 29 Mar 2023 20:15:23 GMT" + }, + { + "name": "ETag", + "value": "W/\"8d3963829b6394c8c198172e36049e5e\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "x-amz-cf-id", + "value": "LbvTezgDPwlIbqKzJ8mAb6KjFaV2RZ5ypt0yP_Iosb6GvAo7RxmQnA==" + }, + { + "name": "Alt-Svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + } + ], + "content": { + "size": 9689, + "compression": 9689, + "mimeType": "text/html", + "text": "\n\n\n \n \n \n \n \n \n\n\n \n \n \n \n \n \n \n \n\n\n" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 36.46425798069686, + "receive": 3.5045590484514832 + }, + "_fetchType": "Memory Cache" + }, + { + "pageref": "page_0", + "startedDateTime": "2023-03-30T00:13:32.564Z", + "time": 0.08079502731561661, + "request": { + "method": "GET", + "url": "https://mitmproxy.org/webfonts/fa-brands-400.woff2", + "httpVersion": "", + "cookies": [], + "headers": [], + "queryString": [], + "headersSize": -1, + "bodySize": -1 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "font/woff2" + }, + { + "name": "Last-Modified", + "value": "Sat, 04 Mar 2023 16:01:16 GMT" + }, + { + "name": "Age", + "value": "53839" + }, + { + "name": "Via", + "value": "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + }, + { + "name": "Date", + "value": "Wed, 29 Mar 2023 09:06:45 GMT" + }, + { + "name": "Content-Length", + "value": "76736" + }, + { + "name": "ETag", + "value": "\"ed311c7a0ade9a75bb3ebf5a7670f31d\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "x-amz-cf-id", + "value": "hvJPvCfMaOdyjiv3XEJDOSGjbj3kKG2CuGVtsbr7txlNZy1gxeM9Fg==" + }, + { + "name": "Alt-Svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + } + ], + "content": { + "size": 0, + "compression": 0, + "mimeType": "font/woff2", + "text": "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", + "encoding": "base64" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 0.04063302185386419, + "receive": 0.040162005461752415 + }, + "_fetchType": "Memory Cache" + }, + { + "pageref": "page_0", + "startedDateTime": "2023-03-30T00:13:32.565Z", + "time": 0.044024025555700064, + "request": { + "method": "GET", + "url": "https://mitmproxy.org/webfonts/fa-regular-400.woff2", + "httpVersion": "", + "cookies": [], + "headers": [], + "queryString": [], + "headersSize": -1, + "bodySize": -1 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "font/woff2" + }, + { + "name": "Last-Modified", + "value": "Sat, 04 Mar 2023 16:01:16 GMT" + }, + { + "name": "Age", + "value": "76813" + }, + { + "name": "Via", + "value": "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + }, + { + "name": "Date", + "value": "Wed, 29 Mar 2023 02:43:50 GMT" + }, + { + "name": "Content-Length", + "value": "13224" + }, + { + "name": "ETag", + "value": "\"b91d376b8d7646d671cd820950d5f7f1\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "x-amz-cf-id", + "value": "6aMkeaYsVWAcaHBSXTTiUG6zYF-hZwzGYmxjsmL4p2E7RDlx0qlPzg==" + }, + { + "name": "Alt-Svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + } + ], + "content": { + "size": 0, + "compression": 0, + "mimeType": "font/woff2", + "text": "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", + "encoding": "base64" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 0.018696009647101164, + "receive": 0.0253280159085989 + }, + "_fetchType": "Memory Cache" + }, + { + "pageref": "page_0", + "startedDateTime": "2023-03-30T00:13:32.565Z", + "time": 0.057383032981306314, + "request": { + "method": "GET", + "url": "https://mitmproxy.org/webfonts/fa-solid-900.woff2", + "httpVersion": "", + "cookies": [], + "headers": [], + "queryString": [], + "headersSize": -1, + "bodySize": -1 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "font/woff2" + }, + { + "name": "Last-Modified", + "value": "Sat, 04 Mar 2023 16:01:17 GMT" + }, + { + "name": "Age", + "value": "9778" + }, + { + "name": "Via", + "value": "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + }, + { + "name": "Date", + "value": "Wed, 29 Mar 2023 21:21:06 GMT" + }, + { + "name": "Content-Length", + "value": "78268" + }, + { + "name": "ETag", + "value": "\"d824df7eb2e268626a2dd9a6a741ac4e\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "x-amz-cf-id", + "value": "TIE2Qxv1KxGM6hmwjOKTd7EYjuHE4XB2Qx8cc-QKGy90pGVWmb3CAw==" + }, + { + "name": "Alt-Svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + } + ], + "content": { + "size": 0, + "compression": 0, + "mimeType": "font/woff2", + "text": "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", + "encoding": "base64" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 0.028962036594748497, + "receive": 0.028420996386557817 + }, + "_fetchType": "Memory Cache" + }, + { + "pageref": "page_0", + "startedDateTime": "2023-03-30T00:13:32.583Z", + "time": 0.14684605412185192, + "request": { + "method": "GET", + "url": "https://mitmproxy.org/polyfills.js", + "httpVersion": "", + "cookies": [], + "headers": [], + "queryString": [], + "headersSize": -1, + "bodySize": -1 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "text/javascript" + }, + { + "name": "Last-Modified", + "value": "Sat, 04 Mar 2023 16:01:12 GMT" + }, + { + "name": "Age", + "value": "15118" + }, + { + "name": "Via", + "value": "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + }, + { + "name": "Content-Encoding", + "value": "gzip" + }, + { + "name": "Date", + "value": "Wed, 29 Mar 2023 19:52:06 GMT" + }, + { + "name": "ETag", + "value": "W/\"542d62f852e229d44f16469475b7500b\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "x-amz-cf-id", + "value": "qpYqxaUt0IvgbAjLrfNoD842uJAcA4VX5ahIu-P0HpGWFPd6rUawVg==" + }, + { + "name": "Alt-Svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + } + ], + "content": { + "size": 0, + "compression": 0, + "mimeType": "text/javascript", + "text": "// https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js\n!function(e,n){\"object\"==typeof exports&&\"undefined\"!=typeof module?n():\"function\"==typeof define&&define.amd?define(n):n()}(0,function(){\"use strict\";function e(e){var n=this.constructor;return this.then(function(t){return n.resolve(e()).then(function(){return t})},function(t){return n.resolve(e()).then(function(){return n.reject(t)})})}function n(e){return!(!e||\"undefined\"==typeof e.length)}function t(){}function o(e){if(!(this instanceof o))throw new TypeError(\"Promises must be constructed via new\");if(\"function\"!=typeof e)throw new TypeError(\"not a function\");this._state=0,this._handled=!1,this._value=undefined,this._deferreds=[],c(e,this)}function r(e,n){for(;3===e._state;)e=e._value;0!==e._state?(e._handled=!0,o._immediateFn(function(){var t=1===e._state?n.onFulfilled:n.onRejected;if(null!==t){var o;try{o=t(e._value)}catch(r){return void f(n.promise,r)}i(n.promise,o)}else(1===e._state?i:f)(n.promise,e._value)})):e._deferreds.push(n)}function i(e,n){try{if(n===e)throw new TypeError(\"A promise cannot be resolved with itself.\");if(n&&(\"object\"==typeof n||\"function\"==typeof n)){var t=n.then;if(n instanceof o)return e._state=3,e._value=n,void u(e);if(\"function\"==typeof t)return void c(function(e,n){return function(){e.apply(n,arguments)}}(t,n),e)}e._state=1,e._value=n,u(e)}catch(r){f(e,r)}}function f(e,n){e._state=2,e._value=n,u(e)}function u(e){2===e._state&&0===e._deferreds.length&&o._immediateFn(function(){e._handled||o._unhandledRejectionFn(e._value)});for(var n=0,t=e._deferreds.length;t>n;n++)r(e,e._deferreds[n]);e._deferreds=null}function c(e,n){var t=!1;try{e(function(e){t||(t=!0,i(n,e))},function(e){t||(t=!0,f(n,e))})}catch(o){if(t)return;t=!0,f(n,o)}}var a=setTimeout;o.prototype[\"catch\"]=function(e){return this.then(null,e)},o.prototype.then=function(e,n){var o=new this.constructor(t);return r(this,new function(e,n,t){this.onFulfilled=\"function\"==typeof e?e:null,this.onRejected=\"function\"==typeof n?n:null,this.promise=t}(e,n,o)),o},o.prototype[\"finally\"]=e,o.all=function(e){return new o(function(t,o){function r(e,n){try{if(n&&(\"object\"==typeof n||\"function\"==typeof n)){var u=n.then;if(\"function\"==typeof u)return void u.call(n,function(n){r(e,n)},o)}i[e]=n,0==--f&&t(i)}catch(c){o(c)}}if(!n(e))return o(new TypeError(\"Promise.all accepts an array\"));var i=Array.prototype.slice.call(e);if(0===i.length)return t([]);for(var f=i.length,u=0;i.length>u;u++)r(u,i[u])})},o.resolve=function(e){return e&&\"object\"==typeof e&&e.constructor===o?e:new o(function(n){n(e)})},o.reject=function(e){return new o(function(n,t){t(e)})},o.race=function(e){return new o(function(t,r){if(!n(e))return r(new TypeError(\"Promise.race accepts an array\"));for(var i=0,f=e.length;f>i;i++)o.resolve(e[i]).then(t,r)})},o._immediateFn=\"function\"==typeof setImmediate&&function(e){setImmediate(e)}||function(e){a(e,0)},o._unhandledRejectionFn=function(e){void 0!==console&&console&&console.warn(\"Possible Unhandled Promise Rejection:\",e)};var l=function(){if(\"undefined\"!=typeof self)return self;if(\"undefined\"!=typeof window)return window;if(\"undefined\"!=typeof global)return global;throw Error(\"unable to locate global object\")}();\"Promise\"in l?l.Promise.prototype[\"finally\"]||(l.Promise.prototype[\"finally\"]=e):l.Promise=o});\n\n// https://cdn.jsdelivr.net/npm/whatwg-fetch@3.0.0/dist/fetch.umd.min.js\n!function(t,e){\"object\"==typeof exports&&\"undefined\"!=typeof module?e(exports):\"function\"==typeof define&&define.amd?define([\"exports\"],e):e(t.WHATWGFetch={})}(this,function(a){\"use strict\";var r=\"URLSearchParams\"in self,o=\"Symbol\"in self&&\"iterator\"in Symbol,h=\"FileReader\"in self&&\"Blob\"in self&&function(){try{return new Blob,!0}catch(t){return!1}}(),n=\"FormData\"in self,i=\"ArrayBuffer\"in self;if(i)var e=[\"[object Int8Array]\",\"[object Uint8Array]\",\"[object Uint8ClampedArray]\",\"[object Int16Array]\",\"[object Uint16Array]\",\"[object Int32Array]\",\"[object Uint32Array]\",\"[object Float32Array]\",\"[object Float64Array]\"],s=ArrayBuffer.isView||function(t){return t&&-1this.length)&&-1!==this.indexOf(t,n)});\n" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 0.05030800821259618, + "receive": 0.09653804590925574 + }, + "_fetchType": "Memory Cache" + }, + { + "pageref": "page_0", + "startedDateTime": "2023-03-30T00:13:32.588Z", + "time": 177.2234208183363, + "request": { + "method": "GET", + "url": "https://s3-us-west-2.amazonaws.com/snapshots.mitmproxy.org?delimiter=/&prefix=", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "*/*" + }, + { + "name": "Origin", + "value": "https://mitmproxy.org" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Host", + "value": "s3-us-west-2.amazonaws.com" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.9" + }, + { + "name": "Referer", + "value": "https://mitmproxy.org/" + }, + { + "name": "Connection", + "value": "keep-alive" + } + ], + "queryString": [ + { + "name": "delimiter", + "value": "/" + }, + { + "name": "prefix", + "value": "" + } + ], + "headersSize": 396, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Access-Control-Allow-Methods", + "value": "GET" + }, + { + "name": "Content-Type", + "value": "application/xml" + }, + { + "name": "Access-Control-Max-Age", + "value": "3000" + }, + { + "name": "Transfer-Encoding", + "value": "Identity" + }, + { + "name": "Date", + "value": "Thu, 30 Mar 2023 00:13:33 GMT" + }, + { + "name": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "name": "Vary", + "value": "Origin, Access-Control-Request-Headers, Access-Control-Request-Method" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "x-amz-request-id", + "value": "3YE17W08DAHB9Y2Z" + }, + { + "name": "x-amz-id-2", + "value": "I+4U5T8T5kd6ZdMnkc855kObSN4DyPPDZ++OVCSvRXTm/FCSwWXUodMFhUBRv3YIVKs0fdaxBtU=" + }, + { + "name": "x-amz-bucket-region", + "value": "us-west-2" + } + ], + "content": { + "size": 3406, + "compression": -10, + "mimeType": "application/xml", + "text": "\nsnapshots.mitmproxy.org1000/falseerror.html2018-03-07T22:33:17.000Z"f5abfbae6b5f0fbf3002d22a00804488"426STANDARDindex.html2018-03-07T22:33:11.000Z"f5abfbae6b5f0fbf3002d22a00804488"426STANDARDlist.js2018-03-07T22:33:14.000Z"2662f70064b002b56bd6768e017efaa9"6790STANDARD0.15/0.16/0.17.1/0.17/0.18.1/0.18.2/0.18.3/0.18/0.19/1.0.0/1.0.1/1.0.2/2.0.0/2.0.1/2.0.2/3.0.0/3.0.1/3.0.2/3.0.3/3.0.4/4.0.0/4.0.1/4.0.2/4.0.3/4.0.4/5.0.0/5.0.1/5.1.0/5.1.1/5.2/5.3.0/6.0.0/6.0.1/6.0.2/7.0.0/7.0.1/7.0.2/7.0.3/7.0.4/8.0.0/8.1.0/8.1.1/9.0.0/9.0.1/branches/" + }, + "redirectURL": "", + "headersSize": 465, + "bodySize": 3416, + "_transferSize": 3881 + }, + "cache": {}, + "timings": { + "blocked": 1.558157498948276, + "dns": 11.000239406712353, + "connect": 98.00022660056129, + "ssl": 66.99999130796641, + "send": 0.41588599560782313, + "wait": 65.05021912744269, + "receive": 0.19867467926815152 + }, + "serverIPAddress": "52.218.233.144", + "_serverPort": 443, + "connection": "2", + "_fetchType": "Network Load", + "_priority": "medium" + }, + { + "pageref": "page_0", + "startedDateTime": "2023-03-30T00:13:32.600Z", + "time": 9.896895266138017, + "request": { + "method": "GET", + "url": "https://mitmproxy.org/data/github-stats.json", + "httpVersion": "HTTP/2", + "cookies": [], + "headers": [ + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15" + }, + { + "name": "Accept", + "value": "*/*" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.9" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Host", + "value": "mitmproxy.org" + } + ], + "queryString": [], + "headersSize": 38, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/2", + "cookies": [], + "headers": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Last-Modified", + "value": "Wed, 29 Mar 2023 23:55:21 GMT" + }, + { + "name": "Age", + "value": "1078" + }, + { + "name": "Via", + "value": "1.1 39464b01f314ad3cb531f46c3049bf58.cloudfront.net (CloudFront)" + }, + { + "name": "Content-Encoding", + "value": "gzip" + }, + { + "name": "Date", + "value": "Wed, 29 Mar 2023 23:55:35 GMT" + }, + { + "name": "ETag", + "value": "W/\"07201abd774cb0523be31d94fffe67a3\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "x-amz-cf-id", + "value": "PQpxRAdMo0lTv5SaLHRoYZub3C5Y07kciClJXgO2_KKrV79sb88kNQ==" + }, + { + "name": "Alt-Svc", + "value": "h3=\":443\"; ma=86400" + }, + { + "name": "Server", + "value": "AmazonS3" + }, + { + "name": "x-amz-cf-pop", + "value": "SFO5-C1" + }, + { + "name": "x-cache", + "value": "Hit from cloudfront" + } + ], + "content": { + "size": 6986, + "compression": 5531, + "mimeType": "application/json", + "text": "{\n \"id\": 519832,\n \"node_id\": \"MDEwOlJlcG9zaXRvcnk1MTk4MzI=\",\n \"name\": \"mitmproxy\",\n \"full_name\": \"mitmproxy/mitmproxy\",\n \"private\": false,\n \"owner\": {\n \"login\": \"mitmproxy\",\n \"id\": 4652787,\n \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjQ2NTI3ODc=\",\n \"avatar_url\": \"https://avatars.githubusercontent.com/u/4652787?v=4\",\n \"gravatar_id\": \"\",\n \"url\": \"https://api.github.com/users/mitmproxy\",\n \"html_url\": \"https://github.com/mitmproxy\",\n \"followers_url\": \"https://api.github.com/users/mitmproxy/followers\",\n \"following_url\": \"https://api.github.com/users/mitmproxy/following{/other_user}\",\n \"gists_url\": \"https://api.github.com/users/mitmproxy/gists{/gist_id}\",\n \"starred_url\": \"https://api.github.com/users/mitmproxy/starred{/owner}{/repo}\",\n \"subscriptions_url\": \"https://api.github.com/users/mitmproxy/subscriptions\",\n \"organizations_url\": \"https://api.github.com/users/mitmproxy/orgs\",\n \"repos_url\": \"https://api.github.com/users/mitmproxy/repos\",\n \"events_url\": \"https://api.github.com/users/mitmproxy/events{/privacy}\",\n \"received_events_url\": \"https://api.github.com/users/mitmproxy/received_events\",\n \"type\": \"Organization\",\n \"site_admin\": false\n },\n \"html_url\": \"https://github.com/mitmproxy/mitmproxy\",\n \"description\": \"An interactive TLS-capable intercepting HTTP proxy for penetration testers and software developers.\",\n \"fork\": false,\n \"url\": \"https://api.github.com/repos/mitmproxy/mitmproxy\",\n \"forks_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/forks\",\n \"keys_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/keys{/key_id}\",\n \"collaborators_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/collaborators{/collaborator}\",\n \"teams_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/teams\",\n \"hooks_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/hooks\",\n \"issue_events_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/issues/events{/number}\",\n \"events_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/events\",\n \"assignees_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/assignees{/user}\",\n \"branches_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/branches{/branch}\",\n \"tags_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/tags\",\n \"blobs_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/git/blobs{/sha}\",\n \"git_tags_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/git/tags{/sha}\",\n \"git_refs_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/git/refs{/sha}\",\n \"trees_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/git/trees{/sha}\",\n \"statuses_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/statuses/{sha}\",\n \"languages_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/languages\",\n \"stargazers_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/stargazers\",\n \"contributors_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/contributors\",\n \"subscribers_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/subscribers\",\n \"subscription_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/subscription\",\n \"commits_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/commits{/sha}\",\n \"git_commits_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/git/commits{/sha}\",\n \"comments_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/comments{/number}\",\n \"issue_comment_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/issues/comments{/number}\",\n \"contents_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/contents/{+path}\",\n \"compare_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/compare/{base}...{head}\",\n \"merges_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/merges\",\n \"archive_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/{archive_format}{/ref}\",\n \"downloads_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/downloads\",\n \"issues_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/issues{/number}\",\n \"pulls_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/pulls{/number}\",\n \"milestones_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/milestones{/number}\",\n \"notifications_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/notifications{?since,all,participating}\",\n \"labels_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/labels{/name}\",\n \"releases_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/releases{/id}\",\n \"deployments_url\": \"https://api.github.com/repos/mitmproxy/mitmproxy/deployments\",\n \"created_at\": \"2010-02-16T04:10:13Z\",\n \"updated_at\": \"2023-03-29T21:46:17Z\",\n \"pushed_at\": \"2023-03-29T11:13:54Z\",\n \"git_url\": \"git://github.com/mitmproxy/mitmproxy.git\",\n \"ssh_url\": \"git@github.com:mitmproxy/mitmproxy.git\",\n \"clone_url\": \"https://github.com/mitmproxy/mitmproxy.git\",\n \"svn_url\": \"https://github.com/mitmproxy/mitmproxy\",\n \"homepage\": \"https://mitmproxy.org\",\n \"size\": 57388,\n \"stargazers_count\": 30554,\n \"watchers_count\": 30554,\n \"language\": \"Python\",\n \"has_issues\": true,\n \"has_projects\": true,\n \"has_downloads\": true,\n \"has_wiki\": false,\n \"has_pages\": false,\n \"has_discussions\": true,\n \"forks_count\": 3658,\n \"mirror_url\": null,\n \"archived\": false,\n \"disabled\": false,\n \"open_issues_count\": 260,\n \"license\": {\n \"key\": \"mit\",\n \"name\": \"MIT License\",\n \"spdx_id\": \"MIT\",\n \"url\": \"https://api.github.com/licenses/mit\",\n \"node_id\": \"MDc6TGljZW5zZTEz\"\n },\n \"allow_forking\": true,\n \"is_template\": false,\n \"web_commit_signoff_required\": false,\n \"topics\": [\n \"debugging\",\n \"http\",\n \"http2\",\n \"man-in-the-middle\",\n \"mitmproxy\",\n \"proxy\",\n \"python\",\n \"security\",\n \"ssl\",\n \"tls\",\n \"websocket\"\n ],\n \"visibility\": \"public\",\n \"forks\": 3658,\n \"open_issues\": 260,\n \"watchers\": 30554,\n \"default_branch\": \"main\",\n \"temp_clone_token\": null,\n \"organization\": {\n \"login\": \"mitmproxy\",\n \"id\": 4652787,\n \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjQ2NTI3ODc=\",\n \"avatar_url\": \"https://avatars.githubusercontent.com/u/4652787?v=4\",\n \"gravatar_id\": \"\",\n \"url\": \"https://api.github.com/users/mitmproxy\",\n \"html_url\": \"https://github.com/mitmproxy\",\n \"followers_url\": \"https://api.github.com/users/mitmproxy/followers\",\n \"following_url\": \"https://api.github.com/users/mitmproxy/following{/other_user}\",\n \"gists_url\": \"https://api.github.com/users/mitmproxy/gists{/gist_id}\",\n \"starred_url\": \"https://api.github.com/users/mitmproxy/starred{/owner}{/repo}\",\n \"subscriptions_url\": \"https://api.github.com/users/mitmproxy/subscriptions\",\n \"organizations_url\": \"https://api.github.com/users/mitmproxy/orgs\",\n \"repos_url\": \"https://api.github.com/users/mitmproxy/repos\",\n \"events_url\": \"https://api.github.com/users/mitmproxy/events{/privacy}\",\n \"received_events_url\": \"https://api.github.com/users/mitmproxy/received_events\",\n \"type\": \"Organization\",\n \"site_admin\": false\n },\n \"network_count\": 3658,\n \"subscribers_count\": 619\n}\n" + }, + "redirectURL": "", + "headersSize": 349, + "bodySize": 1455, + "_transferSize": 1804 + }, + "cache": {}, + "timings": { + "blocked": 2.4669530685059726, + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 7.008915941696614, + "receive": 0.4210262559354305 + }, + "serverIPAddress": "13.35.121.29", + "_serverPort": 443, + "connection": "1", + "_fetchType": "Network Load", + "_priority": "medium" + } + ] + } +} \ No newline at end of file diff --git a/test/mitmproxy/data/har_files/safari.json b/test/mitmproxy/data/har_files/safari.json new file mode 100644 index 0000000000..32ff04fff3 --- /dev/null +++ b/test/mitmproxy/data/har_files/safari.json @@ -0,0 +1,2567 @@ +[ + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680135212.418, + "timestamp_tls_setup": null, + "timestamp_end": 1680135212.5293427 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": [ + "13.35.121.29", + 443 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/", + "http_version": "HTTP/2", + "headers": [ + [ + "Accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + ], + [ + "Accept-Encoding", + "gzip, deflate, br" + ], + [ + "Host", + "mitmproxy.org" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15" + ], + [ + "Accept-Language", + "en-US,en;q=0.9" + ], + [ + "Referer", + "https://www.google.com/" + ], + [ + "Connection", + "keep-alive" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680135212.418, + "timestamp_end": 1680135212.5293427, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/2", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "text/html" + ], + [ + "Last-Modified", + "Sat, 04 Mar 2023 18:02:08 GMT" + ], + [ + "Age", + "33218" + ], + [ + "Via", + "1.1 39464b01f314ad3cb531f46c3049bf58.cloudfront.net (CloudFront)" + ], + [ + "Content-Encoding", + "gzip" + ], + [ + "Date", + "Wed, 29 Mar 2023 14:59:54 GMT" + ], + [ + "ETag", + "W/\"a1550c2bd25c5bcfef789d730f5bbddf\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "x-amz-cf-id", + "Qd8Jlu_8NgzlCi_CafxkuPQE_pdS4nei9rjPK9088Zhdsz4j7tcVcA==" + ], + [ + "Alt-Svc", + "h3=\":443\"; ma=86400" + ], + [ + "Server", + "AmazonS3" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-cache", + "Hit from cloudfront" + ] + ], + "contentLength": 4946, + "contentHash": "b643b55c326524222b66cf8d6676c9b640e6417d23aaaa12c8a4ac58216d6586", + "timestamp_start": 1680135212.418, + "timestamp_end": 1680135212.5293427 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680135212.542, + "timestamp_tls_setup": null, + "timestamp_end": 1680135212.5420642 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/style.min.css", + "http_version": "HTTP/1.1", + "headers": [ + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680135212.542, + "timestamp_end": 1680135212.5420642, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "text/css" + ], + [ + "Last-Modified", + "Sat, 04 Mar 2023 18:02:10 GMT" + ], + [ + "Age", + "53840" + ], + [ + "Via", + "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + ], + [ + "Content-Encoding", + "gzip" + ], + [ + "Date", + "Wed, 29 Mar 2023 09:06:44 GMT" + ], + [ + "ETag", + "W/\"98a27a6d8538067b552f3a85c757dd25\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "x-amz-cf-id", + "X2i3z35vQWE0vwoa9PeqSCeJU2nusZQGFA5JWaEOH-RHLOF8bzILtw==" + ], + [ + "Alt-Svc", + "h3=\":443\"; ma=86400" + ], + [ + "Server", + "AmazonS3" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-cache", + "Hit from cloudfront" + ] + ], + "contentLength": 36819, + "contentHash": "62c365a16e642d4b512700140d3e99371aed31b74edc736f9927b6375b1230c0", + "timestamp_start": 1680135212.542, + "timestamp_end": 1680135212.5420642 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680135212.552, + "timestamp_tls_setup": null, + "timestamp_end": 1680135212.5520737 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/logo-navbar.png", + "http_version": "HTTP/1.1", + "headers": [ + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680135212.552, + "timestamp_end": 1680135212.5520737, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "image/png" + ], + [ + "Last-Modified", + "Sat, 04 Mar 2023 16:01:11 GMT" + ], + [ + "Age", + "53839" + ], + [ + "Via", + "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + ], + [ + "Date", + "Wed, 29 Mar 2023 09:06:44 GMT" + ], + [ + "Content-Length", + "2145" + ], + [ + "ETag", + "\"fae2ae3cb7832bd9fbd0f12e08e185ee\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "x-amz-cf-id", + "IW86a_cg24ojMavKL5JYn8hlQ2M4VVVCXzsSkL-HobMhQ9ihGmRiVQ==" + ], + [ + "Alt-Svc", + "h3=\":443\"; ma=86400" + ], + [ + "Server", + "AmazonS3" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-cache", + "Hit from cloudfront" + ] + ], + "contentLength": 2145, + "contentHash": "4947ce36b175decddf46297b9bdce05c6bea88aec0547117c2a2483c202bb603", + "timestamp_start": 1680135212.552, + "timestamp_end": 1680135212.5520737 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680135212.554, + "timestamp_tls_setup": null, + "timestamp_end": 1680135212.5540507 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/screenshot.png", + "http_version": "HTTP/1.1", + "headers": [ + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680135212.554, + "timestamp_end": 1680135212.5540507, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "image/png" + ], + [ + "Last-Modified", + "Sat, 04 Mar 2023 16:01:13 GMT" + ], + [ + "Age", + "28469" + ], + [ + "Via", + "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + ], + [ + "Date", + "Wed, 29 Mar 2023 16:09:35 GMT" + ], + [ + "Content-Length", + "117218" + ], + [ + "ETag", + "\"4f98eb11153c35af50112da2abe191b9\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "x-amz-cf-id", + "hZNJFIRL_Rh_dwh-8yZ4qQg9WelLhTw2JMmGrq9IAy3GTBSi8uFMOg==" + ], + [ + "Alt-Svc", + "h3=\":443\"; ma=86400" + ], + [ + "Server", + "AmazonS3" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-cache", + "Hit from cloudfront" + ] + ], + "contentLength": 117218, + "contentHash": "46b65528ed54e9077594c932b754a3a2f82059c7febc672d3279b87ec672d9b7", + "timestamp_start": 1680135212.554, + "timestamp_end": 1680135212.5540507 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680135212.555, + "timestamp_tls_setup": null, + "timestamp_end": 1680135212.555055 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/mitmweb.png", + "http_version": "HTTP/1.1", + "headers": [ + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680135212.555, + "timestamp_end": 1680135212.555055, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "image/png" + ], + [ + "Last-Modified", + "Sat, 04 Mar 2023 16:01:12 GMT" + ], + [ + "Age", + "15118" + ], + [ + "Via", + "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + ], + [ + "Date", + "Wed, 29 Mar 2023 19:52:06 GMT" + ], + [ + "Content-Length", + "26548" + ], + [ + "ETag", + "\"e2209c09538257b5222ab66de75471c9\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "x-amz-cf-id", + "OOEfB2ge39p2xq2ulC-JusFaIsZnAoWIPu0PEwHcQ9137i2fYbl4fg==" + ], + [ + "Alt-Svc", + "h3=\":443\"; ma=86400" + ], + [ + "Server", + "AmazonS3" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-cache", + "Hit from cloudfront" + ] + ], + "contentLength": 26548, + "contentHash": "909ddb806a674602080bb5d8311cf6fd54362b939ca35d2152f80e88c5093b83", + "timestamp_start": 1680135212.555, + "timestamp_end": 1680135212.555055 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680135212.555, + "timestamp_tls_setup": null, + "timestamp_end": 1680135212.5553012 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/sponsors/proxyman.png", + "http_version": "HTTP/1.1", + "headers": [ + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680135212.555, + "timestamp_end": 1680135212.5553012, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "image/png" + ], + [ + "Last-Modified", + "Sat, 04 Mar 2023 18:02:10 GMT" + ], + [ + "Age", + "15118" + ], + [ + "Via", + "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + ], + [ + "Date", + "Wed, 29 Mar 2023 19:52:06 GMT" + ], + [ + "Content-Length", + "10780" + ], + [ + "ETag", + "\"addeb8eedd4a1a33a6ac74921cfe7674\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "x-amz-cf-id", + "zPDSuhbRpFby5s1HSS4Yx7BXLfxmFabIzQ-YoVqwsx56vin_-g4zsg==" + ], + [ + "Alt-Svc", + "h3=\":443\"; ma=86400" + ], + [ + "Server", + "AmazonS3" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-cache", + "Hit from cloudfront" + ] + ], + "contentLength": 10780, + "contentHash": "7daa40f6d8bd5c5d6cb7adc350378695cb6c7e2ea6b58a1a2c4460a9f427a6ca", + "timestamp_start": 1680135212.555, + "timestamp_end": 1680135212.5553012 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680135212.557, + "timestamp_tls_setup": null, + "timestamp_end": 1680135212.5573125 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/sponsors/netograph.svg", + "http_version": "HTTP/1.1", + "headers": [ + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680135212.557, + "timestamp_end": 1680135212.5573125, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "image/svg+xml" + ], + [ + "Last-Modified", + "Sat, 04 Mar 2023 18:02:10 GMT" + ], + [ + "Age", + "5762" + ], + [ + "Via", + "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + ], + [ + "Content-Encoding", + "gzip" + ], + [ + "Date", + "Wed, 29 Mar 2023 22:44:19 GMT" + ], + [ + "ETag", + "W/\"bf618d63750cd4028045db92584a9c1b\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "x-amz-cf-id", + "EnEfhuTUZNfB8XbGj6LKFRv260yMl4vs6tC2IgCnDZ2zZ6DzC_DOQA==" + ], + [ + "Alt-Svc", + "h3=\":443\"; ma=86400" + ], + [ + "Server", + "AmazonS3" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-cache", + "Hit from cloudfront" + ] + ], + "contentLength": 5167, + "contentHash": "8a74d8c765558a54c3fb4eeb2e24367cfca6a889f0d56b7fc179eb722e5f8ebf", + "timestamp_start": 1680135212.557, + "timestamp_end": 1680135212.5573125 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680135212.559, + "timestamp_tls_setup": null, + "timestamp_end": 1680135212.5590725 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/clipboard.min.js", + "http_version": "HTTP/1.1", + "headers": [ + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680135212.559, + "timestamp_end": 1680135212.5590725, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "text/javascript" + ], + [ + "Last-Modified", + "Sat, 04 Mar 2023 16:01:11 GMT" + ], + [ + "Age", + "28859" + ], + [ + "Via", + "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + ], + [ + "Content-Encoding", + "gzip" + ], + [ + "Date", + "Wed, 29 Mar 2023 16:03:05 GMT" + ], + [ + "ETag", + "W/\"af8ab36589315582ccdd82f22e84bffb\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "x-amz-cf-id", + "_rRb3fewwMatT3geo3A2Ou669UeSLF5K8O_N_r6US4rrCBkg-11r5g==" + ], + [ + "Alt-Svc", + "h3=\":443\"; ma=86400" + ], + [ + "Server", + "AmazonS3" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-cache", + "Hit from cloudfront" + ] + ], + "contentLength": 3346, + "contentHash": "e6c21f88023539515a971172a00e500b7e4444fbf9506e47ceee126ace246808", + "timestamp_start": 1680135212.559, + "timestamp_end": 1680135212.5590725 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680135212.559, + "timestamp_tls_setup": null, + "timestamp_end": 1680135212.5590615 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/snapshots.js", + "http_version": "HTTP/1.1", + "headers": [ + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680135212.559, + "timestamp_end": 1680135212.5590615, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "text/javascript" + ], + [ + "Last-Modified", + "Sat, 04 Mar 2023 16:01:14 GMT" + ], + [ + "Age", + "76814" + ], + [ + "Via", + "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + ], + [ + "Content-Encoding", + "gzip" + ], + [ + "Date", + "Wed, 29 Mar 2023 02:43:50 GMT" + ], + [ + "ETag", + "W/\"20a8c9dba8b59dc27b96998053650836\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "x-amz-cf-id", + "EznjdkbrsG0UwK7hwDtqzrDzJnxfjKpECGQAbxtXEiWMGEoi8eFXwg==" + ], + [ + "Alt-Svc", + "h3=\":443\"; ma=86400" + ], + [ + "Server", + "AmazonS3" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-cache", + "Hit from cloudfront" + ] + ], + "contentLength": 794, + "contentHash": "43cb84ef784bafbab5472abc7c396d95fc4468973d6501c83709e40963b2a953", + "timestamp_start": 1680135212.559, + "timestamp_end": 1680135212.5590615 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680135212.56, + "timestamp_tls_setup": null, + "timestamp_end": 1680135212.5948346 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/github-btn.html?user=mhils&type=sponsor&size=large", + "http_version": "HTTP/1.1", + "headers": [ + [ + "Referer", + "https://mitmproxy.org/" + ], + [ + "Accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680135212.56, + "timestamp_end": 1680135212.5948346, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "text/html" + ], + [ + "Last-Modified", + "Sat, 04 Mar 2023 16:01:11 GMT" + ], + [ + "Age", + "13721" + ], + [ + "Via", + "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + ], + [ + "Content-Encoding", + "gzip" + ], + [ + "Date", + "Wed, 29 Mar 2023 20:15:23 GMT" + ], + [ + "ETag", + "W/\"8d3963829b6394c8c198172e36049e5e\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "x-amz-cf-id", + "V4OiAmBuPQryEdGywm_hx7I_Mlg9qjOGfujiFqdU838H6tGDJHqJSg==" + ], + [ + "Alt-Svc", + "h3=\":443\"; ma=86400" + ], + [ + "Server", + "AmazonS3" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-cache", + "Hit from cloudfront" + ] + ], + "contentLength": 3346, + "contentHash": "1f3ac146af1c45c1a2b4e6c694ecb234382d77b4256524d5ffc365fc8d6130b0", + "timestamp_start": 1680135212.56, + "timestamp_end": 1680135212.5948346 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680135212.56, + "timestamp_tls_setup": null, + "timestamp_end": 1680135212.5948346 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/github-btn.html?user=mhils&type=sponsor&size=large", + "http_version": "HTTP/1.1", + "headers": [ + [ + "Referer", + "https://mitmproxy.org/" + ], + [ + "Accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680135212.56, + "timestamp_end": 1680135212.5948346, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "text/html" + ], + [ + "Last-Modified", + "Sat, 04 Mar 2023 16:01:11 GMT" + ], + [ + "Age", + "13721" + ], + [ + "Via", + "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + ], + [ + "Content-Encoding", + "gzip" + ], + [ + "Date", + "Wed, 29 Mar 2023 20:15:23 GMT" + ], + [ + "ETag", + "W/\"8d3963829b6394c8c198172e36049e5e\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "x-amz-cf-id", + "V4OiAmBuPQryEdGywm_hx7I_Mlg9qjOGfujiFqdU838H6tGDJHqJSg==" + ], + [ + "Alt-Svc", + "h3=\":443\"; ma=86400" + ], + [ + "Server", + "AmazonS3" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-cache", + "Hit from cloudfront" + ] + ], + "contentLength": 3346, + "contentHash": "1f3ac146af1c45c1a2b4e6c694ecb234382d77b4256524d5ffc365fc8d6130b0", + "timestamp_start": 1680135212.56, + "timestamp_end": 1680135212.5948346 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680135212.56, + "timestamp_tls_setup": null, + "timestamp_end": 1680135212.5999687 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/github-btn.html?user=mitmproxy&repo=mitmproxy&type=star&count=true&size=large", + "http_version": "HTTP/1.1", + "headers": [ + [ + "Referer", + "https://mitmproxy.org/" + ], + [ + "Accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680135212.56, + "timestamp_end": 1680135212.5999687, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "text/html" + ], + [ + "Last-Modified", + "Sat, 04 Mar 2023 16:01:11 GMT" + ], + [ + "Age", + "13721" + ], + [ + "Via", + "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + ], + [ + "Content-Encoding", + "gzip" + ], + [ + "Date", + "Wed, 29 Mar 2023 20:15:23 GMT" + ], + [ + "ETag", + "W/\"8d3963829b6394c8c198172e36049e5e\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "x-amz-cf-id", + "LbvTezgDPwlIbqKzJ8mAb6KjFaV2RZ5ypt0yP_Iosb6GvAo7RxmQnA==" + ], + [ + "Alt-Svc", + "h3=\":443\"; ma=86400" + ], + [ + "Server", + "AmazonS3" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-cache", + "Hit from cloudfront" + ] + ], + "contentLength": 3346, + "contentHash": "1f3ac146af1c45c1a2b4e6c694ecb234382d77b4256524d5ffc365fc8d6130b0", + "timestamp_start": 1680135212.56, + "timestamp_end": 1680135212.5999687 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680135212.56, + "timestamp_tls_setup": null, + "timestamp_end": 1680135212.5999687 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/github-btn.html?user=mitmproxy&repo=mitmproxy&type=star&count=true&size=large", + "http_version": "HTTP/1.1", + "headers": [ + [ + "Referer", + "https://mitmproxy.org/" + ], + [ + "Accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680135212.56, + "timestamp_end": 1680135212.5999687, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "text/html" + ], + [ + "Last-Modified", + "Sat, 04 Mar 2023 16:01:11 GMT" + ], + [ + "Age", + "13721" + ], + [ + "Via", + "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + ], + [ + "Content-Encoding", + "gzip" + ], + [ + "Date", + "Wed, 29 Mar 2023 20:15:23 GMT" + ], + [ + "ETag", + "W/\"8d3963829b6394c8c198172e36049e5e\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "x-amz-cf-id", + "LbvTezgDPwlIbqKzJ8mAb6KjFaV2RZ5ypt0yP_Iosb6GvAo7RxmQnA==" + ], + [ + "Alt-Svc", + "h3=\":443\"; ma=86400" + ], + [ + "Server", + "AmazonS3" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-cache", + "Hit from cloudfront" + ] + ], + "contentLength": 3346, + "contentHash": "1f3ac146af1c45c1a2b4e6c694ecb234382d77b4256524d5ffc365fc8d6130b0", + "timestamp_start": 1680135212.56, + "timestamp_end": 1680135212.5999687 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680135212.564, + "timestamp_tls_setup": null, + "timestamp_end": 1680135212.5640807 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/webfonts/fa-brands-400.woff2", + "http_version": "HTTP/1.1", + "headers": [ + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680135212.564, + "timestamp_end": 1680135212.5640807, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "font/woff2" + ], + [ + "Last-Modified", + "Sat, 04 Mar 2023 16:01:16 GMT" + ], + [ + "Age", + "53839" + ], + [ + "Via", + "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + ], + [ + "Date", + "Wed, 29 Mar 2023 09:06:45 GMT" + ], + [ + "Content-Length", + "76736" + ], + [ + "ETag", + "\"ed311c7a0ade9a75bb3ebf5a7670f31d\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "x-amz-cf-id", + "hvJPvCfMaOdyjiv3XEJDOSGjbj3kKG2CuGVtsbr7txlNZy1gxeM9Fg==" + ], + [ + "Alt-Svc", + "h3=\":443\"; ma=86400" + ], + [ + "Server", + "AmazonS3" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-cache", + "Hit from cloudfront" + ] + ], + "contentLength": 76736, + "contentHash": "8ea8791754915a898a3100e63e32978a6d1763be6df8e73a39d3a90d691cdeef", + "timestamp_start": 1680135212.564, + "timestamp_end": 1680135212.5640807 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680135212.565, + "timestamp_tls_setup": null, + "timestamp_end": 1680135212.5650442 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/webfonts/fa-regular-400.woff2", + "http_version": "HTTP/1.1", + "headers": [ + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680135212.565, + "timestamp_end": 1680135212.5650442, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "font/woff2" + ], + [ + "Last-Modified", + "Sat, 04 Mar 2023 16:01:16 GMT" + ], + [ + "Age", + "76813" + ], + [ + "Via", + "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + ], + [ + "Date", + "Wed, 29 Mar 2023 02:43:50 GMT" + ], + [ + "Content-Length", + "13224" + ], + [ + "ETag", + "\"b91d376b8d7646d671cd820950d5f7f1\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "x-amz-cf-id", + "6aMkeaYsVWAcaHBSXTTiUG6zYF-hZwzGYmxjsmL4p2E7RDlx0qlPzg==" + ], + [ + "Alt-Svc", + "h3=\":443\"; ma=86400" + ], + [ + "Server", + "AmazonS3" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-cache", + "Hit from cloudfront" + ] + ], + "contentLength": 13224, + "contentHash": "e42a88444448ac3d60549cc7c1ff2c8a9cac721034c073d80a14a44e79730cca", + "timestamp_start": 1680135212.565, + "timestamp_end": 1680135212.5650442 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680135212.565, + "timestamp_tls_setup": null, + "timestamp_end": 1680135212.5650575 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/webfonts/fa-solid-900.woff2", + "http_version": "HTTP/1.1", + "headers": [ + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680135212.565, + "timestamp_end": 1680135212.5650575, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "font/woff2" + ], + [ + "Last-Modified", + "Sat, 04 Mar 2023 16:01:17 GMT" + ], + [ + "Age", + "9778" + ], + [ + "Via", + "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + ], + [ + "Date", + "Wed, 29 Mar 2023 21:21:06 GMT" + ], + [ + "Content-Length", + "78268" + ], + [ + "ETag", + "\"d824df7eb2e268626a2dd9a6a741ac4e\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "x-amz-cf-id", + "TIE2Qxv1KxGM6hmwjOKTd7EYjuHE4XB2Qx8cc-QKGy90pGVWmb3CAw==" + ], + [ + "Alt-Svc", + "h3=\":443\"; ma=86400" + ], + [ + "Server", + "AmazonS3" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-cache", + "Hit from cloudfront" + ] + ], + "contentLength": 78268, + "contentHash": "9834b82ad26e2a37583d22676a12dd2eb0fe7c80356a2114d0db1aa8b3899537", + "timestamp_start": 1680135212.565, + "timestamp_end": 1680135212.5650575 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680135212.583, + "timestamp_tls_setup": null, + "timestamp_end": 1680135212.5831468 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": null, + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/polyfills.js", + "http_version": "HTTP/1.1", + "headers": [ + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680135212.583, + "timestamp_end": 1680135212.5831468, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "text/javascript" + ], + [ + "Last-Modified", + "Sat, 04 Mar 2023 16:01:12 GMT" + ], + [ + "Age", + "15118" + ], + [ + "Via", + "1.1 05aec04162b0fed6e9762cd1edd66a72.cloudfront.net (CloudFront)" + ], + [ + "Content-Encoding", + "gzip" + ], + [ + "Date", + "Wed, 29 Mar 2023 19:52:06 GMT" + ], + [ + "ETag", + "W/\"542d62f852e229d44f16469475b7500b\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "x-amz-cf-id", + "qpYqxaUt0IvgbAjLrfNoD842uJAcA4VX5ahIu-P0HpGWFPd6rUawVg==" + ], + [ + "Alt-Svc", + "h3=\":443\"; ma=86400" + ], + [ + "Server", + "AmazonS3" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-cache", + "Hit from cloudfront" + ] + ], + "contentLength": 3969, + "contentHash": "3c4880b4b424071aa5e5c5f652b934179099ee8786ea67520b2fadbc4305e5a8", + "timestamp_start": 1680135212.583, + "timestamp_end": 1680135212.5831468 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680135212.588, + "timestamp_tls_setup": null, + "timestamp_end": 1680135212.7652235 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": [ + "52.218.233.144", + 443 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "s3-us-west-2.amazonaws.com", + "port": 443, + "path": "/snapshots.mitmproxy.org?delimiter=/&prefix=", + "http_version": "HTTP/1.1", + "headers": [ + [ + "Accept", + "*/*" + ], + [ + "Origin", + "https://mitmproxy.org" + ], + [ + "Accept-Encoding", + "gzip, deflate, br" + ], + [ + "Host", + "s3-us-west-2.amazonaws.com" + ], + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15" + ], + [ + "Accept-Language", + "en-US,en;q=0.9" + ], + [ + "Referer", + "https://mitmproxy.org/" + ], + [ + "Connection", + "keep-alive" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680135212.588, + "timestamp_end": 1680135212.7652235, + "pretty_host": "s3-us-west-2.amazonaws.com" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Access-Control-Allow-Methods", + "GET" + ], + [ + "Content-Type", + "application/xml" + ], + [ + "Access-Control-Max-Age", + "3000" + ], + [ + "Transfer-Encoding", + "Identity" + ], + [ + "Date", + "Thu, 30 Mar 2023 00:13:33 GMT" + ], + [ + "Access-Control-Allow-Origin", + "*" + ], + [ + "Vary", + "Origin, Access-Control-Request-Headers, Access-Control-Request-Method" + ], + [ + "Server", + "AmazonS3" + ], + [ + "x-amz-request-id", + "3YE17W08DAHB9Y2Z" + ], + [ + "x-amz-id-2", + "I+4U5T8T5kd6ZdMnkc855kObSN4DyPPDZ++OVCSvRXTm/FCSwWXUodMFhUBRv3YIVKs0fdaxBtU=" + ], + [ + "x-amz-bucket-region", + "us-west-2" + ] + ], + "contentLength": 3406, + "contentHash": "1463cf2c4e430b2373b9cd16548f263d3335bc245fdca8019d56a4c9e6ae3b14", + "timestamp_start": 1680135212.588, + "timestamp_end": 1680135212.7652235 + } + }, + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1680135212.6, + "timestamp_tls_setup": null, + "timestamp_end": 1680135212.609897 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": [ + "13.35.121.29", + 443 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "GET", + "scheme": "https", + "host": "mitmproxy.org", + "port": 443, + "path": "/data/github-stats.json", + "http_version": "HTTP/2", + "headers": [ + [ + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15" + ], + [ + "Accept", + "*/*" + ], + [ + "Accept-Language", + "en-US,en;q=0.9" + ], + [ + "Connection", + "keep-alive" + ], + [ + "Accept-Encoding", + "gzip, deflate, br" + ], + [ + "Host", + "mitmproxy.org" + ], + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1680135212.6, + "timestamp_end": 1680135212.609897, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/2", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "application/json" + ], + [ + "Last-Modified", + "Wed, 29 Mar 2023 23:55:21 GMT" + ], + [ + "Age", + "1078" + ], + [ + "Via", + "1.1 39464b01f314ad3cb531f46c3049bf58.cloudfront.net (CloudFront)" + ], + [ + "Content-Encoding", + "gzip" + ], + [ + "Date", + "Wed, 29 Mar 2023 23:55:35 GMT" + ], + [ + "ETag", + "W/\"07201abd774cb0523be31d94fffe67a3\"" + ], + [ + "Vary", + "Accept-Encoding" + ], + [ + "x-amz-cf-id", + "PQpxRAdMo0lTv5SaLHRoYZub3C5Y07kciClJXgO2_KKrV79sb88kNQ==" + ], + [ + "Alt-Svc", + "h3=\":443\"; ma=86400" + ], + [ + "Server", + "AmazonS3" + ], + [ + "x-amz-cf-pop", + "SFO5-C1" + ], + [ + "x-cache", + "Hit from cloudfront" + ] + ], + "contentLength": 1421, + "contentHash": "81de6d4a4bdb984627d61de60369ec4f0ce182170fbe6d9a980b15574d5f6c50", + "timestamp_start": 1680135212.6, + "timestamp_end": 1680135212.609897 + } + } +] \ No newline at end of file diff --git a/test/mitmproxy/data/har_files/with-bom.har b/test/mitmproxy/data/har_files/with-bom.har new file mode 100644 index 0000000000..1e790e2ebc --- /dev/null +++ b/test/mitmproxy/data/har_files/with-bom.har @@ -0,0 +1,189 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "WebInspector", + "version": "537.36" + }, + "pages": [], + "entries": [ + { + "_initiator": { + "type": "script", + "stack": { + "callFrames": [ + { + "functionName": "send", + "scriptId": "108", + "url": "https://signal-beacon.s-onetag.com/beacon.min.js", + "lineNumber": 21, + "columnNumber": 529 + }, + { + "functionName": "X", + "scriptId": "108", + "url": "https://signal-beacon.s-onetag.com/beacon.min.js", + "lineNumber": 33, + "columnNumber": 422 + }, + { + "functionName": "", + "scriptId": "108", + "url": "https://signal-beacon.s-onetag.com/beacon.min.js", + "lineNumber": 34, + "columnNumber": 343 + } + ] + } + }, + "_priority": "VeryLow", + "_resourceType": "ping", + "cache": {}, + "connection": "159834", + "request": { + "method": "POST", + "url": "https://signal-metrics-collector-beta.s-onetag.com/metrics", + "httpVersion": "http/2.0", + "headers": [ + { + "name": ":authority", + "value": "signal-metrics-collector-beta.s-onetag.com" + }, + { + "name": ":method", + "value": "POST" + }, + { + "name": ":path", + "value": "/metrics" + }, + { + "name": ":scheme", + "value": "https" + }, + { + "name": "accept", + "value": "*/*" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate, br" + }, + { + "name": "accept-language", + "value": "en-US,en;q=0.9" + }, + { + "name": "cache-control", + "value": "no-cache" + }, + { + "name": "content-length", + "value": "1310" + }, + { + "name": "content-type", + "value": "text/plain;charset=UTF-8" + }, + { + "name": "dnt", + "value": "1" + }, + { + "name": "origin", + "value": "https://reqbin.com" + }, + { + "name": "pragma", + "value": "no-cache" + }, + { + "name": "sec-ch-ua", + "value": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Google Chrome\";v=\"114\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + }, + { + "name": "sec-fetch-dest", + "value": "empty" + }, + { + "name": "sec-fetch-mode", + "value": "no-cors" + }, + { + "name": "sec-fetch-site", + "value": "cross-site" + }, + { + "name": "user-agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + } + ], + "queryString": [], + "cookies": [], + "headersSize": -1, + "bodySize": 1310, + "postData": { + "mimeType": "text/plain;charset=UTF-8", + "text": "{\"metadata\":{\"pageViewId\":1689428126308,\"affiliateId\":62299,\"domain\":\"reqbin.com\",\"path\":\"/post-online\",\"isCollectable\":true,\"consentString\":\"\",\"gppString\":\"\",\"location\":\"AT\",\"ljtReader\":\"\",\"query\":\"\",\"referrer\":\"https://www.google.com/\",\"canCollectIp\":false},\"payloads\":[{\"scrollDepth\":856,\"dwellTime\":76,\"engagedDwellTime\":5,\"documentHeight\":2958,\"type\":\"page\",\"userEids\":[]},{\"impressionId\":1689428184383,\"zoneId\":null,\"transactionId\":null,\"secondsInView\":0,\"gamAdUnitPath\":\"/1254144,21910826702/reqbin_com-box-4\",\"clicks\":0,\"viewableEngagedSeconds\":0,\"isSovrnReload\":false,\"isAboveTheFold\":false,\"adSize\":\"970x250\",\"adElementId\":\"#div-gpt-ad-reqbin_com-box-4-0\",\"type\":\"impression\"},{\"impressionId\":1689428230692,\"zoneId\":null,\"transactionId\":null,\"secondsInView\":0,\"gamAdUnitPath\":\"/1254144,21910826702/reqbin_com-box-1\",\"clicks\":0,\"viewableEngagedSeconds\":0,\"isSovrnReload\":false,\"isAboveTheFold\":true,\"adSize\":\"300x250\",\"adElementId\":\"#div-gpt-ad-reqbin_com-box-1-0\",\"type\":\"impression\"},{\"impressionId\":1689428203420,\"zoneId\":null,\"transactionId\":null,\"secondsInView\":0,\"gamAdUnitPath\":\"/1254144,21910826702/reqbin_com-banner-2\",\"clicks\":0,\"viewableEngagedSeconds\":0,\"isSovrnReload\":false,\"isAboveTheFold\":true,\"adSize\":\"300x250\",\"adElementId\":\"#div-gpt-ad-reqbin_com-banner-2-0\",\"type\":\"impression\"}]}" + } + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "http/2.0", + "headers": [ + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "content-length", + "value": "0" + }, + { + "name": "date", + "value": "Sat, 15 Jul 2023 13:37:26 GMT" + }, + { + "name": "vary", + "value": "Origin" + } + ], + "cookies": [], + "content": { + "size": 0, + "mimeType": "text/plain", + "text": "" + }, + "redirectURL": "", + "headersSize": -1, + "bodySize": -1, + "_transferSize": 73, + "_error": null + }, + "serverIPAddress": "99.83.181.31", + "startedDateTime": "2023-07-15T13:37:26.093Z", + "time": 169.79599999582302, + "timings": { + "blocked": 0.6539999998793937, + "dns": 18.781000000000002, + "ssl": 55.379, + "connect": 96.754, + "send": 0.3190000000000026, + "wait": 53.01799999912642, + "receive": 0.2699999968172051, + "_blocked_queueing": 0.4079999998793937 + } + } + + ] + } +} \ No newline at end of file diff --git a/test/mitmproxy/data/har_files/with-bom.json b/test/mitmproxy/data/har_files/with-bom.json new file mode 100644 index 0000000000..08c8b8abbf --- /dev/null +++ b/test/mitmproxy/data/har_files/with-bom.json @@ -0,0 +1,173 @@ +[ + { + "id": "hardcoded_for_test", + "intercepted": false, + "is_replay": null, + "type": "http", + "modified": false, + "marked": "", + "comment": "", + "timestamp_created": 0, + "client_conn": { + "id": "hardcoded_for_test", + "peername": [ + "127.0.0.1", + 0 + ], + "sockname": [ + "127.0.0.1", + 0 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": 1689428246.093, + "timestamp_tls_setup": null, + "timestamp_end": 1689428246.262796 + }, + "server_conn": { + "id": "hardcoded_for_test", + "peername": null, + "sockname": null, + "address": [ + "99.83.181.31", + 443 + ], + "tls_established": false, + "cert": null, + "sni": null, + "cipher": null, + "alpn": null, + "tls_version": null, + "timestamp_start": null, + "timestamp_tcp_setup": null, + "timestamp_tls_setup": null, + "timestamp_end": null + }, + "request": { + "method": "POST", + "scheme": "https", + "host": "signal-metrics-collector-beta.s-onetag.com", + "port": 443, + "path": "/metrics", + "http_version": "HTTP/2", + "headers": [ + [ + ":authority", + "signal-metrics-collector-beta.s-onetag.com" + ], + [ + ":method", + "POST" + ], + [ + ":path", + "/metrics" + ], + [ + ":scheme", + "https" + ], + [ + "accept", + "*/*" + ], + [ + "accept-encoding", + "gzip, deflate, br" + ], + [ + "accept-language", + "en-US,en;q=0.9" + ], + [ + "cache-control", + "no-cache" + ], + [ + "content-length", + "1310" + ], + [ + "content-type", + "text/plain;charset=UTF-8" + ], + [ + "dnt", + "1" + ], + [ + "origin", + "https://reqbin.com" + ], + [ + "pragma", + "no-cache" + ], + [ + "sec-ch-ua", + "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Google Chrome\";v=\"114\"" + ], + [ + "sec-ch-ua-mobile", + "?0" + ], + [ + "sec-ch-ua-platform", + "\"macOS\"" + ], + [ + "sec-fetch-dest", + "empty" + ], + [ + "sec-fetch-mode", + "no-cors" + ], + [ + "sec-fetch-site", + "cross-site" + ], + [ + "user-agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + ] + ], + "contentLength": 1310, + "contentHash": "94c0d23b4e9f828b4b9062885ba0b785ce53fc374aef106b01fa62ff9f15c35b", + "timestamp_start": 1689428246.093, + "timestamp_end": 1689428246.262796, + "pretty_host": "signal-metrics-collector-beta.s-onetag.com" + }, + "response": { + "http_version": "HTTP/2", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "access-control-allow-origin", + "*" + ], + [ + "content-length", + "0" + ], + [ + "date", + "Sat, 15 Jul 2023 13:37:26 GMT" + ], + [ + "vary", + "Origin" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1689428246.093, + "timestamp_end": 1689428246.262796 + } + } +] \ No newline at end of file diff --git a/test/mitmproxy/data/test_config.yml b/test/mitmproxy/data/test_config.yml new file mode 100644 index 0000000000..edda67204f --- /dev/null +++ b/test/mitmproxy/data/test_config.yml @@ -0,0 +1,3 @@ +scripts: ['~/abc', 'abc', '../abc', '/abc'] + +not_scripts: ['~/abc', 'abc', '../abc', '/abc'] diff --git a/test/mitmproxy/io/test_compat.py b/test/mitmproxy/io/test_compat.py index e19c8909b8..744a14610f 100644 --- a/test/mitmproxy/io/test_compat.py +++ b/test/mitmproxy/io/test_compat.py @@ -1,7 +1,7 @@ import pytest -from mitmproxy import io from mitmproxy import exceptions +from mitmproxy import io @pytest.mark.parametrize( @@ -11,7 +11,9 @@ ["dumpfile-018.mitm", "https://www.example.com/", 1], ["dumpfile-019.mitm", "https://webrv.rtb-seller.com/", 1], ["dumpfile-7-websocket.mitm", "https://echo.websocket.org/", 6], + ["dumpfile-7.mitm", "https://example.com/", 2], ["dumpfile-10.mitm", "https://example.com/", 1], + ["dumpfile-19.mitm", "https://cloudflare-quic.com/", 1], ], ) def test_load(tdata, dumpfile, url, count): diff --git a/test/mitmproxy/io/test_har.py b/test/mitmproxy/io/test_har.py new file mode 100644 index 0000000000..ffcc31c5af --- /dev/null +++ b/test/mitmproxy/io/test_har.py @@ -0,0 +1,64 @@ +import json +from pathlib import Path + +import pytest + +from mitmproxy import exceptions +from mitmproxy.io.har import fix_headers +from mitmproxy.io.har import request_to_flow +from mitmproxy.tools.web.app import flow_to_json + +data_dir = Path(__file__).parent.parent / "data" + + +def hardcode_variable_fields_for_tests(flow: dict) -> None: + flow["id"] = "hardcoded_for_test" + flow["timestamp_created"] = 0 + flow["server_conn"]["id"] = "hardcoded_for_test" + flow["client_conn"]["id"] = "hardcoded_for_test" + + +def file_to_flows(path_name: Path) -> list[dict]: + file_json = json.loads(path_name.read_bytes())["log"]["entries"] + flows = [] + + for entry in file_json: + expected = request_to_flow(entry) + flow_json = flow_to_json(expected) + hardcode_variable_fields_for_tests(flow_json) + flows.append(flow_json) + + return flows + + +def test_corrupt(): + file_json = json.loads( + Path(data_dir / "corrupted_har/broken_headers.json").read_bytes() + ) + with pytest.raises(exceptions.OptionsError): + fix_headers(file_json["headers"]) + + +@pytest.mark.parametrize( + "har_file", [pytest.param(x, id=x.stem) for x in data_dir.glob("har_files/*.har")] +) +def test_har_to_flow(har_file: Path): + expected_file = har_file.with_suffix(".json") + + expected_flows = json.loads(expected_file.read_bytes()) + actual_flows = file_to_flows(har_file) + + for expected, actual in zip(expected_flows, actual_flows): + actual = json.loads(json.dumps(actual)) + + assert actual == expected + + +if __name__ == "__main__": + for path_name in data_dir.glob("har_files/*.har"): + print(path_name) + + flows = file_to_flows(path_name) + + with open(data_dir / f"har_files/{path_name.stem}.json", "w") as f: + json.dump(flows, f, indent=4) diff --git a/test/mitmproxy/io/test_io.py b/test/mitmproxy/io/test_io.py index 9d7ad80809..f7b9f1e1d9 100644 --- a/test/mitmproxy/io/test_io.py +++ b/test/mitmproxy/io/test_io.py @@ -1,11 +1,17 @@ import io +from pathlib import Path import pytest -from hypothesis import example, given +from hypothesis import example +from hypothesis import given from hypothesis.strategies import binary -from mitmproxy import exceptions, version -from mitmproxy.io import FlowReader, tnetstring +from mitmproxy import exceptions +from mitmproxy import version +from mitmproxy.io import FlowReader +from mitmproxy.io import tnetstring + +here = Path(__file__).parent.parent / "data" class TestFlowReader: @@ -21,6 +27,18 @@ def test_fuzz(self, data): except exceptions.FlowReadException: pass # should never raise anything else. + @pytest.mark.parametrize( + "file", [pytest.param(x, id=x.stem) for x in here.glob("har_files/*.har")] + ) + def test_har(self, file): + with open(file, "rb") as f: + reader = FlowReader(f) + try: + for _ in reader.stream(): + pass + except exceptions.FlowReadException: + pass # should never raise anything else. + def test_empty(self): assert list(FlowReader(io.BytesIO(b"")).stream()) == [] diff --git a/test/mitmproxy/io/test_tnetstring.py b/test/mitmproxy/io/test_tnetstring.py index ccda4cfb8f..60fc4ad125 100644 --- a/test/mitmproxy/io/test_tnetstring.py +++ b/test/mitmproxy/io/test_tnetstring.py @@ -1,8 +1,8 @@ -import unittest -import random -import math import io +import math +import random import struct +import unittest from mitmproxy.io import tnetstring @@ -38,10 +38,10 @@ def get_random_object(random=random, depth=0): what = random.randint(0, 1) if what == 0: n = random.randint(0, 10) - l = [] + lst = [] for _ in range(n): - l.append(get_random_object(random, depth + 1)) - return l + lst.append(get_random_object(random, depth + 1)) + return lst if what == 1: n = random.randint(0, 10) d = {} @@ -72,19 +72,19 @@ def test_roundtrip_format_examples(self): for data, expect in FORMAT_EXAMPLES.items(): self.assertEqual(expect, tnetstring.loads(data)) self.assertEqual(expect, tnetstring.loads(tnetstring.dumps(expect))) - self.assertEqual((expect, b""), tnetstring.pop(data)) + self.assertEqual((expect, b""), tnetstring.pop(memoryview(data))) def test_roundtrip_format_random(self): for _ in range(10): v = get_random_object() self.assertEqual(v, tnetstring.loads(tnetstring.dumps(v))) - self.assertEqual((v, b""), tnetstring.pop(tnetstring.dumps(v))) + self.assertEqual((v, b""), tnetstring.pop(memoryview(tnetstring.dumps(v)))) def test_roundtrip_format_unicode(self): for _ in range(10): v = get_random_object() self.assertEqual(v, tnetstring.loads(tnetstring.dumps(v))) - self.assertEqual((v, b""), tnetstring.pop(tnetstring.dumps(v))) + self.assertEqual((v, b""), tnetstring.pop(memoryview(tnetstring.dumps(v)))) def test_roundtrip_big_integer(self): # Recent Python versions do not like ints above 4300 digits, https://github.com/python/cpython/issues/95778 diff --git a/test/mitmproxy/net/data/verificationcerts/generate.py b/test/mitmproxy/net/data/verificationcerts/generate.py index 599e2c0dd5..570db7110f 100644 --- a/test/mitmproxy/net/data/verificationcerts/generate.py +++ b/test/mitmproxy/net/data/verificationcerts/generate.py @@ -1,6 +1,7 @@ """ Generate SSL test certificates. """ + import os import shlex import shutil @@ -91,3 +92,22 @@ def mkcert(cert, subject, ip: bool): pem.write(key.read()) shutil.copyfile("trusted-leaf.pem", "example.mitmproxy.org.pem") +with ( + open(f"trusted-leaf.crt") as crt, + open(f"self-signed.key") as key, + open(f"private-public-mismatch.pem", "w") as pem, +): + pem.write(crt.read()) + pem.write(key.read()) + +with ( + open(f"trusted-leaf.pem") as crt1, + open(f"trusted-root.crt") as crt2, + open(f"trusted-chain.pem", "w") as pem, +): + pem.write(crt1.read()) + pem.write(crt2.read()) + +with open(f"trusted-leaf.pem") as crt1, open(f"trusted-chain-invalid.pem", "w") as pem: + pem.write(crt1.read()) + pem.write("-----BEGIN CERTIFICATE-----\nnotacert\n-----END CERTIFICATE-----\n") diff --git a/test/mitmproxy/net/data/verificationcerts/private-public-mismatch.pem b/test/mitmproxy/net/data/verificationcerts/private-public-mismatch.pem new file mode 100644 index 0000000000..220559bf61 --- /dev/null +++ b/test/mitmproxy/net/data/verificationcerts/private-public-mismatch.pem @@ -0,0 +1,48 @@ +-----BEGIN CERTIFICATE----- +MIIDZDCCAkygAwIBAgIUSWoLwl7Rc//TKPwKwv25Vhb6uFAwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDExMDMwNzAzMjZaFw00MDEw +MjkwNzAzMjZaMDQxHjAcBgNVBAMMFWV4YW1wbGUubWl0bXByb3h5Lm9yZzESMBAG +A1UECgwJbWl0bXByb3h5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +qcEdB7TZD7DxJlJH/ZTyrovP0din2vLCRZRZ07ix48arITGWj8wFJ/XpZsFmAWlH +2LriXGC8BB54EJt8Epix5tcdKvCZ/7vTgaBQ+DQVdCNiscaKwvDbigy037OY97sT +6Ou9xYjjgg0cb+WTkQGMOTMbCW2e5iuSvyMPdZ2+2RzbTcNV1/6O/5cQ7TOW2E83 +xeaN4stBMfRmsQRoHXBri0tqtQ3bHd3V4H9iai7HFBOcB/nsJUwLliZpz3j/dUD0 +yZAgdgB9XBlGvs/e746bGctyvl2QZu2JeACEVPmVrAUhqe2TVMVLDT17uzNeH1x5 +UXxdB4cexTOm4amvWSwvTQIDAQABo10wWzAfBgNVHSMEGDAWgBTkDj66Wmaf9YP0 +YMqGKG51PejxWzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIFoDAgBgNVHREEGTAXghVl +eGFtcGxlLm1pdG1wcm94eS5vcmcwDQYJKoZIhvcNAQELBQADggEBABwKrf+Gy1I5 +BnfhAqbDUYj8OVcjbB5esbvVYAADNNNQ10RREyz5OTTo2dSKy2hIVQeGxaBy5W6d +cNk0CoLzOgpgkwQPQ351+Y0C5yQztXIg/JM5aSeFuAbQg+NtzOQISrcDB390Xr2f +7YhyYL0XLX4NCP7ek7+lAClrf+lL9MysHTTP0pmFWLPP6AIR8JIsLpNpTDV10c8p +QcnJ8+5q6oLHNKN+M/HRcdKzj/XyJ538UzSOlmU+izhNT1j7mbszS0tc9yh0dmpP +9uuBY0wcD/NAHBpSX9v9ZJcOuWntsAxTbMXj1eWNRyt0QJLURlkqeVn6ZrE1/BDS +BqDdOKYJbkw= +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAw1IZNjnWZOU8jJqlHVK45PLOpr+y8U0ks52OT6pDhTBWGFkn +hEffHAey/kSwSoszdb7e19VF0SLj4wXr36X7CKkJfFhzHrFOjg16ciNo0GRic82K +4VvlC0qRJVXakTVhN/Tm/zNL90GJLTBMti47jmDup52I8j39/PSb8n/6IWMfNMqt +6fqSDYfeATgzeK0jxG+SEpCm+iqeAdzLE3ZotZJMtZ3CTiQfZ5eJamHSJ8pURlZD +fha8JyS+DzeeIZEvA2QGWgDRigS4n20p7ykrwUxaUbLh2RV2n+kVy+CwgdsgFfE1 +40HI5MJ3WU7/TfoxcKUJF5gB3zmkhwD+s22i5QIDAQABAoIBADK7Gi1JbHQcTlO+ +vvAU0k00+5O36sRd4xB79cCfWpY3bcU5Mthayoo/PbBpKtjRuvX0M3EfxdiCFWqb +2R3nwIIJVZtkZdIs/1hKC+mlZM3rpN6rHk1WTvFV1sk5uWFJ2gxsoarbKfn4naaN +Cv+ulm1uo84JTs6MZ3HSHscnklIlNnDG3piC8JnzXVgS7kzbJPXTccZJI1Mwgyho +D1HoWatIep5XAp3OZePlApfRpKn/+2Hg6USkbcHYzX/XpA3b3/YEBqsXVUFxAs5m +nChdLpKUCIjcARInAXoULh0AQHGcH9dfJk1QF1Lw6nCoY12P1sZfdGU5trcslA1O +W28syoECgYEA6GjaqFYffceJHlODukHgpLNfa1EUy49ZWjmdmXTYXJpufzjD4n4H +sUkEnJ00ssaCVdd0XClbN/l/QvIAz3W8ZXXQWtZJHV2wAIyjWpqiOwaiXQFZIZ67 +kxGsLVjkuiST33CX6yl8IKAD+8B67uZhmWTnYBmxGnIZODqUatpqW3ECgYEA1yV+ +CGoNp6VQqej0t0y8C8pXiMGe2DGj5p1WFn00cn/fC1E2Yc0HG+E62OB7tgUFI180 +/nArOQHnTZCzAQAc44M63gesRMeu+0cxuTUqOUCn1lGD+4K/3IA59Rgzfvvnq6xu +tn2jidfqaDPfKavqtCqRbs8wJYaGEhgcb8pUvLUCgYEA4pjpKFvgFGip/lF7C+0T +NEJXdHD3j4lSmy+1w1szYQaJWa1k/73VjjsdLf3w1aXKihuprfn8oFS4ifMeayfl +6h62aPqpCuK/qal10+8U4ewT/g5Ecw0q4bfHYedcC0mCi8ZhuL0X809Q0vLWaXti +CYdiOEaUcK5yfGpRLuWJ8WECgYAWWXa2OQ4iFDJE9EY3pGkEcIiXVEXD/6QfGMkQ +nQENw+rPqigUENBkPQl37hnr1qmp+wHuTIiw61mz3Qw7Vl+p4sACwJlMq9GpmMO5 +kaRJPkYxJVaokfSMW2Wp6FGxJ0nxs3/sxTBv6VYYbQsJsSo4fROOh0dhHpBe4NJT +aplS4QKBgQCaMUN88HNaVr0+DPQKeFoflqwCCYsdK43QTWEirrFDWDliJ0k81DZJ +k7N3oR5UujsTMgfYDJ6Cp3enZVYyuoFnOlVT8v4XzvzAGTehyo4rMiE2TbMir7Xn +agPTZYSuQc4Iy7BPwh/PMlX6bDOTeq/ftyWrDuF9+pC5w3v4SkuMew== +-----END RSA PRIVATE KEY----- diff --git a/test/mitmproxy/net/data/verificationcerts/trusted-chain-invalid.pem b/test/mitmproxy/net/data/verificationcerts/trusted-chain-invalid.pem new file mode 100644 index 0000000000..ce51e2ce66 --- /dev/null +++ b/test/mitmproxy/net/data/verificationcerts/trusted-chain-invalid.pem @@ -0,0 +1,51 @@ +-----BEGIN CERTIFICATE----- +MIIDZDCCAkygAwIBAgIUSWoLwl7Rc//TKPwKwv25Vhb6uFAwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDExMDMwNzAzMjZaFw00MDEw +MjkwNzAzMjZaMDQxHjAcBgNVBAMMFWV4YW1wbGUubWl0bXByb3h5Lm9yZzESMBAG +A1UECgwJbWl0bXByb3h5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +qcEdB7TZD7DxJlJH/ZTyrovP0din2vLCRZRZ07ix48arITGWj8wFJ/XpZsFmAWlH +2LriXGC8BB54EJt8Epix5tcdKvCZ/7vTgaBQ+DQVdCNiscaKwvDbigy037OY97sT +6Ou9xYjjgg0cb+WTkQGMOTMbCW2e5iuSvyMPdZ2+2RzbTcNV1/6O/5cQ7TOW2E83 +xeaN4stBMfRmsQRoHXBri0tqtQ3bHd3V4H9iai7HFBOcB/nsJUwLliZpz3j/dUD0 +yZAgdgB9XBlGvs/e746bGctyvl2QZu2JeACEVPmVrAUhqe2TVMVLDT17uzNeH1x5 +UXxdB4cexTOm4amvWSwvTQIDAQABo10wWzAfBgNVHSMEGDAWgBTkDj66Wmaf9YP0 +YMqGKG51PejxWzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIFoDAgBgNVHREEGTAXghVl +eGFtcGxlLm1pdG1wcm94eS5vcmcwDQYJKoZIhvcNAQELBQADggEBABwKrf+Gy1I5 +BnfhAqbDUYj8OVcjbB5esbvVYAADNNNQ10RREyz5OTTo2dSKy2hIVQeGxaBy5W6d +cNk0CoLzOgpgkwQPQ351+Y0C5yQztXIg/JM5aSeFuAbQg+NtzOQISrcDB390Xr2f +7YhyYL0XLX4NCP7ek7+lAClrf+lL9MysHTTP0pmFWLPP6AIR8JIsLpNpTDV10c8p +QcnJ8+5q6oLHNKN+M/HRcdKzj/XyJ538UzSOlmU+izhNT1j7mbszS0tc9yh0dmpP +9uuBY0wcD/NAHBpSX9v9ZJcOuWntsAxTbMXj1eWNRyt0QJLURlkqeVn6ZrE1/BDS +BqDdOKYJbkw= +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAqcEdB7TZD7DxJlJH/ZTyrovP0din2vLCRZRZ07ix48arITGW +j8wFJ/XpZsFmAWlH2LriXGC8BB54EJt8Epix5tcdKvCZ/7vTgaBQ+DQVdCNiscaK +wvDbigy037OY97sT6Ou9xYjjgg0cb+WTkQGMOTMbCW2e5iuSvyMPdZ2+2RzbTcNV +1/6O/5cQ7TOW2E83xeaN4stBMfRmsQRoHXBri0tqtQ3bHd3V4H9iai7HFBOcB/ns +JUwLliZpz3j/dUD0yZAgdgB9XBlGvs/e746bGctyvl2QZu2JeACEVPmVrAUhqe2T +VMVLDT17uzNeH1x5UXxdB4cexTOm4amvWSwvTQIDAQABAoIBAEb3arF8E3qR2F7S +6zHCASqjXIA3+QR5lGoOOPdgMU4uEgDQgEchXc506dyBYamZX+XlSxifgDqgmkUn +G1mS6Fy+9XysFVVqKmP4p6D79TQWTv5PKFeS5dTytvMGXB7E4O/xDeb08Ve/L3JA +Ic7vPLX0/YqVf2ZuNO0fNSlQhyawUVlSS6yq/B1GUo44PX/PXrWYZd66mc29g+KF +7D0nnbloLY+5QmOEVYbf99RHJ64Cwxzwxi4ic9AxLaNMfkrydjbGQ0VhJq14WEux +/TJLvinzIDyslnx4aYstno3ryoxLtR/9ALysrdQ8vVMaVCdbf0aMu832R2dI4s9K +ixQlgmkCgYEA0Ho0a4qzwk9oLWYk1x16kOCqmfuenlEz3Tv8uBB/jdz7UM63sS3h +OUQYKhhtzOGf6CDA3CE11ie6SX7zHrowPLJBSpacJZJcrHPGZxdBZ/As/QpGnZll +1eTMlVRXk06k/fEYplU1dS5IDbsvUIaaF9CF9Qd3MkDRglhbM19Iy+sCgYEA0HM1 +d7OBsjSi09zFDE+Joq6Gih8LqqFLtMsIHEKAJ0gJgmt6MTfjtdPo55ZKVLCR2pbP +yRHagK4LRUFY9Pl5eGlEUF9cKEKKtZOURUFVz3yBMYPBje/B1o/UCCj+nr2reWgx +qj/jwZrph6QKCkEsnQXi98tE7XSgihUh6UEQO6cCgYASVe0uWDCfMmSzOXyb/te8 +zkWy7VJyEipBlvkPJ0RQsdLYtJWrW6Gna7nEWgmuL1nlDJxpv/IAN9ZGiIfReAau +D+92I/DvzQOhlz0n6/+wqIsMZk73pXozacAkkhpxtkUEoKPOXUgqWju0GXZ72prK +5WgiuNle7hx/Hk5HImZAqQKBgQCK7W4iRGpZeklXiNlvtgcWfNlAbyaYZ34MlhDm +vM+q3pEv8i/zY7uJcR3WU81gmnnrRP5hlVuazeTHGKGQTEFQJmCYbKYAUzEdiamV +atElQ2bbuGOlFLmNJjj7406oP+NsPCx1urUyUOv6MjNa2EtCsCywWDKtTEC/Jwx9 +6JZIGwKBgDa4Ephl+lcyGC8D9Tdc4Nme6n/10pIN53ZCplqVSuma+kOOQX7ZXpJb +0csHR9KODcpQuo2yOnUfZ1z4+tsjms2Uv97GgcZ9sO84I/3RGJCvxmzAEiRmJMfH +y38GtEYCnat5VOKdo//eIYtiu9YSXuuS1ENSWUPZ9YInrkUk0j8S +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +notacert +-----END CERTIFICATE----- diff --git a/test/mitmproxy/net/data/verificationcerts/trusted-chain.pem b/test/mitmproxy/net/data/verificationcerts/trusted-chain.pem new file mode 100644 index 0000000000..0f41465105 --- /dev/null +++ b/test/mitmproxy/net/data/verificationcerts/trusted-chain.pem @@ -0,0 +1,69 @@ +-----BEGIN CERTIFICATE----- +MIIDZDCCAkygAwIBAgIUSWoLwl7Rc//TKPwKwv25Vhb6uFAwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDExMDMwNzAzMjZaFw00MDEw +MjkwNzAzMjZaMDQxHjAcBgNVBAMMFWV4YW1wbGUubWl0bXByb3h5Lm9yZzESMBAG +A1UECgwJbWl0bXByb3h5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +qcEdB7TZD7DxJlJH/ZTyrovP0din2vLCRZRZ07ix48arITGWj8wFJ/XpZsFmAWlH +2LriXGC8BB54EJt8Epix5tcdKvCZ/7vTgaBQ+DQVdCNiscaKwvDbigy037OY97sT +6Ou9xYjjgg0cb+WTkQGMOTMbCW2e5iuSvyMPdZ2+2RzbTcNV1/6O/5cQ7TOW2E83 +xeaN4stBMfRmsQRoHXBri0tqtQ3bHd3V4H9iai7HFBOcB/nsJUwLliZpz3j/dUD0 +yZAgdgB9XBlGvs/e746bGctyvl2QZu2JeACEVPmVrAUhqe2TVMVLDT17uzNeH1x5 +UXxdB4cexTOm4amvWSwvTQIDAQABo10wWzAfBgNVHSMEGDAWgBTkDj66Wmaf9YP0 +YMqGKG51PejxWzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIFoDAgBgNVHREEGTAXghVl +eGFtcGxlLm1pdG1wcm94eS5vcmcwDQYJKoZIhvcNAQELBQADggEBABwKrf+Gy1I5 +BnfhAqbDUYj8OVcjbB5esbvVYAADNNNQ10RREyz5OTTo2dSKy2hIVQeGxaBy5W6d +cNk0CoLzOgpgkwQPQ351+Y0C5yQztXIg/JM5aSeFuAbQg+NtzOQISrcDB390Xr2f +7YhyYL0XLX4NCP7ek7+lAClrf+lL9MysHTTP0pmFWLPP6AIR8JIsLpNpTDV10c8p +QcnJ8+5q6oLHNKN+M/HRcdKzj/XyJ538UzSOlmU+izhNT1j7mbszS0tc9yh0dmpP +9uuBY0wcD/NAHBpSX9v9ZJcOuWntsAxTbMXj1eWNRyt0QJLURlkqeVn6ZrE1/BDS +BqDdOKYJbkw= +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAqcEdB7TZD7DxJlJH/ZTyrovP0din2vLCRZRZ07ix48arITGW +j8wFJ/XpZsFmAWlH2LriXGC8BB54EJt8Epix5tcdKvCZ/7vTgaBQ+DQVdCNiscaK +wvDbigy037OY97sT6Ou9xYjjgg0cb+WTkQGMOTMbCW2e5iuSvyMPdZ2+2RzbTcNV +1/6O/5cQ7TOW2E83xeaN4stBMfRmsQRoHXBri0tqtQ3bHd3V4H9iai7HFBOcB/ns +JUwLliZpz3j/dUD0yZAgdgB9XBlGvs/e746bGctyvl2QZu2JeACEVPmVrAUhqe2T +VMVLDT17uzNeH1x5UXxdB4cexTOm4amvWSwvTQIDAQABAoIBAEb3arF8E3qR2F7S +6zHCASqjXIA3+QR5lGoOOPdgMU4uEgDQgEchXc506dyBYamZX+XlSxifgDqgmkUn +G1mS6Fy+9XysFVVqKmP4p6D79TQWTv5PKFeS5dTytvMGXB7E4O/xDeb08Ve/L3JA +Ic7vPLX0/YqVf2ZuNO0fNSlQhyawUVlSS6yq/B1GUo44PX/PXrWYZd66mc29g+KF +7D0nnbloLY+5QmOEVYbf99RHJ64Cwxzwxi4ic9AxLaNMfkrydjbGQ0VhJq14WEux +/TJLvinzIDyslnx4aYstno3ryoxLtR/9ALysrdQ8vVMaVCdbf0aMu832R2dI4s9K +ixQlgmkCgYEA0Ho0a4qzwk9oLWYk1x16kOCqmfuenlEz3Tv8uBB/jdz7UM63sS3h +OUQYKhhtzOGf6CDA3CE11ie6SX7zHrowPLJBSpacJZJcrHPGZxdBZ/As/QpGnZll +1eTMlVRXk06k/fEYplU1dS5IDbsvUIaaF9CF9Qd3MkDRglhbM19Iy+sCgYEA0HM1 +d7OBsjSi09zFDE+Joq6Gih8LqqFLtMsIHEKAJ0gJgmt6MTfjtdPo55ZKVLCR2pbP +yRHagK4LRUFY9Pl5eGlEUF9cKEKKtZOURUFVz3yBMYPBje/B1o/UCCj+nr2reWgx +qj/jwZrph6QKCkEsnQXi98tE7XSgihUh6UEQO6cCgYASVe0uWDCfMmSzOXyb/te8 +zkWy7VJyEipBlvkPJ0RQsdLYtJWrW6Gna7nEWgmuL1nlDJxpv/IAN9ZGiIfReAau +D+92I/DvzQOhlz0n6/+wqIsMZk73pXozacAkkhpxtkUEoKPOXUgqWju0GXZ72prK +5WgiuNle7hx/Hk5HImZAqQKBgQCK7W4iRGpZeklXiNlvtgcWfNlAbyaYZ34MlhDm +vM+q3pEv8i/zY7uJcR3WU81gmnnrRP5hlVuazeTHGKGQTEFQJmCYbKYAUzEdiamV +atElQ2bbuGOlFLmNJjj7406oP+NsPCx1urUyUOv6MjNa2EtCsCywWDKtTEC/Jwx9 +6JZIGwKBgDa4Ephl+lcyGC8D9Tdc4Nme6n/10pIN53ZCplqVSuma+kOOQX7ZXpJb +0csHR9KODcpQuo2yOnUfZ1z4+tsjms2Uv97GgcZ9sO84I/3RGJCvxmzAEiRmJMfH +y38GtEYCnat5VOKdo//eIYtiu9YSXuuS1ENSWUPZ9YInrkUk0j8S +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIUB5WLNdtHRvciUwDfySAEHYVSNe0wDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDExMDMwNzAzMjZaFw00MDEw +MjkwNzAzMjZaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDVWUg2YdsztWIZpsWdbIB6Aewa1Igt7mNkKMQ1NJ6v +priVlzWhZr0FfSVTid2m3Od2f5A751Q+IXLkNJ8d6Y4qVirAstEM57sCH/Hmt1E3 +cZOAzLQY2xCIrKPmbu6d8eSGO1q+Y8KstCK/V94/QRahjjoHxkLcSAmdg6PAkbhB +7MwOxiJslIbsBY4UCXP0l4kUUIuMi8W2Y4M1VgvLRpUjaKVo1NQ0hLi4XBnU7Bsq +A8/PZDuVeP0NcJtAxRicqqLX/MjgkTrA2tPnSj9m60lO79S40GSzB238o2zJQ5ON +jHJdAhLlrsrkxZmDmKM59IdmV2OhN7O844ktbXDOqvq5AgMBAAGjUzBRMB0GA1Ud +DgQWBBTkDj66Wmaf9YP0YMqGKG51PejxWzAfBgNVHSMEGDAWgBTkDj66Wmaf9YP0 +YMqGKG51PejxWzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAo +9FwbGob1WRAo7ORShaDhopIDAYMCL1oc9YZ2ZQK8aDuZbqIZ/7+1LbWCeMAV4PTH +Tcx+n9Zg/g/RkoSNu8KqFoQGFmdBZnKOMU4vlITu/ORpDu1sjSA+Eo9YbipemX+n +jv+YHI4STFAWnyext3IUZVkT6wpU3pwUjX0fbk4LJfVLR41y4iD11XGergxAcpjj +T03txkJcrTiX65SnB041Y4exUMLOUn5lTs/q2rBNkiLNljXQ6l+8L1rdQEN/j0Mx +OYdc6FIUIESC1mMOf80+YOwxPJ862SyTv/cJB3npwTj/DdQu7blf/z2hMP7a7w+X +l5d31XzcDOrCf/bTthvb +-----END CERTIFICATE----- diff --git a/test/mitmproxy/net/dns/test_domain_names.py b/test/mitmproxy/net/dns/test_domain_names.py index 72e6e5391b..72467ed270 100644 --- a/test/mitmproxy/net/dns/test_domain_names.py +++ b/test/mitmproxy/net/dns/test_domain_names.py @@ -1,28 +1,30 @@ import re import struct + import pytest from mitmproxy.net.dns import domain_names +from mitmproxy.net.dns import types def test_unpack_from_with_compression(): assert domain_names.unpack_from_with_compression( - b"\xFF\x03www\x07example\x03org\x00", 1, domain_names.cache() - ) == ("www.example.org", 17) + b"\xff\x03www\x07example\x03org\x00", 1, domain_names.cache() + ) == ( + "www.example.org", + 17, + ) with pytest.raises( struct.error, match=re.escape("unpack encountered domain name loop") ): domain_names.unpack_from_with_compression( b"\x03www\xc0\x00", 0, domain_names.cache() ) - assert ( - domain_names.unpack_from_with_compression( - b"\xFF\xFF\xFF\x07example\x03org\x00\xFF\xFF\xFF\x03www\xc0\x03", - 19, - domain_names.cache(), - ) - == ("www.example.org", 6) - ) + assert domain_names.unpack_from_with_compression( + b"\xff\xff\xff\x07example\x03org\x00\xff\xff\xff\x03www\xc0\x03", + 19, + domain_names.cache(), + ) == ("www.example.org", 6) def test_unpack(): @@ -30,7 +32,7 @@ def test_unpack(): with pytest.raises( struct.error, match=re.escape("unpack requires a buffer of 17 bytes") ): - domain_names.unpack(b"\x03www\x07example\x03org\x00\xFF") + domain_names.unpack(b"\x03www\x07example\x03org\x00\xff") with pytest.raises( struct.error, match=re.escape("unpack encountered a pointer which is not supported in RDATA"), @@ -46,7 +48,7 @@ def test_unpack(): domain_names.unpack(b"\x40" + (b"a" * 64) + b"\x00") with pytest.raises( struct.error, - match=re.escape("unpack encountered a illegal characters at offset 1"), + match=re.escape("unpack encountered an illegal characters at offset 1"), ): domain_names.unpack(b"\x03\xff\xff\xff\00") @@ -61,9 +63,39 @@ def test_pack(): name = f"www.{label}.com" with pytest.raises( ValueError, - match=re.escape( - "encoding with 'idna' codec failed (UnicodeError: label too long)" - ), + match="label too long", ): domain_names.pack(name) assert domain_names.pack("www.example.org") == b"\x03www\x07example\x03org\x00" + + +def test_record_data_can_have_compression(): + assert domain_names.record_data_can_have_compression(types.NS) + assert not domain_names.record_data_can_have_compression(types.HTTPS) + + +def test_decompress_from_record_data(): + buffer = ( + b"\x10}\x81\x80\x00\x01\x00\x01\x00\x00\x00\x01\x06google\x03com\x00\x00\x06\x00\x01\xc0\x0c\x00\x06\x00" + + b"\x01\x00\x00\x00\x0c\x00&\x03ns1\xc0\x0c\tdns-admin\xc0\x0c&~gw\x00\x00\x03\x84\x00\x00\x03\x84\x00" + + b"\x00\x07\x08\x00\x00\x00<\x00\x00)\x02\x00\x00\x00\x00\x00\x00\x00" + ) + assert ( + domain_names.decompress_from_record_data(buffer, 40, 78, domain_names.cache()) + == b"\x03ns1\x06google\x03com\x00\tdns-admin\x06google\x03com\x00&~gw\x00\x00\x03\x84\x00\x00\x03\x84\x00" + + b"\x00\x07\x08\x00\x00\x00<" + ) + + +def test_record_data_contains_fake_pointer(): + # \xd2\a2 and \xc2\x00 seem like domain name compression pointers but are actually part of some other data type + buffer = ( + b"\xfc\xc7\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00\x06google\x03com\x00\x00\x06\x00\x01\xc0\x0c\x00\x06\x00" + + b"\x01\x00\x00\x008\x00&\x03ns1\xc0\x0c\tdns-admin\xc0\x0c&\xd2\xa2\xc2\x00\x00\x03\x84\x00\x00\x03\x84\x00" + + b"\x00\x07\x08\x00\x00\x00<" + ) + assert ( + domain_names.decompress_from_record_data(buffer, 40, 78, domain_names.cache()) + == b"\x03ns1\x06google\x03com\x00\tdns-admin\x06google\x03com\x00&\xd2\xa2\xc2\x00\x00\x03\x84\x00\x00\x03" + + b"\x84\x00\x00\x07\x08\x00\x00\x00<" + ) diff --git a/test/mitmproxy/net/dns/test_https_records.py b/test/mitmproxy/net/dns/test_https_records.py new file mode 100644 index 0000000000..77db528505 --- /dev/null +++ b/test/mitmproxy/net/dns/test_https_records.py @@ -0,0 +1,109 @@ +import re +import struct + +import pytest +from hypothesis import given +from hypothesis import strategies as st + +from mitmproxy.net.dns import https_records + + +class TestHTTPSRecords: + def test_simple(self): + assert https_records.SVCParamKeys.ALPN.value == 1 + assert https_records.SVCParamKeys(1).name == "ALPN" + + def test_httpsrecord(self): + with pytest.raises( + TypeError, + match=re.escape( + "HTTPSRecord.__init__() missing 3 required positional arguments: 'priority', 'target_name', and 'params'" + ), + ): + https_records.HTTPSRecord() + + def test_unpack(self): + params = { + 0: b"\x00\x04\x00\x06", + 1: b"\x02h2\x02h3", + 2: b"", + 3: b"\x01\xbb", + 4: b"\xb9\xc7l\x99\xb9\xc7m\x99\xb9\xc7n\x99\xb9\xc7o\x99", + 5: b"testbytes", + 6: b"&\x06P\xc0\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01S&\x06P\xc0\x80\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01S&\x06P\xc0\x80\x02\x00\x00\x00\x00\x00\x00\x00\x00\x01S&\x06P\xc0\x80\x03\x00\x00\x00\x00\x00\x00\x00\x00\x01S", + } + record = https_records.HTTPSRecord(1, "example.com", params) + assert https_records.unpack(https_records.pack(record)) == record + + with pytest.raises( + struct.error, match=re.escape("unpack requires a buffer of 2 bytes") + ): + https_records.unpack(b"") + + with pytest.raises( + struct.error, + match=re.escape("unpack encountered an illegal characters at offset 3"), + ): + https_records.unpack( + b"\x00\x01\x07exampl\x87\x03com\x00\x00\x01\x00\x06\x02h2\x02h3" + ) + + with pytest.raises( + struct.error, match=re.escape("unpack requires a buffer of 25 bytes") + ): + https_records.unpack( + b"\x00\x01\x07example\x03com\x00\x00\x01\x00\x06\x02h2" + ) + + with pytest.raises( + struct.error, match=re.escape("unpack requires a label buffer of 7 bytes") + ): + https_records.unpack(b"\x00\x01\x07exa") + + def test_pack(self): + params = { + 0: b"\x00\x04\x00\x06", + 1: b"\x02h2\x02h3", + 2: b"", + 3: b"\x01\xbb", + 4: b"\xb9\xc7l\x99\xb9\xc7m\x99\xb9\xc7n\x99\xb9\xc7o\x99", + 5: b"testbytes", + 6: b"&\x06P\xc0\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01S&\x06P\xc0\x80\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01S&\x06P\xc0\x80\x02\x00\x00\x00\x00\x00\x00\x00\x00\x01S&\x06P\xc0\x80\x03\x00\x00\x00\x00\x00\x00\x00\x00\x01S", + } + record = https_records.HTTPSRecord(1, "example.com", params) + assert ( + https_records.pack(record) + == b"\x00\x01\x07example\x03com\x00\x00\x00\x00\x04\x00\x04\x00\x06\x00\x01\x00\x06\x02h2\x02h3\x00\x02\x00\x00\x00\x03\x00\x02\x01\xbb\x00\x04\x00\x10\xb9\xc7l\x99\xb9\xc7m\x99\xb9\xc7n\x99\xb9\xc7o\x99\x00\x05\x00\ttestbytes\x00\x06\x00@&\x06P\xc0\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01S&\x06P\xc0\x80\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01S&\x06P\xc0\x80\x02\x00\x00\x00\x00\x00\x00\x00\x00\x01S&\x06P\xc0\x80\x03\x00\x00\x00\x00\x00\x00\x00\x00\x01S" + ) + + record = https_records.HTTPSRecord(1, "", {}) + assert https_records.pack(record) == b"\x00\x01\x00" + + @given(st.binary()) + def test_fuzz_unpack(self, data: bytes): + try: + https_records.unpack(data) + except struct.error: + pass + + def test_str(self): + params = { + 0: b"\x00", + 1: b"\x01", + 2: b"", + 3: b"\x02", + 4: b"\x03", + 5: b"\x04", + 6: b"\x05", + } + record = https_records.HTTPSRecord(1, "example.com", params) + assert ( + str(record) + == "priority: 1 target_name: 'example.com' {'mandatory': b'\\x00', 'alpn': b'\\x01', 'no_default_alpn': b'', 'port': b'\\x02', 'ipv4hint': b'\\x03', 'ech': b'\\x04', 'ipv6hint': b'\\x05'}" + ) + + params = {111: b"\x00"} + record = https_records.HTTPSRecord(1, "example.com", params) + assert ( + str(record) == "priority: 1 target_name: 'example.com' {'key111': b'\\x00'}" + ) diff --git a/test/mitmproxy/net/http/http1/test_assemble.py b/test/mitmproxy/net/http/http1/test_assemble.py index 5d17e1bfb1..eb246cf19f 100644 --- a/test/mitmproxy/net/http/http1/test_assemble.py +++ b/test/mitmproxy/net/http/http1/test_assemble.py @@ -1,17 +1,16 @@ import pytest from mitmproxy.http import Headers -from mitmproxy.net.http.http1.assemble import ( - assemble_request, - assemble_request_head, - assemble_response, - assemble_response_head, - _assemble_request_line, - _assemble_request_headers, - _assemble_response_headers, - assemble_body, -) -from mitmproxy.test.tutils import treq, tresp +from mitmproxy.net.http.http1.assemble import _assemble_request_headers +from mitmproxy.net.http.http1.assemble import _assemble_request_line +from mitmproxy.net.http.http1.assemble import _assemble_response_headers +from mitmproxy.net.http.http1.assemble import assemble_body +from mitmproxy.net.http.http1.assemble import assemble_request +from mitmproxy.net.http.http1.assemble import assemble_request_head +from mitmproxy.net.http.http1.assemble import assemble_response +from mitmproxy.net.http.http1.assemble import assemble_response_head +from mitmproxy.test.tutils import treq +from mitmproxy.test.tutils import tresp def test_assemble_request(): diff --git a/test/mitmproxy/net/http/http1/test_read.py b/test/mitmproxy/net/http/http1/test_read.py index 3f48a672e3..d2d391d0af 100644 --- a/test/mitmproxy/net/http/http1/test_read.py +++ b/test/mitmproxy/net/http/http1/test_read.py @@ -1,18 +1,16 @@ import pytest from mitmproxy.http import Headers -from mitmproxy.net.http.http1.read import ( - read_request_head, - read_response_head, - connection_close, - expected_http_body_size, - _read_request_line, - _read_response_line, - _read_headers, - get_header_tokens, - validate_headers, -) -from mitmproxy.test.tutils import treq, tresp +from mitmproxy.net.http.http1.read import _read_headers +from mitmproxy.net.http.http1.read import _read_request_line +from mitmproxy.net.http.http1.read import _read_response_line +from mitmproxy.net.http.http1.read import connection_close +from mitmproxy.net.http.http1.read import expected_http_body_size +from mitmproxy.net.http.http1.read import get_header_tokens +from mitmproxy.net.http.http1.read import read_request_head +from mitmproxy.net.http.http1.read import read_response_head +from mitmproxy.test.tutils import treq +from mitmproxy.test.tutils import tresp def test_get_header_tokens(): @@ -65,22 +63,6 @@ def test_read_response_head(): assert r.content is None -def test_validate_headers(): - # both content-length and chunked (possible request smuggling) - with pytest.raises( - ValueError, - match="Received both a Transfer-Encoding and a Content-Length header", - ): - validate_headers( - Headers(transfer_encoding="chunked", content_length="42"), - ) - - with pytest.raises(ValueError, match="Received an invalid header name"): - validate_headers( - Headers([(b"content-length ", b"42")]), - ) - - def test_expected_http_body_size(): # Expect: 100-continue assert ( @@ -121,19 +103,19 @@ def test_expected_http_body_size(): ) is None ) - with pytest.raises(ValueError, match="Invalid transfer encoding"): + with pytest.raises(ValueError, match="invalid transfer-encoding header"): expected_http_body_size( treq( - headers=Headers(transfer_encoding="chun\u212Aed") + headers=Headers(transfer_encoding="chun\u212aed") ), # "chunKed".lower() == "chunked" ) - with pytest.raises(ValueError, match="Unknown transfer encoding"): + with pytest.raises(ValueError, match="unknown transfer-encoding header"): expected_http_body_size( treq( headers=Headers(transfer_encoding="chun ked") ), # "chunKed".lower() == "chunked" ) - with pytest.raises(ValueError, match="Unknown transfer encoding"): + with pytest.raises(ValueError, match="unknown transfer-encoding header"): expected_http_body_size( treq(headers=Headers(transfer_encoding="qux")), ) @@ -151,12 +133,11 @@ def test_expected_http_body_size(): ) # explicit length - for val in (b"foo", b"-7"): - with pytest.raises(ValueError): - expected_http_body_size(treq(headers=Headers(content_length=val))) assert expected_http_body_size(treq(headers=Headers(content_length="42"))) == 42 - # multiple content-length headers with same value - assert ( + # invalid lengths + with pytest.raises(ValueError): + expected_http_body_size(treq(headers=Headers(content_length=b"foo"))) + with pytest.raises(ValueError): expected_http_body_size( treq( headers=Headers( @@ -164,24 +145,6 @@ def test_expected_http_body_size(): ) ) ) - == 42 - ) - # multiple content-length headers with conflicting value - with pytest.raises(ValueError, match="Conflicting Content-Length headers"): - expected_http_body_size( - treq( - headers=Headers( - [(b"content-length", b"42"), (b"content-length", b"45")] - ) - ) - ) - - # non-int content-length - with pytest.raises(ValueError, match="Invalid Content-Length header"): - expected_http_body_size(treq(headers=Headers([(b"content-length", b"NaN")]))) - # negative content-length - with pytest.raises(ValueError, match="Negative Content-Length header"): - expected_http_body_size(treq(headers=Headers([(b"content-length", b"-1")]))) # no length assert expected_http_body_size(treq(headers=Headers())) == 0 diff --git a/test/mitmproxy/net/http/test_cookies.py b/test/mitmproxy/net/http/test_cookies.py index 4b7f3dd652..3e4e824e7b 100644 --- a/test/mitmproxy/net/http/test_cookies.py +++ b/test/mitmproxy/net/http/test_cookies.py @@ -1,9 +1,9 @@ import time -import pytest from unittest import mock -from mitmproxy.net.http import cookies +import pytest +from mitmproxy.net.http import cookies cookie_pairs = [ ["=uno", [["", "uno"]]], @@ -93,23 +93,23 @@ def test_cookie_roundtrips(): def test_parse_set_cookie_pairs(): pairs = [ - ["=", [[["", ""]]]], - ["=;foo=bar", [[["", ""], ["foo", "bar"]]]], - ["=;=;foo=bar", [[["", ""], ["", ""], ["foo", "bar"]]]], - ["=uno", [[["", "uno"]]]], - ["one=uno", [[["one", "uno"]]]], - ["one=un\x20", [[["one", "un\x20"]]]], - ["one=uno; foo", [[["one", "uno"], ["foo", ""]]]], + ["=", [[("", "")]]], + ["=;foo=bar", [[("", ""), ("foo", "bar")]]], + ["=;=;foo=bar", [[("", ""), ("", ""), ("foo", "bar")]]], + ["=uno", [[("", "uno")]]], + ["one=uno", [[("one", "uno")]]], + ["one=un\x20", [[("one", "un\x20")]]], + ["one=uno; foo", [[("one", "uno"), ("foo", None)]]], [ "mun=1.390.f60; " "expires=sun, 11-oct-2015 12:38:31 gmt; path=/; " "domain=b.aol.com", [ [ - ["mun", "1.390.f60"], - ["expires", "sun, 11-oct-2015 12:38:31 gmt"], - ["path", "/"], - ["domain", "b.aol.com"], + ("mun", "1.390.f60"), + ("expires", "sun, 11-oct-2015 12:38:31 gmt"), + ("path", "/"), + ("domain", "b.aol.com"), ] ], ], @@ -120,10 +120,10 @@ def test_parse_set_cookie_pairs(): "path=/", [ [ - ["rpb", r"190%3d1%2616726%3d1%2634832%3d1%2634874%3d1"], - ["domain", ".rubiconproject.com"], - ["expires", "mon, 11-may-2015 21:54:57 gmt"], - ["path", "/"], + ("rpb", r"190%3d1%2616726%3d1%2634832%3d1%2634874%3d1"), + ("domain", ".rubiconproject.com"), + ("expires", "mon, 11-may-2015 21:54:57 gmt"), + ("path", "/"), ] ], ], @@ -169,15 +169,15 @@ def set_cookie_equal(obs, exp): ], ], [ - "foo=bar; expires=Mon, 24 Aug 2037", + "foo=bar; expires=Mon, 24 Aug 2133", [ - ("foo", "bar", (("expires", "Mon, 24 Aug 2037"),)), + ("foo", "bar", (("expires", "Mon, 24 Aug 2133"),)), ], ], [ - "foo=bar; expires=Mon, 24 Aug 2037 00:00:00 GMT, doo=dar", + "foo=bar; expires=Mon, 24 Aug 2133 00:00:00 GMT, doo=dar", [ - ("foo", "bar", (("expires", "Mon, 24 Aug 2037 00:00:00 GMT"),)), + ("foo", "bar", (("expires", "Mon, 24 Aug 2133 00:00:00 GMT"),)), ("doo", "dar", ()), ], ], @@ -197,15 +197,14 @@ def set_cookie_equal(obs, exp): def test_refresh_cookie(): - # Invalid expires format, sent to us by Reddit. - c = "rfoo=bar; Domain=reddit.com; expires=Thu, 31 Dec 2037 23:59:59 GMT; Path=/" + c = "rfoo=bar; Domain=reddit.com; expires=Thu, 31 Dec 2133 23:59:59 GMT; Path=/" assert cookies.refresh_set_cookie_header(c, 60) c = "MOO=BAR; Expires=Tue, 08-Mar-2011 00:20:38 GMT; Path=foo.com; Secure" assert "00:21:38" in cookies.refresh_set_cookie_header(c, 60) - c = "rfoo=bar; Domain=reddit.com; expires=Thu, 31 Dec 2037; Path=/" + c = "rfoo=bar; Domain=reddit.com; expires=Thu, 31 Dec 2133; Path=/" assert "expires" not in cookies.refresh_set_cookie_header(c, 60) c = "foo,bar" @@ -237,7 +236,7 @@ def test_get_expiration_ts(*args): F = cookies.get_expiration_ts assert F(CA([("Expires", "Thu, 01-Jan-1970 00:00:00 GMT")])) == 0 - assert F(CA([("Expires", "Mon, 24-Aug-2037 00:00:00 GMT")])) == 2134684800 + assert F(CA([("Expires", "Mon, 24-Aug-2133 00:00:00 GMT")])) == 5164128000 assert F(CA([("Max-Age", "0")])) == now_ts assert F(CA([("Max-Age", "31")])) == now_ts + 31 @@ -258,10 +257,10 @@ def test_is_expired(): CA([("Expires", "Thu, 01-Jan-1970 00:00:00 GMT"), ("Max-Age", "0")]) ) - assert not cookies.is_expired(CA([("Expires", "Mon, 24-Aug-2037 00:00:00 GMT")])) + assert not cookies.is_expired(CA([("Expires", "Mon, 24-Aug-2133 00:00:00 GMT")])) assert not cookies.is_expired(CA([("Max-Age", "1")])) assert not cookies.is_expired( - CA([("Expires", "Wed, 15-Jul-2037 00:00:00 GMT"), ("Max-Age", "1")]) + CA([("Expires", "Wed, 15-Jul-2133 00:00:00 GMT"), ("Max-Age", "1")]) ) assert not cookies.is_expired(CA([("Max-Age", "nan")])) diff --git a/test/mitmproxy/net/http/test_headers.py b/test/mitmproxy/net/http/test_headers.py index b7dff51d98..f91f46dea9 100644 --- a/test/mitmproxy/net/http/test_headers.py +++ b/test/mitmproxy/net/http/test_headers.py @@ -1,6 +1,10 @@ import collections -from mitmproxy.net.http.headers import parse_content_type, assemble_content_type +import pytest + +from mitmproxy.net.http.headers import assemble_content_type +from mitmproxy.net.http.headers import infer_content_encoding +from mitmproxy.net.http.headers import parse_content_type def test_parse_content_type(): @@ -24,3 +28,34 @@ def test_assemble_content_type(): ) == "text/html; charset=utf8; foo=bar" ) + + +@pytest.mark.parametrize( + "content_type,content,expected", + [ + ("", b"", "latin-1"), + ("", b"foo", "latin-1"), + ("", b"\xfc", "latin-1"), + ("", b"\xf0\xe2", "latin-1"), + ("text/html; charset=latin1", b"\xc3\xbc", "latin1"), + ("text/html; charset=utf8", b"\xc3\xbc", "utf8"), + # json + ("application/json", b'"\xc3\xbc"', "utf8"), + # meta charset + ( + "text/html", + b'\xe6\x98\x8e\xe4\xbc\xaf', + "gb18030", + ), + # css charset + ( + "text/css", + b'@charset "gb2312";' b'#foo::before {content: "\xe6\x98\x8e\xe4\xbc\xaf"}', + "gb18030", + ), + ], +) +def test_infer_content_encoding(content_type, content, expected): + # Additional test coverage in `test_http::TestMessageText` + assert infer_content_encoding(content_type, content) == expected diff --git a/test/mitmproxy/net/http/test_multipart.py b/test/mitmproxy/net/http/test_multipart.py index 1045d70c60..b1d83b654e 100644 --- a/test/mitmproxy/net/http/test_multipart.py +++ b/test/mitmproxy/net/http/test_multipart.py @@ -1,6 +1,5 @@ import pytest -from mitmproxy.http import Headers from mitmproxy.net.http import multipart @@ -15,23 +14,28 @@ def test_decode(): "value2\n" "--{0}--".format(boundary).encode() ) - form = multipart.decode(f"multipart/form-data; boundary={boundary}", content) + form = multipart.decode_multipart( + f"multipart/form-data; boundary={boundary}", content + ) assert len(form) == 2 assert form[0] == (b"field1", b"value1") assert form[1] == (b"field2", b"value2") boundary = "boundary茅莽" - result = multipart.decode(f"multipart/form-data; boundary={boundary}", content) + result = multipart.decode_multipart( + f"multipart/form-data; boundary={boundary}", content + ) assert result == [] - assert multipart.decode("", content) == [] + assert multipart.decode_multipart("", content) == [] def test_encode(): data = [(b"file", b"shell.jpg"), (b"file_size", b"1000")] - headers = Headers(content_type="multipart/form-data; boundary=127824672498") - content = multipart.encode(headers, data) + content = multipart.encode_multipart( + "multipart/form-data; boundary=127824672498", data + ) assert b'Content-Disposition: form-data; name="file"' in content assert ( @@ -42,9 +46,13 @@ def test_encode(): assert len(content) == 252 with pytest.raises(ValueError, match=r"boundary found in encoded string"): - multipart.encode(headers, [(b"key", b"--127824672498")]) + multipart.encode_multipart( + "multipart/form-data; boundary=127824672498", [(b"key", b"--127824672498")] + ) - boundary = "boundary茅莽" - headers = Headers(content_type="multipart/form-data; boundary=" + boundary) - result = multipart.encode(headers, data) + result = multipart.encode_multipart( + "multipart/form-data; boundary=boundary茅莽", data + ) assert result == b"" + + assert multipart.encode_multipart("", data) == b"" diff --git a/test/mitmproxy/net/http/test_url.py b/test/mitmproxy/net/http/test_url.py index ca6e332b82..c03fd3f507 100644 --- a/test/mitmproxy/net/http/test_url.py +++ b/test/mitmproxy/net/http/test_url.py @@ -52,9 +52,7 @@ def test_parse(): def test_ascii_check(): - test_url = ( - "https://xyz.tax-edu.net?flag=selectCourse&lc_id=42825&lc_name=茅莽莽猫氓猫氓".encode() - ) + test_url = "https://xyz.tax-edu.net?flag=selectCourse&lc_id=42825&lc_name=茅莽莽猫氓猫氓".encode() scheme, host, port, full_path = url.parse(test_url) assert scheme == b"https" assert host == b"xyz.tax-edu.net" diff --git a/test/mitmproxy/net/http/test_validate.py b/test/mitmproxy/net/http/test_validate.py new file mode 100644 index 0000000000..b118f50d7c --- /dev/null +++ b/test/mitmproxy/net/http/test_validate.py @@ -0,0 +1,100 @@ +import pytest + +from mitmproxy.http import Headers +from mitmproxy.http import Response +from mitmproxy.net.http.validate import parse_content_length +from mitmproxy.net.http.validate import parse_transfer_encoding +from mitmproxy.net.http.validate import validate_headers + + +def test_parse_content_length_ok(): + assert parse_content_length("0") == 0 + assert parse_content_length("42") == 42 + assert parse_content_length(b"0") == 0 + assert parse_content_length(b"42") == 42 + + +@pytest.mark.parametrize( + "cl", ["NaN", "", " ", "-1", "+1", "0x42", "010", "foo", "1, 1"] +) +def test_parse_content_length_invalid(cl): + with pytest.raises(ValueError, match="invalid content-length"): + parse_content_length(cl) + with pytest.raises(ValueError, match="invalid content-length"): + parse_content_length(cl.encode()) + + +def test_parse_transfer_encoding_ok(): + assert parse_transfer_encoding(b"chunked") == "chunked" + assert parse_transfer_encoding("chunked") == "chunked" + assert parse_transfer_encoding("gzip,chunked") == "gzip,chunked" + assert parse_transfer_encoding("gzip, chunked") == "gzip,chunked" + + +@pytest.mark.parametrize( + "te", + [ + "unknown", + "chunked,chunked", + "chunked,gzip", + "", + "chunKed", + "chun ked", + ], +) +def test_parse_transfer_encoding_invalid(te): + with pytest.raises(ValueError, match="transfer-encoding"): + parse_transfer_encoding(te) + with pytest.raises(ValueError, match="transfer-encoding"): + parse_transfer_encoding(te.encode()) + + +def test_validate_headers_ok(): + validate_headers( + Response.make(headers=Headers(content_length="42")), + ) + + +@pytest.mark.parametrize( + "headers", + [ + pytest.param( + Headers(transfer_encoding="chunked", content_length="42"), id="cl.te" + ), + pytest.param(Headers([(b"content-length ", b"42")]), id="whitespace-key"), + pytest.param(Headers([(b"content-length", b"42 ")]), id="whitespace-value"), + pytest.param(Headers(content_length="-42"), id="negative-cl"), + pytest.param(Headers(content_length="042"), id="zero-prefixed-cl"), + pytest.param(Headers(transfer_encoding="unknown"), id="unknown-te"), + pytest.param( + Headers([(b"content-length", b"42"), (b"content-length", b"43")]), + id="multi-cl", + ), + pytest.param( + Headers([(b"transfer-encoding", b""), (b"transfer-encoding", b"chunked")]), + id="multi-te", + ), + ], +) +def test_validate_headers_invalid(headers: Headers): + resp = Response.make() + resp.headers = ( + headers # update manually as Response.make() fixes content-length headers. + ) + with pytest.raises(ValueError): + validate_headers(resp) + + +def test_validate_headers_te_forbidden(): + te_headers = Headers(transfer_encoding="chunked") + resp = Response.make(headers=te_headers) + resp.headers = te_headers + resp.http_version = "HTTP/1.0" + + with pytest.raises(ValueError): + validate_headers(resp) + + resp = Response.make(status_code=204) + resp.headers = te_headers + with pytest.raises(ValueError): + validate_headers(Response.make(headers=te_headers, status_code=204)) diff --git a/test/mitmproxy/net/test_encoding.py b/test/mitmproxy/net/test_encoding.py index 9d155961b8..f3b79523a9 100644 --- a/test/mitmproxy/net/test_encoding.py +++ b/test/mitmproxy/net/test_encoding.py @@ -1,4 +1,5 @@ from unittest import mock + import pytest from mitmproxy.net import encoding @@ -92,3 +93,22 @@ def test_cache(): # This is not in the cache anymore assert encoding.encode(b"decoded", "gzip") == b"encoded" assert encode_gzip.call_count == 1 + + +def test_zstd(): + FRAME_SIZE = 1024 + + # Create payload of 1024b + test_content = "a" * FRAME_SIZE + + # Compress it, will result a single frame + single_frame = encoding.encode_zstd(test_content.encode()) + + # Concat compressed frame, it'll result two frames, total size of 2048b payload + two_frames = single_frame + single_frame + + # Uncompressed single frame should have the size of FRAME_SIZE + assert len(encoding.decode_zstd(single_frame)) == FRAME_SIZE + + # Uncompressed two frames should have the size of FRAME_SIZE * 2 + assert len(encoding.decode_zstd(two_frames)) == FRAME_SIZE * 2 diff --git a/test/mitmproxy/net/test_tls.py b/test/mitmproxy/net/test_tls.py index c4fb160621..4af6918009 100644 --- a/test/mitmproxy/net/test_tls.py +++ b/test/mitmproxy/net/test_tls.py @@ -1,10 +1,20 @@ from pathlib import Path -from OpenSSL import SSL, crypto +import pytest +from OpenSSL import crypto +from OpenSSL import SSL + from mitmproxy import certs from mitmproxy.net import tls +@pytest.mark.parametrize("version", [tls.Version.UNBOUNDED, tls.Version.SSL3]) +def test_supported(version): + # wild assumption: test environments should not do SSLv3 by default. + expected_support = version is tls.Version.UNBOUNDED + assert tls.is_supported_version(version) == expected_support + + def test_make_master_secret_logger(): assert tls.make_master_secret_logger(None) is None assert isinstance(tls.make_master_secret_logger("filepath"), tls.MasterSecretLogger) @@ -27,16 +37,19 @@ def test_sslkeylogfile(tdata, monkeypatch): min_version=tls.DEFAULT_MIN_VERSION, max_version=tls.DEFAULT_MAX_VERSION, cipher_list=None, + ecdh_curve=None, verify=tls.Verify.VERIFY_NONE, ca_path=None, ca_pemfile=None, client_cert=None, + legacy_server_connect=False, ) sctx = tls.create_client_proxy_context( method=tls.Method.TLS_SERVER_METHOD, min_version=tls.DEFAULT_MIN_VERSION, max_version=tls.DEFAULT_MAX_VERSION, cipher_list=None, + ecdh_curve=None, chain_file=entry.chain_file, alpn_select_callback=None, request_client_cert=False, @@ -58,7 +71,7 @@ def test_sslkeylogfile(tdata, monkeypatch): try: read.do_handshake() except SSL.WantReadError: - write.bio_write(read.bio_read(2 ** 16)) + write.bio_write(read.bio_read(2**16)) else: break read, write = write, read @@ -68,10 +81,24 @@ def test_sslkeylogfile(tdata, monkeypatch): def test_is_record_magic(): - assert not tls.is_tls_record_magic(b"POST /") - assert not tls.is_tls_record_magic(b"\x16\x03") - assert not tls.is_tls_record_magic(b"\x16\x03\x04") - assert tls.is_tls_record_magic(b"\x16\x03\x00") - assert tls.is_tls_record_magic(b"\x16\x03\x01") - assert tls.is_tls_record_magic(b"\x16\x03\x02") - assert tls.is_tls_record_magic(b"\x16\x03\x03") + assert not tls.starts_like_tls_record(b"POST /") + assert not tls.starts_like_tls_record(b"\x16\x03\x04") + assert not tls.starts_like_tls_record(b"") + assert not tls.starts_like_tls_record(b"\x16") + assert not tls.starts_like_tls_record(b"\x16\x03") + assert tls.starts_like_tls_record(b"\x16\x03\x00") + assert tls.starts_like_tls_record(b"\x16\x03\x01") + assert tls.starts_like_tls_record(b"\x16\x03\x02") + assert tls.starts_like_tls_record(b"\x16\x03\x03") + assert not tls.starts_like_tls_record(bytes.fromhex("16fefe")) + + +def test_is_dtls_record_magic(): + assert not tls.starts_like_dtls_record(bytes.fromhex("")) + assert not tls.starts_like_dtls_record(bytes.fromhex("16")) + assert not tls.starts_like_dtls_record(bytes.fromhex("16fe")) + assert tls.starts_like_dtls_record(bytes.fromhex("16fefd")) + assert tls.starts_like_dtls_record(bytes.fromhex("16fefe")) + assert not tls.starts_like_dtls_record(bytes.fromhex("160300")) + assert not tls.starts_like_dtls_record(bytes.fromhex("160304")) + assert not tls.starts_like_dtls_record(bytes.fromhex("150301")) diff --git a/test/mitmproxy/net/test_udp.py b/test/mitmproxy/net/test_udp.py deleted file mode 100644 index 29a848cd1f..0000000000 --- a/test/mitmproxy/net/test_udp.py +++ /dev/null @@ -1,93 +0,0 @@ -import asyncio -from typing import Optional -import pytest -from mitmproxy.connection import Address -from mitmproxy.net.udp import MAX_DATAGRAM_SIZE, DatagramReader, DatagramWriter, open_connection, start_server - - -async def test_client_server(): - server_reader = DatagramReader() - server_writer: Optional[DatagramWriter] = None - - def handle_datagram( - transport: asyncio.DatagramTransport, - data: bytes, - remote_addr: Address, - local_addr: Address - ): - nonlocal server_reader, server_writer - if server_writer is None: - server_writer = DatagramWriter(transport, remote_addr, server_reader) - server_reader.feed_data(data, remote_addr) - - server = await start_server(handle_datagram, "127.0.0.1", 0) - assert repr(server).startswith(" context.Context: opts = options.Options() Proxyserver().load(opts) return context.Context( - connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), opts + connection.Client( + peername=("client", 1234), + sockname=("127.0.0.1", 8080), + timestamp_start=1605699329, + state=connection.ConnectionState.OPEN, + ), + opts, ) -settings.register_profile("fast", max_examples=10) +settings.register_profile("fast", max_examples=10, deadline=None) settings.register_profile("deep", max_examples=100_000, deadline=None) settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "fast")) diff --git a/test/mitmproxy/proxy/layers/http/hyper_h2_test_helpers.py b/test/mitmproxy/proxy/layers/http/hyper_h2_test_helpers.py index d5f8e01821..2932ad8356 100644 --- a/test/mitmproxy/proxy/layers/http/hyper_h2_test_helpers.py +++ b/test/mitmproxy/proxy/layers/http/hyper_h2_test_helpers.py @@ -1,6 +1,5 @@ # This file has been copied from https://github.com/python-hyper/hyper-h2/blob/master/test/helpers.py, # MIT License - # -*- coding: utf-8 -*- """ helpers @@ -8,20 +7,19 @@ This module contains helpers for the h2 tests. """ + from hpack.hpack import Encoder -from hyperframe.frame import ( - HeadersFrame, - DataFrame, - SettingsFrame, - WindowUpdateFrame, - PingFrame, - GoAwayFrame, - RstStreamFrame, - PushPromiseFrame, - PriorityFrame, - ContinuationFrame, - AltSvcFrame, -) +from hyperframe.frame import AltSvcFrame +from hyperframe.frame import ContinuationFrame +from hyperframe.frame import DataFrame +from hyperframe.frame import GoAwayFrame +from hyperframe.frame import HeadersFrame +from hyperframe.frame import PingFrame +from hyperframe.frame import PriorityFrame +from hyperframe.frame import PushPromiseFrame +from hyperframe.frame import RstStreamFrame +from hyperframe.frame import SettingsFrame +from hyperframe.frame import WindowUpdateFrame SAMPLE_SETTINGS = { SettingsFrame.HEADER_TABLE_SIZE: 4096, diff --git a/test/mitmproxy/proxy/layers/http/test_http.py b/test/mitmproxy/proxy/layers/http/test_http.py index 86ff6fc3a1..5854a0f76e 100644 --- a/test/mitmproxy/proxy/layers/http/test_http.py +++ b/test/mitmproxy/proxy/layers/http/test_http.py @@ -1,27 +1,34 @@ -from logging import WARNING - import gc +from logging import WARNING import pytest -from mitmproxy.connection import ConnectionState, Server -from mitmproxy.http import HTTPFlow, Response +from mitmproxy.connection import ConnectionState +from mitmproxy.connection import Server +from mitmproxy.http import HTTPFlow +from mitmproxy.http import Response from mitmproxy.proxy import layer -from mitmproxy.proxy.commands import CloseConnection, Log, OpenConnection, SendData -from mitmproxy.proxy.events import ConnectionClosed, DataReceived -from mitmproxy.proxy.layers import TCPLayer, http, tls +from mitmproxy.proxy.commands import CloseConnection +from mitmproxy.proxy.commands import Log +from mitmproxy.proxy.commands import OpenConnection +from mitmproxy.proxy.commands import SendData +from mitmproxy.proxy.events import ConnectionClosed +from mitmproxy.proxy.events import DataReceived +from mitmproxy.proxy.layers import http +from mitmproxy.proxy.layers import TCPLayer +from mitmproxy.proxy.layers import tls from mitmproxy.proxy.layers.http import HTTPMode -from mitmproxy.proxy.layers.tcp import TcpMessageInjected, TcpStartHook +from mitmproxy.proxy.layers.tcp import TcpMessageInjected +from mitmproxy.proxy.layers.tcp import TcpStartHook from mitmproxy.proxy.layers.websocket import WebsocketStartHook from mitmproxy.proxy.mode_specs import ProxyMode -from mitmproxy.tcp import TCPFlow, TCPMessage -from test.mitmproxy.proxy.tutils import ( - BytesMatching, - Placeholder, - Playbook, - reply, - reply_next_layer, -) +from mitmproxy.tcp import TCPFlow +from mitmproxy.tcp import TCPMessage +from test.mitmproxy.proxy.tutils import BytesMatching +from test.mitmproxy.proxy.tutils import Placeholder +from test.mitmproxy.proxy.tutils import Playbook +from test.mitmproxy.proxy.tutils import reply +from test.mitmproxy.proxy.tutils import reply_next_layer def test_http_proxy(tctx): @@ -57,16 +64,23 @@ def test_http_proxy(tctx): @pytest.mark.parametrize("strategy", ["lazy", "eager"]) -def test_https_proxy(strategy, tctx): +@pytest.mark.parametrize("http_connect_send_host_header", [True, False]) +def test_https_proxy(strategy, http_connect_send_host_header, tctx): """Test a CONNECT request, followed by a HTTP GET /""" server = Placeholder(Server) flow = Placeholder(HTTPFlow) playbook = Playbook(http.HttpLayer(tctx, HTTPMode.regular)) tctx.options.connection_strategy = strategy + tctx.options.http_connect_send_host_header = http_connect_send_host_header ( playbook - >> DataReceived(tctx.client, b"CONNECT example.proxy:80 HTTP/1.1\r\n\r\n") + >> DataReceived( + tctx.client, + b"CONNECT example.proxy:80 HTTP/1.1" + + (b"\r\nHost: example.com:80" if http_connect_send_host_header else b"") + + b"\r\n\r\n", + ) << http.HttpConnectHook(Placeholder()) >> reply() ) @@ -75,6 +89,8 @@ def test_https_proxy(strategy, tctx): playbook >> reply(None) ( playbook + << http.HttpConnectedHook(Placeholder()) + >> reply(None) << SendData(tctx.client, b"HTTP/1.1 200 Connection established\r\n\r\n") >> DataReceived( tctx.client, b"GET /foo?hello=1 HTTP/1.1\r\nHost: example.com\r\n\r\n" @@ -126,7 +142,10 @@ def redirect(flow: HTTPFlow): flow.request.url = "http://redirected.site/" if https_client: - p >> DataReceived(tctx.client, b"CONNECT example.com:80 HTTP/1.1\r\n\r\n") + p >> DataReceived( + tctx.client, + b"CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n", + ) if strategy == "eager": p << OpenConnection(Placeholder()) p >> reply(None) @@ -288,11 +307,16 @@ def test_disconnect_while_intercept(tctx): assert ( Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) - >> DataReceived(tctx.client, b"CONNECT example.com:80 HTTP/1.1\r\n\r\n") + >> DataReceived( + tctx.client, + b"CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n", + ) << http.HttpConnectHook(Placeholder(HTTPFlow)) >> reply() << OpenConnection(server1) >> reply(None) + << http.HttpConnectedHook(Placeholder(HTTPFlow)) + >> reply(None) << SendData(tctx.client, b"HTTP/1.1 200 Connection established\r\n\r\n") >> DataReceived(tctx.client, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") << layer.NextLayerHook(Placeholder()) @@ -658,7 +682,8 @@ def test_server_unreachable(tctx, connect): playbook = Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) if connect: playbook >> DataReceived( - tctx.client, b"CONNECT example.com:443 HTTP/1.1\r\n\r\n" + tctx.client, + b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n", ) else: playbook >> DataReceived( @@ -668,11 +693,10 @@ def test_server_unreachable(tctx, connect): playbook << OpenConnection(server) playbook >> reply("Connection failed") if not connect: - # Our API isn't ideal here, there is no error hook for CONNECT requests currently. - # We could fix this either by having CONNECT request go through all our regular hooks, - # or by adding dedicated ok/error hooks. playbook << http.HttpErrorHook(flow) - playbook >> reply() + else: + playbook << http.HttpConnectErrorHook(flow) + playbook >> reply() playbook << SendData( tctx.client, BytesMatching(b"502 Bad Gateway.+Connection failed") ) @@ -680,9 +704,9 @@ def test_server_unreachable(tctx, connect): playbook << CloseConnection(tctx.client) assert playbook + assert not flow().live if not connect: assert flow().error - assert not flow().live @pytest.mark.parametrize( @@ -744,21 +768,28 @@ def test_upstream_proxy(tctx, redirect, domain, scheme): << OpenConnection(server) >> reply(None) << SendData( - server, b"GET http://%s/ HTTP/1.1\r\nHost: %s\r\n\r\n" % (domain, domain), + server, + b"GET http://%s/ HTTP/1.1\r\nHost: %s\r\n\r\n" % (domain, domain), ) ) else: assert ( playbook - >> DataReceived(tctx.client, b"CONNECT %s:443 HTTP/1.1\r\n\r\n" % domain) + >> DataReceived( + tctx.client, + b"CONNECT %s:443 HTTP/1.1\r\nHost: %s:443\r\n\r\n" % (domain, domain), + ) << SendData(tctx.client, b"HTTP/1.1 200 Connection established\r\n\r\n") >> DataReceived(tctx.client, b"GET / HTTP/1.1\r\nHost: %s\r\n\r\n" % domain) << layer.NextLayerHook(Placeholder()) >> reply_next_layer(lambda ctx: http.HttpLayer(ctx, HTTPMode.transparent)) << OpenConnection(server) >> reply(None) - << SendData(server, b"CONNECT %s:443 HTTP/1.1\r\n\r\n" % domain) + << SendData( + server, + b"CONNECT %s:443 HTTP/1.1\r\nHost: %s:443\r\n\r\n" % (domain, domain), + ) >> DataReceived(server, b"HTTP/1.1 200 Connection established\r\n\r\n") << SendData(server, b"GET / HTTP/1.1\r\nHost: %s\r\n\r\n" % domain) ) @@ -799,7 +830,8 @@ def test_upstream_proxy(tctx, redirect, domain, scheme): if redirect == "change-destination": playbook << SendData( server2, - b"GET http://%s.test/two HTTP/1.1\r\nHost: %s\r\n\r\n" % (domain, domain), + b"GET http://%s.test/two HTTP/1.1\r\nHost: %s\r\n\r\n" + % (domain, domain), ) else: playbook << SendData( @@ -808,12 +840,19 @@ def test_upstream_proxy(tctx, redirect, domain, scheme): ) else: if redirect == "change-destination": - playbook << SendData(server2, b"CONNECT %s.test:443 HTTP/1.1\r\n\r\n" % domain) + playbook << SendData( + server2, + b"CONNECT %s.test:443 HTTP/1.1\r\nHost: %s.test:443\r\n\r\n" + % (domain, domain), + ) playbook >> DataReceived( server2, b"HTTP/1.1 200 Connection established\r\n\r\n" ) elif redirect == "change-proxy": - playbook << SendData(server2, b"CONNECT %s:443 HTTP/1.1\r\n\r\n" % domain) + playbook << SendData( + server2, + b"CONNECT %s:443 HTTP/1.1\r\nHost: %s:443\r\n\r\n" % (domain, domain), + ) playbook >> DataReceived( server2, b"HTTP/1.1 200 Connection established\r\n\r\n" ) @@ -830,9 +869,7 @@ def test_upstream_proxy(tctx, redirect, domain, scheme): assert flow().server_conn.address[0] == domain.decode("idna") if redirect == "change-proxy": - assert ( - server2().address == flow().server_conn.via[1] == ("other-proxy", 1234) - ) + assert server2().address == flow().server_conn.via[1] == ("other-proxy", 1234) else: assert server2().address == flow().server_conn.via[1] == ("proxy", 8080) @@ -859,7 +896,9 @@ def test_http_proxy_tcp(tctx, mode, close_first): playbook = Playbook(toplayer, hooks=False) assert ( playbook - >> DataReceived(tctx.client, b"CONNECT example:443 HTTP/1.1\r\n\r\n") + >> DataReceived( + tctx.client, b"CONNECT example:443 HTTP/1.1\r\nHost: example:443\r\n\r\n" + ) << SendData(tctx.client, b"HTTP/1.1 200 Connection established\r\n\r\n") >> DataReceived(tctx.client, b"this is not http") << layer.NextLayerHook(Placeholder()) @@ -871,7 +910,9 @@ def test_http_proxy_tcp(tctx, mode, close_first): playbook >> reply(None) if mode == "upstream": - playbook << SendData(server, b"CONNECT example:443 HTTP/1.1\r\n\r\n") + playbook << SendData( + server, b"CONNECT example:443 HTTP/1.1\r\nHost: example:443\r\n\r\n" + ) playbook >> DataReceived(server, b"HTTP/1.1 200 Connection established\r\n\r\n") assert ( @@ -913,23 +954,26 @@ def test_proxy_chain(tctx, strategy): tctx.options.connection_strategy = strategy playbook = Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) - playbook >> DataReceived(tctx.client, b"CONNECT proxy:8080 HTTP/1.1\r\n\r\n") + playbook >> DataReceived( + tctx.client, b"CONNECT proxy:8080 HTTP/1.1\r\nHost: proxy:8080\r\n\r\n" + ) if strategy == "eager": playbook << OpenConnection(server) playbook >> reply(None) playbook << SendData(tctx.client, b"HTTP/1.1 200 Connection established\r\n\r\n") - playbook >> DataReceived(tctx.client, b"CONNECT second-proxy:8080 HTTP/1.1\r\n\r\n") + playbook >> DataReceived( + tctx.client, b"CONNECT second-proxy:8080 HTTP/1.1\r\nHost: proxy:8080\r\n\r\n" + ) playbook << layer.NextLayerHook(Placeholder()) playbook >> reply_next_layer(lambda ctx: http.HttpLayer(ctx, HTTPMode.transparent)) playbook << SendData( tctx.client, - b"HTTP/1.1 502 Bad Gateway\r\n" - b"content-length: 198\r\n" - b"\r\n" - b"mitmproxy received an HTTP CONNECT request even though it is not running in regular/upstream mode. " - b"This usually indicates a misconfiguration, please see the mitmproxy mode documentation for details.", + BytesMatching( + b"mitmproxy received an HTTP CONNECT request even though it is not running in regular/upstream mode." + ), ) + playbook << CloseConnection(tctx.client) assert playbook @@ -958,8 +1002,12 @@ def test_http_proxy_without_empty_chunk_in_head_request(tctx): << OpenConnection(server) >> reply(None) << SendData(server, b"HEAD / HTTP/1.1\r\n\r\n") - >> DataReceived(server, b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n") - << SendData(tctx.client, b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n") + >> DataReceived( + server, b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" + ) + << SendData( + tctx.client, b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" + ) ) @@ -1174,7 +1222,9 @@ def assert_kill(err_hook: bool = True): playbook = Playbook(http.HttpLayer(tctx, HTTPMode.regular)) assert ( playbook - >> DataReceived(tctx.client, b"CONNECT example.com:80 HTTP/1.1\r\n\r\n") + >> DataReceived( + tctx.client, b"CONNECT example.com:80 HTTP/1.1\r\nHost: example:80\r\n\r\n" + ) << http.HttpConnectHook(connect_flow) ) if when == "http_connect": @@ -1182,6 +1232,8 @@ def assert_kill(err_hook: bool = True): assert ( playbook >> reply() + << http.HttpConnectedHook(connect_flow) + >> reply() << SendData(tctx.client, b"HTTP/1.1 200 Connection established\r\n\r\n") >> DataReceived( tctx.client, b"GET /foo?hello=1 HTTP/1.1\r\nHost: example.com\r\n\r\n" @@ -1419,6 +1471,29 @@ def test_transparent_sni(tctx): assert server().sni == "example.com" +def test_reverse_sni(tctx): + """Test that we use the destination address as SNI in reverse mode.""" + tctx.client.sni = "localhost" + tctx.server.address = ("192.0.2.42", 443) + tctx.server.tls = True + tctx.server.sni = "example.local" + + flow = Placeholder(HTTPFlow) + + server = Placeholder(Server) + assert ( + Playbook(http.HttpLayer(tctx, HTTPMode.transparent)) + >> DataReceived(tctx.client, b"GET / HTTP/1.1\r\n\r\n") + << http.HttpRequestHeadersHook(flow) + >> reply() + << http.HttpRequestHook(flow) + >> reply() + << OpenConnection(server) + ) + assert server().address == ("192.0.2.42", 443) + assert server().sni == "example.local" + + def test_original_server_disconnects(tctx): """Test that we correctly handle the case where the initial server conn is just closed.""" tctx.server.state = ConnectionState.OPEN @@ -1443,7 +1518,7 @@ def test_request_smuggling(tctx): << SendData( tctx.client, BytesMatching( - b"Received both a Transfer-Encoding and a Content-Length header" + b"Disable the validate_inbound_headers option to skip this security check" ), ) << CloseConnection(tctx.client) @@ -1460,7 +1535,34 @@ def test_request_smuggling_whitespace(tctx): b"Host: example.com\r\n" b"Content-Length : 42\r\n\r\n", ) - << SendData(tctx.client, BytesMatching(b"Received an invalid header name")) + << SendData(tctx.client, BytesMatching(b"invalid header name")) + << CloseConnection(tctx.client) + ) + + +def test_request_smuggling_response(tctx): + """Test that we reject response smuggling""" + server = Placeholder(Server) + assert ( + Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) + >> DataReceived( + tctx.client, + b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n", + ) + << OpenConnection(server) + >> reply(None) + << SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + >> DataReceived( + server, + b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nContent-Length: 42\r\n\r\n", + ) + << CloseConnection(server) + << SendData( + tctx.client, + BytesMatching( + b"Disable the validate_inbound_headers option to skip this security check" + ), + ) << CloseConnection(tctx.client) ) @@ -1497,12 +1599,13 @@ def test_request_smuggling_te_te(tctx): "Transfer-Encoding: chunKed\r\n\r\n" ).encode(), ) # note the non-standard "K" - << SendData(tctx.client, BytesMatching(b"Invalid transfer encoding")) + << SendData(tctx.client, BytesMatching(b"invalid transfer-encoding header")) << CloseConnection(tctx.client) ) -def test_invalid_content_length(tctx): +@pytest.mark.parametrize("cl", [b"NaN", b"-1"]) +def test_invalid_content_length(tctx, cl): """Test that we still trigger flow hooks for requests with semantic errors""" flow = Placeholder(HTTPFlow) assert ( @@ -1512,10 +1615,10 @@ def test_invalid_content_length(tctx): ( b"GET http://example.com/ HTTP/1.1\r\n" b"Host: example.com\r\n" - b"Content-Length: NaN\r\n\r\n" + b"Content-Length: " + cl + b"\r\n\r\n" ), ) - << SendData(tctx.client, BytesMatching(b"Invalid Content-Length header")) + << SendData(tctx.client, BytesMatching(b"invalid content-length header")) << CloseConnection(tctx.client) << http.HttpRequestHeadersHook(flow) >> reply() @@ -1584,11 +1687,16 @@ def test_connect_more_newlines(tctx): assert ( playbook - >> DataReceived(tctx.client, b"CONNECT example.com:80 HTTP/1.1\r\n\r\n\r\n") + >> DataReceived( + tctx.client, + b"CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n\r\n", + ) << http.HttpConnectHook(Placeholder()) >> reply() << OpenConnection(server) >> reply(None) + << http.HttpConnectedHook(Placeholder()) + >> reply() << SendData(tctx.client, b"HTTP/1.1 200 Connection established\r\n\r\n") >> DataReceived(tctx.client, b"\x16\x03\x03\x00\xb3\x01\x00\x00\xaf\x03\x03") << layer.NextLayerHook(nl) @@ -1596,6 +1704,44 @@ def test_connect_more_newlines(tctx): assert nl().data_client() == b"\x16\x03\x03\x00\xb3\x01\x00\x00\xaf\x03\x03" +def test_connect_unauthorized(tctx): + """Continue a connection after proxyauth returns a 407, https://github.com/mitmproxy/mitmproxy/issues/6420""" + playbook = Playbook(http.HttpLayer(tctx, HTTPMode.regular)) + flow = Placeholder(HTTPFlow) + + def require_auth(f: HTTPFlow): + f.response = Response.make( + status_code=407, headers={"Proxy-Authenticate": f'Basic realm="mitmproxy"'} + ) + + assert ( + playbook + >> DataReceived( + tctx.client, + b"CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n", + ) + << http.HttpConnectHook(flow) + >> reply(side_effect=require_auth) + << http.HttpConnectErrorHook(flow) + >> reply() + << SendData( + tctx.client, + b"HTTP/1.1 407 Proxy Authentication Required\r\n" + b'Proxy-Authenticate: Basic realm="mitmproxy"\r\n' + b"content-length: 0\r\n\r\n", + ) + >> DataReceived( + tctx.client, + b"CONNECT example.com:80 HTTP/1.1\r\n" + b"Host: example.com:80\r\n" + b"Proxy-Authorization: Basic dGVzdDp0ZXN0\r\n\r\n", + ) + << http.HttpConnectHook(Placeholder(HTTPFlow)) + >> reply() + << OpenConnection(Placeholder(Server)) + ) + + def flows_tracked() -> int: return sum(isinstance(x, HTTPFlow) for x in gc.get_objects()) @@ -1658,7 +1804,7 @@ def test_drop_stream_with_paused_events(tctx): << http.HttpRequestHeadersHook(flow) >> reply() << OpenConnection(server) - >> reply('Connection killed: error') + >> reply("Connection killed: error") << http.HttpErrorHook(flow) >> reply() << SendData(tctx.client, BytesMatching(b"502 Bad Gateway.+Connection killed")) diff --git a/test/mitmproxy/proxy/layers/http/test_http1.py b/test/mitmproxy/proxy/layers/http/test_http1.py index 05e84e1fc1..160493ad1b 100644 --- a/test/mitmproxy/proxy/layers/http/test_http1.py +++ b/test/mitmproxy/proxy/layers/http/test_http1.py @@ -3,18 +3,17 @@ from mitmproxy import http from mitmproxy.proxy.commands import SendData from mitmproxy.proxy.events import DataReceived -from mitmproxy.proxy.layers.http import ( - Http1Server, - ReceiveHttp, - RequestHeaders, - RequestEndOfMessage, - ResponseHeaders, - ResponseEndOfMessage, - RequestData, - Http1Client, - ResponseData, -) -from test.mitmproxy.proxy.tutils import Placeholder, Playbook +from mitmproxy.proxy.layers.http import Http1Client +from mitmproxy.proxy.layers.http import Http1Server +from mitmproxy.proxy.layers.http import ReceiveHttp +from mitmproxy.proxy.layers.http import RequestData +from mitmproxy.proxy.layers.http import RequestEndOfMessage +from mitmproxy.proxy.layers.http import RequestHeaders +from mitmproxy.proxy.layers.http import ResponseData +from mitmproxy.proxy.layers.http import ResponseEndOfMessage +from mitmproxy.proxy.layers.http import ResponseHeaders +from test.mitmproxy.proxy.tutils import Placeholder +from test.mitmproxy.proxy.tutils import Playbook class TestServer: diff --git a/test/mitmproxy/proxy/layers/http/test_http2.py b/test/mitmproxy/proxy/layers/http/test_http2.py index 6609f96ead..5829faa089 100644 --- a/test/mitmproxy/proxy/layers/http/test_http2.py +++ b/test/mitmproxy/proxy/layers/http/test_http2.py @@ -1,28 +1,35 @@ +import time +from logging import DEBUG + import h2.settings import hpack import hyperframe.frame import pytest -import time from h2.errors import ErrorCodes -from mitmproxy.connection import ConnectionState, Server +from mitmproxy.connection import ConnectionState +from mitmproxy.connection import Server from mitmproxy.flow import Error -from mitmproxy.http import HTTPFlow, Headers, Request +from mitmproxy.http import Headers +from mitmproxy.http import HTTPFlow +from mitmproxy.http import Request from mitmproxy.net.http import status_codes -from mitmproxy.proxy.commands import ( - CloseConnection, - Log, - OpenConnection, - SendData, - RequestWakeup, -) +from mitmproxy.proxy.commands import CloseConnection +from mitmproxy.proxy.commands import Log +from mitmproxy.proxy.commands import OpenConnection +from mitmproxy.proxy.commands import RequestWakeup +from mitmproxy.proxy.commands import SendData from mitmproxy.proxy.context import Context -from mitmproxy.proxy.events import ConnectionClosed, DataReceived +from mitmproxy.proxy.events import ConnectionClosed +from mitmproxy.proxy.events import DataReceived from mitmproxy.proxy.layers import http from mitmproxy.proxy.layers.http import HTTPMode -from mitmproxy.proxy.layers.http._http2 import Http2Client, split_pseudo_headers +from mitmproxy.proxy.layers.http._http2 import Http2Client +from mitmproxy.proxy.layers.http._http2 import split_pseudo_headers from test.mitmproxy.proxy.layers.http.hyper_h2_test_helpers import FrameFactory -from test.mitmproxy.proxy.tutils import Placeholder, Playbook, reply +from test.mitmproxy.proxy.tutils import Placeholder +from test.mitmproxy.proxy.tutils import Playbook +from test.mitmproxy.proxy.tutils import reply example_request_headers = ( (b":method", b"GET"), @@ -42,7 +49,7 @@ def open_h2_server_conn(): # this is a bit fake here (port 80, with alpn, but no tls - c'mon), # but we don't want to pollute our tests with TLS handshakes. - s = Server(("example.com", 80)) + s = Server(address=("example.com", 80)) s.state = ConnectionState.OPEN s.alpn = b"h2" return s @@ -79,6 +86,9 @@ def start_h2_client(tctx: Context, keepalive: int = 0) -> tuple[Playbook, FrameF def make_h2(open_connection: OpenConnection) -> None: + assert isinstance( + open_connection, OpenConnection + ), f"Expected OpenConnection event, not {open_connection}" open_connection.connection.alpn = b"h2" @@ -106,6 +116,7 @@ def test_simple(tctx): frames = decode_frames(initial()) assert [type(x) for x in frames] == [ hyperframe.frame.SettingsFrame, + hyperframe.frame.WindowUpdateFrame, hyperframe.frame.HeadersFrame, ] sff = FrameFactory() @@ -248,6 +259,7 @@ def enable_streaming(flow: HTTPFlow): frames = decode_frames(server_data1.setdefault(b"") + server_data2()) assert [type(x) for x in frames] == [ hyperframe.frame.SettingsFrame, + hyperframe.frame.WindowUpdateFrame, hyperframe.frame.HeadersFrame, hyperframe.frame.DataFrame, hyperframe.frame.HeadersFrame, @@ -313,6 +325,7 @@ def test_long_response(tctx: Context, trailers): frames = decode_frames(initial()) assert [type(x) for x in frames] == [ hyperframe.frame.SettingsFrame, + hyperframe.frame.WindowUpdateFrame, hyperframe.frame.HeadersFrame, ] sff = FrameFactory() @@ -325,8 +338,7 @@ def test_long_response(tctx: Context, trailers): << http.HttpResponseHeadersHook(flow) >> reply() >> DataReceived( - server, - sff.build_data_frame(b"a" * 10000, flags=[]).serialize() + server, sff.build_data_frame(b"a" * 10000, flags=[]).serialize() ) >> DataReceived( server, @@ -340,11 +352,6 @@ def test_long_response(tctx: Context, trailers): server, sff.build_data_frame(b"a" * 10000, flags=[]).serialize(), ) - << SendData( - server, - sff.build_window_update_frame(0, 40000).serialize() - + sff.build_window_update_frame(1, 40000).serialize(), - ) >> DataReceived( server, sff.build_data_frame(b"a" * 10000, flags=[]).serialize(), @@ -373,9 +380,7 @@ def test_long_response(tctx: Context, trailers): playbook >> DataReceived( server, - sff.build_data_frame( - b'', flags=["END_STREAM"] - ).serialize(), + sff.build_data_frame(b"", flags=["END_STREAM"]).serialize(), ) ) ( @@ -412,10 +417,7 @@ def test_long_response(tctx: Context, trailers): tctx.client, cff.build_data_frame(b"a" * 1).serialize(), ) - << SendData( - tctx.client, - cff.build_data_frame(b"a" * 4464).serialize() - ) + << SendData(tctx.client, cff.build_data_frame(b"a" * 4464).serialize()) << SendData( tctx.client, cff.build_headers_frame( @@ -430,15 +432,10 @@ def test_long_response(tctx: Context, trailers): tctx.client, cff.build_data_frame(b"a" * 1).serialize(), ) + << SendData(tctx.client, cff.build_data_frame(b"a" * 4464).serialize()) << SendData( tctx.client, - cff.build_data_frame(b"a" * 4464).serialize() - ) - << SendData( - tctx.client, - cff.build_data_frame( - b"", flags=["END_STREAM"] - ).serialize(), + cff.build_data_frame(b"", flags=["END_STREAM"]).serialize(), ) ) assert flow().request.url == "http://example.com/" @@ -591,10 +588,11 @@ def test_no_normalization(tctx, normalize): frames = decode_frames(initial()) assert [type(x) for x in frames] == [ hyperframe.frame.SettingsFrame, + hyperframe.frame.WindowUpdateFrame, hyperframe.frame.HeadersFrame, ] assert ( - hpack.hpack.Decoder().decode(frames[1].data, True) == request_headers_lower + hpack.hpack.Decoder().decode(frames[2].data, True) == request_headers_lower if normalize else request_headers ) @@ -624,6 +622,61 @@ def test_no_normalization(tctx, normalize): assert flow().response.headers.fields == ((b"Same", b"Here"),) +@pytest.mark.parametrize("stream", ["stream", ""]) +def test_end_stream_via_headers(tctx, stream): + playbook, cff = start_h2_client(tctx) + server = Placeholder(Server) + flow = Placeholder(HTTPFlow) + sff = FrameFactory() + forwarded_request_frames = Placeholder(bytes) + forwarded_response_frames = Placeholder(bytes) + + def enable_streaming(flow: HTTPFlow): + flow.request.stream = bool(stream) + + assert ( + playbook + >> DataReceived( + tctx.client, + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"] + ).serialize(), + ) + << http.HttpRequestHeadersHook(flow) + >> reply(side_effect=enable_streaming) + << http.HttpRequestHook(flow) + >> reply() + << OpenConnection(server) + >> reply(None, side_effect=make_h2) + << SendData(server, forwarded_request_frames) + >> DataReceived( + server, + sff.build_headers_frame( + example_response_headers, flags=["END_STREAM"] + ).serialize(), + ) + << http.HttpResponseHeadersHook(flow) + >> reply() + << http.HttpResponseHook(flow) + >> reply() + << SendData(tctx.client, forwarded_response_frames) + ) + + frames = decode_frames(forwarded_request_frames()) + assert [type(x) for x in frames] == [ + hyperframe.frame.SettingsFrame, + hyperframe.frame.WindowUpdateFrame, + hyperframe.frame.HeadersFrame, + ] + assert "END_STREAM" in frames[2].flags + + frames = decode_frames(forwarded_response_frames()) + assert [type(x) for x in frames] == [ + hyperframe.frame.HeadersFrame, + ] + assert "END_STREAM" in frames[0].flags + + @pytest.mark.parametrize( "input,pseudo,headers", [ @@ -808,6 +861,7 @@ def test_stream_concurrency(tctx): frames = decode_frames(data_req2()) assert [type(x) for x in frames] == [ hyperframe.frame.SettingsFrame, + hyperframe.frame.WindowUpdateFrame, hyperframe.frame.HeadersFrame, ] frames = decode_frames(data_req1()) @@ -867,14 +921,14 @@ def test_max_concurrency(tctx): ) << SendData(tctx.client, Placeholder(bytes)) ) - settings, req1 = decode_frames(req1_bytes()) + settings, _, req1 = decode_frames(req1_bytes()) (settings_ack,) = decode_frames(settings_ack_bytes()) (req2,) = decode_frames(req2_bytes()) - assert type(settings) == hyperframe.frame.SettingsFrame - assert type(req1) == hyperframe.frame.HeadersFrame - assert type(settings_ack) == hyperframe.frame.SettingsFrame - assert type(req2) == hyperframe.frame.HeadersFrame + assert type(settings) is hyperframe.frame.SettingsFrame + assert type(req1) is hyperframe.frame.HeadersFrame + assert type(settings_ack) is hyperframe.frame.SettingsFrame + assert type(req2) is hyperframe.frame.HeadersFrame assert req1.stream_id == 1 assert req2.stream_id == 3 @@ -908,6 +962,7 @@ def test_stream_concurrent_get_connection(tctx): frames = decode_frames(data()) assert [type(x) for x in frames] == [ hyperframe.frame.SettingsFrame, + hyperframe.frame.WindowUpdateFrame, hyperframe.frame.HeadersFrame, hyperframe.frame.HeadersFrame, ] @@ -960,6 +1015,7 @@ def kill(flow: HTTPFlow): frames = decode_frames(data_req1()) assert [type(x) for x in frames] == [ hyperframe.frame.SettingsFrame, + hyperframe.frame.WindowUpdateFrame, hyperframe.frame.HeadersFrame, ] @@ -1067,6 +1123,7 @@ def test_early_server_data(tctx): ) assert [type(x) for x in decode_frames(server1())] == [ hyperframe.frame.SettingsFrame, + hyperframe.frame.WindowUpdateFrame, hyperframe.frame.SettingsFrame, ] assert [type(x) for x in decode_frames(server2())] == [ @@ -1196,3 +1253,31 @@ def advance_time(_): >> reply(to=wakeup_command, side_effect=advance_time) << None ) + + +def test_alt_svc(tctx): + playbook, cff = start_h2_client(tctx) + flow = Placeholder(HTTPFlow) + server = Placeholder(Server) + initial = Placeholder(bytes) + + assert ( + playbook + >> DataReceived( + tctx.client, + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"] + ).serialize(), + ) + << http.HttpRequestHeadersHook(flow) + >> reply() + << http.HttpRequestHook(flow) + >> reply() + << OpenConnection(server) + >> reply(None, side_effect=make_h2) + << SendData(server, initial) + >> DataReceived( + server, cff.build_alt_svc_frame(0, b"example.com", b'h3=":443"').serialize() + ) + << Log("Received HTTP/2 Alt-Svc frame, which will not be forwarded.", DEBUG) + ) diff --git a/test/mitmproxy/proxy/layers/http/test_http3.py b/test/mitmproxy/proxy/layers/http/test_http3.py new file mode 100644 index 0000000000..2079ebbaf8 --- /dev/null +++ b/test/mitmproxy/proxy/layers/http/test_http3.py @@ -0,0 +1,1173 @@ +from collections.abc import Callable +from collections.abc import Iterable + +import pylsqpack +import pytest +from aioquic._buffer import Buffer +from aioquic.h3.connection import encode_frame +from aioquic.h3.connection import encode_settings +from aioquic.h3.connection import encode_uint_var +from aioquic.h3.connection import ErrorCode +from aioquic.h3.connection import FrameType +from aioquic.h3.connection import Headers as H3Headers +from aioquic.h3.connection import parse_settings +from aioquic.h3.connection import Setting +from aioquic.h3.connection import StreamType +from aioquic.quic.packet import QuicErrorCode + +from mitmproxy import connection +from mitmproxy import version +from mitmproxy.flow import Error +from mitmproxy.http import Headers +from mitmproxy.http import HTTPFlow +from mitmproxy.http import Request +from mitmproxy.proxy import commands +from mitmproxy.proxy import context +from mitmproxy.proxy import events +from mitmproxy.proxy import layers +from mitmproxy.proxy.layers import http +from mitmproxy.proxy.layers import quic +from mitmproxy.proxy.layers.http._http3 import Http3Client +from test.mitmproxy.proxy import tutils + +example_request_headers = [ + (b":method", b"GET"), + (b":scheme", b"http"), + (b":path", b"/"), + (b":authority", b"example.com"), +] + +example_response_headers = [(b":status", b"200")] + +example_request_trailers = [(b"req-trailer-a", b"a"), (b"req-trailer-b", b"b")] + +example_response_trailers = [(b"resp-trailer-a", b"a"), (b"resp-trailer-b", b"b")] + + +def decode_frame(frame_type: int, frame_data: bytes) -> bytes: + buf = Buffer(data=frame_data) + assert buf.pull_uint_var() == frame_type + return buf.pull_bytes(buf.pull_uint_var()) + + +class CallbackPlaceholder(tutils._Placeholder[bytes]): + """Data placeholder that invokes a callback once its bytes get set.""" + + def __init__(self, cb: Callable[[bytes], None]): + super().__init__(bytes) + self._cb = cb + + def setdefault(self, value: bytes) -> bytes: + if self._obj is None: + self._cb(value) + return super().setdefault(value) + + +class DelayedPlaceholder(tutils._Placeholder[bytes]): + """Data placeholder that resolves its bytes when needed.""" + + def __init__(self, resolve: Callable[[], bytes]): + super().__init__(bytes) + self._resolve = resolve + + def __call__(self) -> bytes: + if self._obj is None: + self._obj = self._resolve() + return super().__call__() + + +class FrameFactory: + """Helper class for generating QUIC stream events and commands.""" + + def __init__(self, conn: connection.Connection, is_client: bool) -> None: + self.conn = conn + self.is_client = is_client + self.decoder = pylsqpack.Decoder( + max_table_capacity=4096, + blocked_streams=16, + ) + self.decoder_placeholders: list[tutils.Placeholder[bytes]] = [] + self.encoder = pylsqpack.Encoder() + self.encoder_placeholder: tutils.Placeholder[bytes] | None = None + self.peer_stream_id: dict[StreamType, int] = {} + self.local_stream_id: dict[StreamType, int] = {} + + def get_default_stream_id(self, stream_type: StreamType, for_local: bool) -> int: + if stream_type == StreamType.CONTROL: + stream_id = 2 + elif stream_type == StreamType.QPACK_ENCODER: + stream_id = 6 + elif stream_type == StreamType.QPACK_DECODER: + stream_id = 10 + else: + raise AssertionError(stream_type) + if self.is_client is not for_local: + stream_id = stream_id + 1 + return stream_id + + def send_stream_type( + self, + stream_type: StreamType, + stream_id: int | None = None, + ) -> quic.SendQuicStreamData: + assert stream_type not in self.peer_stream_id + if stream_id is None: + stream_id = self.get_default_stream_id(stream_type, for_local=False) + self.peer_stream_id[stream_type] = stream_id + return quic.SendQuicStreamData( + connection=self.conn, + stream_id=stream_id, + data=encode_uint_var(stream_type), + end_stream=False, + ) + + def receive_stream_type( + self, + stream_type: StreamType, + stream_id: int | None = None, + ) -> quic.QuicStreamDataReceived: + assert stream_type not in self.local_stream_id + if stream_id is None: + stream_id = self.get_default_stream_id(stream_type, for_local=True) + self.local_stream_id[stream_type] = stream_id + return quic.QuicStreamDataReceived( + connection=self.conn, + stream_id=stream_id, + data=encode_uint_var(stream_type), + end_stream=False, + ) + + def send_settings(self) -> quic.SendQuicStreamData: + assert self.encoder_placeholder is None + placeholder = tutils.Placeholder(bytes) + self.encoder_placeholder = placeholder + + def cb(data: bytes) -> None: + buf = Buffer(data=data) + assert buf.pull_uint_var() == FrameType.SETTINGS + settings = parse_settings(buf.pull_bytes(buf.pull_uint_var())) + placeholder.setdefault( + self.encoder.apply_settings( + max_table_capacity=settings[Setting.QPACK_MAX_TABLE_CAPACITY], + blocked_streams=settings[Setting.QPACK_BLOCKED_STREAMS], + ) + ) + + return quic.SendQuicStreamData( + connection=self.conn, + stream_id=self.peer_stream_id[StreamType.CONTROL], + data=CallbackPlaceholder(cb), + end_stream=False, + ) + + def receive_settings( + self, + settings: dict[int, int] = { + Setting.QPACK_MAX_TABLE_CAPACITY: 4096, + Setting.QPACK_BLOCKED_STREAMS: 16, + Setting.ENABLE_CONNECT_PROTOCOL: 1, + Setting.DUMMY: 1, + }, + ) -> quic.QuicStreamDataReceived: + return quic.QuicStreamDataReceived( + connection=self.conn, + stream_id=self.local_stream_id[StreamType.CONTROL], + data=encode_frame(FrameType.SETTINGS, encode_settings(settings)), + end_stream=False, + ) + + def send_encoder(self) -> quic.SendQuicStreamData: + def cb(data: bytes) -> bytes: + self.decoder.feed_encoder(data) + return data + + return quic.SendQuicStreamData( + connection=self.conn, + stream_id=self.peer_stream_id[StreamType.QPACK_ENCODER], + data=CallbackPlaceholder(cb), + end_stream=False, + ) + + def receive_encoder(self) -> quic.QuicStreamDataReceived: + assert self.encoder_placeholder is not None + placeholder = self.encoder_placeholder + self.encoder_placeholder = None + + return quic.QuicStreamDataReceived( + connection=self.conn, + stream_id=self.local_stream_id[StreamType.QPACK_ENCODER], + data=placeholder, + end_stream=False, + ) + + def send_decoder(self) -> quic.SendQuicStreamData: + def cb(data: bytes) -> None: + self.encoder.feed_decoder(data) + + return quic.SendQuicStreamData( + self.conn, + stream_id=self.peer_stream_id[StreamType.QPACK_DECODER], + data=CallbackPlaceholder(cb), + end_stream=False, + ) + + def receive_decoder(self) -> quic.QuicStreamDataReceived: + assert self.decoder_placeholders + placeholder = self.decoder_placeholders.pop(0) + + return quic.QuicStreamDataReceived( + self.conn, + stream_id=self.local_stream_id[StreamType.QPACK_DECODER], + data=placeholder, + end_stream=False, + ) + + def send_headers( + self, + headers: H3Headers, + stream_id: int = 0, + end_stream: bool = False, + ) -> Iterable[quic.SendQuicStreamData]: + placeholder = tutils.Placeholder(bytes) + self.decoder_placeholders.append(placeholder) + + def decode(data: bytes) -> None: + buf = Buffer(data=data) + assert buf.pull_uint_var() == FrameType.HEADERS + frame_data = buf.pull_bytes(buf.pull_uint_var()) + decoder, actual_headers = self.decoder.feed_header(stream_id, frame_data) + placeholder.setdefault(decoder) + assert headers == actual_headers + + yield self.send_encoder() + yield quic.SendQuicStreamData( + connection=self.conn, + stream_id=stream_id, + data=CallbackPlaceholder(decode), + end_stream=end_stream, + ) + + def receive_headers( + self, + headers: H3Headers, + stream_id: int = 0, + end_stream: bool = False, + ) -> Iterable[quic.QuicStreamDataReceived]: + data = tutils.Placeholder(bytes) + + def encode() -> bytes: + encoder, frame_data = self.encoder.encode(stream_id, headers) + data.setdefault(encode_frame(FrameType.HEADERS, frame_data)) + return encoder + + yield quic.QuicStreamDataReceived( + connection=self.conn, + stream_id=self.local_stream_id[StreamType.QPACK_ENCODER], + data=DelayedPlaceholder(encode), + end_stream=False, + ) + yield quic.QuicStreamDataReceived( + connection=self.conn, + stream_id=stream_id, + data=data, + end_stream=end_stream, + ) + + def send_data( + self, + data: bytes, + stream_id: int = 0, + end_stream: bool = False, + ) -> quic.SendQuicStreamData: + return quic.SendQuicStreamData( + self.conn, + stream_id=stream_id, + data=encode_frame(FrameType.DATA, data), + end_stream=end_stream, + ) + + def receive_data( + self, + data: bytes, + stream_id: int = 0, + end_stream: bool = False, + ) -> quic.QuicStreamDataReceived: + return quic.QuicStreamDataReceived( + connection=self.conn, + stream_id=stream_id, + data=encode_frame(FrameType.DATA, data), + end_stream=end_stream, + ) + + def send_reset( + self, error_code: ErrorCode, stream_id: int = 0 + ) -> quic.ResetQuicStream: + return quic.ResetQuicStream( + connection=self.conn, + stream_id=stream_id, + error_code=int(error_code), + ) + + def receive_reset( + self, error_code: ErrorCode, stream_id: int = 0 + ) -> quic.QuicStreamReset: + return quic.QuicStreamReset( + connection=self.conn, + stream_id=stream_id, + error_code=int(error_code), + ) + + def send_stop( + self, error_code: ErrorCode, stream_id: int = 0 + ) -> quic.StopSendingQuicStream: + return quic.StopSendingQuicStream( + connection=self.conn, + stream_id=stream_id, + error_code=int(error_code), + ) + + def receive_stop( + self, error_code: ErrorCode, stream_id: int = 0 + ) -> quic.QuicStreamStopSending: + return quic.QuicStreamStopSending( + connection=self.conn, + stream_id=stream_id, + error_code=int(error_code), + ) + + def send_init(self) -> Iterable[quic.SendQuicStreamData]: + yield self.send_stream_type(StreamType.CONTROL) + yield self.send_settings() + yield self.send_stream_type(StreamType.QPACK_ENCODER) + yield self.send_stream_type(StreamType.QPACK_DECODER) + + def receive_init(self) -> Iterable[quic.QuicStreamDataReceived]: + yield self.receive_stream_type(StreamType.CONTROL) + yield self.receive_stream_type(StreamType.QPACK_ENCODER) + yield self.receive_stream_type(StreamType.QPACK_DECODER) + yield self.receive_settings() + + @property + def is_done(self) -> bool: + return self.encoder_placeholder is None and not self.decoder_placeholders + + +@pytest.fixture +def open_h3_server_conn(): + # this is a bit fake here (port 80, with alpn, but no tls - c'mon), + # but we don't want to pollute our tests with TLS handshakes. + server = connection.Server(address=("example.com", 80), transport_protocol="udp") + server.state = connection.ConnectionState.OPEN + server.alpn = b"h3" + return server + + +def start_h3_proxy(tctx: context.Context) -> tuple[tutils.Playbook, FrameFactory]: + tctx.client.alpn = b"h3" + tctx.client.transport_protocol = "udp" + tctx.server.transport_protocol = "udp" + + playbook = tutils.Playbook(layers.HttpLayer(tctx, layers.http.HTTPMode.regular)) + cff = FrameFactory(conn=tctx.client, is_client=True) + assert ( + playbook + << cff.send_init() + >> cff.receive_init() + << cff.send_encoder() + >> cff.receive_encoder() + ) + return playbook, cff + + +def make_h3(open_connection: commands.OpenConnection) -> None: + open_connection.connection.alpn = b"h3" + + +def test_ignore_push(tctx: context.Context): + playbook, cff = start_h3_proxy(tctx) + + +def test_fail_without_header(tctx: context.Context): + playbook = tutils.Playbook(layers.http.Http3Server(tctx)) + cff = FrameFactory(tctx.client, is_client=True) + assert ( + playbook + << cff.send_init() + >> cff.receive_init() + << cff.send_encoder() + >> cff.receive_encoder() + >> http.ResponseProtocolError(0, "first message", http.status_codes.NO_RESPONSE) + << cff.send_reset(ErrorCode.H3_INTERNAL_ERROR) + << cff.send_stop(ErrorCode.H3_INTERNAL_ERROR) + ) + assert cff.is_done + + +def test_invalid_header(tctx: context.Context): + playbook, cff = start_h3_proxy(tctx) + assert ( + playbook + >> cff.receive_headers( + [ + (b":method", b"CONNECT"), + (b":path", b"/"), + (b":authority", b"example.com"), + ], + end_stream=True, + ) + << cff.send_decoder() # for receive_headers + << quic.CloseQuicConnection( + tctx.client, + error_code=ErrorCode.H3_GENERAL_PROTOCOL_ERROR.value, + frame_type=None, + reason_phrase="Invalid HTTP/3 request headers: Required pseudo header is missing: b':scheme'", + ) + # ensure that once we close, we don't process messages anymore + >> cff.receive_headers( + [ + (b":method", b"CONNECT"), + (b":path", b"/"), + (b":authority", b"example.com"), + ], + end_stream=True, + ) + ) + + +def test_simple(tctx: context.Context): + playbook, cff = start_h3_proxy(tctx) + flow = tutils.Placeholder(HTTPFlow) + server = tutils.Placeholder(connection.Server) + sff = FrameFactory(server, is_client=False) + assert ( + playbook + # request client + >> cff.receive_headers(example_request_headers, end_stream=True) + << (request := http.HttpRequestHeadersHook(flow)) + << cff.send_decoder() # for receive_headers + >> tutils.reply(to=request) + << http.HttpRequestHook(flow) + >> tutils.reply() + # request server + << commands.OpenConnection(server) + >> tutils.reply(None, side_effect=make_h3) + << sff.send_init() + << sff.send_headers(example_request_headers, end_stream=True) + >> sff.receive_init() + << sff.send_encoder() + >> sff.receive_encoder() + >> sff.receive_decoder() # for send_headers + # response server + >> sff.receive_headers(example_response_headers) + << (response := http.HttpResponseHeadersHook(flow)) + << sff.send_decoder() # for receive_headers + >> tutils.reply(to=response) + >> sff.receive_data(b"Hello, World!", end_stream=True) + << http.HttpResponseHook(flow) + >> tutils.reply() + # response client + << cff.send_headers(example_response_headers) + << cff.send_data(b"Hello, World!") + << cff.send_data(b"", end_stream=True) + >> cff.receive_decoder() # for send_headers + ) + assert cff.is_done and sff.is_done + assert flow().request.url == "http://example.com/" + assert flow().response.text == "Hello, World!" + + +@pytest.mark.parametrize("stream", ["stream", ""]) +def test_response_trailers( + tctx: context.Context, + open_h3_server_conn: connection.Server, + stream: str, +): + playbook, cff = start_h3_proxy(tctx) + tctx.server = open_h3_server_conn + sff = FrameFactory(tctx.server, is_client=False) + + def enable_streaming(flow: HTTPFlow): + flow.response.stream = stream + + flow = tutils.Placeholder(HTTPFlow) + ( + playbook + # request client + >> cff.receive_headers(example_request_headers, end_stream=True) + << (request := http.HttpRequestHeadersHook(flow)) + << cff.send_decoder() # for receive_headers + >> tutils.reply(to=request) + << http.HttpRequestHook(flow) + >> tutils.reply() + # request server + << sff.send_init() + << sff.send_headers(example_request_headers, end_stream=True) + >> sff.receive_init() + << sff.send_encoder() + >> sff.receive_encoder() + >> sff.receive_decoder() # for send_headers + # response server + >> sff.receive_headers(example_response_headers) + << (response_headers := http.HttpResponseHeadersHook(flow)) + << sff.send_decoder() # for receive_headers + >> tutils.reply(to=response_headers, side_effect=enable_streaming) + ) + if stream: + ( + playbook + << cff.send_headers(example_response_headers) + >> cff.receive_decoder() # for send_headers + >> sff.receive_data(b"Hello, World!") + << cff.send_data(b"Hello, World!") + ) + else: + playbook >> sff.receive_data(b"Hello, World!") + assert ( + playbook + >> sff.receive_headers(example_response_trailers, end_stream=True) + << (response := http.HttpResponseHook(flow)) + << sff.send_decoder() # for receive_headers + ) + assert flow().response.trailers + del flow().response.trailers["resp-trailer-a"] + if stream: + assert ( + playbook + >> tutils.reply(to=response) + << cff.send_headers(example_response_trailers[1:], end_stream=True) + >> cff.receive_decoder() # for send_headers + ) + else: + assert ( + playbook + >> tutils.reply(to=response) + << cff.send_headers(example_response_headers) + << cff.send_data(b"Hello, World!") + << cff.send_headers(example_response_trailers[1:], end_stream=True) + >> cff.receive_decoder() # for send_headers + >> cff.receive_decoder() # for send_headers + ) + assert cff.is_done and sff.is_done + + +@pytest.mark.parametrize("stream", ["stream", ""]) +def test_request_trailers( + tctx: context.Context, + open_h3_server_conn: connection.Server, + stream: str, +): + playbook, cff = start_h3_proxy(tctx) + tctx.server = open_h3_server_conn + sff = FrameFactory(tctx.server, is_client=False) + + def enable_streaming(flow: HTTPFlow): + flow.request.stream = stream + + flow = tutils.Placeholder(HTTPFlow) + ( + playbook + # request client + >> cff.receive_headers(example_request_headers) + << (request_headers := http.HttpRequestHeadersHook(flow)) + << cff.send_decoder() # for receive_headers + >> cff.receive_data(b"Hello World!") + >> tutils.reply(to=request_headers, side_effect=enable_streaming) + ) + if not stream: + ( + playbook + >> cff.receive_headers(example_request_trailers, end_stream=True) + << (request := http.HttpRequestHook(flow)) + << cff.send_decoder() # for receive_headers + >> tutils.reply(to=request) + ) + ( + playbook + # request server + << sff.send_init() + << sff.send_headers(example_request_headers) + << sff.send_data(b"Hello World!") + ) + if not stream: + playbook << sff.send_headers(example_request_trailers, end_stream=True) + ( + playbook + >> sff.receive_init() + << sff.send_encoder() + >> sff.receive_encoder() + >> sff.receive_decoder() # for send_headers + ) + if stream: + ( + playbook + >> cff.receive_headers(example_request_trailers, end_stream=True) + << (request := http.HttpRequestHook(flow)) + << cff.send_decoder() # for receive_headers + >> tutils.reply(to=request) + << sff.send_headers(example_request_trailers, end_stream=True) + ) + assert playbook >> sff.receive_decoder() # for send_headers + + assert cff.is_done and sff.is_done + + +def test_upstream_error(tctx: context.Context): + playbook, cff = start_h3_proxy(tctx) + flow = tutils.Placeholder(HTTPFlow) + server = tutils.Placeholder(connection.Server) + err = tutils.Placeholder(bytes) + assert ( + playbook + # request client + >> cff.receive_headers(example_request_headers, end_stream=True) + << (request := http.HttpRequestHeadersHook(flow)) + << cff.send_decoder() # for receive_headers + >> tutils.reply(to=request) + << http.HttpRequestHook(flow) + >> tutils.reply() + # request server + << commands.OpenConnection(server) + >> tutils.reply("oops server <> error") + << http.HttpErrorHook(flow) + >> tutils.reply() + << cff.send_headers( + [ + (b":status", b"502"), + (b"server", version.MITMPROXY.encode()), + (b"content-type", b"text/html"), + ] + ) + << quic.SendQuicStreamData( + tctx.client, + stream_id=0, + data=err, + end_stream=True, + ) + >> cff.receive_decoder() # for send_headers + ) + assert cff.is_done + data = decode_frame(FrameType.DATA, err()) + assert b"502 Bad Gateway" in data + assert b"server <> error" in data + + +@pytest.mark.parametrize("stream", ["stream", ""]) +@pytest.mark.parametrize("when", ["request", "response"]) +@pytest.mark.parametrize("how", ["RST", "disconnect", "RST+disconnect"]) +def test_http3_client_aborts(tctx: context.Context, stream: str, when: str, how: str): + """ + Test handling of the case where a client aborts during request or response transmission. + + If the client aborts the request transmission, we must trigger an error hook, + if the client disconnects during response transmission, no error hook is triggered. + """ + server = tutils.Placeholder(connection.Server) + flow = tutils.Placeholder(HTTPFlow) + playbook, cff = start_h3_proxy(tctx) + + def enable_request_streaming(flow: HTTPFlow): + flow.request.stream = True + + def enable_response_streaming(flow: HTTPFlow): + flow.response.stream = True + + assert ( + playbook + >> cff.receive_headers(example_request_headers) + << (request_headers := http.HttpRequestHeadersHook(flow)) + << cff.send_decoder() # for receive_headers + ) + if stream and when == "request": + assert ( + playbook + >> tutils.reply(side_effect=enable_request_streaming, to=request_headers) + << commands.OpenConnection(server) + >> tutils.reply(None) + << commands.SendData( + server, b"GET / HTTP/1.1\r\n" b"Host: example.com\r\n\r\n" + ) + ) + else: + assert playbook >> tutils.reply(to=request_headers) + + if when == "request": + if "RST" in how: + playbook >> cff.receive_reset(ErrorCode.H3_REQUEST_CANCELLED) + else: + playbook >> quic.QuicConnectionClosed( + tctx.client, + error_code=ErrorCode.H3_REQUEST_CANCELLED.value, + frame_type=None, + reason_phrase="peer closed connection", + ) + + if stream: + playbook << commands.CloseConnection(server) + playbook << (error_hook := http.HttpErrorHook(flow)) + if "RST" in how: + playbook << cff.send_reset(ErrorCode.H3_REQUEST_CANCELLED) + playbook >> tutils.reply(to=error_hook) + + if how == "RST+disconnect": + playbook >> quic.QuicConnectionClosed( + tctx.client, + error_code=ErrorCode.H3_NO_ERROR.value, + frame_type=None, + reason_phrase="peer closed connection", + ) + assert playbook + assert ( + "stream closed by client" in flow().error.msg + or "peer closed connection" in flow().error.msg + ) + return + + assert ( + playbook + >> cff.receive_data(b"", end_stream=True) + << http.HttpRequestHook(flow) + >> tutils.reply() + << commands.OpenConnection(server) + >> tutils.reply(None) + << commands.SendData(server, b"GET / HTTP/1.1\r\n" b"Host: example.com\r\n\r\n") + >> events.DataReceived( + server, b"HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\n123" + ) + << http.HttpResponseHeadersHook(flow) + ) + if stream: + assert ( + playbook + >> tutils.reply(side_effect=enable_response_streaming) + << cff.send_headers( + [ + (b":status", b"200"), + (b"content-length", b"6"), + ] + ) + << cff.send_data(b"123") + ) + else: + assert playbook >> tutils.reply() + + if "RST" in how: + playbook >> cff.receive_reset(ErrorCode.H3_REQUEST_CANCELLED) + else: + playbook >> quic.QuicConnectionClosed( + tctx.client, + error_code=ErrorCode.H3_REQUEST_CANCELLED.value, + frame_type=None, + reason_phrase="peer closed connection", + ) + + playbook << commands.CloseConnection(server) + playbook << (error_hook := http.HttpErrorHook(flow)) + if "RST" in how: + playbook << cff.send_reset(ErrorCode.H3_REQUEST_CANCELLED) + playbook >> tutils.reply(to=error_hook) + assert playbook + + if how == "RST+disconnect": + playbook >> quic.QuicConnectionClosed( + tctx.client, + error_code=ErrorCode.H3_REQUEST_CANCELLED.value, + frame_type=None, + reason_phrase="peer closed connection", + ) + assert playbook + + if "RST" in how: + assert "stream closed by client" in flow().error.msg + else: + assert "peer closed connection" in flow().error.msg + + +def test_rst_then_close(tctx): + """ + Test that we properly handle the case of a client that first causes protocol errors and then disconnects. + + This is slightly different to H2, as QUIC will close the connection immediately. + """ + playbook, cff = start_h3_proxy(tctx) + flow = tutils.Placeholder(HTTPFlow) + server = tutils.Placeholder(connection.Server) + err = tutils.Placeholder(str) + + assert ( + playbook + # request client + >> cff.receive_headers(example_request_headers, end_stream=True) + << (request := http.HttpRequestHeadersHook(flow)) + << cff.send_decoder() # for receive_headers + >> tutils.reply(to=request) + << http.HttpRequestHook(flow) + >> tutils.reply() + # request server + << (open := commands.OpenConnection(server)) + >> cff.receive_data(b"unexpected data frame") + << quic.CloseQuicConnection( + tctx.client, + error_code=QuicErrorCode.PROTOCOL_VIOLATION.value, + frame_type=None, + reason_phrase=err, + ) + >> quic.QuicConnectionClosed( + tctx.client, + error_code=QuicErrorCode.PROTOCOL_VIOLATION.value, + frame_type=None, + reason_phrase=err, + ) + >> tutils.reply("connection cancelled", to=open) + << http.HttpErrorHook(flow) + >> tutils.reply() + ) + assert flow().error.msg == "connection cancelled" + + +def test_cancel_then_server_disconnect(tctx: context.Context): + """ + Test that we properly handle the case of the following event sequence: + - client cancels a stream + - we start an error hook + - server disconnects + - error hook completes. + """ + playbook, cff = start_h3_proxy(tctx) + flow = tutils.Placeholder(HTTPFlow) + server = tutils.Placeholder(connection.Server) + assert ( + playbook + # request client + >> cff.receive_headers(example_request_headers, end_stream=True) + << (request := http.HttpRequestHeadersHook(flow)) + << cff.send_decoder() # for receive_headers + >> tutils.reply(to=request) + << http.HttpRequestHook(flow) + >> tutils.reply() + # request server + << commands.OpenConnection(server) + >> tutils.reply(None) + << commands.SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + # cancel + >> cff.receive_reset(error_code=ErrorCode.H3_REQUEST_CANCELLED) + << commands.CloseConnection(server) + << (err_hook := http.HttpErrorHook(flow)) + << cff.send_reset(ErrorCode.H3_REQUEST_CANCELLED) + >> tutils.reply(to=err_hook) + >> events.ConnectionClosed(server) + << None + ) + assert cff.is_done + + +def test_cancel_during_response_hook(tctx: context.Context): + """ + Test that we properly handle the case of the following event sequence: + - we receive a server response + - we trigger the response hook + - the client cancels the stream + - the response hook completes + + Given that we have already triggered the response hook, we don't want to trigger the error hook. + """ + playbook, cff = start_h3_proxy(tctx) + flow = tutils.Placeholder(HTTPFlow) + server = tutils.Placeholder(connection.Server) + assert ( + playbook + # request client + >> cff.receive_headers(example_request_headers, end_stream=True) + << (request := http.HttpRequestHeadersHook(flow)) + << cff.send_decoder() # for receive_headers + >> tutils.reply(to=request) + << http.HttpRequestHook(flow) + >> tutils.reply() + # request server + << commands.OpenConnection(server) + >> tutils.reply(None) + << commands.SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + # response server + >> events.DataReceived(server, b"HTTP/1.1 204 No Content\r\n\r\n") + << (reponse_headers := http.HttpResponseHeadersHook(flow)) + << commands.CloseConnection(server) + >> tutils.reply(to=reponse_headers) + << (response := http.HttpResponseHook(flow)) + >> cff.receive_reset(error_code=ErrorCode.H3_REQUEST_CANCELLED) + << cff.send_reset(ErrorCode.H3_REQUEST_CANCELLED) + >> tutils.reply(to=response) + ) + assert cff.is_done + + +def test_stream_concurrency(tctx: context.Context): + """Test that we can send an intercepted request with a lower stream id than one that has already been sent.""" + playbook, cff = start_h3_proxy(tctx) + flow1 = tutils.Placeholder(HTTPFlow) + flow2 = tutils.Placeholder(HTTPFlow) + server = tutils.Placeholder(connection.Server) + sff = FrameFactory(server, is_client=False) + headers1 = [*example_request_headers, (b"x-order", b"1")] + headers2 = [*example_request_headers, (b"x-order", b"2")] + assert ( + playbook + # request client + >> cff.receive_headers(headers1, stream_id=0, end_stream=True) + << (request_header1 := http.HttpRequestHeadersHook(flow1)) + << cff.send_decoder() # for receive_headers + >> cff.receive_headers(headers2, stream_id=4, end_stream=True) + << (request_header2 := http.HttpRequestHeadersHook(flow2)) + << cff.send_decoder() # for receive_headers + >> tutils.reply(to=request_header1) + << (request1 := http.HttpRequestHook(flow1)) + >> tutils.reply(to=request_header2) + << (request2 := http.HttpRequestHook(flow2)) + # req 2 overtakes 1 and we already have a reply: + >> tutils.reply(to=request2) + # request server + << commands.OpenConnection(server) + >> tutils.reply(None, side_effect=make_h3) + << sff.send_init() + << sff.send_headers(headers2, stream_id=0, end_stream=True) + >> sff.receive_init() + << sff.send_encoder() + >> sff.receive_encoder() + >> sff.receive_decoder() # for send_headers + >> tutils.reply(to=request1) + << sff.send_headers(headers1, stream_id=4, end_stream=True) + >> sff.receive_decoder() # for send_headers + ) + assert cff.is_done and sff.is_done + + +def test_stream_concurrent_get_connection(tctx: context.Context): + """Test that an immediate second request for the same domain does not trigger a second connection attempt.""" + playbook, cff = start_h3_proxy(tctx) + playbook.hooks = False + server = tutils.Placeholder(connection.Server) + sff = FrameFactory(server, is_client=False) + assert ( + playbook + >> cff.receive_headers(example_request_headers, stream_id=0, end_stream=True) + << cff.send_decoder() # for receive_headers + << (o := commands.OpenConnection(server)) + >> cff.receive_headers(example_request_headers, stream_id=4, end_stream=True) + << cff.send_decoder() # for receive_headers + >> tutils.reply(None, to=o, side_effect=make_h3) + << sff.send_init() + << sff.send_headers(example_request_headers, stream_id=0, end_stream=True) + << sff.send_headers(example_request_headers, stream_id=4, end_stream=True) + >> sff.receive_init() + << sff.send_encoder() + >> sff.receive_encoder() + >> sff.receive_decoder() # for send_headers + >> sff.receive_decoder() # for send_headers + ) + assert cff.is_done and sff.is_done + + +def test_kill_stream(tctx: context.Context): + """Test that we can kill individual streams.""" + playbook, cff = start_h3_proxy(tctx) + flow1 = tutils.Placeholder(HTTPFlow) + flow2 = tutils.Placeholder(HTTPFlow) + server = tutils.Placeholder(connection.Server) + sff = FrameFactory(server, is_client=False) + headers1 = [*example_request_headers, (b"x-order", b"1")] + headers2 = [*example_request_headers, (b"x-order", b"2")] + + def kill(flow: HTTPFlow): + # Can't use flow.kill() here because that currently still depends on a reply object. + flow.error = Error(Error.KILLED_MESSAGE) + + assert ( + playbook + # request client + >> cff.receive_headers(headers1, stream_id=0, end_stream=True) + << (request_header1 := http.HttpRequestHeadersHook(flow1)) + << cff.send_decoder() # for receive_headers + >> cff.receive_headers(headers2, stream_id=4, end_stream=True) + << (request_header2 := http.HttpRequestHeadersHook(flow2)) + << cff.send_decoder() # for receive_headers + >> tutils.reply(to=request_header2, side_effect=kill) + << http.HttpErrorHook(flow2) + >> tutils.reply() + << cff.send_reset(ErrorCode.H3_INTERNAL_ERROR, stream_id=4) + << cff.send_stop(ErrorCode.H3_INTERNAL_ERROR, stream_id=4) + >> tutils.reply(to=request_header1) + << http.HttpRequestHook(flow1) + >> tutils.reply() + # request server + << commands.OpenConnection(server) + >> tutils.reply(None, side_effect=make_h3) + << sff.send_init() + << sff.send_headers(headers1, stream_id=0, end_stream=True) + >> sff.receive_init() + << sff.send_encoder() + >> sff.receive_encoder() + >> sff.receive_decoder() # for send_headers + ) + assert cff.is_done and sff.is_done + + +@pytest.mark.parametrize("close_type", ["RESET_STREAM", "STOP_SENDING"]) +def test_receive_stop_sending(tctx: context.Context, close_type: str): + playbook, cff = start_h3_proxy(tctx) + playbook.hooks = False + flow = tutils.Placeholder(HTTPFlow) + server = tutils.Placeholder(connection.Server) + sff = FrameFactory(server, is_client=False) + assert ( + playbook + >> cff.receive_headers(example_request_headers, end_stream=True) + << cff.send_decoder() + << commands.OpenConnection(server) + >> tutils.reply(None, side_effect=make_h3) + << sff.send_init() + << sff.send_headers(example_request_headers, end_stream=True) + >> sff.receive_init() + << sff.send_encoder() + ) + + close1 = cff.receive_reset(ErrorCode.H3_REQUEST_CANCELLED) + close2 = cff.receive_stop(ErrorCode.H3_REQUEST_CANCELLED) + if close_type == "STOP_SENDING": + close1, close2 = close2, close1 + + assert ( + playbook + # Client now closes the stream. + >> close1 + # We shut down the server... + << sff.send_reset(ErrorCode.H3_REQUEST_CANCELLED) + << sff.send_stop(ErrorCode.H3_REQUEST_CANCELLED) + << (err_hook := http.HttpErrorHook(flow)) + # ...and the client stream. + << cff.send_reset(ErrorCode.H3_REQUEST_CANCELLED) + << ( + cff.send_stop(ErrorCode.H3_REQUEST_CANCELLED) + if close_type == "STOP_SENDING" + else None + ) + >> tutils.reply(to=err_hook) + # These don't do anything anymore. + >> close2 + << None + >> sff.receive_reset(ErrorCode.H3_REQUEST_CANCELLED) + << None + >> sff.receive_stop(ErrorCode.H3_REQUEST_CANCELLED) + << None + ) + assert flow().error.msg == "stream closed by client (H3_REQUEST_CANCELLED)" + + +class TestClient: + def test_no_data_on_closed_stream(self, tctx: context.Context): + frame_factory = FrameFactory(tctx.server, is_client=False) + playbook = tutils.Playbook(Http3Client(tctx)) + req = Request.make("GET", "http://example.com/") + resp = [(b":status", b"200")] + assert ( + playbook + << frame_factory.send_init() + >> frame_factory.receive_init() + << frame_factory.send_encoder() + >> frame_factory.receive_encoder() + >> http.RequestHeaders(1, req, end_stream=True) + << frame_factory.send_headers( + [ + (b":method", b"GET"), + (b":scheme", b"http"), + (b":path", b"/"), + (b"content-length", b"0"), + ], + end_stream=True, + ) + >> frame_factory.receive_decoder() # for send_headers + >> http.RequestEndOfMessage(1) + >> frame_factory.receive_headers(resp) + << http.ReceiveHttp(tutils.Placeholder(http.ResponseHeaders)) + << frame_factory.send_decoder() # for receive_headers + >> http.RequestProtocolError( + 1, "cancelled", code=http.status_codes.CLIENT_CLOSED_REQUEST + ) + << frame_factory.send_reset(ErrorCode.H3_REQUEST_CANCELLED) + << frame_factory.send_stop(ErrorCode.H3_REQUEST_CANCELLED) + >> frame_factory.receive_data(b"foo") + << None + ) # important: no ResponseData event here! + + assert frame_factory.is_done + + def test_ignore_wrong_order(self, tctx: context.Context): + frame_factory = FrameFactory(tctx.server, is_client=False) + playbook = tutils.Playbook(Http3Client(tctx)) + req = Request.make("GET", "http://example.com/") + assert ( + playbook + << frame_factory.send_init() + >> frame_factory.receive_init() + << frame_factory.send_encoder() + >> frame_factory.receive_encoder() + >> http.RequestTrailers(1, Headers([(b"x-trailer", b"")])) + << commands.Log( + "Received RequestTrailers(stream_id=0, trailers=Headers[(b'x-trailer', b'')]) unexpectedly: " + "trailing HEADERS frame is not allowed in this state" + ) + >> http.RequestEndOfMessage(1) + << commands.Log( + "Received RequestEndOfMessage(stream_id=0) unexpectedly: " + "DATA frame is not allowed in this state" + ) + >> http.RequestData(1, b"123") + << commands.Log( + "Received RequestData(stream_id=0, data=b'123') unexpectedly: " + "DATA frame is not allowed in this state" + ) + >> http.RequestHeaders(1, req, end_stream=False) + << frame_factory.send_headers( + [ + (b":method", b"GET"), + (b":scheme", b"http"), + (b":path", b"/"), + (b"content-length", b"0"), + ], + end_stream=False, + ) + >> frame_factory.receive_decoder() # for send_headers + >> http.RequestHeaders(1, req, end_stream=False) + << commands.Log( + "Received RequestHeaders(stream_id=0, request=Request(GET example.com:80/)," + " end_stream=False, replay_flow=None) unexpectedly: " + "initial HEADERS frame is not allowed in this state" + ) + ) + + +def test_early_server_data(tctx: context.Context): + playbook, cff = start_h3_proxy(tctx) + sff = FrameFactory(tctx.server, is_client=False) + + tctx.server.address = ("example.com", 80) + tctx.server.state = connection.ConnectionState.OPEN + tctx.server.alpn = b"h3" + + flow = tutils.Placeholder(HTTPFlow) + assert ( + playbook + >> cff.receive_headers(example_request_headers, end_stream=True) + << (request_header := http.HttpRequestHeadersHook(flow)) + << cff.send_decoder() # for receive_headers + >> tutils.reply(to=request_header) + << (request := http.HttpRequestHook(flow)) + # Surprise! We get data from the server before the request hook finishes. + >> sff.receive_stream_type(StreamType.CONTROL) + << sff.send_init() + >> sff.receive_stream_type(StreamType.QPACK_ENCODER) + >> sff.receive_stream_type(StreamType.QPACK_DECODER) + >> sff.receive_settings() + << sff.send_encoder() + >> sff.receive_encoder() + # Request hook finishes... + >> tutils.reply(to=request) + << sff.send_headers(example_request_headers, end_stream=True) + ) diff --git a/test/mitmproxy/proxy/layers/http/test_http_fuzz.py b/test/mitmproxy/proxy/layers/http/test_http_fuzz.py index 6ad352efcf..04c891f59e 100644 --- a/test/mitmproxy/proxy/layers/http/test_http_fuzz.py +++ b/test/mitmproxy/proxy/layers/http/test_http_fuzz.py @@ -2,44 +2,44 @@ import pytest from h2.settings import SettingCodes -from hypothesis import example, given -from hypothesis.strategies import ( - binary, - booleans, - composite, - dictionaries, - integers, - lists, - sampled_from, - sets, - text, - data, -) - -from mitmproxy import options, connection +from hypothesis import example +from hypothesis import given +from hypothesis.strategies import binary +from hypothesis.strategies import booleans +from hypothesis.strategies import composite +from hypothesis.strategies import data +from hypothesis.strategies import dictionaries +from hypothesis.strategies import integers +from hypothesis.strategies import lists +from hypothesis.strategies import sampled_from +from hypothesis.strategies import sets +from hypothesis.strategies import text + +from mitmproxy import connection +from mitmproxy import options from mitmproxy.addons.proxyserver import Proxyserver from mitmproxy.connection import Server from mitmproxy.http import HTTPFlow -from mitmproxy.proxy.layers.http import HTTPMode -from mitmproxy.proxy import context, events -from mitmproxy.proxy.commands import OpenConnection, SendData -from mitmproxy.proxy.events import DataReceived, Start, ConnectionClosed +from mitmproxy.proxy import context +from mitmproxy.proxy import events +from mitmproxy.proxy.commands import OpenConnection +from mitmproxy.proxy.commands import SendData +from mitmproxy.proxy.events import ConnectionClosed +from mitmproxy.proxy.events import DataReceived +from mitmproxy.proxy.events import Start from mitmproxy.proxy.layers import http -from test.mitmproxy.proxy.layers.http.hyper_h2_test_helpers import FrameFactory -from test.mitmproxy.proxy.layers.http.test_http2 import ( - make_h2, - example_response_headers, - example_request_headers, - start_h2_client, -) -from test.mitmproxy.proxy.tutils import ( - Placeholder, - Playbook, - reply, - _TracebackInPlaybook, - _eq, -) from mitmproxy.proxy.layers.http import _http2 +from mitmproxy.proxy.layers.http import HTTPMode +from test.mitmproxy.proxy.layers.http.hyper_h2_test_helpers import FrameFactory +from test.mitmproxy.proxy.layers.http.test_http2 import example_request_headers +from test.mitmproxy.proxy.layers.http.test_http2 import example_response_headers +from test.mitmproxy.proxy.layers.http.test_http2 import make_h2 +from test.mitmproxy.proxy.layers.http.test_http2 import start_h2_client +from test.mitmproxy.proxy.tutils import _eq +from test.mitmproxy.proxy.tutils import _TracebackInPlaybook +from test.mitmproxy.proxy.tutils import Placeholder +from test.mitmproxy.proxy.tutils import Playbook +from test.mitmproxy.proxy.tutils import reply opts = options.Options() Proxyserver().load(opts) @@ -133,9 +133,7 @@ def h2_responses(draw): @given(chunks(mutations(h1_requests()))) def test_fuzz_h1_request(data): - tctx = context.Context( - connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), opts - ) + tctx = _tctx() layer = http.HttpLayer(tctx, HTTPMode.regular) for _ in layer.handle_event(Start()): @@ -148,9 +146,7 @@ def test_fuzz_h1_request(data): @given(chunks(mutations(h2_responses()))) @example([b"0 OK\r\n\r\n", b"\r\n", b"5\r\n12345\r\n0\r\n\r\n"]) def test_fuzz_h1_response(data): - tctx = context.Context( - connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), opts - ) + tctx = _tctx() server = Placeholder(connection.Server) playbook = Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) assert ( @@ -221,7 +217,7 @@ def h2_frames(draw): settings=draw( dictionaries( keys=sampled_from(SettingCodes), - values=integers(0, 2 ** 32 - 1), + values=integers(0, 2**32 - 1), max_size=5, ) ), @@ -248,7 +244,7 @@ def h2_frames(draw): draw(binary()), draw(h2_flags), stream_id=draw(h2_stream_ids_nonzero) ) window_update = ff.build_window_update_frame( - draw(h2_stream_ids), draw(integers(0, 2 ** 32 - 1)) + draw(h2_stream_ids), draw(integers(0, 2**32 - 1)) ) frames = draw( @@ -276,10 +272,7 @@ def h2_frames(draw): def h2_layer(opts): - tctx = context.Context( - connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), opts - ) - tctx.options.http2_ping_keepalive = 0 + tctx = _tctx() tctx.client.alpn = b"h2" layer = http.HttpLayer(tctx, HTTPMode.regular) @@ -322,10 +315,21 @@ def test_fuzz_h2_request_mutations(chunks): _h2_request(chunks) -def _h2_response(chunks): +def _tctx() -> context.Context: tctx = context.Context( - connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), opts + connection.Client( + peername=("client", 1234), + sockname=("127.0.0.1", 8080), + timestamp_start=1605699329, + ), + opts, ) + tctx.options.http2_ping_keepalive = 0 + return tctx + + +def _h2_response(chunks): + tctx = _tctx() playbook = Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) server = Placeholder(connection.Server) assert ( @@ -427,9 +431,7 @@ def _test_cancel(stream_req, stream_resp, draw): """ Test that we don't raise an exception if someone disconnects. """ - tctx = context.Context( - connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), opts - ) + tctx = _tctx() playbook, cff = start_h2_client(tctx) flow = Placeholder(HTTPFlow) server = Placeholder(Server) diff --git a/test/mitmproxy/proxy/layers/http/test_http_version_interop.py b/test/mitmproxy/proxy/layers/http/test_http_version_interop.py index d0ae84248a..b003e0777d 100644 --- a/test/mitmproxy/proxy/layers/http/test_http_version_interop.py +++ b/test/mitmproxy/proxy/layers/http/test_http_version_interop.py @@ -2,19 +2,21 @@ import h2.connection import h2.events +from mitmproxy.connection import Server from mitmproxy.http import HTTPFlow +from mitmproxy.proxy.commands import CloseConnection +from mitmproxy.proxy.commands import OpenConnection +from mitmproxy.proxy.commands import SendData from mitmproxy.proxy.context import Context -from mitmproxy.proxy.layers.http import HTTPMode -from mitmproxy.proxy.commands import CloseConnection, OpenConnection, SendData -from mitmproxy.connection import Server from mitmproxy.proxy.events import DataReceived from mitmproxy.proxy.layers import http +from mitmproxy.proxy.layers.http import HTTPMode from test.mitmproxy.proxy.layers.http.hyper_h2_test_helpers import FrameFactory -from test.mitmproxy.proxy.layers.http.test_http2 import ( - example_response_headers, - make_h2, -) -from test.mitmproxy.proxy.tutils import Placeholder, Playbook, reply +from test.mitmproxy.proxy.layers.http.test_http2 import example_response_headers +from test.mitmproxy.proxy.layers.http.test_http2 import make_h2 +from test.mitmproxy.proxy.tutils import Placeholder +from test.mitmproxy.proxy.tutils import Playbook +from test.mitmproxy.proxy.tutils import reply example_request_headers = ( (b":method", b"GET"), @@ -43,7 +45,8 @@ def h2_client(tctx: Context) -> tuple[h2.connection.H2Connection, Playbook]: server_preamble = Placeholder(bytes) assert playbook << SendData(tctx.client, server_preamble) assert event_types(conn.receive_data(server_preamble())) == [ - h2.events.RemoteSettingsChanged + h2.events.RemoteSettingsChanged, + h2.events.WindowUpdated, ] settings_ack = Placeholder(bytes) @@ -77,7 +80,9 @@ def test_h2_to_h1(tctx): >> reply() << OpenConnection(server) >> reply(None) - << SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\ncookie: a=1; b=2\r\n\r\n") + << SendData( + server, b"GET / HTTP/1.1\r\nHost: example.com\r\ncookie: a=1; b=2\r\n\r\n" + ) >> DataReceived(server, b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\n") << http.HttpResponseHeadersHook(flow) >> reply() @@ -130,6 +135,7 @@ def test_h1_to_h2(tctx): events = conn.receive_data(request()) assert event_types(events) == [ h2.events.RemoteSettingsChanged, + h2.events.WindowUpdated, h2.events.RequestReceived, h2.events.StreamEnded, ] diff --git a/test/mitmproxy/proxy/layers/quic/__init__.py b/test/mitmproxy/proxy/layers/quic/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/mitmproxy/proxy/layers/quic/test__client_hello_parser.py b/test/mitmproxy/proxy/layers/quic/test__client_hello_parser.py new file mode 100644 index 0000000000..82037fb59f --- /dev/null +++ b/test/mitmproxy/proxy/layers/quic/test__client_hello_parser.py @@ -0,0 +1,53 @@ +import pytest +from aioquic.quic.connection import QuicConnection +from aioquic.quic.connection import QuicConnectionError + +from mitmproxy.proxy.layers.quic import _client_hello_parser +from mitmproxy.proxy.layers.quic._client_hello_parser import ( + quic_parse_client_hello_from_datagrams, +) +from test.mitmproxy.proxy.layers.quic.test__stream_layers import client_hello + + +class TestParseClientHello: + def test_input(self): + assert ( + quic_parse_client_hello_from_datagrams([client_hello]).sni == "example.com" + ) + with pytest.raises(ValueError): + quic_parse_client_hello_from_datagrams( + [client_hello[:183] + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00"] + ) + with pytest.raises(ValueError, match="not initial"): + quic_parse_client_hello_from_datagrams( + [ + b"\\s\xd8\xd8\xa5dT\x8bc\xd3\xae\x1c\xb2\x8a7-\x1d\x19j\x85\xb0~\x8c\x80\xa5\x8cY\xac\x0ecK\x7fC2f\xbcm\x1b\xac~" + ] + ) + + def test_invalid(self, monkeypatch): + # XXX: This test is terrible, it should use actual invalid data. + class InvalidClientHello(Exception): + @property + def data(self): + raise EOFError() + + monkeypatch.setattr(_client_hello_parser, "QuicClientHello", InvalidClientHello) + with pytest.raises(ValueError, match="Invalid ClientHello"): + quic_parse_client_hello_from_datagrams([client_hello]) + + def test_connection_error(self, monkeypatch): + def raise_conn_err(self, data, addr, now): + raise QuicConnectionError(0, 0, "Conn err") + + monkeypatch.setattr(QuicConnection, "receive_datagram", raise_conn_err) + with pytest.raises(ValueError, match="Conn err"): + quic_parse_client_hello_from_datagrams([client_hello]) + + def test_no_return(self): + with pytest.raises( + ValueError, match="Invalid ClientHello packet: payload_decrypt_error" + ): + quic_parse_client_hello_from_datagrams( + [client_hello[0:1200] + b"\x00" + client_hello[1200:]] + ) diff --git a/test/mitmproxy/proxy/layers/quic/test__commands.py b/test/mitmproxy/proxy/layers/quic/test__commands.py new file mode 100644 index 0000000000..9ec3e59320 --- /dev/null +++ b/test/mitmproxy/proxy/layers/quic/test__commands.py @@ -0,0 +1,15 @@ +from mitmproxy.proxy.layers.quic._commands import CloseQuicConnection +from mitmproxy.proxy.layers.quic._commands import QuicStreamCommand +from mitmproxy.proxy.layers.quic._commands import ResetQuicStream +from mitmproxy.proxy.layers.quic._commands import SendQuicStreamData +from mitmproxy.proxy.layers.quic._commands import StopSendingQuicStream +from mitmproxy.test.tflow import tclient_conn + + +def test_reprs(): + client = tclient_conn() + assert repr(QuicStreamCommand(client, 42)) + assert repr(SendQuicStreamData(client, 42, b"data")) + assert repr(ResetQuicStream(client, 42, 0xFF)) + assert repr(StopSendingQuicStream(client, 42, 0xFF)) + assert repr(CloseQuicConnection(client, 0xFF, None, "reason")) diff --git a/test/mitmproxy/proxy/layers/quic/test__events.py b/test/mitmproxy/proxy/layers/quic/test__events.py new file mode 100644 index 0000000000..d1d18c034a --- /dev/null +++ b/test/mitmproxy/proxy/layers/quic/test__events.py @@ -0,0 +1,9 @@ +from mitmproxy.proxy.layers.quic._events import QuicConnectionClosed +from mitmproxy.proxy.layers.quic._events import QuicStreamDataReceived +from mitmproxy.test.tflow import tclient_conn + + +def test_reprs(): + client = tclient_conn() + assert repr(QuicStreamDataReceived(client, 42, b"data", end_stream=False)) + assert repr(QuicConnectionClosed(client, 0xFF, None, "reason")) diff --git a/test/mitmproxy/proxy/layers/quic/test__hooks.py b/test/mitmproxy/proxy/layers/quic/test__hooks.py new file mode 100644 index 0000000000..1d660ec9dd --- /dev/null +++ b/test/mitmproxy/proxy/layers/quic/test__hooks.py @@ -0,0 +1,19 @@ +from mitmproxy.options import Options +from mitmproxy.proxy.context import Context +from mitmproxy.proxy.layers.quic._hooks import QuicStartServerHook +from mitmproxy.proxy.layers.quic._hooks import QuicTlsData +from mitmproxy.proxy.layers.quic._hooks import QuicTlsSettings +from mitmproxy.test.tflow import tclient_conn + + +def test_reprs(): + client = tclient_conn() + assert repr( + QuicStartServerHook( + data=QuicTlsData( + conn=client, + context=Context(client, Options()), + settings=QuicTlsSettings(), + ) + ) + ) diff --git a/test/mitmproxy/proxy/layers/quic/test__raw_layers.py b/test/mitmproxy/proxy/layers/quic/test__raw_layers.py new file mode 100644 index 0000000000..4036ce1388 --- /dev/null +++ b/test/mitmproxy/proxy/layers/quic/test__raw_layers.py @@ -0,0 +1,265 @@ +import pytest + +from mitmproxy import connection +from mitmproxy.proxy import commands +from mitmproxy.proxy import context +from mitmproxy.proxy import events +from mitmproxy.proxy import layer +from mitmproxy.proxy import layers +from mitmproxy.proxy import tunnel +from mitmproxy.proxy.layers import tcp +from mitmproxy.proxy.layers import udp +from mitmproxy.proxy.layers.quic._commands import CloseQuicConnection +from mitmproxy.proxy.layers.quic._commands import ResetQuicStream +from mitmproxy.proxy.layers.quic._commands import SendQuicStreamData +from mitmproxy.proxy.layers.quic._commands import StopSendingQuicStream +from mitmproxy.proxy.layers.quic._events import QuicConnectionClosed +from mitmproxy.proxy.layers.quic._events import QuicStreamDataReceived +from mitmproxy.proxy.layers.quic._events import QuicStreamEvent +from mitmproxy.proxy.layers.quic._events import QuicStreamReset +from mitmproxy.proxy.layers.quic._raw_layers import QuicStreamLayer +from mitmproxy.proxy.layers.quic._raw_layers import RawQuicLayer +from mitmproxy.tcp import TCPFlow +from mitmproxy.udp import UDPFlow +from mitmproxy.udp import UDPMessage +from test.mitmproxy.proxy import tutils +from test.mitmproxy.proxy.layers.quic.test__stream_layers import TlsEchoLayer + + +class TestQuicStreamLayer: + def test_force_raw(self, tctx: context.Context): + quic_layer = QuicStreamLayer(tctx, True, 1) + assert isinstance(quic_layer.child_layer, layers.TCPLayer) + quic_layer.child_layer.flow = TCPFlow(tctx.client, tctx.server) + quic_layer.refresh_metadata() + assert quic_layer.child_layer.flow.metadata["quic_is_unidirectional"] is False + assert quic_layer.child_layer.flow.metadata["quic_initiator"] == "server" + assert quic_layer.child_layer.flow.metadata["quic_stream_id_client"] == 1 + assert quic_layer.child_layer.flow.metadata["quic_stream_id_server"] is None + assert quic_layer.stream_id(True) == 1 + assert quic_layer.stream_id(False) is None + + def test_simple(self, tctx: context.Context): + quic_layer = QuicStreamLayer(tctx, False, 2) + assert isinstance(quic_layer.child_layer, layer.NextLayer) + tunnel_layer = tunnel.TunnelLayer(tctx, tctx.client, tctx.server) + quic_layer.child_layer.layer = tunnel_layer + tcp_layer = layers.TCPLayer(tctx) + tunnel_layer.child_layer = tcp_layer + quic_layer.open_server_stream(3) + assert tcp_layer.flow.metadata["quic_is_unidirectional"] is True + assert tcp_layer.flow.metadata["quic_initiator"] == "client" + assert tcp_layer.flow.metadata["quic_stream_id_client"] == 2 + assert tcp_layer.flow.metadata["quic_stream_id_server"] == 3 + assert quic_layer.stream_id(True) == 2 + assert quic_layer.stream_id(False) == 3 + + +class TestRawQuicLayer: + @pytest.mark.parametrize("force_raw", [True, False]) + def test_error(self, tctx: context.Context, force_raw: bool): + quic_layer = RawQuicLayer(tctx, force_raw=force_raw) + assert ( + tutils.Playbook(quic_layer) + << commands.OpenConnection(tctx.server) + >> tutils.reply("failed to open") + << commands.CloseConnection(tctx.client) + ) + assert quic_layer._handle_event == quic_layer.done + + def test_force_raw(self, tctx: context.Context): + quic_layer = RawQuicLayer(tctx, force_raw=True) + assert ( + tutils.Playbook(quic_layer, hooks=False) + << commands.OpenConnection(tctx.server) + >> tutils.reply(None) + >> events.DataReceived(tctx.client, b"msg1") + << commands.SendData(tctx.server, b"msg1") + >> events.DataReceived(tctx.server, b"msg2") + << commands.SendData(tctx.client, b"msg2") + >> QuicStreamDataReceived(tctx.client, 0, b"msg3", end_stream=False) + << SendQuicStreamData(tctx.server, 0, b"msg3", end_stream=False) + >> QuicStreamDataReceived(tctx.client, 6, b"msg4", end_stream=False) + << SendQuicStreamData(tctx.server, 2, b"msg4", end_stream=False) + >> QuicStreamDataReceived(tctx.server, 9, b"msg5", end_stream=False) + << SendQuicStreamData(tctx.client, 1, b"msg5", end_stream=False) + >> QuicStreamDataReceived(tctx.client, 0, b"", end_stream=True) + << SendQuicStreamData(tctx.server, 0, b"", end_stream=True) + >> QuicStreamReset(tctx.client, 6, 142) + << ResetQuicStream(tctx.server, 2, 142) + >> QuicConnectionClosed(tctx.client, 42, None, "closed") + << CloseQuicConnection(tctx.server, 42, None, "closed") + >> QuicConnectionClosed(tctx.server, 42, None, "closed") + << None + ) + assert quic_layer._handle_event == quic_layer.done + + def test_msg_inject(self, tctx: context.Context): + udpflow = tutils.Placeholder(UDPFlow) + playbook = tutils.Playbook(RawQuicLayer(tctx)) + assert ( + playbook + << commands.OpenConnection(tctx.server) + >> tutils.reply(None) + >> events.DataReceived(tctx.client, b"msg1") + << layer.NextLayerHook(tutils.Placeholder()) + >> tutils.reply_next_layer(udp.UDPLayer) + << udp.UdpStartHook(udpflow) + >> tutils.reply() + << udp.UdpMessageHook(udpflow) + >> tutils.reply() + << commands.SendData(tctx.server, b"msg1") + >> udp.UdpMessageInjected(udpflow, UDPMessage(True, b"msg2")) + << udp.UdpMessageHook(udpflow) + >> tutils.reply() + << commands.SendData(tctx.server, b"msg2") + >> udp.UdpMessageInjected( + UDPFlow(("other", 80), tctx.server), UDPMessage(True, b"msg3") + ) + << udp.UdpMessageHook(udpflow) + >> tutils.reply() + << commands.SendData(tctx.server, b"msg3") + ) + with pytest.raises(AssertionError, match="not associated"): + playbook >> udp.UdpMessageInjected( + UDPFlow(("notfound", 0), ("noexist", 0)), UDPMessage(True, b"msg2") + ) + assert playbook + + def test_reset_with_end_hook(self, tctx: context.Context): + tcpflow = tutils.Placeholder(TCPFlow) + assert ( + tutils.Playbook(RawQuicLayer(tctx)) + << commands.OpenConnection(tctx.server) + >> tutils.reply(None) + >> QuicStreamDataReceived(tctx.client, 2, b"msg1", end_stream=False) + << layer.NextLayerHook(tutils.Placeholder()) + >> tutils.reply_next_layer(tcp.TCPLayer) + << tcp.TcpStartHook(tcpflow) + >> tutils.reply() + << tcp.TcpMessageHook(tcpflow) + >> tutils.reply() + << SendQuicStreamData(tctx.server, 2, b"msg1", end_stream=False) + >> QuicStreamReset(tctx.client, 2, 42) + << ResetQuicStream(tctx.server, 2, 42) + << tcp.TcpEndHook(tcpflow) + >> tutils.reply() + ) + + def test_close_with_end_hooks(self, tctx: context.Context): + udpflow = tutils.Placeholder(UDPFlow) + tcpflow = tutils.Placeholder(TCPFlow) + assert ( + tutils.Playbook(RawQuicLayer(tctx)) + << commands.OpenConnection(tctx.server) + >> tutils.reply(None) + >> events.DataReceived(tctx.client, b"msg1") + << layer.NextLayerHook(tutils.Placeholder()) + >> tutils.reply_next_layer(udp.UDPLayer) + << udp.UdpStartHook(udpflow) + >> tutils.reply() + << udp.UdpMessageHook(udpflow) + >> tutils.reply() + << commands.SendData(tctx.server, b"msg1") + >> QuicStreamDataReceived(tctx.client, 2, b"msg2", end_stream=False) + << layer.NextLayerHook(tutils.Placeholder()) + >> tutils.reply_next_layer(tcp.TCPLayer) + << tcp.TcpStartHook(tcpflow) + >> tutils.reply() + << tcp.TcpMessageHook(tcpflow) + >> tutils.reply() + << SendQuicStreamData(tctx.server, 2, b"msg2", end_stream=False) + >> QuicConnectionClosed(tctx.client, 42, None, "bye") + << CloseQuicConnection(tctx.server, 42, None, "bye") + << udp.UdpEndHook(udpflow) + << tcp.TcpEndHook(tcpflow) + >> tutils.reply(to=-2) + >> tutils.reply(to=-2) + >> QuicConnectionClosed(tctx.server, 42, None, "bye") + ) + + def test_invalid_stream_event(self, tctx: context.Context): + playbook = tutils.Playbook(RawQuicLayer(tctx)) + assert ( + tutils.Playbook(RawQuicLayer(tctx)) + << commands.OpenConnection(tctx.server) + >> tutils.reply(None) + ) + with pytest.raises(AssertionError, match="Unexpected stream event"): + + class InvalidStreamEvent(QuicStreamEvent): + pass + + playbook >> InvalidStreamEvent(tctx.client, 0) + assert playbook + + def test_invalid_event(self, tctx: context.Context): + playbook = tutils.Playbook(RawQuicLayer(tctx)) + assert ( + tutils.Playbook(RawQuicLayer(tctx)) + << commands.OpenConnection(tctx.server) + >> tutils.reply(None) + ) + with pytest.raises(AssertionError, match="Unexpected event"): + + class InvalidEvent(events.Event): + pass + + playbook >> InvalidEvent() + assert playbook + + def test_full_close(self, tctx: context.Context): + assert ( + tutils.Playbook(RawQuicLayer(tctx)) + << commands.OpenConnection(tctx.server) + >> tutils.reply(None) + >> QuicStreamDataReceived(tctx.client, 0, b"msg1", end_stream=True) + << layer.NextLayerHook(tutils.Placeholder()) + >> tutils.reply_next_layer(lambda ctx: udp.UDPLayer(ctx, ignore=True)) + << SendQuicStreamData(tctx.server, 0, b"msg1", end_stream=False) + << SendQuicStreamData(tctx.server, 0, b"", end_stream=True) + << StopSendingQuicStream(tctx.server, 0, 0) + ) + + def test_open_connection(self, tctx: context.Context): + server = connection.Server(address=("other", 80)) + + def echo_new_server(ctx: context.Context): + echo_layer = TlsEchoLayer(ctx) + echo_layer.context.server = server + return echo_layer + + assert ( + tutils.Playbook(RawQuicLayer(tctx)) + << commands.OpenConnection(tctx.server) + >> tutils.reply(None) + >> QuicStreamDataReceived( + tctx.client, 0, b"open-connection", end_stream=False + ) + << layer.NextLayerHook(tutils.Placeholder()) + >> tutils.reply_next_layer(echo_new_server) + << commands.OpenConnection(server) + >> tutils.reply("uhoh") + << SendQuicStreamData( + tctx.client, 0, b"open-connection failed: uhoh", end_stream=False + ) + ) + + def test_invalid_connection_command(self, tctx: context.Context): + playbook = tutils.Playbook(RawQuicLayer(tctx)) + assert ( + playbook + << commands.OpenConnection(tctx.server) + >> tutils.reply(None) + >> QuicStreamDataReceived(tctx.client, 0, b"msg1", end_stream=False) + << layer.NextLayerHook(tutils.Placeholder()) + >> tutils.reply_next_layer(TlsEchoLayer) + << SendQuicStreamData(tctx.client, 0, b"msg1", end_stream=False) + ) + with pytest.raises( + AssertionError, match="Unexpected stream connection command" + ): + playbook >> QuicStreamDataReceived( + tctx.client, 0, b"invalid-command", end_stream=False + ) + assert playbook diff --git a/test/mitmproxy/proxy/layers/quic/test__stream_layers.py b/test/mitmproxy/proxy/layers/quic/test__stream_layers.py new file mode 100644 index 0000000000..8c1c26df06 --- /dev/null +++ b/test/mitmproxy/proxy/layers/quic/test__stream_layers.py @@ -0,0 +1,1111 @@ +import ssl +import time +from logging import DEBUG +from logging import ERROR +from logging import WARNING +from typing import Literal +from typing import TypeVar +from unittest.mock import MagicMock + +import pytest +from aioquic.buffer import Buffer as QuicBuffer +from aioquic.quic import events as quic_events +from aioquic.quic.configuration import QuicConfiguration +from aioquic.quic.connection import pull_quic_header +from aioquic.quic.connection import QuicConnection + +from mitmproxy import connection +from mitmproxy.proxy import commands +from mitmproxy.proxy import context +from mitmproxy.proxy import events +from mitmproxy.proxy import layer +from mitmproxy.proxy.layers import tls +from mitmproxy.proxy.layers.quic._commands import CloseQuicConnection +from mitmproxy.proxy.layers.quic._commands import QuicStreamCommand +from mitmproxy.proxy.layers.quic._commands import ResetQuicStream +from mitmproxy.proxy.layers.quic._commands import SendQuicStreamData +from mitmproxy.proxy.layers.quic._commands import StopSendingQuicStream +from mitmproxy.proxy.layers.quic._events import QuicConnectionClosed +from mitmproxy.proxy.layers.quic._events import QuicStreamDataReceived +from mitmproxy.proxy.layers.quic._events import QuicStreamReset +from mitmproxy.proxy.layers.quic._events import QuicStreamStopSending +from mitmproxy.proxy.layers.quic._hooks import QuicStartClientHook +from mitmproxy.proxy.layers.quic._hooks import QuicStartServerHook +from mitmproxy.proxy.layers.quic._hooks import QuicTlsData +from mitmproxy.proxy.layers.quic._hooks import QuicTlsSettings +from mitmproxy.proxy.layers.quic._stream_layers import ClientQuicLayer +from mitmproxy.proxy.layers.quic._stream_layers import error_code_to_str +from mitmproxy.proxy.layers.quic._stream_layers import is_success_error_code +from mitmproxy.proxy.layers.quic._stream_layers import QuicLayer +from mitmproxy.proxy.layers.quic._stream_layers import QuicSecretsLogger +from mitmproxy.proxy.layers.quic._stream_layers import ServerQuicLayer +from mitmproxy.proxy.layers.quic._stream_layers import tls_settings_to_configuration +from mitmproxy.utils import data +from test.mitmproxy.proxy import tutils + +tdata = data.Data("test") + + +T = TypeVar("T", bound=layer.Layer) + + +class DummyLayer(layer.Layer): + child_layer: layer.Layer | None + + def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: + assert self.child_layer + return self.child_layer.handle_event(event) + + +class TlsEchoLayer(tutils.EchoLayer): + err: str | None = None + closed: QuicConnectionClosed | None = None + + def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: + if isinstance(event, events.DataReceived) and event.data == b"open-connection": + err = yield commands.OpenConnection(self.context.server) + if err: + yield commands.SendData( + event.connection, f"open-connection failed: {err}".encode() + ) + elif ( + isinstance(event, events.DataReceived) and event.data == b"close-connection" + ): + yield commands.CloseConnection(event.connection) + elif ( + isinstance(event, events.DataReceived) + and event.data == b"close-connection-error" + ): + yield CloseQuicConnection(event.connection, 123, None, "error") + elif ( + isinstance(event, events.DataReceived) and event.data == b"invalid-command" + ): + + class InvalidConnectionCommand(commands.ConnectionCommand): + pass + + yield InvalidConnectionCommand(event.connection) + elif ( + isinstance(event, events.DataReceived) + and event.data == b"invalid-stream-command" + ): + + class InvalidStreamCommand(QuicStreamCommand): + pass + + yield InvalidStreamCommand(event.connection, 42) + elif isinstance(event, QuicConnectionClosed): + self.closed = event + elif isinstance(event, QuicStreamDataReceived): + yield SendQuicStreamData( + event.connection, event.stream_id, event.data, event.end_stream + ) + elif isinstance(event, QuicStreamReset): + yield ResetQuicStream(event.connection, event.stream_id, event.error_code) + elif isinstance(event, QuicStreamStopSending): + yield StopSendingQuicStream( + event.connection, event.stream_id, event.error_code + ) + else: + yield from super()._handle_event(event) + + +client_hello = bytes.fromhex( + "ca0000000108c0618c84b54541320823fcce946c38d8210044e6a93bbb283593f75ffb6f2696b16cfdcb5b1255" + "577b2af5fc5894188c9568bc65eef253faf7f0520e41341cfa81d6aae573586665ce4e1e41676364820402feec" + "a81f3d22dbb476893422069066104a43e121c951a08c53b83f960becf99cf5304d5bc5346f52f472bd1a04d192" + "0bae025064990d27e5e4c325ac46121d3acadebe7babdb96192fb699693d65e2b2e21c53beeb4f40b50673a2f6" + "c22091cb7c76a845384fedee58df862464d1da505a280bfef91ca83a10bebbcb07855219dbc14aecf8a48da049" + "d03c77459b39d5355c95306cd03d6bdb471694fa998ca3b1f875ce87915b88ead15c5d6313a443f39aad808922" + "57ddfa6b4a898d773bb6fb520ede47ebd59d022431b1054a69e0bbbdf9f0fb32fc8bcc4b6879dd8cd5389474b1" + "99e18333e14d0347740a11916429a818bb8d93295d36e99840a373bb0e14c8b3adcf5e2165e70803f15316fd5e" + "5eeec04ae68d98f1adb22c54611c80fcd8ece619dbdf97b1510032ec374b7a71f94d9492b8b8cb56f56556dd97" + "edf1e50fa90e868ff93636a365678bdf3ee3f8e632588cd506b6f44fbfd4d99988238fbd5884c98f6a124108c1" + "878970780e42b111e3be6215776ef5be5a0205915e6d720d22c6a81a475c9e41ba94e4983b964cb5c8e1f40607" + "76d1d8d1adcef7587ea084231016bd6ee2643d11a3a35eb7fe4cca2b3f1a4b21e040b0d426412cca6c4271ea63" + "fb54ed7f57b41cd1af1be5507f87ea4f4a0c997367e883291de2f1b8a49bdaa52bae30064351b1139703400730" + "18a4104344ec6b4454b50a42e804bc70e78b9b3c82497273859c82ed241b643642d76df6ceab8f916392113a62" + "b231f228c7300624d74a846bec2f479ab8a8c3461f91c7bf806236e3bd2f54ba1ef8e2a1e0bfdde0c5ad227f7d" + "364c52510b1ade862ce0c8d7bd24b6d7d21c99b34de6d177eb3d575787b2af55060d76d6c2060befbb7953a816" + "6f66ad88ecf929dbb0ad3a16cf7dfd39d925e0b4b649c6d0c07ad46ed0229c17fb6a1395f16e1b138aab3af760" + "2b0ac762c4f611f7f3468997224ffbe500a7c53f92f65e41a3765a9f1d7e3f78208f5b4e147962d8c97d6c1a80" + "91ffc36090b2043d71853616f34c2185dc883c54ab6d66e10a6c18e0b9a4742597361f8554a42da3373241d0c8" + "54119bfadccffaf2335b2d97ffee627cb891bda8140a39399f853da4859f7e19682e152243efbaffb662edd19b" + "3819a74107c7dbe05ecb32e79dcdb1260f153b1ef133e978ccca3d9e400a7ed6c458d77e2956d2cb897b7a298b" + "fe144b5defdc23dfd2adf69f1fb0917840703402d524987ae3b1dcb85229843c9a419ef46e1ba0ba7783f2a2ec" + "d057a57518836aef2a7839ebd3688da98b54c942941f642e434727108d59ea25875b3050ca53d4637c76cbcbb9" + "e972c2b0b781131ee0a1403138b55486fe86bbd644920ee6aa578e3bab32d7d784b5c140295286d90c99b14823" + "1487f7ea64157001b745aa358c9ea6bec5a8d8b67a7534ec1f7648ff3b435911dfc3dff798d32fbf2efe2c1fcc" + "278865157590572387b76b78e727d3e7682cb501cdcdf9a0f17676f99d9aa67f10edccc9a92080294e88bf28c2" + "a9f32ae535fdb27fff7706540472abb9eab90af12b2bea005da189874b0ca69e6ae1690a6f2adf75be3853c94e" + "fd8098ed579c20cb37be6885d8d713af4ba52958cee383089b98ed9cb26e11127cf88d1b7d254f15f7903dd7ed" + "297c0013924e88248684fe8f2098326ce51aa6e5" +) + + +fragmented_client_hello1 = bytes.fromhex( + "c20000000108d520c3803f5de4d3000044bcb607af28f41aef1616d37bdc7697d73d7963a2d622e7ccddfb4859" + "f369d840f949a29bb19ad7264728eb31eada17a4e1ba666bba67868cf2c30ca4e1d41f67d392c296787a50615a" + "1caf4282f9cc59c98816e1734b57ba4dedf02c225a3f57163bb77703299fafb46d09a4d281eb44f988edd28984" + "04a7161cf7454d8e184f87ae9be1f3bd2c2ae04ba14233ec92960a75a4201bc114070ecfd4c10a4fb0c72749ee" + "b5fa0e52b53dc0da6a485eb8bb467e7a1972c4e1c3a38622857b44eb94d653ee2f2e1fa3bf3f01cacd17b2668a" + "8578e04da4181f3d6ad4031e4f7adec95d015d4f275505ae14fa03154b18c3b838143fac06cb2c8b395effa47c" + "08923e352d1c4beff9e228760f5a80e6214485c7e53efd8d649492aafb3a9c9472335569c2d7971c86f319069e" + "c6ccd13b0b8f517c51fc2e42dc5e7bc3434f306955cf1dc575ea9e18617699045b92b006599afd94abb25018ea" + "f63cfcc247f76b728c4fc4e663dff64b90059d1d27f8ecd63bb548862b88bcd52e0711f222b15c022d214a2cc3" + "93e537e32d149c67aa84692f1a204475a7acceaa0ab5f823ea90af601bdfb7f4036971e1c786fca7fa7e8ab042" + "24307bcc3093886b54e4c9e6b7cb286d6259a8231ffae0f589f687f92232ac5384988631efb70dc85fc594bb3c" + "1c0ceebc08b37d8989da0ae786e30d1278ffddbac47484346afd8439495aa1d392ce76f8ebc8d3d1870a0698ca" + "b133cabbacae924013025e7bce5ac6aaf87684b6409a6a58e8bb294c249a5c7ca9b2961c57dc031485e3a000ec" + "ea4e908cf9f33e86f0fd5d4ca5996b73c273dfda3fe68aa6a385984cb7fd2bf5f69d997580b407d48845215422" + "c3c9fe52e7aa4b4e11c067db3e7c87c55f3b1400f796a4b873b666b7027c33138c1f310f65e20b53bcba019f1e" + "08aee1a89430744c8bd2dd3a788410caa4356099b87cab2463da107a6919af38c159a258ff6693dd71f1941a52" + "01d6a0b2fc52cfab6e0ba2c84c6231bc2a54fe1b6af1641e1168599ea03da913e537880f13128515085fd17b47" + "fe202b82152d1c7df2e66788a2d0e0aab0e6375d368f8064e29912f32d4c509408a642a597bcf39c3e6fe31873" + "e6173067cf3fc65702152e43a9d2cc7262e69550bd3c10e833c3c5ec48878b214426eca9cdc169f59cbc2c93dc" + "94562e05d94761c9f76191b505097dca964d56b9889f904347f6b250f5a1f2bf3c9e9f4370a164a4185e0d83c0" + "96e1799b8d950535cf96eec690fa765e9e74baea45f3157ba8c78158d365acc1a5abb358093cca6afcde287096" + "ba74b4238789ede0947083facfc9bb3129361a283d72fe860c9666877fb263650410ae5af9fd48e9a2214f9f0a" + "39f3b55edca84c836a745f8fc294d176b878fede1e375358d2e63bbbc0632752b19afda03e527b6e9deb32b0a8" + "e617f5396312b7769ccd164e43ba1ada90d97005ab8e4eda57d3a953b5cf5fac9676fc64dd7163bdb6b17f6984" + "f70070f2eadace62317215f240100db10283cd4b7c62f2ba1191c0feee9e6fc6026dcaec12ecb2329221130aac" + "18f08b091f5292e51c0ca35cfefabf9b86d8478f7cc9f2983260e6cec537081684119a02d51e0895d9ee9294cf" + "a6f695173fa816f168751cf1d79730ded3e7e97325d2582a6516436aa165260f576f330535cf28d6f9c26a6f7d" + "dd74b60e702826392ac9f16a1ccdb5" +) + + +fragmented_client_hello2 = bytes.fromhex( + "cd0000000108d520c3803f5de4d3000044bceadc93d1a46ab45f934299ba642a3a4bdac62cf80981105cc546c2" + "96b78fda0acdec8e8cb8a69e4d3446033f3edd0f52fe02d99c841336402b9c2419852414b9bc6b17128b1c198e" + "0f2a709895cddef029b738c7a8bf7917162e5709f7fa4933e6a9db5da418db8794e8458dd699eb31752c402cbf" + "3b6f0d7e6983dba686285a49b4c8724f9653ed4430667a242f4b0613aa37b039226b1c42a1cfaeab40cabbf6be" + "d7d49cbca3a10e8aced1560e44a22073a9432f39e16d177ecf89c4b3807ac748fed84d9811fe91aad76bf85bc8" + "c8b1def2985b8cce6226ce441924418f0c4c6895918e86065a3143dda8afce756c7318b3d861a1f0160d0814ef" + "118389f55198b0c5da4ed6d95a72b6f2a35ffc56bda85753dd146dd6eb29f64b51f7ca7e4e0bf7de82a5041e1f" + "a4dc7303f5b7dc31901185f787876ce81213a587cbe42bdcab63be1c146798641664fecc477b8112109cb317f6" + "f6fe1f3e36c2e843ec875ed8631ac7527817ab928c68a16ae672ca56464556a8c4c700c4f40920a028207911f3" + "2cd2840fce3504ca29f25524b1e9108dbded72ff0364443da17573badc99ad33f6c91baeca3c933258500e7b78" + "347ce76cf893a85f163698edd6209ac5d990f092cc609ff7faa6a0c2e5f57e4154bec72e2441028ad00cdce202" + "a07e0e9696578e0c17c152b2880874cad11631db5210efaf260d18dccecd04987f8ceea7e534c381d9aed5be28" + "8b2086103bd84fd6150037cb0bc1abacd3c2b3f1db213998b4b36e86e46264809fa2757f2b2764c0b94dc222ac" + "674a9f5c183fb40ef52e7b36ed8f3aaf1fe776643d819fb55284387b83f0ad688461ae8612784b36494585caa5" + "f05fb391216bedc23b00e759bebe0cd19f1d514b5faba8a061d36204dd7c4e8daf1150ae8441aadbbeff7735ff" + "613ecb2d1dabf256ffcee5b2ba07f0d2d53c7e98691b261cdadf5fac8ed2985720f7f460a8140fdb094870cc65" + "4656779bbfc095cd5bc666f18e44e86d765004b16a763c330fc165fdb604038067288d56fbd2e6ead2a7352406" + "4f6995a54ef529990239065ccf33ab5fa3e56ec2ff15b6981bab32658c5d4184407865f3a0e7c37d8d53ac4850" + "cfdb16887e04eea4284517b2141c1824babae24207ba14e91eb6a30735f33f664d7fefde94d582c06dd26922a6" + "6e4657c144ee9f99b7985ba1fd7dceb700cecdcb8950a57fc3b239709e84a4616d8e0f7865025b37d27e5cc7c2" + "b24b02745a89e12315ff4c4e87ea0d4ff90018f4243de3668b22547ba3a147540582b28152ad9412f0c2aea0c1" + "c0bf71c4176fed4c1d96853ef1d5db80ce4ba66d67c6998c052ebb2cf05511c54d233c24c2f9ed1ea14c305eba" + "9aed02ad0f1c48772646bfc4edc3f735cd3c16c885e1c54918e0070e1bcc68d835097fe43183e3ef26ab3d1993" + "dca6960b6ca0ffb1b90417114e55364211c1bd9688adfbb77ebfd7b7ffe47c45f3813390aeb5020fb63c018641" + "5a260ae26fab479e170843936d8e786120afa6edacecb32abfbe180237b0684507636fe221b2b980683a9f3610" + "8619c5ab4e271dd450d855f0085814750347da051a903bfa251b395cdc59356c68a7dae062e770c37f4d14f8b9" + "dd989248e7449e9b581ef9925d85e06372dd61bcbde872791e71855a5aa0c734d387731dde31d02500e1cd5f51" + "954f0e999398e6b0762bf6bb6bef9a" +) + + +def test_error_code_to_str(): + assert error_code_to_str(0x6) == "FINAL_SIZE_ERROR" + assert error_code_to_str(0x104) == "H3_CLOSED_CRITICAL_STREAM" + assert error_code_to_str(0xDEAD) == f"unknown error (0xdead)" + + +def test_is_success_error_code(): + assert is_success_error_code(0x0) + assert not is_success_error_code(0x6) + assert is_success_error_code(0x100) + assert not is_success_error_code(0x104) + assert not is_success_error_code(0xDEAD) + + +@pytest.mark.parametrize("value", ["s1 s2\n", "s1 s2"]) +def test_secrets_logger(value: str): + logger = MagicMock() + quic_logger = QuicSecretsLogger(logger) + assert quic_logger.write(value) == 6 + quic_logger.flush() + logger.assert_called_once_with(None, b"s1 s2") + + +class MockQuic(QuicConnection): + def __init__(self, event) -> None: + super().__init__(configuration=QuicConfiguration(is_client=True)) + self.event = event + + def next_event(self): + event = self.event + self.event = None + return event + + def datagrams_to_send(self, now: float): + return [] + + def get_timer(self): + return None + + +def make_mock_quic( + tctx: context.Context, + event: quic_events.QuicEvent | None = None, + established: bool = True, +) -> tuple[tutils.Playbook, MockQuic]: + tctx.client.state = connection.ConnectionState.CLOSED + quic_layer = QuicLayer(tctx, tctx.client, time=lambda: 0) + quic_layer.child_layer = TlsEchoLayer(tctx) + mock = MockQuic(event) + quic_layer.quic = mock + quic_layer.tunnel_state = ( + tls.tunnel.TunnelState.OPEN + if established + else tls.tunnel.TunnelState.ESTABLISHING + ) + return tutils.Playbook(quic_layer), mock + + +class TestQuicLayer: + @pytest.mark.parametrize("established", [True, False]) + def test_invalid_event(self, tctx: context.Context, established: bool): + class InvalidEvent(quic_events.QuicEvent): + pass + + playbook, conn = make_mock_quic( + tctx, event=InvalidEvent(), established=established + ) + with pytest.raises(AssertionError, match="Unexpected event"): + assert playbook >> events.DataReceived(tctx.client, b"") + + def test_invalid_stream_command(self, tctx: context.Context): + playbook, conn = make_mock_quic( + tctx, quic_events.DatagramFrameReceived(b"invalid-stream-command") + ) + with pytest.raises(AssertionError, match="Unexpected stream command"): + assert playbook >> events.DataReceived(tctx.client, b"") + + def test_close(self, tctx: context.Context): + playbook, conn = make_mock_quic( + tctx, quic_events.DatagramFrameReceived(b"close-connection") + ) + assert not conn._close_event + assert ( + playbook + >> events.DataReceived(tctx.client, b"") + << commands.CloseConnection(tctx.client) + ) + assert conn._close_event + assert conn._close_event.error_code == 0 + + def test_close_error(self, tctx: context.Context): + playbook, conn = make_mock_quic( + tctx, quic_events.DatagramFrameReceived(b"close-connection-error") + ) + assert not conn._close_event + assert ( + playbook + >> events.DataReceived(tctx.client, b"") + << CloseQuicConnection(tctx.client, 123, None, "error") + ) + assert conn._close_event + assert conn._close_event.error_code == 123 + + def test_datagram(self, tctx: context.Context): + playbook, conn = make_mock_quic( + tctx, quic_events.DatagramFrameReceived(b"packet") + ) + assert not conn._datagrams_pending + assert playbook >> events.DataReceived(tctx.client, b"") + assert len(conn._datagrams_pending) == 1 + assert conn._datagrams_pending[0] == b"packet" + + def test_stream_data(self, tctx: context.Context): + playbook, conn = make_mock_quic( + tctx, quic_events.StreamDataReceived(b"packet", False, 42) + ) + assert 42 not in conn._streams + assert playbook >> events.DataReceived(tctx.client, b"") + assert b"packet" == conn._streams[42].sender._buffer + + def test_stream_reset(self, tctx: context.Context): + playbook, conn = make_mock_quic(tctx, quic_events.StreamReset(123, 42)) + assert 42 not in conn._streams + assert playbook >> events.DataReceived(tctx.client, b"") + assert conn._streams[42].sender.reset_pending + assert conn._streams[42].sender._reset_error_code == 123 + + def test_stream_stop(self, tctx: context.Context): + playbook, conn = make_mock_quic(tctx, quic_events.StopSendingReceived(123, 24)) + assert 24 not in conn._streams + conn._get_or_create_stream_for_send(24) + assert playbook >> events.DataReceived(tctx.client, b"") + assert conn._streams[24].receiver.stop_pending + assert conn._streams[24].receiver._stop_error_code == 123 + + +class SSLTest: + """Helper container for QuicConnection object.""" + + def __init__( + self, + server_side: bool = False, + alpn: list[str] | None = None, + sni: str | None = "example.mitmproxy.org", + version: int | None = None, + settings: QuicTlsSettings | None = None, + ): + if settings is None: + self.ctx = QuicConfiguration( + is_client=not server_side, + max_datagram_frame_size=65536, + ) + + self.ctx.verify_mode = ssl.CERT_OPTIONAL + self.ctx.load_verify_locations( + cafile=tdata.path( + "mitmproxy/net/data/verificationcerts/trusted-root.crt" + ), + ) + + if alpn: + self.ctx.alpn_protocols = alpn + if server_side: + if sni == "192.0.2.42": + filename = "trusted-leaf-ip" + else: + filename = "trusted-leaf" + self.ctx.load_cert_chain( + certfile=tdata.path( + f"mitmproxy/net/data/verificationcerts/{filename}.crt" + ), + keyfile=tdata.path( + f"mitmproxy/net/data/verificationcerts/{filename}.key" + ), + ) + + self.ctx.server_name = None if server_side else sni + + if version is not None: + self.ctx.supported_versions = [version] + else: + assert alpn is None + assert version is None + self.ctx = tls_settings_to_configuration( + settings=settings, + is_client=not server_side, + server_name=sni, + ) + + self.now = 0.0 + self.address = (sni, 443) + self.quic = None if server_side else QuicConnection(configuration=self.ctx) + if not server_side: + self.quic.connect(self.address, now=self.now) + + def write(self, buf: bytes): + self.now = self.now + 0.1 + if self.quic is None: + quic_buf = QuicBuffer(data=buf) + header = pull_quic_header(quic_buf, host_cid_length=8) + self.quic = QuicConnection( + configuration=self.ctx, + original_destination_connection_id=header.destination_cid, + ) + self.quic.receive_datagram(buf, self.address, self.now) + + def read(self) -> bytes: + self.now = self.now + 0.1 + buf = b"" + has_data = False + for datagram, addr in self.quic.datagrams_to_send(self.now): + assert addr == self.address + buf += datagram + has_data = True + if not has_data: + raise AssertionError("no datagrams to send") + return buf + + def handshake_completed(self) -> bool: + while event := self.quic.next_event(): + if isinstance(event, quic_events.HandshakeCompleted): + return True + else: + return False + + +def _test_echo( + playbook: tutils.Playbook, tssl: SSLTest, conn: connection.Connection +) -> None: + tssl.quic.send_datagram_frame(b"Hello World") + data = tutils.Placeholder(bytes) + assert ( + playbook + >> events.DataReceived(conn, tssl.read()) + << commands.SendData(conn, data) + ) + tssl.write(data()) + while event := tssl.quic.next_event(): + if isinstance(event, quic_events.DatagramFrameReceived): + assert event.data == b"hello world" + break + else: + raise AssertionError() + + +def finish_handshake( + playbook: tutils.Playbook, + conn: connection.Connection, + tssl: SSLTest, + child_layer: type[T], +) -> T: + result: T | None = None + + def set_layer(next_layer: layer.NextLayer) -> None: + nonlocal result + result = child_layer(next_layer.context) + next_layer.layer = result + + data = tutils.Placeholder(bytes) + tls_hook_data = tutils.Placeholder(tls.TlsData) + if isinstance(conn, connection.Client): + established_hook = tls.TlsEstablishedClientHook(tls_hook_data) + else: + established_hook = tls.TlsEstablishedServerHook(tls_hook_data) + assert ( + playbook + >> events.DataReceived(conn, tssl.read()) + << established_hook + >> tutils.reply() + << commands.SendData(conn, data) + << layer.NextLayerHook(tutils.Placeholder()) + >> tutils.reply(side_effect=set_layer) + ) + assert tls_hook_data().conn.error is None + tssl.write(data()) + + assert result + return result + + +def reply_tls_start_client(alpn: str | None = None, *args, **kwargs) -> tutils.reply: + """ + Helper function to simplify the syntax for quic_start_client hooks. + """ + + def make_client_conn(tls_start: QuicTlsData) -> None: + config = QuicConfiguration() + config.load_cert_chain( + tdata.path("mitmproxy/net/data/verificationcerts/trusted-leaf.crt"), + tdata.path("mitmproxy/net/data/verificationcerts/trusted-leaf.key"), + ) + tls_start.settings = QuicTlsSettings( + certificate=config.certificate, + certificate_chain=config.certificate_chain, + certificate_private_key=config.private_key, + ) + if alpn is not None: + tls_start.settings.alpn_protocols = [alpn] + + return tutils.reply(*args, side_effect=make_client_conn, **kwargs) + + +def reply_tls_start_server(alpn: str | None = None, *args, **kwargs) -> tutils.reply: + """ + Helper function to simplify the syntax for quic_start_server hooks. + """ + + def make_server_conn(tls_start: QuicTlsData) -> None: + tls_start.settings = QuicTlsSettings( + ca_file=tdata.path("mitmproxy/net/data/verificationcerts/trusted-root.crt"), + verify_mode=ssl.CERT_REQUIRED, + ) + if alpn is not None: + tls_start.settings.alpn_protocols = [alpn] + + return tutils.reply(*args, side_effect=make_server_conn, **kwargs) + + +class TestServerQuic: + def test_repr(self, tctx: context.Context): + assert repr(ServerQuicLayer(tctx, time=lambda: 0)) + + def test_not_connected(self, tctx: context.Context): + """Test that we don't do anything if no server connection exists.""" + layer = ServerQuicLayer(tctx, time=lambda: 0) + layer.child_layer = TlsEchoLayer(tctx) + + assert ( + tutils.Playbook(layer) + >> events.DataReceived(tctx.client, b"Hello World") + << commands.SendData(tctx.client, b"hello world") + ) + + def test_simple(self, tctx: context.Context): + tssl = SSLTest(server_side=True) + + playbook = tutils.Playbook(ServerQuicLayer(tctx, time=lambda: tssl.now)) + tctx.server.address = ("example.mitmproxy.org", 443) + tctx.server.state = connection.ConnectionState.OPEN + tctx.server.sni = "example.mitmproxy.org" + + # send ClientHello, receive ClientHello + data = tutils.Placeholder(bytes) + assert ( + playbook + << QuicStartServerHook(tutils.Placeholder()) + >> reply_tls_start_server() + << commands.SendData(tctx.server, data) + << commands.RequestWakeup(0.2) + ) + tssl.write(data()) + assert not tssl.handshake_completed() + + # finish handshake (mitmproxy) + echo = finish_handshake(playbook, tctx.server, tssl, TlsEchoLayer) + + # finish handshake (locally) + assert tssl.handshake_completed() + playbook >> events.DataReceived(tctx.server, tssl.read()) + playbook << None + assert playbook + + assert tctx.server.tls_established + + # Echo + assert ( + playbook + >> events.DataReceived(tctx.client, b"foo") + << commands.SendData(tctx.client, b"foo") + ) + _test_echo(playbook, tssl, tctx.server) + + tssl.quic.close(42, None, "goodbye from simple") + playbook >> events.DataReceived(tctx.server, tssl.read()) + playbook << None + assert playbook + tssl.now = tssl.now + 60 + assert ( + playbook + >> tutils.reply(to=commands.RequestWakeup) + << commands.CloseConnection(tctx.server) + >> events.ConnectionClosed(tctx.server) + << None + ) + assert echo.closed + assert echo.closed.error_code == 42 + assert echo.closed.reason_phrase == "goodbye from simple" + + def test_untrusted_cert(self, tctx: context.Context): + """If the certificate is not trusted, we should fail.""" + tssl = SSLTest(server_side=True) + + playbook = tutils.Playbook(ServerQuicLayer(tctx, time=lambda: tssl.now)) + tctx.server.address = ("wrong.host.mitmproxy.org", 443) + tctx.server.sni = "wrong.host.mitmproxy.org" + + # send ClientHello + data = tutils.Placeholder(bytes) + assert ( + playbook + << layer.NextLayerHook(tutils.Placeholder()) + >> tutils.reply_next_layer(TlsEchoLayer) + >> events.DataReceived(tctx.client, b"open-connection") + << commands.OpenConnection(tctx.server) + >> tutils.reply(None) + << QuicStartServerHook(tutils.Placeholder()) + >> reply_tls_start_server() + << commands.SendData(tctx.server, data) + << commands.RequestWakeup(0.2) + ) + + # receive ServerHello, finish client handshake + tssl.write(data()) + assert not tssl.handshake_completed() + + # exchange termination data + data = tutils.Placeholder(bytes) + assert ( + playbook + >> events.DataReceived(tctx.server, tssl.read()) + << commands.SendData(tctx.server, data) + ) + tssl.write(data()) + tssl.now = tssl.now + 60 + + tls_hook_data = tutils.Placeholder(QuicTlsData) + assert ( + playbook + >> tutils.reply(to=commands.RequestWakeup) + << commands.Log( + tutils.StrMatching( + "Server QUIC handshake failed. hostname 'wrong.host.mitmproxy.org' doesn't match" + ), + WARNING, + ) + << tls.TlsFailedServerHook(tls_hook_data) + >> tutils.reply() + << commands.CloseConnection(tctx.server) + << commands.SendData( + tctx.client, + tutils.BytesMatching( + b"open-connection failed: hostname 'wrong.host.mitmproxy.org' doesn't match" + ), + ) + ) + assert tls_hook_data().conn.error.startswith( + "hostname 'wrong.host.mitmproxy.org' doesn't match" + ) + assert not tctx.server.tls_established + + +def make_client_tls_layer( + tctx: context.Context, no_server: bool = False, **kwargs +) -> tuple[tutils.Playbook, ClientQuicLayer, SSLTest]: + tssl_client = SSLTest(**kwargs) + + # This is a bit contrived as the client layer expects a server layer as parent. + # We also set child layers manually to avoid NextLayer noise. + server_layer = ( + DummyLayer(tctx) + if no_server + else ServerQuicLayer(tctx, time=lambda: tssl_client.now) + ) + client_layer = ClientQuicLayer(tctx, time=lambda: tssl_client.now) + server_layer.child_layer = client_layer + playbook = tutils.Playbook(server_layer) + + # Add some server config, this is needed anyways. + tctx.server.__dict__["address"] = ( + "example.mitmproxy.org", + 443, + ) # .address fails because connection is open + tctx.server.sni = "example.mitmproxy.org" + + # Start handshake. + assert not tssl_client.handshake_completed() + + return playbook, client_layer, tssl_client + + +class TestClientQuic: + def test_http3_disabled(self, tctx: context.Context): + """Test that we swallow QUIC packets if QUIC and HTTP/3 are disabled.""" + tctx.options.http3 = False + assert ( + tutils.Playbook(ClientQuicLayer(tctx, time=time.time), logs=True) + >> events.DataReceived(tctx.client, client_hello) + << commands.Log( + "Swallowing QUIC handshake because HTTP/3 is disabled.", DEBUG + ) + << None + ) + + def test_client_only(self, tctx: context.Context): + """Test QUIC with client only""" + playbook, client_layer, tssl_client = make_client_tls_layer(tctx) + client_layer.debug = " " + assert not tctx.client.tls_established + + # Send ClientHello, receive ServerHello + data = tutils.Placeholder(bytes) + assert ( + playbook + >> events.DataReceived(tctx.client, tssl_client.read()) + << tls.TlsClienthelloHook(tutils.Placeholder()) + >> tutils.reply() + << QuicStartClientHook(tutils.Placeholder()) + >> reply_tls_start_client() + << commands.SendData(tctx.client, data) + << commands.RequestWakeup(tutils.Placeholder()) + ) + tssl_client.write(data()) + assert tssl_client.handshake_completed() + # Finish Handshake + finish_handshake(playbook, tctx.client, tssl_client, TlsEchoLayer) + + assert tssl_client.quic.tls._peer_certificate + assert tctx.client.tls_established + + # Echo + _test_echo(playbook, tssl_client, tctx.client) + other_server = connection.Server(address=None) + assert ( + playbook + >> events.DataReceived(other_server, b"Plaintext") + << commands.SendData(other_server, b"plaintext") + ) + + # test the close log + tssl_client.now = tssl_client.now + 60 + assert ( + playbook + >> tutils.reply(to=commands.RequestWakeup) + << commands.Log( + tutils.StrMatching( + r" >> Wakeup\(command=RequestWakeup\({'delay': [.\d]+}\)\)" + ), + DEBUG, + ) + << commands.Log( + " [quic] close_notify Client(client:1234, state=open, tls) (reason=Idle timeout)", + DEBUG, + ) + << commands.CloseConnection(tctx.client) + ) + + @pytest.mark.parametrize("server_state", ["open", "closed"]) + def test_server_required( + self, tctx: context.Context, server_state: Literal["open", "closed"] + ): + """ + Test the scenario where a server connection is required (for example, because of an unknown ALPN) + to establish TLS with the client. + """ + if server_state == "open": + tctx.server.state = connection.ConnectionState.OPEN + tssl_server = SSLTest(server_side=True, alpn=["quux"]) + playbook, client_layer, tssl_client = make_client_tls_layer(tctx, alpn=["quux"]) + + # We should now get instructed to open a server connection. + data = tutils.Placeholder(bytes) + + def require_server_conn(client_hello: tls.ClientHelloData) -> None: + client_hello.establish_server_tls_first = True + + ( + playbook + >> events.DataReceived(tctx.client, tssl_client.read()) + << tls.TlsClienthelloHook(tutils.Placeholder()) + >> tutils.reply(side_effect=require_server_conn) + ) + if server_state == "closed": + playbook << commands.OpenConnection(tctx.server) + playbook >> tutils.reply(None) + assert ( + playbook + << QuicStartServerHook(tutils.Placeholder()) + >> reply_tls_start_server(alpn="quux") + << commands.SendData(tctx.server, data) + << commands.RequestWakeup(tutils.Placeholder()) + ) + + # Establish TLS with the server... + tssl_server.write(data()) + assert not tssl_server.handshake_completed() + + data = tutils.Placeholder(bytes) + assert ( + playbook + >> events.DataReceived(tctx.server, tssl_server.read()) + << tls.TlsEstablishedServerHook(tutils.Placeholder()) + >> tutils.reply() + << commands.SendData(tctx.server, data) + << commands.RequestWakeup(tutils.Placeholder()) + << QuicStartClientHook(tutils.Placeholder()) + ) + tssl_server.write(data()) + assert tctx.server.tls_established + # Server TLS is established, we can now reply to the client handshake... + + data = tutils.Placeholder(bytes) + assert ( + playbook + >> reply_tls_start_client(alpn="quux") + << commands.SendData(tctx.client, data) + << commands.RequestWakeup(tutils.Placeholder()) + ) + tssl_client.write(data()) + assert tssl_client.handshake_completed() + finish_handshake(playbook, tctx.client, tssl_client, TlsEchoLayer) + + # Both handshakes completed! + assert tctx.client.tls_established + assert tctx.server.tls_established + assert tctx.server.sni == tctx.client.sni + assert tctx.client.alpn == b"quux" + assert tctx.server.alpn == b"quux" + _test_echo(playbook, tssl_client, tctx.client) + _test_echo(playbook, tssl_server, tctx.server) + + @pytest.mark.parametrize("server_state", ["open", "closed"]) + def test_passthrough_from_clienthello( + self, tctx: context.Context, server_state: Literal["open", "closed"] + ): + """ + Test the scenario where the connection is moved to passthrough mode in the tls_clienthello hook. + """ + if server_state == "open": + tctx.server.timestamp_start = time.time() + tctx.server.state = connection.ConnectionState.OPEN + + playbook, client_layer, tssl_client = make_client_tls_layer(tctx, alpn=["quux"]) + client_layer.child_layer = TlsEchoLayer(client_layer.context) + + def make_passthrough(client_hello: tls.ClientHelloData) -> None: + client_hello.ignore_connection = True + + client_hello = tssl_client.read() + ( + playbook + >> events.DataReceived(tctx.client, client_hello) + << tls.TlsClienthelloHook(tutils.Placeholder()) + >> tutils.reply(side_effect=make_passthrough) + ) + if server_state == "closed": + playbook << commands.OpenConnection(tctx.server) + playbook >> tutils.reply(None) + assert ( + playbook + << commands.SendData(tctx.server, client_hello) # passed through unmodified + >> events.DataReceived( + tctx.server, b"ServerHello" + ) # and the same for the serverhello. + << commands.SendData(tctx.client, b"ServerHello") + ) + + @pytest.mark.parametrize( + "fragments", + [ + [fragmented_client_hello1, fragmented_client_hello2], + [fragmented_client_hello2, fragmented_client_hello1], + ], + ) + def test_fragmented_client_hello( + self, tctx: context.Context, fragments: list[bytes] + ): + client_layer = ClientQuicLayer(tctx, time=time.time) + playbook = tutils.Playbook(client_layer) + + assert not tctx.client.sni + + assert ( + playbook + >> events.Start() + >> events.DataReceived(tctx.client, fragments[0]) + >> events.DataReceived(tctx.client, fragments[1]) + << tls.TlsClienthelloHook(tutils.Placeholder()) + >> tutils.reply() + << QuicStartClientHook(tutils.Placeholder()) + ) + + assert tctx.client.sni == "localhost" + + @pytest.mark.parametrize( + "data,err", + [ + (b"\x16\x03\x01\x00\x00", "Packet fixed bit is zero (1603010000)"), + (b"test", "Malformed head (74657374)"), + ], + ) + def test_cannot_parse_clienthello( + self, tctx: context.Context, data: bytes, err: str + ): + """Test the scenario where we cannot parse the ClientHello""" + playbook, client_layer, tssl_client = make_client_tls_layer(tctx) + tls_hook_data = tutils.Placeholder(QuicTlsData) + + assert ( + playbook + >> events.DataReceived(tctx.client, data) + << commands.Log( + f"Client QUIC handshake failed. Cannot parse QUIC header: {err}", + level=WARNING, + ) + << tls.TlsFailedClientHook(tls_hook_data) + >> tutils.reply() + << commands.CloseConnection(tctx.client) + ) + assert tls_hook_data().conn.error + assert not tctx.client.tls_established + + # Make sure that an active server connection does not cause child layers to spawn. + client_layer.debug = "" + assert ( + playbook + >> events.DataReceived( + connection.Server(address=None), b"data on other stream" + ) + << commands.Log(">> DataReceived(server, b'data on other stream')", DEBUG) + << commands.Log( + "[quic] Swallowing DataReceived(server, b'data on other stream') as handshake failed.", + DEBUG, + ) + ) + + def test_mitmproxy_ca_is_untrusted(self, tctx: context.Context): + """Test the scenario where the client doesn't trust the mitmproxy CA.""" + playbook, client_layer, tssl_client = make_client_tls_layer( + tctx, sni="wrong.host.mitmproxy.org" + ) + playbook.logs = True + + data = tutils.Placeholder(bytes) + assert ( + playbook + >> events.DataReceived(tctx.client, tssl_client.read()) + << tls.TlsClienthelloHook(tutils.Placeholder()) + >> tutils.reply() + << QuicStartClientHook(tutils.Placeholder()) + >> reply_tls_start_client() + << commands.SendData(tctx.client, data) + << commands.RequestWakeup(tutils.Placeholder()) + ) + tssl_client.write(data()) + assert not tssl_client.handshake_completed() + + # Finish Handshake + tls_hook_data = tutils.Placeholder(QuicTlsData) + playbook >> events.DataReceived(tctx.client, tssl_client.read()) + assert playbook + tssl_client.now = tssl_client.now + 60 + assert ( + playbook + >> tutils.reply(to=commands.RequestWakeup) + << commands.Log( + tutils.StrMatching( + "Client QUIC handshake failed. hostname 'wrong.host.mitmproxy.org' doesn't match" + ), + WARNING, + ) + << tls.TlsFailedClientHook(tls_hook_data) + >> tutils.reply() + << commands.CloseConnection(tctx.client) + >> events.ConnectionClosed(tctx.client) + ) + assert not tctx.client.tls_established + assert tls_hook_data().conn.error + + def test_server_unavailable_and_no_settings(self, tctx: context.Context): + playbook, client_layer, tssl_client = make_client_tls_layer(tctx) + + def require_server_conn(client_hello: tls.ClientHelloData) -> None: + client_hello.establish_server_tls_first = True + + assert ( + playbook + >> events.DataReceived(tctx.client, tssl_client.read()) + << tls.TlsClienthelloHook(tutils.Placeholder()) + >> tutils.reply(side_effect=require_server_conn) + << commands.OpenConnection(tctx.server) + >> tutils.reply("I cannot open the server, Dave") + << commands.Log( + f"Unable to establish QUIC connection with server (I cannot open the server, Dave). " + f"Trying to establish QUIC with client anyway. " + f"If you plan to redirect requests away from this server, " + f"consider setting `connection_strategy` to `lazy` to suppress early connections." + ) + << QuicStartClientHook(tutils.Placeholder()) + ) + tctx.client.state = connection.ConnectionState.CLOSED + assert ( + playbook + >> tutils.reply() + << commands.Log(f"No QUIC context was provided, failing connection.", ERROR) + << commands.CloseConnection(tctx.client) + << commands.Log( + "Client QUIC handshake failed. connection closed early", WARNING + ) + << tls.TlsFailedClientHook(tutils.Placeholder()) + ) + + def test_no_server_tls(self, tctx: context.Context): + playbook, client_layer, tssl_client = make_client_tls_layer( + tctx, no_server=True + ) + + def require_server_conn(client_hello: tls.ClientHelloData) -> None: + client_hello.establish_server_tls_first = True + + assert ( + playbook + >> events.DataReceived(tctx.client, tssl_client.read()) + << tls.TlsClienthelloHook(tutils.Placeholder()) + >> tutils.reply(side_effect=require_server_conn) + << commands.Log( + f"Unable to establish QUIC connection with server (No server QUIC available.). " + f"Trying to establish QUIC with client anyway. " + f"If you plan to redirect requests away from this server, " + f"consider setting `connection_strategy` to `lazy` to suppress early connections." + ) + << QuicStartClientHook(tutils.Placeholder()) + ) + + def test_version_negotiation(self, tctx: context.Context): + # To trigger a version negotiation, use one of the reserved 0x?A?A?A?A versions. + # https://datatracker.ietf.org/doc/html/rfc9000#section-15 + playbook, client_layer, tssl_client = make_client_tls_layer( + tctx, version=0x1A2A3A4A + ) + assert ( + playbook + >> events.DataReceived(tctx.client, tssl_client.read()) + << commands.SendData(tctx.client, tutils.Placeholder()) + ) + assert client_layer.tunnel_state == tls.tunnel.TunnelState.ESTABLISHING + + def test_non_init_clienthello(self, tctx: context.Context): + playbook, client_layer, tssl_client = make_client_tls_layer(tctx) + data = ( + b"\xc2\x00\x00\x00\x01\x08q\xda\x98\x03X-\x13o\x08y\xa5RQv\xbe\xe3\xeb\x00@a\x98\x19\xf95t\xad-\x1c\\a\xdd\x8c\xd0\x15F" + b"\xdf\xdc\x87cb\x1eu\xb0\x95*\xac\xa8\xf7a \xb8\nQ\xbd=\xf5x\xca\r\xe6\x8b\x05 w\x9f\xcd\x8d\xcb\xa0\x06\x1e \x8d.\x8f" + b"T\xda\x12et\xe4\x83\x93X\x8aa\xd1\xb2\x18\xb6\xa7\xf50y\x9b\xc5T\xe1\x87\xdd\x9fqv\xb0\x90\xa7s" + b"\xee\x00\x00\x00\x01\x08q\xda\x98\x03X-\x13o\x08y\xa5RQv\xbe\xe3\xeb@a*.\xa8j\x90\x1b\x1a\x7fZ\x04\x0b\\\xc7\x00\x03" + b"\xd7sC\xf8G\x84\x1e\xba\xcf\x08Z\xdd\x98+\xaa\x98J\xca\xe3\xb7u1\x89\x00\xdf\x8e\x16`\xd9^\xc0@i\x1a\x10\x99\r\xd8" + b"\x1dv3\xc6\xb8\"\xb9\xa8F\x95K\x9a/\xbc'\xd8\xd8\x94\x8f\xe7B/\x05\x9d\xfb\x80\xa9\xda@\xe6\xb0J\xfe\xe0\x0f\x02L}" + b"\xd9\xed\xd2L\xa7\xcf" + ) + assert ( + playbook + >> events.DataReceived(tctx.client, data) + << commands.Log( + f"Client QUIC handshake failed. Invalid handshake received, roaming not supported. ({data.hex()})", + WARNING, + ) + << tls.TlsFailedClientHook(tutils.Placeholder()) + ) + assert client_layer.tunnel_state == tls.tunnel.TunnelState.ESTABLISHING + + def test_invalid_clienthello(self, tctx: context.Context): + playbook, client_layer, tssl_client = make_client_tls_layer(tctx) + data = client_hello[0:1200] + b"\x00" + client_hello[1200:] + assert ( + playbook + >> events.DataReceived(tctx.client, data) + << commands.Log( + f"Client QUIC handshake failed. Cannot parse ClientHello: Invalid ClientHello packet: payload_decrypt_error ({data.hex()})", + WARNING, + ) + << tls.TlsFailedClientHook(tutils.Placeholder()) + ) + assert client_layer.tunnel_state == tls.tunnel.TunnelState.ESTABLISHING + + def test_invalid_fragmented_clienthello(self, tctx: context.Context): + client_layer = ClientQuicLayer(tctx, time=time.time) + playbook = tutils.Playbook(client_layer) + + assert not tctx.client.sni + + invalid_frag2 = ( + fragmented_client_hello2[:300] + b"\x00" + fragmented_client_hello2[300:] + ) + data = fragmented_client_hello1 + b"\n" + invalid_frag2 + + assert ( + playbook + >> events.Start() + >> events.DataReceived(tctx.client, fragmented_client_hello1) + >> events.DataReceived(tctx.client, invalid_frag2) + << commands.Log( + f"Client QUIC handshake failed. Cannot parse ClientHello: Invalid ClientHello packet: payload_decrypt_error ({data.hex()})", + WARNING, + ) + << tls.TlsFailedClientHook(tutils.Placeholder()) + ) + assert client_layer.tunnel_state == tls.tunnel.TunnelState.ESTABLISHING + + def test_tls_reset(self, tctx: context.Context): + tctx.client.tls = True + tctx.client.sni = "some" + DummyLayer(tctx) + ClientQuicLayer(tctx, time=lambda: 0) + assert tctx.client.sni is None diff --git a/test/mitmproxy/proxy/layers/test_dns.py b/test/mitmproxy/proxy/layers/test_dns.py index ab520bb625..3ce36273b3 100644 --- a/test/mitmproxy/proxy/layers/test_dns.py +++ b/test/mitmproxy/proxy/layers/test_dns.py @@ -1,17 +1,64 @@ +import struct import time -from mitmproxy.proxy.commands import CloseConnection, Log, OpenConnection, SendData -from mitmproxy.proxy.events import ConnectionClosed, DataReceived -from mitmproxy.proxy.layers import dns +import pytest +from hypothesis import given +from hypothesis import HealthCheck +from hypothesis import settings +from hypothesis import strategies as st + +from ..tutils import Placeholder +from ..tutils import Playbook +from ..tutils import reply from mitmproxy.dns import DNSFlow -from mitmproxy.test.tutils import tdnsreq, tdnsresp -from ..tutils import Placeholder, Playbook, reply +from mitmproxy.net.dns import response_codes +from mitmproxy.proxy.commands import CloseConnection +from mitmproxy.proxy.commands import Log +from mitmproxy.proxy.commands import OpenConnection +from mitmproxy.proxy.commands import SendData +from mitmproxy.proxy.events import ConnectionClosed +from mitmproxy.proxy.events import DataReceived +from mitmproxy.proxy.layers import dns +from mitmproxy.test.tflow import tdnsreq +from mitmproxy.test.tflow import tdnsresp +from mitmproxy.test.tflow import terr + + +@settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) +@given(st.binary()) +def test_fuzz_unpack_tcp_message(tctx, data): + layer = dns.DNSLayer(tctx) + try: + layer.unpack_message(data, True) + except struct.error: + pass + + +@settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) +@given(st.binary()) +def test_fuzz_unpack_udp_message(tctx, data): + tctx.client.transport_protocol = "udp" + tctx.server.transport_protocol = "udp" + layer = dns.DNSLayer(tctx) + try: + layer.unpack_message(data, True) + except struct.error: + pass + + +@pytest.mark.parametrize("transport_protocol", ["tcp", "udp"]) +def test_invalid_and_dummy_end(tctx, transport_protocol): + tctx.client.transport_protocol = transport_protocol + tctx.server.transport_protocol = transport_protocol + + data = b"Not a DNS packet" + if tctx.client.transport_protocol == "tcp": + data = struct.pack("!H", len(data)) + data -def test_invalid_and_dummy_end(tctx): assert ( Playbook(dns.DNSLayer(tctx)) - >> DataReceived(tctx.client, b"Not a DNS packet") + >> DataReceived(tctx.client, data) << Log( "Client(client:1234, state=open) sent an invalid message: question #0: unpack encountered a label of length 99" ) @@ -20,7 +67,11 @@ def test_invalid_and_dummy_end(tctx): ) -def test_regular(tctx): +@pytest.mark.parametrize("transport_protocol", ["tcp", "udp"]) +def test_regular(tctx, transport_protocol): + tctx.client.transport_protocol = transport_protocol + tctx.server.transport_protocol = transport_protocol + f = Placeholder(DNSFlow) req = tdnsreq() @@ -36,12 +87,12 @@ def resolve(flow: DNSFlow): assert ( Playbook(dns.DNSLayer(tctx)) - >> DataReceived(tctx.client, req.packed) + >> DataReceived(tctx.client, dns.pack_message(req, transport_protocol)) << dns.DnsRequestHook(f) >> reply(side_effect=resolve) << dns.DnsResponseHook(f) >> reply() - << SendData(tctx.client, resp.packed) + << SendData(tctx.client, dns.pack_message(resp, transport_protocol)) >> ConnectionClosed(tctx.client) << None ) @@ -50,7 +101,11 @@ def resolve(flow: DNSFlow): assert not f().live -def test_regular_mode_no_hook(tctx): +@pytest.mark.parametrize("transport_protocol", ["tcp", "udp"]) +def test_regular_mode_no_hook(tctx, transport_protocol): + tctx.client.transport_protocol = transport_protocol + tctx.server.transport_protocol = transport_protocol + f = Placeholder(DNSFlow) layer = dns.DNSLayer(tctx) layer.context.server.address = None @@ -65,20 +120,35 @@ def no_resolve(flow: DNSFlow): assert ( Playbook(layer) - >> DataReceived(tctx.client, req.packed) + >> DataReceived( + tctx.client, dns.pack_message(req, tctx.client.transport_protocol) + ) << dns.DnsRequestHook(f) >> reply(side_effect=no_resolve) << dns.DnsErrorHook(f) >> reply() + << SendData( + tctx.client, + dns.pack_message( + req.fail(response_codes.SERVFAIL), tctx.client.transport_protocol + ), + ) >> ConnectionClosed(tctx.client) << None ) assert f().request == req assert not f().response assert not f().live + assert ( + f().error.msg == "No hook has set a response and there is no upstream server." + ) -def test_reverse_premature_close(tctx): +@pytest.mark.parametrize("transport_protocol", ["tcp", "udp"]) +def test_reverse_premature_close(tctx, transport_protocol): + tctx.client.transport_protocol = transport_protocol + tctx.server.transport_protocol = transport_protocol + f = Placeholder(DNSFlow) layer = dns.DNSLayer(tctx) layer.context.server.address = ("8.8.8.8", 53) @@ -87,12 +157,14 @@ def test_reverse_premature_close(tctx): assert ( Playbook(layer) - >> DataReceived(tctx.client, req.packed) + >> DataReceived( + tctx.client, dns.pack_message(req, tctx.client.transport_protocol) + ) << dns.DnsRequestHook(f) >> reply() << OpenConnection(tctx.server) >> reply(None) - << SendData(tctx.server, req.packed) + << SendData(tctx.server, dns.pack_message(req, tctx.server.transport_protocol)) >> ConnectionClosed(tctx.client) << CloseConnection(tctx.server) << None @@ -104,7 +176,41 @@ def test_reverse_premature_close(tctx): assert f().request == req -def test_reverse(tctx): +def test_regular_hook_err(tctx): + f = Placeholder(DNSFlow) + + req = tdnsreq() + + def err(flow: DNSFlow): + flow.error = terr() + + assert ( + Playbook(dns.DNSLayer(tctx)) + >> DataReceived( + tctx.client, dns.pack_message(req, tctx.client.transport_protocol) + ) + << dns.DnsRequestHook(f) + >> reply(side_effect=err) + << dns.DnsErrorHook(f) + >> reply() + << SendData( + tctx.client, + dns.pack_message( + req.fail(response_codes.SERVFAIL), tctx.client.transport_protocol + ), + ) + >> ConnectionClosed(tctx.client) + << None + ) + assert f().error + assert not f().live + + +@pytest.mark.parametrize("transport_protocol", ["tcp", "udp"]) +def test_reverse(tctx, transport_protocol): + tctx.client.transport_protocol = transport_protocol + tctx.server.transport_protocol = transport_protocol + f = Placeholder(DNSFlow) layer = dns.DNSLayer(tctx) layer.context.server.address = ("8.8.8.8", 53) @@ -114,16 +220,20 @@ def test_reverse(tctx): assert ( Playbook(layer) - >> DataReceived(tctx.client, req.packed) + >> DataReceived( + tctx.client, dns.pack_message(req, tctx.client.transport_protocol) + ) << dns.DnsRequestHook(f) >> reply() << OpenConnection(tctx.server) >> reply(None) - << SendData(tctx.server, req.packed) - >> DataReceived(tctx.server, resp.packed) + << SendData(tctx.server, dns.pack_message(req, tctx.server.transport_protocol)) + >> DataReceived( + tctx.server, dns.pack_message(resp, tctx.server.transport_protocol) + ) << dns.DnsResponseHook(f) >> reply() - << SendData(tctx.client, resp.packed) + << SendData(tctx.client, dns.pack_message(resp, tctx.client.transport_protocol)) >> ConnectionClosed(tctx.client) << CloseConnection(tctx.server) << None @@ -136,7 +246,11 @@ def test_reverse(tctx): assert f().request == req and f().response == resp -def test_reverse_fail_connection(tctx): +@pytest.mark.parametrize("transport_protocol", ["tcp", "udp"]) +def test_reverse_fail_connection(tctx, transport_protocol): + tctx.client.transport_protocol = transport_protocol + tctx.server.transport_protocol = transport_protocol + f = Placeholder(DNSFlow) layer = dns.DNSLayer(tctx) layer.context.server.address = ("8.8.8.8", 53) @@ -145,13 +259,21 @@ def test_reverse_fail_connection(tctx): assert ( Playbook(layer) - >> DataReceived(tctx.client, req.packed) + >> DataReceived( + tctx.client, dns.pack_message(req, tctx.client.transport_protocol) + ) << dns.DnsRequestHook(f) >> reply() << OpenConnection(tctx.server) >> reply("UDP no likey today.") << dns.DnsErrorHook(f) >> reply() + << SendData( + tctx.client, + dns.pack_message( + req.fail(response_codes.SERVFAIL), tctx.client.transport_protocol + ), + ) << None ) assert f().request @@ -161,7 +283,11 @@ def test_reverse_fail_connection(tctx): assert f().request == req -def test_reverse_with_query_resend(tctx): +@pytest.mark.parametrize("transport_protocol", ["tcp", "udp"]) +def test_reverse_with_query_resend(tctx, transport_protocol): + tctx.client.transport_protocol = transport_protocol + tctx.server.transport_protocol = transport_protocol + f = Placeholder(DNSFlow) layer = dns.DNSLayer(tctx) layer.context.server.address = ("8.8.8.8", 53) @@ -173,20 +299,26 @@ def test_reverse_with_query_resend(tctx): assert ( Playbook(layer) - >> DataReceived(tctx.client, req.packed) + >> DataReceived( + tctx.client, dns.pack_message(req, tctx.client.transport_protocol) + ) << dns.DnsRequestHook(f) >> reply() << OpenConnection(tctx.server) >> reply(None) - << SendData(tctx.server, req.packed) - >> DataReceived(tctx.client, req2.packed) + << SendData(tctx.server, dns.pack_message(req, tctx.server.transport_protocol)) + >> DataReceived( + tctx.client, dns.pack_message(req2, tctx.client.transport_protocol) + ) << dns.DnsRequestHook(f) >> reply() - << SendData(tctx.server, req2.packed) - >> DataReceived(tctx.server, resp.packed) + << SendData(tctx.server, dns.pack_message(req2, tctx.server.transport_protocol)) + >> DataReceived( + tctx.server, dns.pack_message(resp, tctx.server.transport_protocol) + ) << dns.DnsResponseHook(f) >> reply() - << SendData(tctx.client, resp.packed) + << SendData(tctx.client, dns.pack_message(resp, tctx.client.transport_protocol)) >> ConnectionClosed(tctx.client) << CloseConnection(tctx.server) << None @@ -198,3 +330,151 @@ def test_reverse_with_query_resend(tctx): resp.timestamp = f().response.timestamp assert f().request == req2 assert f().response == resp + + +def test_tcp_message_over_multiple_events(tctx): + tctx.client.transport_protocol = "tcp" + tctx.server.transport_protocol = "tcp" + + layer = dns.DNSLayer(tctx) + layer.context.server.address = ("8.8.8.8", 53) + f = Placeholder(DNSFlow) + req = tdnsreq() + resp = tdnsresp() + resp_bytes = dns.pack_message(resp, tctx.client.transport_protocol) + split = len(resp_bytes) // 2 + + assert ( + Playbook(layer) + >> DataReceived( + tctx.client, dns.pack_message(req, tctx.client.transport_protocol) + ) + << dns.DnsRequestHook(f) + >> reply() + << OpenConnection(tctx.server) + >> reply(None) + << SendData(tctx.server, dns.pack_message(req, tctx.server.transport_protocol)) + >> DataReceived(tctx.server, resp_bytes[:split]) + >> DataReceived(tctx.server, resp_bytes[split:]) + << dns.DnsResponseHook(f) + >> reply() + << SendData(tctx.client, dns.pack_message(resp, tctx.client.transport_protocol)) + >> ConnectionClosed(tctx.client) + << CloseConnection(tctx.server) + << None + ) + + +def test_query_pipelining_same_event(tctx): + tctx.client.transport_protocol = "tcp" + tctx.server.transport_protocol = "tcp" + + layer = dns.DNSLayer(tctx) + layer.context.server.address = ("8.8.8.8", 53) + f1 = Placeholder(DNSFlow) + f2 = Placeholder(DNSFlow) + req1 = tdnsreq(id=1) + req2 = tdnsreq(id=2) + resp1 = tdnsresp(id=1) + resp2 = tdnsresp(id=2) + req_bytes = dns.pack_message( + req1, tctx.client.transport_protocol + ) + dns.pack_message(req2, tctx.client.transport_protocol) + + assert ( + Playbook(layer) + >> DataReceived(tctx.client, req_bytes) + << dns.DnsRequestHook(f1) + >> reply() + << OpenConnection(tctx.server) + >> reply(None) + << SendData(tctx.server, dns.pack_message(req1, tctx.server.transport_protocol)) + << dns.DnsRequestHook(f2) + >> reply() + << SendData(tctx.server, dns.pack_message(req2, tctx.server.transport_protocol)) + >> DataReceived( + tctx.server, dns.pack_message(resp1, tctx.server.transport_protocol) + ) + << dns.DnsResponseHook(f1) + >> reply() + << SendData( + tctx.client, dns.pack_message(resp1, tctx.server.transport_protocol) + ) + >> DataReceived( + tctx.server, dns.pack_message(resp2, tctx.server.transport_protocol) + ) + << dns.DnsResponseHook(f2) + >> reply() + << SendData( + tctx.client, dns.pack_message(resp2, tctx.server.transport_protocol) + ) + >> ConnectionClosed(tctx.client) + << CloseConnection(tctx.server) + << None + ) + + +def test_query_pipelining_multiple_events(tctx): + tctx.client.transport_protocol = "tcp" + tctx.server.transport_protocol = "tcp" + + layer = dns.DNSLayer(tctx) + layer.context.server.address = ("8.8.8.8", 53) + f1 = Placeholder(DNSFlow) + f2 = Placeholder(DNSFlow) + req1 = tdnsreq(id=1) + req2 = tdnsreq(id=2) + resp1 = tdnsresp(id=1) + resp2 = tdnsresp(id=2) + req_bytes = dns.pack_message( + req1, tctx.client.transport_protocol + ) + dns.pack_message(req2, tctx.client.transport_protocol) + split = len(req_bytes) * 3 // 4 + + assert ( + Playbook(layer) + >> DataReceived(tctx.client, req_bytes[:split]) + << dns.DnsRequestHook(f1) + >> reply() + << OpenConnection(tctx.server) + >> reply(None) + << SendData(tctx.server, dns.pack_message(req1, tctx.server.transport_protocol)) + >> DataReceived( + tctx.server, dns.pack_message(resp1, tctx.server.transport_protocol) + ) + << dns.DnsResponseHook(f1) + >> reply() + << SendData( + tctx.client, dns.pack_message(resp1, tctx.server.transport_protocol) + ) + >> DataReceived(tctx.client, req_bytes[split:]) + << dns.DnsRequestHook(f2) + >> reply() + << SendData(tctx.server, dns.pack_message(req2, tctx.server.transport_protocol)) + >> DataReceived( + tctx.server, dns.pack_message(resp2, tctx.server.transport_protocol) + ) + << dns.DnsResponseHook(f2) + >> reply() + << SendData( + tctx.client, dns.pack_message(resp2, tctx.server.transport_protocol) + ) + >> ConnectionClosed(tctx.client) + << CloseConnection(tctx.server) + << None + ) + + +def test_invalid_tcp_message_length(tctx): + tctx.client.transport_protocol = "tcp" + tctx.server.transport_protocol = "tcp" + + assert ( + Playbook(dns.DNSLayer(tctx)) + >> DataReceived(tctx.client, b"\x00\x00") + << Log( + "Client(client:1234, state=open) sent an invalid message: Message length field cannot be zero" + ) + << CloseConnection(tctx.client) + >> ConnectionClosed(tctx.client) + ) diff --git a/test/mitmproxy/proxy/layers/test_modes.py b/test/mitmproxy/proxy/layers/test_modes.py index 3937213751..8ad48c74d2 100644 --- a/test/mitmproxy/proxy/layers/test_modes.py +++ b/test/mitmproxy/proxy/layers/test_modes.py @@ -1,36 +1,46 @@ import copy import pytest -from mitmproxy import dns +from mitmproxy import dns from mitmproxy.addons.proxyauth import ProxyAuth -from mitmproxy.connection import Client, Server +from mitmproxy.connection import Client +from mitmproxy.connection import ConnectionState +from mitmproxy.connection import Server from mitmproxy.proxy import layers -from mitmproxy.proxy.commands import ( - CloseConnection, - Log, - OpenConnection, - SendData, -) +from mitmproxy.proxy.commands import CloseConnection +from mitmproxy.proxy.commands import Log +from mitmproxy.proxy.commands import OpenConnection +from mitmproxy.proxy.commands import RequestWakeup +from mitmproxy.proxy.commands import SendData from mitmproxy.proxy.context import Context -from mitmproxy.proxy.events import ConnectionClosed, DataReceived -from mitmproxy.proxy.layer import NextLayer, NextLayerHook -from mitmproxy.proxy.layers import http, modes, tcp, tls +from mitmproxy.proxy.events import ConnectionClosed +from mitmproxy.proxy.events import DataReceived +from mitmproxy.proxy.layer import NextLayer +from mitmproxy.proxy.layer import NextLayerHook +from mitmproxy.proxy.layers import http +from mitmproxy.proxy.layers import modes +from mitmproxy.proxy.layers import quic +from mitmproxy.proxy.layers import tcp +from mitmproxy.proxy.layers import tls +from mitmproxy.proxy.layers import udp from mitmproxy.proxy.layers.http import HTTPMode -from mitmproxy.proxy.layers.tcp import TcpMessageHook, TcpStartHook -from mitmproxy.proxy.layers.tls import ( - ClientTLSLayer, - TlsStartClientHook, - TlsStartServerHook, -) +from mitmproxy.proxy.layers.tcp import TcpMessageHook +from mitmproxy.proxy.layers.tcp import TcpStartHook +from mitmproxy.proxy.layers.tls import ClientTLSLayer +from mitmproxy.proxy.layers.tls import TlsStartClientHook +from mitmproxy.proxy.layers.tls import TlsStartServerHook from mitmproxy.proxy.mode_specs import ProxyMode from mitmproxy.tcp import TCPFlow +from mitmproxy.test import taddons from mitmproxy.test import tflow -from test.mitmproxy.proxy.layers.test_tls import ( - reply_tls_start_client, - reply_tls_start_server, -) -from test.mitmproxy.proxy.tutils import Placeholder, Playbook, reply, reply_next_layer +from mitmproxy.udp import UDPFlow +from test.mitmproxy.proxy.layers.test_tls import reply_tls_start_client +from test.mitmproxy.proxy.layers.test_tls import reply_tls_start_server +from test.mitmproxy.proxy.tutils import Placeholder +from test.mitmproxy.proxy.tutils import Playbook +from test.mitmproxy.proxy.tutils import reply +from test.mitmproxy.proxy.tutils import reply_next_layer def test_upstream_https(tctx): @@ -43,12 +53,24 @@ def test_upstream_https(tctx): curl -x localhost:8080 -k http://example.com """ tctx1 = Context( - Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), + Client( + peername=("client", 1234), + sockname=("127.0.0.1", 8080), + timestamp_start=1605699329, + state=ConnectionState.OPEN, + ), copy.deepcopy(tctx.options), ) - tctx1.client.proxy_mode = ProxyMode.parse("upstream:https://example.mitmproxy.org:8081") + tctx1.client.proxy_mode = ProxyMode.parse( + "upstream:https://example.mitmproxy.org:8081" + ) tctx2 = Context( - Client(("client", 4321), ("127.0.0.1", 8080), 1605699329), + Client( + peername=("client", 4321), + sockname=("127.0.0.1", 8080), + timestamp_start=1605699329, + state=ConnectionState.OPEN, + ), copy.deepcopy(tctx.options), ) assert tctx2.client.proxy_mode == ProxyMode.parse("regular") @@ -91,8 +113,8 @@ def test_upstream_https(tctx): << SendData(tctx2.client, serverhello) ) assert ( - # forward serverhello to proxy1 proxy1 + # forward serverhello to proxy1 >> DataReceived(upstream, serverhello()) << SendData(upstream, request) ) @@ -152,6 +174,9 @@ def test_reverse_proxy(tctx, keep_host_header): def test_reverse_dns(tctx): + tctx.client.transport_protocol = "udp" + tctx.server.transport_protocol = "udp" + f = Placeholder(dns.DNSFlow) server = Placeholder(Server) tctx.client.proxy_mode = ProxyMode.parse("reverse:dns://8.8.8.8:53") @@ -159,6 +184,8 @@ def test_reverse_dns(tctx): assert ( Playbook(modes.ReverseProxy(tctx), hooks=False) >> DataReceived(tctx.client, tflow.tdnsreq().packed) + << NextLayerHook(Placeholder(NextLayer)) + >> reply_next_layer(layers.DNSLayer) << layers.dns.DnsRequestHook(f) >> reply(None) << OpenConnection(server) @@ -168,6 +195,56 @@ def test_reverse_dns(tctx): assert server().address == ("8.8.8.8", 53) +@pytest.mark.parametrize("keep_host_header", [True, False]) +def test_quic(tctx: Context, keep_host_header: bool): + with taddons.context(): + tctx.options.keep_host_header = keep_host_header + tctx.server.sni = "other.example.com" + tctx.client.proxy_mode = ProxyMode.parse("reverse:quic://example.org:443") + client_hello = Placeholder(bytes) + + def set_settings(data: quic.QuicTlsData): + data.settings = quic.QuicTlsSettings() + + assert ( + Playbook(modes.ReverseProxy(tctx)) + << OpenConnection(tctx.server) + >> reply(None) + >> DataReceived(tctx.client, b"\x00") + << NextLayerHook(Placeholder(NextLayer)) + >> reply_next_layer(layers.ServerQuicLayer) + << quic.QuicStartServerHook(Placeholder(quic.QuicTlsData)) + >> reply(side_effect=set_settings) + << SendData(tctx.server, client_hello) + << RequestWakeup(Placeholder(float)) + ) + assert tctx.server.address == ("example.org", 443) + assert quic.quic_parse_client_hello_from_datagrams([client_hello()]).sni == ( + "other.example.com" if keep_host_header else "example.org" + ) + + +def test_udp(tctx: Context): + tctx.client.proxy_mode = ProxyMode.parse("reverse:udp://1.2.3.4:5") + flow = Placeholder(UDPFlow) + assert ( + Playbook(modes.ReverseProxy(tctx)) + << OpenConnection(tctx.server) + >> reply(None) + >> DataReceived(tctx.client, b"test-input") + << NextLayerHook(Placeholder(NextLayer)) + >> reply_next_layer(layers.UDPLayer) + << udp.UdpStartHook(flow) + >> reply() + << udp.UdpMessageHook(flow) + >> reply() + << SendData(tctx.server, b"test-input") + ) + assert tctx.server.address == ("1.2.3.4", 5) + assert len(flow().messages) == 1 + assert flow().messages[0].content == b"test-input" + + @pytest.mark.parametrize("patch", [True, False]) @pytest.mark.parametrize("connection_strategy", ["eager", "lazy"]) def test_reverse_proxy_tcp_over_tls( @@ -179,9 +256,6 @@ def test_reverse_proxy_tcp_over_tls( reverse proxying. """ - if patch: - monkeypatch.setattr(tls, "ServerTLSLayer", tls.MockTLSLayer) - flow = Placeholder(TCPFlow) data = Placeholder(bytes) tctx.client.proxy_mode = ProxyMode.parse("reverse:https://localhost:8000") @@ -206,8 +280,8 @@ def test_reverse_proxy_tcp_over_tls( ) if connection_strategy == "lazy": ( - # only now we open a connection playbook + # only now we open a connection << OpenConnection(tctx.server) >> reply(None) ) @@ -216,6 +290,11 @@ def test_reverse_proxy_tcp_over_tls( ) assert data() == b"\x01\x02\x03" else: + ( + playbook + << NextLayerHook(Placeholder(NextLayer)) + >> reply_next_layer(tls.ServerTLSLayer) + ) if connection_strategy == "lazy": ( playbook @@ -372,7 +451,7 @@ def test_socks5_trickle(tctx: Context): r"Unsupported SOCKS5 request: b'\x05\x02\x00\x01\x7f\x00\x00\x01\x124'", ), ( - CLIENT_HELLO + b"\x05\x01\x00\xFF\x00\x00", + CLIENT_HELLO + b"\x05\x01\x00\xff\x00\x00", SERVER_HELLO + b"\x05\x08\x00\x01\x00\x00\x00\x00\x00\x00", r"Unknown address type: 255", ), @@ -451,7 +530,7 @@ def test_socks5_auth_success( b"\x05\x01\x00", None, None, - b"\x05\xFF\x00\x01\x00\x00\x00\x00\x00\x00", + b"\x05\xff\x00\x01\x00\x00\x00\x00\x00\x00", "Client does not support SOCKS5 with user/password authentication.", ), ( diff --git a/test/mitmproxy/proxy/layers/test_socks5_fuzz.py b/test/mitmproxy/proxy/layers/test_socks5_fuzz.py index 4e882889a5..e8762fd35a 100644 --- a/test/mitmproxy/proxy/layers/test_socks5_fuzz.py +++ b/test/mitmproxy/proxy/layers/test_socks5_fuzz.py @@ -8,7 +8,14 @@ from mitmproxy.proxy.layers.modes import Socks5Proxy opts = options.Options() -tctx = Context(Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), opts) +tctx = Context( + Client( + peername=("client", 1234), + sockname=("127.0.0.1", 8080), + timestamp_start=1605699329, + ), + opts, +) @given(binary()) diff --git a/test/mitmproxy/proxy/layers/test_tcp.py b/test/mitmproxy/proxy/layers/test_tcp.py index d4f4947b09..7864622599 100644 --- a/test/mitmproxy/proxy/layers/test_tcp.py +++ b/test/mitmproxy/proxy/layers/test_tcp.py @@ -1,11 +1,18 @@ import pytest -from mitmproxy.proxy.commands import CloseConnection, OpenConnection, SendData -from mitmproxy.proxy.events import ConnectionClosed, DataReceived +from ..tutils import Placeholder +from ..tutils import Playbook +from ..tutils import reply +from mitmproxy.proxy.commands import CloseConnection +from mitmproxy.proxy.commands import CloseTcpConnection +from mitmproxy.proxy.commands import OpenConnection +from mitmproxy.proxy.commands import SendData +from mitmproxy.proxy.events import ConnectionClosed +from mitmproxy.proxy.events import DataReceived from mitmproxy.proxy.layers import tcp from mitmproxy.proxy.layers.tcp import TcpMessageInjected -from mitmproxy.tcp import TCPFlow, TCPMessage -from ..tutils import Placeholder, Playbook, reply +from mitmproxy.tcp import TCPFlow +from mitmproxy.tcp import TCPMessage def test_open_connection(tctx): @@ -52,7 +59,7 @@ def test_simple(tctx): >> reply() << SendData(tctx.client, b"hi") >> ConnectionClosed(tctx.server) - << CloseConnection(tctx.client, half_close=True) + << CloseTcpConnection(tctx.client, half_close=True) >> ConnectionClosed(tctx.client) << CloseConnection(tctx.server) << tcp.TcpEndHook(f) @@ -88,7 +95,7 @@ def test_receive_data_after_half_close(tctx): >> DataReceived(tctx.client, b"eof-delimited-request") << SendData(tctx.server, b"eof-delimited-request") >> ConnectionClosed(tctx.client) - << CloseConnection(tctx.server, half_close=True) + << CloseTcpConnection(tctx.server, half_close=True) >> DataReceived(tctx.server, b"i'm late") << SendData(tctx.client, b"i'm late") >> ConnectionClosed(tctx.server) diff --git a/test/mitmproxy/proxy/layers/test_tls.py b/test/mitmproxy/proxy/layers/test_tls.py index 9f1ae5a138..6e08fa7c10 100644 --- a/test/mitmproxy/proxy/layers/test_tls.py +++ b/test/mitmproxy/proxy/layers/test_tls.py @@ -1,35 +1,29 @@ import ssl -from logging import DEBUG, WARNING - import time -from typing import Optional +from logging import DEBUG +from logging import WARNING import pytest - from OpenSSL import SSL + from mitmproxy import connection -from mitmproxy.connection import ConnectionState, Server -from mitmproxy.proxy import commands, context, events, layer +from mitmproxy.connection import ConnectionState +from mitmproxy.connection import Server +from mitmproxy.proxy import commands +from mitmproxy.proxy import context +from mitmproxy.proxy import events +from mitmproxy.proxy import layer from mitmproxy.proxy.layers import tls -from mitmproxy.tls import ClientHelloData, TlsData +from mitmproxy.tls import ClientHelloData +from mitmproxy.tls import TlsData from mitmproxy.utils import data from test.mitmproxy.proxy import tutils -from test.mitmproxy.proxy.tutils import BytesMatching, StrMatching +from test.mitmproxy.proxy.tutils import BytesMatching +from test.mitmproxy.proxy.tutils import StrMatching tlsdata = data.Data(__name__) -def test_is_tls_handshake_record(): - assert tls.is_tls_handshake_record(bytes.fromhex("160300")) - assert tls.is_tls_handshake_record(bytes.fromhex("160301")) - assert tls.is_tls_handshake_record(bytes.fromhex("160302")) - assert tls.is_tls_handshake_record(bytes.fromhex("160303")) - assert not tls.is_tls_handshake_record(bytes.fromhex("ffffff")) - assert not tls.is_tls_handshake_record(bytes.fromhex("")) - assert not tls.is_tls_handshake_record(bytes.fromhex("160304")) - assert not tls.is_tls_handshake_record(bytes.fromhex("150301")) - - def test_record_contents(): data = bytes.fromhex("1603010002beef" "1603010001ff") assert list(tls.handshake_record_contents(data)) == [b"\xbe\xef", b"\xff"] @@ -93,9 +87,9 @@ class SSLTest: def __init__( self, server_side: bool = False, - alpn: Optional[list[str]] = None, - sni: Optional[bytes] = b"example.mitmproxy.org", - max_ver: Optional[ssl.TLSVersion] = None, + alpn: list[str] | None = None, + sni: bytes | None = b"example.mitmproxy.org", + max_ver: ssl.TLSVersion | None = None, ): self.inc = ssl.MemoryBIO() self.out = ssl.MemoryBIO() @@ -136,7 +130,7 @@ def __init__( def bio_write(self, buf: bytes) -> int: return self.inc.write(buf) - def bio_read(self, bufsize: int = 2 ** 16) -> bytes: + def bio_read(self, bufsize: int = 2**16) -> bytes: return self.out.read(bufsize) def do_handshake(self) -> None: @@ -158,7 +152,7 @@ def _test_echo( class TlsEchoLayer(tutils.EchoLayer): - err: Optional[str] = None + err: str | None = None def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: if isinstance(event, events.DataReceived) and event.data == b"open-connection": @@ -191,9 +185,7 @@ def finish_handshake( tssl.bio_write(data()) -def reply_tls_start_client( - alpn: Optional[bytes] = None, *args, **kwargs -) -> tutils.reply: +def reply_tls_start_client(alpn: bytes | None = None, *args, **kwargs) -> tutils.reply: """ Helper function to simplify the syntax for tls_start_client hooks. """ @@ -220,9 +212,7 @@ def make_client_conn(tls_start: TlsData) -> None: return tutils.reply(*args, side_effect=make_client_conn, **kwargs) -def reply_tls_start_server( - alpn: Optional[bytes] = None, *args, **kwargs -) -> tutils.reply: +def reply_tls_start_server(alpn: bytes | None = None, *args, **kwargs) -> tutils.reply: """ Helper function to simplify the syntax for tls_start_server hooks. """ @@ -365,7 +355,9 @@ def test_untrusted_cert(self, tctx): >> events.DataReceived(tctx.server, tssl.bio_read()) << commands.Log( # different casing in OpenSSL < 3.0 - StrMatching("Server TLS handshake failed. Certificate verify failed: [Hh]ostname mismatch"), + StrMatching( + "Server TLS handshake failed. Certificate verify failed: [Hh]ostname mismatch" + ), WARNING, ) << tls.TlsFailedServerHook(tls_hook_data) @@ -374,11 +366,14 @@ def test_untrusted_cert(self, tctx): << commands.SendData( tctx.client, # different casing in OpenSSL < 3.0 - BytesMatching(b"open-connection failed: Certificate verify failed: [Hh]ostname mismatch"), + BytesMatching( + b"open-connection failed: Certificate verify failed: [Hh]ostname mismatch" + ), ) ) assert ( - tls_hook_data().conn.error.lower() == "Certificate verify failed: Hostname mismatch".lower() + tls_hook_data().conn.error.lower() + == "Certificate verify failed: Hostname mismatch".lower() ) assert not tctx.server.tls_established @@ -501,7 +496,7 @@ def test_client_only(self, tctx: context.Context): # Echo _test_echo(playbook, tssl_client, tctx.client) - other_server = Server(None) + other_server = Server(address=None) assert ( playbook >> events.DataReceived(other_server, b"Plaintext") @@ -636,10 +631,10 @@ def test_cannot_parse_clienthello(self, tctx: context.Context): client_layer.debug = "" assert ( playbook - >> events.DataReceived(Server(None), b"data on other stream") + >> events.DataReceived(Server(address=None), b"data on other stream") << commands.Log(">> DataReceived(server, b'data on other stream')", DEBUG) << commands.Log( - "Swallowing DataReceived(server, b'data on other stream') as handshake failed.", + "[tls] Swallowing DataReceived(server, b'data on other stream') as handshake failed.", DEBUG, ) ) @@ -670,8 +665,10 @@ def test_mitmproxy_ca_is_untrusted(self, tctx: context.Context): playbook >> events.DataReceived(tctx.client, tssl_client.bio_read()) << commands.Log( - "Client TLS handshake failed. The client does not trust the proxy's certificate " - "for wrong.host.mitmproxy.org (sslv3 alert bad certificate)", + tutils.StrMatching( + "Client TLS handshake failed. The client does not trust the proxy's certificate " + "for wrong.host.mitmproxy.org" + ), WARNING, ) << tls.TlsFailedClientHook(tls_hook_data) @@ -770,17 +767,10 @@ def test_unsupported_protocol(self, tctx: context.Context): assert tls_hook_data().conn.error -def test_is_dtls_handshake_record(): - assert tls.is_dtls_handshake_record(bytes.fromhex("16fefd")) - assert not tls.is_dtls_handshake_record(bytes.fromhex("160300")) - assert not tls.is_dtls_handshake_record(bytes.fromhex("16fefe")) - assert not tls.is_dtls_handshake_record(bytes.fromhex("")) - assert not tls.is_dtls_handshake_record(bytes.fromhex("160304")) - assert not tls.is_dtls_handshake_record(bytes.fromhex("150301")) - - def test_dtls_record_contents(): - data = bytes.fromhex("16fefd00000000000000000002beef" "16fefd00000000000000000001ff") + data = bytes.fromhex( + "16fefd00000000000000000002beef" "16fefd00000000000000000001ff" + ) assert list(tls.dtls_handshake_record_contents(data)) == [b"\xbe\xef", b"\xff"] for i in range(12): assert list(tls.dtls_handshake_record_contents(data[:i])) == [] @@ -800,8 +790,8 @@ def test__dtls_record_contents_err(): "cc02bc02fc00ac014c02cc03001000000" ) dtls_client_hello_with_extensions = bytes.fromhex( - "16fefd00000000000000000085" # record layer - "010000790000000000000079" # hanshake layer + "16fefd00000000000000000085" # record layer + "010000790000000000000079" # hanshake layer "fefd62bf0e0bf809df43e7669197be831919878b1a72c07a584d3c0a8ca6665878010000000cc02bc02fc00ac014c02cc0" "3001000043000d0010000e0403050306030401050106010807ff01000100000a00080006001d00170018000b00020100001" "7000000000010000e00000b6578616d706c652e636f6d" @@ -809,26 +799,35 @@ def test__dtls_record_contents_err(): def test_dtls_get_client_hello(): - single_record = bytes.fromhex("16fefd00000000000000000042") + dtls_client_hello_no_extensions + single_record = ( + bytes.fromhex("16fefd00000000000000000042") + dtls_client_hello_no_extensions + ) assert tls.get_dtls_client_hello(single_record) == dtls_client_hello_no_extensions split_over_two_records = ( - bytes.fromhex("16fefd00000000000000000020") - + dtls_client_hello_no_extensions[:32] - + bytes.fromhex("16fefd00000000000000000022") - + dtls_client_hello_no_extensions[32:] + bytes.fromhex("16fefd00000000000000000020") + + dtls_client_hello_no_extensions[:32] + + bytes.fromhex("16fefd00000000000000000022") + + dtls_client_hello_no_extensions[32:] + ) + assert ( + tls.get_dtls_client_hello(split_over_two_records) + == dtls_client_hello_no_extensions ) - assert tls.get_dtls_client_hello(split_over_two_records) == dtls_client_hello_no_extensions incomplete = split_over_two_records[:42] assert tls.get_dtls_client_hello(incomplete) is None def test_dtls_parse_client_hello(): - assert tls.dtls_parse_client_hello(dtls_client_hello_with_extensions).sni == "example.com" + assert ( + tls.dtls_parse_client_hello(dtls_client_hello_with_extensions).sni + == "example.com" + ) assert tls.dtls_parse_client_hello(dtls_client_hello_with_extensions[:50]) is None with pytest.raises(ValueError): tls.dtls_parse_client_hello( # Server Name Length longer than actual Server Name - dtls_client_hello_with_extensions[:-16] + b"\x00\x0e\x00\x00\x20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + dtls_client_hello_with_extensions[:-16] + + b"\x00\x0e\x00\x00\x20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" ) diff --git a/test/mitmproxy/proxy/layers/test_tls_fuzz.py b/test/mitmproxy/proxy/layers/test_tls_fuzz.py index 402c08a5fc..e157409891 100644 --- a/test/mitmproxy/proxy/layers/test_tls_fuzz.py +++ b/test/mitmproxy/proxy/layers/test_tls_fuzz.py @@ -1,8 +1,10 @@ -from hypothesis import given, example -from hypothesis.strategies import binary, integers +from hypothesis import example +from hypothesis import given +from hypothesis.strategies import binary +from hypothesis.strategies import integers -from mitmproxy.tls import ClientHello from mitmproxy.proxy.layers.tls import parse_client_hello +from mitmproxy.tls import ClientHello client_hello_with_extensions = bytes.fromhex( "16030300bb" # record layer diff --git a/test/mitmproxy/proxy/layers/test_udp.py b/test/mitmproxy/proxy/layers/test_udp.py index 14b344a574..1253a510a0 100644 --- a/test/mitmproxy/proxy/layers/test_udp.py +++ b/test/mitmproxy/proxy/layers/test_udp.py @@ -1,11 +1,17 @@ import pytest -from mitmproxy.proxy.commands import CloseConnection, OpenConnection, SendData -from mitmproxy.proxy.events import ConnectionClosed, DataReceived +from ..tutils import Placeholder +from ..tutils import Playbook +from ..tutils import reply +from mitmproxy.proxy.commands import CloseConnection +from mitmproxy.proxy.commands import OpenConnection +from mitmproxy.proxy.commands import SendData +from mitmproxy.proxy.events import ConnectionClosed +from mitmproxy.proxy.events import DataReceived from mitmproxy.proxy.layers import udp from mitmproxy.proxy.layers.udp import UdpMessageInjected -from mitmproxy.udp import UDPFlow, UDPMessage -from ..tutils import Placeholder, Playbook, reply +from mitmproxy.udp import UDPFlow +from mitmproxy.udp import UDPMessage def test_open_connection(tctx): @@ -53,7 +59,6 @@ def test_simple(tctx): << SendData(tctx.client, b"hi") >> ConnectionClosed(tctx.server) << CloseConnection(tctx.client) - >> ConnectionClosed(tctx.client) << udp.UdpEndHook(f) >> reply() >> DataReceived(tctx.server, b"ignored") diff --git a/test/mitmproxy/proxy/layers/test_websocket.py b/test/mitmproxy/proxy/layers/test_websocket.py index a1d96133f4..0749b690a1 100644 --- a/test/mitmproxy/proxy/layers/test_websocket.py +++ b/test/mitmproxy/proxy/layers/test_websocket.py @@ -2,19 +2,27 @@ from dataclasses import dataclass import pytest - -import wsproto import wsproto.events -from mitmproxy.http import HTTPFlow, Request, Response -from mitmproxy.proxy.layers.http import HTTPMode -from mitmproxy.proxy.commands import SendData, CloseConnection, Log +from wsproto.frame_protocol import Opcode + from mitmproxy.connection import ConnectionState -from mitmproxy.proxy.events import DataReceived, ConnectionClosed -from mitmproxy.proxy.layers import http, websocket +from mitmproxy.http import HTTPFlow +from mitmproxy.http import Request +from mitmproxy.http import Response +from mitmproxy.proxy.commands import CloseConnection +from mitmproxy.proxy.commands import Log +from mitmproxy.proxy.commands import SendData +from mitmproxy.proxy.events import ConnectionClosed +from mitmproxy.proxy.events import DataReceived +from mitmproxy.proxy.layers import http +from mitmproxy.proxy.layers import websocket +from mitmproxy.proxy.layers.http import HTTPMode from mitmproxy.proxy.layers.websocket import WebSocketMessageInjected -from mitmproxy.websocket import WebSocketData, WebSocketMessage -from test.mitmproxy.proxy.tutils import Placeholder, Playbook, reply -from wsproto.frame_protocol import Opcode +from mitmproxy.websocket import WebSocketData +from mitmproxy.websocket import WebSocketMessage +from test.mitmproxy.proxy.tutils import Placeholder +from test.mitmproxy.proxy.tutils import Playbook +from test.mitmproxy.proxy.tutils import reply @dataclass @@ -122,7 +130,9 @@ def test_upgrade(tctx): def test_upgrade_streamed(tctx): - """If the HTTP response is streamed, we may get early data from the client.""" + """ + Test that streaming the response does not change behavior. + """ tctx.server.address = ("example.com", 80) tctx.server.state = ConnectionState.OPEN flow = Placeholder(HTTPFlow) @@ -161,6 +171,10 @@ def enable_streaming(flow: HTTPFlow): ) << http.HttpResponseHeadersHook(flow) >> reply(side_effect=enable_streaming) + # Current implementation: We know that body size for 101 responses must be zero, + # so we never trigger streaming logic in the first place. + << http.HttpResponseHook(flow) + >> reply() << SendData( tctx.client, b"HTTP/1.1 101 Switching Protocols\r\n" @@ -168,11 +182,9 @@ def enable_streaming(flow: HTTPFlow): b"Connection: Upgrade\r\n" b"\r\n", ) - << http.HttpResponseHook(flow) - >> DataReceived(tctx.client, masked_bytes(b"\x81\x0bhello world")) # early !! - >> reply(to=-2) << websocket.WebsocketStartHook(flow) - >> reply() + >> DataReceived(tctx.client, masked_bytes(b"\x81\x0bhello world")) # early data + >> reply(to=-2) << websocket.WebsocketMessageHook(flow) >> reply() << SendData(tctx.server, masked(b"\x81\x0bhello world")) @@ -402,9 +414,9 @@ def test_close_code(ws_testdata): def test_deflate(ws_testdata): tctx, playbook, flow = ws_testdata - flow.response.headers[ - "Sec-WebSocket-Extensions" - ] = "permessage-deflate; server_max_window_bits=10" + flow.response.headers["Sec-WebSocket-Extensions"] = ( + "permessage-deflate; server_max_window_bits=10" + ) assert ( playbook << websocket.WebsocketStartHook(flow) diff --git a/test/mitmproxy/proxy/test_commands.py b/test/mitmproxy/proxy/test_commands.py index c69591e32b..d84f770106 100644 --- a/test/mitmproxy/proxy/test_commands.py +++ b/test/mitmproxy/proxy/test_commands.py @@ -9,7 +9,7 @@ @pytest.fixture def tconn() -> connection.Server: - return connection.Server(None) + return connection.Server(address=None) def test_dataclasses(tconn): @@ -17,6 +17,7 @@ def test_dataclasses(tconn): assert repr(commands.SendData(tconn, b"foo")) assert repr(commands.OpenConnection(tconn)) assert repr(commands.CloseConnection(tconn)) + assert repr(commands.CloseTcpConnection(tconn, half_close=True)) assert repr(commands.Log("hello")) diff --git a/test/mitmproxy/proxy/test_context.py b/test/mitmproxy/proxy/test_context.py index 62e9c9b7a7..1ac2bd8332 100644 --- a/test/mitmproxy/proxy/test_context.py +++ b/test/mitmproxy/proxy/test_context.py @@ -1,5 +1,6 @@ from mitmproxy.proxy import context -from mitmproxy.test import tflow, taddons +from mitmproxy.test import taddons +from mitmproxy.test import tflow def test_context(): diff --git a/test/mitmproxy/proxy/test_events.py b/test/mitmproxy/proxy/test_events.py index 2061f27c8c..5e867369e9 100644 --- a/test/mitmproxy/proxy/test_events.py +++ b/test/mitmproxy/proxy/test_events.py @@ -3,12 +3,13 @@ import pytest from mitmproxy import connection -from mitmproxy.proxy import events, commands +from mitmproxy.proxy import commands +from mitmproxy.proxy import events @pytest.fixture def tconn() -> connection.Server: - return connection.Server(None) + return connection.Server(address=None) def test_dataclasses(tconn): diff --git a/test/mitmproxy/proxy/test_layer.py b/test/mitmproxy/proxy/test_layer.py index 59aebef6dd..307dd546dc 100644 --- a/test/mitmproxy/proxy/test_layer.py +++ b/test/mitmproxy/proxy/test_layer.py @@ -2,7 +2,9 @@ import pytest -from mitmproxy.proxy import commands, events, layer +from mitmproxy.proxy import commands +from mitmproxy.proxy import events +from mitmproxy.proxy import layer from mitmproxy.proxy.context import Context from test.mitmproxy.proxy import tutils @@ -51,8 +53,7 @@ def state_bar(self, event: events.Event) -> layer.CommandGenerator[None]: tutils.Playbook(tlayer, hooks=True, logs=True) << commands.Log(" >> Start({})", DEBUG) << commands.Log( - " << OpenConnection({'connection': Server({'id': '…rverid', 'address': None, " - "'state': , 'transport_protocol': 'tcp'})})", + " << OpenConnection({'connection': Server({'id': '…rverid', 'address': None})})", DEBUG, ) << commands.OpenConnection(tctx.server) @@ -60,9 +61,8 @@ def state_bar(self, event: events.Event) -> layer.CommandGenerator[None]: << commands.Log(" >! DataReceived(client, b'foo')", DEBUG) >> tutils.reply(None, to=-3) << commands.Log( - " >> Reply(OpenConnection({'connection': Server(" - "{'id': '…rverid', 'address': None, 'state': , " - "'transport_protocol': 'tcp', 'timestamp_start': 1624544785})}), None)", + " >> Reply(OpenConnection({'connection': Server({'id': '…rverid', 'address': None, " + "'state': , 'timestamp_start': 1624544785})}), None)", DEBUG, ) << commands.Log(" !> DataReceived(client, b'foo')", DEBUG) @@ -73,8 +73,8 @@ def state_bar(self, event: events.Event) -> layer.CommandGenerator[None]: def test_debug_shorten(self, tctx): t = layer.Layer(tctx) t.debug = " " - assert t._Layer__debug("x" * 600).message == " " + "x" * 512 + "…" - assert t._Layer__debug("x" * 600).message == " " + "x" * 256 + "…" + assert t._Layer__debug("x" * 4096).message == " " + "x" * 2048 + "…" + assert t._Layer__debug("x" * 4096).message == " " + "x" * 256 + "…" assert t._Layer__debug("foo").message == " foo" diff --git a/test/mitmproxy/proxy/test_mode_servers.py b/test/mitmproxy/proxy/test_mode_servers.py index c1228b3caa..228363d7fd 100644 --- a/test/mitmproxy/proxy/test_mode_servers.py +++ b/test/mitmproxy/proxy/test_mode_servers.py @@ -1,14 +1,21 @@ import asyncio import platform -from typing import cast -from unittest.mock import AsyncMock, MagicMock, Mock +import socket +from unittest.mock import AsyncMock +from unittest.mock import MagicMock +from unittest.mock import Mock import pytest +from ...conftest import no_ipv6 +from ...conftest import skip_not_linux import mitmproxy.platform +import mitmproxy_rs from mitmproxy.addons.proxyserver import Proxyserver -from mitmproxy.net import udp -from mitmproxy.proxy.mode_servers import DnsInstance, ServerInstance, WireGuardServerInstance +from mitmproxy.proxy.mode_servers import LocalRedirectorInstance +from mitmproxy.proxy.mode_servers import ServerInstance +from mitmproxy.proxy.mode_servers import TunInstance +from mitmproxy.proxy.mode_servers import WireGuardServerInstance from mitmproxy.proxy.server import ConnectionHandler from mitmproxy.test import taddons @@ -18,14 +25,23 @@ def test_make(): context = MagicMock() assert ServerInstance.make("regular", manager) - for mode in ["regular", "upstream:example.com", "transparent", "reverse:example.com", "socks5"]: + for mode in [ + "regular", + # "http3", + "upstream:example.com", + "transparent", + "reverse:example.com", + "socks5", + ]: inst = ServerInstance.make(mode, manager) assert inst assert inst.make_top_layer(context) assert inst.mode.description assert inst.to_json() - with pytest.raises(ValueError, match="is not a spec for a WireGuardServerInstance server."): + with pytest.raises( + ValueError, match="is not a spec for a WireGuardServerInstance server." + ): WireGuardServerInstance.make("regular", manager) @@ -33,7 +49,11 @@ async def test_last_exception_and_running(monkeypatch): manager = MagicMock() err = ValueError("something else") - async def _raise(*_): + def _raise(*_): + nonlocal err + raise err + + async def _raise_async(*_): nonlocal err raise err @@ -42,12 +62,12 @@ async def _raise(*_): await inst1.start() assert inst1.last_exception is None assert inst1.is_running - monkeypatch.setattr(inst1._server, "wait_closed", _raise) + monkeypatch.setattr(inst1._servers[0], "close", _raise) with pytest.raises(type(err), match=str(err)): await inst1.stop() assert inst1.last_exception is err - monkeypatch.setattr(asyncio, "start_server", _raise) + monkeypatch.setattr(asyncio, "start_server", _raise_async) inst2 = ServerInstance.make("regular@127.0.0.1:0", manager) assert inst2.last_exception is None with pytest.raises(type(err), match=str(err)): @@ -75,7 +95,7 @@ async def test_tcp_start_stop(caplog_async): assert await caplog_async.await_log("client disconnect") await inst.stop() - assert await caplog_async.await_log("Stopped HTTP(S) proxy") + assert await caplog_async.await_log("stopped") @pytest.mark.parametrize("failure", [True, False]) @@ -86,13 +106,15 @@ async def test_transparent(failure, monkeypatch, caplog_async): if failure: monkeypatch.setattr(mitmproxy.platform, "original_addr", None) else: - monkeypatch.setattr(mitmproxy.platform, "original_addr", lambda s: ("address", 42)) + monkeypatch.setattr( + mitmproxy.platform, "original_addr", lambda s: ("address", 42) + ) with taddons.context(Proxyserver()) as tctx: tctx.options.connection_strategy = "lazy" inst = ServerInstance.make("transparent@127.0.0.1:0", manager) await inst.start() - await caplog_async.await_log("proxy listening") + await caplog_async.await_log("listening") host, port, *_ = inst.listen_addrs[0] reader, writer = await asyncio.open_connection(host, port) @@ -108,20 +130,21 @@ async def test_transparent(failure, monkeypatch, caplog_async): assert await caplog_async.await_log("client disconnect") await inst.stop() - assert await caplog_async.await_log("Stopped transparent proxy") + assert await caplog_async.await_log("stopped") + + +async def _echo_server(self: ConnectionHandler): + t = self.transports[self.client] + data = await t.reader.read(65535) + t.writer.write(data.upper()) + await t.writer.drain() + t.writer.close() async def test_wireguard(tdata, monkeypatch, caplog): caplog.set_level("DEBUG") - async def handle_client(self: ConnectionHandler): - t = self.transports[self.client] - data = await t.reader.read(65535) - t.writer.write(data.upper()) - await t.writer.drain() - t.writer.close() - - monkeypatch.setattr(ConnectionHandler, "handle_client", handle_client) + monkeypatch.setattr(ConnectionHandler, "handle_client", _echo_server) system = platform.system() if system == "Linux": @@ -133,6 +156,10 @@ async def handle_client(self: ConnectionHandler): else: return pytest.skip("Unsupported platform for wg-test-client.") + arch = platform.machine() + if arch != "AMD64" and arch != "x86_64": + return pytest.skip("Unsupported architecture for wg-test-client.") + test_client_path = tdata.path(f"wg-test-client/{test_client_name}") test_conf = tdata.path(f"wg-test-client/test.conf") @@ -161,7 +188,7 @@ async def handle_client(self: ConnectionHandler): raise await inst.stop() - assert "Stopped WireGuard server" in caplog.text + assert "stopped" in caplog.text async def test_wireguard_generate_conf(tmp_path): @@ -190,7 +217,7 @@ async def test_wireguard_invalid_conf(tmp_path): # directory instead of filename inst = WireGuardServerInstance.make(f"wireguard:{tmp_path}", MagicMock()) - with pytest.raises(OSError): + with pytest.raises(ValueError, match="Invalid configuration file"): await inst.start() assert "Invalid configuration file" in repr(inst.last_exception) @@ -199,12 +226,16 @@ async def test_wireguard_invalid_conf(tmp_path): async def test_tcp_start_error(): manager = MagicMock() - server = await asyncio.start_server(MagicMock(), host="127.0.0.1", port=0, reuse_address=False) + server = await asyncio.start_server( + MagicMock(), host="127.0.0.1", port=0, reuse_address=False + ) port = server.sockets[0].getsockname()[1] with taddons.context() as tctx: inst = ServerInstance.make(f"regular@127.0.0.1:{port}", manager) - with pytest.raises(OSError, match=f"proxy failed to listen on 127\\.0\\.0\\.1:{port}"): + with pytest.raises( + OSError, match=f"proxy failed to listen on 127\\.0\\.0\\.1:{port}" + ): await inst.start() tctx.options.listen_host = "127.0.0.1" tctx.options.listen_port = port @@ -234,41 +265,219 @@ async def test_udp_start_stop(caplog_async): assert await caplog_async.await_log("server listening") host, port, *_ = inst.listen_addrs[0] - reader, writer = await udp.open_connection(host, port) + stream = await mitmproxy_rs.udp.open_udp_connection(host, port) - writer.write(b"\x00\x00\x01") + stream.write(b"\x00\x00\x01") assert await caplog_async.await_log("sent an invalid message") - writer.close() + stream.close() + await stream.wait_closed() await inst.stop() - assert await caplog_async.await_log("Stopped") + assert await caplog_async.await_log("stopped") async def test_udp_start_error(): manager = MagicMock() with taddons.context(): - inst = ServerInstance.make("dns@127.0.0.1:0", manager) + inst = ServerInstance.make("reverse:udp://127.0.0.1:1234@127.0.0.1:0", manager) await inst.start() port = inst.listen_addrs[0][1] - inst2 = ServerInstance.make(f"dns@127.0.0.1:{port}", manager) - with pytest.raises(OSError, match=f"server failed to listen on 127\\.0\\.0\\.1:{port}"): + inst2 = ServerInstance.make( + f"reverse:udp://127.0.0.1:1234@127.0.0.1:{port}", manager + ) + with pytest.raises( + Exception, match=f"Failed to bind UDP socket to 127.0.0.1:{port}" + ): await inst2.start() await inst.stop() -async def test_udp_connection_reuse(monkeypatch): +@pytest.mark.parametrize("ip_version", ["v4", "v6"]) +@pytest.mark.parametrize("protocol", ["tcp", "udp"]) +async def test_dual_stack(ip_version, protocol, caplog_async): + """Test that a server bound to "" binds on both IPv4 and IPv6 for both TCP and UDP.""" + + if ip_version == "v6" and no_ipv6: + pytest.skip("Skipped because IPv6 is unavailable.") + + if ip_version == "v4": + addr = "127.0.0.1" + else: + addr = "::1" + + caplog_async.set_level("DEBUG") manager = MagicMock() manager.connections = {} - monkeypatch.setattr(udp, "DatagramWriter", MagicMock()) - monkeypatch.setattr(DnsInstance, "handle_udp_connection", AsyncMock()) + with taddons.context(): + inst = ServerInstance.make("dns@0", manager) + await inst.start() + assert await caplog_async.await_log("server listening") + _, port, *_ = inst.listen_addrs[0] + + if protocol == "tcp": + _, stream = await asyncio.open_connection(addr, port) + else: + stream = await mitmproxy_rs.udp.open_udp_connection(addr, port) + stream.write(b"\x00\x00\x01") + assert await caplog_async.await_log("sent an invalid message") + stream.close() + await stream.wait_closed() + + await inst.stop() + assert await caplog_async.await_log("stopped") + + +@pytest.mark.parametrize("transport_protocol", ["udp", "tcp"]) +async def test_dns_start_stop(caplog_async, transport_protocol): + caplog_async.set_level("INFO") + manager = MagicMock() + manager.connections = {} + + with taddons.context(): + inst = ServerInstance.make("dns@127.0.0.1:0", manager) + await inst.start() + assert await caplog_async.await_log("server listening") + + host, port, *_ = inst.listen_addrs[0] + if transport_protocol == "tcp": + _, stream = await asyncio.open_connection("127.0.0.1", port) + elif transport_protocol == "udp": + stream = await mitmproxy_rs.udp.open_udp_connection("127.0.0.1", port) + + stream.write(b"\x00\x00\x01") + assert await caplog_async.await_log("sent an invalid message") + + stream.close() + await stream.wait_closed() + + await inst.stop() + assert await caplog_async.await_log("stopped") + + +@skip_not_linux +async def test_tun_mode(monkeypatch, caplog): + monkeypatch.setattr(ConnectionHandler, "handle_client", _echo_server) + + with taddons.context(Proxyserver()): + inst = TunInstance.make(f"tun", MagicMock()) + assert inst.tun_name is None + try: + await inst.start() + except RuntimeError as e: + if "Operation not permitted" in str(e): + return pytest.skip("tun mode test must be run as root") + raise + assert inst.tun_name + assert inst.is_running + assert "tun_name" in inst.to_json() + + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, inst.tun_name.encode()) + await asyncio.get_running_loop().sock_connect(s, ("192.0.2.1", 1234)) + reader, writer = await asyncio.open_connection(sock=s) + writer.write(b"hello") + await writer.drain() + assert await reader.readexactly(5) == b"HELLO" + writer.close() + await writer.wait_closed() + await inst.stop() + + +async def test_tun_mode_mocked(monkeypatch): + tun_interface = Mock() + tun_interface.tun_name = lambda: "tun0" + tun_interface.wait_closed = AsyncMock() + create_tun_interface = AsyncMock(return_value=tun_interface) + monkeypatch.setattr(mitmproxy_rs.tun, "create_tun_interface", create_tun_interface) + + inst = TunInstance.make(f"tun", MagicMock()) + assert not inst.is_running + assert inst.tun_name is None + + await inst.start() + assert inst.is_running + assert inst.tun_name == "tun0" + assert inst.to_json()["tun_name"] == "tun0" + + await inst.stop() + assert not inst.is_running + assert inst.tun_name is None + + +@pytest.fixture() +def patched_local_redirector(monkeypatch): + start_local_redirector = AsyncMock(return_value=Mock()) + monkeypatch.setattr( + mitmproxy_rs.local, "start_local_redirector", start_local_redirector + ) + # make sure _server and _instance are restored after this test + monkeypatch.setattr(LocalRedirectorInstance, "_server", None) + monkeypatch.setattr(LocalRedirectorInstance, "_instance", None) + return start_local_redirector + + +async def test_local_redirector(patched_local_redirector, caplog_async): + caplog_async.set_level("INFO") with taddons.context(): - inst = cast(DnsInstance, ServerInstance.make("dns", manager)) - inst.handle_udp_datagram(MagicMock(), b"\x00\x00\x01", ("remoteaddr", 0), ("localaddr", 0)) - inst.handle_udp_datagram(MagicMock(), b"\x00\x00\x02", ("remoteaddr", 0), ("localaddr", 0)) - await asyncio.sleep(0) + inst = ServerInstance.make(f"local", MagicMock()) + assert not inst.is_running + + await inst.start() + assert patched_local_redirector.called + assert await caplog_async.await_log("Local redirector started.") + assert inst.is_running + + await inst.stop() + assert await caplog_async.await_log("Local redirector stopped") + assert not inst.is_running + + # just called for coverage + inst.make_top_layer(MagicMock()) + + +async def test_local_redirector_startup_err(patched_local_redirector): + patched_local_redirector.side_effect = RuntimeError( + "Local redirector startup error" + ) + + with taddons.context(): + inst = ServerInstance.make(f"local:!curl", MagicMock()) + with pytest.raises(RuntimeError): + await inst.start() + assert not inst.is_running + + +async def test_multiple_local_redirectors(patched_local_redirector): + manager = MagicMock() + + with taddons.context(): + inst1 = ServerInstance.make(f"local:curl", manager) + await inst1.start() + + inst2 = ServerInstance.make(f"local:wget", manager) + with pytest.raises( + RuntimeError, match="Cannot spawn more than one local redirector" + ): + await inst2.start() + + +async def test_always_uses_current_instance(patched_local_redirector, monkeypatch): + manager = MagicMock() + + with taddons.context(): + inst1 = LocalRedirectorInstance.make(f"local:curl", manager) + await inst1.start() + await inst1.stop() + + handle_stream, _ = patched_local_redirector.await_args[0] + + inst2 = LocalRedirectorInstance.make(f"local:wget", manager) + await inst2.start() - assert len(inst.manager.connections) == 1 + monkeypatch.setattr(inst2, "handle_stream", handler := AsyncMock()) + await handle_stream(Mock()) + assert handler.await_count diff --git a/test/mitmproxy/proxy/test_mode_specs.py b/test/mitmproxy/proxy/test_mode_specs.py index e2e7eecf93..50b19cfa22 100644 --- a/test/mitmproxy/proxy/test_mode_specs.py +++ b/test/mitmproxy/proxy/test_mode_specs.py @@ -1,6 +1,9 @@ +import dataclasses + import pytest -from mitmproxy.proxy.mode_specs import ProxyMode, Socks5Mode +from mitmproxy.proxy.mode_specs import ProxyMode +from mitmproxy.proxy.mode_specs import Socks5Mode def test_parse(): @@ -24,7 +27,7 @@ def test_parse(): ProxyMode.parse("regular@99999") m.set_state(m.get_state()) - with pytest.raises(RuntimeError, match="Proxy modes are frozen"): + with pytest.raises(dataclasses.FrozenInstanceError): m.set_state("regular") @@ -39,11 +42,15 @@ def test_listen_addr(): assert ProxyMode.parse("regular@1234").listen_port() == 1234 assert ProxyMode.parse("regular").listen_port(default=4424) == 4424 assert ProxyMode.parse("regular@1234").listen_port(default=4424) == 1234 + assert ProxyMode.parse("local").listen_port() is None assert ProxyMode.parse("regular").listen_host() == "" assert ProxyMode.parse("regular@127.0.0.2:8080").listen_host() == "127.0.0.2" assert ProxyMode.parse("regular").listen_host(default="127.0.0.3") == "127.0.0.3" - assert ProxyMode.parse("regular@127.0.0.2:8080").listen_host(default="127.0.0.3") == "127.0.0.2" + assert ( + ProxyMode.parse("regular@127.0.0.2:8080").listen_host(default="127.0.0.3") + == "127.0.0.2" + ) assert ProxyMode.parse("reverse:https://1.2.3.4").listen_port() == 8080 assert ProxyMode.parse("reverse:dns://8.8.8.8").listen_port() == 53 @@ -51,9 +58,11 @@ def test_listen_addr(): def test_parse_specific_modes(): assert ProxyMode.parse("regular") + # assert ProxyMode.parse("http3") assert ProxyMode.parse("transparent") assert ProxyMode.parse("upstream:https://proxy") assert ProxyMode.parse("reverse:https://host@443") + assert ProxyMode.parse("reverse:http3://host@443") assert ProxyMode.parse("socks5") assert ProxyMode.parse("dns") assert ProxyMode.parse("reverse:dns://8.8.8.8") @@ -61,6 +70,10 @@ def test_parse_specific_modes(): assert ProxyMode.parse("wireguard") assert ProxyMode.parse("wireguard:foo.conf").data == "foo.conf" assert ProxyMode.parse("wireguard@51821").listen_port() == 51821 + assert ProxyMode.parse("tun") + assert ProxyMode.parse("tun:utun42") + + assert ProxyMode.parse("local") with pytest.raises(ValueError, match="invalid port"): ProxyMode.parse("regular@invalid-port") @@ -68,6 +81,9 @@ def test_parse_specific_modes(): with pytest.raises(ValueError, match="takes no arguments"): ProxyMode.parse("regular:configuration") + # with pytest.raises(ValueError, match="takes no arguments"): + # ProxyMode.parse("http3:configuration") + with pytest.raises(ValueError, match="invalid upstream proxy scheme"): ProxyMode.parse("upstream:dns://example.com") @@ -76,3 +92,6 @@ def test_parse_specific_modes(): with pytest.raises(ValueError, match="Port specification missing."): ProxyMode.parse("reverse:dtls://127.0.0.1") + + with pytest.raises(ValueError, match="invalid intercept spec"): + ProxyMode.parse("local:,,,") diff --git a/test/mitmproxy/proxy/test_server.py b/test/mitmproxy/proxy/test_server.py index e69de29bb2..40448eecc0 100644 --- a/test/mitmproxy/proxy/test_server.py +++ b/test/mitmproxy/proxy/test_server.py @@ -0,0 +1,106 @@ +import asyncio +import collections +import textwrap +from dataclasses import dataclass +from typing import Callable +from unittest import mock + +import pytest + +from mitmproxy import options +from mitmproxy.connection import Server +from mitmproxy.proxy import commands +from mitmproxy.proxy import layer +from mitmproxy.proxy import server +from mitmproxy.proxy import server_hooks +from mitmproxy.proxy.events import Event +from mitmproxy.proxy.events import HookCompleted +from mitmproxy.proxy.events import Start +from mitmproxy.proxy.mode_specs import ProxyMode + + +class MockConnectionHandler(server.SimpleConnectionHandler): + hook_handlers: dict[str, mock.Mock | Callable] + + def __init__(self): + super().__init__( + reader=mock.Mock(), + writer=mock.Mock(), + options=options.Options(), + mode=ProxyMode.parse("regular"), + hook_handlers=collections.defaultdict(lambda: mock.Mock()), + ) + + +@pytest.mark.parametrize("result", ("success", "killed", "failed")) +async def test_open_connection(result, monkeypatch): + handler = MockConnectionHandler() + server_connect = handler.hook_handlers["server_connect"] + server_connected = handler.hook_handlers["server_connected"] + server_connect_error = handler.hook_handlers["server_connect_error"] + server_disconnected = handler.hook_handlers["server_disconnected"] + + match result: + case "success": + monkeypatch.setattr( + asyncio, + "open_connection", + mock.AsyncMock(return_value=(mock.MagicMock(), mock.MagicMock())), + ) + monkeypatch.setattr( + MockConnectionHandler, "handle_connection", mock.AsyncMock() + ) + case "failed": + monkeypatch.setattr( + asyncio, "open_connection", mock.AsyncMock(side_effect=OSError) + ) + case "killed": + + def _kill(d: server_hooks.ServerConnectionHookData) -> None: + d.server.error = "do not connect" + + server_connect.side_effect = _kill + + await handler.open_connection( + commands.OpenConnection(connection=Server(address=("server", 1234))) + ) + + assert server_connect.call_args[0][0].server.address == ("server", 1234) + + assert server_connected.called == (result == "success") + assert server_connect_error.called == (result != "success") + + assert server_disconnected.called == (result == "success") + + +async def test_no_reentrancy(capsys): + class ReentrancyTestLayer(layer.Layer): + def handle_event(self, event: Event) -> layer.CommandGenerator[None]: + if isinstance(event, Start): + print("Starting...") + yield FastHook() + print("Start completed.") + elif isinstance(event, HookCompleted): + print(f"Hook completed (must not happen before start is completed).") + + def _handle_event(self, event: Event) -> layer.CommandGenerator[None]: + raise NotImplementedError + + @dataclass + class FastHook(commands.StartHook): + pass + + handler = MockConnectionHandler() + handler.layer = ReentrancyTestLayer(handler.layer.context) + + # This instead would fail: handler._server_event(Start()) + await handler.server_event(Start()) + await asyncio.sleep(0) + + assert capsys.readouterr().out == textwrap.dedent( + """\ + Starting... + Start completed. + Hook completed (must not happen before start is completed). + """ + ) diff --git a/test/mitmproxy/proxy/test_tunnel.py b/test/mitmproxy/proxy/test_tunnel.py index 24103637c6..8f5ddf4f6e 100644 --- a/test/mitmproxy/proxy/test_tunnel.py +++ b/test/mitmproxy/proxy/test_tunnel.py @@ -1,17 +1,25 @@ -from typing import Optional - import pytest -from mitmproxy.proxy import tunnel, layer -from mitmproxy.proxy.commands import SendData, Log, CloseConnection, OpenConnection -from mitmproxy.connection import Server, ConnectionState +from mitmproxy.connection import ConnectionState +from mitmproxy.connection import Server +from mitmproxy.proxy import layer +from mitmproxy.proxy import tunnel +from mitmproxy.proxy.commands import CloseConnection +from mitmproxy.proxy.commands import CloseTcpConnection +from mitmproxy.proxy.commands import Log +from mitmproxy.proxy.commands import OpenConnection +from mitmproxy.proxy.commands import SendData from mitmproxy.proxy.context import Context -from mitmproxy.proxy.events import Event, DataReceived, Start, ConnectionClosed -from test.mitmproxy.proxy.tutils import Playbook, reply +from mitmproxy.proxy.events import ConnectionClosed +from mitmproxy.proxy.events import DataReceived +from mitmproxy.proxy.events import Event +from mitmproxy.proxy.events import Start +from test.mitmproxy.proxy.tutils import Playbook +from test.mitmproxy.proxy.tutils import reply class TChildLayer(layer.Layer): - child_layer: Optional[layer.Layer] = None + child_layer: layer.Layer | None = None def _handle_event(self, event: Event) -> layer.CommandGenerator[None]: if isinstance(event, Start): @@ -24,7 +32,7 @@ def _handle_event(self, event: Event) -> layer.CommandGenerator[None]: err = yield OpenConnection(self.context.server) yield Log(f"Opened: {err=}. Server state: {self.context.server.state.name}") elif isinstance(event, DataReceived) and event.data == b"half-close": - err = yield CloseConnection(event.connection, half_close=True) + err = yield CloseTcpConnection(event.connection, half_close=True) elif isinstance(event, ConnectionClosed): yield Log(f"Got {event.connection.__class__.__name__.lower()} close.") yield CloseConnection(event.connection) @@ -38,7 +46,7 @@ def start_handshake(self) -> layer.CommandGenerator[None]: def receive_handshake_data( self, data: bytes - ) -> layer.CommandGenerator[tuple[bool, Optional[str]]]: + ) -> layer.CommandGenerator[tuple[bool, str | None]]: yield SendData(self.tunnel_connection, data) if data == b"handshake-success": return True, None @@ -56,7 +64,7 @@ def receive_data(self, data: bytes) -> layer.CommandGenerator[None]: @pytest.mark.parametrize("success", ["success", "fail"]) def test_tunnel_handshake_start(tctx: Context, success): - server = Server(("proxy", 1234)) + server = Server(address=("proxy", 1234)) server.state = ConnectionState.OPEN tl = TTunnelLayer(tctx, server, tctx.server) @@ -87,7 +95,7 @@ def test_tunnel_handshake_start(tctx: Context, success): @pytest.mark.parametrize("success", ["success", "fail"]) def test_tunnel_handshake_command(tctx: Context, success): - server = Server(("proxy", 1234)) + server = Server(address=("proxy", 1234)) tl = TTunnelLayer(tctx, server, tctx.server) tl.child_layer = TChildLayer(tctx) @@ -137,7 +145,7 @@ def test_tunnel_default_impls(tctx: Context): Some tunnels don't need certain features, so the default behaviour should be to be transparent. """ - server = Server(None) + server = Server(address=None) server.state = ConnectionState.OPEN tl = tunnel.TunnelLayer(tctx, server, tctx.server) tl.child_layer = TChildLayer(tctx) @@ -164,12 +172,12 @@ def test_tunnel_default_impls(tctx: Context): >> reply(None) << Log("Opened: err=None. Server state: OPEN") >> DataReceived(server, b"half-close") - << CloseConnection(server, half_close=True) + << CloseTcpConnection(server, half_close=True) ) def test_tunnel_openconnection_error(tctx: Context): - server = Server(("proxy", 1234)) + server = Server(address=("proxy", 1234)) tl = TTunnelLayer(tctx, server, tctx.server) tl.child_layer = TChildLayer(tctx) @@ -192,7 +200,7 @@ def test_tunnel_openconnection_error(tctx: Context): @pytest.mark.parametrize("disconnect", ["client", "server"]) def test_disconnect_during_handshake_start(tctx: Context, disconnect): - server = Server(("proxy", 1234)) + server = Server(address=("proxy", 1234)) server.state = ConnectionState.OPEN tl = TTunnelLayer(tctx, server, tctx.server) @@ -224,7 +232,7 @@ def test_disconnect_during_handshake_start(tctx: Context, disconnect): @pytest.mark.parametrize("disconnect", ["client", "server"]) def test_disconnect_during_handshake_command(tctx: Context, disconnect): - server = Server(("proxy", 1234)) + server = Server(address=("proxy", 1234)) tl = TTunnelLayer(tctx, server, tctx.server) tl.child_layer = TChildLayer(tctx) diff --git a/test/mitmproxy/proxy/test_tutils.py b/test/mitmproxy/proxy/test_tutils.py index 04880990e4..735b31dda5 100644 --- a/test/mitmproxy/proxy/test_tutils.py +++ b/test/mitmproxy/proxy/test_tutils.py @@ -4,8 +4,10 @@ import pytest -from mitmproxy.proxy import commands, events, layer from . import tutils +from mitmproxy.proxy import commands +from mitmproxy.proxy import events +from mitmproxy.proxy import layer class TEvent(events.Event): @@ -112,6 +114,12 @@ def test_command_reply(tplaybook): assert tplaybook assert tplaybook.actual[1] == tplaybook.actual[2].command + tplaybook >> TEvent((42,)) + tplaybook << TCommand(42) + tplaybook >> tutils.reply(to=TCommand) + assert tplaybook + assert tplaybook.actual[4] == tplaybook.actual[5].command + def test_default_playbook(tctx): p = tutils.Playbook(TLayer(tctx)) diff --git a/test/mitmproxy/proxy/test_utils.py b/test/mitmproxy/proxy/test_utils.py index b3504271d5..f4cc274cf4 100644 --- a/test/mitmproxy/proxy/test_utils.py +++ b/test/mitmproxy/proxy/test_utils.py @@ -1,6 +1,7 @@ import pytest from mitmproxy.proxy.utils import expect +from mitmproxy.proxy.utils import ReceiveBuffer def test_expect(): @@ -19,3 +20,25 @@ def bar(self, x): assert list(f.bar("bar")) == ["rab"] with pytest.raises(AssertionError, match=r"Expected str\|int, got None."): f.foo(None) + + +def test_receive_buffer(): + buf = ReceiveBuffer() + assert len(buf) == 0 + assert bytes(buf) == b"" + assert not buf + + buf += b"foo" + assert len(buf) == 3 + assert bytes(buf) == b"foo" + assert buf + + buf += b"bar" + assert len(buf) == 6 + assert bytes(buf) == b"foobar" + assert buf + + buf.clear() + assert len(buf) == 0 + assert bytes(buf) == b"" + assert not buf diff --git a/test/mitmproxy/proxy/tutils.py b/test/mitmproxy/proxy/tutils.py index 61b7489b1a..5f3c74b93b 100644 --- a/test/mitmproxy/proxy/tutils.py +++ b/test/mitmproxy/proxy/tutils.py @@ -1,17 +1,23 @@ import collections.abc import difflib -import logging - import itertools +import logging import re import textwrap import traceback -from collections.abc import Callable, Iterable -from typing import Any, AnyStr, Generic, Optional, TypeVar, Union +from collections.abc import Callable +from collections.abc import Iterable +from typing import Any +from typing import AnyStr +from typing import Generic +from typing import TypeVar +from typing import Union -from mitmproxy.proxy import commands, context, layer -from mitmproxy.proxy import events from mitmproxy.connection import ConnectionState +from mitmproxy.proxy import commands +from mitmproxy.proxy import context +from mitmproxy.proxy import events +from mitmproxy.proxy import layer from mitmproxy.proxy.events import command_reply_subclasses from mitmproxy.proxy.layer import Layer @@ -21,7 +27,7 @@ def _eq(a: PlaybookEntry, b: PlaybookEntry) -> bool: """Compare two commands/events, and possibly update placeholders.""" - if type(a) != type(b): + if type(a) is not type(b): return False a_dict = a.__dict__ @@ -50,8 +56,8 @@ def _eq(a: PlaybookEntry, b: PlaybookEntry) -> bool: def eq( - a: Union[PlaybookEntry, Iterable[PlaybookEntry]], - b: Union[PlaybookEntry, Iterable[PlaybookEntry]], + a: PlaybookEntry | Iterable[PlaybookEntry], + b: PlaybookEntry | Iterable[PlaybookEntry], ): """ Compare an indiviual event/command or a list of events/commands. @@ -139,7 +145,7 @@ def __init__( layer: Layer, hooks: bool = True, logs: bool = False, - expected: Optional[PlaybookEntryList] = None, + expected: PlaybookEntryList | None = None, ): if expected is None: expected = [events.Start()] @@ -153,6 +159,10 @@ def __init__( def __rshift__(self, e): """Add an event to send""" + if isinstance(e, collections.abc.Iterable): + for ev in e: + self.__rshift__(ev) + return self assert isinstance(e, events.Event) self.expected.append(e) return self @@ -161,6 +171,10 @@ def __lshift__(self, c): """Add an expected command""" if c is None: return self + if isinstance(c, collections.abc.Iterable): + for cmd in c: + self.__lshift__(cmd) + return self assert isinstance(c, commands.Command) prev = self.expected[-1] @@ -224,11 +238,10 @@ def __bool__(self): for cmd in cmds: pos += 1 assert self.actual[pos] == cmd - if isinstance(cmd, commands.CloseConnection): - if cmd.half_close: - cmd.connection.state &= ~ConnectionState.CAN_WRITE - else: - cmd.connection.state = ConnectionState.CLOSED + if isinstance(cmd, commands.CloseTcpConnection) and cmd.half_close: + cmd.connection.state &= ~ConnectionState.CAN_WRITE + elif isinstance(cmd, commands.CloseConnection): + cmd.connection.state = ConnectionState.CLOSED elif isinstance(cmd, commands.Log): need_to_emulate_log = ( not self.logs @@ -293,13 +306,13 @@ def __del__(self): class reply(events.Event): args: tuple[Any, ...] - to: Union[commands.Command, int] + to: commands.Command | type[commands.Command] | int side_effect: Callable[[Any], Any] def __init__( self, *args, - to: Union[commands.Command, int] = -1, + to: commands.Command | type[commands.Command] | int = -1, side_effect: Callable[[Any], None] = lambda x: None, ): """Utility method to reply to the latest hook in playbooks.""" @@ -317,6 +330,14 @@ def playbook_eval(self, playbook: Playbook) -> events.CommandCompleted: raise AssertionError(f"There is no command at offset {self.to}: {to}") else: self.to = to + elif isinstance(self.to, type): + for cmd in reversed(playbook.actual): + if isinstance(cmd, self.to): + assert isinstance(cmd, commands.Command) + self.to = cmd + break + else: + raise AssertionError(f"There is no command of type {self.to}.") for cmd in reversed(playbook.actual): if eq(self.to, cmd): self.to = cmd @@ -381,7 +402,7 @@ def __str__(self): # noinspection PyPep8Naming -def Placeholder(cls: type[T] = Any) -> Union[T, _Placeholder[T]]: +def Placeholder(cls: type[T] = Any) -> T | _Placeholder[T]: return _Placeholder(cls) @@ -399,12 +420,12 @@ def setdefault(self, value: AnyStr) -> AnyStr: # noinspection PyPep8Naming -def BytesMatching(match: bytes) -> Union[bytes, _AnyStrPlaceholder[bytes]]: +def BytesMatching(match: bytes) -> bytes | _AnyStrPlaceholder[bytes]: return _AnyStrPlaceholder(match) # noinspection PyPep8Naming -def StrMatching(match: str) -> Union[str, _AnyStrPlaceholder[str]]: +def StrMatching(match: str) -> str | _AnyStrPlaceholder[str]: return _AnyStrPlaceholder(match) @@ -433,7 +454,7 @@ def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: def reply_next_layer( - child_layer: Union[type[Layer], Callable[[context.Context], Layer]], *args, **kwargs + child_layer: type[Layer] | Callable[[context.Context], Layer], *args, **kwargs ) -> reply: """Helper function to simplify the syntax for next_layer events to this: << NextLayerHook(nl) diff --git a/test/mitmproxy/script/test_concurrent.py b/test/mitmproxy/script/test_concurrent.py index 4bc5f15826..446fbb8d96 100644 --- a/test/mitmproxy/script/test_concurrent.py +++ b/test/mitmproxy/script/test_concurrent.py @@ -4,8 +4,8 @@ import pytest -from mitmproxy.test import tflow from mitmproxy.test import taddons +from mitmproxy.test import tflow class TestConcurrent: diff --git a/test/mitmproxy/test_addonmanager.py b/test/mitmproxy/test_addonmanager.py index 77057d11b5..c76187ffa6 100644 --- a/test/mitmproxy/test_addonmanager.py +++ b/test/mitmproxy/test_addonmanager.py @@ -7,7 +7,9 @@ from mitmproxy import hooks from mitmproxy import master from mitmproxy import options -from mitmproxy.proxy.layers.http import HttpRequestHook, HttpResponseHook +from mitmproxy.addonmanager import Loader +from mitmproxy.proxy.layers.http import HttpRequestHook +from mitmproxy.proxy.layers.http import HttpResponseHook from mitmproxy.test import taddons from mitmproxy.test import tflow @@ -53,8 +55,8 @@ async def running(self): class AOption: - def load(self, l): - l.add_option("custom_option", bool, False, "help") + def load(self, loader: Loader): + loader.add_option("custom_option", bool, False, "help") class AOldAPI: @@ -154,22 +156,22 @@ async def test_mixed_async_sync(caplog): async def test_loader(caplog): with taddons.context() as tctx: - l = addonmanager.Loader(tctx.master) - l.add_option("custom_option", bool, False, "help") - assert "custom_option" in l.master.options + loader = addonmanager.Loader(tctx.master) + loader.add_option("custom_option", bool, False, "help") + assert "custom_option" in loader.master.options # calling this again with the same signature is a no-op. - l.add_option("custom_option", bool, False, "help") + loader.add_option("custom_option", bool, False, "help") assert not caplog.text # a different signature should emit a warning though. - l.add_option("custom_option", bool, True, "help") + loader.add_option("custom_option", bool, True, "help") assert "Over-riding existing option" in caplog.text def cmd(a: str) -> str: return "foo" - l.add_command("test.command", cmd) + loader.add_command("test.command", cmd) async def test_simple(caplog): diff --git a/test/mitmproxy/test_certs.py b/test/mitmproxy/test_certs.py index 7ccb52d5c2..9e80bfc99e 100644 --- a/test/mitmproxy/test_certs.py +++ b/test/mitmproxy/test_certs.py @@ -1,14 +1,15 @@ +import ipaddress import os -from datetime import datetime, timezone +from datetime import datetime +from datetime import timezone from pathlib import Path -from cryptography import x509 -from cryptography.x509 import NameOID import pytest +from cryptography import x509 +from cryptography.x509 import NameOID -from mitmproxy import certs from ..conftest import skip_windows - +from mitmproxy import certs # class TestDNTree: # def test_simple(self): @@ -65,22 +66,24 @@ def test_chain_file(self, tdata, tmp_path): (tmp_path / "mitmproxy-ca.pem").write_bytes(cert) ca = certs.CertStore.from_store(tmp_path, "mitmproxy", 2048) assert ca.default_chain_file is None + assert len(ca.default_chain_certs) == 1 (tmp_path / "mitmproxy-ca.pem").write_bytes(2 * cert) ca = certs.CertStore.from_store(tmp_path, "mitmproxy", 2048) assert ca.default_chain_file == (tmp_path / "mitmproxy-ca.pem") + assert len(ca.default_chain_certs) == 2 def test_sans(self, tstore): - c1 = tstore.get_cert("foo.com", ["*.bar.com"]) + c1 = tstore.get_cert("foo.com", [x509.DNSName("*.bar.com")]) tstore.get_cert("foo.bar.com", []) # assert c1 == c2 c3 = tstore.get_cert("bar.com", []) assert not c1 == c3 def test_sans_change(self, tstore): - tstore.get_cert("foo.com", ["*.bar.com"]) - entry = tstore.get_cert("foo.bar.com", ["*.baz.com"]) - assert "*.baz.com" in entry.cert.altnames + tstore.get_cert("foo.com", [x509.DNSName("*.bar.com")]) + entry = tstore.get_cert("foo.bar.com", [x509.DNSName("*.baz.com")]) + assert x509.DNSName("*.baz.com") in entry.cert.altnames def test_expire(self, tstore): tstore.STORE_CAP = 3 @@ -88,35 +91,22 @@ def test_expire(self, tstore): tstore.get_cert("two.com", []) tstore.get_cert("three.com", []) - assert ("one.com", ()) in tstore.certs - assert ("two.com", ()) in tstore.certs - assert ("three.com", ()) in tstore.certs + assert ("one.com", x509.GeneralNames([])) in tstore.certs + assert ("two.com", x509.GeneralNames([])) in tstore.certs + assert ("three.com", x509.GeneralNames([])) in tstore.certs tstore.get_cert("one.com", []) - assert ("one.com", ()) in tstore.certs - assert ("two.com", ()) in tstore.certs - assert ("three.com", ()) in tstore.certs + assert ("one.com", x509.GeneralNames([])) in tstore.certs + assert ("two.com", x509.GeneralNames([])) in tstore.certs + assert ("three.com", x509.GeneralNames([])) in tstore.certs tstore.get_cert("four.com", []) - assert ("one.com", ()) not in tstore.certs - assert ("two.com", ()) in tstore.certs - assert ("three.com", ()) in tstore.certs - assert ("four.com", ()) in tstore.certs - - def test_overrides(self, tmp_path): - ca1 = certs.CertStore.from_store(tmp_path / "ca1", "test", 2048) - ca2 = certs.CertStore.from_store(tmp_path / "ca2", "test", 2048) - assert not ca1.default_ca.serial == ca2.default_ca.serial - - dc = ca2.get_cert("foo.com", ["sans.example.com"]) - dcp = tmp_path / "dc" - dcp.write_bytes(dc.cert.to_pem()) - ca1.add_cert_file("foo.com", dcp) - - ret = ca1.get_cert("foo.com", []) - assert ret.cert.serial == dc.cert.serial + assert ("one.com", x509.GeneralNames([])) not in tstore.certs + assert ("two.com", x509.GeneralNames([])) in tstore.certs + assert ("three.com", x509.GeneralNames([])) in tstore.certs + assert ("four.com", x509.GeneralNames([])) in tstore.certs def test_create_dhparams(self, tmp_path): filename = tmp_path / "dhparam.pem" @@ -131,6 +121,23 @@ def test_umask_secret(self, tmpdir): # TODO: How do we actually attempt to read that file as another user? assert os.stat(filename).st_mode & 0o77 == 0 + @pytest.mark.parametrize( + "input,output", + [ + ( + "subdomain.example.com", + ["subdomain.example.com", "*.example.com", "*.com"], + ), + ( + x509.DNSName("subdomain.example.com"), + ["subdomain.example.com", "*.example.com", "*.com"], + ), + (x509.IPAddress(ipaddress.ip_address("127.0.0.1")), ["127.0.0.1"]), + ], + ) + def test_asterisk_forms(self, input, output): + assert certs.CertStore.asterisk_forms(input) == output + class TestDummyCert: def test_with_ca(self, tstore): @@ -138,11 +145,25 @@ def test_with_ca(self, tstore): tstore.default_privatekey, tstore.default_ca._cert, "foo.com", - ["one.com", "two.com", "*.three.com", "127.0.0.1"], + [ + x509.DNSName("one.com"), + x509.DNSName("two.com"), + x509.DNSName("*.three.com"), + x509.IPAddress(ipaddress.ip_address("127.0.0.1")), + x509.DNSName("bücher.example".encode("idna").decode("ascii")), + ], "Foo Ltd.", ) assert r.cn == "foo.com" - assert r.altnames == ["one.com", "two.com", "*.three.com", "127.0.0.1"] + assert r.altnames == x509.GeneralNames( + [ + x509.DNSName("one.com"), + x509.DNSName("two.com"), + x509.DNSName("*.three.com"), + x509.IPAddress(ipaddress.ip_address("127.0.0.1")), + x509.DNSName("xn--bcher-kva.example"), + ] + ) assert r.organization == "Foo Ltd." r = certs.dummy_cert( @@ -150,7 +171,7 @@ def test_with_ca(self, tstore): ) assert r.cn is None assert r.organization is None - assert r.altnames == [] + assert r.altnames == x509.GeneralNames([]) class TestCert: @@ -169,6 +190,7 @@ def test_simple(self, tdata): assert c2.cn == "www.inode.co.nz" assert len(c2.altnames) == 2 assert c2.fingerprint() + assert c2.public_key() assert c2.notbefore == datetime( year=2010, month=1, @@ -223,6 +245,19 @@ def test_keyinfo(self, tdata, filename, name, bits): c = certs.Cert.from_pem(d) assert c.keyinfo == (name, bits) + @pytest.mark.parametrize( + "filename,is_ca", + [ + ("mitmproxy/net/data/verificationcerts/trusted-leaf.crt", False), + ("mitmproxy/net/data/verificationcerts/trusted-root.crt", True), + ("mitmproxy/data/invalid-subject.pem", False), # no basic constraints + ], + ) + def test_is_ca(self, tdata, filename, is_ca): + pem = Path(tdata.path(filename)).read_bytes() + cert = certs.Cert.from_pem(pem) + assert cert.is_ca == is_ca + def test_err_broken_sans(self, tdata): with open(tdata.path("mitmproxy/net/data/text_cert_weird1"), "rb") as f: d = f.read() @@ -246,6 +281,14 @@ def test_state(self, tdata): c2.set_state(a) assert c == c2 + def test_add_cert_overrides(self, tdata, tstore): + certfile = Path( + tdata.path("mitmproxy/net/data/verificationcerts/trusted-leaf.pem") + ) + cert = certs.Cert.from_pem(certfile.read_bytes()) + tstore.add_cert_file("example.com", certfile) + assert cert == tstore.get_cert("example.com", []).cert + def test_from_store_with_passphrase(self, tdata, tstore): tstore.add_cert_file( "unencrypted-no-pass", Path(tdata.path("mitmproxy/data/testkey.pem")), None @@ -268,6 +311,54 @@ def test_from_store_with_passphrase(self, tdata, tstore): None, ) + def test_add_cert_with_no_private_key(self, tdata, tstore): + with pytest.raises(ValueError, match="Unable to find private key"): + tstore.add_cert_file( + "example.com", + Path( + tdata.path("mitmproxy/net/data/verificationcerts/trusted-leaf.crt") + ), + ) + + def test_add_cert_private_public_mismatch(self, tdata, tstore): + with pytest.raises( + ValueError, match='Private and public keys in ".+" do not match' + ): + tstore.add_cert_file( + "example.com", + Path( + tdata.path( + "mitmproxy/net/data/verificationcerts/private-public-mismatch.pem" + ) + ), + ) + + def test_add_cert_chain(self, tdata, tstore): + tstore.add_cert_file( + "example.com", + Path(tdata.path("mitmproxy/net/data/verificationcerts/trusted-chain.pem")), + ) + assert len(tstore.get_cert("example.com", []).chain_certs) == 2 + + def test_add_cert_chain_invalid(self, tdata, tstore, caplog): + tstore.add_cert_file( + "example.com", + Path( + tdata.path( + "mitmproxy/net/data/verificationcerts/trusted-chain-invalid.pem" + ) + ), + ) + assert "Failed to read certificate chain" in caplog.text + assert len(tstore.get_cert("example.com", []).chain_certs) == 1 + + def test_add_cert_is_ca(self, tdata, tstore, caplog): + tstore.add_cert_file( + "example.com", + Path(tdata.path("mitmproxy/net/data/verificationcerts/trusted-root.pem")), + ) + assert "is a certificate authority and not a leaf certificate" in caplog.text + def test_special_character(self, tdata): with open(tdata.path("mitmproxy/net/data/text_cert_with_comma"), "rb") as f: d = f.read() diff --git a/test/mitmproxy/test_command_lexer.py b/test/mitmproxy/test_command_lexer.py index dfe9b27198..dd7be31dd5 100644 --- a/test/mitmproxy/test_command_lexer.py +++ b/test/mitmproxy/test_command_lexer.py @@ -1,6 +1,7 @@ import pyparsing import pytest -from hypothesis import given, example +from hypothesis import example +from hypothesis import given from hypothesis.strategies import text from mitmproxy import command_lexer diff --git a/test/mitmproxy/test_connection.py b/test/mitmproxy/test_connection.py index 27761e2ac9..eee6607850 100644 --- a/test/mitmproxy/test_connection.py +++ b/test/mitmproxy/test_connection.py @@ -1,12 +1,20 @@ import pytest -from mitmproxy.connection import Server, Client, ConnectionState -from mitmproxy.test.tflow import tclient_conn, tserver_conn +from mitmproxy.connection import Client +from mitmproxy.connection import ConnectionState +from mitmproxy.connection import Server +from mitmproxy.test.tflow import tclient_conn +from mitmproxy.test.tflow import tserver_conn class TestConnection: def test_basic(self): - c = Client(("127.0.0.1", 52314), ("127.0.0.1", 8080), 1607780791) + c = Client( + peername=("127.0.0.1", 52314), + sockname=("127.0.0.1", 8080), + timestamp_start=1607780791, + state=ConnectionState.OPEN, + ) assert not c.tls_established c.timestamp_tls_setup = 1607780792 assert c.tls_established @@ -28,13 +36,18 @@ def test_eq(self): class TestClient: def test_basic(self): - c = Client(("127.0.0.1", 52314), ("127.0.0.1", 8080), 1607780791) + c = Client( + peername=("127.0.0.1", 52314), + sockname=("127.0.0.1", 8080), + timestamp_start=1607780791, + cipher_list=["foo", "bar"], + ) assert repr(c) assert str(c) c.timestamp_tls_setup = 1607780791 assert str(c) c.alpn = b"foo" - assert str(c) == "Client(127.0.0.1:52314, state=open, alpn=foo)" + assert str(c) == "Client(127.0.0.1:52314, state=closed, alpn=foo)" def test_state(self): c = tclient_conn() @@ -55,7 +68,7 @@ def test_state(self): class TestServer: def test_basic(self): - s = Server(("address", 22)) + s = Server(address=("address", 22)) assert repr(s) assert str(s) s.timestamp_tls_setup = 1607780791 @@ -72,7 +85,7 @@ def test_state(self): assert c2.get_state() == c.get_state() def test_address(self): - s = Server(("address", 22)) + s = Server(address=("address", 22)) s.address = ("example.com", 443) s.state = ConnectionState.OPEN with pytest.raises(RuntimeError): diff --git a/test/mitmproxy/test_dns.py b/test/mitmproxy/test_dns.py index 6a5075c91f..b6fa7c7b2b 100644 --- a/test/mitmproxy/test_dns.py +++ b/test/mitmproxy/test_dns.py @@ -1,5 +1,6 @@ import ipaddress import struct + import pytest from mitmproxy import dns @@ -24,7 +25,23 @@ def test_str(self): assert ( str(dns.ResourceRecord.PTR("test", "some.other.host")) == "some.other.host" ) - assert str(dns.ResourceRecord.TXT("test", "unicode text 😀")) == "unicode text 😀" + assert ( + str(dns.ResourceRecord.TXT("test", "unicode text 😀")) == "unicode text 😀" + ) + params = { + 0: b"\x00", + 1: b"\x01", + 2: b"", + 3: b"\x02", + 4: b"\x03", + 5: b"\x04", + 6: b"\x05", + } + record = dns.https_records.HTTPSRecord(1, "example.com", params) + assert ( + str(dns.ResourceRecord.HTTPS("example.com", record)) + == "priority: 1 target_name: 'example.com' {'mandatory': b'\\x00', 'alpn': b'\\x01', 'no_default_alpn': b'', 'port': b'\\x02', 'ipv4hint': b'\\x03', 'ech': b'\\x04', 'ipv6hint': b'\\x05'}" + ) assert ( str( dns.ResourceRecord( @@ -63,6 +80,37 @@ def test_setter(self): rr.text = "sample text" assert rr.text == "sample text" + def test_https_record_ech(self): + rr = dns.ResourceRecord( + "test", dns.types.ANY, dns.classes.IN, dns.ResourceRecord.DEFAULT_TTL, b"" + ) + params = {3: b"\x01\xbb"} + record = dns.https_records.HTTPSRecord(1, "example.org", params) + rr.data = dns.https_records.pack(record) + assert rr.https_ech is None + rr.https_ech = "dGVzdHN0cmluZwo=" + assert rr.https_ech == "dGVzdHN0cmluZwo=" + rr.https_ech = None + assert rr.https_ech is None + + def test_https_record_alpn(self): + rr = dns.ResourceRecord( + "test", dns.types.ANY, dns.classes.IN, dns.ResourceRecord.DEFAULT_TTL, b"" + ) + record = dns.https_records.HTTPSRecord(1, "example.org", {}) + rr.data = dns.https_records.pack(record) + + assert rr.https_alpn is None + assert rr.data == b"\x00\x01\x07example\x03org\x00" + + rr.https_alpn = [b"h2", b"h3"] + assert rr.https_alpn == (b"h2", b"h3") + assert rr.data == b"\x00\x01\x07example\x03org\x00\x00\x01\x00\x06\x02h2\x02h3" + + rr.https_alpn = None + assert rr.https_alpn is None + assert rr.data == b"\x00\x01\x07example\x03org\x00" + class TestMessage: def test_json(self): @@ -111,7 +159,7 @@ def test(what: str, min: int, max: int): with pytest.raises(ValueError): req.packed - test("id", 0, 2 ** 16 - 1) + test("id", 0, 2**16 - 1) test("reserved", 0, 7) test("op_code", 0, 0b1111) test("response_code", 0, 0b1111) @@ -161,8 +209,24 @@ def assert_eq(m: dns.Message, b: bytes) -> None: + b"\x00\x00\x03\x84\x00H\x07ns-1707\tawsdns-21\x02co\x02uk\x00\x11awsdns-hostmaster\x06amazon\xc0\x19\x00" + b"\x00\x00\x01\x00\x00\x1c \x00\x00\x03\x84\x00\x12u\x00\x00\x01Q\x80\x00\x00)\x02\x00\x00\x00\x00\x00\x00\x00" ) - assert invalid_rr_domain_name.answers[0].data == b"\x99live\xc0\x12" - + assert ( + invalid_rr_domain_name.answers[0].data == b"\x99live\x06github\x03com\x00" + ) + valid_compressed_rr_data = dns.Message.unpack( + b"\x10}\x81\x80\x00\x01\x00\x01\x00\x00\x00\x01\x06google\x03com\x00\x00\x06\x00\x01\xc0\x0c\x00\x06\x00" + + b"\x01\x00\x00\x00\x0c\x00&\x03ns1\xc0\x0c\tdns-admin\xc0\x0c&~gw\x00\x00\x03\x84\x00\x00\x03\x84\x00" + + b"\x00\x07\x08\x00\x00\x00<\x00\x00)\x02\x00\x00\x00\x00\x00\x00\x00" + ) + assert ( + valid_compressed_rr_data.answers[0].data + == b"\x03ns1\x06google\x03com\x00\tdns-admin\x06google\x03com\x00&~gw\x00\x00\x03\x84\x00\x00\x03\x84\x00" + + b"\x00\x07\x08\x00\x00\x00<" + ) + A_record_data_contains_pointer_label = dns.Message.unpack( + b"\x98A\x81\x80\x00\x01\x00\x01\x00\x00\x00\x01\x06google\x03com\x00\x00\x01\x00\x01\xc0\x0c\x00\x01\x00" + + b"\x01\x00\x00\x00/\x00\x04\xd8:\xc4\xae\x00\x00)\x02\x00\x00\x00\x00\x00\x00\x00" + ) + assert A_record_data_contains_pointer_label.answers[0].data == b"\xd8:\xc4\xae" req = tutils.tdnsreq() for flag in ( "authoritative_answer", @@ -248,3 +312,9 @@ def test_match(self): def test_repr(self): f = tflow.tdnsflow() assert "DNSFlow" in repr(f) + + def test_question(self): + r = tflow.tdnsreq() + assert r.question + r.questions = [] + assert not r.question diff --git a/test/mitmproxy/test_flow.py b/test/mitmproxy/test_flow.py index a44408c259..3815df4f77 100644 --- a/test/mitmproxy/test_flow.py +++ b/test/mitmproxy/test_flow.py @@ -8,8 +8,10 @@ from mitmproxy import options from mitmproxy.exceptions import FlowReadException from mitmproxy.io import tnetstring -from mitmproxy.proxy import server_hooks, layers -from mitmproxy.test import taddons, tflow +from mitmproxy.proxy import layers +from mitmproxy.proxy import server_hooks +from mitmproxy.test import taddons +from mitmproxy.test import tflow class State: @@ -42,13 +44,14 @@ def test_roundtrip(self): sio.seek(0) r = mitmproxy.io.FlowReader(sio) - l = list(r.stream()) - assert len(l) == 1 + lst = list(r.stream()) + assert len(lst) == 1 - f2 = l[0] + f2 = lst[0] assert f2.get_state() == f.get_state() assert f2.request.data == f.request.data assert f2.marked + assert f2.comment == "test comment" def test_filter(self): sio = io.BytesIO() diff --git a/test/mitmproxy/test_flowfilter.py b/test/mitmproxy/test_flowfilter.py index cf6388c1ea..b494c93337 100644 --- a/test/mitmproxy/test_flowfilter.py +++ b/test/mitmproxy/test_flowfilter.py @@ -1,8 +1,11 @@ import io -import pytest from unittest.mock import patch + +import pytest + +from mitmproxy import flowfilter +from mitmproxy import http from mitmproxy.test import tflow -from mitmproxy import flowfilter, http class TestParsing: diff --git a/test/mitmproxy/test_http.py b/test/mitmproxy/test_http.py index c44bde049a..09cac17480 100644 --- a/test/mitmproxy/test_http.py +++ b/test/mitmproxy/test_http.py @@ -1,17 +1,23 @@ import asyncio import email -import time import json +import time +from typing import Any from unittest import mock import pytest from mitmproxy import flow from mitmproxy import flowfilter -from mitmproxy.http import Headers, Request, Response, HTTPFlow +from mitmproxy.http import Headers +from mitmproxy.http import HTTPFlow +from mitmproxy.http import Message +from mitmproxy.http import Request +from mitmproxy.http import Response from mitmproxy.net.http.cookies import CookieAttrs from mitmproxy.test.tflow import tflow -from mitmproxy.test.tutils import treq, tresp +from mitmproxy.test.tutils import treq +from mitmproxy.test.tutils import tresp class TestRequest: @@ -154,7 +160,9 @@ def test_scheme(self): _test_decoded_attr(treq(), "scheme") def test_port(self): - _test_passthrough_attr(treq(), "port") + _test_passthrough_attr(treq(), "port", 1234) + with pytest.raises(ValueError): + treq().port = "foo" def test_path(self): _test_decoded_attr(treq(), "path") @@ -178,13 +186,13 @@ def test_authority(self): assert request.authority == "fussball" # Don't fail on garbage - request.data.authority = b"foo\xFF\x00bar" + request.data.authority = b"foo\xff\x00bar" assert request.authority.startswith("foo") assert request.authority.endswith("bar") # foo.bar = foo.bar should not cause any side effects. d = request.authority request.authority = d - assert request.data.authority == b"foo\xFF\x00bar" + assert request.data.authority == b"foo\xff\x00bar" def test_host_update_also_updates_header(self): request = treq() @@ -195,8 +203,7 @@ def test_host_update_also_updates_header(self): request.headers["Host"] = "foo" request.authority = "foo" request.host = "example.org" - assert request.headers["Host"] == "example.org" - assert request.authority == "example.org:22" + assert request.headers["Host"] == request.authority == "example.org:22" def test_get_host_header(self): no_hdr = treq() @@ -411,7 +418,7 @@ def test_get_urlencoded_form(self): request.headers["Content-Type"] = "application/x-www-form-urlencoded" assert list(request.urlencoded_form.items()) == [("foobar", "baz")] - request.raw_content = b"\xFF" + request.raw_content = b"\xff" assert len(request.urlencoded_form) == 1 def test_set_urlencoded_form(self): @@ -427,7 +434,7 @@ def test_get_multipart_form(self): request.headers["Content-Type"] = "multipart/form-data" assert list(request.multipart_form.items()) == [] - with mock.patch("mitmproxy.net.http.multipart.decode") as m: + with mock.patch("mitmproxy.net.http.multipart.decode_multipart") as m: m.side_effect = ValueError assert list(request.multipart_form.items()) == [] @@ -562,7 +569,7 @@ def test_get_cookies_with_parameters(self): assert attrs["domain"] == "example.com" assert attrs["expires"] == "Wed Oct 21 16:29:41 2015" assert attrs["path"] == "/" - assert attrs["httponly"] == "" + assert attrs["httponly"] is None def test_get_cookies_no_value(self): resp = tresp() @@ -860,10 +867,10 @@ def test_items(self): ] -def _test_passthrough_attr(message, attr): +def _test_passthrough_attr(message: Message, attr: str, value: Any = b"foo") -> None: assert getattr(message, attr) == getattr(message.data, attr) - setattr(message, attr, b"foo") - assert getattr(message.data, attr) == b"foo" + setattr(message, attr, value) + assert getattr(message.data, attr) == value def _test_decoded_attr(message, attr): @@ -884,13 +891,13 @@ def _test_decoded_attr(message, attr): setattr(message, attr, "Non-Autorisé") assert getattr(message.data, attr) == b"Non-Autoris\xc3\xa9" # Don't fail on garbage - setattr(message.data, attr, b"FOO\xBF\x00BAR") + setattr(message.data, attr, b"FOO\xbf\x00BAR") assert getattr(message, attr).startswith("FOO") assert getattr(message, attr).endswith("BAR") # foo.bar = foo.bar should not cause any side effects. d = getattr(message, attr) setattr(message, attr, d) - assert getattr(message.data, attr) == b"FOO\xBF\x00BAR" + assert getattr(message.data, attr) == b"FOO\xbf\x00BAR" class TestMessageData: @@ -931,7 +938,9 @@ def test_serializable(self): resp = tresp() resp.trailers = Headers() resp2 = Response.from_state(resp.get_state()) - assert resp.data == resp2.data + resp3 = tresp() + resp3.set_state(resp.get_state()) + assert resp.data == resp2.data == resp3.data def test_content_length_update(self): resp = tresp() @@ -1126,7 +1135,7 @@ def test_guess_css_charset(self): assert "鏄庝集" in r.text def test_guess_latin_1(self): - r = tresp(content=b"\xF0\xE2") + r = tresp(content=b"\xf0\xe2") assert r.text == "ðâ" def test_none(self): @@ -1159,7 +1168,7 @@ def test_unknown_ce(self): def test_cannot_decode(self): r = tresp() r.headers["content-type"] = "text/html; charset=utf8" - r.raw_content = b"\xFF" + r.raw_content = b"\xff" with pytest.raises(ValueError): assert r.text @@ -1189,7 +1198,7 @@ def test_cannot_encode(self): r.headers["content-type"] = "text/html; charset=latin1" r.text = "\udcff" assert r.headers["content-type"] == "text/html; charset=utf-8" - assert r.raw_content == b"\xFF" + assert r.raw_content == b"\xff" def test_get_json(self): req = treq(content=None) diff --git a/test/mitmproxy/test_master.py b/test/mitmproxy/test_master.py index df0d2c85ad..04e0bb6005 100644 --- a/test/mitmproxy/test_master.py +++ b/test/mitmproxy/test_master.py @@ -1,4 +1,5 @@ import asyncio +import gc from mitmproxy.master import Master @@ -7,11 +8,26 @@ async def err(): raise RuntimeError -async def test_exception_handler(caplog): - m = Master(None) - running = asyncio.create_task(m.run()) - asyncio.create_task(err()) +async def test_exception_handler(caplog_async): + caplog_async.set_level("ERROR") + + # start proxy master and let it initialize its exception handler + master = Master(None) + running = asyncio.create_task(master.run()) + await asyncio.sleep(0) + + # create a task with an unhandled exception... + task = asyncio.create_task(err()) + # make sure said task is run... await asyncio.sleep(0) - assert "Traceback" in caplog.text - m.shutdown() + + # and garbage-collected... + assert task + del task + gc.collect() + + # and ensure that this triggered a log entry. + await caplog_async.await_log("Traceback") + + master.shutdown() await running diff --git a/test/mitmproxy/test_options.py b/test/mitmproxy/test_options.py index 777ab4dd18..dbaafe1a9d 100644 --- a/test/mitmproxy/test_options.py +++ b/test/mitmproxy/test_options.py @@ -1 +1,5 @@ -# TODO: write tests +from mitmproxy import options + + +def test_simple(): + assert options.Options() diff --git a/test/mitmproxy/test_optmanager.py b/test/mitmproxy/test_optmanager.py index 391a7d0b7e..f2431cbf73 100644 --- a/test/mitmproxy/test_optmanager.py +++ b/test/mitmproxy/test_optmanager.py @@ -1,14 +1,15 @@ +import argparse import copy import io from collections.abc import Sequence +from pathlib import Path from typing import Optional import pytest -import argparse +from mitmproxy import exceptions from mitmproxy import options from mitmproxy import optmanager -from mitmproxy import exceptions class TO(optmanager.OptManager): @@ -41,6 +42,13 @@ def __init__(self): self.add_option("one", Optional[str], None, "help") +class TS(optmanager.OptManager): + def __init__(self): + super().__init__() + self.add_option("scripts", Sequence[str], [], "help") + self.add_option("not_scripts", Sequence[str], [], "help") + + def test_defaults(): o = TD2() defaults = { @@ -302,7 +310,7 @@ def test_serialize_defaults(): def test_saving(tmpdir): o = TD2() o.three = "set" - dst = str(tmpdir.join("conf")) + dst = Path(tmpdir.join("conf")) optmanager.save(o, dst, defaults=True) o2 = TD2() @@ -469,3 +477,43 @@ def test_set(): opts.process_deferred() assert "deferredsequenceoption" not in opts.deferred assert opts.deferredsequenceoption == ["a", "b"] + + +def test_load_paths(tdata): + opts = TS() + conf_path = tdata.path("mitmproxy/data/test_config.yml") + optmanager.load_paths(opts, conf_path) + assert opts.scripts == [ + str(Path.home().absolute().joinpath("abc")), + str(Path(conf_path).parent.joinpath("abc")), + str(Path(conf_path).parent.joinpath("../abc")), + str(Path("/abc").absolute()), + ] + assert opts.not_scripts == ["~/abc", "abc", "../abc", "/abc"] + + +@pytest.mark.parametrize( + "script_path, relative_to, expected", + ( + ("~/abc", ".", Path.home().joinpath("abc")), + ("/abc", ".", Path("/abc")), + ("abc", ".", Path(".").joinpath("abc")), + ("../abc", ".", Path(".").joinpath("../abc")), + ("~/abc", "/tmp", Path.home().joinpath("abc")), + ("/abc", "/tmp", Path("/abc")), + ("abc", "/tmp", Path("/tmp").joinpath("abc")), + ("../abc", "/tmp", Path("/tmp").joinpath("../abc")), + ("~/abc", "foo", Path.home().joinpath("abc")), + ("/abc", "foo", Path("/abc")), + ("abc", "foo", Path("foo").joinpath("abc")), + ("../abc", "foo", Path("foo").joinpath("../abc")), + ), +) +def test_relative_path(script_path, relative_to, expected): + assert ( + optmanager.relative_path( + script_path, + relative_to=relative_to, + ) + == expected.absolute() + ) diff --git a/test/mitmproxy/test_stateobject.py b/test/mitmproxy/test_stateobject.py deleted file mode 100644 index 8c7147de51..0000000000 --- a/test/mitmproxy/test_stateobject.py +++ /dev/null @@ -1,126 +0,0 @@ -from typing import Any - -import pytest - -from mitmproxy.stateobject import StateObject - - -class TObject(StateObject): - def __init__(self, x): - self.x = x - - @classmethod - def from_state(cls, state): - obj = cls(None) - obj.set_state(state) - return obj - - -class Child(TObject): - _stateobject_attributes = dict(x=int) - - def __eq__(self, other): - return isinstance(other, Child) and self.x == other.x - - -class TTuple(TObject): - _stateobject_attributes = dict(x=tuple[int, Child]) - - -class TList(TObject): - _stateobject_attributes = dict(x=list[Child]) - - -class TDict(TObject): - _stateobject_attributes = dict(x=dict[str, Child]) - - -class TAny(TObject): - _stateobject_attributes = dict(x=Any) - - -class TSerializableChild(TObject): - _stateobject_attributes = dict(x=Child) - - -def test_simple(): - a = Child(42) - assert a.get_state() == {"x": 42} - b = a.copy() - a.set_state({"x": 44}) - assert a.x == 44 - assert b.x == 42 - - -def test_serializable_child(): - child = Child(42) - a = TSerializableChild(child) - assert a.get_state() == {"x": {"x": 42}} - a.set_state({"x": {"x": 43}}) - assert a.x.x == 43 - assert a.x is child - b = a.copy() - assert a.x == b.x - assert a.x is not b.x - - -def test_tuple(): - a = TTuple((42, Child(43))) - assert a.get_state() == {"x": (42, {"x": 43})} - b = a.copy() - a.set_state({"x": (44, {"x": 45})}) - assert a.x == (44, Child(45)) - assert b.x == (42, Child(43)) - - -def test_tuple_err(): - a = TTuple(None) - with pytest.raises(ValueError, match="Invalid data"): - a.set_state({"x": (42,)}) - - -def test_list(): - a = TList([Child(1), Child(2)]) - assert a.get_state() == { - "x": [{"x": 1}, {"x": 2}], - } - copy = a.copy() - assert len(copy.x) == 2 - assert copy.x is not a.x - assert copy.x[0] is not a.x[0] - - -def test_dict(): - a = TDict({"foo": Child(42)}) - assert a.get_state() == {"x": {"foo": {"x": 42}}} - b = a.copy() - assert list(a.x.items()) == list(b.x.items()) - assert a.x is not b.x - assert a.x["foo"] is not b.x["foo"] - - -def test_any(): - a = TAny(42) - b = a.copy() - assert a.x == b.x - - a = TAny(object()) - with pytest.raises(ValueError): - a.get_state() - - -def test_too_much_state(): - a = Child(42) - s = a.get_state() - s["foo"] = "bar" - - with pytest.raises(RuntimeWarning): - a.set_state(s) - - -def test_none(): - a = Child(None) - assert a.get_state() == {"x": None} - a = Child(42) - a.set_state({"x": None}) - assert a.x is None diff --git a/test/mitmproxy/test_tcp.py b/test/mitmproxy/test_tcp.py index 13001c8625..3fdde8b23d 100644 --- a/test/mitmproxy/test_tcp.py +++ b/test/mitmproxy/test_tcp.py @@ -1,7 +1,7 @@ import pytest -from mitmproxy import tcp from mitmproxy import flowfilter +from mitmproxy import tcp from mitmproxy.test import tflow diff --git a/test/mitmproxy/test_tls.py b/test/mitmproxy/test_tls.py index af23bc3ab4..434f52b42b 100644 --- a/test/mitmproxy/test_tls.py +++ b/test/mitmproxy/test_tls.py @@ -1,6 +1,5 @@ from mitmproxy import tls - CLIENT_HELLO_NO_EXTENSIONS = bytes.fromhex( "03015658a756ab2c2bff55f636814deac086b7ca56b65058c7893ffc6074f5245f70205658a75475103a152637" "78e1bb6d22e8bbd5b6b0a3a59760ad354e91ba20d353001a0035002f000a000500040009000300060008006000" @@ -103,10 +102,11 @@ def test_extensions(self): assert c.cipher_suites == [2, 3, 10, 5, 4, 9] assert c.alpn_protocols == [b"h2", b"http/1.1"] assert c.extensions == [ - (13, b'\x00\x0e\x04\x03\x05\x03\x06\x03\x04\x01\x05\x01\x06\x01\x08\x07'), - (65281, b'\x00'), - (10, b'\x00\x06\x00\x1d\x00\x17\x00\x18'), - (11, b'\x01\x00'), (23, b''), - (0, b'\x00\x0e\x00\x00\x0bexample.com'), - (16, b'\x00\x0c\x02h2\x08http/1.1') + (13, b"\x00\x0e\x04\x03\x05\x03\x06\x03\x04\x01\x05\x01\x06\x01\x08\x07"), + (65281, b"\x00"), + (10, b"\x00\x06\x00\x1d\x00\x17\x00\x18"), + (11, b"\x01\x00"), + (23, b""), + (0, b"\x00\x0e\x00\x00\x0bexample.com"), + (16, b"\x00\x0c\x02h2\x08http/1.1"), ] diff --git a/test/mitmproxy/test_types.py b/test/mitmproxy/test_types.py index 29d2b1f039..46ac8ec0af 100644 --- a/test/mitmproxy/test_types.py +++ b/test/mitmproxy/test_types.py @@ -1,17 +1,16 @@ +import contextlib +import os from collections.abc import Sequence import pytest -import os -import contextlib +from . import test_command import mitmproxy.exceptions import mitmproxy.types -from mitmproxy.test import taddons -from mitmproxy.test import tflow from mitmproxy import command from mitmproxy import flow - -from . import test_command +from mitmproxy.test import taddons +from mitmproxy.test import tflow @contextlib.contextmanager diff --git a/test/mitmproxy/test_udp.py b/test/mitmproxy/test_udp.py index 2a6a8dd125..ba652f74f1 100644 --- a/test/mitmproxy/test_udp.py +++ b/test/mitmproxy/test_udp.py @@ -1,7 +1,7 @@ import pytest -from mitmproxy import udp from mitmproxy import flowfilter +from mitmproxy import udp from mitmproxy.test import tflow diff --git a/test/mitmproxy/test_version.py b/test/mitmproxy/test_version.py index 1e72ab4ed4..21c89ead12 100644 --- a/test/mitmproxy/test_version.py +++ b/test/mitmproxy/test_version.py @@ -16,8 +16,8 @@ def test_version(capsys): assert stdout.strip() == version.VERSION -def test_get_version(): - version.VERSION = "3.0.0rc2" +def test_get_version(monkeypatch): + monkeypatch.setattr(version, "VERSION", "3.0.0rc2") with mock.patch("subprocess.check_output") as m, mock.patch("subprocess.run") as m2: m2.return_value = True diff --git a/test/mitmproxy/test_websocket.py b/test/mitmproxy/test_websocket.py index f227d0dc50..80cb3d9b3e 100644 --- a/test/mitmproxy/test_websocket.py +++ b/test/mitmproxy/test_websocket.py @@ -1,9 +1,9 @@ import pytest +from wsproto.frame_protocol import Opcode from mitmproxy import http from mitmproxy import websocket from mitmproxy.test import tflow -from wsproto.frame_protocol import Opcode class TestWebSocketData: @@ -15,6 +15,13 @@ def test_state(self): f2 = http.HTTPFlow.from_state(f.get_state()) f2.set_state(f.get_state()) + def test_formatting(self): + tf = tflow.twebsocketflow().websocket + formatted_messages = tf._get_formatted_messages() + assert b"[OUTGOING] hello binary" in formatted_messages + assert b"[OUTGOING] hello text" in formatted_messages + assert b"[INCOMING] it's me" in formatted_messages + class TestWebSocketMessage: def test_basic(self): @@ -43,3 +50,14 @@ def test_text(self): _ = bin.text with pytest.raises(AttributeError, match="do not have a 'text' attribute."): bin.text = "bar" + + def test_message_formatting(self): + incoming_message = websocket.WebSocketMessage( + Opcode.BINARY, False, b"Test Incoming" + ) + outgoing_message = websocket.WebSocketMessage( + Opcode.BINARY, True, b"Test OutGoing" + ) + + assert incoming_message._format_ws_message() == b"[INCOMING] Test Incoming" + assert outgoing_message._format_ws_message() == b"[OUTGOING] Test OutGoing" diff --git a/test/mitmproxy/tools/console/conftest.py b/test/mitmproxy/tools/console/conftest.py index 01bbcbcb38..9a5fe85b80 100644 --- a/test/mitmproxy/tools/console/conftest.py +++ b/test/mitmproxy/tools/console/conftest.py @@ -4,8 +4,10 @@ import pytest from mitmproxy import options +from mitmproxy.tools.console import signals from mitmproxy.tools.console import window from mitmproxy.tools.console.master import ConsoleMaster +from mitmproxy.utils.signals import _SignalMixin def tokenize(input: str) -> list[str]: @@ -19,12 +21,16 @@ def tokenize(input: str) -> list[str]: class ConsoleTestMaster(ConsoleMaster): + def __init__(self, opts: options.Options) -> None: + super().__init__(opts) + self.addons.remove(self.addons.get("tlsconfig")) + def type(self, input: str) -> None: for key in tokenize(input): self.window.keypress(self.ui.get_cols_rows(), key) def screen_contents(self) -> str: - return b"\n".join(self.window.render((80, 24), True)._text_content()).decode() + return b"\n".join(self.window.render((80, 24), True).text).decode() @pytest.fixture @@ -39,6 +45,13 @@ async def console(monkeypatch) -> ConsoleTestMaster: # noqa monkeypatch.setattr(ConsoleTestMaster, "sig_call_in", lambda *_, **__: True) monkeypatch.setattr(sys.stdout, "isatty", lambda: True) + # extremely hacky: the console UI heavily depends on global signals + # that are unfortunately shared across tests + # Here we clear all existing signals so that we don't interact with previous instantiations. + for sig in signals.__dict__.values(): + if isinstance(sig, _SignalMixin): + sig.receivers.clear() + opts = options.Options() m = ConsoleTestMaster(opts) opts.server = False diff --git a/test/mitmproxy/tools/console/test_contentview.py b/test/mitmproxy/tools/console/test_contentview.py index 9819ea9a2e..ee0b727579 100644 --- a/test/mitmproxy/tools/console/test_contentview.py +++ b/test/mitmproxy/tools/console/test_contentview.py @@ -1,6 +1,6 @@ -from mitmproxy.test import tflow from mitmproxy import contentviews from mitmproxy.contentviews.base import format_text +from mitmproxy.test import tflow class TContentView(contentviews.View): diff --git a/test/mitmproxy/tools/console/test_keymap.py b/test/mitmproxy/tools/console/test_keymap.py index 12e12f7c0d..f2e3f2cf6b 100644 --- a/test/mitmproxy/tools/console/test_keymap.py +++ b/test/mitmproxy/tools/console/test_keymap.py @@ -1,8 +1,10 @@ -from mitmproxy.tools.console import keymap -from mitmproxy.test import taddons from unittest import mock + import pytest +from mitmproxy.test import taddons +from mitmproxy.tools.console import keymap + def test_binding(): b = keymap.Binding("space", "cmd", ["options"], "") @@ -75,8 +77,8 @@ def test_remove(): def test_load_path(tmpdir): dst = str(tmpdir.join("conf")) - kmc = keymap.KeymapConfig() - with taddons.context(kmc) as tctx: + with taddons.context() as tctx: + kmc = keymap.KeymapConfig(tctx.master) km = keymap.Keymap(tctx.master) tctx.master.keymap = km @@ -148,8 +150,8 @@ def test_load_path(tmpdir): def test_parse(): - kmc = keymap.KeymapConfig() - with taddons.context(kmc): + with taddons.context() as tctx: + kmc = keymap.KeymapConfig(tctx.master) assert kmc.parse("") == [] assert kmc.parse("\n\n\n \n") == [] with pytest.raises(keymap.KeyBindingError, match="expected a list of keys"): @@ -186,9 +188,8 @@ def test_parse(): cmd: cmd """ ) - assert ( - kmc.parse( - """ + assert kmc.parse( + """ - key: key1 ctx: [one, two] help: one @@ -196,13 +197,11 @@ def test_parse(): foo bar foo bar """ - ) - == [ - { - "key": "key1", - "ctx": ["one", "two"], - "help": "one", - "cmd": "foo bar foo bar\n", - } - ] - ) + ) == [ + { + "key": "key1", + "ctx": ["one", "two"], + "help": "one", + "cmd": "foo bar foo bar\n", + } + ] diff --git a/test/mitmproxy/tools/console/test_master.py b/test/mitmproxy/tools/console/test_master.py index e69de29bb2..47651482c1 100644 --- a/test/mitmproxy/tools/console/test_master.py +++ b/test/mitmproxy/tools/console/test_master.py @@ -0,0 +1,30 @@ +from unittest.mock import Mock + + +def test_spawn_editor(monkeypatch, console): + text_data = "text" + binary_data = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09" + + console.get_editor = Mock() + console.get_editor.return_value = "editor" + console.get_hex_editor = Mock() + console.get_hex_editor.return_value = "editor" + monkeypatch.setattr("subprocess.call", (lambda _: None)) + + console.loop = Mock() + console.loop.stop = Mock() + console.loop.start = Mock() + console.loop.draw_screen = Mock() + + console.spawn_editor(text_data) + console.get_editor.assert_called_once() + + console.spawn_editor(binary_data) + console.get_hex_editor.assert_called_once() + + +def test_get_hex_editor(monkeypatch, console): + test_editor = "hexedit" + monkeypatch.setattr("shutil.which", lambda x: x == test_editor) + editor = console.get_hex_editor() + assert editor == test_editor diff --git a/test/mitmproxy/tools/console/test_quickhelp.py b/test/mitmproxy/tools/console/test_quickhelp.py index 958bf6ae40..e129850d57 100644 --- a/test/mitmproxy/tools/console/test_quickhelp.py +++ b/test/mitmproxy/tools/console/test_quickhelp.py @@ -1,7 +1,8 @@ import pytest from mitmproxy.test.tflow import tflow -from mitmproxy.tools.console import defaultkeys, quickhelp +from mitmproxy.tools.console import defaultkeys +from mitmproxy.tools.console import quickhelp from mitmproxy.tools.console.eventlog import EventLog from mitmproxy.tools.console.flowlist import FlowListBox from mitmproxy.tools.console.flowview import FlowView @@ -38,12 +39,12 @@ def keymap() -> Keymap: (EventLog, None, True), (PathEditor, None, False), (SimpleOverlay, None, False), - ] + ], ) def test_quickhelp(widget, flow, keymap, is_root_widget): qh = quickhelp.make(widget, flow, is_root_widget) for row in [qh.top_items, qh.bottom_items]: - for (title, v) in row.items(): + for title, v in row.items(): if isinstance(v, quickhelp.BasicKeyHelp): key_short = v.key else: diff --git a/test/mitmproxy/tools/console/test_statusbar.py b/test/mitmproxy/tools/console/test_statusbar.py index 080f9197a2..bcf5f3d606 100644 --- a/test/mitmproxy/tools/console/test_statusbar.py +++ b/test/mitmproxy/tools/console/test_statusbar.py @@ -18,7 +18,7 @@ async def test_statusbar(console, monkeypatch): anticomp=True, showhost=True, server_replay_refresh=False, - server_replay_kill_extra=True, + server_replay_extra="kill", upstream_cert=False, stream_large_bodies="3m", mode=["transparent"], diff --git a/test/mitmproxy/tools/test_main.py b/test/mitmproxy/tools/test_main.py index 41437d89bb..d5a77db77d 100644 --- a/test/mitmproxy/tools/test_main.py +++ b/test/mitmproxy/tools/test_main.py @@ -2,7 +2,6 @@ from mitmproxy.tools import main - shutdown_script = "mitmproxy/data/addonscripts/shutdown.py" diff --git a/test/mitmproxy/tools/web/test_app.py b/test/mitmproxy/tools/web/test_app.py index 9a8ece62a0..eac14fa286 100644 --- a/test/mitmproxy/tools/web/test_app.py +++ b/test/mitmproxy/tools/web/test_app.py @@ -1,12 +1,8 @@ -import io import gzip +import importlib import json import logging -import textwrap -from collections.abc import Sequence -from contextlib import redirect_stdout from pathlib import Path -from typing import Optional from unittest import mock import pytest @@ -14,13 +10,15 @@ from tornado import httpclient from tornado import websocket -from mitmproxy import certs, log, options, optmanager -from mitmproxy.http import Headers -from mitmproxy.proxy.mode_servers import ServerInstance +import mitmproxy_rs +from mitmproxy import log +from mitmproxy import options from mitmproxy.test import tflow from mitmproxy.tools.web import app from mitmproxy.tools.web import master as webmaster +here = Path(__file__).parent.absolute() + @pytest.fixture(scope="module") def no_tornado_logging(): @@ -37,119 +35,14 @@ def get_json(resp: httpclient.HTTPResponse): return json.loads(resp.body.decode()) -def test_generate_tflow_js(tdata): - tf_http = tflow.tflow(resp=True, err=True, ws=True) - tf_http.id = "d91165be-ca1f-4612-88a9-c0f8696f3e29" - tf_http.client_conn.id = "4a18d1a0-50a1-48dd-9aa6-d45d74282939" - tf_http.server_conn.id = "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8" - tf_http.server_conn.certificate_list = [ - certs.Cert.from_pem( - Path( - tdata.path("mitmproxy/net/data/verificationcerts/self-signed.pem") - ).read_bytes() - ) - ] - tf_http.request.trailers = Headers(trailer="qvalue") - tf_http.response.trailers = Headers(trailer="qvalue") - tf_http.comment = "I'm a comment!" - - tf_tcp = tflow.ttcpflow(err=True) - tf_tcp.id = "2ea7012b-21b5-4f8f-98cd-d49819954001" - tf_tcp.client_conn.id = "8be32b99-a0b3-446e-93bc-b29982fe1322" - tf_tcp.server_conn.id = "e33bb2cd-c07e-4214-9a8e-3a8f85f25200" - - tf_udp = tflow.tudpflow(err=True) - tf_udp.id = "f9f7b2b9-7727-4477-822d-d3526e5b8951" - tf_udp.client_conn.id = "0a8833da-88e4-429d-ac54-61cda8a7f91c" - tf_udp.server_conn.id = "c49f9c2b-a729-4b16-9212-d181717e294b" - - tf_dns = tflow.tdnsflow(resp=True, err=True) - tf_dns.id = "5434da94-1017-42fa-872d-a189508d48e4" - tf_dns.client_conn.id = "0b4cc0a3-6acb-4880-81c0-1644084126fc" - tf_dns.server_conn.id = "db5294af-c008-4098-a320-a94f901eaf2f" - - # language=TypeScript - content = ( - "/** Auto-generated by test_app.py:test_generate_tflow_js */\n" - "import {HTTPFlow, TCPFlow, UDPFlow, DNSFlow} from '../../flow';\n" - "export function THTTPFlow(): Required {\n" - " return %s\n" - "}\n" - "export function TTCPFlow(): Required {\n" - " return %s\n" - "}\n" - "export function TUDPFlow(): Required {\n" - " return %s\n" - "}\n" - "export function TDNSFlow(): Required {\n" - " return %s\n" - "}\n" - % ( - textwrap.indent( - json.dumps(app.flow_to_json(tf_http), indent=4, sort_keys=True), " " - ), - textwrap.indent( - json.dumps(app.flow_to_json(tf_tcp), indent=4, sort_keys=True), " " - ), - textwrap.indent( - json.dumps(app.flow_to_json(tf_udp), indent=4, sort_keys=True), " " - ), - textwrap.indent( - json.dumps(app.flow_to_json(tf_dns), indent=4, sort_keys=True), " " - ), - ) - ) - content = content.replace(": null", ": undefined") - - ( - Path(__file__).parent / "../../../../web/src/js/__tests__/ducks/_tflow.ts" - ).write_bytes(content.encode()) - - -async def test_generate_options_js(): - o = options.Options() - m = webmaster.WebMaster(o) - opt: optmanager._Option - - def ts_type(t): - if t == bool: - return "boolean" - if t == str: - return "string" - if t == int: - return "number" - if t == Sequence[str]: - return "string[]" - if t == Optional[str]: - return "string | undefined" - if t == Optional[int]: - return "number | undefined" - raise RuntimeError(t) - - with redirect_stdout(io.StringIO()) as s: - - print("/** Auto-generated by test_app.py:test_generate_options_js */") - - print("export interface OptionsState {") - for _, opt in sorted(m.options.items()): - print(f" {opt.name}: {ts_type(opt.typespec)}") - print("}") - print("") - print("export type Option = keyof OptionsState") - print("") - print("export const defaultState: OptionsState = {") - for _, opt in sorted(m.options.items()): - print( - f" {opt.name}: {json.dumps(opt.default)},".replace( - ": null", ": undefined" - ) - ) - print("}") - - ( - Path(__file__).parent / "../../../../web/src/js/ducks/_options_gen.ts" - ).write_bytes(s.getvalue().encode()) - await m.done() +@pytest.mark.parametrize("filename", list((here / "../../../../web/gen").glob("*.py"))) +async def test_generated_files(filename): + mod = importlib.import_module(f"web.gen.{filename.stem}") + expected = await mod.make() + actual = mod.filename.read_text().replace("\r\n", "\n") + assert ( + actual == expected + ), f"{mod.filename} must be regenerated by running {filename.resolve()}." @pytest.mark.usefixtures("no_tornado_logging", "tdata") @@ -159,7 +52,9 @@ async def make_master() -> webmaster.WebMaster: o = options.Options(http2=False) return webmaster.WebMaster(o, with_termlog=False) - m: webmaster.WebMaster = self.io_loop.asyncio_loop.run_until_complete(make_master()) + m: webmaster.WebMaster = self.io_loop.asyncio_loop.run_until_complete( + make_master() + ) f = tflow.tflow(resp=True) f.id = "42" f.request.content = b"foo\nbar" @@ -171,17 +66,6 @@ async def make_master() -> webmaster.WebMaster: m.view.add([tflow.tflow(err=True)]) m.events._add_log(log.LogEntry("test log", "info")) m.events.done() - si1 = ServerInstance.make("regular", m.proxyserver) - si1._listen_addrs = [("127.0.0.1", 8080), ("::1", 8080)] - si1._server = True # spoof is_running - si2 = ServerInstance.make("reverse:example.com", m.proxyserver) - si2.last_exception = RuntimeError("I failed somehow.") - si3 = ServerInstance.make("socks5", m.proxyserver) - m.proxyserver.servers._instances.update({ - si1.mode: si1, - si2.mode: si2, - si3.mode: si3, - }) self.master = m self.view = m.view self.events = m.events @@ -202,7 +86,11 @@ def put_json(self, url, data: dict) -> httpclient.HTTPResponse: ) def test_index(self): - assert self.fetch("/").code == 200 + response: httpclient.HTTPResponse = self.fetch("/") + assert response.code == 200 + assert '"/' not in str( + response.body + ), "HTML content should not contain root-relative paths" def test_filter_help(self): assert self.fetch("/filter-help").code == 200 @@ -292,6 +180,7 @@ def test_flow_update(self): "content": "resp", }, "marked": ":red_circle:", + "comment": "I'm a modified comment!", } assert self.put_json("/flows/42", upd).code == 200 assert f.request.method == "PATCH" @@ -302,6 +191,7 @@ def test_flow_update(self): assert f.response.status_code == 404 assert f.response.headers["bar"] == "baz" assert f.response.text == "resp" + assert f.comment == "I'm a modified comment!" upd = { "request": { @@ -471,8 +361,8 @@ def test_events(self): def test_options(self): j = get_json(self.fetch("/options")) - assert type(j) == dict - assert type(j["anticache"]) == dict + assert isinstance(j, dict) + assert isinstance(j["anticache"], dict) def test_option_update(self): assert self.put_json("/options", {"anticache": True}).code == 200 @@ -482,28 +372,6 @@ def test_option_update(self): def test_option_save(self): assert self.fetch("/options/save", method="POST").code == 200 - def test_generate_state_js(self): - resp = self.fetch("/state") - assert resp.code == 200 - data = json.loads(resp.body) - data.update(available=True) - data["contentViews"] = ["Auto", "Raw"] - data["version"] = "1.2.3" - - # language=TypeScript - content = ( - "/** Auto-generated by test_app.py:test_generate_state_js */\n" - "import {BackendState} from '../../ducks/backendState';\n" - "export function TBackendState(): Required {\n" - " return %s\n" - "}\n" - % textwrap.indent(json.dumps(data, indent=4, sort_keys=True), " ").lstrip() - ) - - ( - Path(__file__).parent / "../../../../web/src/js/__tests__/ducks/_tbackendstate.ts" - ).write_bytes(content.encode()) - def test_err(self): with mock.patch("mitmproxy.tools.web.app.IndexHandler.get") as f: f.side_effect = RuntimeError @@ -536,3 +404,32 @@ def test_websocket(self): # trigger on_close by opening a second connection. ws_client2 = yield websocket.websocket_connect(ws_url) ws_client2.close() + + def test_process_list(self): + try: + mitmproxy_rs.process_info.active_executables() + except NotImplementedError: + pytest.skip( + "mitmproxy_rs.process_info.active_executables not available on this platform." + ) + resp = self.fetch("/processes") + assert resp.code == 200 + assert get_json(resp) + + def test_process_icon(self): + try: + mitmproxy_rs.process_info.executable_icon("invalid") + except NotImplementedError: + pytest.skip( + "mitmproxy_rs.process_info.executable_icon not available on this platform." + ) + except Exception: + pass + resp = self.fetch("/executable-icon") + assert resp.code == 400 + assert "Missing 'path' parameter." in resp.body.decode() + + resp = self.fetch("/executable-icon?path=invalid_path") + assert resp.code == 200 + assert resp.headers["Content-Type"] == "image/png" + assert resp.body == app.TRANSPARENT_PNG diff --git a/test/mitmproxy/tools/web/test_master.py b/test/mitmproxy/tools/web/test_master.py index c193ea87bb..e956b73d4c 100644 --- a/test/mitmproxy/tools/web/test_master.py +++ b/test/mitmproxy/tools/web/test_master.py @@ -1,17 +1,25 @@ import asyncio -from unittest.mock import MagicMock import pytest + from mitmproxy.options import Options from mitmproxy.tools.web.master import WebMaster async def test_reuse(): - server = await asyncio.start_server(MagicMock(), host="127.0.0.1", port=0, reuse_address=False) + async def handler(r, w): + pass + + server = await asyncio.start_server( + handler, host="127.0.0.1", port=0, reuse_address=False + ) port = server.sockets[0].getsockname()[1] master = WebMaster(Options(), with_termlog=False) master.options.web_host = "127.0.0.1" master.options.web_port = port - with pytest.raises(OSError, match=f"--set web_port={port + 1}"): + with pytest.raises(OSError, match=f"--set web_port={port + 2}"): await master.running() server.close() + # tornado registers some callbacks, + # we want to run them to avoid fatal warnings. + await asyncio.sleep(0) diff --git a/test/mitmproxy/tools/web/test_static_viewer.py b/test/mitmproxy/tools/web/test_static_viewer.py index 4364e2557c..74473a18f1 100644 --- a/test/mitmproxy/tools/web/test_static_viewer.py +++ b/test/mitmproxy/tools/web/test_static_viewer.py @@ -1,14 +1,13 @@ import json from unittest import mock +from mitmproxy import flowfilter +from mitmproxy.addons import readfile +from mitmproxy.addons import save from mitmproxy.test import taddons from mitmproxy.test import tflow - -from mitmproxy import flowfilter -from mitmproxy.tools.web.app import flow_to_json - from mitmproxy.tools.web import static_viewer -from mitmproxy.addons import save, readfile +from mitmproxy.tools.web.app import flow_to_json def test_save_static(tmpdir): diff --git a/test/mitmproxy/tools/web/test_web_columns.py b/test/mitmproxy/tools/web/test_web_columns.py new file mode 100644 index 0000000000..6bb85f21f2 --- /dev/null +++ b/test/mitmproxy/tools/web/test_web_columns.py @@ -0,0 +1,5 @@ +from mitmproxy.tools.web.web_columns import AVAILABLE_WEB_COLUMNS + + +def test_web_columns(): + assert isinstance(AVAILABLE_WEB_COLUMNS, list) diff --git a/test/mitmproxy/utils/test_arg_check.py b/test/mitmproxy/utils/test_arg_check.py index 97102f49b9..29018ddade 100644 --- a/test/mitmproxy/utils/test_arg_check.py +++ b/test/mitmproxy/utils/test_arg_check.py @@ -1,5 +1,5 @@ -import io import contextlib +import io from unittest import mock import pytest @@ -35,7 +35,7 @@ "Please use `--proxyauth SPEC` instead.\n" 'SPEC Format: "username:pass", "any" to accept any user/pass combination,\n' '"@path" to use an Apache htpasswd file, or\n' - '"ldap[s]:url_server_ldap:dn_auth:password:dn_subtree" ' + '"ldap[s]:url_server_ldap[:port]:dn_auth:password:dn_subtree[?search_filter_key=...]" ' "for LDAP authentication.", ), ( diff --git a/test/mitmproxy/utils/test_asyncio_utils.py b/test/mitmproxy/utils/test_asyncio_utils.py index 1b59da8b62..05891221b3 100644 --- a/test/mitmproxy/utils/test_asyncio_utils.py +++ b/test/mitmproxy/utils/test_asyncio_utils.py @@ -1,4 +1,8 @@ import asyncio +import gc +import sys + +import pytest from mitmproxy.utils import asyncio_utils @@ -8,10 +12,52 @@ async def ttask(): await asyncio.sleep(999) -async def test_simple(): +async def test_simple(monkeypatch): + monkeypatch.setenv("PYTEST_CURRENT_TEST", "test_foo") task = asyncio_utils.create_task(ttask(), name="ttask", client=("127.0.0.1", 42313)) - assert asyncio_utils.task_repr(task) == "127.0.0.1:42313: ttask (age: 0s)" + assert ( + asyncio_utils.task_repr(task) + == "127.0.0.1:42313: ttask [created in test_foo] (age: 0s)" + ) await asyncio.sleep(0) assert "newname" in asyncio_utils.task_repr(task) delattr(task, "created") assert asyncio_utils.task_repr(task) + + +async def _raise(): + raise RuntimeError() + + +async def test_install_exception_handler(): + e = asyncio.Event() + with asyncio_utils.install_exception_handler(lambda *_, **__: e.set()): + t = asyncio.create_task(_raise()) + await asyncio.sleep(0) + assert t.done() + del t + gc.collect() + await e.wait() + + +async def test_eager_task_factory(): + x = False + + async def task(): + nonlocal x + x = True + + # assert that override works... + assert type(asyncio.get_event_loop_policy()) is asyncio.DefaultEventLoopPolicy + + with asyncio_utils.set_eager_task_factory(): + _ = asyncio.create_task(task()) + if sys.version_info >= (3, 12): + # ...and the context manager is effective + assert x + + +@pytest.fixture() +def event_loop_policy(request): + # override EagerTaskCreationEventLoopPolicy from top-level conftest + return asyncio.DefaultEventLoopPolicy() diff --git a/test/mitmproxy/utils/test_data.py b/test/mitmproxy/utils/test_data.py index f40fc86657..4e7c7af2af 100644 --- a/test/mitmproxy/utils/test_data.py +++ b/test/mitmproxy/utils/test_data.py @@ -1,4 +1,5 @@ import pytest + from mitmproxy.utils import data diff --git a/test/mitmproxy/utils/test_debug.py b/test/mitmproxy/utils/test_debug.py index a61bff8682..6384a59818 100644 --- a/test/mitmproxy/utils/test_debug.py +++ b/test/mitmproxy/utils/test_debug.py @@ -1,6 +1,7 @@ import io import sys from unittest import mock + import pytest from mitmproxy.utils import debug diff --git a/test/mitmproxy/utils/test_emoji.py b/test/mitmproxy/utils/test_emoji.py index a147ba885a..2b099926ba 100644 --- a/test/mitmproxy/utils/test_emoji.py +++ b/test/mitmproxy/utils/test_emoji.py @@ -1,5 +1,5 @@ -from mitmproxy.utils import emoji from mitmproxy.tools.console.common import SYMBOL_MARK +from mitmproxy.utils import emoji def test_emoji(): diff --git a/test/mitmproxy/utils/test_human.py b/test/mitmproxy/utils/test_human.py index 944740611f..d4791de3c2 100644 --- a/test/mitmproxy/utils/test_human.py +++ b/test/mitmproxy/utils/test_human.py @@ -1,5 +1,7 @@ import time + import pytest + from mitmproxy.utils import human @@ -16,8 +18,8 @@ def test_parse_size(): assert human.parse_size("0b") == 0 assert human.parse_size("1") == 1 assert human.parse_size("1k") == 1024 - assert human.parse_size("1m") == 1024 ** 2 - assert human.parse_size("1g") == 1024 ** 3 + assert human.parse_size("1m") == 1024**2 + assert human.parse_size("1g") == 1024**3 with pytest.raises(ValueError): human.parse_size("1f") with pytest.raises(ValueError): diff --git a/test/mitmproxy/utils/test_magisk.py b/test/mitmproxy/utils/test_magisk.py index 83116d7f3c..2382e3921a 100644 --- a/test/mitmproxy/utils/test_magisk.py +++ b/test/mitmproxy/utils/test_magisk.py @@ -1,7 +1,9 @@ -from mitmproxy.utils import magisk +import os + from cryptography import x509 + from mitmproxy.test import taddons -import os +from mitmproxy.utils import magisk def test_get_ca(tdata): diff --git a/test/mitmproxy/utils/test_signals.py b/test/mitmproxy/utils/test_signals.py index 1fcc4f26dd..dd856eb587 100644 --- a/test/mitmproxy/utils/test_signals.py +++ b/test/mitmproxy/utils/test_signals.py @@ -1,7 +1,9 @@ from unittest import mock import pytest -from mitmproxy.utils.signals import AsyncSignal, SyncSignal + +from mitmproxy.utils.signals import AsyncSignal +from mitmproxy.utils.signals import SyncSignal def test_sync_signal() -> None: diff --git a/test/mitmproxy/utils/test_spec.py b/test/mitmproxy/utils/test_spec.py index 6cefcacc72..630dd17998 100644 --- a/test/mitmproxy/utils/test_spec.py +++ b/test/mitmproxy/utils/test_spec.py @@ -1,4 +1,5 @@ import pytest + from mitmproxy.utils.spec import parse_spec diff --git a/test/mitmproxy/utils/test_strutils.py b/test/mitmproxy/utils/test_strutils.py index 3459a673f3..c1fae9d8e7 100644 --- a/test/mitmproxy/utils/test_strutils.py +++ b/test/mitmproxy/utils/test_strutils.py @@ -44,7 +44,7 @@ def test_escape_control_characters(): def test_bytes_to_escaped_str(): assert strutils.bytes_to_escaped_str(b"foo") == "foo" assert strutils.bytes_to_escaped_str(b"\b") == r"\x08" - assert strutils.bytes_to_escaped_str(br"&!?=\)") == r"&!?=\\)" + assert strutils.bytes_to_escaped_str(rb"&!?=\)") == r"&!?=\\)" assert strutils.bytes_to_escaped_str(b"\xc3\xbc") == r"\xc3\xbc" assert strutils.bytes_to_escaped_str(b"'") == r"'" assert strutils.bytes_to_escaped_str(b'"') == r'"' @@ -69,9 +69,9 @@ def test_bytes_to_escaped_str(): def test_escaped_str_to_bytes(): assert strutils.escaped_str_to_bytes("foo") == b"foo" assert strutils.escaped_str_to_bytes("\x08") == b"\b" - assert strutils.escaped_str_to_bytes("&!?=\\\\)") == br"&!?=\)" + assert strutils.escaped_str_to_bytes("&!?=\\\\)") == rb"&!?=\)" assert strutils.escaped_str_to_bytes("\\x08") == b"\b" - assert strutils.escaped_str_to_bytes("&!?=\\\\)") == br"&!?=\)" + assert strutils.escaped_str_to_bytes("&!?=\\\\)") == rb"&!?=\)" assert strutils.escaped_str_to_bytes("\u00fc") == b"\xc3\xbc" with pytest.raises(ValueError): @@ -79,8 +79,8 @@ def test_escaped_str_to_bytes(): def test_is_mostly_bin(): - assert not strutils.is_mostly_bin(b"foo\xFF") - assert strutils.is_mostly_bin(b"foo" + b"\xFF" * 10) + assert not strutils.is_mostly_bin(b"foo\xff") + assert strutils.is_mostly_bin(b"foo" + b"\xff" * 10) assert not strutils.is_mostly_bin("") diff --git a/test/mitmproxy/utils/test_typecheck.py b/test/mitmproxy/utils/test_typecheck.py index 347e39f303..0f480157ca 100644 --- a/test/mitmproxy/utils/test_typecheck.py +++ b/test/mitmproxy/utils/test_typecheck.py @@ -1,7 +1,10 @@ import io import typing from collections.abc import Sequence -from typing import Any, Optional, TextIO, Union +from typing import Any +from typing import Optional +from typing import TextIO +from typing import Union import pytest @@ -84,4 +87,4 @@ def test_typesec_to_str(): def test_typing_aliases(): assert (typecheck.typespec_to_str(typing.Sequence[str])) == "sequence of str" typecheck.check_option_type("foo", [10], typing.Sequence[int]) - typecheck.check_option_type("foo", (42, "42"), typing.Tuple[int, str]) + typecheck.check_option_type("foo", (42, "42"), tuple[int, str]) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 63c9015105..0000000000 --- a/tox.ini +++ /dev/null @@ -1,56 +0,0 @@ -[tox] -envlist = py, flake8, mypy -skipsdist = True -toxworkdir={env:TOX_WORK_DIR:.tox} -exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,mitmproxy/.tox - -[testenv] -deps = - -e .[dev] -setenv = HOME = {envtmpdir} -commands = - mitmdump --version - pytest --timeout 60 -vv --cov-report xml \ - --continue-on-collection-errors \ - --cov=mitmproxy --cov=release \ - --full-cov=mitmproxy/ \ - {posargs} - -[testenv:flake8] -deps = - flake8>=3.8.4,<5.1 - flake8-tidy-imports>=4.2.0,<5 -commands = - flake8 --jobs 8 mitmproxy examples test release {posargs} - -[testenv:filename_matching] -deps = -commands = - python ./test/filename_matching.py - -[testenv:mypy] -deps = - mypy==0.982 - types-certifi==2021.10.8.3 - types-Flask==1.1.6 - types-Werkzeug==1.0.9 - types-requests==2.28.11.2 - types-cryptography==3.3.23.1 - types-pyOpenSSL==22.1.0.1 - -e .[dev] - -commands = - mypy {posargs} - -[testenv:individual_coverage] -commands = - python ./test/individual_coverage.py {posargs} - -[testenv:wheeltest] -recreate = True -deps = -commands = - pip install {posargs} - mitmproxy --version - mitmdump --version - mitmweb --version diff --git a/web/.prettierignore b/web/.prettierignore new file mode 100644 index 0000000000..bd599e40ef --- /dev/null +++ b/web/.prettierignore @@ -0,0 +1,7 @@ +node_modules +coverage +*.md +.tox +src/js/filt/*.js +src/js/__tests__/ducks/_tflow.ts +src/js/__tests__/ducks/_tbackendstate.ts \ No newline at end of file diff --git a/web/README.md b/web/README.md index 0c08dbda79..7cb616ca17 100644 --- a/web/README.md +++ b/web/README.md @@ -1,7 +1,7 @@ # Quick Start - Install mitmproxy as described in [`../CONTRIBUTING.md`](../CONTRIBUTING.md) -- Run `node --version` to make sure that you have at least Node.js 14 or above. If you are on **Ubuntu <= 20.04**, you +- Run `node --version` to make sure that you have at least Node.js 18 or above. If you are on **Ubuntu <= 22.04**, you need to [upgrade](https://github.com/nodesource/distributions/blob/master/README.md#installation-instructions). - Run `cd mitmproxy/web` to change to the directory with package.json @@ -13,6 +13,10 @@ - Run `npm test` to run the test suite. +## Code formatting + +- Run `npm run prettier` to format your code. You can also integrate prettier into your editor, see https://prettier.io/docs/en/editors.html + ## Architecture There are two components: diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs new file mode 100644 index 0000000000..e9d6300699 --- /dev/null +++ b/web/eslint.config.mjs @@ -0,0 +1,54 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; +import pluginReactConfig from "eslint-plugin-react/configs/recommended.js"; + +export default [ + { files: ["**/*.{ts,tsx}"] }, + { languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } }, + { languageOptions: { globals: globals.browser } }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + pluginReactConfig, + { + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + args: "after-used", + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + "one-var": ["error", { const: "never", let: "consecutive" }], + }, + settings: { + react: { + version: "detect", + }, + }, + }, + { + files: ["src/**/*Spec.{ts,tsx}"], + rules: { + "prefer-const": "off", + }, + }, + { + files: ["jest.config.js", "gulpfile.js"], + languageOptions: { globals: globals.node }, + }, + { + files: ["src/**/*.{js,jsx}"], + rules: { + "no-restricted-syntax": [ + "error", + { + selector: "*", + message: "Only TypeScript (ts, tsx) files are allowed.", + }, + ], + }, + }, +]; diff --git a/web/gen/all b/web/gen/all new file mode 100755 index 0000000000..f314d2e1db --- /dev/null +++ b/web/gen/all @@ -0,0 +1,9 @@ +#!/bin/bash + +set -euxo pipefail + +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +for file in "$script_dir"/*.py; do + "$file" +done diff --git a/web/gen/backend_consts.py b/web/gen/backend_consts.py new file mode 100755 index 0000000000..40ec0e6ff0 --- /dev/null +++ b/web/gen/backend_consts.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 + +import asyncio +import typing +from pathlib import Path + +from mitmproxy.proxy.mode_specs import ReverseMode + +here = Path(__file__).parent.absolute() + +filename = here / "../src/js/backends/consts.ts" + + +async def make() -> str: + data = { + "protocols": typing.get_args(typing.get_type_hints(ReverseMode)["scheme"]), + } + + protocols = ",\n ".join( + f'{protocol.upper()} = "{protocol.lower()}"' for protocol in data["protocols"] + ) + + # language=TypeScript + content = ( + "/** Auto-generated by web/gen/backend_consts.py */\n" + "export enum ReverseProxyProtocols {\n" + f" {protocols},\n" + "}\n" + ) + + return content + + +if __name__ == "__main__": + filename.write_bytes(asyncio.run(make()).encode()) diff --git a/web/gen/options_js.py b/web/gen/options_js.py new file mode 100755 index 0000000000..2bd1ddbc37 --- /dev/null +++ b/web/gen/options_js.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 + +import asyncio +import io +import json +from collections.abc import Sequence +from contextlib import redirect_stdout +from pathlib import Path + +from mitmproxy import options +from mitmproxy import optmanager +from mitmproxy.tools.web import master + +here = Path(__file__).parent.absolute() + +filename = here / "../src/js/ducks/_options_gen.ts" + + +def _ts_type(t): + if t is bool: + return "boolean" + if t is str: + return "string" + if t is int: + return "number" + if t == Sequence[str]: + return "string[]" + if t == str | None: + return "string | undefined" + if t == int | None: + return "number | undefined" + raise RuntimeError(t) + + +async def make() -> str: + o = options.Options() + m = master.WebMaster(o) + opt: optmanager._Option + + with redirect_stdout(io.StringIO()) as s: + print("/** Auto-generated by web/gen/options_js.py */") + + print("export interface OptionsState {") + for _, opt in sorted(m.options.items()): + print(f" {opt.name}: {_ts_type(opt.typespec)};") + print("}") + print("") + print("export type Option = keyof OptionsState;") + print("") + print("export const defaultState: OptionsState = {") + for _, opt in sorted(m.options.items()): + print( + f" {opt.name}: {json.dumps(opt.default)},".replace( + ": null", ": undefined" + ) + ) + print("};") + + await m.done() + return s.getvalue() + + +if __name__ == "__main__": + filename.write_bytes(asyncio.run(make()).encode()) diff --git a/web/gen/state_js.py b/web/gen/state_js.py new file mode 100755 index 0000000000..0d3becf7ca --- /dev/null +++ b/web/gen/state_js.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +import asyncio +import json +import textwrap +from pathlib import Path +from unittest.mock import Mock + +from mitmproxy import options +from mitmproxy.proxy.mode_servers import ServerInstance +from mitmproxy.tools.web import app +from mitmproxy.tools.web import master + +here = Path(__file__).parent.absolute() + +filename = here / "../src/js/__tests__/ducks/_tbackendstate.ts" + + +async def make() -> str: + o = options.Options() + m = master.WebMaster(o) + + si1 = ServerInstance.make("regular", m.proxyserver) + sock1 = Mock() + sock1.getsockname.return_value = ("127.0.0.1", 8080) + sock2 = Mock() + sock2.getsockname.return_value = ("::1", 8080) + server = Mock() + server.sockets = [sock1, sock2] + si1._servers = [server] + si2 = ServerInstance.make("reverse:example.com", m.proxyserver) + si2.last_exception = RuntimeError("I failed somehow.") + si3 = ServerInstance.make("socks5", m.proxyserver) + si4 = ServerInstance.make("tun", m.proxyserver) + si4._server = Mock() + si4._server.tun_name = lambda: "tun0" + m.proxyserver.servers._instances.update( + { + si1.mode: si1, + si2.mode: si2, + si3.mode: si3, + si4.mode: si4, + } + ) + + data = app.State.get_json(m) + await m.done() + + data.update(available=True) + data["contentViews"] = ["Auto", "Raw"] + data["version"] = "1.2.3" + data["platform"] = "darwin" + + # language=TypeScript + content = ( + "/** Auto-generated by web/gen/state_js.py */\n" + "import {BackendState} from '../../ducks/backendState';\n" + "export function TBackendState(): Required {\n" + " return %s\n" + "}\n" + % textwrap.indent(json.dumps(data, indent=4, sort_keys=True), " ").lstrip() + ) + + return content + + +if __name__ == "__main__": + filename.write_bytes(asyncio.run(make()).encode()) diff --git a/web/gen/tflow_js.py b/web/gen/tflow_js.py new file mode 100755 index 0000000000..32d71c84d0 --- /dev/null +++ b/web/gen/tflow_js.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 + +import asyncio +import json +import textwrap +from pathlib import Path + +from mitmproxy import certs +from mitmproxy.http import Headers +from mitmproxy.test import tflow +from mitmproxy.tools.web import app + +here = Path(__file__).parent.absolute() + +filename = here / "../src/js/__tests__/ducks/_tflow.ts" + + +async def make() -> str: + tf_http = tflow.tflow(resp=True, err=True, ws=True) + tf_http.id = "d91165be-ca1f-4612-88a9-c0f8696f3e29" + tf_http.client_conn.id = "4a18d1a0-50a1-48dd-9aa6-d45d74282939" + tf_http.server_conn.id = "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8" + tf_http.server_conn.certificate_list = [ + certs.Cert.from_pem( + ( + here / "../../test/mitmproxy/net/data/verificationcerts/self-signed.pem" + ).read_bytes() + ) + ] + tf_http.request.trailers = Headers(trailer="qvalue") + tf_http.response.trailers = Headers(trailer="qvalue") + tf_http.comment = "I'm a comment!" + + tf_tcp = tflow.ttcpflow(err=True) + tf_tcp.id = "2ea7012b-21b5-4f8f-98cd-d49819954001" + tf_tcp.client_conn.id = "8be32b99-a0b3-446e-93bc-b29982fe1322" + tf_tcp.server_conn.id = "e33bb2cd-c07e-4214-9a8e-3a8f85f25200" + + tf_udp = tflow.tudpflow(err=True) + tf_udp.id = "f9f7b2b9-7727-4477-822d-d3526e5b8951" + tf_udp.client_conn.id = "0a8833da-88e4-429d-ac54-61cda8a7f91c" + tf_udp.server_conn.id = "c49f9c2b-a729-4b16-9212-d181717e294b" + + tf_dns = tflow.tdnsflow(resp=True, err=True) + tf_dns.id = "5434da94-1017-42fa-872d-a189508d48e4" + tf_dns.client_conn.id = "0b4cc0a3-6acb-4880-81c0-1644084126fc" + tf_dns.server_conn.id = "db5294af-c008-4098-a320-a94f901eaf2f" + + # language=TypeScript + content = ( + "/** Auto-generated by web/gen/tflow_js.py */\n" + "import {HTTPFlow, TCPFlow, UDPFlow, DNSFlow} from '../../flow';\n" + "export function THTTPFlow(): Required {\n" + " return %s\n" + "}\n" + "export function TTCPFlow(): Required {\n" + " return %s\n" + "}\n" + "export function TUDPFlow(): Required {\n" + " return %s\n" + "}\n" + "export function TDNSFlow(): Required {\n" + " return %s\n" + "}\n" + % ( + textwrap.indent( + json.dumps(app.flow_to_json(tf_http), indent=4, sort_keys=True), " " + ), + textwrap.indent( + json.dumps(app.flow_to_json(tf_tcp), indent=4, sort_keys=True), " " + ), + textwrap.indent( + json.dumps(app.flow_to_json(tf_udp), indent=4, sort_keys=True), " " + ), + textwrap.indent( + json.dumps(app.flow_to_json(tf_dns), indent=4, sort_keys=True), " " + ), + ) + ) + content = content.replace(": null", ": undefined") + return content + + +if __name__ == "__main__": + filename.write_bytes(asyncio.run(make()).encode()) diff --git a/web/gen/web_columns.py b/web/gen/web_columns.py new file mode 100755 index 0000000000..5a5b0b00de --- /dev/null +++ b/web/gen/web_columns.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 + +import asyncio +import json +import re +from pathlib import Path + +here = Path(__file__).parent.absolute() + +input_filename = here / "../src/js/components/FlowTable/FlowColumns.tsx" +filename = here / "../../mitmproxy/tools/web/web_columns.py" + + +def extract_columns() -> list: + # Read the Typescript file content + input_file_content = input_filename.read_text() + + pattern = r"//\s*parsed by web/gen/web_columns\s*\n([\s\w,]+)" + + match = re.search(pattern, input_file_content, re.MULTILINE) + + columns_str = match.group(1) + + columns = [col.strip() for col in columns_str.split(",") if col.strip()] + + return columns + + +async def make() -> str: + available_web_columns = extract_columns() + + # language=Python + content = ( + "# Auto-generated by web/gen/web_columns.py\n" + f"AVAILABLE_WEB_COLUMNS = {json.dumps(available_web_columns, indent=4)}" + ).replace("\n]", ",\n]\n") + + return content + + +if __name__ == "__main__": + filename.write_bytes(asyncio.run(make()).encode()) diff --git a/web/gulpfile.js b/web/gulpfile.js index b7dafce778..f59de42b36 100644 --- a/web/gulpfile.js +++ b/web/gulpfile.js @@ -1,59 +1,65 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ const gulp = require("gulp"); -const gulpEsbuild = require('gulp-esbuild'); +const gulpEsbuild = require("gulp-esbuild"); const less = require("gulp-less"); const livereload = require("gulp-livereload"); -const cleanCSS = require('gulp-clean-css'); +const cleanCSS = require("gulp-clean-css"); const notify = require("gulp-notify"); const compilePeg = require("gulp-peg"); const plumber = require("gulp-plumber"); -const replace = require('gulp-replace'); -const sourcemaps = require('gulp-sourcemaps'); +const replace = require("gulp-replace"); +const sourcemaps = require("gulp-sourcemaps"); const through = require("through2"); const noop = () => through.obj(); -var handleError = {errorHandler: notify.onError("Error: <%= error.message %>")}; +var handleError = { + errorHandler: notify.onError("Error: <%= error.message %>"), +}; function styles(files, dev) { - return gulp.src(files) + return gulp + .src(files) .pipe(dev ? plumber(handleError) : noop()) .pipe(sourcemaps.init()) .pipe(less()) .pipe(dev ? noop() : cleanCSS()) - .pipe(sourcemaps.write(".", {sourceRoot: '/src/css'})) + .pipe(sourcemaps.write(".", { sourceRoot: "/src/css" })) .pipe(gulp.dest("../mitmproxy/tools/web/static")) - .pipe(livereload({auto: false})); + .pipe(livereload({ auto: false })); } function styles_vendor_prod() { - return styles("src/css/vendor.less", false) + return styles("src/css/vendor.less", false); } function styles_vendor_dev() { - return styles("src/css/vendor.less", true) + return styles("src/css/vendor.less", true); } function styles_app_prod() { - return styles("src/css/app.less", false) + return styles("src/css/app.less", false); } function styles_app_dev() { - return styles("src/css/app.less", true) + return styles("src/css/app.less", true); } - function esbuild(dev) { - return gulp.src('src/js/app.tsx').pipe( - gulpEsbuild({ - outfile: 'app.js', - sourcemap: true, - sourceRoot: "/", - minify: !dev, - keepNames: true, - bundle: true, - })) + return gulp + .src("src/js/app.tsx") + .pipe( + gulpEsbuild({ + outfile: "app.js", + sourcemap: true, + sourceRoot: "/", + minify: !dev, + keepNames: true, + bundle: true, + }), + ) .pipe(gulp.dest("../mitmproxy/tools/web/static")) - .pipe(livereload({auto: false})); + .pipe(livereload({ auto: false })); } function scripts_dev() { @@ -64,29 +70,41 @@ function scripts_prod() { return esbuild(false); } -const copy_src = ["src/images/**", "src/fonts/fontawesome-webfont.*", "!**/*.psd"]; +const copy_src = [ + "src/images/**", + "src/fonts/fontawesome-webfont.*", + "!**/*.psd", +]; function copy() { - return gulp.src(copy_src, {base: "src/"}) + return gulp + .src(copy_src, { base: "src/", encoding: false }) .pipe(gulp.dest("../mitmproxy/tools/web/static")); } const template_src = "src/templates/*"; function templates() { - return gulp.src(template_src, {base: "src/"}) + return gulp + .src(template_src, { base: "src/" }) .pipe(gulp.dest("../mitmproxy/tools/web")); } const peg_src = "src/js/filt/*.peg"; function peg() { - return gulp.src(peg_src, {base: "src/"}) + return gulp + .src(peg_src, { base: "src/" }) .pipe(plumber(handleError)) .pipe(compilePeg()) - .pipe(replace('module.exports = ', - 'import * as flowutils from "../flow/utils"\n' + - 'export default ')) + .pipe( + replace( + "module.exports = ", + "/* eslint-disable */\n" + + 'import * as flowutils from "../flow/utils"\n' + + "export default ", + ), + ) .pipe(gulp.dest("src/")); } @@ -96,7 +114,7 @@ const dev = gulp.parallel( styles_app_dev, peg, scripts_dev, - templates + templates, ); const prod = gulp.parallel( @@ -105,18 +123,18 @@ const prod = gulp.parallel( styles_app_prod, peg, scripts_prod, - templates + templates, ); exports.dev = dev; exports.prod = prod; exports.default = function watch() { - const opts = {ignoreInitial: false}; - livereload.listen({auto: true}); + const opts = { ignoreInitial: false }; + livereload.listen({ auto: true }); gulp.watch(["src/css/vendor*"], opts, styles_vendor_dev); gulp.watch(["src/css/**"], opts, styles_app_dev); gulp.watch(["src/js/**"], opts, scripts_dev); gulp.watch(template_src, opts, templates); gulp.watch(peg_src, opts, peg); gulp.watch(copy_src, opts, copy); -} +}; diff --git a/web/jest.config.js b/web/jest.config.js index a1e4daaed1..bedccc4c1f 100644 --- a/web/jest.config.js +++ b/web/jest.config.js @@ -1,36 +1,29 @@ module.exports = async () => { - - process.env.TZ = 'UTC'; + process.env.TZ = "UTC"; return { - "testEnvironment": "jsdom", - "testRegex": "__tests__/.*Spec.(js|ts)x?$", - "roots": [ - "/src/js" - ], - "unmockedModulePathPatterns": [ - "react" - ], - "coverageDirectory": "./coverage", - "coveragePathIgnorePatterns": [ + testEnvironment: "jsdom", + testRegex: "__tests__/.*Spec.(js|ts)x?$", + roots: ["/src/js"], + unmockedModulePathPatterns: ["react"], + coverageDirectory: "./coverage", + coveragePathIgnorePatterns: [ "/src/js/contrib/", "/src/js/filt/", - "/src/js/components/editors/" - ], - "collectCoverageFrom": [ - "src/js/**/*.{js,jsx,ts,tsx}" + "/src/js/components/editors/", ], - "transform": { + collectCoverageFrom: ["src/js/**/*.{js,jsx,ts,tsx}"], + transform: { "^.+\\.[jt]sx?$": [ "esbuild-jest", { - "loaders": { - ".js": "tsx" + loaders: { + ".js": "tsx", }, - "format": "cjs", - "sourcemap": true, - } - ] - } - } -} + format: "cjs", + sourcemap: true, + }, + ], + }, + }; +}; diff --git a/web/package-lock.json b/web/package-lock.json index 002b7e3efe..442ffbfaf5 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,8715 +1,13910 @@ { "name": "mitmproxy", + "lockfileVersion": 3, "requires": true, - "lockfileVersion": 1, - "dependencies": { - "@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "packages": { + "": { + "name": "mitmproxy", + "dependencies": { + "@popperjs/core": "^2.11.8", + "@reduxjs/toolkit": "^2.2.5", + "bootstrap": "^3.4.1", + "classnames": "^2.3.1", + "codemirror": "^5.62.3", + "lodash": "^4.17.21", + "prop-types": "^15.7.2", + "qrcode": "^1.5.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-popper": "^2.2.5", + "react-redux": "^9.1.2", + "stable": "^0.1.8" + }, + "devDependencies": { + "@eslint/js": "^9.6.0", + "@testing-library/dom": "^10.3.1", + "@testing-library/jest-dom": "^6.4.6", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", + "@types/jest": "^29.5.12", + "@types/redux-mock-store": "^1.0.3", + "esbuild": "^0.23.0", + "esbuild-jest": "^0.5.0", + "eslint": "^8.57.0", + "eslint-plugin-react": "^7.34.3", + "globals": "^15.8.0", + "gulp": "^5.0.0", + "gulp-clean-css": "^4.3.0", + "gulp-esbuild": "^0.12.0", + "gulp-less": "^5.0.0", + "gulp-livereload": "^4.0.2", + "gulp-notify": "^4.0.0", + "gulp-peg": "^0.2.0", + "gulp-plumber": "^1.2.1", + "gulp-replace": "^1.1.3", + "gulp-sourcemaps": "^3.0.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "jest-fetch-mock": "^3.0.3", + "prettier": "3.3.2", + "react-test-renderer": "^18.3.1", + "redux-mock-store": "^1.5.4", + "through2": "^4.0.2", + "typescript": "^5.5.3", + "typescript-eslint": "^7.15.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", + "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", + "dev": true + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, - "requires": { - "@babel/highlight": "^7.12.13" + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" } }, - "@babel/compat-data": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.5.tgz", - "integrity": "sha512-kixrYn4JwfAVPa0f2yfzc2AWti6WRRyO3XjWW5PJAvtE11qhSayrrcrEnee05KAtNaPC+EwehE8Qt1UedEVB8w==", - "dev": true + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } }, - "@babel/core": { - "version": "7.14.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.3.tgz", - "integrity": "sha512-jB5AmTKOCSJIZ72sd78ECEhuPiDMKlQdDI/4QRI6lzYATx5SSogS1oQA2AoPecRCknm30gHi2l+QVvNUu3wZAg==", + "node_modules/@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.14.3", - "@babel/helper-compilation-targets": "^7.13.16", - "@babel/helper-module-transforms": "^7.14.2", - "@babel/helpers": "^7.14.0", - "@babel/parser": "^7.14.3", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.14.2", - "@babel/types": "^7.14.2", - "convert-source-map": "^1.7.0", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.1.2", - "semver": "^6.3.0", - "source-map": "^0.5.0" + "json5": "^2.2.3", + "semver": "^6.3.1" }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "@babel/generator": { - "version": "7.14.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.3.tgz", - "integrity": "sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA==", + "node_modules/@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", "dev": true, - "requires": { - "@babel/types": "^7.14.2", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - }, "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-compilation-targets": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.5.tgz", - "integrity": "sha512-v+QtZqXEiOnpO6EYvlImB6zCD2Lel06RzOPzmkz/D/XgQiUu3C/Jb1LOqSt/AIA34TYi/Q+KlT8vTQrgdxkbLw==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", "dev": true, - "requires": { - "@babel/compat-data": "^7.14.5", - "@babel/helper-validator-option": "^7.14.5", - "browserslist": "^4.16.6", - "semver": "^6.3.0" + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-function-name": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", - "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.14.5", - "@babel/template": "^7.14.5", - "@babel/types": "^7.14.5" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.14.5" - } - }, - "@babel/parser": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.6.tgz", - "integrity": "sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ==", - "dev": true - }, - "@babel/template": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", - "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/parser": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/types": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", - "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "to-fast-properties": "^2.0.0" - } - } + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-get-function-arity": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz", - "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - }, - "dependencies": { - "@babel/types": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", - "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "to-fast-properties": "^2.0.0" - } - } + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-hoist-variables": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz", - "integrity": "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - }, - "dependencies": { - "@babel/types": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", - "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "to-fast-properties": "^2.0.0" - } - } + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-member-expression-to-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.5.tgz", - "integrity": "sha512-UxUeEYPrqH1Q/k0yRku1JE7dyfyehNwT6SVkMHvYvPDv4+uu627VXBckVj891BO8ruKBkiDoGnZf4qPDD8abDQ==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - }, - "dependencies": { - "@babel/types": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", - "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "to-fast-properties": "^2.0.0" - } - } + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-module-imports": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz", - "integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - }, - "dependencies": { - "@babel/types": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", - "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "to-fast-properties": "^2.0.0" - } - } + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "@babel/helper-module-transforms": { - "version": "7.14.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.2.tgz", - "integrity": "sha512-OznJUda/soKXv0XhpvzGWDnml4Qnwp16GN+D/kZIdLsWoHj05kyu8Rm5kXmMef+rVJZ0+4pSGLkeixdqNUATDA==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.13.12", - "@babel/helper-replace-supers": "^7.13.12", - "@babel/helper-simple-access": "^7.13.12", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/helper-validator-identifier": "^7.14.0", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.14.2", - "@babel/types": "^7.14.2" + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-optimise-call-expression": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz", - "integrity": "sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - }, - "dependencies": { - "@babel/types": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", - "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "to-fast-properties": "^2.0.0" - } - } + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-plugin-utils": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", - "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", - "dev": true + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } }, - "@babel/helper-replace-supers": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.5.tgz", - "integrity": "sha512-3i1Qe9/8x/hCHINujn+iuHy+mMRLoc77b2nI9TB0zjH1hvn9qGlXjWlggdwUcju36PkPCy/lpM7LLUdcTyH4Ow==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.14.5", - "@babel/helper-optimise-call-expression": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.14.5" - } - }, - "@babel/generator": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz", - "integrity": "sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/parser": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.6.tgz", - "integrity": "sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ==", - "dev": true - }, - "@babel/traverse": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz", - "integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.14.5", - "@babel/helper-function-name": "^7.14.5", - "@babel/helper-hoist-variables": "^7.14.5", - "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.5", - "@babel/types": "^7.14.5", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", - "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "to-fast-properties": "^2.0.0" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "dev": true, + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-simple-access": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz", - "integrity": "sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, - "requires": { - "@babel/types": "^7.13.12" + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-split-export-declaration": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", - "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - }, - "dependencies": { - "@babel/types": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", - "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "to-fast-properties": "^2.0.0" - } - } + "node_modules/@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "dev": true, + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-validator-identifier": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", - "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", - "dev": true + "node_modules/@babel/helpers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } }, - "@babel/helper-validator-option": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", - "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", - "dev": true + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } }, - "@babel/helpers": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.6.tgz", - "integrity": "sha512-yesp1ENQBiLI+iYHSJdoZKUtRpfTlL1grDIX9NRlAVppljLw/4tTyYupIB7uIYmC3stW/imAv8EqaKaS/ibmeA==", + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, - "requires": { - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5" + "dependencies": { + "color-convert": "^1.9.0" }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "dependencies": { - "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.14.5" - } - }, - "@babel/generator": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz", - "integrity": "sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/parser": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.6.tgz", - "integrity": "sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ==", - "dev": true - }, - "@babel/template": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", - "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/parser": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/traverse": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz", - "integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.14.5", - "@babel/helper-function-name": "^7.14.5", - "@babel/helper-hoist-variables": "^7.14.5", - "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.5", - "@babel/types": "^7.14.5", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", - "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "to-fast-properties": "^2.0.0" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" } }, - "@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" } }, - "@babel/parser": { - "version": "7.14.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.4.tgz", - "integrity": "sha512-ArliyUsWDUqEGfWcmzpGUzNfLxTdTp6WU4IuP6QFSp9gGfWS6boxFCkJSJ/L4+RG8z/FnIU3WxCk6hPL9SSWeA==", + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, - "@babel/plugin-syntax-async-generators": { + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-bigint": { + "node_modules/@babel/plugin-syntax-bigint": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-class-properties": { + "node_modules/@babel/plugin-syntax-class-properties": { "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-import-meta": { + "node_modules/@babel/plugin-syntax-import-meta": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-json-strings": { + "node_modules/@babel/plugin-syntax-json-strings": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-logical-assignment-operators": { + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-nullish-coalescing-operator": { + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-numeric-separator": { + "node_modules/@babel/plugin-syntax-numeric-separator": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-object-rest-spread": { + "node_modules/@babel/plugin-syntax-object-rest-spread": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-optional-catch-binding": { + "node_modules/@babel/plugin-syntax-optional-catch-binding": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-optional-chaining": { + "node_modules/@babel/plugin-syntax-optional-chaining": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-top-level-await": { + "node_modules/@babel/plugin-syntax-top-level-await": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", - "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", - "dev": true - } + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-typescript": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.14.5.tgz", - "integrity": "sha512-u6OXzDaIXjEstBRRoBCQ/uKQKlbuaeE5in0RvWdA4pN6AhqxTIwUsnHPU1CFZA/amYObMsuWhYfRl3Ch90HD0Q==", + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - }, "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", - "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", - "dev": true - } + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.14.0.tgz", - "integrity": "sha512-EX4QePlsTaRZQmw9BsoPeyh5OCtRGIhwfLquhxGp5e32w+dyL8htOcDwamlitmNFK6xBZYlygjdye9dbd9rUlQ==", + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", + "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.14.0", - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/helper-simple-access": "^7.13.12", - "babel-plugin-dynamic-import-node": "^2.3.3" + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/runtime": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz", - "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", - "requires": { - "regenerator-runtime": "^0.13.4" + "node_modules/@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/runtime-corejs3": { - "version": "7.15.3", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.15.3.tgz", - "integrity": "sha512-30A3lP+sRL6ml8uhoJSs+8jwpKzbw8CqBvDc1laeptxPm5FahumJxirigcbD2qTs71Sonvj1cyZB0OKGAmxQ+A==", + "node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", "dev": true, - "requires": { - "core-js-pure": "^3.16.0", - "regenerator-runtime": "^0.13.4" + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/template": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", - "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "node_modules/@babel/traverse": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/types": "^7.12.13" + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/traverse": { - "version": "7.14.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.2.tgz", - "integrity": "sha512-TsdRgvBFHMyHOOzcP9S6QU0QQtjxlRpEYOy3mcCO5RgmC305ki42aSAmfZEMSSYBla2oZ9BMqYlncBaKmD/7iA==", + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.14.2", - "@babel/helper-function-name": "^7.14.2", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/parser": "^7.14.2", - "@babel/types": "^7.14.2", - "debug": "^4.1.0", - "globals": "^11.1.0" + "engines": { + "node": ">=4" } }, - "@babel/types": { - "version": "7.14.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.4.tgz", - "integrity": "sha512-lCj4aIs0xUefJFQnwwQv2Bxg7Omd6bgquZ6LGC+gGMh6/s5qDVfjuCMlDmYQ15SLsWHd9n+X3E75lKIhl5Lkiw==", + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.0", + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "@bcoe/v8-coverage": { + "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "@cnakazawa/watch": { + "node_modules/@cnakazawa/watch": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", "dev": true, - "requires": { + "dependencies": { "exec-sh": "^0.3.2", "minimist": "^1.2.0" + }, + "bin": { + "watch": "cli.js" + }, + "engines": { + "node": ">=0.1.95" } }, - "@gulp-sourcemaps/identity-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", - "integrity": "sha512-Tb+nSISZku+eQ4X1lAkevcQa+jknn/OVUgZ3XCxEKIsLsqYuPoJwJOPQeaOk75X3WPftb29GWY1eqE7GLsXb1Q==", + "node_modules/@cnakazawa/watch/node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "requires": { - "acorn": "^6.4.1", - "normalize-path": "^3.0.0", - "postcss": "^7.0.16", - "source-map": "^0.6.0", - "through2": "^3.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "through2": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", - "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", - "dev": true, - "requires": { - "inherits": "^2.0.4", - "readable-stream": "2 || 3" - } - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "@gulp-sourcemaps/map-sources": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", - "integrity": "sha1-iQrnxdjId/bThIYCFazp1+yUW9o=", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "requires": { - "normalize-path": "^2.0.1", - "through2": "^2.0.3" - }, - "dependencies": { - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" } }, - "@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], "dev": true, - "requires": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - } + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, - "@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true - }, - "@jest/console": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.0.6.tgz", - "integrity": "sha512-fMlIBocSHPZ3JxgWiDNW/KPj6s+YRd0hicb33IrmelCcjXo/pXPwvuiKFmZz+XuqI/1u7nbUK10zSsWL/1aegg==", + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^27.0.6", - "jest-util": "^27.0.6", - "slash": "^3.0.0" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - } + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, - "@jest/core": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.0.6.tgz", - "integrity": "sha512-SsYBm3yhqOn5ZLJCtccaBcvD/ccTLCeuDv8U41WJH/V1MW5eKUkeMHT9U+Pw/v1m1AIWlnIW/eM2XzQr0rEmow==", + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], "dev": true, - "requires": { - "@jest/console": "^27.0.6", - "@jest/reporters": "^27.0.6", - "@jest/test-result": "^27.0.6", - "@jest/transform": "^27.0.6", - "@jest/types": "^27.0.6", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-changed-files": "^27.0.6", - "jest-config": "^27.0.6", - "jest-haste-map": "^27.0.6", - "jest-message-util": "^27.0.6", - "jest-regex-util": "^27.0.6", - "jest-resolve": "^27.0.6", - "jest-resolve-dependencies": "^27.0.6", - "jest-runner": "^27.0.6", - "jest-runtime": "^27.0.6", - "jest-snapshot": "^27.0.6", - "jest-util": "^27.0.6", - "jest-validate": "^27.0.6", - "jest-watcher": "^27.0.6", - "micromatch": "^4.0.4", - "p-each-series": "^2.1.0", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - } + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, - "@jest/environment": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.0.6.tgz", - "integrity": "sha512-4XywtdhwZwCpPJ/qfAkqExRsERW+UaoSRStSHCCiQTUpoYdLukj+YJbQSFrZjhlUDRZeNiU9SFH0u7iNimdiIg==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], "dev": true, - "requires": { - "@jest/fake-timers": "^27.0.6", - "@jest/types": "^27.0.6", - "@types/node": "*", - "jest-mock": "^27.0.6" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - } + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" } }, - "@jest/fake-timers": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.0.6.tgz", - "integrity": "sha512-sqd+xTWtZ94l3yWDKnRTdvTeZ+A/V7SSKrxsrOKSqdyddb9CeNRF8fbhAU0D7ZJBpTTW2nbp6MftmKJDZfW2LQ==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "@sinonjs/fake-timers": "^7.0.2", - "@types/node": "*", - "jest-message-util": "^27.0.6", - "jest-mock": "^27.0.6", - "jest-util": "^27.0.6" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - } - } - }, - "@jest/globals": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.0.6.tgz", - "integrity": "sha512-DdTGCP606rh9bjkdQ7VvChV18iS7q0IMJVP1piwTWyWskol4iqcVwthZmoJEf7obE1nc34OpIyoVGPeqLC+ryw==", - "dev": true, - "requires": { - "@jest/environment": "^27.0.6", - "@jest/types": "^27.0.6", - "expect": "^27.0.6" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - } + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" } }, - "@jest/reporters": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.0.6.tgz", - "integrity": "sha512-TIkBt09Cb2gptji3yJXb3EE+eVltW6BjO7frO7NEfjI9vSIYoISi5R3aI3KpEDXlB1xwB+97NXIqz84qYeYsfA==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], "dev": true, - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^27.0.6", - "@jest/test-result": "^27.0.6", - "@jest/transform": "^27.0.6", - "@jest/types": "^27.0.6", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.4", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^4.0.3", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.2", - "jest-haste-map": "^27.0.6", - "jest-resolve": "^27.0.6", - "jest-util": "^27.0.6", - "jest-worker": "^27.0.6", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^4.0.1", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^8.0.0" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - } + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" } }, - "@jest/source-map": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.0.6.tgz", - "integrity": "sha512-Fek4mi5KQrqmlY07T23JRi0e7Z9bXTOOD86V/uS0EIW4PClvPDqZOyFlLpNJheS6QI0FNX1CgmPjtJ4EA/2M+g==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], "dev": true, - "requires": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.4", - "source-map": "^0.6.0" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" } }, - "@jest/test-result": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.0.6.tgz", - "integrity": "sha512-ja/pBOMTufjX4JLEauLxE3LQBPaI2YjGFtXexRAjt1I/MbfNlMx0sytSX3tn5hSLzQsR3Qy2rd0hc1BWojtj9w==", + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], "dev": true, - "requires": { - "@jest/console": "^27.0.6", - "@jest/types": "^27.0.6", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - } + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "@jest/test-sequencer": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.0.6.tgz", - "integrity": "sha512-bISzNIApazYOlTHDum9PwW22NOyDa6VI31n6JucpjTVM0jD6JDgqEZ9+yn575nDdPF0+4csYDxNNW13NvFQGZA==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], "dev": true, - "requires": { - "@jest/test-result": "^27.0.6", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^27.0.6", - "jest-runtime": "^27.0.6" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "@jest/transform": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.0.6.tgz", - "integrity": "sha512-rj5Dw+mtIcntAUnMlW/Vju5mr73u8yg+irnHwzgtgoeI6cCPOvUwQ0D1uQtc/APmWgvRweEb1g05pkUpxH3iCA==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^27.0.6", - "babel-plugin-istanbul": "^6.0.0", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^27.0.6", - "jest-regex-util": "^27.0.6", - "jest-util": "^27.0.6", - "micromatch": "^4.0.4", - "pirates": "^4.0.1", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - } + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "@jest/types": { - "version": "27.0.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.2.tgz", - "integrity": "sha512-XpjCtJ/99HB4PmyJ2vgmN7vT+JLP7RW1FBT9RgnMFS4Dt7cvIyBee8O3/j98aUZ34ZpenPZFqmaaObWSeL65dg==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "@popperjs/core": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.3.tgz", - "integrity": "sha512-xDu17cEfh7Kid/d95kB6tZsLOmSWKCZKtprnhVepjsSaCij+lM3mItSJDuuHDMbCWTh8Ejmebwb+KONcCJ0eXQ==" - }, - "@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], "dev": true, - "requires": { - "type-detect": "4.0.8" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "@sinonjs/fake-timers": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", - "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "@testing-library/dom": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.1.0.tgz", - "integrity": "sha512-kmW9alndr19qd6DABzQ978zKQ+J65gU2Rzkl8hriIetPnwpesRaK4//jEQyYh8fEALmGhomD/LBQqt+o+DL95Q==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^4.2.0", - "aria-query": "^4.2.2", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.6", - "lz-string": "^1.4.4", - "pretty-format": "^27.0.2" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "@testing-library/jest-dom": { - "version": "5.14.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.14.1.tgz", - "integrity": "sha512-dfB7HVIgTNCxH22M1+KU6viG5of2ldoA5ly8Ar8xkezKHKXjRvznCdbMbqjYGgO2xjRbwnR+rR8MLUIqF3kKbQ==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], "dev": true, - "requires": { - "@babel/runtime": "^7.9.2", - "@types/testing-library__jest-dom": "^5.9.1", - "aria-query": "^4.2.2", - "chalk": "^3.0.0", - "css": "^3.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.5.6", - "lodash": "^4.17.15", - "redent": "^3.0.0" - }, - "dependencies": { - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - } - }, - "strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "requires": { - "min-indent": "^1.0.0" - } - } + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "@testing-library/react": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.0.0.tgz", - "integrity": "sha512-sh3jhFgEshFyJ/0IxGltRhwZv2kFKfJ3fN1vTZ6hhMXzz9ZbbcTgmDYM4e+zJv+oiVKKEWZPyqPAh4MQBI65gA==", + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], "dev": true, - "requires": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^8.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "@testing-library/user-event": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.2.1.tgz", - "integrity": "sha512-cczlgVl+krjOb3j1625usarNEibI0IFRJrSWX9UsJ1HKYFgCQv9Nb7QAipUDXl3Xdz8NDTsiS78eAkPSxlzTlw==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], "dev": true, - "requires": { - "@babel/runtime": "^7.12.5" + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" } }, - "@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true - }, - "@types/aria-query": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==", - "dev": true - }, - "@types/babel__core": { - "version": "7.1.14", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.14.tgz", - "integrity": "sha512-zGZJzzBUVDo/eV6KgbE0f0ZI7dInEYvo12Rb70uNQDshC3SkRMb67ja0GgRHZgAX3Za6rhaWlvbDO8rrGyAb1g==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", + "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "cpu": [ + "arm64" + ], "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, - "@types/babel__generator": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.2.tgz", - "integrity": "sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], "dev": true, - "requires": { - "@babel/types": "^7.0.0" + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" } }, - "@types/babel__template": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.0.tgz", - "integrity": "sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" } }, - "@types/babel__traverse": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.11.1.tgz", - "integrity": "sha512-Vs0hm0vPahPMYi9tDjtP66llufgO3ST16WXaSTtDGEl9cewAl3AibmxWw6TINOqHPT9z0uABKAYjT9jNSg4npw==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], "dev": true, - "requires": { - "@babel/types": "^7.3.0" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" } }, - "@types/expect": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", - "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==", - "dev": true - }, - "@types/graceful-fs": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", - "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], "dev": true, - "requires": { - "@types/node": "*" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" } }, - "@types/hoist-non-react-statics": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", - "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", - "requires": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" } }, - "@types/istanbul-lib-coverage": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", - "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", - "dev": true - }, - "@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, - "requires": { - "@types/istanbul-lib-report": "*" + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "@types/jest": { - "version": "27.0.1", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.0.1.tgz", - "integrity": "sha512-HTLpVXHrY69556ozYkcq47TtQJXpcWAWfkoqz+ZGz2JnmZhzlRjprCIyFnetSy8gpDWwTTGBcRVv1J1I1vBrHw==", + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, - "requires": { - "jest-diff": "^27.0.0", - "pretty-format": "^27.0.0" + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "@types/node": { - "version": "15.12.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.4.tgz", - "integrity": "sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA==", - "dev": true - }, - "@types/prettier": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.3.2.tgz", - "integrity": "sha512-eI5Yrz3Qv4KPUa/nSIAi0h+qX0XyewOliug5F2QAtuRg6Kjg6jfmxe1GIwoIRhZspD1A0RP8ANrPwvEXXtRFog==", + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "@types/prop-types": { - "version": "15.7.3", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", - "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" - }, - "@types/react": { - "version": "17.0.11", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.11.tgz", - "integrity": "sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA==", - "requires": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "@types/react-redux": { - "version": "7.1.16", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.16.tgz", - "integrity": "sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==", - "requires": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, "dependencies": { - "redux": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.0.tgz", - "integrity": "sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g==", - "requires": { - "@babel/runtime": "^7.9.2" - } - } + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "@types/redux-mock-store": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.3.tgz", - "integrity": "sha512-Wqe3tJa6x9MxMN4DJnMfZoBRBRak1XTPklqj4qkVm5VBpZnC8PSADf4kLuFQ9NAdHaowfWoEeUMz7NWc2GMtnA==", + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "requires": { - "redux": "^4.0.5" + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "@types/scheduler": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz", - "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==" - }, - "@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", - "dev": true - }, - "@types/testing-library__jest-dom": { - "version": "5.14.1", - "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.1.tgz", - "integrity": "sha512-Gk9vaXfbzc5zCXI9eYE9BI5BNHEp4D3FWjgqBE/ePGYElLAP+KvxBcsdkwfIVvezs605oiyd/VrpiHe3Oeg+Aw==", + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, - "requires": { - "@types/jest": "*" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "@types/vinyl": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.4.tgz", - "integrity": "sha512-2o6a2ixaVI2EbwBPg1QYLGQoHK56p/8X/sGfKbFC8N6sY9lfjsMf/GprtkQkSya0D4uRiutRZ2BWj7k3JvLsAQ==", + "node_modules/@eslint/js": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.6.0.tgz", + "integrity": "sha512-D9B0/3vNg44ZeWbYMpBoXqNP4j6eQD5vNwIlGAuFRRzK/WtT/jvDQW3Bi9kkf3PMDMlM7Yi+73VLUsn5bJcl8A==", "dev": true, - "requires": { - "@types/expect": "^1.20.4", - "@types/node": "*" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "@types/yargs": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.3.tgz", - "integrity": "sha512-YlFfTGS+zqCgXuXNV26rOIeETOkXnGQXP/pjjL9P0gO/EP9jTmc7pUBhx+jVEIxpq41RX33GQ7N3DzOSfZoglQ==", + "node_modules/@gulp-sourcemaps/identity-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", + "integrity": "sha512-Tb+nSISZku+eQ4X1lAkevcQa+jknn/OVUgZ3XCxEKIsLsqYuPoJwJOPQeaOk75X3WPftb29GWY1eqE7GLsXb1Q==", "dev": true, - "requires": { - "@types/yargs-parser": "*" + "dependencies": { + "acorn": "^6.4.1", + "normalize-path": "^3.0.0", + "postcss": "^7.0.16", + "source-map": "^0.6.0", + "through2": "^3.0.1" + }, + "engines": { + "node": ">= 0.10" } }, - "@types/yargs-parser": { - "version": "20.2.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz", - "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", - "dev": true - }, - "abab": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", - "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", - "dev": true - }, - "acorn": { + "node_modules/@gulp-sourcemaps/identity-map/node_modules/acorn": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", - "dev": true - }, - "acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", "dev": true, - "requires": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" + "bin": { + "acorn": "bin/acorn" }, - "dependencies": { - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - } + "engines": { + "node": ">=0.4.0" } }, - "acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "node_modules/@gulp-sourcemaps/identity-map/node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", "dev": true, - "requires": { - "debug": "4" + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" } }, - "ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "node_modules/@gulp-sourcemaps/map-sources": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", + "integrity": "sha512-o/EatdaGt8+x2qpb0vFLC/2Gug/xYPRXb6a+ET1wGYKozKN3krDWC/zZFZAtrzxJHuDL12mwdfEFKcKMNvc55A==", "dev": true, - "requires": { - "ansi-wrap": "^0.1.0" + "dependencies": { + "normalize-path": "^2.0.1", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" } }, - "ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "requires": { - "type-fest": "^0.21.3" - } + "node_modules/@gulp-sourcemaps/map-sources/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true }, - "ansi-gray": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", - "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", + "node_modules/@gulp-sourcemaps/map-sources/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", "dev": true, - "requires": { - "ansi-wrap": "0.1.0" + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - }, + "node_modules/@gulp-sourcemaps/map-sources/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, "dependencies": { - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - } + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "ansi-wrap": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", + "node_modules/@gulp-sourcemaps/map-sources/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "node_modules/@gulp-sourcemaps/map-sources/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, "dependencies": { - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - } + "safe-buffer": "~5.1.0" } }, - "append-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", - "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE=", + "node_modules/@gulp-sourcemaps/map-sources/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, - "requires": { - "buffer-equal": "^1.0.0" + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" } }, - "archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/@gulpjs/messages": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@gulpjs/messages/-/messages-1.1.0.tgz", + "integrity": "sha512-Ys9sazDatyTgZVb4xPlDufLweJ/Os2uHWOv+Caxvy2O85JcnT4M3vc73bi8pdLWlv3fdWQz3pdI9tVwo8rQQSg==", "dev": true, - "requires": { - "sprintf-js": "~1.0.2" + "engines": { + "node": ">=10.13.0" } }, - "aria-query": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "node_modules/@gulpjs/to-absolute-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", + "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", "dev": true, - "requires": { - "@babel/runtime": "^7.10.2", - "@babel/runtime-corejs3": "^7.10.2" + "dependencies": { + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0" } }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-filter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz", - "integrity": "sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4=", + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", "dev": true, - "requires": { - "make-iterator": "^1.0.0" + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" } }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", - "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ=", + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, - "requires": { - "make-iterator": "^1.0.0" + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", - "dev": true - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true }, - "array-initial": { + "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", - "integrity": "sha1-L6dLJnOTccOUe9enrcc74zSz15U=", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, - "requires": { - "array-slice": "^1.0.0", - "is-number": "^4.0.0" - }, "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - } + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" } }, - "array-last": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array-last/-/array-last-1.3.0.tgz", - "integrity": "sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==", + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, - "requires": { - "is-number": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - } + "engines": { + "node": ">=8" } }, - "array-slice": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", - "dev": true - }, - "array-sort": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-1.0.0.tgz", - "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==", + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dev": true, - "requires": { - "default-compare": "^1.0.0", - "get-value": "^2.0.6", - "kind-of": "^5.0.2" - }, "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "async-done": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", - "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", + "node_modules/@jest/console/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.2", - "process-nextick-args": "^2.0.0", - "stream-exhaust": "^1.0.1" + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true - }, - "async-settle": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", - "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", + "node_modules/@jest/console/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", "dev": true, - "requires": { - "async-done": "^1.2.2" + "dependencies": { + "@types/yargs-parser": "*" } }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "babel-jest": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", - "integrity": "sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA==", + "node_modules/@jest/console/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, - "requires": { - "@jest/transform": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/babel__core": "^7.1.7", - "babel-plugin-istanbul": "^6.0.0", - "babel-preset-jest": "^26.6.2", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "slash": "^3.0.0" + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, "dependencies": { - "@jest/transform": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.6.2.tgz", - "integrity": "sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^26.6.2", - "babel-plugin-istanbul": "^6.0.0", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^26.6.2", - "jest-regex-util": "^26.0.0", - "jest-util": "^26.6.2", - "micromatch": "^4.0.2", - "pirates": "^4.0.1", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - } - }, - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "15.0.13", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz", - "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - } - }, - "jest-haste-map": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", - "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/graceful-fs": "^4.1.2", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.1.2", - "graceful-fs": "^4.2.4", - "jest-regex-util": "^26.0.0", - "jest-serializer": "^26.6.2", - "jest-util": "^26.6.2", - "jest-worker": "^26.6.2", - "micromatch": "^4.0.2", - "sane": "^4.0.3", - "walker": "^1.0.7" - } - }, - "jest-regex-util": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", - "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", - "dev": true - }, - "jest-serializer": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", - "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==", - "dev": true, - "requires": { - "@types/node": "*", - "graceful-fs": "^4.2.4" - } - }, - "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" - } - }, - "jest-worker": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", - "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", - "dev": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true } } }, - "babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", - "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "node_modules/@jest/core/node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", "dev": true, - "requires": { - "object.assign": "^4.1.0" + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "babel-plugin-istanbul": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", - "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", + "node_modules/@jest/core/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^4.0.0", - "test-exclude": "^6.0.0" + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "babel-plugin-jest-hoist": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz", - "integrity": "sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw==", + "node_modules/@jest/core/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", "dev": true, - "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.0.0", - "@types/babel__traverse": "^7.0.6" + "dependencies": { + "@types/yargs-parser": "*" } }, - "babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, - "requires": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "babel-preset-jest": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz", - "integrity": "sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ==", + "node_modules/@jest/core/node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^26.6.2", - "babel-preset-current-node-syntax": "^1.0.0" + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" } }, - "bach": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", - "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=", + "node_modules/@jest/core/node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, - "requires": { - "arr-filter": "^1.1.1", - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "array-each": "^1.0.0", - "array-initial": "^1.0.0", - "array-last": "^1.1.1", - "async-done": "^1.2.2", - "async-settle": "^1.0.0", - "now-and-later": "^2.0.0" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "node_modules/@jest/core/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - } + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "binaryextensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-2.3.0.tgz", - "integrity": "sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==", + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, - "body": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", - "integrity": "sha1-5LoM5BCkaTYyM2dgnstOZVMSUGk=", + "node_modules/@jest/core/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", "dev": true, - "requires": { - "continuable-cache": "^0.3.1", - "error": "^7.0.0", - "raw-body": "~1.1.0", - "safe-json-parse": "~1.0.1" + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "bootstrap": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-3.4.1.tgz", - "integrity": "sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA==" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "node_modules/@jest/environment/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, "dependencies": { - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", - "dev": true - }, - "browserslist": { - "version": "4.16.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", - "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", + "node_modules/@jest/environment/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001219", - "colorette": "^1.2.2", - "electron-to-chromium": "^1.3.723", - "escalade": "^3.1.1", - "node-releases": "^1.1.71" + "dependencies": { + "@types/yargs-parser": "*" } }, - "bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "dev": true, - "requires": { - "node-int64": "^0.4.0" + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "buffer-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", - "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", - "dev": true - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, - "bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", - "integrity": "sha1-NWnt6Lo0MV+rmcPpLLBMciDeH6g=", - "dev": true - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "node_modules/@jest/fake-timers/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, - "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - }, "dependencies": { - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true - } + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "caniuse-lite": { - "version": "1.0.30001239", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001239.tgz", - "integrity": "sha512-cyBkXJDMeI4wthy8xJ2FvDU6+0dtcZSJW3voUF8+e9f1bBeuvyZfc3PNbkOETyhbR+dGCPzn9E7MA3iwzusOhQ==", - "dev": true - }, - "capture-exit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", - "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "node_modules/@jest/fake-timers/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", "dev": true, - "requires": { - "rsvp": "^4.8.4" + "dependencies": { + "@types/yargs-parser": "*" } }, - "chalk": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", - "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "node_modules/@jest/fake-timers/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - }, "dependencies": { - "fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "dev": true, - "optional": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - } + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "ci-info": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.2.0.tgz", - "integrity": "sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==", - "dev": true - }, - "cjs-module-lexer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", - "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", - "dev": true - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "node_modules/@jest/globals/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "classnames": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" - }, - "clean-css": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", - "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", + "node_modules/@jest/globals/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", "dev": true, - "requires": { - "source-map": "~0.6.0" + "dependencies": { + "@types/yargs-parser": "*" } }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", "dev": true, - "requires": { - "string-width": "^4.2.0", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" + "v8-to-istanbul": "^9.0.1" }, - "dependencies": { - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true } } }, - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", - "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "cloneable-readable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", - "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", + "node_modules/@jest/reporters/node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", "dev": true, - "requires": { - "inherits": "^2.0.1", - "process-nextick-args": "^2.0.0", - "readable-stream": "^2.3.5" + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "codemirror": { - "version": "5.62.3", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.3.tgz", - "integrity": "sha512-zZAyOfN8TU67ngqrxhOgtkSAGV9jSpN1snbl8elPtnh9Z5A11daR405+dhLzLnuXrwX0WCShWlybxPN3QC/9Pg==" - }, - "collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", - "dev": true - }, - "collection-map": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", - "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw=", + "node_modules/@jest/reporters/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, - "requires": { - "arr-map": "^2.0.2", - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "node_modules/@jest/reporters/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" + "dependencies": { + "@types/yargs-parser": "*" } }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, - "requires": { - "color-name": "1.1.3" + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" } }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true - }, - "colorette": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", - "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "node_modules/@jest/reporters/node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, - "requires": { - "delayed-stream": "~1.0.0" + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" } }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "node_modules/@jest/reporters/node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "continuable-cache": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz", - "integrity": "sha1-vXJ6f67XfnH/OYWskzUakSczrQ8=", - "dev": true - }, - "convert-source-map": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", - "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "node_modules/@jest/reporters/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - }, "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - } + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "copy-anything": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.3.tgz", - "integrity": "sha512-GK6QUtisv4fNS+XcI7shX0Gx9ORg7QqIznyfho79JTnX1XhLiyZHfftvGiziqzRiEi/Bjhgpi+D2o7HxJFPnDQ==", + "node_modules/@jest/reporters/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "requires": { - "is-what": "^3.12.0" + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "copy-props": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.5.tgz", - "integrity": "sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==", + "node_modules/@jest/reporters/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", "dev": true, - "requires": { - "each-props": "^1.3.2", - "is-plain-object": "^5.0.0" - }, "dependencies": { - "is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true - } + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "core-js-pure": { - "version": "3.16.2", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.16.2.tgz", - "integrity": "sha512-oxKe64UH049mJqrKkynWp6Vu0Rlm/BTXO/bJZuN2mmR3RtOFNepLlSWDd1eo16PzHpQAoNG97rLU1V/YxesJjw==", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "cross-fetch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz", - "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==", + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, - "requires": { - "node-fetch": "2.6.1" + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, "dependencies": { - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "css": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", - "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", "dev": true, - "requires": { - "inherits": "^2.0.4", - "source-map": "^0.6.1", - "source-map-resolve": "^0.6.0" - }, "dependencies": { - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "source-map-resolve": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", - "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", - "dev": true, - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0" - } - } + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", - "dev": true - }, - "cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", - "dev": true - }, - "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "node_modules/@jest/test-result/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, - "requires": { - "cssom": "~0.3.6" + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - } + "@types/yargs-parser": "*" } }, - "csstype": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz", - "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==" + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "node_modules/@jest/test-sequencer/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, - "requires": { - "array-find-index": "^1.0.1" + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "d": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "node_modules/@jest/test-sequencer/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", "dev": true, - "requires": { - "es5-ext": "^0.10.9" + "dependencies": { + "@types/yargs-parser": "*" } }, - "data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "node_modules/@jest/test-sequencer/node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, - "requires": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" } }, - "dateformat": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", - "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", + "node_modules/@jest/test-sequencer/node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, - "requires": { - "get-stdin": "^4.0.1", - "meow": "^3.3.0" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "node_modules/@jest/test-sequencer/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, - "requires": { - "ms": "2.1.2" - }, "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "debug-fabulous": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-1.1.0.tgz", - "integrity": "sha512-GZqvGIgKNlUnHUPQhepnUZFIMoi3dgZKQBzKDeL2g7oJF9SNAji/AAu36dusFUas0O+pae74lNeoIPHqXWDkLg==", + "node_modules/@jest/transform": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.6.2.tgz", + "integrity": "sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==", "dev": true, - "requires": { - "debug": "3.X", - "memoizee": "0.4.X", - "object-assign": "4.X" - }, "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - } + "@babel/core": "^7.1.0", + "@jest/types": "^26.6.2", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-util": "^26.6.2", + "micromatch": "^4.0.2", + "pirates": "^4.0.1", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">= 10.14.2" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "decimal.js": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", - "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==", + "node_modules/@jest/transform/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true + "node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } }, - "dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", - "dev": true + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", "dev": true }, - "default-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", - "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, - "requires": { - "kind-of": "^5.0.2" - }, "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "default-resolution": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", - "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=", - "dev": true - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, - "requires": { - "object-keys": "^1.0.12" + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" } }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" + "engines": { + "node": ">= 8" } }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } }, - "detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", - "dev": true + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } }, - "detect-newline": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", - "dev": true + "node_modules/@reduxjs/toolkit": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.6.tgz", + "integrity": "sha512-kH0r495c5z1t0g796eDQAkYbEQ3a1OLYN9o8jQQVZyKyw367pfRGS+qZLkHYvFHiUUdafpoSlQ2QYObIApjPWA==", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } }, - "diff-sequences": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.0.6.tgz", - "integrity": "sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==", + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, - "dijkstrajs": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.2.tgz", - "integrity": "sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==" + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } }, - "dom-accessibility-api": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.7.tgz", - "integrity": "sha512-ml3lJIq9YjUfM9TUnEPvEYWFSwivwIGBPKpewX7tii7fwCazA8yCioGdqQcNsItPpfFvSJ3VIdMQPj60LJhcQA==", - "dev": true + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } }, - "domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "node_modules/@testing-library/dom": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.3.1.tgz", + "integrity": "sha512-q/WL+vlXMpC0uXDyfsMtc1rmotzLV8Y0gq6q1gfrrDjQeHoeLrqHbxdPvPNAh1i+xuJl7+BezywcXArz7vLqKQ==", "dev": true, - "requires": { - "webidl-conversions": "^5.0.0" + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.6.tgz", + "integrity": "sha512-8qpnGVincVDLEcQXWaHOf6zmlbwTKc6Us6PPu4CRnPXCzo2OGBS5cwgMMOWdxDpEz1mkbvXHpEy99M5Yvt682w==", + "dev": true, "dependencies": { - "webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "dev": true + "@adobe/css-tools": "^4.4.0", + "@babel/runtime": "^7.9.2", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@jest/globals": ">= 28", + "@types/bun": "latest", + "@types/jest": ">= 28", + "jest": ">= 28", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "@jest/globals": { + "optional": true + }, + "@types/bun": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "jest": { + "optional": true + }, + "vitest": { + "optional": true } } }, - "duplexer2": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", - "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "dev": true, - "requires": { - "readable-stream": "~1.1.9" + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/@testing-library/react": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.0.tgz", + "integrity": "sha512-guuxUKRWQ+FgNX0h0NS0FIq3Q3uLtWVpBzcLOggmfMoUpgBnzBzvLLd4fbm6yS8ydJd94cIfY4yP9qUQjM2KwQ==", + "dev": true, "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } + "@types/react-dom": { + "optional": true } } }, - "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", "dev": true, - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" } }, - "each-props": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", - "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "dev": true, - "requires": { - "is-plain-object": "^2.0.1", - "object.defaults": "^1.1.0" + "engines": { + "node": ">= 10" } }, - "electron-to-chromium": { - "version": "1.3.752", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.752.tgz", - "integrity": "sha512-2Tg+7jSl3oPxgsBsWKh5H83QazTkmWG/cnNwJplmyZc7KcN61+I10oUgaXSVk/NwfvN3BdkKDR4FYuRBQQ2v0A==", + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true }, - "emittery": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", - "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "encode-utf8": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", - "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, - "requires": { - "once": "^1.4.0" + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" } }, - "errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dev": true, - "optional": true, - "requires": { - "prr": "~1.0.1" + "dependencies": { + "@babel/types": "^7.0.0" } }, - "error": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/error/-/error-7.2.1.tgz", - "integrity": "sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==", + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, - "requires": { - "string-template": "~0.2.1" + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" } }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", "dev": true, - "requires": { - "is-arrayish": "^0.2.1" + "dependencies": { + "@babel/types": "^7.20.7" } }, - "es5-ext": { - "version": "0.10.53", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", - "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "node_modules/@types/expect": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", + "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==", + "dev": true + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", "dev": true, - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.3", - "next-tick": "~1.0.0" + "dependencies": { + "@types/node": "*" } }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" + "dependencies": { + "@types/istanbul-lib-coverage": "*" } }, - "es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, - "requires": { - "d": "^1.0.1", - "ext": "^1.1.2" - }, - "dependencies": { - "d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "dev": true, - "requires": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - } + "dependencies": { + "@types/istanbul-lib-report": "*" } }, - "es6-weak-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" } }, - "esbuild": { - "version": "0.12.21", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.21.tgz", - "integrity": "sha512-7hyXbU3g94aREufI/5nls7Xcc+RGQeZWZApm6hoBaFvt2BPtpT4TjFMQ9Tb1jU8XyBGz00ShmiyflCogphMHFQ==", - "dev": true + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, - "esbuild-jest": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/esbuild-jest/-/esbuild-jest-0.5.0.tgz", - "integrity": "sha512-AMZZCdEpXfNVOIDvURlqYyHwC8qC1/BFjgsrOiSL1eyiIArVtHL8YAC83Shhn16cYYoAWEW17yZn0W/RJKJKHQ==", + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, - "requires": { - "@babel/core": "^7.12.17", - "@babel/plugin-transform-modules-commonjs": "^7.12.13", - "babel-jest": "^26.6.3" + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } }, - "escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "node_modules/@types/node": { + "version": "20.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", "dev": true, - "requires": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" + "dependencies": { + "undici-types": "~5.26.4" } }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "optional": true, + "peer": true }, - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "optional": true, + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@types/react": "*" + } }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "node_modules/@types/redux-mock-store": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.6.tgz", + "integrity": "sha512-eg5RDfhJTXuoJjOMyXiJbaDb1B8tfTaJixscmu+jOusj6adGC0Krntz09Tf4gJgXeCqCrM5bBMd+B7ez0izcAQ==", "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" + "dependencies": { + "redux": "^4.0.5" } }, - "exec-sh": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz", - "integrity": "sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==", + "node_modules/@types/redux-mock-store/node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, - "execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, + "node_modules/@types/vinyl": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.12.tgz", + "integrity": "sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw==", "dev": true, - "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, "dependencies": { - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true - } + "@types/expect": "^1.20.4", + "@types/node": "*" } }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "node_modules/@types/yargs": { + "version": "15.0.19", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", + "integrity": "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz", + "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==", "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.16.0", + "@typescript-eslint/type-utils": "7.16.0", + "@typescript-eslint/utils": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz", + "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==", + "dev": true, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true + "@typescript-eslint/scope-manager": "7.16.0", + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/typescript-estree": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true } } }, - "expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", + "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" + "dependencies": { + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "expect": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/expect/-/expect-27.0.6.tgz", - "integrity": "sha512-psNLt8j2kwg42jGBDSfAlU49CEZxejN1f1PlANWDZqIhBOVU/c2Pm888FcjWJzFewhIsNWfZJeLjUjtKGiPuSw==", + "node_modules/@typescript-eslint/type-utils": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz", + "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==", "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "ansi-styles": "^5.0.0", - "jest-get-type": "^27.0.6", - "jest-matcher-utils": "^27.0.6", - "jest-message-util": "^27.0.6", - "jest-regex-util": "^27.0.6" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "jest-get-type": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.0.6.tgz", - "integrity": "sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg==", - "dev": true + "dependencies": { + "@typescript-eslint/typescript-estree": "7.16.0", + "@typescript-eslint/utils": "7.16.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true } } }, - "ext": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", - "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "node_modules/@typescript-eslint/types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", + "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", "dev": true, - "requires": { - "type": "^2.0.0" + "engines": { + "node": "^18.18.0 || >=20.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", + "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", + "dev": true, "dependencies": { - "type": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz", - "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==", - "dev": true + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true } } }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "bin": { + "semver": "bin/semver.js" }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", + "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", + "dev": true, "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.16.0", + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/typescript-estree": "7.16.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" } }, - "fancy-log": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", - "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", + "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", "dev": true, - "requires": { - "ansi-gray": "^0.1.1", - "color-support": "^1.1.3", - "parse-node-version": "^1.0.0", - "time-stamp": "^1.0.0" + "dependencies": { + "@typescript-eslint/types": "7.16.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, - "fast-levenshtein": { + "node_modules/abab": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", "dev": true }, - "faye-websocket": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", - "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" } }, - "fb-watchman": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", - "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", "dev": true, - "requires": { - "bser": "^2.0.0" + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" } }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "node_modules/acorn-walk": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" } }, - "findup-sync": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", - "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - }, "dependencies": { - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - } + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" } }, - "fined": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", - "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", - "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "flagged-respawn": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", - "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", - "dev": true - }, - "flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, - "requires": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" + "engines": { + "node": ">=6" } }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "node_modules/ansi-cyan": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", + "integrity": "sha512-eCjan3AVo/SxZ0/MyIYRtkpxIu/H3xZN7URr1vXVrISxeyz8fUFz0FJziamK4sS8I+t35y4rHg1b2PklyBe/7A==", "dev": true, - "requires": { - "for-in": "^1.0.1" + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, - "requires": { - "map-cache": "^0.2.2" + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "fs-mkdirp-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", - "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=", + "node_modules/ansi-gray": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", + "integrity": "sha512-HrgGIZUl8h2EHuZaU9hTR/cU5nhKxpVE1V6kdGsQ8e4zirElJ5fvtfc8N7Q1oq1aatO275i8pUFUCpNWCAnVWw==", "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "through2": "^2.0.3" - }, "dependencies": { - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "node_modules/ansi-red": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", + "integrity": "sha512-ewaIr5y+9CUTGFwZfpECUbFlGcC0GCw1oqR9RI6h1gQCd9Aj2GxSckCnPsVJnmfMZbwFYE+leZGASgkWl06Jow==", "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } }, - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "get-package-type": { + "node_modules/ansi-wrap": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } }, - "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "engines": { + "node": ">=0.10.0" } }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, - "glob-stream": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", - "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", "dev": true, - "requires": { - "extend": "^3.0.0", - "glob": "^7.1.1", - "glob-parent": "^3.1.0", - "is-negated-glob": "^1.0.0", - "ordered-read-streams": "^1.0.0", - "pumpify": "^1.3.5", - "readable-stream": "^2.1.5", - "remove-trailing-separator": "^1.0.1", - "to-absolute-glob": "^2.0.0", - "unique-stream": "^2.0.2" + "engines": { + "node": ">=0.10.0" } }, - "glob-watcher": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.3.tgz", - "integrity": "sha512-8tWsULNEPHKQ2MR4zXuzSmqbdyV5PtwwCaWSGQ1WwHsJ07ilNeN1JB8ntxhckbnpSHaf9dXFUHzIWvm1I13dsg==", + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-done": "^1.2.0", - "chokidar": "^2.0.0", - "is-negated-glob": "^1.0.0", - "just-debounce": "^1.0.0", - "object.defaults": "^1.1.0" + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "node_modules/array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", "dev": true, - "requires": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" + "engines": { + "node": ">=0.10.0" } }, - "global-prefix": { + "node_modules/array-find-index": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" + "engines": { + "node": ">=0.10.0" } }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "glogg": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", - "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", + "node_modules/array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", "dev": true, - "requires": { - "sparkles": "^1.0.0" + "engines": { + "node": ">=0.10.0" } }, - "graceful-fs": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", - "dev": true + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true + "node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "gulp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", - "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, - "requires": { - "glob-watcher": "^5.0.3", - "gulp-cli": "^2.2.0", - "undertaker": "^1.2.1", - "vinyl-fs": "^3.0.0" - }, "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "gulp-cli": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.2.0.tgz", - "integrity": "sha512-rGs3bVYHdyJpLqR0TUBnlcZ1O5O++Zs4bA0ajm+zr3WFCfiSLjGwoCBqFs18wzN+ZxahT9DkOK5nDf26iDsWjA==", - "dev": true, - "requires": { - "ansi-colors": "^1.0.1", - "archy": "^1.0.0", - "array-sort": "^1.0.0", - "color-support": "^1.1.3", - "concat-stream": "^1.6.0", - "copy-props": "^2.0.1", - "fancy-log": "^1.3.2", - "gulplog": "^1.0.0", - "interpret": "^1.1.0", - "isobject": "^3.0.1", - "liftoff": "^3.1.0", - "matchdep": "^2.0.0", - "mute-stdout": "^1.0.0", - "pretty-hrtime": "^1.0.0", - "replace-homedir": "^1.0.0", - "semver-greatest-satisfied-range": "^1.1.0", - "v8flags": "^3.0.1", - "yargs": "^7.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - } - }, - "y18n": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", - "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", - "dev": true - }, - "yargs": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz", - "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==", - "dev": true, - "requires": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^5.0.1" - } - }, - "yargs-parser": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", - "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", - "dev": true, - "requires": { - "camelcase": "^3.0.0", - "object.assign": "^4.1.0" - } - } + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "gulp-clean-css": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/gulp-clean-css/-/gulp-clean-css-4.3.0.tgz", - "integrity": "sha512-mGyeT3qqFXTy61j0zOIciS4MkYziF2U594t2Vs9rUnpkEHqfu6aDITMp8xOvZcvdX61Uz3y1mVERRYmjzQF5fg==", + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", "dev": true, - "requires": { - "clean-css": "4.2.3", - "plugin-error": "1.0.1", - "through2": "3.0.1", - "vinyl-sourcemaps-apply": "0.2.1" - }, "dependencies": { - "through2": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", - "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", - "dev": true, - "requires": { - "readable-stream": "2 || 3" - } - } + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "gulp-esbuild": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/gulp-esbuild/-/gulp-esbuild-0.8.5.tgz", - "integrity": "sha512-t69FaHlVjxcV2NMZAvqlKqGwecrDuTWizMhkmoPfVLlCT1lMa4x7Oz99dIBjeRrzEQ8Y+uPk0kDb7AwWZ5bWAw==", + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", "dev": true, - "requires": { - "esbuild": "^0.12.18", - "plugin-error": "^1.0.1", - "vinyl": "^2.2.1" - }, - "dependencies": { - "vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "gulp-less": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/gulp-less/-/gulp-less-5.0.0.tgz", - "integrity": "sha512-W2I3TewO/By6UZsM/wJG3pyK5M6J0NYmJAAhwYXQHR+38S0iDtZasmUgFCH3CQj+pQYw/PAIzxvFvwtEXz1HhQ==", + "node_modules/array.prototype.toreversed": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", + "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", "dev": true, - "requires": { - "less": "^3.7.1 || ^4.0.0", - "object-assign": "^4.0.1", - "plugin-error": "^1.0.0", - "replace-ext": "^2.0.0", - "through2": "^4.0.0", - "vinyl-sourcemaps-apply": "^0.2.0" - }, "dependencies": { - "replace-ext": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", - "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", - "dev": true - } + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" } }, - "gulp-livereload": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/gulp-livereload/-/gulp-livereload-4.0.2.tgz", - "integrity": "sha512-InmaR50Xl1xB1WdEk4mrUgGHv3VhhlRLrx7u60iY5AAer90FlK95KXitPcGGQoi28zrUJM189d/h6+V470Ncgg==", + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, - "requires": { - "chalk": "^2.4.1", - "debug": "^3.1.0", - "fancy-log": "^1.3.2", - "lodash.assign": "^4.2.0", - "readable-stream": "^3.0.6", - "tiny-lr": "^1.1.1", - "vinyl": "^2.2.0" - }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" } }, - "gulp-notify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/gulp-notify/-/gulp-notify-4.0.0.tgz", - "integrity": "sha512-0cdDvZkHVqu4tqrcOI/jL5YdxYEIPQ7+p3YxnO48w5hhPSisvogZ887qL+fpYItg9m4MUhJ5Se8p8xGy3uJESA==", + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dev": true, - "requires": { - "ansi-colors": "^4.1.1", - "fancy-log": "^1.3.3", - "lodash.template": "^4.5.0", - "node-notifier": "^9.0.1", - "node.extend": "^2.0.2", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async-done": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-2.0.0.tgz", + "integrity": "sha512-j0s3bzYq9yKIVLKGE/tWlCpa3PfFLcrDZLTSVdnnCTGagXuXBJO4SsY9Xdk/fQBirCkH4evW5xOeJXqlAQFdsw==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.4.4", + "once": "^1.4.0", + "stream-exhaust": "^1.0.2" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/async-settle": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-2.0.0.tgz", + "integrity": "sha512-Obu/KE8FurfQRN6ODdHN9LuXqwC+JFIM9NRyZqJJ4ZfLJmIYN9Rg0/kb+wF70VV5+fJusTMQlJ1t5rF7J/ETdg==", + "dev": true, + "dependencies": { + "async-done": "^2.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true + }, + "node_modules/babel-jest": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", + "integrity": "sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA==", + "dev": true, + "dependencies": { + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/babel__core": "^7.1.7", + "babel-plugin-istanbul": "^6.0.0", + "babel-preset-jest": "^26.6.2", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "slash": "^3.0.0" + }, + "engines": { + "node": ">= 10.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz", + "integrity": "sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz", + "integrity": "sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^26.6.2", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": ">= 10.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/bach": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bach/-/bach-2.0.1.tgz", + "integrity": "sha512-A7bvGMGiTOxGMpNupYl9HQTf0FFDNF4VCmks4PJpFyN1AX2pdKuxuwdvUz2Hu388wcgp+OvGFNsumBfFNkR7eg==", + "dev": true, + "dependencies": { + "async-done": "^2.0.0", + "async-settle": "^2.0.0", + "now-and-later": "^3.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/bare-events": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", + "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", + "dev": true, + "optional": true + }, + "node_modules/base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/binaryextensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-2.3.0.tgz", + "integrity": "sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==", + "dev": true, + "engines": { + "node": ">=0.8" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", + "integrity": "sha512-chUsBxGRtuElD6fmw1gHLpvnKdVLK302peeFa9ZqAEk8TyzZ3fygLyUEDDPTJvL9+Bor0dIwn6ePOsRM2y0zQQ==", + "dev": true, + "dependencies": { + "continuable-cache": "^0.3.1", + "error": "^7.0.0", + "raw-body": "~1.1.0", + "safe-json-parse": "~1.0.1" + } + }, + "node_modules/bootstrap": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-3.4.1.tgz", + "integrity": "sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", + "integrity": "sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ==", + "dev": true + }, + "node_modules/cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha512-bA/Z/DERHKqoEOrp+qeGKw1QlvEQkGZSc0XaY6VnTxZr+Kv1G5zFwttpjv8qxZ/sBPT4nthwZaAcsAZTJlSKXQ==", + "dev": true, + "dependencies": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/camelcase-keys/node_modules/camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001640", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz", + "integrity": "sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/capture-exit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", + "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "dev": true, + "dependencies": { + "rsvp": "^4.8.4" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "dev": true + }, + "node_modules/class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "node_modules/clean-css": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", + "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", + "dev": true + }, + "node_modules/cloneable-readable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", + "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + } + }, + "node_modules/cloneable-readable/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/cloneable-readable/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/cloneable-readable/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/cloneable-readable/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/codemirror": { + "version": "5.65.16", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.16.tgz", + "integrity": "sha512-br21LjYmSlVL0vFCPWPfhzUCT34FM/pAdK7rRIZwa0rrtrIdotvP4Oh4GUHsu2E3IrQMCfRkL/fN3ytMNxVQvg==" + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "dev": true, + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/continuable-cache": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz", + "integrity": "sha512-TF30kpKhTH8AGCG3dut0rdd/19B7Z+qCnrMoBLpyQu/2drZdNrrpcjPEoJeSVsQM+8KmWG5O56oPDjSSUsuTyA==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/copy-props": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-4.0.0.tgz", + "integrity": "sha512-bVWtw1wQLzzKiYROtvNlbJgxgBYt2bMJpkCbKmXM3xyijvcjjWXEk5nyrrT3bgJ7ODb19ZohE2T0Y3FgNPyoTw==", + "dev": true, + "dependencies": { + "each-props": "^3.0.0", + "is-plain-object": "^5.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/create-jest/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-fetch": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "dev": true, + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "optional": true, + "peer": true + }, + "node_modules/currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==", + "dev": true, + "dependencies": { + "array-find-index": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "dev": true, + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dateformat": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", + "integrity": "sha512-5sFRfAAmbHdIts+eKjR9kYJoF0ViCMVX9yqLu5A7S/v+nd077KgCITOMiirmyCBiZpKLDXbBOkYm6tu7rX/TKg==", + "dev": true, + "dependencies": { + "get-stdin": "^4.0.1", + "meow": "^3.3.0" + }, + "bin": { + "dateformat": "bin/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug-fabulous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-1.1.0.tgz", + "integrity": "sha512-GZqvGIgKNlUnHUPQhepnUZFIMoi3dgZKQBzKDeL2g7oJF9SNAji/AAu36dusFUas0O+pae74lNeoIPHqXWDkLg==", + "dev": true, + "dependencies": { + "debug": "3.X", + "memoizee": "0.4.X", + "object-assign": "4.X" + } + }, + "node_modules/debug-fabulous/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/duplexer2": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", + "integrity": "sha512-+AWBwjGadtksxjOQSFDhPNQbed7icNXApT4+2BNpsXzcCBiInq2H9XW0O8sfHFaPmnQRs7cg/P0fAr2IWQSW0g==", + "dev": true, + "dependencies": { + "readable-stream": "~1.1.9" + } + }, + "node_modules/duplexer2/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true + }, + "node_modules/each-props": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-3.0.0.tgz", + "integrity": "sha512-IYf1hpuWrdzse/s/YJOrFmU15lyhSzxelNVAHTEG3DtP4QsLTWZUzcUL3HMXmKQxXpa4EIrBPpwRgj0aehdvAw==", + "dev": true, + "dependencies": { + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.820", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.820.tgz", + "integrity": "sha512-kK/4O/YunacfboFEk/BDf7VO1HoPmDudLTJAU9NmXIOSjsV7qVIX3OrI4REZo0VmdqhcpUcncQc6N8Q3aEXlHg==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encode-utf8": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/error/-/error-7.2.1.tgz", + "integrity": "sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==", + "dev": true, + "dependencies": { + "string-template": "~0.2.1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", + "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dev": true, + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "dev": true, + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/esbuild": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", + "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.0", + "@esbuild/android-arm": "0.23.0", + "@esbuild/android-arm64": "0.23.0", + "@esbuild/android-x64": "0.23.0", + "@esbuild/darwin-arm64": "0.23.0", + "@esbuild/darwin-x64": "0.23.0", + "@esbuild/freebsd-arm64": "0.23.0", + "@esbuild/freebsd-x64": "0.23.0", + "@esbuild/linux-arm": "0.23.0", + "@esbuild/linux-arm64": "0.23.0", + "@esbuild/linux-ia32": "0.23.0", + "@esbuild/linux-loong64": "0.23.0", + "@esbuild/linux-mips64el": "0.23.0", + "@esbuild/linux-ppc64": "0.23.0", + "@esbuild/linux-riscv64": "0.23.0", + "@esbuild/linux-s390x": "0.23.0", + "@esbuild/linux-x64": "0.23.0", + "@esbuild/netbsd-x64": "0.23.0", + "@esbuild/openbsd-arm64": "0.23.0", + "@esbuild/openbsd-x64": "0.23.0", + "@esbuild/sunos-x64": "0.23.0", + "@esbuild/win32-arm64": "0.23.0", + "@esbuild/win32-ia32": "0.23.0", + "@esbuild/win32-x64": "0.23.0" + } + }, + "node_modules/esbuild-jest": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/esbuild-jest/-/esbuild-jest-0.5.0.tgz", + "integrity": "sha512-AMZZCdEpXfNVOIDvURlqYyHwC8qC1/BFjgsrOiSL1eyiIArVtHL8YAC83Shhn16cYYoAWEW17yZn0W/RJKJKHQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.17", + "@babel/plugin-transform-modules-commonjs": "^7.12.13", + "babel-jest": "^26.6.3" + }, + "peerDependencies": { + "esbuild": ">=0.8.50" + } + }, + "node_modules/esbuild/node_modules/@esbuild/aix-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", + "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", + "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", + "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", + "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/darwin-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", + "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/darwin-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", + "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", + "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/freebsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", + "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", + "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", + "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", + "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-loong64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", + "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-mips64el": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", + "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", + "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-riscv64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", + "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-s390x": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", + "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", + "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/netbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", + "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/openbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", + "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/sunos-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", + "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", + "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", + "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", + "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.34.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.3.tgz", + "integrity": "sha512-aoW4MV891jkUulwDApQbPYTVZmeuSyFrudpbTAQuj5Fv8VL+o6df2xIGpw8B0hPjAaih1/Fb0om9grCdyFYemA==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.toreversed": "^1.1.2", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.19", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.hasown": "^1.1.4", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dev": true, + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/exec-sh": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz", + "integrity": "sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==", + "dev": true + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "dev": true, + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/expand-brackets/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/expect/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dev": true, + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fancy-log": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", + "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", + "dev": true, + "dependencies": { + "ansi-gray": "^0.1.1", + "color-support": "^1.1.3", + "parse-node-version": "^1.0.0", + "time-stamp": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha512-Xhj93RXbMSq8urNCUq4p9l0P6hnySJ/7YNRhYNug0bLOuii7pKO7xQFb5mx9xZXWCar88pLPb805PvUkwrLZpQ==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dev": true, + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/fined": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-2.0.0.tgz", + "integrity": "sha512-OFRzsL6ZMHz5s0JrsEr+TpdGNCtrVtnuG3x1yzGNiQHT0yaDnXAj8V/lWcpJVrnoDpcwXcASxAZYbuXda2Y82A==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0", + "object.pick": "^1.3.0", + "parse-filepath": "^1.0.2" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/flagged-respawn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-2.0.0.tgz", + "integrity": "sha512-Gq/a6YCi8zexmGHMuJwahTGzXlAZAOsbCVKduWXC6TlLCjjFRlExMJc4GC2NYPYZ0r/brw9P7CpRgQmlPVeOoA==", + "dev": true, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", + "dev": true, + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "dev": true, + "dependencies": { + "map-cache": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fs-mkdirp-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", + "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-stream": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.2.tgz", + "integrity": "sha512-R8z6eTB55t3QeZMmU1C+Gv+t5UnNRkA55c5yo67fAVfxODxieTwsjNG7utxS/73NdP1NbDgCrhVEg2h00y4fFw==", + "dev": true, + "dependencies": { + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "is-negated-glob": "^1.0.0", + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-watcher": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-6.0.0.tgz", + "integrity": "sha512-wGM28Ehmcnk2NqRORXFOTOR064L4imSw3EeOqU5bIwUf62eXGwg89WivH6VMahL8zlQHeodzvHpXplrqzrz3Nw==", + "dev": true, + "dependencies": { + "async-done": "^2.0.0", + "chokidar": "^3.5.3" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.8.0.tgz", + "integrity": "sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glogg": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-2.2.0.tgz", + "integrity": "sha512-eWv1ds/zAlz+M1ioHsyKJomfY7jbDDPpwSkv14KQj89bycx1nvK5/2Cj/T9g7kzJcX5Bc7Yv22FjfBZS/jl94A==", + "dev": true, + "dependencies": { + "sparkles": "^2.1.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==", + "dev": true + }, + "node_modules/gulp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-5.0.0.tgz", + "integrity": "sha512-S8Z8066SSileaYw1S2N1I64IUc/myI2bqe2ihOBzO6+nKpvNSg7ZcWJt/AwF8LC/NVN+/QZ560Cb/5OPsyhkhg==", + "dev": true, + "dependencies": { + "glob-watcher": "^6.0.0", + "gulp-cli": "^3.0.0", + "undertaker": "^2.0.0", + "vinyl-fs": "^4.0.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gulp-clean-css": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gulp-clean-css/-/gulp-clean-css-4.3.0.tgz", + "integrity": "sha512-mGyeT3qqFXTy61j0zOIciS4MkYziF2U594t2Vs9rUnpkEHqfu6aDITMp8xOvZcvdX61Uz3y1mVERRYmjzQF5fg==", + "dev": true, + "dependencies": { + "clean-css": "4.2.3", + "plugin-error": "1.0.1", + "through2": "3.0.1", + "vinyl-sourcemaps-apply": "0.2.1" + } + }, + "node_modules/gulp-clean-css/node_modules/through2": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", + "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", + "dev": true, + "dependencies": { + "readable-stream": "2 || 3" + } + }, + "node_modules/gulp-cli": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-3.0.0.tgz", + "integrity": "sha512-RtMIitkT8DEMZZygHK2vEuLPqLPAFB4sntSxg4NoDta7ciwGZ18l7JuhCTiS5deOJi2IoK0btE+hs6R4sfj7AA==", + "dev": true, + "dependencies": { + "@gulpjs/messages": "^1.1.0", + "chalk": "^4.1.2", + "copy-props": "^4.0.0", + "gulplog": "^2.2.0", + "interpret": "^3.1.1", + "liftoff": "^5.0.0", + "mute-stdout": "^2.0.0", + "replace-homedir": "^2.0.0", + "semver-greatest-satisfied-range": "^2.0.0", + "string-width": "^4.2.3", + "v8flags": "^4.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gulp-esbuild": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/gulp-esbuild/-/gulp-esbuild-0.12.1.tgz", + "integrity": "sha512-dkcN2AHtXTVu+KNw0Zw8SWysziNwpYg6kw41E8frUkil5ZtwktIsot/OCLEpRT6clFpVQ7Hw3+YZQvoNdyTF1A==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.5", + "plugin-error": "^2.0.1", + "vinyl": "^3.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/gulp-esbuild/node_modules/ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "dependencies": { + "ansi-wrap": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-esbuild/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/gulp-esbuild/node_modules/plugin-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-2.0.1.tgz", + "integrity": "sha512-zMakqvIDyY40xHOvzXka0kUvf40nYIuwRE8dWhti2WtjQZ31xAgBZBhxsK7vK3QbRXS1Xms/LO7B5cuAsfB2Gg==", + "dev": true, + "dependencies": { + "ansi-colors": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gulp-less": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gulp-less/-/gulp-less-5.0.0.tgz", + "integrity": "sha512-W2I3TewO/By6UZsM/wJG3pyK5M6J0NYmJAAhwYXQHR+38S0iDtZasmUgFCH3CQj+pQYw/PAIzxvFvwtEXz1HhQ==", + "dev": true, + "dependencies": { + "less": "^3.7.1 || ^4.0.0", + "object-assign": "^4.0.1", + "plugin-error": "^1.0.0", + "replace-ext": "^2.0.0", + "through2": "^4.0.0", + "vinyl-sourcemaps-apply": "^0.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/gulp-livereload": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/gulp-livereload/-/gulp-livereload-4.0.2.tgz", + "integrity": "sha512-InmaR50Xl1xB1WdEk4mrUgGHv3VhhlRLrx7u60iY5AAer90FlK95KXitPcGGQoi28zrUJM189d/h6+V470Ncgg==", + "dev": true, + "dependencies": { + "chalk": "^2.4.1", + "debug": "^3.1.0", + "fancy-log": "^1.3.2", + "lodash.assign": "^4.2.0", + "readable-stream": "^3.0.6", + "tiny-lr": "^1.1.1", + "vinyl": "^2.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/gulp-livereload/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/gulp-livereload/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/gulp-livereload/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/gulp-livereload/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/gulp-livereload/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/gulp-livereload/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/gulp-livereload/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/gulp-livereload/node_modules/replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-livereload/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/gulp-livereload/node_modules/vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "dev": true, + "dependencies": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-notify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/gulp-notify/-/gulp-notify-4.0.0.tgz", + "integrity": "sha512-0cdDvZkHVqu4tqrcOI/jL5YdxYEIPQ7+p3YxnO48w5hhPSisvogZ887qL+fpYItg9m4MUhJ5Se8p8xGy3uJESA==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "fancy-log": "^1.3.3", + "lodash.template": "^4.5.0", + "node-notifier": "^9.0.1", + "node.extend": "^2.0.2", "plugin-error": "^1.0.1", "through2": "^4.0.2" }, + "engines": { + "node": ">=0.8.0", + "npm": ">=1.2.10" + } + }, + "node_modules/gulp-peg": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/gulp-peg/-/gulp-peg-0.2.0.tgz", + "integrity": "sha512-A8SWTea7/tj/6/mdfHI92C2ATdABLs9arbRi8a/aZLhQ9ZK70S73udo0qv6OjgxWZoQqEjwrPmbJaQzym/yHfg==", + "dev": true, + "dependencies": { + "gulp-util": "^2.2.14", + "pegjs": "^0.9.0", + "through2": "^0.4.1" + } + }, + "node_modules/gulp-peg/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/gulp-peg/node_modules/object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==", + "dev": true + }, + "node_modules/gulp-peg/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/gulp-peg/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true + }, + "node_modules/gulp-peg/node_modules/through2": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", + "integrity": "sha512-45Llu+EwHKtAZYTPPVn3XZHBgakWMN3rokhEv5hu596XP+cNgplMg+Gj+1nmAvj+L0K7+N49zBKx5rah5u0QIQ==", + "dev": true, + "dependencies": { + "readable-stream": "~1.0.17", + "xtend": "~2.1.1" + } + }, + "node_modules/gulp-peg/node_modules/xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ==", + "dev": true, + "dependencies": { + "object-keys": "~0.4.0" + }, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/gulp-plumber": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/gulp-plumber/-/gulp-plumber-1.2.1.tgz", + "integrity": "sha512-mctAi9msEAG7XzW5ytDVZ9PxWMzzi1pS2rBH7lA095DhMa6KEXjm+St0GOCc567pJKJ/oCvosVAZEpAey0q2eQ==", + "dev": true, + "dependencies": { + "chalk": "^1.1.3", + "fancy-log": "^1.3.2", + "plugin-error": "^0.1.2", + "through2": "^2.0.3" + }, + "engines": { + "node": ">=0.10", + "npm": ">=1.2.10" + } + }, + "node_modules/gulp-plumber/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-plumber/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-plumber/node_modules/arr-diff": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", + "integrity": "sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.0.1", + "array-slice": "^0.2.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-plumber/node_modules/arr-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", + "integrity": "sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-plumber/node_modules/array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-plumber/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-plumber/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/gulp-plumber/node_modules/extend-shallow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", + "integrity": "sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==", + "dev": true, + "dependencies": { + "kind-of": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-plumber/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/gulp-plumber/node_modules/plugin-error": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", + "integrity": "sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==", + "dev": true, + "dependencies": { + "ansi-cyan": "^0.1.1", + "ansi-red": "^0.1.1", + "arr-diff": "^1.0.1", + "arr-union": "^2.0.1", + "extend-shallow": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-plumber/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/gulp-plumber/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/gulp-plumber/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/gulp-plumber/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-plumber/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/gulp-plumber/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/gulp-replace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-1.1.4.tgz", + "integrity": "sha512-SVSF7ikuWKhpAW4l4wapAqPPSToJoiNKsbDoUnRrSgwZHH7lH8pbPeQj1aOVYQrbZKhfSVBxVW+Py7vtulRktw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/vinyl": "^2.0.4", + "istextorbinary": "^3.0.0", + "replacestream": "^4.0.3", + "yargs-parser": ">=5.0.0-security.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gulp-sourcemaps": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-3.0.0.tgz", + "integrity": "sha512-RqvUckJkuYqy4VaIH60RMal4ZtG0IbQ6PXMNkNsshEGJ9cldUPRb/YCgboYae+CLAs1HQNb4ADTKCx65HInquQ==", + "dev": true, + "dependencies": { + "@gulp-sourcemaps/identity-map": "^2.0.1", + "@gulp-sourcemaps/map-sources": "^1.0.0", + "acorn": "^6.4.1", + "convert-source-map": "^1.0.0", + "css": "^3.0.0", + "debug-fabulous": "^1.0.0", + "detect-newline": "^2.0.0", + "graceful-fs": "^4.0.0", + "source-map": "^0.6.0", + "strip-bom-string": "^1.0.0", + "through2": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gulp-sourcemaps/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/gulp-sourcemaps/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/gulp-sourcemaps/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/gulp-sourcemaps/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/gulp-sourcemaps/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/gulp-sourcemaps/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/gulp-sourcemaps/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/gulp-util": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-2.2.20.tgz", + "integrity": "sha512-9rtv4sj9EtCWYGD15HQQvWtRBtU9g1t0+w29tphetHxjxEAuBKQJkhGqvlLkHEtUjEgoqIpsVwPKU1yMZAa+wA==", + "deprecated": "gulp-util is deprecated - replace it, following the guidelines at https://medium.com/gulpjs/gulp-util-ca3b1f9f9ac5", + "dev": true, + "dependencies": { + "chalk": "^0.5.0", + "dateformat": "^1.0.7-1.2.3", + "lodash._reinterpolate": "^2.4.1", + "lodash.template": "^2.4.1", + "minimist": "^0.2.0", + "multipipe": "^0.1.0", + "through2": "^0.5.0", + "vinyl": "^0.2.1" + }, + "engines": { + "node": ">= 0.9" + } + }, + "node_modules/gulp-util/node_modules/ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha512-sGwIGMjhYdW26/IhwK2gkWWI8DRCVO6uj3hYgHT+zD+QL1pa37tM3ujhyfcJIYSbsxp7Gxhy7zrRW/1AHm4BmA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-util/node_modules/ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha512-f2PKUkN5QngiSemowa6Mrk9MPCdtFiOSmibjZ+j1qhLGHHYsqZwmBMRF3IRMVXo8sybDqx2fJl2d/8OphBoWkA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-util/node_modules/chalk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha512-bIKA54hP8iZhyDT81TOsJiQvR1gW+ZYSXFaZUAvoD4wCHdbHY2actmpTE4x344ZlFqHbvoxKOaESULTZN2gstg==", + "dev": true, + "dependencies": { + "ansi-styles": "^1.1.0", + "escape-string-regexp": "^1.0.0", + "has-ansi": "^0.1.0", + "strip-ansi": "^0.3.0", + "supports-color": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-util/node_modules/clone-stats": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", + "integrity": "sha512-dhUqc57gSMCo6TX85FLfe51eC/s+Im2MLkAgJwfaRRexR2tA4dd3eLEW4L6efzHc2iNorrRRXITifnDLlRrhaA==", + "dev": true + }, + "node_modules/gulp-util/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/gulp-util/node_modules/has-ansi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha512-1YsTg1fk2/6JToQhtZkArMkurq8UoWU1Qe0aR3VUHjgij4nOylSWLWAtBXoZ4/dXOmugfLGm1c+QhuD0JyedFA==", + "dev": true, + "dependencies": { + "ansi-regex": "^0.2.0" + }, + "bin": { + "has-ansi": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-util/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/gulp-util/node_modules/lodash.template": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-2.4.1.tgz", + "integrity": "sha512-5yLOQwlS69xbaez3g9dA1i0GMAj8pLDHp8lhA4V7M1vRam1lqD76f0jg5EV+65frbqrXo1WH9ZfKalfYBzJ5yQ==", + "dev": true, + "dependencies": { + "lodash._escapestringchar": "~2.4.1", + "lodash._reinterpolate": "~2.4.1", + "lodash.defaults": "~2.4.1", + "lodash.escape": "~2.4.1", + "lodash.keys": "~2.4.1", + "lodash.templatesettings": "~2.4.1", + "lodash.values": "~2.4.1" + } + }, + "node_modules/gulp-util/node_modules/lodash.templatesettings": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-2.4.1.tgz", + "integrity": "sha512-vY3QQ7GxbeLe8XfTvoYDbaMHO5iyTDJS1KIZrxp00PRMmyBKr8yEcObHSl2ppYTwd8MgqPXAarTvLA14hx8ffw==", + "dev": true, + "dependencies": { + "lodash._reinterpolate": "~2.4.1", + "lodash.escape": "~2.4.1" + } + }, + "node_modules/gulp-util/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/gulp-util/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true + }, + "node_modules/gulp-util/node_modules/strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha512-DerhZL7j6i6/nEnVG0qViKXI0OKouvvpsAiaj7c+LfqZZZxdwZtv8+UiA/w4VUJpT8UzX0pR1dcHOii1GbmruQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^0.2.1" + }, + "bin": { + "strip-ansi": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-util/node_modules/supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha512-tdCZ28MnM7k7cJDJc7Eq80A9CsRFAAOZUy41npOZCs++qSjfIy7o5Rh46CBk+Dk5FbKJ33X3Tqg4YrV07N5RaA==", + "dev": true, + "bin": { + "supports-color": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-util/node_modules/through2": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.5.1.tgz", + "integrity": "sha512-zexCrAOTbjkBCXGyozn7hhS3aEaqdrc59mAD2E3dKYzV1vFuEGQ1hEDJN2oQMQFwy4he2zyLqPZV+AlfS8ZWJA==", + "dev": true, + "dependencies": { + "readable-stream": "~1.0.17", + "xtend": "~3.0.0" + } + }, + "node_modules/gulp-util/node_modules/vinyl": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.2.3.tgz", + "integrity": "sha512-4gFk9xrecazOTuFKcUYrE1TjHSYL63dio72D+q0d1mHF51FEcxTT2RHFpHbN5TNJgmPYHuVsBdhvXEOCDcytSA==", + "dev": true, + "dependencies": { + "clone-stats": "~0.0.1" + }, + "engines": { + "node": ">= 0.9" + } + }, + "node_modules/gulp-util/node_modules/xtend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", + "integrity": "sha512-sp/sT9OALMjRW1fKDlPeuSZlDQpkqReA0pyJukniWbTGoEKefHxhGJynE3PNhUMlcM8qWIjPwecwCw4LArS5Eg==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/gulplog": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-2.2.0.tgz", + "integrity": "sha512-V2FaKiOhpR3DRXZuYdRLn/qiY0yI5XmqbTKrYbdemJ+xOh2d2MOweI/XFgMzd/9+1twdvMwllnZbWZNJ+BOm4A==", + "dev": true, + "dependencies": { + "glogg": "^2.2.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "dev": true, + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", + "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-accessor-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", + "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-ci/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", + "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", + "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-descriptor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extendable/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "dev": true + }, + "node_modules/is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istextorbinary": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-3.3.0.tgz", + "integrity": "sha512-Tvq1W6NAcZeJ8op+Hq7tdZ434rqnMx4CCZ7H0ff83uEloDvVbqAwaMTZcafKGJT0VHkYzuXUiCY4hlXQg6WfoQ==", + "dev": true, + "dependencies": { + "binaryextensions": "^2.2.0", + "textextensions": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-changed-files/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-cli/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/jest-config/node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/jest-config/node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-config/node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-config/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock/node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-environment-node/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "dependencies": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", + "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", + "dev": true, + "dependencies": { + "@jest/types": "^26.6.2", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-regex-util": "^26.0.0", + "jest-serializer": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7" + }, + "engines": { + "node": ">= 10.14.2" + }, + "optionalDependencies": { + "fsevents": "^2.1.2" + } + }, + "node_modules/jest-haste-map/node_modules/jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-mock/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", + "dev": true, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-resolve/node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-resolve/node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-runner/node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-runner/node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-runtime/node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-runtime/node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/jest-serializer": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", + "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==", + "dev": true, + "dependencies": { + "@types/node": "*", + "graceful-fs": "^4.2.4" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-snapshot/node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "dependencies": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-watcher/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, "dependencies": { - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - } + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "gulp-peg": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/gulp-peg/-/gulp-peg-0.2.0.tgz", - "integrity": "sha1-aap3iezv+ajBA94ghYt8t4jXlSY=", + "node_modules/jest-worker/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, - "requires": { - "gulp-util": "^2.2.14", - "pegjs": "^0.9.0", - "through2": "^0.4.1" + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "object-keys": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", - "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", - "dev": true - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "through2": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", - "integrity": "sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s=", - "dev": true, - "requires": { - "readable-stream": "~1.0.17", - "xtend": "~2.1.1" - } - }, - "xtend": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", - "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", - "dev": true, - "requires": { - "object-keys": "~0.4.0" - } - } + "@types/yargs-parser": "*" } }, - "gulp-plumber": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/gulp-plumber/-/gulp-plumber-1.2.1.tgz", - "integrity": "sha512-mctAi9msEAG7XzW5ytDVZ9PxWMzzi1pS2rBH7lA095DhMa6KEXjm+St0GOCc567pJKJ/oCvosVAZEpAey0q2eQ==", + "node_modules/jest-worker/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, - "requires": { - "chalk": "^1.1.3", - "fancy-log": "^1.3.2", - "plugin-error": "^0.1.2", - "through2": "^2.0.3" + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "arr-diff": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", - "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=", - "dev": true, - "requires": { - "arr-flatten": "^1.0.1", - "array-slice": "^0.2.3" - } - }, - "arr-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", - "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=", - "dev": true - }, - "array-slice": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", - "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "dependencies": { - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "extend-shallow": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", - "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=", - "dev": true, - "requires": { - "kind-of": "^1.1.0" - } - }, - "kind-of": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", - "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", - "dev": true - }, - "plugin-error": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", - "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=", - "dev": true, - "requires": { - "ansi-cyan": "^0.1.1", - "ansi-red": "^0.1.1", - "arr-diff": "^1.0.1", - "arr-union": "^2.0.1", - "extend-shallow": "^1.1.2" - }, - "dependencies": { - "ansi-cyan": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", - "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=", - "dev": true, - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-red": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", - "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", - "dev": true, - "requires": { - "ansi-wrap": "0.1.0" - } - } - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", - "dev": true, - "requires": { - "readable-stream": "^2.1.5", - "xtend": "~4.0.1" - } - } + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "gulp-replace": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-1.1.3.tgz", - "integrity": "sha512-HcPHpWY4XdF8zxYkDODHnG2+7a3nD/Y8Mfu3aBgMiCFDW3X2GiOKXllsAmILcxe3KZT2BXoN18WrpEFm48KfLQ==", + "node_modules/jest/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, - "requires": { - "@types/node": "^14.14.41", - "@types/vinyl": "^2.0.4", - "istextorbinary": "^3.0.0", - "replacestream": "^4.0.3", - "yargs-parser": ">=5.0.0-security.0" + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, "dependencies": { - "@types/node": { - "version": "14.17.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.3.tgz", - "integrity": "sha512-e6ZowgGJmTuXa3GyaPbTGxX17tnThl2aSSizrFthQ7m9uLGZBXiGhgE55cjRZTF5kjZvYn9EOPOMljdjwbflxw==", - "dev": true - } + "@types/yargs-parser": "*" } }, - "gulp-sourcemaps": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-3.0.0.tgz", - "integrity": "sha512-RqvUckJkuYqy4VaIH60RMal4ZtG0IbQ6PXMNkNsshEGJ9cldUPRb/YCgboYae+CLAs1HQNb4ADTKCx65HInquQ==", + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, - "requires": { - "@gulp-sourcemaps/identity-map": "^2.0.1", - "@gulp-sourcemaps/map-sources": "^1.0.0", - "acorn": "^6.4.1", - "convert-source-map": "^1.0.0", - "css": "^3.0.0", - "debug-fabulous": "^1.0.0", - "detect-newline": "^2.0.0", - "graceful-fs": "^4.0.0", - "source-map": "^0.6.0", - "strip-bom-string": "^1.0.0", - "through2": "^2.0.0" + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, "dependencies": { - "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", - "dev": true - }, - "through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", - "dev": true, - "requires": { - "readable-stream": "^2.1.5", - "xtend": "~4.0.1" - } + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true } } }, - "gulp-util": { - "version": "2.2.20", - "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-2.2.20.tgz", - "integrity": "sha1-1xRuVyiRC9jwR6awseVJvCLb1kw=", + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true, - "requires": { - "chalk": "^0.5.0", - "dateformat": "^1.0.7-1.2.3", - "lodash._reinterpolate": "^2.4.1", - "lodash.template": "^2.4.1", - "minimist": "^0.2.0", - "multipipe": "^0.1.0", - "through2": "^0.5.0", - "vinyl": "^0.2.1" + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, "dependencies": { - "ansi-regex": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", - "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", - "dev": true - }, - "ansi-styles": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", - "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", - "dev": true - }, - "chalk": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", - "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", - "dev": true, - "requires": { - "ansi-styles": "^1.1.0", - "escape-string-regexp": "^1.0.0", - "has-ansi": "^0.1.0", - "strip-ansi": "^0.3.0", - "supports-color": "^0.2.0" - } - }, - "clone-stats": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", - "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", - "dev": true - }, - "has-ansi": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", - "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", - "dev": true, - "requires": { - "ansi-regex": "^0.2.0" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "lodash.defaults": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-2.4.1.tgz", - "integrity": "sha1-p+iIXwXmiFEUS24SqPNngCa8TFQ=", - "dev": true, - "requires": { - "lodash._objecttypes": "~2.4.1", - "lodash.keys": "~2.4.1" - } - }, - "lodash.template": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-2.4.1.tgz", - "integrity": "sha1-nmEQB+32KRKal0qzxIuBez4c8g0=", - "dev": true, - "requires": { - "lodash._escapestringchar": "~2.4.1", - "lodash._reinterpolate": "~2.4.1", - "lodash.defaults": "~2.4.1", - "lodash.escape": "~2.4.1", - "lodash.keys": "~2.4.1", - "lodash.templatesettings": "~2.4.1", - "lodash.values": "~2.4.1" - } - }, - "lodash.templatesettings": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-2.4.1.tgz", - "integrity": "sha1-6nbHXRHrhtTb6JqDiTu4YZKaxpk=", - "dev": true, - "requires": { - "lodash._reinterpolate": "~2.4.1", - "lodash.escape": "~2.4.1" - } - }, - "minimist": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.2.1.tgz", - "integrity": "sha512-GY8fANSrTMfBVfInqJAY41QkOM+upUTytK1jZ0c8+3HdHrJxBJ3rF5i9moClXTE8uUSnUo8cAsCoxDXvSY4DHg==", - "dev": true - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "strip-ansi": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", - "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", - "dev": true, - "requires": { - "ansi-regex": "^0.2.1" - } - }, - "supports-color": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", - "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", - "dev": true - }, - "through2": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.5.1.tgz", - "integrity": "sha1-390BLrnHAOIyP9M084rGIqs3Lac=", - "dev": true, - "requires": { - "readable-stream": "~1.0.17", - "xtend": "~3.0.0" - } - }, - "vinyl": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.2.3.tgz", - "integrity": "sha1-vKk4IJWC7FpJrVOKAPofEl5RMlI=", - "dev": true, - "requires": { - "clone-stats": "~0.0.1" - } - }, - "xtend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", - "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=", - "dev": true - } + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" } }, - "gulplog": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", - "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, - "requires": { - "glogg": "^1.0.0" + "dependencies": { + "json-buffer": "3.0.1" } }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/kind-of": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "integrity": "sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/last-run": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-2.0.0.tgz", + "integrity": "sha512-j+y6WhTLN4Itnf9j5ZQos1BGPCS8DAwmgMroR3OzfxAsBxam0hMw7J8M3KqZl0pLQJ1jNnwIexg5DYpC/ctwEQ==", "dev": true, - "requires": { - "function-bind": "^1.1.1" + "engines": { + "node": ">= 10.13.0" } }, - "has-flag": { + "node_modules/lead": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/less": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", + "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "dev": true, + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/liftoff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-5.0.0.tgz", + "integrity": "sha512-a5BQjbCHnB+cy+gsro8lXJ4kZluzOijzJ1UVVfyJYZC+IP2pLv1h4+aysQeKuTmyO8NAqfyQAk4HWaP/HjcKTg==", + "dev": true, + "dependencies": { + "extend": "^3.0.2", + "findup-sync": "^5.0.0", + "fined": "^2.0.0", + "flagged-respawn": "^2.0.0", + "is-plain-object": "^5.0.0", + "rechoir": "^0.8.0", + "resolve": "^1.20.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/liftoff/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "node_modules/livereload-js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz", + "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==", "dev": true }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "node_modules/load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "node_modules/load-json-file/node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "requires": { - "react-is": "^16.7.0" - }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dependencies": { - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - } + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" } }, - "homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash._escapehtmlchar": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._escapehtmlchar/-/lodash._escapehtmlchar-2.4.1.tgz", + "integrity": "sha512-eHm2t2Lg476lq5v4FVmm3B5mCaRlDyTE8fnMfPCEq2o46G4au0qNXIKh7YWhjprm1zgSMLcMSs1XHMgkw02PbQ==", "dev": true, - "requires": { - "parse-passwd": "^1.0.0" + "dependencies": { + "lodash._htmlescapes": "~2.4.1" } }, - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "node_modules/lodash._escapestringchar": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._escapestringchar/-/lodash._escapestringchar-2.4.1.tgz", + "integrity": "sha512-iZ6Os4iipaE43pr9SBks+UpZgAjJgRC+lGf7onEoByMr1+Nagr1fmR7zCM6Q4RGMB/V3a57raEN0XZl7Uub3/g==", "dev": true }, - "html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.5" - } + "node_modules/lodash._htmlescapes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._htmlescapes/-/lodash._htmlescapes-2.4.1.tgz", + "integrity": "sha512-g79hNmMOBVyV+4oKIHM7MWy9Awtk3yqf0Twlawr6f+CmG44nTwBh9I5XiLUnk39KTfYoDBpS66glQGgQCnFIuA==", + "dev": true }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "node_modules/lodash._isnative": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._isnative/-/lodash._isnative-2.4.1.tgz", + "integrity": "sha512-BOlKGKNHhCHswGOWtmVb5zBygyxN7EmTuzVOSQI6QSoGhG+kvv71gICFS1TBpnqvT1n53txK8CDK3u5D2/GZxQ==", "dev": true }, - "http-parser-js": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", - "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==", + "node_modules/lodash._objecttypes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz", + "integrity": "sha512-XpqGh1e7hhkOzftBfWE7zt+Yn9mVHFkDhicVttvKLsoCMLVVL+xTQjfjB4X4vtznauxv0QZ5ZAeqjvat0dh62Q==", "dev": true }, - "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "node_modules/lodash._reinterpolate": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-2.4.1.tgz", + "integrity": "sha512-QGEOOjJi7W9LIgDAMVgtGBb8Qgo8ieDlSOCoZjtG45ZNRvDJZjwVMTYlfTIWdNRUiR1I9BjIqQ3Zaf1+DYM94g==", + "dev": true + }, + "node_modules/lodash._reunescapedhtml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._reunescapedhtml/-/lodash._reunescapedhtml-2.4.1.tgz", + "integrity": "sha512-CfmZRU1Mk4E/5jh+Wu8lc7tuc3VkuwWZYVIgdPDH9NRSHgiL4Or3AA4JCIpgrkVzHOM+jKu2OMkAVquruhRHDQ==", "dev": true, - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" + "dependencies": { + "lodash._htmlescapes": "~2.4.1", + "lodash.keys": "~2.4.1" } }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "node_modules/lodash._shimkeys": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._shimkeys/-/lodash._shimkeys-2.4.1.tgz", + "integrity": "sha512-lBrglYxLD/6KAJ8IEa5Lg+YHgNAL7FyKqXg4XOUI+Du/vtniLs1ZqS+yHNKPkK54waAgkdUnDOYaWf+rv4B+AA==", "dev": true, - "requires": { - "agent-base": "6", - "debug": "4" + "dependencies": { + "lodash._objecttypes": "~2.4.1" } }, - "human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "node_modules/lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==", "dev": true }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "node_modules/lodash.defaults": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-2.4.1.tgz", + "integrity": "sha512-5wTIPWwGGr07JFysAZB8+7JB2NjJKXDIwogSaRX5zED85zyUAQwtOqUk8AsJkkigUcL3akbHYXd5+BPtTGQPZw==", "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" + "dependencies": { + "lodash._objecttypes": "~2.4.1", + "lodash.keys": "~2.4.1" } }, - "image-size": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", - "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "node_modules/lodash.escape": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-2.4.1.tgz", + "integrity": "sha512-PiEStyvZ8gz37qBE+HqME1Yc/ewb/59AMOu8pG7Ztani86foPTxgzckQvMdphmXPY6V5f20Ex/CaNBqHG4/ycQ==", "dev": true, - "optional": true + "dependencies": { + "lodash._escapehtmlchar": "~2.4.1", + "lodash._reunescapedhtml": "~2.4.1", + "lodash.keys": "~2.4.1" + } }, - "import-local": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", - "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", + "node_modules/lodash.isobject": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", + "integrity": "sha512-sTebg2a1PoicYEZXD5PBdQcTlIJ6hUslrlWr7iV0O7n+i4596s2NQ9I5CaZ5FbXSfya/9WQsrYLANUJv9paYVA==", "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" + "dependencies": { + "lodash._objecttypes": "~2.4.1" } }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "dev": true }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "node_modules/lodash.keys": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz", + "integrity": "sha512-ZpJhwvUXHSNL5wYd1RM6CUa2ZuqorG9ngoJ9Ix5Cce+uX7I5O/E06FCJdhSZ33b5dVyeQDnIlWH7B2s5uByZ7g==", "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" + "dependencies": { + "lodash._isnative": "~2.4.1", + "lodash._shimkeys": "~2.4.1", + "lodash.isobject": "~2.4.1" } }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true + "node_modules/lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "dev": true, + "dependencies": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } }, - "interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "node_modules/lodash.template/node_modules/lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", "dev": true }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true + "node_modules/lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "dev": true, + "dependencies": { + "lodash._reinterpolate": "^3.0.0" + } }, - "is": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", - "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==", + "node_modules/lodash.templatesettings/node_modules/lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", "dev": true }, - "is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "node_modules/lodash.values": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.values/-/lodash.values-2.4.1.tgz", + "integrity": "sha512-fQwubKvj2Nox2gy6YnjFm8C1I6MIlzKUtBB+Pj7JGtloGqDDL5CPRr4DUUFWPwXWwAl2k3f4C3Aw8H1qAPB9ww==", "dev": true, - "requires": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" + "dependencies": { + "lodash.keys": "~2.4.1" } }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dependencies": { - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" } }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "node_modules/loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==", "dev": true, - "requires": { - "binary-extensions": "^1.0.0" + "dependencies": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, - "requires": { - "builtin-modules": "^1.0.0" + "dependencies": { + "yallist": "^3.0.2" } }, - "is-ci": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.0.tgz", - "integrity": "sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ==", + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", "dev": true, - "requires": { - "ci-info": "^3.1.1" + "dependencies": { + "es5-ext": "~0.10.2" } }, - "is-core-module": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", - "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, - "requires": { - "has": "^1.0.3" + "bin": { + "lz-string": "bin/bin.js" } }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, - "requires": { - "kind-of": "^6.0.0" - }, "dependencies": { - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "bin": { + "semver": "bin/semver.js" }, - "dependencies": { - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } + "engines": { + "node": ">=10" } }, - "is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-finite": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } }, - "is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", "dev": true, - "requires": { - "is-extglob": "^2.1.1" + "engines": { + "node": ">=0.10.0" } }, - "is-negated-glob": { + "node_modules/map-visit": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", - "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=", - "dev": true + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "dev": true, + "dependencies": { + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", "dev": true, - "requires": { - "kind-of": "^3.0.2" + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha512-TNdwZs0skRlpPpCUK25StC4VH+tP5GgeY1HQOOGP+lQ2xtdkN2VtT/5tiX9k3IWpkBPV9b3LsAWXn4GGi/PrSA==", + "dev": true, + "dependencies": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "node_modules/meow/node_modules/indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha512-aqwDFWSgSgfRaEwao5lg5KEcVd/2a+D1rvoG7NdilmYz0NwRk6StWpWdz/Hpk34MKPpx7s8XxUqimfcQK6gGlg==", "dev": true, - "requires": { - "isobject": "^3.0.1" + "dependencies": { + "repeating": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true - }, - "is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true - }, - "is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "node_modules/meow/node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "requires": { - "is-unc-path": "^1.0.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-unc-path": { + "node_modules/meow/node_modules/redent": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha512-qtW5hKzGQZqKoh6JNSD+4lfitfPKGz42e6QwiRmPM5mmKtR0N41AbJRYu0xJi7nhOJ4WDgRkKvAk6tw4WIwR4g==", "dev": true, - "requires": { - "unc-path-regex": "^0.1.2" + "dependencies": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "is-valid-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", - "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=", - "dev": true - }, - "is-what": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", - "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "node_modules/meow/node_modules/strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha512-I5iQq6aFMM62fBEAIB/hXzwJD6EEZ0xEGCX2t7oXqaKPIRgt4WruAQ285BISgdkP+HLGWyeGmNJcpIwFeRYRUA==", "dev": true, - "requires": { - "is-docker": "^2.0.0" + "dependencies": { + "get-stdin": "^4.0.1" + }, + "bin": { + "strip-indent": "cli.js" + }, + "engines": { + "node": ">=0.10.0" } }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isexe": { + "node_modules/merge-stream": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, - "istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" + "engines": { + "node": ">= 8" } }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" } }, - "istanbul-lib-source-maps": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", - "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" } }, - "istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" + "engines": { + "node": ">= 0.6" } }, - "istextorbinary": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-3.3.0.tgz", - "integrity": "sha512-Tvq1W6NAcZeJ8op+Hq7tdZ434rqnMx4CCZ7H0ff83uEloDvVbqAwaMTZcafKGJT0VHkYzuXUiCY4hlXQg6WfoQ==", + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, - "requires": { - "binaryextensions": "^2.2.0", - "textextensions": "^3.2.0" + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" } }, - "jest": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest/-/jest-27.0.6.tgz", - "integrity": "sha512-EjV8aETrsD0wHl7CKMibKwQNQc3gIRBXlTikBmmHUeVMKaPFxdcUIBfoDqTSXDoGJIivAYGqCWVlzCSaVjPQsA==", + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, - "requires": { - "@jest/core": "^27.0.6", - "import-local": "^3.0.2", - "jest-cli": "^27.0.6" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "jest-cli": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.0.6.tgz", - "integrity": "sha512-qUUVlGb9fdKir3RDE+B10ULI+LQrz+MCflEH2UJyoUjoHHCbxDrMxSzjQAPUMsic4SncI62ofYCcAvW6+6rhhg==", - "dev": true, - "requires": { - "@jest/core": "^27.0.6", - "@jest/test-result": "^27.0.6", - "@jest/types": "^27.0.6", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "import-local": "^3.0.2", - "jest-config": "^27.0.6", - "jest-util": "^27.0.6", - "jest-validate": "^27.0.6", - "prompts": "^2.0.1", - "yargs": "^16.0.3" - } - } + "engines": { + "node": ">=6" } }, - "jest-changed-files": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.0.6.tgz", - "integrity": "sha512-BuL/ZDauaq5dumYh5y20sn4IISnf1P9A0TDswTxUi84ORGtVa86ApuBHqICL0vepqAnZiY6a7xeSPWv2/yy4eA==", + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "execa": "^5.0.0", - "throat": "^6.0.1" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - } + "engines": { + "node": ">=4" } }, - "jest-circus": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.0.6.tgz", - "integrity": "sha512-OJlsz6BBeX9qR+7O9lXefWoc2m9ZqcZ5Ohlzz0pTEAG4xMiZUJoacY8f4YDHxgk0oKYxj277AfOk9w6hZYvi1Q==", + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "requires": { - "@jest/environment": "^27.0.6", - "@jest/test-result": "^27.0.6", - "@jest/types": "^27.0.6", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "expect": "^27.0.6", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.0.6", - "jest-matcher-utils": "^27.0.6", - "jest-message-util": "^27.0.6", - "jest-runtime": "^27.0.6", - "jest-snapshot": "^27.0.6", - "jest-util": "^27.0.6", - "pretty-format": "^27.0.6", - "slash": "^3.0.0", - "stack-utils": "^2.0.3", - "throat": "^6.0.1" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "pretty-format": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.0.6.tgz", - "integrity": "sha512-8tGD7gBIENgzqA+UBzObyWqQ5B778VIFZA/S66cclyd5YkFLYs2Js7gxDKf0MXtTc9zcS7t1xhdfcElJ3YIvkQ==", - "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "ansi-regex": "^5.0.0", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - } - } + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "jest-config": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.0.6.tgz", - "integrity": "sha512-JZRR3I1Plr2YxPBhgqRspDE2S5zprbga3swYNrvY3HfQGu7p/GjyLOqwrYad97tX3U3mzT53TPHVmozacfP/3w==", + "node_modules/minimist": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.2.4.tgz", + "integrity": "sha512-Pkrrm8NjyQ8yVt8Am9M+yUt74zE3iokhzbG1bFVNjLB92vwM71hf40RkEsryg98BujhVOncKm/C1xROxZ030LQ==", "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/test-sequencer": "^27.0.6", - "@jest/types": "^27.0.6", - "babel-jest": "^27.0.6", - "chalk": "^4.0.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "graceful-fs": "^4.2.4", - "is-ci": "^3.0.0", - "jest-circus": "^27.0.6", - "jest-environment-jsdom": "^27.0.6", - "jest-environment-node": "^27.0.6", - "jest-get-type": "^27.0.6", - "jest-jasmine2": "^27.0.6", - "jest-regex-util": "^27.0.6", - "jest-resolve": "^27.0.6", - "jest-runner": "^27.0.6", - "jest-util": "^27.0.6", - "jest-validate": "^27.0.6", - "micromatch": "^4.0.4", - "pretty-format": "^27.0.6" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "babel-jest": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.0.6.tgz", - "integrity": "sha512-iTJyYLNc4wRofASmofpOc5NK9QunwMk+TLFgGXsTFS8uEqmd8wdI7sga0FPe2oVH3b5Agt/EAK1QjPEuKL8VfA==", - "dev": true, - "requires": { - "@jest/transform": "^27.0.6", - "@jest/types": "^27.0.6", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.0.0", - "babel-preset-jest": "^27.0.6", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "slash": "^3.0.0" - } - }, - "babel-plugin-jest-hoist": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.0.6.tgz", - "integrity": "sha512-CewFeM9Vv2gM7Yr9n5eyyLVPRSiBnk6lKZRjgwYnGKSl9M14TMn2vkN02wTF04OGuSDLEzlWiMzvjXuW9mB6Gw==", - "dev": true, - "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.0.0", - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-preset-jest": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.0.6.tgz", - "integrity": "sha512-WObA0/Biw2LrVVwZkF/2GqbOdzhKD6Fkdwhoy9ASIrOWr/zodcSpQh72JOkEn6NWyjmnPDjNSqaGN4KnpKzhXw==", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^27.0.6", - "babel-preset-current-node-syntax": "^1.0.0" - } - }, - "jest-get-type": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.0.6.tgz", - "integrity": "sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg==", - "dev": true - }, - "pretty-format": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.0.6.tgz", - "integrity": "sha512-8tGD7gBIENgzqA+UBzObyWqQ5B778VIFZA/S66cclyd5YkFLYs2Js7gxDKf0MXtTc9zcS7t1xhdfcElJ3YIvkQ==", - "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "ansi-regex": "^5.0.0", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - } - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "jest-diff": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.0.6.tgz", - "integrity": "sha512-Z1mqgkTCSYaFgwTlP/NUiRzdqgxmmhzHY1Tq17zL94morOHfHu3K4bgSgl+CR4GLhpV8VxkuOYuIWnQ9LnFqmg==", + "node_modules/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^27.0.6", - "jest-get-type": "^27.0.6", - "pretty-format": "^27.0.6" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "pretty-format": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.0.6.tgz", - "integrity": "sha512-8tGD7gBIENgzqA+UBzObyWqQ5B778VIFZA/S66cclyd5YkFLYs2Js7gxDKf0MXtTc9zcS7t1xhdfcElJ3YIvkQ==", - "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "ansi-regex": "^5.0.0", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - } - } + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "jest-docblock": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.0.6.tgz", - "integrity": "sha512-Fid6dPcjwepTFraz0YxIMCi7dejjJ/KL9FBjPYhBp4Sv1Y9PdhImlKZqYU555BlN4TQKaTc+F2Av1z+anVyGkA==", + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/multipipe": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", + "integrity": "sha512-7ZxrUybYv9NonoXgwoOqtStIu18D1c3eFZj27hqgf5kBrBF8Q+tE8V0MW8dKM5QLkQPh1JhhbKgHLY9kifov4Q==", "dev": true, - "requires": { - "detect-newline": "^3.0.0" - }, "dependencies": { - "detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true - } + "duplexer2": "0.0.2" } }, - "jest-each": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.0.6.tgz", - "integrity": "sha512-m6yKcV3bkSWrUIjxkE9OC0mhBZZdhovIW5ergBYirqnkLXkyEn3oUUF/QZgyecA1cF1QFyTE8bRRl8Tfg1pfLA==", + "node_modules/mute-stdout": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-2.0.0.tgz", + "integrity": "sha512-32GSKM3Wyc8dg/p39lWPKYu8zci9mJFzV1Np9Of0ZEpe6Fhssn/FbI7ywAMd40uX+p3ZKh3T5EeCFv81qS3HmQ==", "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "chalk": "^4.0.0", - "jest-get-type": "^27.0.6", - "jest-util": "^27.0.6", - "pretty-format": "^27.0.6" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "jest-get-type": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.0.6.tgz", - "integrity": "sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg==", - "dev": true - }, - "pretty-format": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.0.6.tgz", - "integrity": "sha512-8tGD7gBIENgzqA+UBzObyWqQ5B778VIFZA/S66cclyd5YkFLYs2Js7gxDKf0MXtTc9zcS7t1xhdfcElJ3YIvkQ==", - "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "ansi-regex": "^5.0.0", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - } - } + "engines": { + "node": ">= 10.13.0" } }, - "jest-environment-jsdom": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.0.6.tgz", - "integrity": "sha512-FvetXg7lnXL9+78H+xUAsra3IeZRTiegA3An01cWeXBspKXUhAwMM9ycIJ4yBaR0L7HkoMPaZsozCLHh4T8fuw==", + "node_modules/nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", "dev": true, - "requires": { - "@jest/environment": "^27.0.6", - "@jest/fake-timers": "^27.0.6", - "@jest/types": "^27.0.6", - "@types/node": "*", - "jest-mock": "^27.0.6", - "jest-util": "^27.0.6", - "jsdom": "^16.6.0" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - } + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "jest-environment-node": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.0.6.tgz", - "integrity": "sha512-+Vi6yLrPg/qC81jfXx3IBlVnDTI6kmRr08iVa2hFCWmJt4zha0XW7ucQltCAPhSR0FEKEoJ3i+W4E6T0s9is0w==", + "node_modules/nanomatch/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, - "requires": { - "@jest/environment": "^27.0.6", - "@jest/fake-timers": "^27.0.6", - "@jest/types": "^27.0.6", - "@types/node": "*", - "jest-mock": "^27.0.6", - "jest-util": "^27.0.6" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - } + "engines": { + "node": ">=0.10.0" } }, - "jest-fetch-mock": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", - "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", "dev": true, - "requires": { - "cross-fetch": "^3.0.4", - "promise-polyfill": "^8.1.3" + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" } }, - "jest-get-type": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.0.6.tgz", - "integrity": "sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg==", + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, - "jest-haste-map": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.0.6.tgz", - "integrity": "sha512-4ldjPXX9h8doB2JlRzg9oAZ2p6/GpQUNAeiYXqcpmrKbP0Qev0wdZlxSMOmz8mPOEnt4h6qIzXFLDi8RScX/1w==", + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "@types/graceful-fs": "^4.1.2", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", - "graceful-fs": "^4.2.4", - "jest-regex-util": "^27.0.6", - "jest-serializer": "^27.0.6", - "jest-util": "^27.0.6", - "jest-worker": "^27.0.6", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - }, "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true } } }, - "jest-jasmine2": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.0.6.tgz", - "integrity": "sha512-cjpH2sBy+t6dvCeKBsHpW41mjHzXgsavaFMp+VWRf0eR4EW8xASk1acqmljFtK2DgyIECMv2yCdY41r2l1+4iA==", - "dev": true, - "requires": { - "@babel/traverse": "^7.1.0", - "@jest/environment": "^27.0.6", - "@jest/source-map": "^27.0.6", - "@jest/test-result": "^27.0.6", - "@jest/types": "^27.0.6", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "expect": "^27.0.6", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.0.6", - "jest-matcher-utils": "^27.0.6", - "jest-message-util": "^27.0.6", - "jest-runtime": "^27.0.6", - "jest-snapshot": "^27.0.6", - "jest-util": "^27.0.6", - "pretty-format": "^27.0.6", - "throat": "^6.0.1" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "pretty-format": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.0.6.tgz", - "integrity": "sha512-8tGD7gBIENgzqA+UBzObyWqQ5B778VIFZA/S66cclyd5YkFLYs2Js7gxDKf0MXtTc9zcS7t1xhdfcElJ3YIvkQ==", - "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "ansi-regex": "^5.0.0", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - } - } - } + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true }, - "jest-leak-detector": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.0.6.tgz", - "integrity": "sha512-2/d6n2wlH5zEcdctX4zdbgX8oM61tb67PQt4Xh8JFAIy6LRKUnX528HulkaG6nD5qDl5vRV1NXejCe1XRCH5gQ==", - "dev": true, - "requires": { - "jest-get-type": "^27.0.6", - "pretty-format": "^27.0.6" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "jest-get-type": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.0.6.tgz", - "integrity": "sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg==", - "dev": true - }, - "pretty-format": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.0.6.tgz", - "integrity": "sha512-8tGD7gBIENgzqA+UBzObyWqQ5B778VIFZA/S66cclyd5YkFLYs2Js7gxDKf0MXtTc9zcS7t1xhdfcElJ3YIvkQ==", - "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "ansi-regex": "^5.0.0", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - } - } - } + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true }, - "jest-matcher-utils": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.0.6.tgz", - "integrity": "sha512-OFgF2VCQx9vdPSYTHWJ9MzFCehs20TsyFi6bIHbk5V1u52zJOnvF0Y/65z3GLZHKRuTgVPY4Z6LVePNahaQ+tA==", + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dev": true, - "requires": { - "chalk": "^4.0.0", - "jest-diff": "^27.0.6", - "jest-get-type": "^27.0.6", - "pretty-format": "^27.0.6" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "diff-sequences": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.0.6.tgz", - "integrity": "sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==", - "dev": true - }, - "jest-diff": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.0.6.tgz", - "integrity": "sha512-Z1mqgkTCSYaFgwTlP/NUiRzdqgxmmhzHY1Tq17zL94morOHfHu3K4bgSgl+CR4GLhpV8VxkuOYuIWnQ9LnFqmg==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^27.0.6", - "jest-get-type": "^27.0.6", - "pretty-format": "^27.0.6" - } - }, - "jest-get-type": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.0.6.tgz", - "integrity": "sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg==", - "dev": true - }, - "pretty-format": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.0.6.tgz", - "integrity": "sha512-8tGD7gBIENgzqA+UBzObyWqQ5B778VIFZA/S66cclyd5YkFLYs2Js7gxDKf0MXtTc9zcS7t1xhdfcElJ3YIvkQ==", - "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "ansi-regex": "^5.0.0", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - } - } + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, - "jest-message-util": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.0.6.tgz", - "integrity": "sha512-rBxIs2XK7rGy+zGxgi+UJKP6WqQ+KrBbD1YMj517HYN3v2BG66t3Xan3FWqYHKZwjdB700KiAJ+iES9a0M+ixw==", + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-notifier": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-9.0.1.tgz", + "integrity": "sha512-fPNFIp2hF/Dq7qLDzSg4vZ0J4e9v60gJR+Qx7RbjbWqzPDdEqeVpEx5CFeDAELIl+A/woaaNn1fQ5nEVerMxJg==", "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^27.0.6", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "micromatch": "^4.0.4", - "pretty-format": "^27.0.6", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "pretty-format": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.0.6.tgz", - "integrity": "sha512-8tGD7gBIENgzqA+UBzObyWqQ5B778VIFZA/S66cclyd5YkFLYs2Js7gxDKf0MXtTc9zcS7t1xhdfcElJ3YIvkQ==", - "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "ansi-regex": "^5.0.0", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - } - } + "growly": "^1.3.0", + "is-wsl": "^2.2.0", + "semver": "^7.3.2", + "shellwords": "^0.1.1", + "uuid": "^8.3.0", + "which": "^2.0.2" } }, - "jest-mock": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.0.6.tgz", - "integrity": "sha512-lzBETUoK8cSxts2NYXSBWT+EJNzmUVtVVwS1sU9GwE1DLCfGsngg+ZVSIe0yd0ZSm+y791esiuo+WSwpXJQ5Bw==", + "node_modules/node-notifier/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "@types/node": "*" + "bin": { + "semver": "bin/semver.js" }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - } + "engines": { + "node": ">=10" } }, - "jest-pnp-resolver": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", - "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true - }, - "jest-regex-util": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.0.6.tgz", - "integrity": "sha512-SUhPzBsGa1IKm8hx2F4NfTGGp+r7BXJ4CulsZ1k2kI+mGLG+lxGrs76veN2LF/aUdGosJBzKgXmNCw+BzFqBDQ==", + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, - "jest-resolve": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.0.6.tgz", - "integrity": "sha512-yKmIgw2LgTh7uAJtzv8UFHGF7Dm7XfvOe/LQ3Txv101fLM8cx2h1QVwtSJ51Q/SCxpIiKfVn6G2jYYMDNHZteA==", + "node_modules/node.extend": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-2.0.3.tgz", + "integrity": "sha512-xwADg/okH48PvBmRZyoX8i8GJaKuJ1CqlqotlZOhUio8egD1P5trJupHKBzcPjSF9ifK2gPcEICRBnkfPqQXZw==", "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "chalk": "^4.0.0", - "escalade": "^3.1.1", - "graceful-fs": "^4.2.4", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^27.0.6", - "jest-validate": "^27.0.6", - "resolve": "^1.20.0", - "slash": "^3.0.0" - }, "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - } + "hasown": "^2.0.0", + "is": "^3.3.0" + }, + "engines": { + "node": ">=0.4.0" } }, - "jest-resolve-dependencies": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.0.6.tgz", - "integrity": "sha512-mg9x9DS3BPAREWKCAoyg3QucCr0n6S8HEEsqRCKSPjPcu9HzRILzhdzY3imsLoZWeosEbJZz6TKasveczzpJZA==", - "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "jest-regex-util": "^27.0.6", - "jest-snapshot": "^27.0.6" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - } + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" } }, - "jest-runner": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.0.6.tgz", - "integrity": "sha512-W3Bz5qAgaSChuivLn+nKOgjqNxM7O/9JOJoKDCqThPIg2sH/d4A/lzyiaFgnb9V1/w29Le11NpzTJSzga1vyYQ==", + "node_modules/normalize-package-data/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, - "requires": { - "@jest/console": "^27.0.6", - "@jest/environment": "^27.0.6", - "@jest/test-result": "^27.0.6", - "@jest/transform": "^27.0.6", - "@jest/types": "^27.0.6", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-docblock": "^27.0.6", - "jest-environment-jsdom": "^27.0.6", - "jest-environment-node": "^27.0.6", - "jest-haste-map": "^27.0.6", - "jest-leak-detector": "^27.0.6", - "jest-message-util": "^27.0.6", - "jest-resolve": "^27.0.6", - "jest-runtime": "^27.0.6", - "jest-util": "^27.0.6", - "jest-worker": "^27.0.6", - "source-map-support": "^0.5.6", - "throat": "^6.0.1" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - } + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "jest-runtime": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.0.6.tgz", - "integrity": "sha512-BhvHLRVfKibYyqqEFkybsznKwhrsu7AWx2F3y9G9L95VSIN3/ZZ9vBpm/XCS2bS+BWz3sSeNGLzI3TVQ0uL85Q==", - "dev": true, - "requires": { - "@jest/console": "^27.0.6", - "@jest/environment": "^27.0.6", - "@jest/fake-timers": "^27.0.6", - "@jest/globals": "^27.0.6", - "@jest/source-map": "^27.0.6", - "@jest/test-result": "^27.0.6", - "@jest/transform": "^27.0.6", - "@jest/types": "^27.0.6", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^27.0.6", - "jest-message-util": "^27.0.6", - "jest-mock": "^27.0.6", - "jest-regex-util": "^27.0.6", - "jest-resolve": "^27.0.6", - "jest-snapshot": "^27.0.6", - "jest-util": "^27.0.6", - "jest-validate": "^27.0.6", - "slash": "^3.0.0", - "strip-bom": "^4.0.0", - "yargs": "^16.0.3" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" } }, - "jest-serializer": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.0.6.tgz", - "integrity": "sha512-PtGdVK9EGC7dsaziskfqaAPib6wTViY3G8E5wz9tLVPhHyiDNTZn/xjZ4khAw+09QkoOVpn7vF5nPSN6dtBexA==", + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, - "requires": { - "@types/node": "*", - "graceful-fs": "^4.2.4" + "engines": { + "node": ">=0.10.0" } }, - "jest-snapshot": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.0.6.tgz", - "integrity": "sha512-NTHaz8He+ATUagUgE7C/UtFcRoHqR2Gc+KDfhQIyx+VFgwbeEMjeP+ILpUTLosZn/ZtbNdCF5LkVnN/l+V751A==", + "node_modules/now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", "dev": true, - "requires": { - "@babel/core": "^7.7.2", - "@babel/generator": "^7.7.2", - "@babel/parser": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.0.0", - "@jest/transform": "^27.0.6", - "@jest/types": "^27.0.6", - "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^27.0.6", - "graceful-fs": "^4.2.4", - "jest-diff": "^27.0.6", - "jest-get-type": "^27.0.6", - "jest-haste-map": "^27.0.6", - "jest-matcher-utils": "^27.0.6", - "jest-message-util": "^27.0.6", - "jest-resolve": "^27.0.6", - "jest-util": "^27.0.6", - "natural-compare": "^1.4.0", - "pretty-format": "^27.0.6", - "semver": "^7.3.2" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "diff-sequences": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.0.6.tgz", - "integrity": "sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==", - "dev": true - }, - "jest-diff": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.0.6.tgz", - "integrity": "sha512-Z1mqgkTCSYaFgwTlP/NUiRzdqgxmmhzHY1Tq17zL94morOHfHu3K4bgSgl+CR4GLhpV8VxkuOYuIWnQ9LnFqmg==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^27.0.6", - "jest-get-type": "^27.0.6", - "pretty-format": "^27.0.6" - } - }, - "jest-get-type": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.0.6.tgz", - "integrity": "sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg==", - "dev": true - }, - "pretty-format": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.0.6.tgz", - "integrity": "sha512-8tGD7gBIENgzqA+UBzObyWqQ5B778VIFZA/S66cclyd5YkFLYs2Js7gxDKf0MXtTc9zcS7t1xhdfcElJ3YIvkQ==", - "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "ansi-regex": "^5.0.0", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - } - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } + "dependencies": { + "once": "^1.4.0" + }, + "engines": { + "node": ">= 10.13.0" } }, - "jest-util": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.0.6.tgz", - "integrity": "sha512-1JjlaIh+C65H/F7D11GNkGDDZtDfMEM8EBXsvd+l/cxtgQ6QhxuloOaiayt89DxUvDarbVhqI98HhgrM1yliFQ==", + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^3.0.0", - "picomatch": "^2.2.3" - }, "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - } + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "jest-validate": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.0.6.tgz", - "integrity": "sha512-yhZZOaMH3Zg6DC83n60pLmdU1DQE46DW+KLozPiPbSbPhlXXaiUTDlhHQhHFpaqIFRrInko1FHXjTRpjWRuWfA==", - "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^27.0.6", - "leven": "^3.1.0", - "pretty-format": "^27.0.6" - }, - "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", - "dev": true - }, - "jest-get-type": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.0.6.tgz", - "integrity": "sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg==", - "dev": true - }, - "pretty-format": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.0.6.tgz", - "integrity": "sha512-8tGD7gBIENgzqA+UBzObyWqQ5B778VIFZA/S66cclyd5YkFLYs2Js7gxDKf0MXtTc9zcS7t1xhdfcElJ3YIvkQ==", - "dev": true, - "requires": { - "@jest/types": "^27.0.6", - "ansi-regex": "^5.0.0", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - } - } + "node_modules/nwsapi": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", + "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==", + "dev": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" } }, - "jest-watcher": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.0.6.tgz", - "integrity": "sha512-/jIoKBhAP00/iMGnTwUBLgvxkn7vsOweDrOTSPzc7X9uOyUtJIDthQBTI1EXz90bdkrxorUZVhJwiB69gcHtYQ==", + "node_modules/object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", "dev": true, - "requires": { - "@jest/test-result": "^27.0.6", - "@jest/types": "^27.0.6", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "jest-util": "^27.0.6", - "string-length": "^4.0.1" - }, "dependencies": { - "@jest/types": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz", - "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - } + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" } }, - "jest-worker": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.0.6.tgz", - "integrity": "sha512-qupxcj/dRuA3xHPMUd40gr2EaAurFbkwzOh7wfPaeE9id7hyjURRQoqNfHifHK3XjJU6YJJUQKILGUnwGPEOCA==", + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", "dev": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, "dependencies": { - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "node_modules/object-copy/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "node_modules/object-copy/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" } }, - "jsdom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", "dev": true, - "requires": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", - "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "acorn": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", - "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==", - "dev": true - }, - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", - "dev": true, - "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.1.2" - } - } + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } }, - "json-stable-stringify-without-jsonify": { + "node_modules/object-visit": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", "dev": true, - "requires": { - "minimist": "^1.2.5" + "dependencies": { + "isobject": "^3.0.0" }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - } + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "just-debounce": { + "node_modules/object.defaults": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.1.0.tgz", - "integrity": "sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ==", - "dev": true + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", + "dev": true, + "dependencies": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", "dev": true, - "requires": { - "is-buffer": "^1.1.5" + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" } }, - "kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "last-run": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", - "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls=", + "node_modules/object.hasown": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", + "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", "dev": true, - "requires": { - "default-resolution": "^2.0.0", - "es6-weak-map": "^2.0.1" + "dependencies": { + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "lazystream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", "dev": true, - "requires": { - "readable-stream": "^2.0.5" + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", "dev": true, - "requires": { - "invert-kv": "^1.0.0" + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "lead": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", - "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, - "requires": { - "flush-write-stream": "^1.0.2" + "dependencies": { + "wrappy": "1" } }, - "less": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/less/-/less-4.1.1.tgz", - "integrity": "sha512-w09o8tZFPThBscl5d0Ggp3RcrKIouBoQscnOMgFH3n5V3kN/CXGHNfCkRPtxJk6nKryDXaV9aHLK55RXuH4sAw==", + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, - "requires": { - "copy-anything": "^2.0.1", - "errno": "^0.1.1", - "graceful-fs": "^4.1.2", - "image-size": "~0.5.0", - "make-dir": "^2.1.0", - "mime": "^1.4.1", - "needle": "^2.5.2", - "parse-node-version": "^1.0.1", - "source-map": "~0.6.0", - "tslib": "^1.10.0" - }, - "dependencies": { - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "optional": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "optional": true - } + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" + "engines": { + "node": ">=4" } }, - "liftoff": { + "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", - "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, - "requires": { - "extend": "^3.0.0", - "findup-sync": "^3.0.0", - "fined": "^1.0.1", - "flagged-respawn": "^1.0.0", - "is-plain-object": "^2.0.4", - "object.map": "^1.0.0", - "rechoir": "^0.6.2", - "resolve": "^1.1.7" + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "livereload-js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz", - "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==", - "dev": true - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dependencies": { - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - } + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" } }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "requires": { - "p-locate": "^4.1.0" + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } }, - "lodash._escapehtmlchar": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._escapehtmlchar/-/lodash._escapehtmlchar-2.4.1.tgz", - "integrity": "sha1-32fDu2t+jh6DGrSL+geVuSr+iZ0=", + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "requires": { - "lodash._htmlescapes": "~2.4.1" + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "lodash._escapestringchar": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._escapestringchar/-/lodash._escapestringchar-2.4.1.tgz", - "integrity": "sha1-7P4iYYoq3lC/7qQ5N+Ud9m8O23I=", - "dev": true - }, - "lodash._htmlescapes": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._htmlescapes/-/lodash._htmlescapes-2.4.1.tgz", - "integrity": "sha1-MtFL8IRLbeb4tioFG09nwii2JMs=", - "dev": true - }, - "lodash._isnative": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._isnative/-/lodash._isnative-2.4.1.tgz", - "integrity": "sha1-PqZAS3hKe+g2x7V1gOHN95sUgyw=", - "dev": true - }, - "lodash._objecttypes": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz", - "integrity": "sha1-fAt/admKH3ZSn4kLDNsbTf7BHBE=", - "dev": true - }, - "lodash._reinterpolate": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-2.4.1.tgz", - "integrity": "sha1-TxInqlqHEfxjL1sHofRgequLMiI=", - "dev": true + "node_modules/parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", + "dev": true, + "dependencies": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + }, + "engines": { + "node": ">=0.8" + } }, - "lodash._reunescapedhtml": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._reunescapedhtml/-/lodash._reunescapedhtml-2.4.1.tgz", - "integrity": "sha1-dHxPxAED6zu4oJduVx96JlnpO6c=", + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, - "requires": { - "lodash._htmlescapes": "~2.4.1", - "lodash.keys": "~2.4.1" + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "lodash._shimkeys": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._shimkeys/-/lodash._shimkeys-2.4.1.tgz", - "integrity": "sha1-bpzJZm/wgfC1psl4uD4kLmlJ0gM=", + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", "dev": true, - "requires": { - "lodash._objecttypes": "~2.4.1" + "engines": { + "node": ">= 0.10" } }, - "lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", - "dev": true + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "lodash.escape": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-2.4.1.tgz", - "integrity": "sha1-LOEsXghNsKV92l5dHu659dF1o7Q=", + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", "dev": true, - "requires": { - "lodash._escapehtmlchar": "~2.4.1", - "lodash._reunescapedhtml": "~2.4.1", - "lodash.keys": "~2.4.1" + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "lodash.isobject": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", - "integrity": "sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU=", + "node_modules/pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", "dev": true, - "requires": { - "lodash._objecttypes": "~2.4.1" + "engines": { + "node": ">=0.10.0" } }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", - "dev": true + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } }, - "lodash.keys": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz", - "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, - "requires": { - "lodash._isnative": "~2.4.1", - "lodash._shimkeys": "~2.4.1", - "lodash.isobject": "~2.4.1" + "engines": { + "node": ">=0.10.0" } }, - "lodash.template": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - }, - "dependencies": { - "lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", - "dev": true - } + "engines": { + "node": ">=8" } }, - "lodash.templatesettings": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0" - }, "dependencies": { - "lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", - "dev": true - } + "path-root-regex": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "lodash.values": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.values/-/lodash.values-2.4.1.tgz", - "integrity": "sha1-q/UUQ2s8twUAFieXjLzzCxKA7qQ=", + "node_modules/path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", "dev": true, - "requires": { - "lodash.keys": "~2.4.1" + "engines": { + "node": ">=0.10.0" } }, - "loose-envify": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", - "requires": { - "js-tokens": "^3.0.0" - }, + "node_modules/path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", + "dev": true, "dependencies": { - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" - } + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "node_modules/pegjs": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.9.0.tgz", + "integrity": "sha512-r8rulH2EvfG7vWrR7nSVRyqsDXWnq3xwW3hCMVEg3xRL0AfjpqfegkHK/WR258qC1wK/dQBn3NIu2RGetaERZg==", "dev": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" + "bin": { + "pegjs": "bin/pegjs" + }, + "engines": { + "node": ">= 0.10.0" } }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "requires": { - "yallist": "^4.0.0" + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "lru-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", - "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, - "requires": { - "es5-ext": "~0.10.2" + "engines": { + "node": ">=0.10.0" } }, - "lz-string": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", - "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", - "dev": true - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", "dev": true, - "requires": { - "semver": "^6.0.0" + "engines": { + "node": ">=0.10.0" } }, - "make-iterator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "dev": true, - "requires": { - "kind-of": "^6.0.2" - }, "dependencies": { - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true, - "requires": { - "tmpl": "1.0.x" + "engines": { + "node": ">= 6" } }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, - "requires": { - "object-visit": "^1.0.0" + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "matchdep": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", - "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=", - "dev": true, - "requires": { - "findup-sync": "^2.0.0", - "micromatch": "^3.0.4", - "resolve": "^1.4.0", - "stack-trace": "0.0.10" - }, - "dependencies": { - "findup-sync": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", - "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^3.1.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - } - }, - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - } + "node_modules/plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "dev": true, + "dependencies": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + }, + "engines": { + "node": ">= 0.10" } }, - "memoizee": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", - "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", + "node_modules/plugin-error/node_modules/ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", "dev": true, - "requires": { - "d": "^1.0.1", - "es5-ext": "^0.10.53", - "es6-weak-map": "^2.0.3", - "event-emitter": "^0.3.5", - "is-promise": "^2.2.2", - "lru-queue": "^0.1.0", - "next-tick": "^1.1.0", - "timers-ext": "^0.1.7" - }, "dependencies": { - "d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "dev": true, - "requires": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, - "next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "dev": true - } + "ansi-wrap": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "dev": true, - "requires": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } + "engines": { + "node": ">= 0.4" } }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", "dev": true, - "optional": true + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } }, - "mime-db": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.26.0.tgz", - "integrity": "sha1-6v/NDk/Gk1z4E02iRuLmw1MFrf8=", + "node_modules/postcss/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", "dev": true }, - "mime-types": { - "version": "2.1.14", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.14.tgz", - "integrity": "sha1-9+99l1g/yvO30oK2+LVnnaselO4=", + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, - "requires": { - "mime-db": "~1.26.0" + "engines": { + "node": ">= 0.8.0" } }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true + "node_modules/prettier": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } }, - "min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, - "requires": { - "brace-expansion": "^1.1.7" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "node_modules/promise-polyfill": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==", + "dev": true + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "multipipe": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", - "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=", + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", "dev": true, - "requires": { - "duplexer2": "0.0.2" - } + "optional": true }, - "mute-stdout": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", - "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "dev": true }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, "dependencies": { - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } }, - "needle": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.8.0.tgz", - "integrity": "sha512-ZTq6WYkN/3782H1393me3utVYdq2XyqNUFBsprEE3VMAT0+hP/cItpnITpqsY6ep2yeFE4Tqtqwc74VqUlUYtw==", + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", "dev": true, - "optional": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "optional": true + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" } + ] + }, + "node_modules/qrcode": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", + "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", + "dependencies": { + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" } }, - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true + "node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } }, - "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "dev": true + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } }, - "node-modules-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", - "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", - "dev": true + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } }, - "node-notifier": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-9.0.1.tgz", - "integrity": "sha512-fPNFIp2hF/Dq7qLDzSg4vZ0J4e9v60gJR+Qx7RbjbWqzPDdEqeVpEx5CFeDAELIl+A/woaaNn1fQ5nEVerMxJg==", + "node_modules/qs": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.3.tgz", + "integrity": "sha512-AWJm14H1vVaO/iNZ4/hO+HyaTehuy9nRqVdkTqlJt0HWvBiBIEXFmb4C0DGeYo3Xes9rrEW+TxHsaigCbN5ICQ==", "dev": true, - "requires": { - "growly": "^1.3.0", - "is-wsl": "^2.2.0", - "semver": "^7.3.2", - "shellwords": "^0.1.1", - "uuid": "^8.3.0", - "which": "^2.0.2" - }, "dependencies": { - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node-releases": { - "version": "1.1.73", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz", - "integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==", + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "dev": true }, - "node.extend": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-2.0.2.tgz", - "integrity": "sha512-pDT4Dchl94/+kkgdwyS2PauDFjZG0Hk0IcHIB+LkW27HLDtdoeMxHTxZh39DYbPP8UflWXWj9JcdDozF+YDOpQ==", + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, - "requires": { - "has": "^1.0.3", - "is": "^3.2.1" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, - "normalize-package-data": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.3.5.tgz", - "integrity": "sha1-jZJPFClg4Xd+f/4XBUNjHMfLAt8=", + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, + "node_modules/raw-body": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz", + "integrity": "sha512-WmJJU2e9Y6M5UzTOkHaM7xJGAPQD8PNzx3bAd2+uhZAim6wDk6dAZxPVYLF67XhbR4hmKGh33Lpmh4XWrCH5Mg==", "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "is-builtin-module": "^1.0.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } + "bytes": "1", + "string_decoder": "0.10" + }, + "engines": { + "node": ">= 0.8.0" } }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } + "node_modules/raw-body/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true }, - "now-and-later": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", - "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", - "dev": true, - "requires": { - "once": "^1.3.2" + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dependencies": { - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - } + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" } }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" }, - "nwsapi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" + "node_modules/react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "dependencies": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" }, + "peerDependencies": { + "@popperjs/core": "^2.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/react-redux": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", + "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25", + "react": "^18.0", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } + "redux": { + "optional": true } } }, - "object-inspect": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", - "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==", - "dev": true - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "node_modules/react-shallow-renderer": { + "version": "16.15.0", + "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", + "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", "dev": true, - "requires": { - "isobject": "^3.0.0" + "dependencies": { + "object-assign": "^4.1.1", + "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, - "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "node_modules/react-test-renderer": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.3.1.tgz", + "integrity": "sha512-KkAgygexHUkQqtvvx/otwxtuFu5cVjfzTCtjXLH9boS19/Nbtg84zS7wIQn39G8IlrhThBpQsMKkq5ZHZIYFXA==", "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" + "dependencies": { + "react-is": "^18.3.1", + "react-shallow-renderer": "^16.15.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" } }, - "object.defaults": { + "node_modules/react-test-renderer/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/read-pkg": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", "dev": true, - "requires": { - "array-each": "^1.0.1", - "array-slice": "^1.0.0", - "for-own": "^1.0.0", - "isobject": "^3.0.0" + "dependencies": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "object.map": { + "node_modules/read-pkg-up": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", "dev": true, - "requires": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" + "dependencies": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", "dev": true, - "requires": { - "isobject": "^3.0.1" + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "object.reduce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", - "integrity": "sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60=", + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", "dev": true, - "requires": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" + "dependencies": { + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, - "requires": { - "wrappy": "1" + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, - "requires": { - "mimic-fn": "^2.1.0" + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" } }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" } }, - "ordered-read-streams": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", - "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", + "node_modules/rechoir/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, - "requires": { - "readable-stream": "^2.0.1" + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", "dev": true, - "requires": { - "lcid": "^1.0.0" + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "p-each-series": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", - "integrity": "sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA==", - "dev": true + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true + "node_modules/redux-mock-store": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.4.tgz", + "integrity": "sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==", + "dev": true, + "dependencies": { + "lodash.isplainobject": "^4.0.6" + } }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "peerDependencies": { + "redux": "^5.0.0" } }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "requires": { - "p-limit": "^2.2.0" + "node_modules/reflect.getprototypeof": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true }, - "parse-filepath": { + "node_modules/regex-not": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", "dev": true, - "requires": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dev": true, - "requires": { - "error-ex": "^1.2.0" + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "parse-node-version": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", - "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", - "dev": true - }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", - "dev": true - }, - "parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", "dev": true }, - "path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "node_modules/repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", "dev": true, - "requires": { - "path-root-regex": "^0.1.0" + "engines": { + "node": ">=0.10.0" } }, - "path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", - "dev": true - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" + "engines": { + "node": ">=0.10" } }, - "pegjs": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.9.0.tgz", - "integrity": "sha1-9q76LjzlYWkgjlIXnf5B+JFBo2k=", - "dev": true - }, - "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", - "dev": true - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { + "node_modules/repeating": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==", "dev": true, - "requires": { - "pinkie": "^2.0.0" + "dependencies": { + "is-finite": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "pirates": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", - "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", "dev": true, - "requires": { - "node-modules-regexp": "^1.0.0" + "engines": { + "node": ">= 10" } }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "node_modules/replace-homedir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-2.0.0.tgz", + "integrity": "sha512-bgEuQQ/BHW0XkkJtawzrfzHFSN70f/3cNOiHa2QsYxqrjaC30X1k74FJ6xswVBP0sr0SpGIdVFuPwfrYziVeyw==", "dev": true, - "requires": { - "find-up": "^4.0.0" + "engines": { + "node": ">= 10.13.0" } }, - "plugin-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", - "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "node_modules/replacestream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/replacestream/-/replacestream-4.0.3.tgz", + "integrity": "sha512-AC0FiLS352pBBiZhd4VXB1Ab/lh0lEgpP+GGvZqbQh8a5cmXVoTe5EX/YeTFArnp4SRGTHh1qCHu9lGs1qG8sA==", "dev": true, - "requires": { - "ansi-colors": "^1.0.1", - "arr-diff": "^4.0.0", - "arr-union": "^3.1.0", - "extend-shallow": "^3.0.2" + "dependencies": { + "escape-string-regexp": "^1.0.3", + "object-assign": "^4.0.1", + "readable-stream": "^2.0.2" } }, - "pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" + "node_modules/replacestream/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "node_modules/replacestream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true }, - "postcss": { - "version": "7.0.36", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", - "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "node_modules/replacestream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "node_modules/replacestream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, - "pretty-format": { - "version": "27.0.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.0.2.tgz", - "integrity": "sha512-mXKbbBPnYTG7Yra9qFBtqj+IXcsvxsvOBco3QHxtxTl+hHKq6QdzMZ+q0CtL4ORHZgwGImRr2XZUX2EWzORxig==", + "node_modules/replacestream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, - "requires": { - "@jest/types": "^27.0.2", - "ansi-regex": "^5.0.0", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } + "safe-buffer": "~5.1.0" } }, - "pretty-hrtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", - "dev": true + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, - "promise-polyfill": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.0.tgz", - "integrity": "sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g==", + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, - "prompts": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.1.tgz", - "integrity": "sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ==", - "dev": true, - "requires": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - } + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" }, - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - }, + "node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, "dependencies": { - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - } + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true, - "optional": true - }, - "psl": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.6.0.tgz", - "integrity": "sha512-SYKKmVel98NCOYXpkwUqZqh0ahZeeKfmisiLIcEZdsb+WbLv02g/dI5BUmZnIyOe7RzZtLax81nnb2HbvC2tzA==", - "dev": true - }, - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" } }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", "dev": true, - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "qrcode": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.1.tgz", - "integrity": "sha512-nS8NJ1Z3md8uTjKtP+SGGhfqmTCs5flU/xR623oI0JX+Wepz9R8UrRVCTBTJm3qGw3rH6jJ6MUHjkDx15cxSSg==", - "requires": { - "dijkstrajs": "^1.0.1", - "encode-utf8": "^1.0.3", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, + "node_modules/resolve-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", + "dev": true, "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "dependencies": { - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==" - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } + "value-or-function": "^4.0.0" + }, + "engines": { + "node": ">= 10.13.0" } }, - "qs": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", - "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "deprecated": "https://github.com/lydell/resolve-url#deprecated", + "dev": true + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", "dev": true, - "requires": { - "side-channel": "^1.0.4" + "engines": { + "node": ">=10" } }, - "raw-body": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz", - "integrity": "sha1-HQJ8K/oRasxmI7yo8AAWVyqH1CU=", + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true, - "requires": { - "bytes": "1", - "string_decoder": "0.10" + "engines": { + "node": ">=0.12" } }, - "react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, - "react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "react-fast-compare": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", - "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" - }, - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, - "react-popper": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.2.5.tgz", - "integrity": "sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw==", - "requires": { - "react-fast-compare": "^3.0.1", - "warning": "^4.0.2" + "node_modules/rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true, + "engines": { + "node": "6.* || >= 7.*" } }, - "react-redux": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.4.tgz", - "integrity": "sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==", - "requires": { - "@babel/runtime": "^7.12.1", - "@types/react-redux": "^7.1.16", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^16.13.1" - }, - "dependencies": { - "@babel/runtime": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz", - "integrity": "sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==", - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + { + "type": "consulting", + "url": "https://feross.org/support" } + ], + "dependencies": { + "queue-microtask": "^1.2.2" } }, - "react-shallow-renderer": { - "version": "16.14.1", - "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz", - "integrity": "sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==", + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", "dev": true, - "requires": { - "object-assign": "^4.1.1", - "react-is": "^16.12.0 || ^17.0.0" + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "react-test-renderer": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz", - "integrity": "sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ==", + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, - "requires": { - "object-assign": "^4.1.1", - "react-is": "^17.0.2", - "react-shallow-renderer": "^16.13.1", - "scheduler": "^0.20.2" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-json-parse": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz", + "integrity": "sha512-o0JmTu17WGUaUOHa1l0FPGXKBfijbxK6qoHzlkihsDXxzBHvJcA7zgviKR92Xs841rX9pK16unfphLq0/KqX7A==", + "dev": true }, - "read-pkg": { + "node_modules/safe-regex": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" + "dependencies": { + "ret": "~0.1.10" } }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - }, "dependencies": { - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - } + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sane": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", + "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "deprecated": "some dependency vulnerabilities fixed, support for node < 10 dropped, and newer ECMAScript syntax/features added", "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "dependencies": { + "@cnakazawa/watch": "^1.0.3", + "anymatch": "^2.0.0", + "capture-exit": "^2.0.0", + "exec-sh": "^0.3.2", + "execa": "^1.0.0", + "fb-watchman": "^2.0.0", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5" }, + "bin": { + "sane": "src/cli.js" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/sane/node_modules/anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" } }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "node_modules/sane/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sane/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, "dependencies": { - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - } + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "node_modules/sane/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", "dev": true, - "requires": { - "resolve": "^1.1.6" + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" } }, - "redent": { + "node_modules/sane/node_modules/execa": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "dev": true, - "requires": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" } }, - "redux": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", - "integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==", - "requires": { - "@babel/runtime": "^7.9.2" + "node_modules/sane/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "redux-mock-store": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.4.tgz", - "integrity": "sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==", + "node_modules/sane/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, - "requires": { - "lodash.isplainobject": "^4.0.6" + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "redux-thunk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", - "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" - }, - "regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + "node_modules/sane/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "node_modules/sane/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" + "engines": { + "node": ">=0.10.0" } }, - "remove-bom-buffer": { + "node_modules/sane/node_modules/is-number": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", - "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", "dev": true, - "requires": { - "is-buffer": "^1.1.5", - "is-utf8": "^0.2.1" + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" } }, - "remove-bom-stream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", - "integrity": "sha1-BfGlk/FuQuH7kOv1nejlaVJflSM=", + "node_modules/sane/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dev": true, - "requires": { - "remove-bom-buffer": "^3.0.0", - "safe-buffer": "^5.1.0", - "through2": "^2.0.3" - }, "dependencies": { - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" } }, - "remove-trailing-separator": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz", - "integrity": "sha1-abBi2XhyetFNxrVrpKt3L9jXBRE=", - "dev": true - }, - "repeat-element": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", - "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "node_modules/sane/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "dev": true, - "requires": { - "is-finite": "^1.0.0" + "engines": { + "node": ">=0.10.0" } }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "replace-homedir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", - "integrity": "sha1-6H9tUTuSjd6AgmDBK+f+xv9ueYw=", + "node_modules/sane/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sane/node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1", - "is-absolute": "^1.0.0", - "remove-trailing-separator": "^1.1.0" - }, "dependencies": { - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - } + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" } }, - "replacestream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/replacestream/-/replacestream-4.0.3.tgz", - "integrity": "sha512-AC0FiLS352pBBiZhd4VXB1Ab/lh0lEgpP+GGvZqbQh8a5cmXVoTe5EX/YeTFArnp4SRGTHh1qCHu9lGs1qG8sA==", + "node_modules/sane/node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "requires": { - "escape-string-regexp": "^1.0.3", - "object-assign": "^4.0.1", - "readable-stream": "^2.0.2" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "require-directory": { + "node_modules/sane/node_modules/normalize-path": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "resolve": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.13.1.tgz", - "integrity": "sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w==", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", "dev": true, - "requires": { - "path-parse": "^1.0.6" + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "node_modules/sane/node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", "dev": true, - "requires": { - "resolve-from": "^5.0.0" + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" } }, - "resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "node_modules/sane/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", "dev": true, - "requires": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" + "engines": { + "node": ">=4" } }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, - "resolve-options": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", - "integrity": "sha1-MrueOcBtZzONyTeMDW1gdFZq0TE=", + "node_modules/sane/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, - "requires": { - "value-or-function": "^3.0.0" + "bin": { + "semver": "bin/semver" } }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "node_modules/sane/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", "dev": true, - "requires": { - "glob": "^7.1.3" - }, "dependencies": { - "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "rsvp": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", - "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", - "dev": true - }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", - "dev": true - }, - "safe-json-parse": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz", - "integrity": "sha1-PnZyPjjf3aE8mx0poeB//uSzC1c=", - "dev": true - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "node_modules/sane/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", "dev": true, - "requires": { - "ret": "~0.1.10" + "engines": { + "node": ">=0.10.0" } }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sane": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", - "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "node_modules/sane/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", "dev": true, - "requires": { - "@cnakazawa/watch": "^1.0.3", - "anymatch": "^2.0.0", - "capture-exit": "^2.0.0", - "exec-sh": "^0.3.2", - "execa": "^1.0.0", - "fb-watchman": "^2.0.0", - "micromatch": "^3.1.4", - "minimist": "^1.1.1", - "walker": "~1.0.5" + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sane/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - } + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" } }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "dev": true, "optional": true }, - "saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, - "requires": { + "dependencies": { "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" } }, - "scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" } }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } }, - "semver-greatest-satisfied-range": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", - "integrity": "sha1-E+jCZYq5aRywzXEJMkAoDTb3els=", + "node_modules/semver-greatest-satisfied-range": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-2.0.0.tgz", + "integrity": "sha512-lH3f6kMbwyANB7HuOWRMlLCa2itaCrZJ+SAqqkSZrZKO/cAsk2EOyaKHUtNkVLFyFW9pct22SFesFp3Z7zpA0g==", "dev": true, - "requires": { - "sver-compat": "^1.5.0" + "dependencies": { + "sver": "^1.8.3" + }, + "engines": { + "node": ">= 10.13.0" } }, - "set-blocking": { + "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } }, - "set-value": { + "node_modules/set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", "dev": true, - "requires": { + "dependencies": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", "is-plain-object": "^2.0.3", "split-string": "^3.0.1" }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + "node_modules/set-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "shebang-command": { + "node_modules/set-value/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, - "requires": { + "dependencies": { "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "shebang-regex": { + "node_modules/shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "shellwords": { + "node_modules/shellwords": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", "dev": true }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "sisteransi": { + "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true }, - "slash": { + "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "snapdragon": { + "node_modules/snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", "dev": true, - "requires": { + "dependencies": { "base": "^0.11.1", "debug": "^2.2.0", "define-property": "^0.2.5", @@ -8719,142 +13914,137 @@ "source-map-resolve": "^0.5.0", "use": "^3.1.0" }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } + "engines": { + "node": ">=0.10.0" } }, - "snapdragon-node": { + "node_modules/snapdragon-node": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", "dev": true, - "requires": { + "dependencies": { "define-property": "^1.0.0", "isobject": "^3.0.0", "snapdragon-util": "^3.0.1" }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - } + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "snapdragon-util": { + "node_modules/snapdragon-util": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", "dev": true, - "requires": { + "dependencies": { "kind-of": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/snapdragon-util/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/snapdragon/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "source-map-resolve": { + "node_modules/snapdragon/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", "dev": true, - "requires": { + "dependencies": { "atob": "^2.1.2", "decode-uri-component": "^0.2.0", "resolve-url": "^0.2.1", @@ -8862,1063 +14052,1491 @@ "urix": "^0.1.0" } }, - "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, - "requires": { + "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, - "source-map-url": { + "node_modules/source-map-url": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated", "dev": true }, - "sparkles": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", - "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", - "dev": true + "node_modules/sparkles": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-2.1.0.tgz", + "integrity": "sha512-r7iW1bDw8R/cFifrD3JnQJX0K1jqT0kprL48BiBpLZLJPmAm34zsVBsK5lc7HirZYZqMW65dOXZgbAGt/I6frg==", + "dev": true, + "engines": { + "node": ">= 10.13.0" + } }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, - "requires": { + "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true }, - "spdx-expression-parse": { + "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, - "requires": { + "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, - "spdx-license-ids": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz", - "integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==", + "node_modules/spdx-license-ids": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", "dev": true }, - "split-string": { + "node_modules/split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", "dev": true, - "requires": { + "dependencies": { "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "sprintf-js": { + "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, - "stable": { + "node_modules/stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" - }, - "stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", - "dev": true + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility" }, - "stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw==", + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, - "requires": { + "dependencies": { "escape-string-regexp": "^2.0.0" }, - "dependencies": { - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true - } + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" } }, - "static-extend": { + "node_modules/static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", "dev": true, - "requires": { + "dependencies": { "define-property": "^0.2.5", "object-copy": "^0.1.0" }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/stream-composer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", + "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", + "dev": true, + "dependencies": { + "streamx": "^2.13.2" } }, - "stream-exhaust": { + "node_modules/stream-exhaust": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", "dev": true }, - "stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", - "dev": true + "node_modules/streamx": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } }, - "string-length": { + "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", "dev": true, - "requires": { + "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" } }, - "string-template": { + "node_modules/string-template": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", - "integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=", + "integrity": "sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==", "dev": true }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "strip-bom": { + "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "strip-bom-string": { + "node_modules/strip-bom-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", - "integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=", - "dev": true + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "strip-eof": { + "node_modules/strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "strip-final-newline": { + "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true + "dev": true, + "engines": { + "node": ">=6" + } }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", "dev": true, - "requires": { - "get-stdin": "^4.0.1" + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" } }, - "supports-color": { + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "requires": { + "dependencies": { "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "supports-hyperlinks": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", - "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, - "requires": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "sver-compat": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", - "integrity": "sha1-PPh9/rTQe0o/FIJ7wYaz/QxkXNg=", + "node_modules/sver": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/sver/-/sver-1.8.4.tgz", + "integrity": "sha512-71o1zfzyawLfIWBOmw8brleKyvnbn73oVHNCsu51uPMz/HWiKkkXsI31JjHW5zqXEqnPYkIiHd8ZmL7FCimLEA==", "dev": true, - "requires": { - "es6-iterator": "^2.0.1", - "es6-symbol": "^3.1.1" + "optionalDependencies": { + "semver": "^6.3.0" } }, - "symbol-tree": { + "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, - "terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" + "dependencies": { + "streamx": "^2.12.5" } }, - "test-exclude": { + "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, - "requires": { + "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz", + "integrity": "sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==", + "dev": true, "dependencies": { - "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } + "b4a": "^1.6.4" } }, - "textextensions": { + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/textextensions": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-3.3.0.tgz", "integrity": "sha512-mk82dS8eRABNbeVJrEiN5/UMSCliINAuz8mkUwH4SwslkNP//gbEzlWNS5au0z5Dpx40SQxzqZevZkn+WYJ9Dw==", - "dev": true - }, - "throat": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", - "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://bevry.me/fund" + } }, - "through2": { + "node_modules/through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", "dev": true, - "requires": { - "readable-stream": "3" - }, "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - } - } - }, - "through2-filter": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", - "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", - "dev": true, - "requires": { - "through2": "~2.0.0", - "xtend": "~4.0.0" - }, - "dependencies": { - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } + "readable-stream": "3" } }, - "time-stamp": { + "node_modules/time-stamp": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", - "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", - "dev": true + "integrity": "sha512-gLCeArryy2yNTRzTGKbZbloctj64jkZ57hj5zdraXue6aFgd6PmvVtEyiUU+hvU0v7q08oVv8r8ev0tRo6bvgw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "timers-ext": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", - "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", "dev": true, - "requires": { - "es5-ext": "~0.10.46", - "next-tick": "1" + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" } }, - "tiny-lr": { + "node_modules/tiny-lr": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz", "integrity": "sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==", "dev": true, - "requires": { + "dependencies": { "body": "^5.1.0", "debug": "^3.1.0", "faye-websocket": "~0.10.0", "livereload-js": "^2.3.0", "object-assign": "^4.1.0", "qs": "^6.4.0" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - } } }, - "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", - "dev": true - }, - "to-absolute-glob": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", - "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", + "node_modules/tiny-lr/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "requires": { - "is-absolute": "^1.0.0", - "is-negated-glob": "^1.0.0" + "dependencies": { + "ms": "^2.1.1" } }, - "to-fast-properties": { + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } }, - "to-object-path": { + "node_modules/to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", "dev": true, - "requires": { + "dependencies": { "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-object-path/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" } }, - "to-regex": { + "node_modules/to-regex": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", "dev": true, - "requires": { + "dependencies": { "define-property": "^2.0.2", "extend-shallow": "^3.0.2", "regex-not": "^1.0.2", "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, - "to-through": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", - "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=", + "node_modules/to-through": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", + "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", "dev": true, - "requires": { - "through2": "^2.0.3" + "dependencies": { + "streamx": "^2.12.5" }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, "dependencies": { - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" } }, - "tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", "dev": true, - "requires": { + "dependencies": { "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" } }, - "trim-newlines": { + "node_modules/trim-newlines": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", - "dev": true + "integrity": "sha512-Nm4cF79FhSTzrLKGDMi3I4utBtFv8qKy4sq1enftf2gMdpqI8oVQTAfySkTz5r49giVzDj88SVZXP4CeYQwjaw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true }, - "type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", "dev": true }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, - "requires": { - "prelude-ls": "~1.1.2" + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" } }, - "type-detect": { + "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true + "dev": true, + "engines": { + "node": ">=4" + } }, - "type-fest": { + "node_modules/type-fest": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "typedarray-to-buffer": { + "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", "dev": true, - "requires": { + "dependencies": { "is-typedarray": "^1.0.0" } }, - "typescript": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", - "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", - "dev": true + "node_modules/typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.16.0.tgz", + "integrity": "sha512-kaVRivQjOzuoCXU6+hLnjo3/baxyzWVO5GrnExkFzETRYJKVHYkrJglOu2OCm8Hi9RPDWX1PTNNTpU5KRV0+RA==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "7.16.0", + "@typescript-eslint/parser": "7.16.0", + "@typescript-eslint/utils": "7.16.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "unc-path-regex": { + "node_modules/unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", - "dev": true + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "undertaker": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.2.1.tgz", - "integrity": "sha512-71WxIzDkgYk9ZS+spIB8iZXchFhAdEo2YU8xYqBYJ39DIUIqziK78ftm26eecoIY49X0J2MLhG4hr18Yp6/CMA==", + "node_modules/undertaker": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-2.0.0.tgz", + "integrity": "sha512-tO/bf30wBbTsJ7go80j0RzA2rcwX6o7XPBpeFcb+jzoeb4pfMM2zUeSDIkY1AWqeZabWxaQZ/h8N9t35QKDLPQ==", "dev": true, - "requires": { - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "bach": "^1.0.0", - "collection-map": "^1.0.0", - "es6-weak-map": "^2.0.1", - "last-run": "^1.1.0", - "object.defaults": "^1.0.0", - "object.reduce": "^1.0.0", - "undertaker-registry": "^1.0.0" + "dependencies": { + "bach": "^2.0.1", + "fast-levenshtein": "^3.0.0", + "last-run": "^2.0.0", + "undertaker-registry": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" } }, - "undertaker-registry": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", - "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=", + "node_modules/undertaker-registry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-2.0.0.tgz", + "integrity": "sha512-+hhVICbnp+rlzZMgxXenpvTxpuvA67Bfgtt+O9WOE5jo7w/dyiF1VmoZVIHvP2EkUjsyKyTwYKlLhA+j47m1Ew==", + "dev": true, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/undertaker/node_modules/fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "dev": true, + "dependencies": { + "fastest-levenshtein": "^1.0.7" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, - "union-value": { + "node_modules/union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", "dev": true, - "requires": { + "dependencies": { "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "unique-stream": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", - "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", + "node_modules/union-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "dev": true, - "requires": { - "json-stable-stringify-without-jsonify": "^1.0.1", - "through2-filter": "^3.0.0" + "engines": { + "node": ">=0.10.0" } }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } }, - "unset-value": { + "node_modules/unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", "dev": true, - "requires": { + "dependencies": { "has-value": "^0.3.1", "isobject": "^3.0.0" }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "dev": true, + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } }, - "urix": { + "node_modules/urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "deprecated": "Please see https://github.com/lydell/urix#deprecated", "dev": true }, - "use": { + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } }, - "util-deprecate": { + "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, - "uuid": { + "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } }, - "v8-to-istanbul": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.0.0.tgz", - "integrity": "sha512-LkmXi8UUNxnCC+JlH7/fsfsKr5AU110l+SYGJimWNkWhxbN5EyeOtm1MJ0hhvqMMOhGwBj1Fp70Yv9i+hX0QAg==", + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, - "requires": { + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0", - "source-map": "^0.7.3" + "convert-source-map": "^2.0.0" }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } + "engines": { + "node": ">=10.12.0" } }, - "v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "node_modules/v8flags": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", + "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" + "engines": { + "node": ">= 10.13.0" } }, - "validate-npm-package-license": { + "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, - "requires": { + "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, - "value-or-function": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", - "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=", - "dev": true + "node_modules/value-or-function": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", + "dev": true, + "engines": { + "node": ">= 10.13.0" + } }, - "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", + "node_modules/vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", + "dependencies": { + "clone": "^2.1.2", "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" } }, - "vinyl-fs": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", - "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", + "node_modules/vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", "dev": true, - "requires": { - "fs-mkdirp-stream": "^1.0.0", - "glob-stream": "^6.1.0", - "graceful-fs": "^4.0.0", + "dependencies": { + "bl": "^5.0.0", + "vinyl": "^3.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", + "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", + "dev": true, + "dependencies": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.0", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", "is-valid-glob": "^1.0.0", - "lazystream": "^1.0.0", - "lead": "^1.0.0", - "object.assign": "^4.0.4", - "pumpify": "^1.3.5", - "readable-stream": "^2.3.3", - "remove-bom-buffer": "^3.0.0", - "remove-bom-stream": "^1.2.0", - "resolve-options": "^1.1.0", - "through2": "^2.0.0", - "to-through": "^2.0.0", - "value-or-function": "^3.0.0", - "vinyl": "^2.0.0", - "vinyl-sourcemap": "^1.1.0" - }, - "dependencies": { - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.0", + "vinyl-sourcemap": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" } }, - "vinyl-sourcemap": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", - "integrity": "sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=", + "node_modules/vinyl-sourcemap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", "dev": true, - "requires": { - "append-buffer": "^1.0.2", - "convert-source-map": "^1.5.0", - "graceful-fs": "^4.1.6", - "normalize-path": "^2.1.1", - "now-and-later": "^2.0.0", - "remove-bom-buffer": "^3.0.0", - "vinyl": "^2.0.0" + "dependencies": { + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" } }, - "vinyl-sourcemaps-apply": { + "node_modules/vinyl-sourcemaps-apply": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", - "integrity": "sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=", + "integrity": "sha512-+oDh3KYZBoZC8hfocrbrxbLUeaYtQK7J5WU5Br9VqWqmCll3tFJqKp97GC9GmMsVIL0qnx2DgEDVxdo5EZ5sSw==", "dev": true, - "requires": { - "source-map": "^0.5.1" - }, "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } + "source-map": "^0.5.1" } }, - "w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "node_modules/vinyl-sourcemaps-apply/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "dev": true, - "requires": { - "browser-process-hrtime": "^1.0.0" + "engines": { + "node": ">=0.10.0" } }, - "w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", "dev": true, - "requires": { - "xml-name-validator": "^3.0.0" + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" } }, - "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", "dev": true, - "requires": { - "makeerror": "1.0.x" + "dependencies": { + "makeerror": "1.0.12" } }, - "warning": { + "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "requires": { + "dependencies": { "loose-envify": "^1.0.0" } }, - "webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "dev": true + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } }, - "websocket-driver": { + "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", "dev": true, - "requires": { + "dependencies": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" } }, - "websocket-extensions": { + "node_modules/websocket-extensions": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.8.0" + } }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", "dev": true, - "requires": { - "iconv-lite": "0.4.24" + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" } }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } }, - "whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", "dev": true, - "requires": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, - "requires": { + "dependencies": { "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, - "which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", - "dev": true + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true + "node_modules/which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dev": true, + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "wrap-ansi": { + "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, - "requires": { + "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" }, - "dependencies": { - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - } + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "wrappy": { + "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, - "write-file-atomic": { + "node_modules/write-file-atomic": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", "dev": true, - "requires": { + "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", "signal-exit": "^3.0.2", "typedarray-to-buffer": "^3.1.5" } }, - "ws": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz", - "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==", - "dev": true + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } }, - "xmlchars": { + "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, - "xtend": { + "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.4" + } }, - "y18n": { + "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true + "dev": true, + "engines": { + "node": ">=10" + } }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, - "yargs": { + "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, - "requires": { + "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", @@ -9927,25 +15545,39 @@ "y18n": "^5.0.5", "yargs-parser": "^20.2.2" }, - "dependencies": { - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - } + "engines": { + "node": ">=10" } }, - "yargs-parser": { - "version": "20.2.7", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", - "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", - "dev": true + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/web/package.json b/web/package.json index b2bf84c6fe..e27056325a 100644 --- a/web/package.json +++ b/web/package.json @@ -2,39 +2,43 @@ "name": "mitmproxy", "private": true, "scripts": { - "test": "tsc --noEmit && jest --coverage", + "test": "eslint . && tsc --noEmit && jest --coverage", "build": "gulp prod", - "start": "gulp" + "start": "gulp", + "prettier": "prettier --write .", + "eslint": "eslint --fix ." }, "dependencies": { - "@popperjs/core": "^2.9.3", + "@popperjs/core": "^2.11.8", + "@reduxjs/toolkit": "^2.2.5", "bootstrap": "^3.4.1", "classnames": "^2.3.1", "codemirror": "^5.62.3", "lodash": "^4.17.21", "prop-types": "^15.7.2", "qrcode": "^1.5.1", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-popper": "^2.2.5", - "react-redux": "^7.2.4", - "redux": "^4.1.1", - "redux-thunk": "^2.3.0", - "shallowequal": "^1.1.0", + "react-redux": "^9.1.2", "stable": "^0.1.8" }, "devDependencies": { - "@testing-library/dom": "^8.1.0", - "@testing-library/jest-dom": "^5.14.1", - "@testing-library/react": "^12.0.0", - "@testing-library/user-event": "^13.2.1", - "@types/jest": "^27.0.1", + "@eslint/js": "^9.6.0", + "@testing-library/dom": "^10.3.1", + "@testing-library/jest-dom": "^6.4.6", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", + "@types/jest": "^29.5.12", "@types/redux-mock-store": "^1.0.3", - "esbuild": "^0.12.21", + "esbuild": "^0.23.0", "esbuild-jest": "^0.5.0", - "gulp": "^4.0.2", + "eslint": "^8.57.0", + "eslint-plugin-react": "^7.34.3", + "globals": "^15.8.0", + "gulp": "^5.0.0", "gulp-clean-css": "^4.3.0", - "gulp-esbuild": "^0.8.5", + "gulp-esbuild": "^0.12.0", "gulp-less": "^5.0.0", "gulp-livereload": "^4.0.2", "gulp-notify": "^4.0.0", @@ -42,11 +46,14 @@ "gulp-plumber": "^1.2.1", "gulp-replace": "^1.1.3", "gulp-sourcemaps": "^3.0.0", - "jest": "^27.0.6", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "jest-fetch-mock": "^3.0.3", - "react-test-renderer": "^17.0.2", + "prettier": "3.3.2", + "react-test-renderer": "^18.3.1", "redux-mock-store": "^1.5.4", "through2": "^4.0.2", - "typescript": "^4.3.5" + "typescript": "^5.5.3", + "typescript-eslint": "^7.15.0" } } diff --git a/web/src/css/app.less b/web/src/css/app.less index 57caa98d93..49084ae714 100644 --- a/web/src/css/app.less +++ b/web/src/css/app.less @@ -3,7 +3,9 @@ html { box-sizing: border-box; } -*, *:before, *:after { +*, +*:before, +*:after { box-sizing: inherit; } @@ -22,3 +24,4 @@ html { @import (less) "dropdown.less"; @import (less) "command.less"; @import (less) "capture-setup.less"; +@import (less) "mode.less"; diff --git a/web/src/css/command.less b/web/src/css/command.less index 9701e76b20..8803f6e715 100644 --- a/web/src/css/command.less +++ b/web/src/css/command.less @@ -1,5 +1,5 @@ .command-title { - background-color: #F2F2F2; + background-color: #f2f2f2; border: 1px solid #aaa; } @@ -28,4 +28,4 @@ .available-commands { overflow: auto; -} \ No newline at end of file +} diff --git a/web/src/css/contentview.less b/web/src/css/contentview.less index e483214ff4..da789730d4 100644 --- a/web/src/css/contentview.less +++ b/web/src/css/contentview.less @@ -1,29 +1,28 @@ .contentview { - .header { + .header { font-weight: bold; - } - .highlight{ + } + .highlight { font-weight: bold; } - .offset{ - color: blue + .offset { + color: blue; } - .text{ - + .text { } - .codeeditor{ + .codeeditor { margin-bottom: 12px; } - .Token_Name_Tag{ + .Token_Name_Tag { color: darkgreen; } - .Token_Literal_String{ + .Token_Literal_String { color: firebrick; } - .Token_Literal_Number{ + .Token_Literal_Number { color: purple; } - .Token_Keyword_Constant{ + .Token_Keyword_Constant { color: blue; } } diff --git a/web/src/css/dropdown.less b/web/src/css/dropdown.less index cd27c84c07..bc1cebd5d2 100644 --- a/web/src/css/dropdown.less +++ b/web/src/css/dropdown.less @@ -1,9 +1,11 @@ .dropdown-menu { - // setting a margin is not compatible with popper. margin: 0 !important; > li > a { padding: 3px 10px; } + + max-height: 250px; + overflow-y: scroll; } diff --git a/web/src/css/eventlog.less b/web/src/css/eventlog.less index 393f75dbe2..bc38101c86 100644 --- a/web/src/css/eventlog.less +++ b/web/src/css/eventlog.less @@ -1,5 +1,4 @@ .eventlog { - height: 200px; flex: 0 0 auto; @@ -7,7 +6,7 @@ flex-direction: column; > div { - background-color: #F2F2F2; + background-color: #f2f2f2; padding: 0 5px; flex: 0 0 auto; border-top: 1px solid #aaa; diff --git a/web/src/css/flowdetail.less b/web/src/css/flowdetail.less index ec38bb5f01..fa256b749d 100644 --- a/web/src/css/flowdetail.less +++ b/web/src/css/flowdetail.less @@ -5,7 +5,7 @@ flex-direction: column; nav { - background-color: #F2F2F2; + background-color: #f2f2f2; } section { @@ -21,6 +21,18 @@ } } + .close-button { + margin-top: 2px; + padding-left: 10px; + padding-right: 7px; + color: grey; + font-size: 15px; + border: none; + } + + .close-button:hover { + color: black; + } .first-line { font-family: @font-family-monospace; @@ -59,7 +71,6 @@ } } - .inline-input { display: inline; margin: 0 -3px; @@ -68,7 +79,10 @@ border: solid transparent 1px; &:hover { - box-shadow: 0 0 0 1px rgba(0, 0, 0, 1.25%), 0 2px 4px rgba(0, 0, 0, 5%), 0 2px 6px rgba(0, 0, 0, 2.5%); + box-shadow: + 0 0 0 1px rgba(0, 0, 0, 1.25%), + 0 2px 4px rgba(0, 0, 0, 5%), + 0 2px 6px rgba(0, 0, 0, 2.5%); background-color: rgba(255, 255, 255, 0.1); } @@ -80,10 +94,12 @@ &[contenteditable] { outline-width: 0; - box-shadow: 0 0 0 1px rgba(0, 0, 0, 5%), 0 2px 4px rgba(0, 0, 0, 20%), 0 2px 6px rgba(0, 0, 0, 10%); + box-shadow: + 0 0 0 1px rgba(0, 0, 0, 5%), + 0 2px 4px rgba(0, 0, 0, 20%), + 0 2px 6px rgba(0, 0, 0, 10%); background-color: rgba(255, 255, 255, 0.2); - &.has-warning { color: rgb(255, 184, 184); } @@ -94,7 +110,9 @@ } } -.connection-table, .timing-table, .certificate-table { +.connection-table, +.timing-table, +.certificate-table { td:nth-child(2) { font-family: @font-family-monospace; width: 70%; @@ -125,9 +143,10 @@ } } -.headers, .trailers { +.headers, +.trailers { .kv-row { - margin-bottom: .3em; + margin-bottom: 0.3em; max-height: 12.4ex; overflow-y: auto; } @@ -162,7 +181,8 @@ overflow-wrap: break-word; } -.connection-table, .timing-table { +.connection-table, +.timing-table { td { overflow: hidden; text-overflow: ellipsis; @@ -176,7 +196,8 @@ dl.cert-attributes { flex-wrap: wrap; margin-bottom: 0; - dt, dd { + dt, + dd { text-overflow: ellipsis; overflow: hidden; } @@ -190,8 +211,10 @@ dl.cert-attributes { } } -.dns-request table, .dns-response table { - th, td { +.dns-request table, +.dns-response table { + th, + td { padding-right: 1rem; } } diff --git a/web/src/css/flowtable.less b/web/src/css/flowtable.less index b3c21aad64..d321d6d2a6 100644 --- a/web/src/css/flowtable.less +++ b/web/src/css/flowtable.less @@ -18,7 +18,7 @@ } thead tr { - background-color: #F2F2F2; + background-color: #f2f2f2; border-bottom: solid #bebebe 1px; line-height: 23px; } @@ -29,17 +29,19 @@ padding-left: 1px; .user-select(none); - &.sort-asc, &.sort-desc { - background-color: lighten(#F2F2F2, 3%); + &.sort-asc, + &.sort-desc { + background-color: lighten(#f2f2f2, 3%); } - &.sort-asc:after, &.sort-desc:after { + &.sort-asc:after, + &.sort-desc:after { font: normal normal normal 14px/1 FontAwesome; position: absolute; right: 3px; top: 3px; padding: 2px; - background-color: fadeout(lighten(#F2F2F2, 3%), 20%); + background-color: fadeout(lighten(#f2f2f2, 3%), 20%); } &.sort-asc:after { @@ -49,7 +51,6 @@ &.sort-desc:after { content: "\f0dd"; } - } tr { @@ -86,13 +87,16 @@ @interceptorange: hsl(30, 100%, 50%); tr.intercepted:not(.has-response) { - .col-path, .col-method { + .col-path, + .col-method { color: @interceptorange; } } tr.intercepted.has-response { - .col-status, .col-size, .col-time { + .col-status, + .col-size, + .col-time { color: @interceptorange; } } @@ -109,6 +113,11 @@ background-color: rgba(0, 185, 0, 0.5); } + .col-index { + text-align: right; + width: 4ch; + } + .col-icon { width: 32px; } @@ -130,7 +139,8 @@ color: @interceptorange; } - .fa-exclamation, .fa-times { + .fa-exclamation, + .fa-times { color: darkred; } } @@ -139,6 +149,10 @@ width: 60px; } + .col-version { + width: 80px; + } + .col-status { width: 50px; } @@ -155,7 +169,15 @@ width: 170px; } - td.col-time, td.col-size, td.col-timestamp { + .col-comment { + width: 150px; + padding-left: 5px; + } + + td.col-time, + td.col-size, + td.col-timestamp, + td.col-comment { text-align: right; } @@ -170,7 +192,7 @@ font-size: 20px; } - tr:hover .col-quickactions, .col-quickactions.hover { + tr:hover .col-quickactions { overflow: visible; } diff --git a/web/src/css/flowview.less b/web/src/css/flowview.less index 0f031c5609..7180c270c5 100644 --- a/web/src/css/flowview.less +++ b/web/src/css/flowview.less @@ -1,5 +1,4 @@ .flowview-image { - text-align: center; padding: 10px 0; @@ -14,7 +13,7 @@ right: 20px; } -.edit-flow { +.edit-flow { cursor: pointer; position: absolute; right: 0; @@ -36,6 +35,6 @@ .edit-flow:hover { background-color: rgba(239, 108, 0, 0.7); - color: rgba(0,0,0,0.8); + color: rgba(0, 0, 0, 0.8); border: solid 2px transparent; } diff --git a/web/src/css/header.less b/web/src/css/header.less index a94773d33a..00d696853b 100644 --- a/web/src/css/header.less +++ b/web/src/css/header.less @@ -1,5 +1,5 @@ -@import (reference) '../../node_modules/bootstrap/less/variables.less'; -@import (reference) '../../node_modules/bootstrap/less/mixins/grid.less'; +@import (reference) "../../node_modules/bootstrap/less/variables.less"; +@import (reference) "../../node_modules/bootstrap/less/mixins/grid.less"; @import (reference) "../../node_modules/bootstrap/less/mixins/labels.less"; @import (reference) "../../node_modules/bootstrap/less/labels.less"; @@ -9,7 +9,7 @@ header { padding-top: 6px; background-color: white; @separator-color: lighten(grey, 15%); - > div { + > div:not(:empty) { display: block; margin: 0; padding: 0; @@ -34,7 +34,8 @@ header { > a { display: inline-block; } - > .btn, > a > .btn { + > .btn, + > a > .btn { height: @menu-height - @menu-legend-height; text-align: center; margin: 0 1px; @@ -70,7 +71,7 @@ header { font-weight: normal; margin: 0; } - input[type=checkbox] { + input[type="checkbox"] { margin: 0 2px; vertical-align: middle; } @@ -117,7 +118,6 @@ header { opacity: 0.9; @media (max-width: @screen-xs-max) { - top: 16px; left: 29px; right: 2px; @@ -143,7 +143,8 @@ header { opacity: 1; transition: all 1s linear; - &.init, &.fetching { + &.init, + &.fetching { background-color: @label-info-bg; } &.established { diff --git a/web/src/css/layout.less b/web/src/css/layout.less index ed4adb6986..4b94031397 100644 --- a/web/src/css/layout.less +++ b/web/src/css/layout.less @@ -1,4 +1,7 @@ -html, body, #container, #mitmproxy { +html, +body, +#container, +#mitmproxy { height: 100%; margin: 0; overflow: hidden; @@ -10,7 +13,9 @@ html, body, #container, #mitmproxy { outline: none; // our root element is focused by default. - > header, > footer, > .eventlog { + > header, + > footer, + > .eventlog { flex: 0 0 auto; } } @@ -30,10 +35,10 @@ html, body, #container, #mitmproxy { flex-direction: column; } - .flow-detail, .flow-table { + .flow-detail, + .flow-table { flex: 1 1 auto; } - } .splitter { diff --git a/web/src/css/modal.less b/web/src/css/modal.less index 30e98f9c23..13ef997f77 100644 --- a/web/src/css/modal.less +++ b/web/src/css/modal.less @@ -2,7 +2,6 @@ display: block; } - .modal-dialog { overflow-y: initial !important; } diff --git a/web/src/css/mode.less b/web/src/css/mode.less new file mode 100644 index 0000000000..6465a51111 --- /dev/null +++ b/web/src/css/mode.less @@ -0,0 +1,330 @@ +@import (reference) "../../node_modules/bootstrap/less/variables.less"; +@import (reference) "../../node_modules/bootstrap/less/mixins/grid.less"; +@import (reference) "../../node_modules/bootstrap/less/mixins/labels.less"; +@import (reference) "../../node_modules/bootstrap/less/labels.less"; + +.modes { + padding: 1em 2em; + overflow-y: auto; + height: 100%; + width: 100vw; + + h3 { + margin-bottom: 10px; + } + + .modes-category { + padding-left: 10px; + } + + .green-left-border { + border-left: 10px solid #77c77a; + } + + .gray-left-border { + border-left: 10px solid #b2b2b2; + } + + .modes-container { + display: flex; + flex-direction: column; + gap: 25px; + } + + .mode-title { + font-size: 16px; + font-weight: 600; + } + + .mode-description { + color: #b2b2b2; + margin-top: -10px; + } + + .mode-entry { + text-align: left; + display: flex; + align-items: center; + font-size: 1.5rem; + font-weight: normal; + margin: 0; + + input[type="checkbox"] { + margin: 0 10px; + vertical-align: middle; + width: 1.8rem; + height: 1.8rem; + } + + p { + margin: 0; + } + } + + .mode-status { + margin-left: 10px; + margin-top: 5px; + font-weight: 600; + } + + .mode-local { + i { + font-size: 1.8rem; + cursor: pointer; + } + .processes-container { + display: flex; + flex-direction: row; + align-items: center; + gap: 5px; + margin-left: 5px; + } + .selected-processes { + display: flex; + flex-wrap: wrap; + gap: 5px; + overflow: hidden; + } + .selected-process { + display: flex; + flex-direction: row; + gap: 5px; + background-color: #e0e0e0; + padding: 5px; + border-radius: 4px; + height: 30px; + font-size: 12px; + font-weight: 500; + align-content: center; + align-items: center; + + > i { + font-size: 15px; + } + } + + .dropdown-container { + display: flex; + flex-direction: row; + align-items: center; + } + + .local-dropdown { + position: relative; + width: 200px; + + .dropdown-header { + padding: 5px; + border: 1px solid #ccc; + border-radius: 4px; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + margin-right: 10px; + height: 30px; + } + + input { + border: none; + flex: 1; + outline: none; + color: black; + } + + .dropdown-list { + width: 100%; + max-height: 300px; + overflow-y: auto; + z-index: 1; + list-style: none; + padding: 0; + margin-bottom: 0; + } + + .dropdown-item { + display: flex; + flex-direction: row; + align-items: center; + padding: 8px 10px; + margin: 4px 0; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s ease; + } + + .dropdown-item.selected { + background-color: #e0e0e0; + } + + .dropdown-item:hover { + background-color: #f0f0f0; + } + + .process-details { + display: flex; + flex-direction: row; + align-items: center; + } + + .process-icon { + margin-right: 5px; + margin-left: 5px; + color: #5f6368; + width: 25px; + height: 25px; + } + + .process-name { + font-weight: 500; + color: #333; + } + } + + .local-popover { + button { + font-size: 1rem; + } + i { + font-size: 1.5rem; + } + + @supports (anchor-name: --test) { + > div { + margin: 0; + position: absolute; + left: 0; + top: calc(anchor(bottom) + 10px); + justify-self: anchor-center; + align-self: auto; + } + } + + > div { + &:popover-open { + display: block; + } + gap: 0.5rem; + align-items: center; + } + } + } + + .mode-input { + border: 1px solid #ccc; + margin-left: 10px; + border-radius: 4px; + min-width: 120px; + height: 25px; + } + + .mode-reverse-input { + border: 1px solid #ccc; + margin-left: 10px; + margin-right: 10px; + border-radius: 4px; + min-width: 70px; + height: 25px; + } + + .mode-upstream-input { + border: 1px solid #ccc; + margin-left: 10px; + margin-right: 5px; + border-radius: 4px; + min-width: 70px; + height: 25px; + } + + .mode-reverse-dropdown { + margin: 0 5px; + border: 1px solid #ccc; + border-radius: 4px; + height: 25px; + } + + .mode-reverse-servers { + display: flex; + flex-direction: column; + gap: 10px; + } + + .mode-reverse-add-server { + margin-left: 10px; + display: flex; + flex-direction: row; + cursor: pointer; + font-weight: 500; + + opacity: 0.5; + transition: all 50ms ease-in-out; + + &:hover { + opacity: 1; + } + + i { + font-size: 2rem; + margin-right: 5px; + } + } + + .mode-popover { + > button { + background-color: transparent; + border: none; + cursor: pointer; + font-size: 2rem; + } + + @supports (anchor-name: --test) { + > div { + margin: 0; + position: absolute; + left: calc(anchor(right) + 5px); + align-self: anchor-center; + } + } + + > div { + border: 1px solid #ccc; + border-radius: 4px; + padding: 10px 15px; + background-color: #f9f9f9; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + + &:popover-open { + display: grid; + } + grid-template-columns: 9em 1fr; + grid-auto-rows: min-content; + gap: 0.5rem; + align-items: center; + + > h4 { + text-align: center; + grid-column: 1/-1; + } + > .mode-input { + text-align: right; + background-color: white; + margin: 0; + } + } + } + + .missing-mode-container { + color: #b2b2b2; + + .title-icon-container { + display: flex; + flex-direction: row; + align-items: center; + gap: 5px; + + i { + font-size: 1.8rem; + } + } + } +} diff --git a/web/src/css/sprites.less b/web/src/css/sprites.less index fb4b570ebe..96ff54eb40 100644 --- a/web/src/css/sprites.less +++ b/web/src/css/sprites.less @@ -60,3 +60,7 @@ .resource-icon-dns { background-image: url(images/resourceDnsIcon.png); } + +.resource-icon-quic { + background-image: url(images/resourceQuicIcon.png); +} diff --git a/web/src/css/tabs.less b/web/src/css/tabs.less index a66d30ed6d..5a19705e14 100644 --- a/web/src/css/tabs.less +++ b/web/src/css/tabs.less @@ -1,5 +1,4 @@ .nav-tabs { - @separator-color: lighten(grey, 15%); border-bottom: solid @separator-color 1px; @@ -26,7 +25,6 @@ } } } - } .nav-tabs-lg { diff --git a/web/src/css/vendor-bootstrap-variables.less b/web/src/css/vendor-bootstrap-variables.less index 668fec45da..77ae785773 100644 --- a/web/src/css/vendor-bootstrap-variables.less +++ b/web/src/css/vendor-bootstrap-variables.less @@ -3,5 +3,19 @@ @navbar-default-color: #303030; @navbar-default-bg: #ffffff; @navbar-default-border: #e0e0e0; -@font-family-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; -@font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +@font-family-sans-serif: + system-ui, + -apple-system, + "Segoe UI", + Roboto, + "Helvetica Neue", + Arial, + "Noto Sans", + "Liberation Sans", + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji"; +@font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, + "Liberation Mono", "Courier New", monospace; diff --git a/web/src/css/vendor.less b/web/src/css/vendor.less index e91ae3a843..eb6208842a 100644 --- a/web/src/css/vendor.less +++ b/web/src/css/vendor.less @@ -1,3 +1,3 @@ // Bootstrap -@import 'vendor-bootstrap.less'; -@import (less) '../fonts/font-awesome.css'; +@import "vendor-bootstrap.less"; +@import (less) "../fonts/font-awesome.css"; diff --git a/web/src/fonts/README b/web/src/fonts/README index 218a78e1a7..e922ce1e37 100644 --- a/web/src/fonts/README +++ b/web/src/fonts/README @@ -1,2 +1 @@ - From a rendered version of the FontAwesome github repo. diff --git a/web/src/fonts/font-awesome.css b/web/src/fonts/font-awesome.css index 9510290049..4a337b18d3 100644 --- a/web/src/fonts/font-awesome.css +++ b/web/src/fonts/font-awesome.css @@ -5,2333 +5,2340 @@ /* FONT PATH * -------------------------- */ @font-face { - font-family: 'FontAwesome'; - src: url('./fonts/fontawesome-webfont.eot?v=4.7.0'); - src: url('./fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('./fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('./fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('./fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('./fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg'); - font-weight: normal; - font-style: normal; + font-family: "FontAwesome"; + src: url("./fonts/fontawesome-webfont.eot?v=4.7.0"); + src: + url("./fonts/fontawesome-webfont.eot?#iefix&v=4.7.0") + format("embedded-opentype"), + url("./fonts/fontawesome-webfont.woff2?v=4.7.0") format("woff2"), + url("./fonts/fontawesome-webfont.woff?v=4.7.0") format("woff"), + url("./fonts/fontawesome-webfont.ttf?v=4.7.0") format("truetype"), + url("./fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular") + format("svg"); + font-weight: normal; + font-style: normal; } .fa { - display: inline-block; - font: normal normal normal 14px/1 FontAwesome; - font-size: inherit; - text-rendering: auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } /* makes the font 33% larger relative to the icon container */ .fa-lg { - font-size: 1.33333333em; - line-height: 0.75em; - vertical-align: -15%; + font-size: 1.33333333em; + line-height: 0.75em; + vertical-align: -15%; } .fa-2x { - font-size: 2em; + font-size: 2em; } .fa-3x { - font-size: 3em; + font-size: 3em; } .fa-4x { - font-size: 4em; + font-size: 4em; } .fa-5x { - font-size: 5em; + font-size: 5em; } .fa-fw { - width: 1.28571429em; - text-align: center; + width: 1.28571429em; + text-align: center; } .fa-ul { - padding-left: 0; - margin-left: 2.14285714em; - list-style-type: none; + padding-left: 0; + margin-left: 2.14285714em; + list-style-type: none; } .fa-ul > li { - position: relative; + position: relative; } .fa-li { - position: absolute; - left: -2.14285714em; - width: 2.14285714em; - top: 0.14285714em; - text-align: center; + position: absolute; + left: -2.14285714em; + width: 2.14285714em; + top: 0.14285714em; + text-align: center; } .fa-li.fa-lg { - left: -1.85714286em; + left: -1.85714286em; } .fa-border { - padding: .2em .25em .15em; - border: solid 0.08em #eeeeee; - border-radius: .1em; + padding: 0.2em 0.25em 0.15em; + border: solid 0.08em #eeeeee; + border-radius: 0.1em; } .fa-pull-left { - float: left; + float: left; } .fa-pull-right { - float: right; + float: right; } .fa.fa-pull-left { - margin-right: .3em; + margin-right: 0.3em; } .fa.fa-pull-right { - margin-left: .3em; + margin-left: 0.3em; } /* Deprecated as of 4.4.0 */ .pull-right { - float: right; + float: right; } .pull-left { - float: left; + float: left; } .fa.pull-left { - margin-right: .3em; + margin-right: 0.3em; } .fa.pull-right { - margin-left: .3em; + margin-left: 0.3em; } .fa-spin { - -webkit-animation: fa-spin 2s infinite linear; - animation: fa-spin 2s infinite linear; + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; } .fa-pulse { - -webkit-animation: fa-spin 1s infinite steps(8); - animation: fa-spin 1s infinite steps(8); + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); } @-webkit-keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } } @keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } } .fa-rotate-90 { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; - -webkit-transform: rotate(90deg); - -ms-transform: rotate(90deg); - transform: rotate(90deg); + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); } .fa-rotate-180 { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; - -webkit-transform: rotate(180deg); - -ms-transform: rotate(180deg); - transform: rotate(180deg); + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); } .fa-rotate-270 { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; - -webkit-transform: rotate(270deg); - -ms-transform: rotate(270deg); - transform: rotate(270deg); + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); } .fa-flip-horizontal { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; - -webkit-transform: scale(-1, 1); - -ms-transform: scale(-1, 1); - transform: scale(-1, 1); + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); } .fa-flip-vertical { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; - -webkit-transform: scale(1, -1); - -ms-transform: scale(1, -1); - transform: scale(1, -1); + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); } :root .fa-rotate-90, :root .fa-rotate-180, :root .fa-rotate-270, :root .fa-flip-horizontal, :root .fa-flip-vertical { - filter: none; + filter: none; } .fa-stack { - position: relative; - display: inline-block; - width: 2em; - height: 2em; - line-height: 2em; - vertical-align: middle; + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; } .fa-stack-1x, .fa-stack-2x { - position: absolute; - left: 0; - width: 100%; - text-align: center; + position: absolute; + left: 0; + width: 100%; + text-align: center; } .fa-stack-1x { - line-height: inherit; + line-height: inherit; } .fa-stack-2x { - font-size: 2em; + font-size: 2em; } .fa-inverse { - color: #ffffff; + color: #ffffff; } /* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen readers do not read off random characters that represent icons */ .fa-glass:before { - content: "\f000"; + content: "\f000"; } .fa-music:before { - content: "\f001"; + content: "\f001"; } .fa-search:before { - content: "\f002"; + content: "\f002"; } .fa-envelope-o:before { - content: "\f003"; + content: "\f003"; } .fa-heart:before { - content: "\f004"; + content: "\f004"; } .fa-star:before { - content: "\f005"; + content: "\f005"; } .fa-star-o:before { - content: "\f006"; + content: "\f006"; } .fa-user:before { - content: "\f007"; + content: "\f007"; } .fa-film:before { - content: "\f008"; + content: "\f008"; } .fa-th-large:before { - content: "\f009"; + content: "\f009"; } .fa-th:before { - content: "\f00a"; + content: "\f00a"; } .fa-th-list:before { - content: "\f00b"; + content: "\f00b"; } .fa-check:before { - content: "\f00c"; + content: "\f00c"; } .fa-remove:before, .fa-close:before, .fa-times:before { - content: "\f00d"; + content: "\f00d"; } .fa-search-plus:before { - content: "\f00e"; + content: "\f00e"; } .fa-search-minus:before { - content: "\f010"; + content: "\f010"; } .fa-power-off:before { - content: "\f011"; + content: "\f011"; } .fa-signal:before { - content: "\f012"; + content: "\f012"; } .fa-gear:before, .fa-cog:before { - content: "\f013"; + content: "\f013"; } .fa-trash-o:before { - content: "\f014"; + content: "\f014"; } .fa-home:before { - content: "\f015"; + content: "\f015"; } .fa-file-o:before { - content: "\f016"; + content: "\f016"; } .fa-clock-o:before { - content: "\f017"; + content: "\f017"; } .fa-road:before { - content: "\f018"; + content: "\f018"; } .fa-download:before { - content: "\f019"; + content: "\f019"; } .fa-arrow-circle-o-down:before { - content: "\f01a"; + content: "\f01a"; } .fa-arrow-circle-o-up:before { - content: "\f01b"; + content: "\f01b"; } .fa-inbox:before { - content: "\f01c"; + content: "\f01c"; } .fa-play-circle-o:before { - content: "\f01d"; + content: "\f01d"; } .fa-rotate-right:before, .fa-repeat:before { - content: "\f01e"; + content: "\f01e"; } .fa-refresh:before { - content: "\f021"; + content: "\f021"; } .fa-list-alt:before { - content: "\f022"; + content: "\f022"; } .fa-lock:before { - content: "\f023"; + content: "\f023"; } .fa-flag:before { - content: "\f024"; + content: "\f024"; } .fa-headphones:before { - content: "\f025"; + content: "\f025"; } .fa-volume-off:before { - content: "\f026"; + content: "\f026"; } .fa-volume-down:before { - content: "\f027"; + content: "\f027"; } .fa-volume-up:before { - content: "\f028"; + content: "\f028"; } .fa-qrcode:before { - content: "\f029"; + content: "\f029"; } .fa-barcode:before { - content: "\f02a"; + content: "\f02a"; } .fa-tag:before { - content: "\f02b"; + content: "\f02b"; } .fa-tags:before { - content: "\f02c"; + content: "\f02c"; } .fa-book:before { - content: "\f02d"; + content: "\f02d"; } .fa-bookmark:before { - content: "\f02e"; + content: "\f02e"; } .fa-print:before { - content: "\f02f"; + content: "\f02f"; } .fa-camera:before { - content: "\f030"; + content: "\f030"; } .fa-font:before { - content: "\f031"; + content: "\f031"; } .fa-bold:before { - content: "\f032"; + content: "\f032"; } .fa-italic:before { - content: "\f033"; + content: "\f033"; } .fa-text-height:before { - content: "\f034"; + content: "\f034"; } .fa-text-width:before { - content: "\f035"; + content: "\f035"; } .fa-align-left:before { - content: "\f036"; + content: "\f036"; } .fa-align-center:before { - content: "\f037"; + content: "\f037"; } .fa-align-right:before { - content: "\f038"; + content: "\f038"; } .fa-align-justify:before { - content: "\f039"; + content: "\f039"; } .fa-list:before { - content: "\f03a"; + content: "\f03a"; } .fa-dedent:before, .fa-outdent:before { - content: "\f03b"; + content: "\f03b"; } .fa-indent:before { - content: "\f03c"; + content: "\f03c"; } .fa-video-camera:before { - content: "\f03d"; + content: "\f03d"; } .fa-photo:before, .fa-image:before, .fa-picture-o:before { - content: "\f03e"; + content: "\f03e"; } .fa-pencil:before { - content: "\f040"; + content: "\f040"; } .fa-map-marker:before { - content: "\f041"; + content: "\f041"; } .fa-adjust:before { - content: "\f042"; + content: "\f042"; } .fa-tint:before { - content: "\f043"; + content: "\f043"; } .fa-edit:before, .fa-pencil-square-o:before { - content: "\f044"; + content: "\f044"; } .fa-share-square-o:before { - content: "\f045"; + content: "\f045"; } .fa-check-square-o:before { - content: "\f046"; + content: "\f046"; } .fa-arrows:before { - content: "\f047"; + content: "\f047"; } .fa-step-backward:before { - content: "\f048"; + content: "\f048"; } .fa-fast-backward:before { - content: "\f049"; + content: "\f049"; } .fa-backward:before { - content: "\f04a"; + content: "\f04a"; } .fa-play:before { - content: "\f04b"; + content: "\f04b"; } .fa-pause:before { - content: "\f04c"; + content: "\f04c"; } .fa-stop:before { - content: "\f04d"; + content: "\f04d"; } .fa-forward:before { - content: "\f04e"; + content: "\f04e"; } .fa-fast-forward:before { - content: "\f050"; + content: "\f050"; } .fa-step-forward:before { - content: "\f051"; + content: "\f051"; } .fa-eject:before { - content: "\f052"; + content: "\f052"; } .fa-chevron-left:before { - content: "\f053"; + content: "\f053"; } .fa-chevron-right:before { - content: "\f054"; + content: "\f054"; } .fa-plus-circle:before { - content: "\f055"; + content: "\f055"; } .fa-minus-circle:before { - content: "\f056"; + content: "\f056"; } .fa-times-circle:before { - content: "\f057"; + content: "\f057"; } .fa-check-circle:before { - content: "\f058"; + content: "\f058"; } .fa-question-circle:before { - content: "\f059"; + content: "\f059"; } .fa-info-circle:before { - content: "\f05a"; + content: "\f05a"; } .fa-crosshairs:before { - content: "\f05b"; + content: "\f05b"; } .fa-times-circle-o:before { - content: "\f05c"; + content: "\f05c"; } .fa-check-circle-o:before { - content: "\f05d"; + content: "\f05d"; } .fa-ban:before { - content: "\f05e"; + content: "\f05e"; } .fa-arrow-left:before { - content: "\f060"; + content: "\f060"; } .fa-arrow-right:before { - content: "\f061"; + content: "\f061"; } .fa-arrow-up:before { - content: "\f062"; + content: "\f062"; } .fa-arrow-down:before { - content: "\f063"; + content: "\f063"; } .fa-mail-forward:before, .fa-share:before { - content: "\f064"; + content: "\f064"; } .fa-expand:before { - content: "\f065"; + content: "\f065"; } .fa-compress:before { - content: "\f066"; + content: "\f066"; } .fa-plus:before { - content: "\f067"; + content: "\f067"; } .fa-minus:before { - content: "\f068"; + content: "\f068"; } .fa-asterisk:before { - content: "\f069"; + content: "\f069"; } .fa-exclamation-circle:before { - content: "\f06a"; + content: "\f06a"; } .fa-gift:before { - content: "\f06b"; + content: "\f06b"; } .fa-leaf:before { - content: "\f06c"; + content: "\f06c"; } .fa-fire:before { - content: "\f06d"; + content: "\f06d"; } .fa-eye:before { - content: "\f06e"; + content: "\f06e"; } .fa-eye-slash:before { - content: "\f070"; + content: "\f070"; } .fa-warning:before, .fa-exclamation-triangle:before { - content: "\f071"; + content: "\f071"; } .fa-plane:before { - content: "\f072"; + content: "\f072"; } .fa-calendar:before { - content: "\f073"; + content: "\f073"; } .fa-random:before { - content: "\f074"; + content: "\f074"; } .fa-comment:before { - content: "\f075"; + content: "\f075"; } .fa-magnet:before { - content: "\f076"; + content: "\f076"; } .fa-chevron-up:before { - content: "\f077"; + content: "\f077"; } .fa-chevron-down:before { - content: "\f078"; + content: "\f078"; } .fa-retweet:before { - content: "\f079"; + content: "\f079"; } .fa-shopping-cart:before { - content: "\f07a"; + content: "\f07a"; } .fa-folder:before { - content: "\f07b"; + content: "\f07b"; } .fa-folder-open:before { - content: "\f07c"; + content: "\f07c"; } .fa-arrows-v:before { - content: "\f07d"; + content: "\f07d"; } .fa-arrows-h:before { - content: "\f07e"; + content: "\f07e"; } .fa-bar-chart-o:before, .fa-bar-chart:before { - content: "\f080"; + content: "\f080"; } .fa-twitter-square:before { - content: "\f081"; + content: "\f081"; } .fa-facebook-square:before { - content: "\f082"; + content: "\f082"; } .fa-camera-retro:before { - content: "\f083"; + content: "\f083"; } .fa-key:before { - content: "\f084"; + content: "\f084"; } .fa-gears:before, .fa-cogs:before { - content: "\f085"; + content: "\f085"; } .fa-comments:before { - content: "\f086"; + content: "\f086"; } .fa-thumbs-o-up:before { - content: "\f087"; + content: "\f087"; } .fa-thumbs-o-down:before { - content: "\f088"; + content: "\f088"; } .fa-star-half:before { - content: "\f089"; + content: "\f089"; } .fa-heart-o:before { - content: "\f08a"; + content: "\f08a"; } .fa-sign-out:before { - content: "\f08b"; + content: "\f08b"; } .fa-linkedin-square:before { - content: "\f08c"; + content: "\f08c"; } .fa-thumb-tack:before { - content: "\f08d"; + content: "\f08d"; } .fa-external-link:before { - content: "\f08e"; + content: "\f08e"; } .fa-sign-in:before { - content: "\f090"; + content: "\f090"; } .fa-trophy:before { - content: "\f091"; + content: "\f091"; } .fa-github-square:before { - content: "\f092"; + content: "\f092"; } .fa-upload:before { - content: "\f093"; + content: "\f093"; } .fa-lemon-o:before { - content: "\f094"; + content: "\f094"; } .fa-phone:before { - content: "\f095"; + content: "\f095"; } .fa-square-o:before { - content: "\f096"; + content: "\f096"; } .fa-bookmark-o:before { - content: "\f097"; + content: "\f097"; } .fa-phone-square:before { - content: "\f098"; + content: "\f098"; } .fa-twitter:before { - content: "\f099"; + content: "\f099"; } .fa-facebook-f:before, .fa-facebook:before { - content: "\f09a"; + content: "\f09a"; } .fa-github:before { - content: "\f09b"; + content: "\f09b"; } .fa-unlock:before { - content: "\f09c"; + content: "\f09c"; } .fa-credit-card:before { - content: "\f09d"; + content: "\f09d"; } .fa-feed:before, .fa-rss:before { - content: "\f09e"; + content: "\f09e"; } .fa-hdd-o:before { - content: "\f0a0"; + content: "\f0a0"; } .fa-bullhorn:before { - content: "\f0a1"; + content: "\f0a1"; } .fa-bell:before { - content: "\f0f3"; + content: "\f0f3"; } .fa-certificate:before { - content: "\f0a3"; + content: "\f0a3"; } .fa-hand-o-right:before { - content: "\f0a4"; + content: "\f0a4"; } .fa-hand-o-left:before { - content: "\f0a5"; + content: "\f0a5"; } .fa-hand-o-up:before { - content: "\f0a6"; + content: "\f0a6"; } .fa-hand-o-down:before { - content: "\f0a7"; + content: "\f0a7"; } .fa-arrow-circle-left:before { - content: "\f0a8"; + content: "\f0a8"; } .fa-arrow-circle-right:before { - content: "\f0a9"; + content: "\f0a9"; } .fa-arrow-circle-up:before { - content: "\f0aa"; + content: "\f0aa"; } .fa-arrow-circle-down:before { - content: "\f0ab"; + content: "\f0ab"; } .fa-globe:before { - content: "\f0ac"; + content: "\f0ac"; } .fa-wrench:before { - content: "\f0ad"; + content: "\f0ad"; } .fa-tasks:before { - content: "\f0ae"; + content: "\f0ae"; } .fa-filter:before { - content: "\f0b0"; + content: "\f0b0"; } .fa-briefcase:before { - content: "\f0b1"; + content: "\f0b1"; } .fa-arrows-alt:before { - content: "\f0b2"; + content: "\f0b2"; } .fa-group:before, .fa-users:before { - content: "\f0c0"; + content: "\f0c0"; } .fa-chain:before, .fa-link:before { - content: "\f0c1"; + content: "\f0c1"; } .fa-cloud:before { - content: "\f0c2"; + content: "\f0c2"; } .fa-flask:before { - content: "\f0c3"; + content: "\f0c3"; } .fa-cut:before, .fa-scissors:before { - content: "\f0c4"; + content: "\f0c4"; } .fa-copy:before, .fa-files-o:before { - content: "\f0c5"; + content: "\f0c5"; } .fa-paperclip:before { - content: "\f0c6"; + content: "\f0c6"; } .fa-save:before, .fa-floppy-o:before { - content: "\f0c7"; + content: "\f0c7"; } .fa-square:before { - content: "\f0c8"; + content: "\f0c8"; } .fa-navicon:before, .fa-reorder:before, .fa-bars:before { - content: "\f0c9"; + content: "\f0c9"; } .fa-list-ul:before { - content: "\f0ca"; + content: "\f0ca"; } .fa-list-ol:before { - content: "\f0cb"; + content: "\f0cb"; } .fa-strikethrough:before { - content: "\f0cc"; + content: "\f0cc"; } .fa-underline:before { - content: "\f0cd"; + content: "\f0cd"; } .fa-table:before { - content: "\f0ce"; + content: "\f0ce"; } .fa-magic:before { - content: "\f0d0"; + content: "\f0d0"; } .fa-truck:before { - content: "\f0d1"; + content: "\f0d1"; } .fa-pinterest:before { - content: "\f0d2"; + content: "\f0d2"; } .fa-pinterest-square:before { - content: "\f0d3"; + content: "\f0d3"; } .fa-google-plus-square:before { - content: "\f0d4"; + content: "\f0d4"; } .fa-google-plus:before { - content: "\f0d5"; + content: "\f0d5"; } .fa-money:before { - content: "\f0d6"; + content: "\f0d6"; } .fa-caret-down:before { - content: "\f0d7"; + content: "\f0d7"; } .fa-caret-up:before { - content: "\f0d8"; + content: "\f0d8"; } .fa-caret-left:before { - content: "\f0d9"; + content: "\f0d9"; } .fa-caret-right:before { - content: "\f0da"; + content: "\f0da"; } .fa-columns:before { - content: "\f0db"; + content: "\f0db"; } .fa-unsorted:before, .fa-sort:before { - content: "\f0dc"; + content: "\f0dc"; } .fa-sort-down:before, .fa-sort-desc:before { - content: "\f0dd"; + content: "\f0dd"; } .fa-sort-up:before, .fa-sort-asc:before { - content: "\f0de"; + content: "\f0de"; } .fa-envelope:before { - content: "\f0e0"; + content: "\f0e0"; } .fa-linkedin:before { - content: "\f0e1"; + content: "\f0e1"; } .fa-rotate-left:before, .fa-undo:before { - content: "\f0e2"; + content: "\f0e2"; } .fa-legal:before, .fa-gavel:before { - content: "\f0e3"; + content: "\f0e3"; } .fa-dashboard:before, .fa-tachometer:before { - content: "\f0e4"; + content: "\f0e4"; } .fa-comment-o:before { - content: "\f0e5"; + content: "\f0e5"; } .fa-comments-o:before { - content: "\f0e6"; + content: "\f0e6"; } .fa-flash:before, .fa-bolt:before { - content: "\f0e7"; + content: "\f0e7"; } .fa-sitemap:before { - content: "\f0e8"; + content: "\f0e8"; } .fa-umbrella:before { - content: "\f0e9"; + content: "\f0e9"; } .fa-paste:before, .fa-clipboard:before { - content: "\f0ea"; + content: "\f0ea"; } .fa-lightbulb-o:before { - content: "\f0eb"; + content: "\f0eb"; } .fa-exchange:before { - content: "\f0ec"; + content: "\f0ec"; } .fa-cloud-download:before { - content: "\f0ed"; + content: "\f0ed"; } .fa-cloud-upload:before { - content: "\f0ee"; + content: "\f0ee"; } .fa-user-md:before { - content: "\f0f0"; + content: "\f0f0"; } .fa-stethoscope:before { - content: "\f0f1"; + content: "\f0f1"; } .fa-suitcase:before { - content: "\f0f2"; + content: "\f0f2"; } .fa-bell-o:before { - content: "\f0a2"; + content: "\f0a2"; } .fa-coffee:before { - content: "\f0f4"; + content: "\f0f4"; } .fa-cutlery:before { - content: "\f0f5"; + content: "\f0f5"; } .fa-file-text-o:before { - content: "\f0f6"; + content: "\f0f6"; } .fa-building-o:before { - content: "\f0f7"; + content: "\f0f7"; } .fa-hospital-o:before { - content: "\f0f8"; + content: "\f0f8"; } .fa-ambulance:before { - content: "\f0f9"; + content: "\f0f9"; } .fa-medkit:before { - content: "\f0fa"; + content: "\f0fa"; } .fa-fighter-jet:before { - content: "\f0fb"; + content: "\f0fb"; } .fa-beer:before { - content: "\f0fc"; + content: "\f0fc"; } .fa-h-square:before { - content: "\f0fd"; + content: "\f0fd"; } .fa-plus-square:before { - content: "\f0fe"; + content: "\f0fe"; } .fa-angle-double-left:before { - content: "\f100"; + content: "\f100"; } .fa-angle-double-right:before { - content: "\f101"; + content: "\f101"; } .fa-angle-double-up:before { - content: "\f102"; + content: "\f102"; } .fa-angle-double-down:before { - content: "\f103"; + content: "\f103"; } .fa-angle-left:before { - content: "\f104"; + content: "\f104"; } .fa-angle-right:before { - content: "\f105"; + content: "\f105"; } .fa-angle-up:before { - content: "\f106"; + content: "\f106"; } .fa-angle-down:before { - content: "\f107"; + content: "\f107"; } .fa-desktop:before { - content: "\f108"; + content: "\f108"; } .fa-laptop:before { - content: "\f109"; + content: "\f109"; } .fa-tablet:before { - content: "\f10a"; + content: "\f10a"; } .fa-mobile-phone:before, .fa-mobile:before { - content: "\f10b"; + content: "\f10b"; } .fa-circle-o:before { - content: "\f10c"; + content: "\f10c"; } .fa-quote-left:before { - content: "\f10d"; + content: "\f10d"; } .fa-quote-right:before { - content: "\f10e"; + content: "\f10e"; } .fa-spinner:before { - content: "\f110"; + content: "\f110"; } .fa-circle:before { - content: "\f111"; + content: "\f111"; } .fa-mail-reply:before, .fa-reply:before { - content: "\f112"; + content: "\f112"; } .fa-github-alt:before { - content: "\f113"; + content: "\f113"; } .fa-folder-o:before { - content: "\f114"; + content: "\f114"; } .fa-folder-open-o:before { - content: "\f115"; + content: "\f115"; } .fa-smile-o:before { - content: "\f118"; + content: "\f118"; } .fa-frown-o:before { - content: "\f119"; + content: "\f119"; } .fa-meh-o:before { - content: "\f11a"; + content: "\f11a"; } .fa-gamepad:before { - content: "\f11b"; + content: "\f11b"; } .fa-keyboard-o:before { - content: "\f11c"; + content: "\f11c"; } .fa-flag-o:before { - content: "\f11d"; + content: "\f11d"; } .fa-flag-checkered:before { - content: "\f11e"; + content: "\f11e"; } .fa-terminal:before { - content: "\f120"; + content: "\f120"; } .fa-code:before { - content: "\f121"; + content: "\f121"; } .fa-mail-reply-all:before, .fa-reply-all:before { - content: "\f122"; + content: "\f122"; } .fa-star-half-empty:before, .fa-star-half-full:before, .fa-star-half-o:before { - content: "\f123"; + content: "\f123"; } .fa-location-arrow:before { - content: "\f124"; + content: "\f124"; } .fa-crop:before { - content: "\f125"; + content: "\f125"; } .fa-code-fork:before { - content: "\f126"; + content: "\f126"; } .fa-unlink:before, .fa-chain-broken:before { - content: "\f127"; + content: "\f127"; } .fa-question:before { - content: "\f128"; + content: "\f128"; } .fa-info:before { - content: "\f129"; + content: "\f129"; } .fa-exclamation:before { - content: "\f12a"; + content: "\f12a"; } .fa-superscript:before { - content: "\f12b"; + content: "\f12b"; } .fa-subscript:before { - content: "\f12c"; + content: "\f12c"; } .fa-eraser:before { - content: "\f12d"; + content: "\f12d"; } .fa-puzzle-piece:before { - content: "\f12e"; + content: "\f12e"; } .fa-microphone:before { - content: "\f130"; + content: "\f130"; } .fa-microphone-slash:before { - content: "\f131"; + content: "\f131"; } .fa-shield:before { - content: "\f132"; + content: "\f132"; } .fa-calendar-o:before { - content: "\f133"; + content: "\f133"; } .fa-fire-extinguisher:before { - content: "\f134"; + content: "\f134"; } .fa-rocket:before { - content: "\f135"; + content: "\f135"; } .fa-maxcdn:before { - content: "\f136"; + content: "\f136"; } .fa-chevron-circle-left:before { - content: "\f137"; + content: "\f137"; } .fa-chevron-circle-right:before { - content: "\f138"; + content: "\f138"; } .fa-chevron-circle-up:before { - content: "\f139"; + content: "\f139"; } .fa-chevron-circle-down:before { - content: "\f13a"; + content: "\f13a"; } .fa-html5:before { - content: "\f13b"; + content: "\f13b"; } .fa-css3:before { - content: "\f13c"; + content: "\f13c"; } .fa-anchor:before { - content: "\f13d"; + content: "\f13d"; } .fa-unlock-alt:before { - content: "\f13e"; + content: "\f13e"; } .fa-bullseye:before { - content: "\f140"; + content: "\f140"; } .fa-ellipsis-h:before { - content: "\f141"; + content: "\f141"; } .fa-ellipsis-v:before { - content: "\f142"; + content: "\f142"; } .fa-rss-square:before { - content: "\f143"; + content: "\f143"; } .fa-play-circle:before { - content: "\f144"; + content: "\f144"; } .fa-ticket:before { - content: "\f145"; + content: "\f145"; } .fa-minus-square:before { - content: "\f146"; + content: "\f146"; } .fa-minus-square-o:before { - content: "\f147"; + content: "\f147"; } .fa-level-up:before { - content: "\f148"; + content: "\f148"; } .fa-level-down:before { - content: "\f149"; + content: "\f149"; } .fa-check-square:before { - content: "\f14a"; + content: "\f14a"; } .fa-pencil-square:before { - content: "\f14b"; + content: "\f14b"; } .fa-external-link-square:before { - content: "\f14c"; + content: "\f14c"; } .fa-share-square:before { - content: "\f14d"; + content: "\f14d"; } .fa-compass:before { - content: "\f14e"; + content: "\f14e"; } .fa-toggle-down:before, .fa-caret-square-o-down:before { - content: "\f150"; + content: "\f150"; } .fa-toggle-up:before, .fa-caret-square-o-up:before { - content: "\f151"; + content: "\f151"; } .fa-toggle-right:before, .fa-caret-square-o-right:before { - content: "\f152"; + content: "\f152"; } .fa-euro:before, .fa-eur:before { - content: "\f153"; + content: "\f153"; } .fa-gbp:before { - content: "\f154"; + content: "\f154"; } .fa-dollar:before, .fa-usd:before { - content: "\f155"; + content: "\f155"; } .fa-rupee:before, .fa-inr:before { - content: "\f156"; + content: "\f156"; } .fa-cny:before, .fa-rmb:before, .fa-yen:before, .fa-jpy:before { - content: "\f157"; + content: "\f157"; } .fa-ruble:before, .fa-rouble:before, .fa-rub:before { - content: "\f158"; + content: "\f158"; } .fa-won:before, .fa-krw:before { - content: "\f159"; + content: "\f159"; } .fa-bitcoin:before, .fa-btc:before { - content: "\f15a"; + content: "\f15a"; } .fa-file:before { - content: "\f15b"; + content: "\f15b"; } .fa-file-text:before { - content: "\f15c"; + content: "\f15c"; } .fa-sort-alpha-asc:before { - content: "\f15d"; + content: "\f15d"; } .fa-sort-alpha-desc:before { - content: "\f15e"; + content: "\f15e"; } .fa-sort-amount-asc:before { - content: "\f160"; + content: "\f160"; } .fa-sort-amount-desc:before { - content: "\f161"; + content: "\f161"; } .fa-sort-numeric-asc:before { - content: "\f162"; + content: "\f162"; } .fa-sort-numeric-desc:before { - content: "\f163"; + content: "\f163"; } .fa-thumbs-up:before { - content: "\f164"; + content: "\f164"; } .fa-thumbs-down:before { - content: "\f165"; + content: "\f165"; } .fa-youtube-square:before { - content: "\f166"; + content: "\f166"; } .fa-youtube:before { - content: "\f167"; + content: "\f167"; } .fa-xing:before { - content: "\f168"; + content: "\f168"; } .fa-xing-square:before { - content: "\f169"; + content: "\f169"; } .fa-youtube-play:before { - content: "\f16a"; + content: "\f16a"; } .fa-dropbox:before { - content: "\f16b"; + content: "\f16b"; } .fa-stack-overflow:before { - content: "\f16c"; + content: "\f16c"; } .fa-instagram:before { - content: "\f16d"; + content: "\f16d"; } .fa-flickr:before { - content: "\f16e"; + content: "\f16e"; } .fa-adn:before { - content: "\f170"; + content: "\f170"; } .fa-bitbucket:before { - content: "\f171"; + content: "\f171"; } .fa-bitbucket-square:before { - content: "\f172"; + content: "\f172"; } .fa-tumblr:before { - content: "\f173"; + content: "\f173"; } .fa-tumblr-square:before { - content: "\f174"; + content: "\f174"; } .fa-long-arrow-down:before { - content: "\f175"; + content: "\f175"; } .fa-long-arrow-up:before { - content: "\f176"; + content: "\f176"; } .fa-long-arrow-left:before { - content: "\f177"; + content: "\f177"; } .fa-long-arrow-right:before { - content: "\f178"; + content: "\f178"; } .fa-apple:before { - content: "\f179"; + content: "\f179"; } .fa-windows:before { - content: "\f17a"; + content: "\f17a"; } .fa-android:before { - content: "\f17b"; + content: "\f17b"; } .fa-linux:before { - content: "\f17c"; + content: "\f17c"; } .fa-dribbble:before { - content: "\f17d"; + content: "\f17d"; } .fa-skype:before { - content: "\f17e"; + content: "\f17e"; } .fa-foursquare:before { - content: "\f180"; + content: "\f180"; } .fa-trello:before { - content: "\f181"; + content: "\f181"; } .fa-female:before { - content: "\f182"; + content: "\f182"; } .fa-male:before { - content: "\f183"; + content: "\f183"; } .fa-gittip:before, .fa-gratipay:before { - content: "\f184"; + content: "\f184"; } .fa-sun-o:before { - content: "\f185"; + content: "\f185"; } .fa-moon-o:before { - content: "\f186"; + content: "\f186"; } .fa-archive:before { - content: "\f187"; + content: "\f187"; } .fa-bug:before { - content: "\f188"; + content: "\f188"; } .fa-vk:before { - content: "\f189"; + content: "\f189"; } .fa-weibo:before { - content: "\f18a"; + content: "\f18a"; } .fa-renren:before { - content: "\f18b"; + content: "\f18b"; } .fa-pagelines:before { - content: "\f18c"; + content: "\f18c"; } .fa-stack-exchange:before { - content: "\f18d"; + content: "\f18d"; } .fa-arrow-circle-o-right:before { - content: "\f18e"; + content: "\f18e"; } .fa-arrow-circle-o-left:before { - content: "\f190"; + content: "\f190"; } .fa-toggle-left:before, .fa-caret-square-o-left:before { - content: "\f191"; + content: "\f191"; } .fa-dot-circle-o:before { - content: "\f192"; + content: "\f192"; } .fa-wheelchair:before { - content: "\f193"; + content: "\f193"; } .fa-vimeo-square:before { - content: "\f194"; + content: "\f194"; } .fa-turkish-lira:before, .fa-try:before { - content: "\f195"; + content: "\f195"; } .fa-plus-square-o:before { - content: "\f196"; + content: "\f196"; } .fa-space-shuttle:before { - content: "\f197"; + content: "\f197"; } .fa-slack:before { - content: "\f198"; + content: "\f198"; } .fa-envelope-square:before { - content: "\f199"; + content: "\f199"; } .fa-wordpress:before { - content: "\f19a"; + content: "\f19a"; } .fa-openid:before { - content: "\f19b"; + content: "\f19b"; } .fa-institution:before, .fa-bank:before, .fa-university:before { - content: "\f19c"; + content: "\f19c"; } .fa-mortar-board:before, .fa-graduation-cap:before { - content: "\f19d"; + content: "\f19d"; } .fa-yahoo:before { - content: "\f19e"; + content: "\f19e"; } .fa-google:before { - content: "\f1a0"; + content: "\f1a0"; } .fa-reddit:before { - content: "\f1a1"; + content: "\f1a1"; } .fa-reddit-square:before { - content: "\f1a2"; + content: "\f1a2"; } .fa-stumbleupon-circle:before { - content: "\f1a3"; + content: "\f1a3"; } .fa-stumbleupon:before { - content: "\f1a4"; + content: "\f1a4"; } .fa-delicious:before { - content: "\f1a5"; + content: "\f1a5"; } .fa-digg:before { - content: "\f1a6"; + content: "\f1a6"; } .fa-pied-piper-pp:before { - content: "\f1a7"; + content: "\f1a7"; } .fa-pied-piper-alt:before { - content: "\f1a8"; + content: "\f1a8"; } .fa-drupal:before { - content: "\f1a9"; + content: "\f1a9"; } .fa-joomla:before { - content: "\f1aa"; + content: "\f1aa"; } .fa-language:before { - content: "\f1ab"; + content: "\f1ab"; } .fa-fax:before { - content: "\f1ac"; + content: "\f1ac"; } .fa-building:before { - content: "\f1ad"; + content: "\f1ad"; } .fa-child:before { - content: "\f1ae"; + content: "\f1ae"; } .fa-paw:before { - content: "\f1b0"; + content: "\f1b0"; } .fa-spoon:before { - content: "\f1b1"; + content: "\f1b1"; } .fa-cube:before { - content: "\f1b2"; + content: "\f1b2"; } .fa-cubes:before { - content: "\f1b3"; + content: "\f1b3"; } .fa-behance:before { - content: "\f1b4"; + content: "\f1b4"; } .fa-behance-square:before { - content: "\f1b5"; + content: "\f1b5"; } .fa-steam:before { - content: "\f1b6"; + content: "\f1b6"; } .fa-steam-square:before { - content: "\f1b7"; + content: "\f1b7"; } .fa-recycle:before { - content: "\f1b8"; + content: "\f1b8"; } .fa-automobile:before, .fa-car:before { - content: "\f1b9"; + content: "\f1b9"; } .fa-cab:before, .fa-taxi:before { - content: "\f1ba"; + content: "\f1ba"; } .fa-tree:before { - content: "\f1bb"; + content: "\f1bb"; } .fa-spotify:before { - content: "\f1bc"; + content: "\f1bc"; } .fa-deviantart:before { - content: "\f1bd"; + content: "\f1bd"; } .fa-soundcloud:before { - content: "\f1be"; + content: "\f1be"; } .fa-database:before { - content: "\f1c0"; + content: "\f1c0"; } .fa-file-pdf-o:before { - content: "\f1c1"; + content: "\f1c1"; } .fa-file-word-o:before { - content: "\f1c2"; + content: "\f1c2"; } .fa-file-excel-o:before { - content: "\f1c3"; + content: "\f1c3"; } .fa-file-powerpoint-o:before { - content: "\f1c4"; + content: "\f1c4"; } .fa-file-photo-o:before, .fa-file-picture-o:before, .fa-file-image-o:before { - content: "\f1c5"; + content: "\f1c5"; } .fa-file-zip-o:before, .fa-file-archive-o:before { - content: "\f1c6"; + content: "\f1c6"; } .fa-file-sound-o:before, .fa-file-audio-o:before { - content: "\f1c7"; + content: "\f1c7"; } .fa-file-movie-o:before, .fa-file-video-o:before { - content: "\f1c8"; + content: "\f1c8"; } .fa-file-code-o:before { - content: "\f1c9"; + content: "\f1c9"; } .fa-vine:before { - content: "\f1ca"; + content: "\f1ca"; } .fa-codepen:before { - content: "\f1cb"; + content: "\f1cb"; } .fa-jsfiddle:before { - content: "\f1cc"; + content: "\f1cc"; } .fa-life-bouy:before, .fa-life-buoy:before, .fa-life-saver:before, .fa-support:before, .fa-life-ring:before { - content: "\f1cd"; + content: "\f1cd"; } .fa-circle-o-notch:before { - content: "\f1ce"; + content: "\f1ce"; } .fa-ra:before, .fa-resistance:before, .fa-rebel:before { - content: "\f1d0"; + content: "\f1d0"; } .fa-ge:before, .fa-empire:before { - content: "\f1d1"; + content: "\f1d1"; } .fa-git-square:before { - content: "\f1d2"; + content: "\f1d2"; } .fa-git:before { - content: "\f1d3"; + content: "\f1d3"; } .fa-y-combinator-square:before, .fa-yc-square:before, .fa-hacker-news:before { - content: "\f1d4"; + content: "\f1d4"; } .fa-tencent-weibo:before { - content: "\f1d5"; + content: "\f1d5"; } .fa-qq:before { - content: "\f1d6"; + content: "\f1d6"; } .fa-wechat:before, .fa-weixin:before { - content: "\f1d7"; + content: "\f1d7"; } .fa-send:before, .fa-paper-plane:before { - content: "\f1d8"; + content: "\f1d8"; } .fa-send-o:before, .fa-paper-plane-o:before { - content: "\f1d9"; + content: "\f1d9"; } .fa-history:before { - content: "\f1da"; + content: "\f1da"; } .fa-circle-thin:before { - content: "\f1db"; + content: "\f1db"; } .fa-header:before { - content: "\f1dc"; + content: "\f1dc"; } .fa-paragraph:before { - content: "\f1dd"; + content: "\f1dd"; } .fa-sliders:before { - content: "\f1de"; + content: "\f1de"; } .fa-share-alt:before { - content: "\f1e0"; + content: "\f1e0"; } .fa-share-alt-square:before { - content: "\f1e1"; + content: "\f1e1"; } .fa-bomb:before { - content: "\f1e2"; + content: "\f1e2"; } .fa-soccer-ball-o:before, .fa-futbol-o:before { - content: "\f1e3"; + content: "\f1e3"; } .fa-tty:before { - content: "\f1e4"; + content: "\f1e4"; } .fa-binoculars:before { - content: "\f1e5"; + content: "\f1e5"; } .fa-plug:before { - content: "\f1e6"; + content: "\f1e6"; } .fa-slideshare:before { - content: "\f1e7"; + content: "\f1e7"; } .fa-twitch:before { - content: "\f1e8"; + content: "\f1e8"; } .fa-yelp:before { - content: "\f1e9"; + content: "\f1e9"; } .fa-newspaper-o:before { - content: "\f1ea"; + content: "\f1ea"; } .fa-wifi:before { - content: "\f1eb"; + content: "\f1eb"; } .fa-calculator:before { - content: "\f1ec"; + content: "\f1ec"; } .fa-paypal:before { - content: "\f1ed"; + content: "\f1ed"; } .fa-google-wallet:before { - content: "\f1ee"; + content: "\f1ee"; } .fa-cc-visa:before { - content: "\f1f0"; + content: "\f1f0"; } .fa-cc-mastercard:before { - content: "\f1f1"; + content: "\f1f1"; } .fa-cc-discover:before { - content: "\f1f2"; + content: "\f1f2"; } .fa-cc-amex:before { - content: "\f1f3"; + content: "\f1f3"; } .fa-cc-paypal:before { - content: "\f1f4"; + content: "\f1f4"; } .fa-cc-stripe:before { - content: "\f1f5"; + content: "\f1f5"; } .fa-bell-slash:before { - content: "\f1f6"; + content: "\f1f6"; } .fa-bell-slash-o:before { - content: "\f1f7"; + content: "\f1f7"; } .fa-trash:before { - content: "\f1f8"; + content: "\f1f8"; } .fa-copyright:before { - content: "\f1f9"; + content: "\f1f9"; } .fa-at:before { - content: "\f1fa"; + content: "\f1fa"; } .fa-eyedropper:before { - content: "\f1fb"; + content: "\f1fb"; } .fa-paint-brush:before { - content: "\f1fc"; + content: "\f1fc"; } .fa-birthday-cake:before { - content: "\f1fd"; + content: "\f1fd"; } .fa-area-chart:before { - content: "\f1fe"; + content: "\f1fe"; } .fa-pie-chart:before { - content: "\f200"; + content: "\f200"; } .fa-line-chart:before { - content: "\f201"; + content: "\f201"; } .fa-lastfm:before { - content: "\f202"; + content: "\f202"; } .fa-lastfm-square:before { - content: "\f203"; + content: "\f203"; } .fa-toggle-off:before { - content: "\f204"; + content: "\f204"; } .fa-toggle-on:before { - content: "\f205"; + content: "\f205"; } .fa-bicycle:before { - content: "\f206"; + content: "\f206"; } .fa-bus:before { - content: "\f207"; + content: "\f207"; } .fa-ioxhost:before { - content: "\f208"; + content: "\f208"; } .fa-angellist:before { - content: "\f209"; + content: "\f209"; } .fa-cc:before { - content: "\f20a"; + content: "\f20a"; } .fa-shekel:before, .fa-sheqel:before, .fa-ils:before { - content: "\f20b"; + content: "\f20b"; } .fa-meanpath:before { - content: "\f20c"; + content: "\f20c"; } .fa-buysellads:before { - content: "\f20d"; + content: "\f20d"; } .fa-connectdevelop:before { - content: "\f20e"; + content: "\f20e"; } .fa-dashcube:before { - content: "\f210"; + content: "\f210"; } .fa-forumbee:before { - content: "\f211"; + content: "\f211"; } .fa-leanpub:before { - content: "\f212"; + content: "\f212"; } .fa-sellsy:before { - content: "\f213"; + content: "\f213"; } .fa-shirtsinbulk:before { - content: "\f214"; + content: "\f214"; } .fa-simplybuilt:before { - content: "\f215"; + content: "\f215"; } .fa-skyatlas:before { - content: "\f216"; + content: "\f216"; } .fa-cart-plus:before { - content: "\f217"; + content: "\f217"; } .fa-cart-arrow-down:before { - content: "\f218"; + content: "\f218"; } .fa-diamond:before { - content: "\f219"; + content: "\f219"; } .fa-ship:before { - content: "\f21a"; + content: "\f21a"; } .fa-user-secret:before { - content: "\f21b"; + content: "\f21b"; } .fa-motorcycle:before { - content: "\f21c"; + content: "\f21c"; } .fa-street-view:before { - content: "\f21d"; + content: "\f21d"; } .fa-heartbeat:before { - content: "\f21e"; + content: "\f21e"; } .fa-venus:before { - content: "\f221"; + content: "\f221"; } .fa-mars:before { - content: "\f222"; + content: "\f222"; } .fa-mercury:before { - content: "\f223"; + content: "\f223"; } .fa-intersex:before, .fa-transgender:before { - content: "\f224"; + content: "\f224"; } .fa-transgender-alt:before { - content: "\f225"; + content: "\f225"; } .fa-venus-double:before { - content: "\f226"; + content: "\f226"; } .fa-mars-double:before { - content: "\f227"; + content: "\f227"; } .fa-venus-mars:before { - content: "\f228"; + content: "\f228"; } .fa-mars-stroke:before { - content: "\f229"; + content: "\f229"; } .fa-mars-stroke-v:before { - content: "\f22a"; + content: "\f22a"; } .fa-mars-stroke-h:before { - content: "\f22b"; + content: "\f22b"; } .fa-neuter:before { - content: "\f22c"; + content: "\f22c"; } .fa-genderless:before { - content: "\f22d"; + content: "\f22d"; } .fa-facebook-official:before { - content: "\f230"; + content: "\f230"; } .fa-pinterest-p:before { - content: "\f231"; + content: "\f231"; } .fa-whatsapp:before { - content: "\f232"; + content: "\f232"; } .fa-server:before { - content: "\f233"; + content: "\f233"; } .fa-user-plus:before { - content: "\f234"; + content: "\f234"; } .fa-user-times:before { - content: "\f235"; + content: "\f235"; } .fa-hotel:before, .fa-bed:before { - content: "\f236"; + content: "\f236"; } .fa-viacoin:before { - content: "\f237"; + content: "\f237"; } .fa-train:before { - content: "\f238"; + content: "\f238"; } .fa-subway:before { - content: "\f239"; + content: "\f239"; } .fa-medium:before { - content: "\f23a"; + content: "\f23a"; } .fa-yc:before, .fa-y-combinator:before { - content: "\f23b"; + content: "\f23b"; } .fa-optin-monster:before { - content: "\f23c"; + content: "\f23c"; } .fa-opencart:before { - content: "\f23d"; + content: "\f23d"; } .fa-expeditedssl:before { - content: "\f23e"; + content: "\f23e"; } .fa-battery-4:before, .fa-battery:before, .fa-battery-full:before { - content: "\f240"; + content: "\f240"; } .fa-battery-3:before, .fa-battery-three-quarters:before { - content: "\f241"; + content: "\f241"; } .fa-battery-2:before, .fa-battery-half:before { - content: "\f242"; + content: "\f242"; } .fa-battery-1:before, .fa-battery-quarter:before { - content: "\f243"; + content: "\f243"; } .fa-battery-0:before, .fa-battery-empty:before { - content: "\f244"; + content: "\f244"; } .fa-mouse-pointer:before { - content: "\f245"; + content: "\f245"; } .fa-i-cursor:before { - content: "\f246"; + content: "\f246"; } .fa-object-group:before { - content: "\f247"; + content: "\f247"; } .fa-object-ungroup:before { - content: "\f248"; + content: "\f248"; } .fa-sticky-note:before { - content: "\f249"; + content: "\f249"; } .fa-sticky-note-o:before { - content: "\f24a"; + content: "\f24a"; } .fa-cc-jcb:before { - content: "\f24b"; + content: "\f24b"; } .fa-cc-diners-club:before { - content: "\f24c"; + content: "\f24c"; } .fa-clone:before { - content: "\f24d"; + content: "\f24d"; } .fa-balance-scale:before { - content: "\f24e"; + content: "\f24e"; } .fa-hourglass-o:before { - content: "\f250"; + content: "\f250"; } .fa-hourglass-1:before, .fa-hourglass-start:before { - content: "\f251"; + content: "\f251"; } .fa-hourglass-2:before, .fa-hourglass-half:before { - content: "\f252"; + content: "\f252"; } .fa-hourglass-3:before, .fa-hourglass-end:before { - content: "\f253"; + content: "\f253"; } .fa-hourglass:before { - content: "\f254"; + content: "\f254"; } .fa-hand-grab-o:before, .fa-hand-rock-o:before { - content: "\f255"; + content: "\f255"; } .fa-hand-stop-o:before, .fa-hand-paper-o:before { - content: "\f256"; + content: "\f256"; } .fa-hand-scissors-o:before { - content: "\f257"; + content: "\f257"; } .fa-hand-lizard-o:before { - content: "\f258"; + content: "\f258"; } .fa-hand-spock-o:before { - content: "\f259"; + content: "\f259"; } .fa-hand-pointer-o:before { - content: "\f25a"; + content: "\f25a"; } .fa-hand-peace-o:before { - content: "\f25b"; + content: "\f25b"; } .fa-trademark:before { - content: "\f25c"; + content: "\f25c"; } .fa-registered:before { - content: "\f25d"; + content: "\f25d"; } .fa-creative-commons:before { - content: "\f25e"; + content: "\f25e"; } .fa-gg:before { - content: "\f260"; + content: "\f260"; } .fa-gg-circle:before { - content: "\f261"; + content: "\f261"; } .fa-tripadvisor:before { - content: "\f262"; + content: "\f262"; } .fa-odnoklassniki:before { - content: "\f263"; + content: "\f263"; } .fa-odnoklassniki-square:before { - content: "\f264"; + content: "\f264"; } .fa-get-pocket:before { - content: "\f265"; + content: "\f265"; } .fa-wikipedia-w:before { - content: "\f266"; + content: "\f266"; } .fa-safari:before { - content: "\f267"; + content: "\f267"; } .fa-chrome:before { - content: "\f268"; + content: "\f268"; } .fa-firefox:before { - content: "\f269"; + content: "\f269"; } .fa-opera:before { - content: "\f26a"; + content: "\f26a"; } .fa-internet-explorer:before { - content: "\f26b"; + content: "\f26b"; } .fa-tv:before, .fa-television:before { - content: "\f26c"; + content: "\f26c"; } .fa-contao:before { - content: "\f26d"; + content: "\f26d"; } .fa-500px:before { - content: "\f26e"; + content: "\f26e"; } .fa-amazon:before { - content: "\f270"; + content: "\f270"; } .fa-calendar-plus-o:before { - content: "\f271"; + content: "\f271"; } .fa-calendar-minus-o:before { - content: "\f272"; + content: "\f272"; } .fa-calendar-times-o:before { - content: "\f273"; + content: "\f273"; } .fa-calendar-check-o:before { - content: "\f274"; + content: "\f274"; } .fa-industry:before { - content: "\f275"; + content: "\f275"; } .fa-map-pin:before { - content: "\f276"; + content: "\f276"; } .fa-map-signs:before { - content: "\f277"; + content: "\f277"; } .fa-map-o:before { - content: "\f278"; + content: "\f278"; } .fa-map:before { - content: "\f279"; + content: "\f279"; } .fa-commenting:before { - content: "\f27a"; + content: "\f27a"; } .fa-commenting-o:before { - content: "\f27b"; + content: "\f27b"; } .fa-houzz:before { - content: "\f27c"; + content: "\f27c"; } .fa-vimeo:before { - content: "\f27d"; + content: "\f27d"; } .fa-black-tie:before { - content: "\f27e"; + content: "\f27e"; } .fa-fonticons:before { - content: "\f280"; + content: "\f280"; } .fa-reddit-alien:before { - content: "\f281"; + content: "\f281"; } .fa-edge:before { - content: "\f282"; + content: "\f282"; } .fa-credit-card-alt:before { - content: "\f283"; + content: "\f283"; } .fa-codiepie:before { - content: "\f284"; + content: "\f284"; } .fa-modx:before { - content: "\f285"; + content: "\f285"; } .fa-fort-awesome:before { - content: "\f286"; + content: "\f286"; } .fa-usb:before { - content: "\f287"; + content: "\f287"; } .fa-product-hunt:before { - content: "\f288"; + content: "\f288"; } .fa-mixcloud:before { - content: "\f289"; + content: "\f289"; } .fa-scribd:before { - content: "\f28a"; + content: "\f28a"; } .fa-pause-circle:before { - content: "\f28b"; + content: "\f28b"; } .fa-pause-circle-o:before { - content: "\f28c"; + content: "\f28c"; } .fa-stop-circle:before { - content: "\f28d"; + content: "\f28d"; } .fa-stop-circle-o:before { - content: "\f28e"; + content: "\f28e"; } .fa-shopping-bag:before { - content: "\f290"; + content: "\f290"; } .fa-shopping-basket:before { - content: "\f291"; + content: "\f291"; } .fa-hashtag:before { - content: "\f292"; + content: "\f292"; } .fa-bluetooth:before { - content: "\f293"; + content: "\f293"; } .fa-bluetooth-b:before { - content: "\f294"; + content: "\f294"; } .fa-percent:before { - content: "\f295"; + content: "\f295"; } .fa-gitlab:before { - content: "\f296"; + content: "\f296"; } .fa-wpbeginner:before { - content: "\f297"; + content: "\f297"; } .fa-wpforms:before { - content: "\f298"; + content: "\f298"; } .fa-envira:before { - content: "\f299"; + content: "\f299"; } .fa-universal-access:before { - content: "\f29a"; + content: "\f29a"; } .fa-wheelchair-alt:before { - content: "\f29b"; + content: "\f29b"; } .fa-question-circle-o:before { - content: "\f29c"; + content: "\f29c"; } .fa-blind:before { - content: "\f29d"; + content: "\f29d"; } .fa-audio-description:before { - content: "\f29e"; + content: "\f29e"; } .fa-volume-control-phone:before { - content: "\f2a0"; + content: "\f2a0"; } .fa-braille:before { - content: "\f2a1"; + content: "\f2a1"; } .fa-assistive-listening-systems:before { - content: "\f2a2"; + content: "\f2a2"; } .fa-asl-interpreting:before, .fa-american-sign-language-interpreting:before { - content: "\f2a3"; + content: "\f2a3"; } .fa-deafness:before, .fa-hard-of-hearing:before, .fa-deaf:before { - content: "\f2a4"; + content: "\f2a4"; } .fa-glide:before { - content: "\f2a5"; + content: "\f2a5"; } .fa-glide-g:before { - content: "\f2a6"; + content: "\f2a6"; } .fa-signing:before, .fa-sign-language:before { - content: "\f2a7"; + content: "\f2a7"; } .fa-low-vision:before { - content: "\f2a8"; + content: "\f2a8"; } .fa-viadeo:before { - content: "\f2a9"; + content: "\f2a9"; } .fa-viadeo-square:before { - content: "\f2aa"; + content: "\f2aa"; } .fa-snapchat:before { - content: "\f2ab"; + content: "\f2ab"; } .fa-snapchat-ghost:before { - content: "\f2ac"; + content: "\f2ac"; } .fa-snapchat-square:before { - content: "\f2ad"; + content: "\f2ad"; } .fa-pied-piper:before { - content: "\f2ae"; + content: "\f2ae"; } .fa-first-order:before { - content: "\f2b0"; + content: "\f2b0"; } .fa-yoast:before { - content: "\f2b1"; + content: "\f2b1"; } .fa-themeisle:before { - content: "\f2b2"; + content: "\f2b2"; } .fa-google-plus-circle:before, .fa-google-plus-official:before { - content: "\f2b3"; + content: "\f2b3"; } .fa-fa:before, .fa-font-awesome:before { - content: "\f2b4"; + content: "\f2b4"; } .fa-handshake-o:before { - content: "\f2b5"; + content: "\f2b5"; } .fa-envelope-open:before { - content: "\f2b6"; + content: "\f2b6"; } .fa-envelope-open-o:before { - content: "\f2b7"; + content: "\f2b7"; } .fa-linode:before { - content: "\f2b8"; + content: "\f2b8"; } .fa-address-book:before { - content: "\f2b9"; + content: "\f2b9"; } .fa-address-book-o:before { - content: "\f2ba"; + content: "\f2ba"; } .fa-vcard:before, .fa-address-card:before { - content: "\f2bb"; + content: "\f2bb"; } .fa-vcard-o:before, .fa-address-card-o:before { - content: "\f2bc"; + content: "\f2bc"; } .fa-user-circle:before { - content: "\f2bd"; + content: "\f2bd"; } .fa-user-circle-o:before { - content: "\f2be"; + content: "\f2be"; } .fa-user-o:before { - content: "\f2c0"; + content: "\f2c0"; } .fa-id-badge:before { - content: "\f2c1"; + content: "\f2c1"; } .fa-drivers-license:before, .fa-id-card:before { - content: "\f2c2"; + content: "\f2c2"; } .fa-drivers-license-o:before, .fa-id-card-o:before { - content: "\f2c3"; + content: "\f2c3"; } .fa-quora:before { - content: "\f2c4"; + content: "\f2c4"; } .fa-free-code-camp:before { - content: "\f2c5"; + content: "\f2c5"; } .fa-telegram:before { - content: "\f2c6"; + content: "\f2c6"; } .fa-thermometer-4:before, .fa-thermometer:before, .fa-thermometer-full:before { - content: "\f2c7"; + content: "\f2c7"; } .fa-thermometer-3:before, .fa-thermometer-three-quarters:before { - content: "\f2c8"; + content: "\f2c8"; } .fa-thermometer-2:before, .fa-thermometer-half:before { - content: "\f2c9"; + content: "\f2c9"; } .fa-thermometer-1:before, .fa-thermometer-quarter:before { - content: "\f2ca"; + content: "\f2ca"; } .fa-thermometer-0:before, .fa-thermometer-empty:before { - content: "\f2cb"; + content: "\f2cb"; } .fa-shower:before { - content: "\f2cc"; + content: "\f2cc"; } .fa-bathtub:before, .fa-s15:before, .fa-bath:before { - content: "\f2cd"; + content: "\f2cd"; } .fa-podcast:before { - content: "\f2ce"; + content: "\f2ce"; } .fa-window-maximize:before { - content: "\f2d0"; + content: "\f2d0"; } .fa-window-minimize:before { - content: "\f2d1"; + content: "\f2d1"; } .fa-window-restore:before { - content: "\f2d2"; + content: "\f2d2"; } .fa-times-rectangle:before, .fa-window-close:before { - content: "\f2d3"; + content: "\f2d3"; } .fa-times-rectangle-o:before, .fa-window-close-o:before { - content: "\f2d4"; + content: "\f2d4"; } .fa-bandcamp:before { - content: "\f2d5"; + content: "\f2d5"; } .fa-grav:before { - content: "\f2d6"; + content: "\f2d6"; } .fa-etsy:before { - content: "\f2d7"; + content: "\f2d7"; } .fa-imdb:before { - content: "\f2d8"; + content: "\f2d8"; } .fa-ravelry:before { - content: "\f2d9"; + content: "\f2d9"; } .fa-eercast:before { - content: "\f2da"; + content: "\f2da"; } .fa-microchip:before { - content: "\f2db"; + content: "\f2db"; } .fa-snowflake-o:before { - content: "\f2dc"; + content: "\f2dc"; } .fa-superpowers:before { - content: "\f2dd"; + content: "\f2dd"; } .fa-wpexplorer:before { - content: "\f2de"; + content: "\f2de"; } .fa-meetup:before { - content: "\f2e0"; + content: "\f2e0"; } .sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - border: 0; + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; } .sr-only-focusable:active, .sr-only-focusable:focus { - position: static; - width: auto; - height: auto; - margin: 0; - overflow: visible; - clip: auto; + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; } diff --git a/web/src/images/resourceQuicIcon.png b/web/src/images/resourceQuicIcon.png new file mode 100644 index 0000000000..b5c871c546 Binary files /dev/null and b/web/src/images/resourceQuicIcon.png differ diff --git a/web/src/js/__tests__/backends/staticSpec.tsx b/web/src/js/__tests__/backends/staticSpec.tsx index 003906b002..59322d5308 100644 --- a/web/src/js/__tests__/backends/staticSpec.tsx +++ b/web/src/js/__tests__/backends/staticSpec.tsx @@ -1,7 +1,7 @@ -import {enableFetchMocks} from "jest-fetch-mock"; -import {TStore} from "../ducks/tutils"; +import { enableFetchMocks } from "jest-fetch-mock"; +import { TStore } from "../ducks/tutils"; import StaticBackend from "../../backends/static"; -import {waitFor} from "../test-utils"; +import { waitFor } from "../test-utils"; enableFetchMocks(); @@ -9,9 +9,9 @@ test("static backend", async () => { fetchMock.mockOnceIf("./flows", "[]"); fetchMock.mockOnceIf("./options", "{}"); const store = TStore(); - const backend = new StaticBackend(store); - await waitFor(() => expect(store.getActions()).toEqual([ - {type: "FLOWS_RECEIVE", cmd: "receive", data: [], resource: "flows"}, - {type: "OPTIONS_RECEIVE", cmd: "receive", data: {}, resource: "options"} - ])) + new StaticBackend(store); + await waitFor(() => { + expect(store.getState().flows.list).toEqual([]); + expect(store.getState().options).toEqual({}); + }); }); diff --git a/web/src/js/__tests__/backends/websocketSpec.tsx b/web/src/js/__tests__/backends/websocketSpec.tsx index ba74985c2c..3babfb6493 100644 --- a/web/src/js/__tests__/backends/websocketSpec.tsx +++ b/web/src/js/__tests__/backends/websocketSpec.tsx @@ -1,67 +1,99 @@ -import {enableFetchMocks} from "jest-fetch-mock"; -import {TStore} from "../ducks/tutils"; +import { enableFetchMocks } from "jest-fetch-mock"; import WebSocketBackend from "../../backends/websocket"; -import {waitFor} from "../test-utils"; +import { waitFor } from "../test-utils"; import * as connectionActions from "../../ducks/connection"; +import { UnknownAction } from "@reduxjs/toolkit"; enableFetchMocks(); test("websocket backend", async () => { - // @ts-ignore - jest.spyOn(global, 'WebSocket').mockImplementation(() => ({addEventListener: () => 0})); + // @ts-expect-error jest mock stuff + jest.spyOn(global, "WebSocket").mockImplementation(() => ({ + addEventListener: () => 0, + })); fetchMock.mockOnceIf("./state", "{}"); fetchMock.mockOnceIf("./flows", "[]"); fetchMock.mockOnceIf("./events", "[]"); fetchMock.mockOnceIf("./options", "{}"); - const store = TStore(); - const backend = new WebSocketBackend(store); + + const actions: Array = []; + const backend = new WebSocketBackend({ dispatch: (e) => actions.push(e) }); backend.onOpen(); - await waitFor(() => expect(store.getActions()).toEqual([ - connectionActions.startFetching(), - {type: "STATE_RECEIVE", cmd: "receive", data: {}, resource: "state"}, - {type: "FLOWS_RECEIVE", cmd: "receive", data: [], resource: "flows"}, - {type: "EVENTS_RECEIVE", cmd: "receive", data: [], resource: "events"}, - {type: "OPTIONS_RECEIVE", cmd: "receive", data: {}, resource: "options"}, - connectionActions.connectionEstablished(), - ])) + await waitFor(() => + expect(actions).toEqual([ + connectionActions.startFetching(), + { + type: "STATE_RECEIVE", + payload: {}, + }, + { + type: "FLOWS_RECEIVE", + cmd: "receive", + data: [], + resource: "flows", + }, + { + type: "EVENTS_RECEIVE", + cmd: "receive", + data: [], + resource: "events", + }, + { + type: "OPTIONS_RECEIVE", + cmd: "receive", + data: {}, + resource: "options", + }, + connectionActions.connectionEstablished(), + ]), + ); - store.clearActions(); + actions.length = 0; backend.onMessage({ - "resource": "events", - "cmd": "add", - "data": {"id": "42", "message": "test", "level": "info"} + resource: "events", + cmd: "add", + data: { id: "42", message: "test", level: "info" }, }); - expect(store.getActions()).toEqual([{ - "cmd": "add", - "data": {"id": "42", "level": "info", "message": "test"}, - "resource": "events", - "type": "EVENTS_ADD" - }]); - store.clearActions(); + expect(actions).toEqual([ + { + cmd: "add", + data: { id: "42", level: "info", message: "test" }, + resource: "events", + type: "EVENTS_ADD", + }, + ]); + actions.length = 0; fetchMock.mockOnceIf("./events", "[]"); backend.onMessage({ - "resource": "events", - "cmd": "reset", + resource: "events", + cmd: "reset", }); - await waitFor(() => expect(store.getActions()).toEqual([ - {type: "EVENTS_RECEIVE", cmd: "receive", data: [], resource: "events"}, - connectionActions.connectionEstablished(), - ])) - store.clearActions() + await waitFor(() => + expect(actions).toEqual([ + { + type: "EVENTS_RECEIVE", + cmd: "receive", + data: [], + resource: "events", + }, + connectionActions.connectionEstablished(), + ]), + ); + actions.length = 0; expect(fetchMock.mock.calls).toHaveLength(5); console.error = jest.fn(); backend.onClose(new CloseEvent("Connection closed")); - expect(console.error).toBeCalledTimes(1); - expect(store.getActions()[0].type).toEqual(connectionActions.ConnectionState.ERROR); - store.clearActions(); + expect(console.error).toHaveBeenCalledTimes(1); + expect(actions[0].type).toEqual(connectionActions.ConnectionState.ERROR); + actions.length = 0; backend.onError(null); - expect(console.error).toBeCalledTimes(2); + expect(console.error).toHaveBeenCalledTimes(2); jest.restoreAllMocks(); }); diff --git a/web/src/js/__tests__/components/CaptureSetupSpec.tsx b/web/src/js/__tests__/components/CaptureSetupSpec.tsx deleted file mode 100644 index 57ba97821f..0000000000 --- a/web/src/js/__tests__/components/CaptureSetupSpec.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import * as React from "react" -import {render} from "../test-utils"; -import CaptureSetup from "../../components/CaptureSetup"; -import {TStore} from "../ducks/tutils"; - - -test("CaptureSetup", async () => { - const store = TStore(), - {asFragment} = render(, {store}); - expect(asFragment()).toMatchSnapshot(); -}); diff --git a/web/src/js/__tests__/components/CommandBarSpec.tsx b/web/src/js/__tests__/components/CommandBarSpec.tsx index 47e6bfcea1..70f495bba0 100644 --- a/web/src/js/__tests__/components/CommandBarSpec.tsx +++ b/web/src/js/__tests__/components/CommandBarSpec.tsx @@ -1,67 +1,98 @@ -import * as React from "react" -import {render, screen, userEvent, waitFor} from "../test-utils"; +import * as React from "react"; +import { render, screen, userEvent, waitFor } from "../test-utils"; import CommandBar from "../../components/CommandBar"; -import fetchMock, {enableFetchMocks} from "jest-fetch-mock"; +import fetchMock, { enableFetchMocks } from "jest-fetch-mock"; enableFetchMocks(); test("CommandBar", async () => { - fetchMock.mockOnceIf("./commands", JSON.stringify({ + fetchMock.mockOnceIf( + "./commands", + JSON.stringify({ "flow.decode": { - "help": "Decode flows.", - "parameters": [ - {"name": "flows", "type": "flow[]", "kind": "POSITIONAL_OR_KEYWORD"}, - {"name": "part", "type": "str", "kind": "POSITIONAL_OR_KEYWORD"} + help: "Decode flows.", + parameters: [ + { + name: "flows", + type: "flow[]", + kind: "POSITIONAL_OR_KEYWORD", + }, + { + name: "part", + type: "str", + kind: "POSITIONAL_OR_KEYWORD", + }, ], - "return_type": null, - "signature_help": "flow.decode flows part" + return_type: null, + signature_help: "flow.decode flows part", }, "flow.encode": { - "help": "Encode flows with a specified encoding.", - "parameters": [ - {"name": "flows", "type": "flow[]", "kind": "POSITIONAL_OR_KEYWORD"}, - {"name": "part", "type": "str", "kind": "POSITIONAL_OR_KEYWORD"}, - {"name": "encoding", "type": "choice", "kind": "POSITIONAL_OR_KEYWORD"} + help: "Encode flows with a specified encoding.", + parameters: [ + { + name: "flows", + type: "flow[]", + kind: "POSITIONAL_OR_KEYWORD", + }, + { + name: "part", + type: "str", + kind: "POSITIONAL_OR_KEYWORD", + }, + { + name: "encoding", + type: "choice", + kind: "POSITIONAL_OR_KEYWORD", + }, ], - "return_type": null, - "signature_help": "flow.encode flows part encoding" - } - } - )); - fetchMock.mockOnceIf("./commands/commands.history.get", JSON.stringify({value: ["foo"]})); - fetchMock.mockOnceIf("./commands/commands.history.add", JSON.stringify({value: null})); - fetchMock.mockOnceIf("./commands/flow.encode", JSON.stringify({value: null})); + return_type: null, + signature_help: "flow.encode flows part encoding", + }, + }), + ); + fetchMock.mockOnceIf( + "./commands/commands.history.get", + JSON.stringify({ value: ["foo"] }), + ); + fetchMock.mockOnceIf( + "./commands/commands.history.add", + JSON.stringify({ value: null }), + ); + fetchMock.mockOnceIf( + "./commands/flow.encode", + JSON.stringify({ value: null }), + ); - const {asFragment} = render(); + const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); - await waitFor(() => screen.getByText('["flow.decode","flow.encode"]')) + await waitFor(() => screen.getByText('["flow.decode","flow.encode"]')); expect(asFragment()).toMatchSnapshot(); const input = screen.getByPlaceholderText("Enter command"); - userEvent.type(input, 'x'); + await userEvent.type(input, "x"); expect(screen.getByText("[]")).toBeInTheDocument(); - userEvent.type(input, "{backspace}"); + await userEvent.type(input, "{backspace}"); - userEvent.type(input, 'fl'); - userEvent.tab(); - expect(input).toHaveValue('flow.decode'); - userEvent.tab(); - expect(input).toHaveValue('flow.encode'); + await userEvent.type(input, "fl"); + await userEvent.tab(); + expect(input).toHaveValue("flow.decode"); + await userEvent.tab(); + expect(input).toHaveValue("flow.encode"); - fetchMock.mockOnce(JSON.stringify({value: null})); - userEvent.type(input, "{enter}"); + fetchMock.mockOnce(JSON.stringify({ value: null })); + await userEvent.type(input, "{enter}"); await waitFor(() => screen.getByText("Command Result")); - userEvent.type(input, "{arrowdown}"); + await userEvent.type(input, "{arrowdown}"); expect(input).toHaveValue(""); - userEvent.type(input, "{arrowup}"); + await userEvent.type(input, "{arrowup}"); expect(input).toHaveValue("flow.encode"); - userEvent.type(input, "{arrowup}"); + await userEvent.type(input, "{arrowup}"); expect(input).toHaveValue("foo"); - userEvent.type(input, "{arrowdown}"); + await userEvent.type(input, "{arrowdown}"); expect(input).toHaveValue("flow.encode"); - userEvent.type(input, "{arrowdown}"); + await userEvent.type(input, "{arrowdown}"); expect(input).toHaveValue(""); }); diff --git a/web/src/js/__tests__/components/EventLog/EventListSpec.tsx b/web/src/js/__tests__/components/EventLog/EventListSpec.tsx index 30e41d336d..b217c128bf 100644 --- a/web/src/js/__tests__/components/EventLog/EventListSpec.tsx +++ b/web/src/js/__tests__/components/EventLog/EventListSpec.tsx @@ -1,22 +1,19 @@ -import * as React from "react" -import EventLogList from '../../../components/EventLog/EventList' -import TestUtils from 'react-dom/test-utils' +import * as React from "react"; +import EventLogList from "../../../components/EventLog/EventList"; +import { EventLogItem, LogLevel } from "../../../ducks/eventLog"; +import { render } from "../../test-utils"; -describe('EventList Component', () => { - let mockEventList = [ - { id: 1, level: 'info', message: 'foo' }, - { id: 2, level: 'error', message: 'bar' } - ], - eventLogList = TestUtils.renderIntoDocument() +describe("EventList Component", () => { + const mockEventList: EventLogItem[] = [ + { id: "1", level: LogLevel.info, message: "foo" }, + { id: "2", level: LogLevel.error, message: "bar" }, + ]; - it('should render correctly', () => { - expect(eventLogList.state).toMatchSnapshot() - expect(eventLogList.props).toMatchSnapshot() - }) - - it('should handle componentWillUnmount', () => { - window.removeEventListener = jest.fn() - eventLogList.componentWillUnmount() - expect(window.removeEventListener).toBeCalledWith('resize', eventLogList.onViewportUpdate) - }) -}) + it("should render correctly", () => { + const { asFragment, unmount } = render( + , + ); + expect(asFragment()).toMatchSnapshot(); + unmount(); // no errors + }); +}); diff --git a/web/src/js/__tests__/components/EventLog/__snapshots__/EventListSpec.tsx.snap b/web/src/js/__tests__/components/EventLog/__snapshots__/EventListSpec.tsx.snap index 10bcb59834..3aaed52c6e 100644 --- a/web/src/js/__tests__/components/EventLog/__snapshots__/EventListSpec.tsx.snap +++ b/web/src/js/__tests__/components/EventLog/__snapshots__/EventListSpec.tsx.snap @@ -1,30 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EventList Component should render correctly 1`] = ` -Object { - "vScroll": Object { - "end": 1, - "paddingBottom": 18, - "paddingTop": 0, - "start": 0, - }, -} -`; - -exports[`EventList Component should render correctly 2`] = ` -Object { - "events": Array [ - Object { - "id": 1, - "level": "info", - "message": "foo", - }, - Object { - "id": 2, - "level": "error", - "message": "bar", - }, - ], - "rowHeight": 18, -} + +
    +    
    +
    + + foo +
    +
    +
    +
    `; diff --git a/web/src/js/__tests__/components/EventLogSpec.tsx b/web/src/js/__tests__/components/EventLogSpec.tsx index e6a74a5463..749a34a6be 100644 --- a/web/src/js/__tests__/components/EventLogSpec.tsx +++ b/web/src/js/__tests__/components/EventLogSpec.tsx @@ -1,56 +1,71 @@ -jest.mock('../../components/EventLog/EventList') +jest.mock("../../components/EventLog/EventList"); -import * as React from "react" -import renderer from 'react-test-renderer' -import EventLog, {PureEventLog} from '../../components/EventLog' -import {Provider} from 'react-redux' -import {TStore} from '../ducks/tutils' +import * as React from "react"; +import renderer from "react-test-renderer"; +import EventLog, { PureEventLog } from "../../components/EventLog"; +import { Provider } from "react-redux"; +import { TStore } from "../ducks/tutils"; -window.addEventListener = jest.fn() -window.removeEventListener = jest.fn() +window.addEventListener = jest.fn(); +window.removeEventListener = jest.fn(); -describe('EventLog Component', () => { +describe("EventLog Component", () => { let store = TStore(), provider = renderer.create( - - ), - tree = provider.toJSON() + + , + ), + tree = provider.toJSON(); - it('should connect to state and render correctly', () => { - expect(tree).toMatchSnapshot() - }) + it("should connect to state and render correctly", () => { + expect(tree).toMatchSnapshot(); + }); - it('should handle toggleFilter', () => { - let debugToggleButton = tree.children[0].children[1].children[0] - debugToggleButton.props.onClick() - }) + it("should handle toggleFilter", () => { + const debugToggleButton = tree.children[0].children[1].children[0]; + debugToggleButton.props.onClick(); + }); provider = renderer.create( - ) - let eventLog = provider.root.findByType(PureEventLog), - mockEvent = {preventDefault: jest.fn()} - - it('should handle DragStart', () => { - eventLog.instance.onDragStart(mockEvent) - expect(mockEvent.preventDefault).toBeCalled() - expect(window.addEventListener).toBeCalledWith('mousemove', eventLog.instance.onDragMove) - expect(window.addEventListener).toBeCalledWith('mouseup', eventLog.instance.onDragStop) - expect(window.addEventListener).toBeCalledWith('dragend', eventLog.instance.onDragStop) - mockEvent.preventDefault.mockClear() - }) - - it('should handle DragMove', () => { - eventLog.instance.onDragMove(mockEvent) - expect(mockEvent.preventDefault).toBeCalled() - mockEvent.preventDefault.mockClear() - }) - - console.error = jest.fn() // silent the error. - it('should handle DragStop', () => { - eventLog.instance.onDragStop(mockEvent) - expect(mockEvent.preventDefault).toBeCalled() - expect(window.removeEventListener).toBeCalledWith('mousemove', eventLog.instance.onDragMove) - }) - -}) + + + , + ); + const eventLog = provider.root.findByType(PureEventLog); + const mockEvent = { preventDefault: jest.fn() }; + + it("should handle DragStart", () => { + eventLog.instance.onDragStart(mockEvent); + expect(mockEvent.preventDefault).toBeCalled(); + expect(window.addEventListener).toBeCalledWith( + "mousemove", + eventLog.instance.onDragMove, + ); + expect(window.addEventListener).toBeCalledWith( + "mouseup", + eventLog.instance.onDragStop, + ); + expect(window.addEventListener).toBeCalledWith( + "dragend", + eventLog.instance.onDragStop, + ); + mockEvent.preventDefault.mockClear(); + }); + + it("should handle DragMove", () => { + eventLog.instance.onDragMove(mockEvent); + expect(mockEvent.preventDefault).toBeCalled(); + mockEvent.preventDefault.mockClear(); + }); + + console.error = jest.fn(); // silent the error. + it("should handle DragStop", () => { + eventLog.instance.onDragStop(mockEvent); + expect(mockEvent.preventDefault).toBeCalled(); + expect(window.removeEventListener).toBeCalledWith( + "mousemove", + eventLog.instance.onDragMove, + ); + }); +}); diff --git a/web/src/js/__tests__/components/FlowTable/FlowColumnsSpec.tsx b/web/src/js/__tests__/components/FlowTable/FlowColumnsSpec.tsx index 27e2b3950b..610ab2d23c 100644 --- a/web/src/js/__tests__/components/FlowTable/FlowColumnsSpec.tsx +++ b/web/src/js/__tests__/components/FlowTable/FlowColumnsSpec.tsx @@ -1,103 +1,121 @@ -import * as React from "react" -import renderer from 'react-test-renderer' -import FlowColumns from '../../../components/FlowTable/FlowColumns' -import {TFlow, TTCPFlow} from '../../ducks/tutils' -import {render} from "../../test-utils"; +import * as React from "react"; +import renderer from "react-test-renderer"; +import FlowColumns from "../../../components/FlowTable/FlowColumns"; +import { TFlow, TTCPFlow } from "../../ducks/tutils"; +import { render } from "../../test-utils"; test("should render columns", async () => { const tflow = TFlow(); Object.entries(FlowColumns).forEach(([name, Col]) => { - const {asFragment} = render(
    %s%s
    - - - -
    ) + const { asFragment } = render( + + + + + + +
    , + ); expect(asFragment()).toMatchSnapshot(name); - }) + }); }); - -describe('Flowcolumns Components', () => { - it('should render IconColumn', () => { +describe("Flowcolumns Components", () => { + it("should render IconColumn", () => { let tcpflow = TTCPFlow(), - iconColumn = renderer.create(), - tree = iconColumn.toJSON() - expect(tree).toMatchSnapshot() + iconColumn = renderer.create(), + tree = iconColumn.toJSON(); + expect(tree).toMatchSnapshot(); - let tflow = {...TFlow(), websocket: undefined}; - iconColumn = renderer.create() - tree = iconColumn.toJSON() + const tflow = { ...TFlow(), websocket: undefined }; + iconColumn = renderer.create(); + tree = iconColumn.toJSON(); // plain - expect(tree).toMatchSnapshot() + expect(tree).toMatchSnapshot(); // not modified - tflow.response.status_code = 304 - iconColumn = renderer.create() - tree = iconColumn.toJSON() - expect(tree).toMatchSnapshot() + tflow.response.status_code = 304; + iconColumn = renderer.create(); + tree = iconColumn.toJSON(); + expect(tree).toMatchSnapshot(); // redirect - tflow.response.status_code = 302 - iconColumn = renderer.create() - tree = iconColumn.toJSON() - expect(tree).toMatchSnapshot() + tflow.response.status_code = 302; + iconColumn = renderer.create(); + tree = iconColumn.toJSON(); + expect(tree).toMatchSnapshot(); // image - let imageFlow = {...TFlow(), websocket: undefined} - imageFlow.response.headers = [['Content-Type', 'image/jpeg']] - iconColumn = renderer.create() - tree = iconColumn.toJSON() - expect(tree).toMatchSnapshot() + const imageFlow = { ...TFlow(), websocket: undefined }; + imageFlow.response.headers = [["Content-Type", "image/jpeg"]]; + iconColumn = renderer.create(); + tree = iconColumn.toJSON(); + expect(tree).toMatchSnapshot(); // javascript - let jsFlow = {...TFlow(), websocket: undefined} - jsFlow.response.headers = [['Content-Type', 'application/x-javascript']] - iconColumn = renderer.create() - tree = iconColumn.toJSON() - expect(tree).toMatchSnapshot() + const jsFlow = { ...TFlow(), websocket: undefined }; + jsFlow.response.headers = [ + ["Content-Type", "application/x-javascript"], + ]; + iconColumn = renderer.create(); + tree = iconColumn.toJSON(); + expect(tree).toMatchSnapshot(); // css - let cssFlow = {...TFlow(), websocket: undefined} - cssFlow.response.headers = [['Content-Type', 'text/css']] - iconColumn = renderer.create() - tree = iconColumn.toJSON() - expect(tree).toMatchSnapshot() + const cssFlow = { ...TFlow(), websocket: undefined }; + cssFlow.response.headers = [["Content-Type", "text/css"]]; + iconColumn = renderer.create(); + tree = iconColumn.toJSON(); + expect(tree).toMatchSnapshot(); // html - let htmlFlow = {...TFlow(), websocket: undefined} - htmlFlow.response.headers = [['Content-Type', 'text/html']] - iconColumn = renderer.create() - tree = iconColumn.toJSON() - expect(tree).toMatchSnapshot() + const htmlFlow = { ...TFlow(), websocket: undefined }; + htmlFlow.response.headers = [["Content-Type", "text/html"]]; + iconColumn = renderer.create(); + tree = iconColumn.toJSON(); + expect(tree).toMatchSnapshot(); // default - let fooFlow = {...TFlow(), websocket: undefined} - fooFlow.response.headers = [['Content-Type', 'foo']] - iconColumn = renderer.create() - tree = iconColumn.toJSON() - expect(tree).toMatchSnapshot() + const fooFlow = { ...TFlow(), websocket: undefined }; + fooFlow.response.headers = [["Content-Type", "foo"]]; + iconColumn = renderer.create(); + tree = iconColumn.toJSON(); + expect(tree).toMatchSnapshot(); // no response - let noResponseFlow = {...TFlow(), response: undefined} - iconColumn = renderer.create() - tree = iconColumn.toJSON() - expect(tree).toMatchSnapshot() - }) + const noResponseFlow = { ...TFlow(), response: undefined }; + iconColumn = renderer.create( + , + ); + tree = iconColumn.toJSON(); + expect(tree).toMatchSnapshot(); + }); - it('should render pathColumn', () => { + it("should render pathColumn", () => { let tflow = TFlow(), - pathColumn = renderer.create(), - tree = pathColumn.toJSON() - expect(tree).toMatchSnapshot() + pathColumn = renderer.create(), + tree = pathColumn.toJSON(); + expect(tree).toMatchSnapshot(); - tflow.error.msg = 'Connection killed.' - tflow.intercepted = true - pathColumn = renderer.create() - tree = pathColumn.toJSON() - expect(tree).toMatchSnapshot() - }) + tflow.error.msg = "Connection killed."; + tflow.intercepted = true; + pathColumn = renderer.create(); + tree = pathColumn.toJSON(); + expect(tree).toMatchSnapshot(); + }); - it('should render TimeColumn', () => { + it("should render TimeColumn", () => { let tflow = TFlow(), - timeColumn = renderer.create(), - tree = timeColumn.toJSON() - expect(tree).toMatchSnapshot() + timeColumn = renderer.create(), + tree = timeColumn.toJSON(); + expect(tree).toMatchSnapshot(); + + const noResponseFlow = { ...tflow, response: undefined }; + timeColumn = renderer.create( + , + ); + tree = timeColumn.toJSON(); + expect(tree).toMatchSnapshot(); + }); - let noResponseFlow = {...tflow, response: undefined} - timeColumn = renderer.create() - tree = timeColumn.toJSON() - expect(tree).toMatchSnapshot() - }) -}) + it("should render CommentColumn", () => { + const tflow = TFlow(); + const commentColumn = renderer.create( + , + ); + const tree = commentColumn.toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/web/src/js/__tests__/components/FlowTable/FlowRowSpec.tsx b/web/src/js/__tests__/components/FlowTable/FlowRowSpec.tsx index c1361ac3cc..284695ee98 100644 --- a/web/src/js/__tests__/components/FlowTable/FlowRowSpec.tsx +++ b/web/src/js/__tests__/components/FlowTable/FlowRowSpec.tsx @@ -1,21 +1,22 @@ -import * as React from "react" -import FlowRow from '../../../components/FlowTable/FlowRow' -import {testState} from '../../ducks/tutils' -import {fireEvent, render, screen} from "../../test-utils"; -import {createAppStore} from "../../../ducks"; - +import * as React from "react"; +import FlowRow from "../../../components/FlowTable/FlowRow"; +import { fireEvent, render, screen } from "../../test-utils"; +import { TStore } from "../../ducks/tutils"; test("FlowRow", async () => { - const store = createAppStore(testState), - tflow2 = store.getState().flows.list[0], - {asFragment} = render( + const store = TStore(); + const tflow = store.getState().flows.list[3]; + const { asFragment } = render( +
    - + -
    , {store}) - expect(asFragment()).toMatchSnapshot() + , + { store }, + ); + expect(asFragment()).toMatchSnapshot(); - expect(store.getState().flows.selected[0]).not.toBe(store.getState().flows.list[0].id) - fireEvent.click(screen.getByText("http://address:22/path")) - expect(store.getState().flows.selected[0]).toBe(store.getState().flows.list[0].id) -}) + expect(store.getState().flows.selected[0]).not.toBe(tflow.id); + fireEvent.click(screen.getByText("QUERY")); + expect(store.getState().flows.selected[0]).toBe(tflow.id); +}); diff --git a/web/src/js/__tests__/components/FlowTable/FlowTableHeadSpec.tsx b/web/src/js/__tests__/components/FlowTable/FlowTableHeadSpec.tsx index 511c2ccc26..c7a0256cab 100644 --- a/web/src/js/__tests__/components/FlowTable/FlowTableHeadSpec.tsx +++ b/web/src/js/__tests__/components/FlowTable/FlowTableHeadSpec.tsx @@ -1,29 +1,27 @@ -import * as React from "react" -import FlowTableHead from '../../../components/FlowTable/FlowTableHead' -import {Provider} from 'react-redux' -import {TStore} from '../../ducks/tutils' -import {fireEvent, render, screen} from "@testing-library/react"; -import {setSort} from "../../../ducks/flows"; - +import * as React from "react"; +import FlowTableHead from "../../../components/FlowTable/FlowTableHead"; +import { Provider } from "react-redux"; +import { TStore } from "../../ducks/tutils"; +import { fireEvent, render, screen } from "@testing-library/react"; test("FlowTableHead Component", async () => { + const store = TStore(); + const { asFragment } = render( + + + + + +
    +
    , + ); + expect(asFragment()).toMatchSnapshot(); - const store = TStore(), - {asFragment} = render( - - - - - -
    -
    - ) - expect(asFragment()).toMatchSnapshot() - - fireEvent.click(screen.getByText("Size")) - - expect(store.getActions()).toStrictEqual([ - setSort("size", false) - ] - ) -}) + fireEvent.click(screen.getByText("Size")); + expect(store.getState().flows.sort).toEqual({ + column: "size", + desc: false, + }); + fireEvent.click(screen.getByText("Size")); + expect(store.getState().flows.sort).toEqual({ column: "size", desc: true }); +}); diff --git a/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.tsx.snap b/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.tsx.snap index b8baf8486e..55896842ff 100644 --- a/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.tsx.snap +++ b/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.tsx.snap @@ -1,5 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Flowcolumns Components should render CommentColumn 1`] = ` + + I'm a comment! + +`; + exports[`Flowcolumns Components should render IconColumn 1`] = ` - - + /> http://address:22/path `; @@ -144,13 +150,27 @@ exports[`Flowcolumns Components should render pathColumn 2`] = ` /> - - + /> http://address:22/path `; +exports[`should render columns: comment 1`] = ` + + + + + + + +
    + I'm a comment! +
    +
    +`; + exports[`should render columns: icon 1`] = ` @@ -169,6 +189,22 @@ exports[`should render columns: icon 1`] = ` `; +exports[`should render columns: index 1`] = ` + +
    + + + + + +
    + 1 +
    +
    +`; + exports[`should render columns: method 1`] = ` @@ -299,3 +335,19 @@ exports[`should render columns: tls 1`] = `
    `; + +exports[`should render columns: version 1`] = ` + + + + + + + +
    + HTTP/1.1 +
    +
    +`; diff --git a/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowRowSpec.tsx.snap b/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowRowSpec.tsx.snap index 323d075f58..cbf2745006 100644 --- a/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowRowSpec.tsx.snap +++ b/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowRowSpec.tsx.snap @@ -5,7 +5,7 @@ exports[`FlowRow 1`] = ` + />
    - - http://address:22/path + dns.google A = 8.8.8.8, 8.8.4.4 - WSS + QUERY - 200 + NOERROR - 43b + 8b - 5s + 1s -
    - - - -
    -
    diff --git a/web/src/js/__tests__/components/FlowTableSpec.tsx b/web/src/js/__tests__/components/FlowTableSpec.tsx index 9bc67ebf43..c61294218d 100644 --- a/web/src/js/__tests__/components/FlowTableSpec.tsx +++ b/web/src/js/__tests__/components/FlowTableSpec.tsx @@ -1,50 +1,27 @@ -import * as React from "react" -import renderer from 'react-test-renderer' -import {PureFlowTable as FlowTable} from '../../components/FlowTable' -import TestUtils from 'react-dom/test-utils' -import { TFlow, TStore } from '../ducks/tutils' -import { Provider } from 'react-redux' +import * as React from "react"; +import FlowTable, { PureFlowTable } from "../../components/FlowTable"; -window.addEventListener = jest.fn() +import { act, render } from "../test-utils"; +import { select } from "../../ducks/flows"; -describe('FlowTable Component', () => { - let selectFn = jest.fn(), - tflow = TFlow(), - store = TStore() +window.addEventListener = jest.fn(); - it('should render correctly', () => { - let provider = renderer.create( - - - ), - tree = provider.toJSON() - expect(tree).toMatchSnapshot() - }) +describe("FlowTable Component", () => { + it("should render correctly", () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); - let provider = renderer.create( - - - ), - flowTable = provider.root.findByType(FlowTable) + it("should scroll current selection into view", () => { + const height = PureFlowTable.defaultProps.rowHeight; + const { asFragment, store } = render( +
    + +
    , + ); + expect(asFragment()).toMatchSnapshot(); - it('should handle componentWillUnmount', () => { - flowTable.instance.UNSAFE_componentWillUnmount() - expect(window.addEventListener).toBeCalledWith('resize', flowTable.instance.onViewportUpdate) - }) - - it('should handle componentDidUpdate', () => { - // flowTable.shouldScrollIntoView == false - expect(flowTable.instance.componentDidUpdate()).toEqual(undefined) - // rowTop - headHeight < viewportTop - flowTable.instance.shouldScrollIntoView = true - flowTable.instance.componentDidUpdate() - // rowBottom > viewportTop + viewportHeight - flowTable.instance.shouldScrollIntoView = true - flowTable.instance.componentDidUpdate() - }) - - it('should handle componentWillReceiveProps', () => { - flowTable.instance.UNSAFE_componentWillReceiveProps({selected: tflow}) - expect(flowTable.instance.shouldScrollIntoView).toBeTruthy() - }) -}) + act(() => store.dispatch(select(store.getState().flows.view[3].id))); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/web/src/js/__tests__/components/FlowViewSpec.tsx b/web/src/js/__tests__/components/FlowViewSpec.tsx index 6f8e127508..a24807eefe 100644 --- a/web/src/js/__tests__/components/FlowViewSpec.tsx +++ b/web/src/js/__tests__/components/FlowViewSpec.tsx @@ -1,16 +1,15 @@ -import * as React from "react" -import {render, screen} from "../test-utils"; +import * as React from "react"; +import { act, fireEvent, render, screen } from "../test-utils"; import FlowView from "../../components/FlowView"; -import * as flowActions from "../../ducks/flows" -import fetchMock, {enableFetchMocks} from "jest-fetch-mock"; -import {fireEvent} from "@testing-library/react"; +import * as flowActions from "../../ducks/flows"; +import fetchMock, { enableFetchMocks } from "jest-fetch-mock"; enableFetchMocks(); test("FlowView", async () => { fetchMock.mockReject(new Error("backend missing")); - const {asFragment, store} = render(); + const { asFragment, getByTestId, store } = render(); expect(asFragment()).toMatchSnapshot(); fireEvent.click(screen.getByText("Response")); @@ -25,18 +24,25 @@ test("FlowView", async () => { fireEvent.click(screen.getByText("Timing")); expect(asFragment()).toMatchSnapshot(); + fireEvent.click(screen.getByText("Comment")); + expect(asFragment()).toMatchSnapshot(); + fireEvent.click(screen.getByText("Error")); expect(asFragment()).toMatchSnapshot(); - store.dispatch(flowActions.select(store.getState().flows.list[2].id)); + act(() => + store.dispatch(flowActions.select(store.getState().flows.list[2].id)), + ); - fireEvent.click(screen.getByText("TCP Messages")); + fireEvent.click(screen.getByText("Stream Data")); expect(asFragment()).toMatchSnapshot(); fireEvent.click(screen.getByText("Error")); expect(asFragment()).toMatchSnapshot(); - store.dispatch(flowActions.select(store.getState().flows.list[3].id)); + act(() => + store.dispatch(flowActions.select(store.getState().flows.list[3].id)), + ); fireEvent.click(screen.getByText("Request")); expect(asFragment()).toMatchSnapshot(); @@ -47,11 +53,16 @@ test("FlowView", async () => { fireEvent.click(screen.getByText("Error")); expect(asFragment()).toMatchSnapshot(); - store.dispatch(flowActions.select(store.getState().flows.list[4].id)); + act(() => + store.dispatch(flowActions.select(store.getState().flows.list[4].id)), + ); - fireEvent.click(screen.getByText("UDP Messages")); + fireEvent.click(screen.getByText("Datagrams")); expect(asFragment()).toMatchSnapshot(); fireEvent.click(screen.getByText("Error")); expect(asFragment()).toMatchSnapshot(); + + fireEvent.click(getByTestId("close-button-id")); + expect(store.getState().flows.selected).toEqual([]); }); diff --git a/web/src/js/__tests__/components/Header/CaptureMenuSpec.tsx b/web/src/js/__tests__/components/Header/CaptureMenuSpec.tsx new file mode 100644 index 0000000000..fc67f04e1b --- /dev/null +++ b/web/src/js/__tests__/components/Header/CaptureMenuSpec.tsx @@ -0,0 +1,10 @@ +import * as React from "react"; +import CaptureMenu from "../../../components/Header/CaptureMenu"; +import { render } from "../../test-utils"; + +describe("CaptureMenu Component", () => { + it("should render correctly", () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/web/src/js/__tests__/components/Header/ConnectionIndicatorSpec.tsx b/web/src/js/__tests__/components/Header/ConnectionIndicatorSpec.tsx index 078c1c267a..0e631b9d07 100644 --- a/web/src/js/__tests__/components/Header/ConnectionIndicatorSpec.tsx +++ b/web/src/js/__tests__/components/Header/ConnectionIndicatorSpec.tsx @@ -1,22 +1,21 @@ -import * as React from "react" -import ConnectionIndicator from '../../../components/Header/ConnectionIndicator' -import * as connectionActions from '../../../ducks/connection' -import {render} from "../../test-utils" - +import * as React from "react"; +import ConnectionIndicator from "../../../components/Header/ConnectionIndicator"; +import * as connectionActions from "../../../ducks/connection"; +import { act, render } from "../../test-utils"; test("ConnectionIndicator", async () => { - const {asFragment, store} = render(); - expect(asFragment()).toMatchSnapshot() + const { asFragment, store } = render(); + expect(asFragment()).toMatchSnapshot(); - store.dispatch(connectionActions.startFetching()) - expect(asFragment()).toMatchSnapshot() + act(() => store.dispatch(connectionActions.startFetching())); + expect(asFragment()).toMatchSnapshot(); - store.dispatch(connectionActions.connectionEstablished()) - expect(asFragment()).toMatchSnapshot() + act(() => store.dispatch(connectionActions.connectionEstablished())); + expect(asFragment()).toMatchSnapshot(); - store.dispatch(connectionActions.connectionError("wat")) - expect(asFragment()).toMatchSnapshot() + act(() => store.dispatch(connectionActions.connectionError("wat"))); + expect(asFragment()).toMatchSnapshot(); - store.dispatch(connectionActions.setOffline()) - expect(asFragment()).toMatchSnapshot() + act(() => store.dispatch(connectionActions.setOffline())); + expect(asFragment()).toMatchSnapshot(); }); diff --git a/web/src/js/__tests__/components/Header/FileMenuSpec.tsx b/web/src/js/__tests__/components/Header/FileMenuSpec.tsx index a5ce8d634f..d503f2fbca 100644 --- a/web/src/js/__tests__/components/Header/FileMenuSpec.tsx +++ b/web/src/js/__tests__/components/Header/FileMenuSpec.tsx @@ -1,20 +1,19 @@ -import * as React from "react" -import renderer from 'react-test-renderer' -import FileMenu from '../../../components/Header/FileMenu' -import {Provider} from "react-redux"; -import {TStore} from "../../ducks/tutils"; +import * as React from "react"; +import renderer from "react-test-renderer"; +import FileMenu from "../../../components/Header/FileMenu"; +import { Provider } from "react-redux"; +import { TStore } from "../../ducks/tutils"; -describe('FileMenu Component', () => { +describe("FileMenu Component", () => { + const store = TStore(); + const fileMenu = renderer.create( + + + , + ); + const tree = fileMenu.toJSON(); - let store = TStore(), - fileMenu = renderer.create( - - - - ), - tree = fileMenu.toJSON() - - it('should render correctly', () => { - expect(tree).toMatchSnapshot() - }) -}) + it("should render correctly", () => { + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/web/src/js/__tests__/components/Header/FilterDocsSpec.tsx b/web/src/js/__tests__/components/Header/FilterDocsSpec.tsx index 4feff94be1..a1e48e9d5d 100644 --- a/web/src/js/__tests__/components/Header/FilterDocsSpec.tsx +++ b/web/src/js/__tests__/components/Header/FilterDocsSpec.tsx @@ -1,20 +1,24 @@ -import * as React from "react" -import FilterDocs from '../../../components/Header/FilterDocs' -import {enableFetchMocks} from "jest-fetch-mock"; -import {render, screen, waitFor} from "../../test-utils"; +import * as React from "react"; +import FilterDocs from "../../../components/Header/FilterDocs"; +import { enableFetchMocks } from "jest-fetch-mock"; +import { render, screen, waitFor } from "../../test-utils"; enableFetchMocks(); test("FilterDocs Component", async () => { + fetchMock.mockOnceIf( + "./filter-help", + JSON.stringify({ + commands: [ + ["cmd1", "foo"], + ["cmd2", "bar"], + ], + }), + ); - fetchMock.mockOnceIf("./filter-help", JSON.stringify({ - commands: [['cmd1', 'foo'], ['cmd2', 'bar']] - })) - - const {asFragment} = render( 0}/>); + const { asFragment } = render( 0} />); expect(asFragment()).toMatchSnapshot(); await waitFor(() => screen.getByText("cmd1")); expect(asFragment()).toMatchSnapshot(); - -}) +}); diff --git a/web/src/js/__tests__/components/Header/FilterInputSpec.tsx b/web/src/js/__tests__/components/Header/FilterInputSpec.tsx index 9dc324cdb7..2a9e9367ab 100644 --- a/web/src/js/__tests__/components/Header/FilterInputSpec.tsx +++ b/web/src/js/__tests__/components/Header/FilterInputSpec.tsx @@ -1,94 +1,143 @@ -import * as React from "react" -import renderer from 'react-test-renderer' -import FilterInput from '../../../components/Header/FilterInput' -import FilterDocs from '../../../components/Header/FilterDocs' -import TestUtil from 'react-dom/test-utils' -import ReactDOM from 'react-dom' - -describe('FilterInput Component', () => { - it('should render correctly', () => { - let filterInput = renderer.create( - undefined} value="42"/> - ), - tree = filterInput.toJSON() - expect(tree).toMatchSnapshot() - }) - - let filterInput = TestUtil.renderIntoDocument( - ) - it('should handle componentWillReceiveProps', () => { - filterInput.UNSAFE_componentWillReceiveProps({value: 'foo'}) - expect(filterInput.state.value).toEqual('foo') - }) - - it('should handle isValid', () => { +import * as React from "react"; +import renderer from "react-test-renderer"; +import FilterInput from "../../../components/Header/FilterInput"; +import FilterDocs from "../../../components/Header/FilterDocs"; +import { act, render } from "../../test-utils"; + +describe("FilterInput Component", () => { + it("should render correctly", () => { + const filterInput = renderer.create( + undefined} + value="42" + />, + ); + const tree = filterInput.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + function dummyInput(): FilterInput { + const ref = React.createRef(); + render( + , + ); + return ref.current!; + } + + it("should handle componentWillReceiveProps", () => { + const { rerender, getByDisplayValue } = render( + null} + />, + ); + rerender( + null} + />, + ); + expect(getByDisplayValue("bar")).toBeInTheDocument(); + }); + + it("should handle isValid", () => { + const filterInput = dummyInput(); // valid - expect(filterInput.isValid("~u foo")).toBeTruthy() - expect(filterInput.isValid("~foo bar")).toBeFalsy() - }) - - it('should handle getDesc', () => { - filterInput.state.value = '' - expect(filterInput.getDesc().type).toEqual(FilterDocs) - - filterInput.state.value = '~u foo' - expect(filterInput.getDesc()).toEqual('url matches /foo/i') - - filterInput.state.value = '~foo bar' - expect(filterInput.getDesc()).toEqual('SyntaxError: Expected filter expression but \"~\" found.') - }) - - it('should handle change', () => { - let mockEvent = { target: { value: '~a bar'} } - filterInput.onChange(mockEvent) - expect(filterInput.state.value).toEqual('~a bar') - expect(filterInput.props.onChange).toBeCalledWith('~a bar') - }) - - it('should handle focus', () => { - filterInput.onFocus() - expect(filterInput.state.focus).toBeTruthy() - }) - - it('should handle blur', () => { - filterInput.onBlur() - expect(filterInput.state.focus).toBeFalsy() - }) - - it('should handle mouseEnter', () => { - filterInput.onMouseEnter() - expect(filterInput.state.mousefocus).toBeTruthy() - }) - - it('should handle mouseLeave', () => { - filterInput.onMouseLeave() - expect(filterInput.state.mousefocus).toBeFalsy() - }) - - let input = ReactDOM.findDOMNode(filterInput.refs.input) - - it('should handle keyDown', () => { - input.blur = jest.fn() - let mockEvent = { + expect(filterInput.isValid("~u foo")).toBeTruthy(); + expect(filterInput.isValid("~foo bar")).toBeFalsy(); + }); + + it("should handle getDesc", () => { + const filterInput = dummyInput(); + + act(() => filterInput.setState({ value: "" })); + expect(filterInput.getDesc().type).toEqual(FilterDocs); + + act(() => filterInput.setState({ value: "~u foo" })); + expect(filterInput.getDesc()).toEqual("url matches /foo/i"); + + act(() => filterInput.setState({ value: "~foo bar" })); + expect(filterInput.getDesc()).toEqual( + 'SyntaxError: Expected filter expression but "~" found.', + ); + }); + + it("should handle change", () => { + const filterInput = dummyInput(); + const mockEvent = { target: { value: "~a bar" } }; + act(() => filterInput.onChange(mockEvent)); + expect(filterInput.state.value).toEqual("~a bar"); + expect(filterInput.props.onChange).toBeCalledWith("~a bar"); + }); + + it("should handle focus", () => { + const filterInput = dummyInput(); + act(() => filterInput.onFocus()); + expect(filterInput.state.focus).toBeTruthy(); + }); + + it("should handle blur", () => { + const filterInput = dummyInput(); + act(() => filterInput.onBlur()); + expect(filterInput.state.focus).toBeFalsy(); + }); + + it("should handle mouseEnter", () => { + const filterInput = dummyInput(); + act(() => filterInput.onMouseEnter()); + expect(filterInput.state.mousefocus).toBeTruthy(); + }); + + it("should handle mouseLeave", () => { + const filterInput = dummyInput(); + act(() => filterInput.onMouseLeave()); + expect(filterInput.state.mousefocus).toBeFalsy(); + }); + + it("should handle keyDown", () => { + const filterInput = dummyInput(); + const input = filterInput.inputRef.current!; + input.blur = jest.fn(); + const mockEvent = { key: "Escape", - stopPropagation: jest.fn() - } - filterInput.onKeyDown(mockEvent) - expect(input.blur).toBeCalled() - expect(filterInput.state.mousefocus).toBeFalsy() - expect(mockEvent.stopPropagation).toBeCalled() - }) - - it('should handle selectFilter', () => { - input.focus = jest.fn() - filterInput.selectFilter('bar') - expect(filterInput.state.value).toEqual('bar') - expect(input.focus).toBeCalled() - }) - - it('should handle select', () => { - input.select = jest.fn() - filterInput.select() - expect(input.select).toBeCalled() - }) -}) + stopPropagation: jest.fn(), + }; + act(() => filterInput.onKeyDown(mockEvent)); + expect(input.blur).toBeCalled(); + expect(filterInput.state.mousefocus).toBeFalsy(); + expect(mockEvent.stopPropagation).toBeCalled(); + }); + + it("should handle selectFilter", () => { + const filterInput = dummyInput(); + const input = filterInput.inputRef.current!; + input.focus = jest.fn(); + act(() => filterInput.selectFilter("bar")); + expect(filterInput.state.value).toEqual("bar"); + expect(input.focus).toBeCalled(); + }); + + it("should handle select", () => { + const filterInput = dummyInput(); + const input = filterInput.inputRef.current!; + input.select = jest.fn(); + act(() => filterInput.select()); + expect(input.select).toBeCalled(); + }); +}); diff --git a/web/src/js/__tests__/components/Header/FlowMenuSpec.tsx b/web/src/js/__tests__/components/Header/FlowMenuSpec.tsx index 2151df23bc..a0be1d77ba 100644 --- a/web/src/js/__tests__/components/Header/FlowMenuSpec.tsx +++ b/web/src/js/__tests__/components/Header/FlowMenuSpec.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import FlowMenu from '../../../components/Header/FlowMenu' -import {render} from "../../test-utils" +import * as React from "react"; +import FlowMenu from "../../../components/Header/FlowMenu"; +import { render } from "../../test-utils"; test("FlowMenu", async () => { - const {asFragment} = render(); + const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); diff --git a/web/src/js/__tests__/components/Header/MainMenuSpec.tsx b/web/src/js/__tests__/components/Header/MainMenuSpec.tsx index 5ec02f104d..81ba480da8 100644 --- a/web/src/js/__tests__/components/Header/MainMenuSpec.tsx +++ b/web/src/js/__tests__/components/Header/MainMenuSpec.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import StartMenu from '../../../components/Header/StartMenu' -import {render} from "../../test-utils" +import * as React from "react"; +import FlowListMenu from "../../../components/Header/FlowListMenu"; +import { render } from "../../test-utils"; test("MainMenu", () => { - const {asFragment} = render(); + const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); -}) +}); diff --git a/web/src/js/__tests__/components/Header/MenuToggleSpec.tsx b/web/src/js/__tests__/components/Header/MenuToggleSpec.tsx index ce45242895..342068a977 100644 --- a/web/src/js/__tests__/components/Header/MenuToggleSpec.tsx +++ b/web/src/js/__tests__/components/Header/MenuToggleSpec.tsx @@ -1,44 +1,52 @@ -import * as React from "react" -import renderer from 'react-test-renderer' -import {EventlogToggle, MenuToggle, OptionsToggle} from '../../../components/Header/MenuToggle' -import {Provider} from 'react-redux' -import {TStore} from '../../ducks/tutils' -import * as optionsEditorActions from "../../../ducks/ui/optionsEditor" -import {fireEvent, render, screen} from "../../test-utils" - -describe('MenuToggle Component', () => { - it('should render correctly', () => { - let changeFn = jest.fn(), - menuToggle = renderer.create( - -

    foo children

    -
    ), - tree = menuToggle.toJSON() - expect(tree).toMatchSnapshot() - }) -}) +import * as React from "react"; +import renderer from "react-test-renderer"; +import { + EventlogToggle, + MenuToggle, + OptionsToggle, +} from "../../../components/Header/MenuToggle"; +import { fireEvent, render, screen, waitFor } from "../../test-utils"; -test("OptionsToggle", async () => { - const store = TStore(), - {asFragment} = render( - toggle anticache, - {store} +import { enableFetchMocks } from "jest-fetch-mock"; + +enableFetchMocks(); + +describe("MenuToggle Component", () => { + it("should render correctly", () => { + const changeFn = jest.fn(); + const menuToggle = renderer.create( + +

    foo children

    +
    , ); - globalThis.fetch = jest.fn() + const tree = menuToggle.toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); + +test("OptionsToggle", async () => { + fetchMock.mockReject(new Error("backend missing")); + + const { asFragment, store } = render( + toggle anticache, + ); expect(asFragment()).toMatchSnapshot(); fireEvent.click(screen.getByText("toggle anticache")); - expect(store.getActions()).toEqual([optionsEditorActions.startUpdate("anticache", true)]) + + await waitFor(() => + expect(store.getState().ui.optionsEditor.anticache?.error).toContain( + "backend missing", + ), + ); }); test("EventlogToggle", async () => { - const {asFragment, store} = render( - - ); + const { asFragment, store } = render(); expect(asFragment()).toMatchSnapshot(); expect(store.getState().eventLog.visible).toBeTruthy(); fireEvent.click(screen.getByText("Display Event Log")); expect(store.getState().eventLog.visible).toBeFalsy(); -}) +}); diff --git a/web/src/js/__tests__/components/Header/OptionMenuSpec.tsx b/web/src/js/__tests__/components/Header/OptionMenuSpec.tsx index bb6a51d946..421fd9e637 100644 --- a/web/src/js/__tests__/components/Header/OptionMenuSpec.tsx +++ b/web/src/js/__tests__/components/Header/OptionMenuSpec.tsx @@ -1,18 +1,18 @@ -import * as React from "react" -import renderer from 'react-test-renderer' -import { Provider } from 'react-redux' -import OptionMenu from '../../../components/Header/OptionMenu' -import { TStore } from '../../ducks/tutils' +import * as React from "react"; +import renderer from "react-test-renderer"; +import { Provider } from "react-redux"; +import OptionMenu from "../../../components/Header/OptionMenu"; +import { TStore } from "../../ducks/tutils"; -describe('OptionMenu Component', () => { - it('should render correctly', () => { - let store = TStore(), - provider = renderer.create( - - - - ), - tree = provider.toJSON() - expect(tree).toMatchSnapshot() - }) -}) +describe("OptionMenu Component", () => { + it("should render correctly", () => { + const store = TStore(); + const provider = renderer.create( + + + , + ); + const tree = provider.toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/web/src/js/__tests__/components/Header/__snapshots__/CaptureMenuSpec.tsx.snap b/web/src/js/__tests__/components/Header/__snapshots__/CaptureMenuSpec.tsx.snap new file mode 100644 index 0000000000..ac46e8f8a2 --- /dev/null +++ b/web/src/js/__tests__/components/Header/__snapshots__/CaptureMenuSpec.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CaptureMenu Component should render correctly 1`] = ``; diff --git a/web/src/js/__tests__/components/Header/__snapshots__/FilterDocsSpec.tsx.snap b/web/src/js/__tests__/components/Header/__snapshots__/FilterDocsSpec.tsx.snap index b573d8b515..a26fe8c253 100644 --- a/web/src/js/__tests__/components/Header/__snapshots__/FilterDocsSpec.tsx.snap +++ b/web/src/js/__tests__/components/Header/__snapshots__/FilterDocsSpec.tsx.snap @@ -36,6 +36,7 @@ exports[`FilterDocs Component 2`] = ` > - Strip cache headers + Strip cache headers + { + const { asFragment } = render(
    ); - const {asFragment} = render(
    ); expect(asFragment()).toMatchSnapshot(); fireEvent.click(screen.getByText("Options")); @@ -18,5 +16,8 @@ test("Header", async () => { expect(screen.getByText("Open...")).toBeTruthy(); fireEvent.click(screen.getByText("File")); - expect(screen.queryByText("Open...")).toBeNull() + expect(screen.queryByText("Open...")).toBeNull(); + + fireEvent.click(screen.getByText("Capture")); + expect(asFragment()).toMatchSnapshot(); }); diff --git a/web/src/js/__tests__/components/Modal/ModalSpec.tsx b/web/src/js/__tests__/components/Modal/ModalSpec.tsx index 21e3790dce..ccf6619a9d 100644 --- a/web/src/js/__tests__/components/Modal/ModalSpec.tsx +++ b/web/src/js/__tests__/components/Modal/ModalSpec.tsx @@ -1,13 +1,12 @@ -import * as React from "react" -import Modal from '../../../components/Modal/Modal' -import {render} from "../../test-utils" -import {setActiveModal} from "../../../ducks/ui/modal"; +import * as React from "react"; +import Modal from "../../../components/Modal/Modal"; +import { render, act } from "../../test-utils"; +import { setActiveModal } from "../../../ducks/ui/modal"; test("Modal Component", async () => { - const {asFragment, store} = render(); + const { asFragment, store } = render(); expect(asFragment()).toMatchSnapshot(); - store.dispatch(setActiveModal("OptionModal")); + act(() => store.dispatch(setActiveModal("OptionModal"))); expect(asFragment()).toMatchSnapshot(); - -}) +}); diff --git a/web/src/js/__tests__/components/Modal/OptionModalSpec.tsx b/web/src/js/__tests__/components/Modal/OptionModalSpec.tsx index 65025cb760..4f7a59556f 100644 --- a/web/src/js/__tests__/components/Modal/OptionModalSpec.tsx +++ b/web/src/js/__tests__/components/Modal/OptionModalSpec.tsx @@ -1,54 +1,54 @@ -import * as React from "react" -import renderer from 'react-test-renderer' -import { PureOptionDefault } from '../../../components/Modal/OptionModal' - -describe('PureOptionDefault Component', () => { - - it('should return null when the value is default', () => { - let pureOptionDefault = renderer.create( - - ), - tree = pureOptionDefault.toJSON() - expect(tree).toMatchSnapshot() - }) - - it('should handle boolean type', () => { - let pureOptionDefault = renderer.create( - - ), - tree = pureOptionDefault.toJSON() - expect(tree).toMatchSnapshot() - }) - - it('should handle array', () => { - let a = [""], b = [], c = ['c'], +import * as React from "react"; +import renderer from "react-test-renderer"; +import { PureOptionDefault } from "../../../components/Modal/OptionModal"; + +describe("PureOptionDefault Component", () => { + it("should return null when the value is default", () => { + const pureOptionDefault = renderer.create( + , + ); + const tree = pureOptionDefault.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("should handle boolean type", () => { + const pureOptionDefault = renderer.create( + , + ); + const tree = pureOptionDefault.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("should handle array", () => { + let a = [""], + b = [], + c = ["c"], pureOptionDefault = renderer.create( - + , ), - tree = pureOptionDefault.toJSON() - expect(tree).toMatchSnapshot() + tree = pureOptionDefault.toJSON(); + expect(tree).toMatchSnapshot(); pureOptionDefault = renderer.create( - - ) - tree = pureOptionDefault.toJSON() - expect(tree).toMatchSnapshot() - }) - - it('should handle string', () => { - let pureOptionDefault = renderer.create( - - ), - tree = pureOptionDefault.toJSON() - expect(tree).toMatchSnapshot() - }) - - it('should handle null value', () => { - let pureOptionDefault = renderer.create( - - ), - tree = pureOptionDefault.toJSON() - expect(tree).toMatchSnapshot() - }) - -}) + , + ); + tree = pureOptionDefault.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("should handle string", () => { + const pureOptionDefault = renderer.create( + , + ); + const tree = pureOptionDefault.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("should handle null value", () => { + const pureOptionDefault = renderer.create( + , + ); + const tree = pureOptionDefault.toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/web/src/js/__tests__/components/Modal/OptionSpec.tsx b/web/src/js/__tests__/components/Modal/OptionSpec.tsx index cf6d84fede..942dff6c3c 100644 --- a/web/src/js/__tests__/components/Modal/OptionSpec.tsx +++ b/web/src/js/__tests__/components/Modal/OptionSpec.tsx @@ -1,99 +1,102 @@ -import * as React from "react" -import renderer from 'react-test-renderer' -import { Options, ChoicesOption } from '../../../components/Modal/Option' - -describe('BooleanOption Component', () => { - let BooleanOption = Options['bool'], - onChangeFn = jest.fn(), - booleanOption = renderer.create( - - ), - tree = booleanOption.toJSON() - - it('should render correctly', () => { - expect(tree).toMatchSnapshot() - }) - - it('should handle onChange', () => { - let input = tree.children[0].children[0], - mockEvent = { target: { checked: true }} - input.props.onChange(mockEvent) - expect(onChangeFn).toBeCalledWith(mockEvent.target.checked) - }) -}) - -describe('StringOption Component', () => { - let StringOption = Options['str'], - onChangeFn = jest.fn(), - stringOption = renderer.create( - - ), - tree = stringOption.toJSON() - - it('should render correctly', () => { - expect(tree).toMatchSnapshot() - }) - - it('should handle onChange', () => { - let mockEvent = { target: { value: 'bar' }} - tree.props.onChange(mockEvent) - expect(onChangeFn).toBeCalledWith(mockEvent.target.value) - }) - -}) - -describe('NumberOption Component', () => { - let NumberOption = Options['int'], - onChangeFn = jest.fn(), - numberOption = renderer.create( - - ), - tree = numberOption.toJSON() - - it('should render correctly', () => { - expect(tree).toMatchSnapshot() - }) - - it('should handle onChange', () => { - let mockEvent = {target: { value: '2'}} - tree.props.onChange(mockEvent) - expect(onChangeFn).toBeCalledWith(2) - }) -}) - -describe('ChoiceOption Component', () => { - let onChangeFn = jest.fn(), - choiceOption = renderer.create( - - ), - tree = choiceOption.toJSON() - - it('should render correctly', () => { - expect(tree).toMatchSnapshot() - }) - - it('should handle onChange', () => { - let mockEvent = { target: {value: 'b'} } - tree.props.onChange(mockEvent) - expect(onChangeFn).toBeCalledWith(mockEvent.target.value) - }) -}) - -describe('StringOption Component', () => { - let onChangeFn = jest.fn(), - StringSequenceOption = Options['sequence of str'], - stringSequenceOption = renderer.create( - - ), - tree = stringSequenceOption.toJSON() - - it('should render correctly', () => { - expect(tree).toMatchSnapshot() - }) - - it('should handle onChange', () => { - let mockEvent = { target: {value: 'a\nb\nc\n'}} - tree.props.onChange(mockEvent) - expect(onChangeFn).toBeCalledWith(['a', 'b', 'c', '']) - }) -}) +import * as React from "react"; +import renderer, { act } from "react-test-renderer"; +import { ChoicesOption, Options } from "../../../components/Modal/OptionInput"; + +describe("BooleanOption Component", () => { + const BooleanOption = Options["bool"]; + const onChangeFn = jest.fn(); + const booleanOption = renderer.create( + , + ); + const tree = booleanOption.toJSON(); + + it("should render correctly", () => { + expect(tree).toMatchSnapshot(); + }); + + it("should handle onChange", () => { + const input = tree.children[0].children[0]; + const mockEvent = { target: { checked: true } }; + input.props.onChange(mockEvent); + expect(onChangeFn).toBeCalledWith(mockEvent.target.checked); + }); +}); + +describe("StringOption Component", () => { + const StringOption = Options["str"]; + const onChangeFn = jest.fn(); + const stringOption = renderer.create( + , + ); + const tree = stringOption.toJSON(); + + it("should render correctly", () => { + expect(tree).toMatchSnapshot(); + }); + + it("should handle onChange", () => { + const mockEvent = { target: { value: "bar" } }; + tree.props.onChange(mockEvent); + expect(onChangeFn).toBeCalledWith(mockEvent.target.value); + }); +}); + +describe("NumberOption Component", () => { + const NumberOption = Options["int"]; + const onChangeFn = jest.fn(); + const numberOption = renderer.create( + , + ); + const tree = numberOption.toJSON(); + + it("should render correctly", () => { + expect(tree).toMatchSnapshot(); + }); + + it("should handle onChange", () => { + const mockEvent = { target: { value: "2" } }; + tree.props.onChange(mockEvent); + expect(onChangeFn).toBeCalledWith(2); + }); +}); + +describe("ChoiceOption Component", () => { + const onChangeFn = jest.fn(); + const choiceOption = renderer.create( + , + ); + const tree = choiceOption.toJSON(); + + it("should render correctly", () => { + expect(tree).toMatchSnapshot(); + }); + + it("should handle onChange", () => { + const mockEvent = { target: { value: "b" } }; + tree.props.onChange(mockEvent); + expect(onChangeFn).toBeCalledWith(mockEvent.target.value); + }); +}); + +describe("StringOption Component", () => { + const onChangeFn = jest.fn(); + const StringSequenceOption = Options["sequence of str"]; + const stringSequenceOption = renderer.create( + , + ); + const tree = stringSequenceOption.toJSON(); + + it("should render correctly", () => { + expect(tree).toMatchSnapshot(); + }); + + it("should handle onChange", () => { + const mockEvent = { target: { value: "a\nb\nc\n" } }; + act(() => tree.props.onChange(mockEvent)); + expect(onChangeFn).toBeCalledWith(["a", "b", "c"]); + }); +}); diff --git a/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.tsx.snap b/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.tsx.snap index 3a0c54b2f5..892bae892f 100644 --- a/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.tsx.snap +++ b/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.tsx.snap @@ -81,6 +81,7 @@ exports[`Modal Component 2`] = ` >
    +
    + + - 5s - - - - -
    -
    -
    + + + + + Path + + + Method + + + Status + + + Size + + + Time + + + + + + + + + +
    + + + + + dns.google A = 8.8.8.8, 8.8.4.4 + + + QUERY + + + NOERROR + + + 8b + + + 1s + + + + + + +
    + + + + + 127.0.0.1:22 ↔ address:22 + + + UDP + + + + 12b + + + 5s + + + + + + +
    +
    + `; diff --git a/web/src/js/__tests__/components/__snapshots__/FlowViewSpec.tsx.snap b/web/src/js/__tests__/components/__snapshots__/FlowViewSpec.tsx.snap index 6c8e33a20e..b15f441f10 100644 --- a/web/src/js/__tests__/components/__snapshots__/FlowViewSpec.tsx.snap +++ b/web/src/js/__tests__/components/__snapshots__/FlowViewSpec.tsx.snap @@ -8,6 +8,14 @@ exports[`FlowView 1`] = `
    + Timing + + Comment +
    + Timing + + Comment +
    + Timing + + Comment +
    + Timing + + Comment +
    + + + Request + + + Response + + + WebSocket + + + Error + + + Connection + + + Timing + + + Comment + + +
    +

    + Comment +

    + + I'm a comment! + +
    +
    + +`; + +exports[`FlowView 7`] = ` + +
    +
    `; -exports[`FlowView 7`] = ` +exports[`FlowView 8`] = `
    + - TCP Messages + Stream Data Timing + + Comment +
    -

    - TCP Data -

    @@ -1067,7 +1239,7 @@ exports[`FlowView 7`] = ` `; -exports[`FlowView 8`] = ` +exports[`FlowView 9`] = `
    + - TCP Messages + Stream Data Timing + + Comment +
    `; -exports[`FlowView 9`] = ` +exports[`FlowView 10`] = `
    + Timing + + Comment +
    `; -exports[`FlowView 10`] = ` +exports[`FlowView 11`] = `
    + Timing + + Comment +
    `; -exports[`FlowView 11`] = ` +exports[`FlowView 12`] = `
    + Timing + + Comment +
    `; -exports[`FlowView 12`] = ` +exports[`FlowView 13`] = `
    + - UDP Messages + Datagrams Timing + + Comment +
    -

    - UDP Data -

    @@ -1504,7 +1743,7 @@ exports[`FlowView 12`] = ` `; -exports[`FlowView 13`] = ` +exports[`FlowView 14`] = `
    + - UDP Messages + Datagrams Timing + + Comment +
    - Start + Capture + + + Flow List - Start + Capture + + + Flow List - Start + Capture + + + Flow List `; + +exports[`Header 4`] = ` + +
    + +
    +
    + +`; diff --git a/web/src/js/__tests__/components/common/ButtonSpec.tsx b/web/src/js/__tests__/components/common/ButtonSpec.tsx index 661b4d553a..b72273fc84 100644 --- a/web/src/js/__tests__/components/common/ButtonSpec.tsx +++ b/web/src/js/__tests__/components/common/ButtonSpec.tsx @@ -1,26 +1,30 @@ -import * as React from "react" -import renderer from 'react-test-renderer' -import Button from '../../../components/common/Button' +import * as React from "react"; +import renderer from "react-test-renderer"; +import Button from "../../../components/common/Button"; -describe('Button Component', () => { - - it('should render correctly', () => { - let button = renderer.create( - - ), - tree = button.toJSON() - expect(tree).toMatchSnapshot() - }) + , + ); + const tree = button.toJSON(); + expect(tree).toMatchSnapshot(); + }); - it('should be able to be disabled', () => { - let button = renderer.create( + it("should be able to be disabled", () => { + const button = renderer.create( - ), - tree = button.toJSON() - expect(tree).toMatchSnapshot() - }) -}) + , + ); + const tree = button.toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/web/src/js/__tests__/components/common/DocsLinkSpec.tsx b/web/src/js/__tests__/components/common/DocsLinkSpec.tsx index 0f78a16259..f22c3e65ee 100644 --- a/web/src/js/__tests__/components/common/DocsLinkSpec.tsx +++ b/web/src/js/__tests__/components/common/DocsLinkSpec.tsx @@ -1,17 +1,19 @@ -import * as React from "react" -import renderer from 'react-test-renderer' -import DocsLink from '../../../components/common/DocsLink' +import * as React from "react"; +import renderer from "react-test-renderer"; +import DocsLink from "../../../components/common/DocsLink"; -describe('DocsLink Component', () => { - it('should be able to be rendered with children nodes', () => { - let docsLink = renderer.create(), - tree = docsLink.toJSON() - expect(tree).toMatchSnapshot() - }) +describe("DocsLink Component", () => { + it("should be able to be rendered with children nodes", () => { + const docsLink = renderer.create( + foo, + ); + const tree = docsLink.toJSON(); + expect(tree).toMatchSnapshot(); + }); - it('should be able to be rendered without children nodes', () => { - let docsLink = renderer.create(), - tree = docsLink.toJSON() - expect(tree).toMatchSnapshot() - }) -}) + it("should be able to be rendered without children nodes", () => { + const docsLink = renderer.create(); + const tree = docsLink.toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/web/src/js/__tests__/components/common/DropdownSpec.tsx b/web/src/js/__tests__/components/common/DropdownSpec.tsx index 9f430601a4..27455bcbef 100644 --- a/web/src/js/__tests__/components/common/DropdownSpec.tsx +++ b/web/src/js/__tests__/components/common/DropdownSpec.tsx @@ -1,51 +1,52 @@ import * as React from "react"; -import Dropdown, {Divider, MenuItem, SubMenu} from '../../../components/common/Dropdown' -import {fireEvent, render, screen, waitFor} from "../../test-utils"; - - -test('Dropdown', async () => { - let onOpen = jest.fn(); - const {asFragment} = render( +import Dropdown, { + Divider, + MenuItem, + SubMenu, +} from "../../../components/common/Dropdown"; +import { fireEvent, render, screen, waitFor } from "../../test-utils"; + +test("Dropdown", async () => { + const onOpen = jest.fn(); + const { asFragment } = render( 0}>click me - + 0}>click me - - ) - expect(asFragment()).toMatchSnapshot() + , + ); + expect(asFragment()).toMatchSnapshot(); - fireEvent.click(screen.getByText("open me")) - await waitFor(() => expect(onOpen).toBeCalledWith(true)) - expect(asFragment()).toMatchSnapshot() + fireEvent.click(screen.getByText("open me")); + await waitFor(() => expect(onOpen).toBeCalledWith(true)); + expect(asFragment()).toMatchSnapshot(); - onOpen.mockClear() - fireEvent.click(document.body) + onOpen.mockClear(); + fireEvent.click(document.body); await waitFor(() => expect(onOpen).toBeCalledWith(false)); -}) +}); -test('SubMenu', async () => { - const {asFragment} = render( +test("SubMenu", async () => { + const { asFragment } = render( 0}>click me - - ) - expect(asFragment()).toMatchSnapshot() - - fireEvent.mouseEnter(screen.getByText("submenu")) - await waitFor(() => screen.getByText("click me")) - expect(asFragment()).toMatchSnapshot() - - fireEvent.mouseLeave(screen.getByText("submenu")) - expect(screen.queryByText("click me")).toBeNull() - expect(asFragment()).toMatchSnapshot() -}) - -test('MenuItem', async () => { - let click = jest.fn(); - const {asFragment} = render( - wtf - ) - expect(asFragment()).toMatchSnapshot() - fireEvent.click(screen.getByText("wtf")) + , + ); + expect(asFragment()).toMatchSnapshot(); + + fireEvent.mouseEnter(screen.getByText("submenu")); + await waitFor(() => screen.getByText("click me")); + expect(asFragment()).toMatchSnapshot(); + + fireEvent.mouseLeave(screen.getByText("submenu")); + expect(screen.queryByText("click me")).toBeNull(); + expect(asFragment()).toMatchSnapshot(); +}); + +test("MenuItem", async () => { + const click = jest.fn(); + const { asFragment } = render(wtf); + expect(asFragment()).toMatchSnapshot(); + fireEvent.click(screen.getByText("wtf")); await waitFor(() => expect(click).toBeCalled()); -}) +}); diff --git a/web/src/js/__tests__/components/common/FileChooserSpec.tsx b/web/src/js/__tests__/components/common/FileChooserSpec.tsx index bf7f196b3f..7cdb21cc5d 100644 --- a/web/src/js/__tests__/components/common/FileChooserSpec.tsx +++ b/web/src/js/__tests__/components/common/FileChooserSpec.tsx @@ -1,13 +1,11 @@ -import * as React from "react" -import FileChooser from '../../../components/common/FileChooser' -import {render} from '@testing-library/react' - +import * as React from "react"; +import FileChooser from "../../../components/common/FileChooser"; +import { render } from "@testing-library/react"; test("FileChooser", async () => { - const {asFragment} = render( - 0}/> - ); - - expect(asFragment()).toMatchSnapshot() + const { asFragment } = render( + 0} />, + ); -}) + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/web/src/js/__tests__/components/common/SplitterSpec.tsx b/web/src/js/__tests__/components/common/SplitterSpec.tsx index f2adffe7a9..5ef9af2461 100644 --- a/web/src/js/__tests__/components/common/SplitterSpec.tsx +++ b/web/src/js/__tests__/components/common/SplitterSpec.tsx @@ -1,84 +1,51 @@ -import * as React from "react" -import ReactDOM from 'react-dom' -import renderer from 'react-test-renderer' -import Splitter from '../../../components/common/Splitter' -import TestUtils from 'react-dom/test-utils'; - -describe('Splitter Component', () => { - - it('should render correctly', () => { - let splitter = renderer.create(), - tree = splitter.toJSON() - expect(tree).toMatchSnapshot() - }) - - let splitter = TestUtils.renderIntoDocument(), - dom = ReactDOM.findDOMNode(splitter), - previousElementSibling = { - offsetHeight: 0, - offsetWidth: 0, - style: {flex: ''} - }, - nextElementSibling = { - style: {flex: ''} - } - - it('should handle mouseDown ', () => { - window.addEventListener = jest.fn() - splitter.onMouseDown({pageX: 1, pageY: 2}) - expect(splitter.state.startX).toEqual(1) - expect(splitter.state.startY).toEqual(2) - expect(window.addEventListener).toBeCalledWith('mousemove', splitter.onMouseMove) - expect(window.addEventListener).toBeCalledWith('mouseup', splitter.onMouseUp) - expect(window.addEventListener).toBeCalledWith('dragend', splitter.onDragEnd) - }) - - it('should handle dragEnd', () => { - window.removeEventListener = jest.fn() - splitter.onDragEnd() - expect(dom.style.transform).toEqual('') - expect(window.removeEventListener).toBeCalledWith('dragend', splitter.onDragEnd) - expect(window.removeEventListener).toBeCalledWith('mouseup', splitter.onMouseUp) - expect(window.removeEventListener).toBeCalledWith('mousemove', splitter.onMouseMove) - }) - - it('should handle mouseUp', () => { - - Object.defineProperty(dom, 'previousElementSibling', {value: previousElementSibling}) - Object.defineProperty(dom, 'nextElementSibling', {value: nextElementSibling}) - splitter.onMouseUp({pageX: 3, pageY: 4}) - expect(splitter.state.applied).toBeTruthy() - expect(nextElementSibling.style.flex).toEqual('1 1 auto') - expect(previousElementSibling.style.flex).toEqual('0 0 2px') - }) - - it('should handle mouseMove', () => { - splitter.onMouseMove({pageX: 10, pageY: 10}) - expect(dom.style.transform).toEqual("translate(9px, 0px)") - - let splitterY = TestUtils.renderIntoDocument() - splitterY.onMouseMove({pageX: 10, pageY: 10}) - expect(ReactDOM.findDOMNode(splitterY).style.transform).toEqual("translate(0px, 10px)") - }) - - it('should handle resize', () => { - let x = jest.spyOn(window, 'setTimeout'); - splitter.onResize() - expect(x).toHaveBeenCalled() - }) - - it('should handle componentWillUnmount', () => { - splitter.componentWillUnmount() - expect(previousElementSibling.style.flex).toEqual('') - expect(nextElementSibling.style.flex).toEqual('') - expect(splitter.state.applied).toBeTruthy() - }) - - it('should handle reset', () => { - splitter.reset(false) - expect(splitter.state.applied).toBeFalsy() - - expect(splitter.reset(true)).toEqual(undefined) - }) - -}) +import * as React from "react"; +import Splitter from "../../../components/common/Splitter"; +import { act, render } from "../../test-utils"; + +describe.each([["x"], ["y"]])("Splitter Component", (axis) => { + it(`should render correctly (${axis} axis)`, () => { + const ref = React.createRef(); + const { asFragment, unmount } = render( + <> +
    + +
    + , + ); + const splitter = ref.current!; + + expect(asFragment()).toMatchSnapshot(); + + act(() => { + splitter.onPointerDown({ + target: { + setPointerCapture: () => 0, + } as unknown, + pageX: 100, + pageY: 200, + pointerId: 42, + } as React.PointerEvent); + }); + expect(splitter.state.startPos).toBe(axis === "x" ? 100 : 200); + + act(() => { + splitter.onPointerMove({ + pageX: 300, + pageY: 300, + pointerId: 42, + } as React.PointerEvent); + }); + expect(splitter.node.current!.style.transform).toBeTruthy(); + + act(() => { + splitter.onLostPointerCapture({ + pageX: 400, + pageY: 400, + pointerId: 42, + } as React.PointerEvent); + }); + expect(asFragment()).toMatchSnapshot(); + + unmount(); + }); +}); diff --git a/web/src/js/__tests__/components/common/ToggleButtonSpec.tsx b/web/src/js/__tests__/components/common/ToggleButtonSpec.tsx index 23378f5397..0e249f481f 100644 --- a/web/src/js/__tests__/components/common/ToggleButtonSpec.tsx +++ b/web/src/js/__tests__/components/common/ToggleButtonSpec.tsx @@ -1,22 +1,24 @@ -import * as React from "react" -import renderer from 'react-test-renderer' -import ToggleButton from '../../../components/common/ToggleButton' +import * as React from "react"; +import renderer from "react-test-renderer"; +import ToggleButton from "../../../components/common/ToggleButton"; -describe('ToggleButton Component', () => { - let mockFunc = jest.fn() +describe("ToggleButton Component", () => { + const mockFunc = jest.fn(); - it('should render correctly', () => { - let checkedButton = renderer.create( - ), - tree = checkedButton.toJSON() - expect(tree).toMatchSnapshot() - }) + it("should render correctly", () => { + const checkedButton = renderer.create( + , + ); + const tree = checkedButton.toJSON(); + expect(tree).toMatchSnapshot(); + }); - it('should handle click action', () => { - let uncheckButton = renderer.create( - ), - tree = uncheckButton.toJSON() - tree.props.onClick() - expect(mockFunc).toBeCalled() - }) -}) + it("should handle click action", () => { + const uncheckButton = renderer.create( + , + ); + const tree = uncheckButton.toJSON(); + tree.props.onClick(); + expect(mockFunc).toBeCalled(); + }); +}); diff --git a/web/src/js/__tests__/components/common/__snapshots__/DocsLinkSpec.tsx.snap b/web/src/js/__tests__/components/common/__snapshots__/DocsLinkSpec.tsx.snap index becd2b9045..1cda6e8a6f 100644 --- a/web/src/js/__tests__/components/common/__snapshots__/DocsLinkSpec.tsx.snap +++ b/web/src/js/__tests__/components/common/__snapshots__/DocsLinkSpec.tsx.snap @@ -3,6 +3,7 @@ exports[`DocsLink Component should be able to be rendered with children nodes 1`] = ` foo @@ -12,6 +13,7 @@ exports[`DocsLink Component should be able to be rendered with children nodes 1` exports[`DocsLink Component should be able to be rendered without children nodes 1`] = ` +exports[`Splitter Component should render correctly (x axis) 1`] = ` + +
    +
    +
    +
    + +`; + +exports[`Splitter Component should render correctly (x axis) 2`] = ` + +
    +
    +
    +
    +
    + +`; + +exports[`Splitter Component should render correctly (y axis) 1`] = ` + +
    +
    +
    +
    +
    + +`; + +exports[`Splitter Component should render correctly (y axis) 2`] = ` + +
    +
    +
    +
    +
    -
    + `; diff --git a/web/src/js/__tests__/components/contentviews/CodeEditorSpec.tsx b/web/src/js/__tests__/components/contentviews/CodeEditorSpec.tsx index 1d66c2b860..4a11de9d95 100644 --- a/web/src/js/__tests__/components/contentviews/CodeEditorSpec.tsx +++ b/web/src/js/__tests__/components/contentviews/CodeEditorSpec.tsx @@ -1,10 +1,9 @@ -jest.mock("../../../contrib/CodeMirror") -import * as React from 'react'; -import CodeEditor from '../../../components/contentviews/CodeEditor' -import {render} from "../../test-utils" - +jest.mock("../../../contrib/CodeMirror"); +import * as React from "react"; +import CodeEditor from "../../../components/contentviews/CodeEditor"; +import { render } from "../../test-utils"; test("CodeEditor", async () => { - const {asFragment} = render(); + const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); diff --git a/web/src/js/__tests__/components/contentviews/HttpMessageSpec.tsx b/web/src/js/__tests__/components/contentviews/HttpMessageSpec.tsx index fbb9d5ee84..eb440dc3c0 100644 --- a/web/src/js/__tests__/components/contentviews/HttpMessageSpec.tsx +++ b/web/src/js/__tests__/components/contentviews/HttpMessageSpec.tsx @@ -1,26 +1,30 @@ -import {TFlow} from "../../ducks/tutils"; -import * as React from 'react'; -import HttpMessage, {ViewImage} from '../../../components/contentviews/HttpMessage' -import {fireEvent, render, screen, waitFor} from "../../test-utils" -import fetchMock, {enableFetchMocks} from "jest-fetch-mock"; +import { TFlow } from "../../ducks/tutils"; +import * as React from "react"; +import HttpMessage, { + ViewImage, +} from "../../../components/contentviews/HttpMessage"; +import { fireEvent, render, screen, waitFor } from "../../test-utils"; +import fetchMock, { enableFetchMocks } from "jest-fetch-mock"; -jest.mock("../../../contrib/CodeMirror") +jest.mock("../../../contrib/CodeMirror"); enableFetchMocks(); test("HttpMessage", async () => { - const lines = Array(512).fill([["text", "data"]]).concat( - Array(512).fill([["text", "additional"]]) - ); + const lines = Array(512) + .fill([["text", "data"]]) + .concat(Array(512).fill([["text", "additional"]])); fetchMock.mockResponses( JSON.stringify({ lines: lines.slice(0, 512 + 1), - description: "Auto" - }), JSON.stringify({ + description: "Auto", + }), + JSON.stringify({ lines, - description: "Auto" - }), JSON.stringify({ + description: "Auto", + }), + JSON.stringify({ lines: Array(5).fill([["text", "rawdata"]]), description: "Raw", }), @@ -28,13 +32,15 @@ test("HttpMessage", async () => { JSON.stringify({ lines: Array(5).fill([["text", "rawdata"]]), description: "Raw", - }) + }), ); const tflow = TFlow(); - const {asFragment} = render(); + const { asFragment } = render( + , + ); await waitFor(() => screen.getAllByText("data")); - expect(screen.queryByText('additional')).toBeNull(); + expect(screen.queryByText("additional")).toBeNull(); fireEvent.click(screen.getByText("Show more")); await waitFor(() => screen.getAllByText("additional")); @@ -54,6 +60,8 @@ test("HttpMessage", async () => { test("ViewImage", async () => { const flow = TFlow(); - const {asFragment} = render() + const { asFragment } = render( + , + ); expect(asFragment()).toMatchSnapshot(); }); diff --git a/web/src/js/__tests__/components/contentviews/LineRendererSpec.tsx b/web/src/js/__tests__/components/contentviews/LineRendererSpec.tsx index 60ff4528b4..e1fcd5f842 100644 --- a/web/src/js/__tests__/components/contentviews/LineRendererSpec.tsx +++ b/web/src/js/__tests__/components/contentviews/LineRendererSpec.tsx @@ -1,28 +1,31 @@ -import * as React from 'react'; -import LineRenderer from '../../../components/contentviews/LineRenderer' -import {fireEvent, render, screen} from "../../test-utils" - +import * as React from "react"; +import LineRenderer from "../../../components/contentviews/LineRenderer"; +import { fireEvent, render, screen } from "../../test-utils"; test("LineRenderer", async () => { const lines: [style: string, text: string][][] = [ [ ["header", "foo: "], - ["text", "42"] + ["text", "42"], ], [ ["header", "bar: "], - ["text", "43"] + ["text", "43"], ], - ] + ]; const showMore = jest.fn(); - const {asFragment} = render(); + const { asFragment } = render( + , + ); expect(asFragment()).toMatchSnapshot(); fireEvent.click(screen.getByText("Show more")); expect(showMore).toBeCalled(); }); test("No lines", async () => { - const {asFragment} = render( 0}/>); + const { asFragment } = render( + 0} />, + ); expect(asFragment()).toMatchSnapshot(); -}) +}); diff --git a/web/src/js/__tests__/components/contentviews/ViewSelectorSpec.tsx b/web/src/js/__tests__/components/contentviews/ViewSelectorSpec.tsx index f87e67855b..6450fb96f3 100644 --- a/web/src/js/__tests__/components/contentviews/ViewSelectorSpec.tsx +++ b/web/src/js/__tests__/components/contentviews/ViewSelectorSpec.tsx @@ -1,12 +1,12 @@ -import * as React from 'react'; -import ViewSelector from '../../../components/contentviews/ViewSelector' -import {fireEvent, render, screen} from "../../test-utils" - +import * as React from "react"; +import ViewSelector from "../../../components/contentviews/ViewSelector"; +import { fireEvent, render, screen } from "../../test-utils"; test("ViewSelector", async () => { - const onChange = jest.fn(); - const {asFragment} = render(); + const { asFragment } = render( + , + ); expect(asFragment()).toMatchSnapshot(); fireEvent.click(screen.getByText("auto")); expect(asFragment()).toMatchSnapshot(); diff --git a/web/src/js/__tests__/components/contentviews/useContentSpec.tsx b/web/src/js/__tests__/components/contentviews/useContentSpec.tsx index f03af5f576..0cee59864c 100644 --- a/web/src/js/__tests__/components/contentviews/useContentSpec.tsx +++ b/web/src/js/__tests__/components/contentviews/useContentSpec.tsx @@ -1,31 +1,32 @@ -import * as React from 'react'; -import {render, screen, waitFor} from "../../test-utils" -import {useContent} from "../../../components/contentviews/useContent"; -import fetchMock, {enableFetchMocks} from 'jest-fetch-mock' +import * as React from "react"; +import { render, screen, waitFor } from "../../test-utils"; +import { useContent } from "../../../components/contentviews/useContent"; +import fetchMock, { enableFetchMocks } from "jest-fetch-mock"; -enableFetchMocks() +enableFetchMocks(); - -function TComp({url, hash}: { url: string, hash: string }) { +function TComp({ url, hash }: { url: string; hash: string }) { const content = useContent(url, hash); return
    {content}
    ; } test("caching", async () => { fetchMock.mockResponses("hello", "world"); - const {rerender} = render(); + const { rerender } = render(); await waitFor(() => screen.getByText("hello")); - rerender(); + rerender(); expect(fetchMock.mock.calls).toHaveLength(1); - rerender(); + rerender(); await waitFor(() => screen.getByText("world")); expect(fetchMock.mock.calls).toHaveLength(2); }); test("network error", async () => { fetchMock.mockRejectOnce(new Error("I/O error")); - render(); - await waitFor(() => screen.getByText("Error getting content: Error: I/O error.")); + render(); + await waitFor(() => + screen.getByText("Error getting content: Error: I/O error."), + ); }); diff --git a/web/src/js/__tests__/components/editors/ValidateEditorSpec.tsx b/web/src/js/__tests__/components/editors/ValidateEditorSpec.tsx index ae07b96396..0236aae1fb 100644 --- a/web/src/js/__tests__/components/editors/ValidateEditorSpec.tsx +++ b/web/src/js/__tests__/components/editors/ValidateEditorSpec.tsx @@ -1,11 +1,21 @@ -import * as React from "react" -import ValidateEditor from '../../../components/editors/ValidateEditor' -import {fireEvent, render, screen, userEvent, waitFor} from "../../test-utils"; +import * as React from "react"; +import ValidateEditor from "../../../components/editors/ValidateEditor"; +import { + fireEvent, + render, + screen, + userEvent, + waitFor, +} from "../../test-utils"; test("ValidateEditor", async () => { const onEditDone = jest.fn(); - const {asFragment} = render( - x.includes("ok")} onEditDone={onEditDone}/> + const { asFragment } = render( + x.includes("ok")} + onEditDone={onEditDone} + />, ); expect(asFragment()).toMatchSnapshot(); diff --git a/web/src/js/__tests__/components/editors/ValueEditorSpec.tsx b/web/src/js/__tests__/components/editors/ValueEditorSpec.tsx index 1d2513f543..32d2ab498c 100644 --- a/web/src/js/__tests__/components/editors/ValueEditorSpec.tsx +++ b/web/src/js/__tests__/components/editors/ValueEditorSpec.tsx @@ -1,17 +1,20 @@ -import * as React from "react" -import ValueEditor from '../../../components/editors/ValueEditor' -import {render, waitFor} from "../../test-utils"; +import * as React from "react"; +import ValueEditor from "../../../components/editors/ValueEditor"; +import { render, waitFor } from "../../test-utils"; test("ValueEditor", async () => { const onEditDone = jest.fn(); - let editor: { current?: ValueEditor | null } = {} - const {asFragment} = render( - editor.current = x} content="hello world" onEditDone={onEditDone}/> + const editor: { current?: ValueEditor | null } = {}; + const { asFragment } = render( + (editor.current = x)} + content="hello world" + onEditDone={onEditDone} + />, ); expect(asFragment()).toMatchSnapshot(); - if (!editor.current) - throw "err"; + if (!editor.current) throw "err"; editor.current.startEditing(); await waitFor(() => expect(editor.current?.isEditing()).toBeTruthy()); diff --git a/web/src/js/__tests__/components/helpers/AutoScrollSpec.tsx b/web/src/js/__tests__/components/helpers/AutoScrollSpec.tsx index 31f570463f..3af21025f4 100644 --- a/web/src/js/__tests__/components/helpers/AutoScrollSpec.tsx +++ b/web/src/js/__tests__/components/helpers/AutoScrollSpec.tsx @@ -1,41 +1,56 @@ import * as React from "react"; -import ReactDOM from "react-dom" -import AutoScroll from '../../../components/helpers/AutoScroll' -import { calcVScroll } from '../../../components/helpers/VirtualScroll' -import TestUtils from 'react-dom/test-utils' - -describe('Autoscroll', () => { - let mockFn = jest.fn() - class tComponent extends React.Component { - constructor(props, context){ - super(props, context) - this.state = { vScroll: calcVScroll() } +import * as autoscroll from "../../../components/helpers/AutoScroll"; +import { fireEvent, render } from "../../test-utils"; + +describe("Autoscroll", () => { + interface TComponentProps { + height: number; + } + + class TComponent extends React.Component { + private viewport = React.createRef(); + + getSnapshotBeforeUpdate(prevProps) { + this.fixupJsDom(prevProps.height); + return autoscroll.isAtBottom(this.viewport); } - UNSAFE_componentWillUpdate() { - mockFn("foo") - } + componentDidUpdate(prevProps, prevState, snapshot) { + this.fixupJsDom(this.props.height); + if (snapshot) { + autoscroll.adjustScrollTop(this.viewport); + } + } - componentDidUpdate() { - mockFn("bar") - } + fixupJsDom(scrollHeight: number) { + // work around jsdom limitations + Object.defineProperty(this.viewport.current!, "clientHeight", { + value: 100, + writable: true, + }); + Object.defineProperty(this.viewport.current!, "scrollHeight", { + value: scrollHeight, + writable: true, + }); + } - render() { - return (

    foo

    ) - } + render() { + return
    ; + } } - it('should update component', () => { - let Foo = AutoScroll(tComponent), - autoScroll = TestUtils.renderIntoDocument(), - viewport = ReactDOM.findDOMNode(autoScroll) - viewport.scrollTop = 10 - Object.defineProperty(viewport, "scrollHeight", { value: 10, writable: true }) - autoScroll.UNSAFE_componentWillUpdate() - expect(mockFn).toBeCalledWith("foo") - - Object.defineProperty(viewport, "scrollHeight", { value: 0, writable: true }) - autoScroll.componentDidUpdate() - expect(mockFn).toBeCalledWith("bar") - }) -}) + it("should update component", () => { + const { rerender, container } = render(); + const viewport = container.firstElementChild!; + + fireEvent.scroll(viewport, { target: { scrollTop: 10 } }); + rerender(); + + expect(viewport.scrollTop).toBe(10); + + fireEvent.scroll(viewport, { target: { scrollTop: 40 } }); + rerender(); + + expect(viewport.scrollTop).toBeGreaterThanOrEqual(60); + }); +}); diff --git a/web/src/js/__tests__/components/helpers/VirtualScrollSpec.tsx b/web/src/js/__tests__/components/helpers/VirtualScrollSpec.tsx index 7a6239379f..34d3e35dfa 100644 --- a/web/src/js/__tests__/components/helpers/VirtualScrollSpec.tsx +++ b/web/src/js/__tests__/components/helpers/VirtualScrollSpec.tsx @@ -1,34 +1,78 @@ -import { calcVScroll } from '../../../components/helpers/VirtualScroll' +import { calcVScroll } from "../../../components/helpers/VirtualScroll"; -describe('VirtualScroll', () => { +describe("VirtualScroll", () => { + it("should return default state without options", () => { + expect(calcVScroll()).toEqual({ + start: 0, + end: 0, + paddingTop: 0, + paddingBottom: 0, + }); + }); - it('should return default state without options', () => { - expect(calcVScroll()).toEqual({start: 0, end: 0, paddingTop: 0, paddingBottom: 0}) - }) + it("should calculate position without itemHeights", () => { + expect( + calcVScroll({ + itemCount: 0, + rowHeight: 32, + viewportHeight: 400, + viewportTop: 0, + }), + ).toEqual({ + start: 0, + end: 0, + paddingTop: 0, + paddingBottom: 0, + }); + }); - it('should calculate position without itemHeights', () => { - expect(calcVScroll({itemCount: 0, rowHeight: 32, viewportHeight: 400, viewportTop: 0})).toEqual({ - start: 0, end: 0, paddingTop: 0, paddingBottom: 0 - }) - }) + it("should calculate position with itemHeights", () => { + expect( + calcVScroll({ + itemCount: 5, + itemHeights: [100, 100, 100, 100, 100], + viewportHeight: 300, + viewportTop: 0, + rowHeight: 100, + }), + ).toEqual({ + start: 0, + end: 4, + paddingTop: 0, + paddingBottom: 100, + }); + }); - it('should calculate position with itemHeights', () => { - expect(calcVScroll({itemCount: 5, itemHeights: [100, 100, 100, 100, 100], - viewportHeight: 300, viewportTop: 0, rowHeight: 100})).toEqual({ - start: 0, end: 4, paddingTop: 0, paddingBottom: 100 - }) - }) + it("should handle the case where lots of existing rows are removed without itemHeights", () => { + expect( + calcVScroll({ + itemCount: 10, + rowHeight: 32, + viewportHeight: 400, + viewportTop: 12_000, + }), + ).toEqual({ + start: 0, + end: 10, + paddingTop: 0, + paddingBottom: 0, + }); + }); - it('should handle the case where lots of existing rows are removed without itemHeights', () => { - expect(calcVScroll({itemCount: 10, rowHeight: 32, viewportHeight: 400, viewportTop: 12_000})).toEqual({ - start: 0, end: 10, paddingTop: 0, paddingBottom: 0 - }) - }) - - it('should handle the case where lots of existing rows are removed with itemHeights', () => { - expect(calcVScroll({itemCount: 4, itemHeights: [100, 100, 100, 100], - viewportHeight: 400, viewportTop: 12_000, rowHeight: 32})).toEqual({ - start: 0, end: 4, paddingTop: 0, paddingBottom: 0 - }) - }) -}) + it("should handle the case where lots of existing rows are removed with itemHeights", () => { + expect( + calcVScroll({ + itemCount: 4, + itemHeights: [100, 100, 100, 100], + viewportHeight: 400, + viewportTop: 12_000, + rowHeight: 32, + }), + ).toEqual({ + start: 0, + end: 4, + paddingTop: 0, + paddingBottom: 0, + }); + }); +}); diff --git a/web/src/js/__tests__/ducks/_tbackendstate.ts b/web/src/js/__tests__/ducks/_tbackendstate.ts index a847f318d9..c87e049bae 100644 --- a/web/src/js/__tests__/ducks/_tbackendstate.ts +++ b/web/src/js/__tests__/ducks/_tbackendstate.ts @@ -1,4 +1,4 @@ -/** Auto-generated by test_app.py:test_generate_state_js */ +/** Auto-generated by web/gen/state_js.py */ import {BackendState} from '../../ducks/backendState'; export function TBackendState(): Required { return { @@ -7,8 +7,9 @@ export function TBackendState(): Required { "Auto", "Raw" ], - "servers": [ - { + "platform": "darwin", + "servers": { + "regular": { "description": "HTTP(S) proxy", "full_spec": "regular", "is_running": true, @@ -25,7 +26,7 @@ export function TBackendState(): Required { ], "type": "regular" }, - { + "reverse:example.com": { "description": "reverse proxy to example.com", "full_spec": "reverse:example.com", "is_running": false, @@ -33,15 +34,24 @@ export function TBackendState(): Required { "listen_addrs": [], "type": "reverse" }, - { + "socks5": { "description": "SOCKS v5 proxy", "full_spec": "socks5", "is_running": false, "last_exception": null, "listen_addrs": [], "type": "socks5" + }, + "tun": { + "description": "TUN interface", + "full_spec": "tun", + "is_running": true, + "last_exception": null, + "listen_addrs": [], + "tun_name": "tun0", + "type": "tun" } - ], + }, "version": "1.2.3" } } diff --git a/web/src/js/__tests__/ducks/_tflow.ts b/web/src/js/__tests__/ducks/_tflow.ts index 025fccdfef..9fc122d9a3 100644 --- a/web/src/js/__tests__/ducks/_tflow.ts +++ b/web/src/js/__tests__/ducks/_tflow.ts @@ -1,4 +1,4 @@ -/** Auto-generated by test_app.py:test_generate_tflow_js */ +/** Auto-generated by web/gen/tflow_js.py */ import {HTTPFlow, TCPFlow, UDPFlow, DNSFlow} from '../../flow'; export function THTTPFlow(): Required { return { diff --git a/web/src/js/__tests__/ducks/commandBarSpec.tsx b/web/src/js/__tests__/ducks/commandBarSpec.tsx index 5262cbcb68..04c94e5d42 100644 --- a/web/src/js/__tests__/ducks/commandBarSpec.tsx +++ b/web/src/js/__tests__/ducks/commandBarSpec.tsx @@ -1,10 +1,12 @@ -import reduceCommandBar, * as commandBarActions from '../../ducks/commandBar' +import reduceCommandBar, * as commandBarActions from "../../ducks/commandBar"; test("CommandBar", async () => { - expect(reduceCommandBar(undefined, {type: "other"})).toEqual({ - visible: false - }) - expect(reduceCommandBar(undefined, commandBarActions.toggleVisibility())).toEqual({ - visible: true + expect(reduceCommandBar(undefined, { type: "other" })).toEqual({ + visible: false, + }); + expect( + reduceCommandBar(undefined, commandBarActions.toggleVisibility()), + ).toEqual({ + visible: true, }); }); diff --git a/web/src/js/__tests__/ducks/connectionSpec.tsx b/web/src/js/__tests__/ducks/connectionSpec.tsx index 68631d9ed6..ee90c06c5c 100644 --- a/web/src/js/__tests__/ducks/connectionSpec.tsx +++ b/web/src/js/__tests__/ducks/connectionSpec.tsx @@ -1,41 +1,53 @@ -import reduceConnection from "../../ducks/connection" -import * as ConnectionActions from "../../ducks/connection" -import { ConnectionState } from "../../ducks/connection" +import reduceConnection, * as ConnectionActions from "../../ducks/connection"; +import { ConnectionState } from "../../ducks/connection"; -describe('connection reducer', () => { - it('should return initial state', () => { - expect(reduceConnection(undefined, {type: "other"})).toEqual({ +describe("connection reducer", () => { + it("should return initial state", () => { + expect(reduceConnection(undefined, { type: "other" })).toEqual({ state: ConnectionState.INIT, message: undefined, - }) - }) + }); + }); - it('should handle start fetch', () => { - expect(reduceConnection(undefined, ConnectionActions.startFetching())).toEqual({ + it("should handle start fetch", () => { + expect( + reduceConnection(undefined, ConnectionActions.startFetching()), + ).toEqual({ state: ConnectionState.FETCHING, message: undefined, - }) - }) + }); + }); - it('should handle connection established', () => { - expect(reduceConnection(undefined, ConnectionActions.connectionEstablished())).toEqual({ + it("should handle connection established", () => { + expect( + reduceConnection( + undefined, + ConnectionActions.connectionEstablished(), + ), + ).toEqual({ state: ConnectionState.ESTABLISHED, message: undefined, - }) - }) + }); + }); - it('should handle connection error', () => { - expect(reduceConnection(undefined, ConnectionActions.connectionError("no internet"))).toEqual({ + it("should handle connection error", () => { + expect( + reduceConnection( + undefined, + ConnectionActions.connectionError("no internet"), + ), + ).toEqual({ state: ConnectionState.ERROR, message: "no internet", - }) - }) + }); + }); - it('should handle offline mode', () => { - expect(reduceConnection(undefined, ConnectionActions.setOffline())).toEqual({ + it("should handle offline mode", () => { + expect( + reduceConnection(undefined, ConnectionActions.setOffline()), + ).toEqual({ state: ConnectionState.OFFLINE, message: undefined, - }) - }) - -}) + }); + }); +}); diff --git a/web/src/js/__tests__/ducks/eventLogSpec.tsx b/web/src/js/__tests__/ducks/eventLogSpec.tsx index 2d232b5a5f..f2e042f9e3 100644 --- a/web/src/js/__tests__/ducks/eventLogSpec.tsx +++ b/web/src/js/__tests__/ducks/eventLogSpec.tsx @@ -1,38 +1,53 @@ -import reduceEventLog, * as eventLogActions from '../../ducks/eventLog' -import {reduce} from '../../ducks/utils/store' +import reduceEventLog, * as eventLogActions from "../../ducks/eventLog"; +import { LogLevel } from "../../ducks/eventLog"; +import { reduce } from "../../ducks/utils/store"; -describe('event log reducer', () => { - it('should return initial state', () => { +describe("event log reducer", () => { + it("should return initial state", () => { expect(reduceEventLog(undefined, {})).toEqual({ visible: false, - filters: { debug: false, info: true, web: true, warn: true, error: true }, + filters: { + debug: false, + info: true, + web: true, + warn: true, + error: true, + }, ...reduce(undefined, {}), - }) - }) + }); + }); - it('should be possible to toggle filter', () => { - let state = reduceEventLog(undefined, eventLogActions.add('foo')) - expect(reduceEventLog(state, eventLogActions.toggleFilter('info'))).toEqual({ + it("should be possible to toggle filter", () => { + const state = reduceEventLog(undefined, eventLogActions.add("foo")); + expect( + reduceEventLog(state, eventLogActions.toggleFilter(LogLevel.info)), + ).toEqual({ visible: false, - filters: { ...state.filters, info: false}, - ...reduce(state, {}) - }) - }) + filters: { ...state.filters, info: false }, + ...reduce(state, {}), + }); + }); - it('should be possible to toggle visibility', () => { - let state = reduceEventLog(undefined, {}) - expect(reduceEventLog(state, eventLogActions.toggleVisibility())).toEqual({ + it("should be possible to toggle visibility", () => { + const state = reduceEventLog(undefined, {}); + expect( + reduceEventLog(state, eventLogActions.toggleVisibility()), + ).toEqual({ visible: true, - filters: {...state.filters}, - ...reduce(undefined, {}) - }) - }) + filters: { ...state.filters }, + ...reduce(undefined, {}), + }); + }); - it('should be possible to add message', () => { - let state = reduceEventLog(undefined, eventLogActions.add('foo')) - expect(state.visible).toBeFalsy() + it("should be possible to add message", () => { + const state = reduceEventLog(undefined, eventLogActions.add("foo")); + expect(state.visible).toBeFalsy(); expect(state.filters).toEqual({ - debug: false, info: true, web: true, warn: true, error: true - }) - }) -}) + debug: false, + info: true, + web: true, + warn: true, + error: true, + }); + }); +}); diff --git a/web/src/js/__tests__/ducks/flowsSpec.tsx b/web/src/js/__tests__/ducks/flowsSpec.tsx index fe4db34b93..79d3e53733 100644 --- a/web/src/js/__tests__/ducks/flowsSpec.tsx +++ b/web/src/js/__tests__/ducks/flowsSpec.tsx @@ -1,187 +1,228 @@ import reduceFlows, * as flowActions from "../../ducks/flows"; -import {reduce} from "../../ducks/utils/store" -import {fetchApi} from "../../utils" -import {TFlow, TStore} from "./tutils" -import FlowColumns from "../../components/FlowTable/FlowColumns" +import { reduce } from "../../ducks/utils/store"; +import { fetchApi } from "../../utils"; +import { TFlow, TStore } from "./tutils"; +import FlowColumns from "../../components/FlowTable/FlowColumns"; -jest.mock('../../utils') - -describe('flow reducer', () => { +jest.mock("../../utils"); +describe("flow reducer", () => { let s; - for (let i of ["1", "2", "3", "4"]) { - s = reduceFlows(s, {type: flowActions.ADD, data: {id: i}, cmd: 'add'}) + for (const i of ["1", "2", "3", "4"]) { + s = reduceFlows(s, { + type: flowActions.ADD, + data: { id: i }, + cmd: "add", + }); } - let state = s; + const state = s; - it('should return initial state', () => { + it("should return initial state", () => { expect(reduceFlows(undefined, {})).toEqual({ highlight: undefined, filter: undefined, - sort: {column: undefined, desc: false}, + sort: { column: undefined, desc: false }, selected: [], - ...reduce(undefined, {}) - }) - }) - - describe('selections', () => { - it('should be possible to select a single flow', () => { - expect(reduceFlows(state, flowActions.select("2"))).toEqual( - { - ...state, - selected: ["2"], - } - ) - }) - - it('should be possible to deselect a flow', () => { - expect(reduceFlows({...state, selected: ["1"]}, flowActions.select())).toEqual( - { - ...state, - selected: [], - } - ) - }) - - it('should be possible to select relative', () => { - // haven't selected any flow + ...reduce(undefined, {}), + }); + }); + + describe("selections", () => { + it("should be possible to select a single flow", () => { + expect(reduceFlows(state, flowActions.select("2"))).toEqual({ + ...state, + selected: ["2"], + }); + }); + + it("should be possible to deselect a flow", () => { expect( - flowActions.selectRelative(state, 1) - ).toEqual( - flowActions.select("4") - ) + reduceFlows( + { ...state, selected: ["1"] }, + flowActions.select(), + ), + ).toEqual({ + ...state, + selected: [], + }); + }); + + it("should be possible to select relative", () => { + // haven't selected any flow + expect(flowActions.selectRelative(state, 1)).toEqual( + flowActions.select("4"), + ); // already selected some flows expect( - flowActions.selectRelative({...state, selected: [2]}, 1) - ).toEqual( - flowActions.select("3") - ) - }) - - it('should update state.selected on remove', () => { - let next - next = reduceFlows({...state, selected: ["2"]}, { - type: flowActions.REMOVE, - data: "2", - cmd: 'remove' - }) - expect(next.selected).toEqual(["3"]) + flowActions.selectRelative({ ...state, selected: [2] }, 1), + ).toEqual(flowActions.select("3")); + }); + + it("should update state.selected on remove", () => { + let next; + next = reduceFlows( + { ...state, selected: ["2"] }, + { + type: flowActions.REMOVE, + data: "2", + cmd: "remove", + }, + ); + expect(next.selected).toEqual(["3"]); //last row - next = reduceFlows({...state, selected: ["4"]}, { - type: flowActions.REMOVE, - data: "4", - cmd: 'remove' - }) - expect(next.selected).toEqual(["3"]) + next = reduceFlows( + { ...state, selected: ["4"] }, + { + type: flowActions.REMOVE, + data: "4", + cmd: "remove", + }, + ); + expect(next.selected).toEqual(["3"]); //multiple selection - next = reduceFlows({...state, selected: ["2", "3", "4"]}, { - type: flowActions.REMOVE, - data: "3", - cmd: 'remove' - }) - expect(next.selected).toEqual(["2", "4"]) - }) - }) - - it('should be possible to set filter', () => { - let filt = "~u 123" - expect(reduceFlows(undefined, flowActions.setFilter(filt)).filter).toEqual(filt) - }) - - it('should be possible to set highlight', () => { - let key = "foo" - expect(reduceFlows(undefined, flowActions.setHighlight(key)).highlight).toEqual(key) - }) - - it('should be possible to set sort', () => { - let sort = {column: "tls", desc: true} - expect(reduceFlows(undefined, flowActions.setSort(sort.column, sort.desc)).sort).toEqual(sort) - }) - -}) - -describe('flows actions', () => { - - let store = TStore(); - let tflow = TFlow(); - - it('should handle resume action', () => { - store.dispatch(flowActions.resume(tflow)) - expect(fetchApi).toBeCalledWith('/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/resume', {method: 'POST'}) - }) - - it('should handle resumeAll action', () => { - store.dispatch(flowActions.resumeAll()) - expect(fetchApi).toBeCalledWith('/flows/resume', {method: 'POST'}) - }) - - it('should handle kill action', () => { - store.dispatch(flowActions.kill(tflow)) - expect(fetchApi).toBeCalledWith('/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/kill', {method: 'POST'}) - - }) - - it('should handle killAll action', () => { - store.dispatch(flowActions.killAll()) - expect(fetchApi).toBeCalledWith('/flows/kill', {method: 'POST'}) - }) - - it('should handle remove action', () => { - store.dispatch(flowActions.remove(tflow)) - expect(fetchApi).toBeCalledWith('/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29', {method: 'DELETE'}) - }) - - it('should handle duplicate action', () => { - store.dispatch(flowActions.duplicate(tflow)) - expect(fetchApi).toBeCalledWith('/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/duplicate', {method: 'POST'}) - }) - - it('should handle replay action', () => { - store.dispatch(flowActions.replay(tflow)) - expect(fetchApi).toBeCalledWith('/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/replay', {method: 'POST'}) - }) - - it('should handle revert action', () => { - store.dispatch(flowActions.revert(tflow)) - expect(fetchApi).toBeCalledWith('/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/revert', {method: 'POST'}) - }) - - it('should handle update action', () => { - store.dispatch(flowActions.update(tflow, 'foo')) - expect(fetchApi.put).toBeCalledWith('/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29', 'foo') - }) - - it('should handle uploadContent action', () => { - let body = new FormData(), - file = new window.Blob(['foo'], {type: 'plain/text'}) - body.append('file', file) - store.dispatch(flowActions.uploadContent(tflow, 'foo', 'foo')) + next = reduceFlows( + { ...state, selected: ["2", "3", "4"] }, + { + type: flowActions.REMOVE, + data: "3", + cmd: "remove", + }, + ); + expect(next.selected).toEqual(["2", "4"]); + }); + }); + + it("should be possible to set filter", () => { + const filt = "~u 123"; + expect( + reduceFlows(undefined, flowActions.setFilter(filt)).filter, + ).toEqual(filt); + }); + + it("should be possible to set highlight", () => { + const key = "foo"; + expect( + reduceFlows(undefined, flowActions.setHighlight(key)).highlight, + ).toEqual(key); + }); + + it("should be possible to set sort", () => { + const sort = { column: "tls", desc: true }; + expect( + reduceFlows(undefined, flowActions.setSort(sort.column, sort.desc)) + .sort, + ).toEqual(sort); + }); +}); + +describe("flows actions", () => { + const store = TStore(); + const tflow = TFlow(); + + it("should handle resume action", () => { + store.dispatch(flowActions.resume(tflow)); + expect(fetchApi).toBeCalledWith( + "/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/resume", + { method: "POST" }, + ); + }); + + it("should handle resumeAll action", () => { + store.dispatch(flowActions.resumeAll()); + expect(fetchApi).toBeCalledWith("/flows/resume", { method: "POST" }); + }); + + it("should handle kill action", () => { + store.dispatch(flowActions.kill(tflow)); + expect(fetchApi).toBeCalledWith( + "/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/kill", + { method: "POST" }, + ); + }); + + it("should handle killAll action", () => { + store.dispatch(flowActions.killAll()); + expect(fetchApi).toBeCalledWith("/flows/kill", { method: "POST" }); + }); + + it("should handle remove action", () => { + store.dispatch(flowActions.remove(tflow)); + expect(fetchApi).toBeCalledWith( + "/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29", + { method: "DELETE" }, + ); + }); + + it("should handle duplicate action", () => { + store.dispatch(flowActions.duplicate(tflow)); + expect(fetchApi).toBeCalledWith( + "/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/duplicate", + { method: "POST" }, + ); + }); + + it("should handle replay action", () => { + store.dispatch(flowActions.replay(tflow)); + expect(fetchApi).toBeCalledWith( + "/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/replay", + { method: "POST" }, + ); + }); + + it("should handle revert action", () => { + store.dispatch(flowActions.revert(tflow)); + expect(fetchApi).toBeCalledWith( + "/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/revert", + { method: "POST" }, + ); + }); + + it("should handle update action", () => { + store.dispatch(flowActions.update(tflow, "foo")); + expect(fetchApi.put).toBeCalledWith( + "/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29", + "foo", + ); + }); + + it("should handle uploadContent action", () => { + const body = new FormData(); + const file = new window.Blob(["foo"], { type: "plain/text" }); + body.append("file", file); + store.dispatch(flowActions.uploadContent(tflow, "foo", "foo")); // window.Blob's lastModified is always the current time, // which causes flaky tests on comparison. - expect(fetchApi).toBeCalledWith('/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/foo/content.data', { - method: 'POST', - body: expect.anything() - }) - }) - - it('should handle clear action', () => { - store.dispatch(flowActions.clear()) - expect(fetchApi).toBeCalledWith('/clear', {method: 'POST'}) - }) - - it('should handle upload action', () => { - let body = new FormData() - body.append('file', 'foo') - store.dispatch(flowActions.upload('foo')) - expect(fetchApi).toBeCalledWith('/flows/dump', {method: 'POST', body}) - }) -}) + expect(fetchApi).toBeCalledWith( + "/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/foo/content.data", + { + method: "POST", + body: expect.anything(), + }, + ); + }); + + it("should handle clear action", () => { + store.dispatch(flowActions.clear()); + expect(fetchApi).toBeCalledWith("/clear", { method: "POST" }); + }); + + it("should handle upload action", () => { + const body = new FormData(); + body.append("file", "foo"); + store.dispatch(flowActions.upload("foo")); + expect(fetchApi).toBeCalledWith("/flows/dump", { + method: "POST", + body, + }); + }); +}); test("makeSort", () => { - const a = TFlow(), b = TFlow(); + const a = TFlow(); + const b = TFlow(); a.request.scheme = "https"; a.request.method = "POST"; a.request.path = "/foo"; @@ -189,9 +230,8 @@ test("makeSort", () => { a.response.status_code = 418; Object.keys(FlowColumns).forEach((column, i) => { - // @ts-ignore - const sort = flowActions.makeSort({column, desc: i % 2 == 0}); + // @ts-expect-error jest is funky about type annotations here. + const sort = flowActions.makeSort({ column, desc: i % 2 == 0 }); expect(sort(a, b)).toBeDefined(); - }) - + }); }); diff --git a/web/src/js/__tests__/ducks/indexSpec.tsx b/web/src/js/__tests__/ducks/indexSpec.tsx deleted file mode 100644 index efe6ce7f89..0000000000 --- a/web/src/js/__tests__/ducks/indexSpec.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import {rootReducer} from '../../ducks/index' - -describe('reduceState in js/ducks/index.js', () => { - it('should combine flow and header', () => { - let state = rootReducer(undefined, {type: "other"}) - expect(state.hasOwnProperty('eventLog')).toBeTruthy() - expect(state.hasOwnProperty('flows')).toBeTruthy() - expect(state.hasOwnProperty('connection')).toBeTruthy() - expect(state.hasOwnProperty('ui')).toBeTruthy() - }) -}) diff --git a/web/src/js/__tests__/ducks/modes/dnsSpec.tsx b/web/src/js/__tests__/ducks/modes/dnsSpec.tsx new file mode 100644 index 0000000000..690b3d8dad --- /dev/null +++ b/web/src/js/__tests__/ducks/modes/dnsSpec.tsx @@ -0,0 +1,116 @@ +import dnsReducer, { + initialState, + setListenHost, + setListenPort, + setActive, +} from "./../../../ducks/modes/dns"; +import { + RECEIVE as STATE_RECEIVE, + BackendState, +} from "../../../ducks/backendState"; +import { TStore } from "../tutils"; +import fetchMock, { enableFetchMocks } from "jest-fetch-mock"; +import { PayloadAction } from "@reduxjs/toolkit"; + +describe("dnsSlice", () => { + it("should have working setters", async () => { + enableFetchMocks(); + const store = TStore(); + + expect(store.getState().modes.dns[0]).toEqual({ + active: false, + }); + + const server = store.getState().modes.dns[0]; + await store.dispatch(setActive({ value: true, server })); + await store.dispatch(setListenHost({ value: "127.0.0.1", server })); + await store.dispatch(setListenPort({ value: 4444, server })); + + expect(store.getState().modes.dns[0]).toEqual({ + active: true, + listen_host: "127.0.0.1", + listen_port: 4444, + }); + + expect(fetchMock).toHaveBeenCalledTimes(3); + }); + + it("should handle error when setting dns mode", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.dns[0]; + await store.dispatch(setActive({ value: false, server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.dns[0].error).toBe("invalid spec"); + }); + + it("should handle error when setting listen port", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.dns[0]; + await store.dispatch(setListenPort({ value: 4444, server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.dns[0].error).toBe("invalid spec"); + }); + + it("should handle error when setting listen host", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.dns[0]; + await store.dispatch(setListenHost({ value: "localhost", server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.dns[0].error).toBe("invalid spec"); + }); + + it("should handle RECEIVE_STATE with an active dns proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: { + "dns@localhost:8081": { + description: "HTTP(S) proxy", + full_spec: "dns@localhost:8081", + is_running: true, + last_exception: null, + listen_addrs: [ + ["127.0.0.1", 8081], + ["::1", 8081], + ], + type: "dns", + }, + }, + }, + } as PayloadAction>; + const newState = dnsReducer(initialState, action); + expect(newState).toEqual([ + { + active: true, + listen_host: "localhost", + listen_port: 8081, + ui_id: newState[0].ui_id, + }, + ]); + }); + + it("should handle RECEIVE_STATE with no active dns proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: {}, + }, + } as PayloadAction>; + const newState = dnsReducer(initialState, action); + expect(newState).toEqual([ + { + active: false, + ui_id: newState[0].ui_id, + }, + ]); + }); +}); diff --git a/web/src/js/__tests__/ducks/modes/localSpec.tsx b/web/src/js/__tests__/ducks/modes/localSpec.tsx new file mode 100644 index 0000000000..bfaa1bbf7d --- /dev/null +++ b/web/src/js/__tests__/ducks/modes/localSpec.tsx @@ -0,0 +1,105 @@ +import localReducer, { + initialState, + setActive, + setSelectedProcesses, +} from "../../../ducks/modes/local"; +import { + RECEIVE as STATE_RECEIVE, + BackendState, +} from "../../../ducks/backendState"; +import { TStore } from "../tutils"; +import fetchMock, { enableFetchMocks } from "jest-fetch-mock"; +import { PayloadAction } from "@reduxjs/toolkit"; + +describe("localSlice", () => { + beforeEach(() => { + enableFetchMocks(); + fetchMock.resetMocks(); + }); + + it("should have working setters", async () => { + enableFetchMocks(); + const store = TStore(); + + expect(store.getState().modes.local[0]).toEqual({ + active: false, + selectedProcesses: "", + }); + + const server = store.getState().modes.local[0]; + await store.dispatch(setActive({ value: true, server })); + await store.dispatch(setSelectedProcesses({ value: "curl", server })); + + expect(store.getState().modes.local[0]).toEqual({ + active: true, + selectedProcesses: "curl", + }); + + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + + it("should handle error when setting local mode", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.local[0]; + await store.dispatch(setActive({ value: true, server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.local[0].error).toBe("invalid spec"); + }); + + it("should handle error when setting processes", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.local[0]; + await store.dispatch(setSelectedProcesses({ value: "curl", server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.local[0].error).toBe("invalid spec"); + }); + + it("should handle RECEIVE_STATE with an active local proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: { + "local:curl": { + description: "Local redirector", + full_spec: "local:curl", + is_running: true, + last_exception: null, + listen_addrs: [], + type: "local", + }, + }, + }, + } as PayloadAction>; + const newState = localReducer(initialState, action); + expect(newState).toEqual([ + { + active: true, + selectedProcesses: "curl", + ui_id: newState[0].ui_id, + }, + ]); + }); + + it("should handle RECEIVE_STATE with no active local proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: {}, + }, + } as PayloadAction>; + const newState = localReducer(initialState, action); + expect(newState).toEqual([ + { + active: false, + selectedProcesses: "", + ui_id: newState[0].ui_id, + }, + ]); + }); +}); diff --git a/web/src/js/__tests__/ducks/modes/regularSpec.tsx b/web/src/js/__tests__/ducks/modes/regularSpec.tsx new file mode 100644 index 0000000000..e786a3fb3c --- /dev/null +++ b/web/src/js/__tests__/ducks/modes/regularSpec.tsx @@ -0,0 +1,116 @@ +import regularReducer, { + initialState, + setListenHost, + setListenPort, + setActive, +} from "./../../../ducks/modes/regular"; +import { + RECEIVE as STATE_RECEIVE, + BackendState, +} from "../../../ducks/backendState"; +import { TStore } from "../tutils"; +import fetchMock, { enableFetchMocks } from "jest-fetch-mock"; +import { PayloadAction } from "@reduxjs/toolkit"; + +describe("regularSlice", () => { + it("should have working setters", async () => { + enableFetchMocks(); + const store = TStore(); + + expect(store.getState().modes.regular[0]).toEqual({ + active: true, + }); + + const server = store.getState().modes.regular[0]; + await store.dispatch(setActive({ value: false, server })); + await store.dispatch(setListenHost({ value: "127.0.0.1", server })); + await store.dispatch(setListenPort({ value: 4444, server })); + + expect(store.getState().modes.regular[0]).toEqual({ + active: false, + listen_host: "127.0.0.1", + listen_port: 4444, + }); + + expect(fetchMock).toHaveBeenCalledTimes(3); + }); + + it("should handle error when setting regular mode", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.regular[0]; + await store.dispatch(setActive({ value: false, server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.regular[0].error).toBe("invalid spec"); + }); + + it("should handle error when setting listen port", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.regular[0]; + await store.dispatch(setListenPort({ value: 4444, server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.regular[0].error).toBe("invalid spec"); + }); + + it("should handle error when setting listen host", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.regular[0]; + await store.dispatch(setListenHost({ value: "localhost", server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.regular[0].error).toBe("invalid spec"); + }); + + it("should handle RECEIVE_STATE with an active regular proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: { + "regular@localhost:8081": { + description: "HTTP(S) proxy", + full_spec: "regular@localhost:8081", + is_running: true, + last_exception: null, + listen_addrs: [ + ["127.0.0.1", 8081], + ["::1", 8081], + ], + type: "regular", + }, + }, + }, + } as PayloadAction>; + const newState = regularReducer(initialState, action); + expect(newState).toEqual([ + { + active: true, + listen_host: "localhost", + listen_port: 8081, + ui_id: newState[0].ui_id, + }, + ]); + }); + + it("should handle RECEIVE_STATE with no active regular proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: {}, + }, + } as PayloadAction>; + const newState = regularReducer(initialState, action); + expect(newState).toEqual([ + { + active: false, + ui_id: newState[0].ui_id, + }, + ]); + }); +}); diff --git a/web/src/js/__tests__/ducks/modes/reverseSpec.tsx b/web/src/js/__tests__/ducks/modes/reverseSpec.tsx new file mode 100644 index 0000000000..fb19e908f4 --- /dev/null +++ b/web/src/js/__tests__/ducks/modes/reverseSpec.tsx @@ -0,0 +1,224 @@ +import fetchMock, { enableFetchMocks } from "jest-fetch-mock"; +import reverseReducer, { + initialState, + setDestination, + setProtocol, + setActive, + setListenHost, + setListenPort, + addServer, + removeServer, +} from "../../../ducks/modes/reverse"; +import { + RECEIVE as STATE_RECEIVE, + BackendState, +} from "../../../ducks/backendState"; +import { TStore } from "../tutils"; +import { ReverseProxyProtocols } from "../../../backends/consts"; +import { PayloadAction } from "@reduxjs/toolkit"; + +describe("reverseSlice", () => { + it("should have working setters", async () => { + enableFetchMocks(); + const store = TStore(); + + expect(store.getState().modes.reverse[0]).toEqual({ + active: false, + protocol: ReverseProxyProtocols.HTTPS, + destination: "example.com", + }); + + const firstServer = store.getState().modes.reverse[0]; + await store.dispatch(setActive({ value: true, server: firstServer })); + await store.dispatch( + setListenHost({ value: "127.0.0.1", server: firstServer }), + ); + await store.dispatch( + setListenPort({ value: 4444, server: firstServer }), + ); + await store.dispatch( + setProtocol({ + value: ReverseProxyProtocols.HTTPS, + server: firstServer, + }), + ); + await store.dispatch( + setDestination({ value: "example.com:8085", server: firstServer }), + ); + + expect(store.getState().modes.reverse[0]).toEqual({ + active: true, + listen_host: "127.0.0.1", + listen_port: 4444, + protocol: ReverseProxyProtocols.HTTPS, + destination: "example.com:8085", + }); + + expect(fetchMock).toHaveBeenCalledTimes(5); + }); + + it("should handle error when setting reverse mode", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.reverse[0]; + await store.dispatch(setActive({ value: true, server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.reverse[0].error).toBe("invalid spec"); + }); + + it("should handle error when setting listen port", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.reverse[0]; + await store.dispatch(setListenPort({ value: 4444, server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.reverse[0].error).toBe("invalid spec"); + }); + + it("should handle error when setting listen host", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.reverse[0]; + await store.dispatch(setListenHost({ value: "localhost", server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.reverse[0].error).toBe("invalid spec"); + }); + + it("should handle error when setting destination", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.reverse[0]; + await store.dispatch(setDestination({ value: "example.com", server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.reverse[0].error).toBe("invalid spec"); + }); + + it("should handle the addition of a new default reverse server", async () => { + enableFetchMocks(); + + const store = TStore(); + + expect(store.getState().modes.reverse.length).toBe(2); + + await store.dispatch(addServer()); + + expect(store.getState().modes.reverse.length).toBe(3); + expect(store.getState().modes.reverse[2]).toEqual({ + active: false, + protocol: ReverseProxyProtocols.HTTPS, + destination: "", + ui_id: store.getState().modes.reverse[2].ui_id, + }); + }); + + it("should handle the deletion of an active reverse server", async () => { + enableFetchMocks(); + + const store = TStore(); + + expect(store.getState().modes.reverse.length).toBe(2); + + const firstServer = store.getState().modes.reverse[0]; + await store.dispatch(setActive({ value: true, server: firstServer })); + + const consoleSpy = jest.spyOn(console, "error"); + await store.dispatch(removeServer(store.getState().modes.reverse[0])); + + expect(store.getState().modes.reverse.length).toBe(1); + expect(consoleSpy).toHaveBeenCalledWith( + "servers should be deactivated before removal", + ); + consoleSpy.mockRestore(); + }); + + it("should handle RECEIVE_STATE with an active reverse proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: { + "reverse:tls://example.com:8085@localhost:8080": { + description: "reverse proxy to tls://example.com:8085", + full_spec: + "reverse:tls://example.com:8085@localhost:8080", + is_running: true, + last_exception: null, + listen_addrs: [ + ["127.0.0.1", 8080], + ["::1", 8080, 0, 0], + ], + type: "reverse", + }, + }, + }, + } as PayloadAction>; + const newState = reverseReducer(initialState, action); + expect(newState).toEqual([ + { + active: true, + protocol: ReverseProxyProtocols.TLS, + destination: "example.com:8085", + listen_host: "localhost", + listen_port: 8080, + ui_id: newState[0].ui_id, + }, + ]); + }); + + it("should handle RECEIVE_STATE with no active reverse proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: {}, + }, + } as PayloadAction>; + const newState = reverseReducer(initialState, action); + expect(newState).toEqual([ + { + active: false, + ui_id: newState[0].ui_id, + destination: "", + protocol: ReverseProxyProtocols.HTTPS, + }, + ]); + }); + + it("should handle RECEIVE_STATE with an active reverse proxy and set protocol to HTTPS if destination is missing", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: { + "reverse:example.com:8085@localhost:8080": { + description: "reverse proxy to example.com:8085", + full_spec: "reverse:example.com:8085@localhost:8080", + is_running: true, + last_exception: null, + listen_addrs: [ + ["127.0.0.1", 8080], + ["::1", 8080, 0, 0], + ], + type: "reverse", + }, + }, + }, + } as PayloadAction>; + const newState = reverseReducer(initialState, action); + expect(newState).toEqual([ + { + active: true, + protocol: ReverseProxyProtocols.HTTPS, + destination: "example.com:8085", + listen_host: "localhost", + listen_port: 8080, + ui_id: newState[0].ui_id, + }, + ]); + }); +}); diff --git a/web/src/js/__tests__/ducks/modes/socksSpec.tsx b/web/src/js/__tests__/ducks/modes/socksSpec.tsx new file mode 100644 index 0000000000..dda061c0b5 --- /dev/null +++ b/web/src/js/__tests__/ducks/modes/socksSpec.tsx @@ -0,0 +1,116 @@ +import socksReducer, { + initialState, + setListenHost, + setListenPort, + setActive, +} from "./../../../ducks/modes/socks"; +import { + RECEIVE as STATE_RECEIVE, + BackendState, +} from "../../../ducks/backendState"; +import { TStore } from "../tutils"; +import fetchMock, { enableFetchMocks } from "jest-fetch-mock"; +import { PayloadAction } from "@reduxjs/toolkit"; + +describe("socksSlice", () => { + it("should have working setters", async () => { + enableFetchMocks(); + const store = TStore(); + + expect(store.getState().modes.socks[0]).toEqual({ + active: false, + }); + + const server = store.getState().modes.socks[0]; + await store.dispatch(setActive({ value: true, server })); + await store.dispatch(setListenHost({ value: "127.0.0.1", server })); + await store.dispatch(setListenPort({ value: 4444, server })); + + expect(store.getState().modes.socks[0]).toEqual({ + active: true, + listen_host: "127.0.0.1", + listen_port: 4444, + }); + + expect(fetchMock).toHaveBeenCalledTimes(3); + }); + + it("should handle error when setting socks mode", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.socks[0]; + await store.dispatch(setActive({ value: false, server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.socks[0].error).toBe("invalid spec"); + }); + + it("should handle error when setting listen port", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.socks[0]; + await store.dispatch(setListenPort({ value: 4444, server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.socks[0].error).toBe("invalid spec"); + }); + + it("should handle error when setting listen host", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.socks[0]; + await store.dispatch(setListenHost({ value: "localhost", server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.socks[0].error).toBe("invalid spec"); + }); + + it("should handle RECEIVE_STATE with an active socks proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: { + "socks5@localhost:8081": { + description: "SOCKS v5 proxy", + full_spec: "socks5@localhost:8081", + is_running: true, + last_exception: null, + listen_addrs: [ + ["127.0.0.1", 8081], + ["::1", 8081], + ], + type: "socks5", + }, + }, + }, + } as PayloadAction>; + const newState = socksReducer(initialState, action); + expect(newState).toEqual([ + { + active: true, + listen_host: "localhost", + listen_port: 8081, + ui_id: newState[0].ui_id, + }, + ]); + }); + + it("should handle RECEIVE_STATE with no active socks proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: {}, + }, + } as PayloadAction>; + const newState = socksReducer(initialState, action); + expect(newState).toEqual([ + { + active: false, + ui_id: newState[0].ui_id, + }, + ]); + }); +}); diff --git a/web/src/js/__tests__/ducks/modes/transparentSpec.tsx b/web/src/js/__tests__/ducks/modes/transparentSpec.tsx new file mode 100644 index 0000000000..d2b3c701ce --- /dev/null +++ b/web/src/js/__tests__/ducks/modes/transparentSpec.tsx @@ -0,0 +1,122 @@ +import transparentReducer, { + initialState, + setListenHost, + setListenPort, + setActive, +} from "./../../../ducks/modes/transparent"; +import { + RECEIVE as STATE_RECEIVE, + BackendState, +} from "../../../ducks/backendState"; +import { TStore } from "../tutils"; +import fetchMock, { enableFetchMocks } from "jest-fetch-mock"; +import { PayloadAction } from "@reduxjs/toolkit"; + +describe("transparentSlice", () => { + it("should have working setters", async () => { + enableFetchMocks(); + const store = TStore(); + + expect(store.getState().modes.transparent[0]).toEqual({ + active: false, + }); + + const server = store.getState().modes.transparent[0]; + await store.dispatch(setActive({ value: true, server })); + await store.dispatch(setListenHost({ value: "127.0.0.1", server })); + await store.dispatch(setListenPort({ value: 4444, server })); + + expect(store.getState().modes.transparent[0]).toEqual({ + active: true, + listen_host: "127.0.0.1", + listen_port: 4444, + }); + + expect(fetchMock).toHaveBeenCalledTimes(3); + }); + + it("should handle error when setting transparent mode", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.transparent[0]; + await store.dispatch(setActive({ value: false, server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.transparent[0].error).toBe( + "invalid spec", + ); + }); + + it("should handle error when setting listen port", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.transparent[0]; + await store.dispatch(setListenPort({ value: 4444, server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.transparent[0].error).toBe( + "invalid spec", + ); + }); + + it("should handle error when setting listen host", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.transparent[0]; + await store.dispatch(setListenHost({ value: "localhost", server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.transparent[0].error).toBe( + "invalid spec", + ); + }); + + it("should handle RECEIVE_STATE with an active transparent proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: { + "transparent@localhost:8081": { + description: "Transparent Proxy", + full_spec: "transparent@localhost:8081", + is_running: true, + last_exception: null, + listen_addrs: [ + ["127.0.0.1", 8081], + ["::1", 8081], + ], + type: "transparent", + }, + }, + }, + } as PayloadAction>; + const newState = transparentReducer(initialState, action); + expect(newState).toEqual([ + { + active: true, + listen_host: "localhost", + listen_port: 8081, + ui_id: newState[0].ui_id, + }, + ]); + }); + + it("should handle RECEIVE_STATE with no active transparent proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: {}, + }, + } as PayloadAction>; + const newState = transparentReducer(initialState, action); + expect(newState).toEqual([ + { + active: false, + ui_id: newState[0].ui_id, + }, + ]); + }); +}); diff --git a/web/src/js/__tests__/ducks/modes/upstreamSpec.tsx b/web/src/js/__tests__/ducks/modes/upstreamSpec.tsx new file mode 100644 index 0000000000..daab98690d --- /dev/null +++ b/web/src/js/__tests__/ducks/modes/upstreamSpec.tsx @@ -0,0 +1,153 @@ +import fetchMock, { enableFetchMocks } from "jest-fetch-mock"; +import upstreamReducer, { + initialState, + setDestination, + setActive, + setListenHost, + setListenPort, +} from "../../../ducks/modes/upstream"; +import { + RECEIVE as STATE_RECEIVE, + BackendState, +} from "../../../ducks/backendState"; +import { TStore } from "../tutils"; +import { PayloadAction } from "@reduxjs/toolkit"; + +describe("upstreamSlice", () => { + it("should have working setters", async () => { + enableFetchMocks(); + const store = TStore(); + + expect(store.getState().modes.upstream[0]).toEqual({ + active: false, + destination: "example.com", + }); + + await store.dispatch( + setActive({ + value: true, + server: store.getState().modes.upstream[0], + }), + ); + await store.dispatch( + setListenHost({ + value: "127.0.0.1", + server: store.getState().modes.upstream[0], + }), + ); + await store.dispatch( + setListenPort({ + value: 4444, + server: store.getState().modes.upstream[0], + }), + ); + await store.dispatch( + setDestination({ + value: "example.com:8085", + server: store.getState().modes.upstream[0], + }), + ); + + expect(store.getState().modes.upstream[0]).toEqual({ + active: true, + listen_host: "127.0.0.1", + listen_port: 4444, + destination: "example.com:8085", + }); + + expect(fetchMock).toHaveBeenCalledTimes(4); + }); + + it("should handle error when setting upstream mode", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.upstream[0]; + await store.dispatch(setActive({ value: true, server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.upstream[0].error).toBe("invalid spec"); + }); + + it("should handle error when setting listen port", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.upstream[0]; + await store.dispatch(setListenPort({ value: 4444, server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.upstream[0].error).toBe("invalid spec"); + }); + + it("should handle error when setting listen host", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.upstream[0]; + await store.dispatch(setListenHost({ value: "localhost", server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.upstream[0].error).toBe("invalid spec"); + }); + + it("should handle error when setting destination", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.upstream[0]; + await store.dispatch(setDestination({ value: "example.com", server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.upstream[0].error).toBe("invalid spec"); + }); + + it("should handle RECEIVE_STATE with an active upstream proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: { + "upstream:https://example.com:8085@localhost:8080": { + description: "HTTP(S) proxy (upstream mode)", + full_spec: + "upstream:https://example.com:8085@localhost:8080", + is_running: true, + last_exception: null, + listen_addrs: [ + ["127.0.0.1", 8080], + ["::1", 8080, 0, 0], + ], + type: "upstream", + }, + }, + }, + } as PayloadAction>; + const newState = upstreamReducer(initialState, action); + expect(newState).toEqual([ + { + active: true, + destination: "https://example.com:8085", + listen_host: "localhost", + listen_port: 8080, + ui_id: newState[0].ui_id, + }, + ]); + }); + + it("should handle RECEIVE_STATE with no active upstream proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: {}, + }, + } as PayloadAction>; + const newState = upstreamReducer(initialState, action); + expect(newState).toEqual([ + { + active: false, + ui_id: newState[0].ui_id, + destination: "", + }, + ]); + }); +}); diff --git a/web/src/js/__tests__/ducks/modes/utilsSpec.tsx b/web/src/js/__tests__/ducks/modes/utilsSpec.tsx new file mode 100644 index 0000000000..f02df1fef5 --- /dev/null +++ b/web/src/js/__tests__/ducks/modes/utilsSpec.tsx @@ -0,0 +1,53 @@ +import fetchMock, { enableFetchMocks } from "jest-fetch-mock"; +import { TStore } from "../tutils"; +import { isActiveMode, updateModes } from "../../../ducks/modes/utils"; + +enableFetchMocks(); + +describe("updateMode action creator", () => { + beforeEach(() => { + fetchMock.resetMocks(); + }); + + it("should call updateMode method with a successful api call", async () => { + const store = TStore(); + fetchMock.mockResponseOnce(JSON.stringify({ success: true })); + + await store.dispatch(() => updateModes(null, store)); + + const expectedUrl = "./options"; + const expectedBody = JSON.stringify({ mode: ["regular"] }); + + const actualCall = fetchMock.mock.calls[0]; + const actualUrl = actualCall[0]; + const actualBody = actualCall[1]?.body; + + expect(actualUrl).toEqual(expectedUrl); + expect(actualBody).toEqual(expectedBody); + }); + + it("fetch HTTP status != 200 throws", async () => { + const store = TStore(); + fetchMock.mockResponseOnce("invalid query", { status: 400 }); + await expect( + store.dispatch(() => updateModes(null, store)), + ).rejects.toThrow("invalid query"); + }); + + it("fetch error throws", async () => { + const store = TStore(); + fetchMock.mockRejectOnce(new Error("network error")); + await expect( + store.dispatch(() => updateModes(null, store)), + ).rejects.toThrow("network error"); + }); +}); + +describe("isActiveMode", () => { + it("should work", () => { + expect(isActiveMode({ active: false })).toBe(false); + expect(isActiveMode({ active: true })).toBe(true); + expect(isActiveMode({ active: true, error: "failed" })).toBe(false); + expect(isActiveMode({ active: false, error: "failed" })).toBe(false); + }); +}); diff --git a/web/src/js/__tests__/ducks/modes/wireguardSpec.tsx b/web/src/js/__tests__/ducks/modes/wireguardSpec.tsx new file mode 100644 index 0000000000..da19b7a6f7 --- /dev/null +++ b/web/src/js/__tests__/ducks/modes/wireguardSpec.tsx @@ -0,0 +1,131 @@ +import wireguardReducer, { + initialState, + setListenHost, + setListenPort, + setActive, + setFilePath, +} from "./../../../ducks/modes/wireguard"; +import { + RECEIVE as STATE_RECEIVE, + BackendState, +} from "../../../ducks/backendState"; +import { TStore } from "../tutils"; +import fetchMock, { enableFetchMocks } from "jest-fetch-mock"; +import { PayloadAction } from "@reduxjs/toolkit"; + +describe("wireguardSlice", () => { + it("should have working setters", async () => { + enableFetchMocks(); + const store = TStore(); + + expect(store.getState().modes.wireguard[0]).toEqual({ + active: false, + }); + + const server = store.getState().modes.wireguard[0]; + await store.dispatch(setActive({ value: false, server })); + await store.dispatch(setListenHost({ value: "127.0.0.1", server })); + await store.dispatch(setListenPort({ value: 4444, server })); + await store.dispatch(setFilePath({ value: "/path/example", server })); + + expect(store.getState().modes.wireguard[0]).toEqual({ + active: false, + listen_host: "127.0.0.1", + listen_port: 4444, + file_path: "/path/example", + }); + + expect(fetchMock).toHaveBeenCalledTimes(4); + }); + + it("should handle error when setting wireguard mode", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.wireguard[0]; + await store.dispatch(setActive({ value: true, server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.wireguard[0].error).toBe("invalid spec"); + }); + + it("should handle error when setting listen port", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.wireguard[0]; + await store.dispatch(setListenPort({ value: 4444, server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.wireguard[0].error).toBe("invalid spec"); + }); + + it("should handle error when setting listen host", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.wireguard[0]; + await store.dispatch(setListenHost({ value: "localhost", server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.wireguard[0].error).toBe("invalid spec"); + }); + + it("should handle error when setting file path", async () => { + fetchMock.mockReject(new Error("invalid spec")); + const store = TStore(); + + const server = store.getState().modes.wireguard[0]; + await store.dispatch(setFilePath({ value: "/path/example", server })); + + expect(fetchMock).toHaveBeenCalled(); + expect(store.getState().modes.wireguard[0].error).toBe("invalid spec"); + }); + + it("should handle RECEIVE_STATE with an active wireguard proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: { + "wireguard:/path/example@localhost:8081": { + description: "WireGuard server", + full_spec: "wireguard:/path/example@localhost:8081", + is_running: true, + last_exception: null, + listen_addrs: [ + ["127.0.0.1", 8081], + ["::1", 8081], + ], + type: "wireguard", + }, + }, + }, + } as PayloadAction>; + const newState = wireguardReducer(initialState, action); + expect(newState).toEqual([ + { + active: true, + listen_host: "localhost", + listen_port: 8081, + file_path: "/path/example", + ui_id: newState[0].ui_id, + }, + ]); + }); + + it("should handle RECEIVE_STATE with no active wireguard proxy", () => { + const action = { + type: STATE_RECEIVE.type, + payload: { + servers: {}, + }, + } as PayloadAction>; + const newState = wireguardReducer(initialState, action); + expect(newState).toEqual([ + { + active: false, + ui_id: newState[0].ui_id, + }, + ]); + }); +}); diff --git a/web/src/js/__tests__/ducks/optionsSpec.tsx b/web/src/js/__tests__/ducks/optionsSpec.tsx index 4614b45bc3..9b5d695585 100644 --- a/web/src/js/__tests__/ducks/optionsSpec.tsx +++ b/web/src/js/__tests__/ducks/optionsSpec.tsx @@ -1,66 +1,81 @@ -import reduceOptions, * as OptionsActions from '../../ducks/options' -import * as OptionsEditorActions from '../../ducks/ui/optionsEditor' -import {enableFetchMocks} from "jest-fetch-mock"; -import {TStore} from "./tutils"; +import reduceOptions, * as optionsActions from "../../ducks/options"; +import { enableFetchMocks } from "jest-fetch-mock"; +import { TStore } from "./tutils"; +import { waitFor } from "@testing-library/dom"; +enableFetchMocks(); -describe('option reducer', () => { - it('should return initial state', () => { - expect(reduceOptions(undefined, {type: "other"})).toEqual(OptionsActions.defaultState) - }) +describe("option reducer", () => { + it("should return initial state", () => { + expect(reduceOptions(undefined, { type: "other" })).toEqual( + optionsActions.defaultState, + ); + }); - it('should handle receive action', () => { - let action = {type: OptionsActions.RECEIVE, data: {id: {value: 'foo'}}} - expect(reduceOptions(undefined, action)).toEqual({id: 'foo'}) - }) + it("should handle receive action", () => { + const action = { + type: optionsActions.RECEIVE, + data: { id: { value: "foo" } }, + }; + expect(reduceOptions(undefined, action)).toEqual({ id: "foo" }); + }); - it('should handle update action', () => { - let action = {type: OptionsActions.UPDATE, data: {id: {value: 1}}} - expect(reduceOptions(undefined, action)).toEqual({...OptionsActions.defaultState, id: 1}) - }) -}) + it("should handle update action", () => { + const action = { + type: optionsActions.UPDATE, + data: { id: { value: 1 } }, + }; + expect(reduceOptions(undefined, action)).toEqual({ + ...optionsActions.defaultState, + id: 1, + }); + }); +}); test("sendUpdate", async () => { - enableFetchMocks(); - let store = TStore(); - - fetchMock.mockResponseOnce("fooerror", {status: 404}); - await store.dispatch(dispatch => OptionsActions.pureSendUpdate("intercept", "~~~", dispatch)) - expect(store.getActions()).toEqual([ - OptionsEditorActions.updateError("intercept", "fooerror") - ]) + const store = TStore(); - store.clearActions(); - fetchMock.mockResponseOnce("", {status: 200}); - await store.dispatch(dispatch => OptionsActions.pureSendUpdate("intercept", "valid", dispatch)) - expect(store.getActions()).toEqual([ - OptionsEditorActions.updateSuccess("intercept") - ]) + fetchMock.mockResponseOnce("fooerror", { status: 404 }); + await store.dispatch(optionsActions.update("intercept", "~~~")); + await waitFor(() => + expect(store.getState().ui.optionsEditor.intercept).toEqual({ + error: "fooerror", + isUpdating: false, + value: "~~~", + }), + ); + fetchMock.mockResponseOnce("", { status: 200 }); + await store.dispatch(optionsActions.update("intercept", "valid")); + await waitFor(() => + expect(store.getState().ui.optionsEditor.intercept).toBeUndefined(), + ); }); test("save", async () => { - enableFetchMocks(); fetchMock.mockResponseOnce(""); - let store = TStore(); - await store.dispatch(OptionsActions.save()); + const store = TStore(); + await store.dispatch(optionsActions.save()); expect(fetchMock).toBeCalled(); }); test("addInterceptFilter", async () => { - enableFetchMocks(); fetchMock.mockClear(); fetchMock.mockResponses("", ""); - let store = TStore(); - await store.dispatch(OptionsActions.addInterceptFilter("~u foo")); + const store = TStore(); + await store.dispatch(optionsActions.addInterceptFilter("~u foo")); expect(fetchMock.mock.calls[0][1]?.body).toEqual('{"intercept":"~u foo"}'); - store.getState().options.intercept = "~u foo"; - await store.dispatch(OptionsActions.addInterceptFilter("~u foo")); - expect(fetchMock.mock.calls).toHaveLength(1); - - await store.dispatch(OptionsActions.addInterceptFilter("~u bar")); - expect(fetchMock.mock.calls[1][1]?.body).toEqual('{"intercept":"~u foo | ~u bar"}'); + store.dispatch({ + type: optionsActions.UPDATE, + data: { intercept: { value: "~u foo" } }, + }); + await store.dispatch(optionsActions.addInterceptFilter("~u foo")); + expect(fetchMock.mock.calls).toHaveLength(1); + await store.dispatch(optionsActions.addInterceptFilter("~u bar")); + expect(fetchMock.mock.calls[1][1]?.body).toEqual( + '{"intercept":"~u foo | ~u bar"}', + ); }); diff --git a/web/src/js/__tests__/ducks/options_metaSpec.tsx b/web/src/js/__tests__/ducks/options_metaSpec.tsx index e7d1ac4b48..39ff3a80e6 100644 --- a/web/src/js/__tests__/ducks/options_metaSpec.tsx +++ b/web/src/js/__tests__/ducks/options_metaSpec.tsx @@ -2,15 +2,21 @@ import reduceOptionsMeta, * as OptionsMetaActions from "../../ducks/options_meta import * as OptionsActions from "../../ducks/options"; test("options_meta", async () => { - expect(reduceOptionsMeta(undefined, {type: "other"})).toEqual(OptionsMetaActions.defaultState); + expect(reduceOptionsMeta(undefined, { type: "other" })).toEqual( + OptionsMetaActions.defaultState, + ); - expect(reduceOptionsMeta(undefined, { - type: OptionsActions.RECEIVE, - data: {id: {value: 'foo'}} - })).toEqual({id: {value: 'foo'}}) + expect( + reduceOptionsMeta(undefined, { + type: OptionsActions.RECEIVE, + data: { id: { value: "foo" } }, + }), + ).toEqual({ id: { value: "foo" } }); - expect(reduceOptionsMeta(undefined, { - type: OptionsActions.UPDATE, - data: {id: {value: 1}} - })).toEqual({...OptionsMetaActions.defaultState, id: {value: 1}}) + expect( + reduceOptionsMeta(undefined, { + type: OptionsActions.UPDATE, + data: { id: { value: 1 } }, + }), + ).toEqual({ ...OptionsMetaActions.defaultState, id: { value: 1 } }); }); diff --git a/web/src/js/__tests__/ducks/processesSpec.tsx b/web/src/js/__tests__/ducks/processesSpec.tsx new file mode 100644 index 0000000000..05c79b76d9 --- /dev/null +++ b/web/src/js/__tests__/ducks/processesSpec.tsx @@ -0,0 +1,65 @@ +import fetchMock, { enableFetchMocks } from "jest-fetch-mock"; +import { TStore } from "./tutils"; +import { fetchProcesses } from "../../ducks/processes"; + +describe("processesSlice", () => { + beforeEach(() => { + enableFetchMocks(); + fetchMock.resetMocks(); + }); + + it("should handle fetchProcesses pending state", async () => { + fetchMock.mockResponseOnce(() => new Promise(() => {})); + + const store = TStore(); + + store.dispatch(fetchProcesses()); + expect(store.getState().processes.isLoading).toBe(true); + }); + + it("should handle fetchProcesses fulfilled state", async () => { + const mockProcesses = [ + { + is_visible: true, + executable: "curl.exe", + is_system: "false", + display_name: "curl", + }, + { + is_visible: true, + executable: "http.exe", + is_system: "false", + display_name: "http", + }, + ]; + + fetchMock.mockResponseOnce(JSON.stringify(mockProcesses)); + + const store = TStore(); + + await store.dispatch(fetchProcesses()); + + expect(store.getState().processes.isLoading).toBe(false); + expect(store.getState().processes.currentProcesses).toEqual( + mockProcesses, + ); + expect(fetchMock).toHaveBeenCalledWith("./processes", { + credentials: "same-origin", + }); + }); + + it("should handle fetchProcesses rejected state", async () => { + fetchMock.mockReject(new Error("Failed to fetch processes")); + const store = TStore(); + + await store.dispatch(fetchProcesses()); + + expect(store.getState().processes.isLoading).toBe(false); + expect(store.getState().processes.error).toBe( + "Failed to fetch processes", + ); + expect(fetchMock).toHaveBeenCalledWith("./processes", { + credentials: "same-origin", + }); + }); +}); diff --git a/web/src/js/__tests__/ducks/tutils.ts b/web/src/js/__tests__/ducks/tutils.ts index b555246146..8bbd1d3305 100644 --- a/web/src/js/__tests__/ducks/tutils.ts +++ b/web/src/js/__tests__/ducks/tutils.ts @@ -1,23 +1,25 @@ -import thunk from 'redux-thunk' -import configureStore, {MockStoreCreator, MockStoreEnhanced} from 'redux-mock-store' -import {ConnectionState} from '../../ducks/connection' -import {TDNSFlow, THTTPFlow, TTCPFlow, TUDPFlow} from './_tflow' -import {AppDispatch, RootState} from "../../ducks"; -import {DNSFlow, HTTPFlow, TCPFlow, UDPFlow} from "../../flow"; -import {defaultState as defaultOptions} from "../../ducks/options" -import {TBackendState} from "./_tbackendstate" +import { ConnectionState } from "../../ducks/connection"; +import { TDNSFlow, THTTPFlow, TTCPFlow, TUDPFlow } from "./_tflow"; +import { RootState } from "../../ducks"; +import { reducer } from "../../ducks/store"; +import { DNSFlow, HTTPFlow, TCPFlow, UDPFlow } from "../../flow"; +import { defaultState as defaultOptions } from "../../ducks/options"; +import { TBackendState } from "./_tbackendstate"; +import { configureStore } from "@reduxjs/toolkit"; +import { Tab } from "../../ducks/ui/tabs"; +import { LogLevel } from "../../ducks/eventLog"; +import { ReverseProxyProtocols } from "../../backends/consts"; +import { defaultReverseState } from "../../modes/reverse"; -const mockStoreCreator: MockStoreCreator = configureStore([thunk]) - -export {THTTPFlow as TFlow, TTCPFlow, TUDPFlow} +export { THTTPFlow as TFlow, TTCPFlow, TUDPFlow }; const tflow0: HTTPFlow = THTTPFlow(); const tflow1: HTTPFlow = THTTPFlow(); const tflow2: TCPFlow = TTCPFlow(); const tflow3: DNSFlow = TDNSFlow(); const tflow4: UDPFlow = TUDPFlow(); -tflow0.modified = true -tflow0.intercepted = true +tflow0.modified = true; +tflow0.intercepted = true; tflow1.id = "flow2"; tflow1.request.path = "/second"; @@ -25,51 +27,54 @@ export const testState: RootState = { backendState: TBackendState(), options_meta: { anticache: { - "type": "bool", - "default": false, - "value": false, - "help": "Strip out request headers that might cause the server to return 304-not-modified.", - "choices": undefined + type: "bool", + default: false, + value: false, + help: "Strip out request headers that might cause the server to return 304-not-modified.", + choices: undefined, }, body_size_limit: { - "type": "optional str", - "default": undefined, - "value": undefined, - "help": "Byte size limit of HTTP request and response bodies. Understands k/m/g suffixes, i.e. 3m for 3 megabytes.", - "choices": undefined, + type: "optional str", + default: undefined, + value: undefined, + help: "Byte size limit of HTTP request and response bodies. Understands k/m/g suffixes, i.e. 3m for 3 megabytes.", + choices: undefined, }, connection_strategy: { - "type": "str", - "default": "eager", - "value": "eager", - "help": "Determine when server connections should be established. When set to lazy, mitmproxy tries to defer establishing an upstream connection as long as possible. This makes it possible to use server replay while being offline. When set to eager, mitmproxy can detect protocols with server-side greetings, as well as accurately mirror TLS ALPN negotiation.", - "choices": [ - "eager", - "lazy" - ] + type: "str", + default: "eager", + value: "eager", + help: "Determine when server connections should be established. When set to lazy, mitmproxy tries to defer establishing an upstream connection as long as possible. This makes it possible to use server replay while being offline. When set to eager, mitmproxy can detect protocols with server-side greetings, as well as accurately mirror TLS ALPN negotiation.", + choices: ["eager", "lazy"], }, listen_port: { - "type": "int", - "default": 8080, - "value": 8080, - "help": "Proxy service port.", - "choices": undefined - } + type: "int", + default: 8080, + value: 8080, + help: "Proxy service port.", + choices: undefined, + }, }, ui: { flow: { contentViewFor: {}, - tab: 'request' + tab: "request", }, modal: { - activeModal: undefined + activeModal: undefined, }, optionsEditor: { - booleanOption: {isUpdating: true, error: false}, - strOption: {error: true}, - intOption: {}, - choiceOption: {}, - } + anticache: { isUpdating: true, error: false, value: true }, + cert_passphrase: { + isUpdating: false, + error: "incorrect password", + value: "correcthorsebatterystaple", + }, + }, + tabs: { + current: Tab.Capture, + isInitial: true, + }, }, options: defaultOptions, flows: { @@ -81,11 +86,11 @@ export const testState: RootState = { [tflow3.id]: tflow3, [tflow4.id]: tflow4, }, - filter: '~u /second | ~tcp | ~dns | ~udp', - highlight: '~u /path', + filter: "~u /second | ~tcp | ~dns | ~udp", + highlight: "~u /path", sort: { desc: true, - column: "path" + column: "path", }, view: [tflow1, tflow2, tflow3, tflow4], list: [tflow0, tflow1, tflow2, tflow3, tflow4], @@ -104,7 +109,7 @@ export const testState: RootState = { }, }, connection: { - state: ConnectionState.ESTABLISHED + state: ConnectionState.ESTABLISHED, }, eventLog: { visible: true, @@ -113,23 +118,93 @@ export const testState: RootState = { info: true, web: false, warn: true, - error: true + error: true, }, view: [ - {id: "1", level: 'info', message: 'foo'}, - {id: "2", level: 'error', message: 'bar'} + { id: "1", level: LogLevel.info, message: "foo" }, + { id: "2", level: LogLevel.error, message: "bar" }, ], byId: {}, // TODO: incomplete - list: [], // TODO: incomplete - listIndex: {}, // TODO: incomplete - viewIndex: {}, // TODO: incomplete + list: [], // TODO: incomplete + listIndex: {}, // TODO: incomplete + viewIndex: {}, // TODO: incomplete }, commandBar: { visible: false, - } -} - + }, + modes: { + regular: [ + { + active: true, + }, + ], + local: [ + { + active: false, + selectedProcesses: "", + }, + ], + wireguard: [ + { + active: false, + }, + ], + reverse: [ + { + active: false, + protocol: ReverseProxyProtocols.HTTPS, + destination: "example.com", + }, + defaultReverseState(), + ], + transparent: [ + { + active: false, + }, + ], + socks: [ + { + active: false, + }, + ], + upstream: [ + { + active: false, + destination: "example.com", + }, + ], + dns: [ + { + active: false, + }, + ], + }, + processes: { + currentProcesses: [ + { + is_visible: true, + executable: "curl.exe", + is_system: false, + display_name: "curl", + }, + { + is_visible: true, + executable: "http.exe", + is_system: false, + display_name: "http", + }, + ], + isLoading: false, + }, +}; -export function TStore(): MockStoreEnhanced { - return mockStoreCreator(testState) -} +export const TStore = () => + configureStore({ + reducer, + preloadedState: testState, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + immutableCheck: { warnAfter: 500_000 }, + serializableCheck: { warnAfter: 500_000 }, + }), + }); diff --git a/web/src/js/__tests__/ducks/ui/flowSpec.tsx b/web/src/js/__tests__/ducks/ui/flowSpec.tsx index 7b1bbdeab6..e932177918 100644 --- a/web/src/js/__tests__/ducks/ui/flowSpec.tsx +++ b/web/src/js/__tests__/ducks/ui/flowSpec.tsx @@ -1,20 +1,22 @@ -import reduceFlow, * as FlowActions from '../../../ducks/ui/flow' +import reduceFlow, * as FlowActions from "../../../ducks/ui/flow"; +describe("option reducer", () => { + it("should return initial state", () => { + expect(reduceFlow(undefined, { type: "other" })).toEqual( + FlowActions.defaultState, + ); + }); -describe('option reducer', () => { - it('should return initial state', () => { - expect(reduceFlow(undefined, {type: "other"})).toEqual(FlowActions.defaultState) - }) - - it('should handle set tab', () => { + it("should handle set tab", () => { expect( - reduceFlow(undefined, FlowActions.selectTab("response")).tab - ).toEqual("response") - }) + reduceFlow(undefined, FlowActions.selectTab("response")).tab, + ).toEqual("response"); + }); - it('should handle set content view', () => { + it("should handle set content view", () => { expect( - reduceFlow(undefined, FlowActions.setContentViewFor("foo", "Raw")).contentViewFor["foo"] - ).toEqual("Raw") - }) -}) + reduceFlow(undefined, FlowActions.setContentViewFor("foo", "Raw")) + .contentViewFor["foo"], + ).toEqual("Raw"); + }); +}); diff --git a/web/src/js/__tests__/ducks/ui/indexSpec.tsx b/web/src/js/__tests__/ducks/ui/indexSpec.tsx index 515d1b3196..e04e741552 100644 --- a/web/src/js/__tests__/ducks/ui/indexSpec.tsx +++ b/web/src/js/__tests__/ducks/ui/indexSpec.tsx @@ -1,8 +1,10 @@ -import reduceUI from '../../../ducks/ui/index' +import reduceUI from "../../../ducks/ui/index"; -describe('reduceUI in js/ducks/ui/index.js', () => { - it('should combine flow and header', () => { - let state = reduceUI(undefined, {type: "other"}) - expect(state.hasOwnProperty('flow')).toBeTruthy() - }) -}) +describe("reduceUI in js/ducks/ui/index.js", () => { + it("should combine flow and header", () => { + const state = reduceUI(undefined, { type: "other" }); + expect( + Object.prototype.hasOwnProperty.call(state, "flow"), + ).toBeTruthy(); + }); +}); diff --git a/web/src/js/__tests__/ducks/ui/keyboardSpec.tsx b/web/src/js/__tests__/ducks/ui/keyboardSpec.tsx index 514cfb98cf..b7070a8684 100644 --- a/web/src/js/__tests__/ducks/ui/keyboardSpec.tsx +++ b/web/src/js/__tests__/ducks/ui/keyboardSpec.tsx @@ -1,171 +1,198 @@ -import reduceFlows, * as flowsActions from "../../../ducks/flows"; -import {onKeyDown} from '../../../ducks/ui/keyboard' -import * as UIActions from '../../../ducks/ui/flow' -import * as modalActions from '../../../ducks/ui/modal' -import {fetchApi, runCommand} from '../../../utils' -import {TStore} from "../tutils"; - -jest.mock('../../../utils') - -describe('onKeyDown', () => { - let flows = flowsActions.defaultState; - for (let i = 1; i <= 12; i++) { - flows = reduceFlows(flows, { - type: flowsActions.ADD, - data: {id: i + "", request: true, response: true, type: "http"}, - cmd: 'add' - }) - } - - const store = TStore(); - store.getState().flows = flows; - - let createKeyEvent = (key, ctrlKey = false) => { - // @ts-ignore - return onKeyDown({key, ctrlKey, preventDefault: jest.fn()}) - } +import * as flowsActions from "../../../ducks/flows"; +import { onKeyDown } from "../../../ducks/ui/keyboard"; +import * as modalActions from "../../../ducks/ui/modal"; +import { fetchApi, runCommand } from "../../../utils"; +import { TStore } from "../tutils"; + +jest.mock("../../../utils"); + +describe("onKeyDown", () => { + const makeStore = () => { + const store = TStore(); + store.dispatch({ + type: flowsActions.RECEIVE, + cmd: "receive", + data: [], + }); + store.dispatch(flowsActions.setFilter("")); + store.dispatch(flowsActions.select("1")); + for (let i = 1; i <= 12; i++) { + store.dispatch({ + type: flowsActions.ADD, + cmd: "add", + data: { + id: i + "", + request: true, + response: true, + type: "http", + intercepted: true, + modified: true, + }, + }); + } + return store; + }; + + const createKeyEvent = (key, ctrlKey = false) => { + // @ts-expect-error not a real KeyboardEvent + return onKeyDown({ key, ctrlKey, preventDefault: jest.fn() }); + }; afterEach(() => { - store.clearActions() - // @ts-ignore - fetchApi.mockClear() - }); - - it('should handle cursor up', () => { - store.getState().flows = reduceFlows(flows, flowsActions.select("2")) - store.dispatch(createKeyEvent("k")) - expect(store.getActions()).toEqual([{flowIds: ["1"], type: flowsActions.SELECT}]) - - store.clearActions() - store.dispatch(createKeyEvent("ArrowUp")) - expect(store.getActions()).toEqual([{flowIds: ["1"], type: flowsActions.SELECT}]) - }) - - it('should handle cursor down', () => { - store.dispatch(createKeyEvent("j")) - expect(store.getActions()).toEqual([{flowIds: ["3"], type: flowsActions.SELECT}]) - - store.clearActions() - store.dispatch(createKeyEvent("ArrowDown")) - expect(store.getActions()).toEqual([{flowIds: ["3"], type: flowsActions.SELECT}]) - }) - - it('should handle page down', () => { - store.dispatch(createKeyEvent(" ")) - expect(store.getActions()).toEqual([{flowIds: ["12"], type: flowsActions.SELECT}]) - - store.getState().flows = reduceFlows(flows, flowsActions.select("1")) - store.clearActions() - store.dispatch(createKeyEvent("PageDown")) - expect(store.getActions()).toEqual([{flowIds: ["11"], type: flowsActions.SELECT}]) - }) - - it('should handle page up', () => { - store.getState().flows = reduceFlows(flows, flowsActions.select("11")) - store.dispatch(createKeyEvent("PageUp")) - expect(store.getActions()).toEqual([{flowIds: ["1"], type: flowsActions.SELECT}]) - }) - - it('should handle select first', () => { - store.dispatch(createKeyEvent("Home")) - expect(store.getActions()).toEqual([{flowIds: ["1"], type: flowsActions.SELECT}]) - }) - - it('should handle select last', () => { - store.getState().flows = reduceFlows(flows, flowsActions.select("1")) - store.dispatch(createKeyEvent("End")) - expect(store.getActions()).toEqual([{flowIds: ["12"], type: flowsActions.SELECT}]) - }) - - it('should handle deselect', () => { - store.dispatch(createKeyEvent("Escape")) - expect(store.getActions()).toEqual([{flowIds: [], type: flowsActions.SELECT}]) - }) - - it('should handle switch to left tab', () => { - store.dispatch(createKeyEvent("ArrowLeft")) - expect(store.getActions()).toEqual([{tab: 'timing', type: UIActions.SET_TAB}]) - }) - - it('should handle switch to right tab', () => { - store.dispatch(createKeyEvent("Tab")) - expect(store.getActions()).toEqual([{tab: 'response', type: UIActions.SET_TAB}]) - - store.clearActions() - store.dispatch(createKeyEvent("ArrowRight")) - expect(store.getActions()).toEqual([{tab: 'response', type: UIActions.SET_TAB}]) - }) - - it('should handle delete action', () => { - store.dispatch(createKeyEvent("d")) - expect(fetchApi).toBeCalledWith('/flows/1', {method: 'DELETE'}) - - }) - - it('should handle create action', () => { - store.dispatch(createKeyEvent("n")) - expect(runCommand).toBeCalledWith('view.flows.create', "get", "https://example.com/") - }) - - it('should handle duplicate action', () => { - store.dispatch(createKeyEvent("D")) - expect(fetchApi).toBeCalledWith('/flows/1/duplicate', {method: 'POST'}) - }) - - it('should handle resume action', () => { + // @ts-expect-error mocking + fetchApi.mockClear(); + }); + + it("should handle cursor up/down", () => { + const store = makeStore(); + // down + store.dispatch(createKeyEvent("j")); + expect(store.getState().flows.selected).toEqual(["2"]); + store.dispatch(createKeyEvent("ArrowDown")); + expect(store.getState().flows.selected).toEqual(["3"]); + + // up + store.dispatch(createKeyEvent("k")); + expect(store.getState().flows.selected).toEqual(["2"]); + store.dispatch(createKeyEvent("ArrowUp")); + expect(store.getState().flows.selected).toEqual(["1"]); + store.dispatch(createKeyEvent("ArrowUp")); + expect(store.getState().flows.selected).toEqual(["1"]); + }); + + it("should handle scrolling", () => { + const store = makeStore(); + store.dispatch(createKeyEvent("PageDown")); + expect(store.getState().flows.selected).toEqual(["11"]); + store.dispatch(createKeyEvent("End")); + expect(store.getState().flows.selected).toEqual(["12"]); + store.dispatch(createKeyEvent("PageUp")); + expect(store.getState().flows.selected).toEqual(["2"]); + store.dispatch(createKeyEvent("Home")); + expect(store.getState().flows.selected).toEqual(["1"]); + }); + + it("should handle deselect", () => { + const store = makeStore(); + store.dispatch(createKeyEvent("Escape")); + expect(store.getState().flows.selected).toEqual([]); + }); + + it("should handle switch to left tab", () => { + const store = makeStore(); + expect(store.getState().ui.flow.tab).toBe("request"); + store.dispatch(createKeyEvent("ArrowLeft")); + expect(store.getState().ui.flow.tab).toBe("comment"); + }); + + it("should handle switch to right tab", () => { + const store = makeStore(); + expect(store.getState().ui.flow.tab).toBe("request"); + store.dispatch(createKeyEvent("Tab")); + expect(store.getState().ui.flow.tab).toBe("response"); + store.dispatch(createKeyEvent("ArrowRight")); + expect(store.getState().ui.flow.tab).toBe("connection"); + }); + + it("should handle delete action", () => { + const store = makeStore(); + store.dispatch(createKeyEvent("d")); + expect(fetchApi).toHaveBeenCalledWith("/flows/1", { method: "DELETE" }); + }); + + it("should handle create action", () => { + const store = makeStore(); + store.dispatch(createKeyEvent("n")); + expect(runCommand).toHaveBeenCalledWith( + "view.flows.create", + "get", + "https://example.com/", + ); + }); + + it("should handle duplicate action", () => { + const store = makeStore(); + store.dispatch(createKeyEvent("D")); + expect(fetchApi).toHaveBeenCalledWith("/flows/1/duplicate", { + method: "POST", + }); + }); + + it("should handle resume action", () => { + const store = makeStore(); // resume all - store.dispatch(createKeyEvent("A")) - expect(fetchApi).toBeCalledWith('/flows/resume', {method: 'POST'}) + store.dispatch(createKeyEvent("A")); + expect(fetchApi).toHaveBeenCalledWith("/flows/resume", { + method: "POST", + }); // resume - store.getState().flows.byId[store.getState().flows.selected[0]].intercepted = true - store.dispatch(createKeyEvent("a")) - expect(fetchApi).toBeCalledWith('/flows/1/resume', {method: 'POST'}) - }) - - it('should handle replay action', () => { - store.dispatch(createKeyEvent("r")) - expect(fetchApi).toBeCalledWith('/flows/1/replay', {method: 'POST'}) - }) - - it('should handle revert action', () => { - store.getState().flows.byId[store.getState().flows.selected[0]].modified = true - store.dispatch(createKeyEvent("v")) - expect(fetchApi).toBeCalledWith('/flows/1/revert', {method: 'POST'}) - }) - - it('should handle kill action', () => { + store.getState().flows.byId[ + store.getState().flows.selected[0] + ].intercepted = true; + store.dispatch(createKeyEvent("a")); + expect(fetchApi).toHaveBeenCalledWith("/flows/1/resume", { + method: "POST", + }); + }); + + it("should handle replay action", () => { + const store = makeStore(); + store.dispatch(createKeyEvent("r")); + expect(fetchApi).toHaveBeenCalledWith("/flows/1/replay", { + method: "POST", + }); + }); + + it("should handle revert action", () => { + const store = makeStore(); + store.dispatch(createKeyEvent("v")); + expect(fetchApi).toHaveBeenCalledWith("/flows/1/revert", { + method: "POST", + }); + }); + + it("should handle kill action", () => { + const store = makeStore(); // kill all - store.dispatch(createKeyEvent("X")) - expect(fetchApi).toBeCalledWith('/flows/kill', {method: 'POST'}) + store.dispatch(createKeyEvent("X")); + expect(fetchApi).toHaveBeenCalledWith("/flows/kill", { + method: "POST", + }); // kill - store.dispatch(createKeyEvent("x")) - expect(fetchApi).toBeCalledWith('/flows/1/kill', {method: 'POST'}) - }) - - it('should handle clear action', () => { - store.dispatch(createKeyEvent("z")) - expect(fetchApi).toBeCalledWith('/clear', {method: 'POST'}) - }) - - it('should stop on some action with no flow is selected', () => { - store.getState().flows = reduceFlows(undefined, {}) - store.dispatch(createKeyEvent("ArrowLeft")) - store.dispatch(createKeyEvent("Tab")) - store.dispatch(createKeyEvent("ArrowRight")) - store.dispatch(createKeyEvent("D")) - expect(fetchApi).not.toBeCalled() - }) - - it('should do nothing when Ctrl and undefined key is pressed ', () => { - store.dispatch(createKeyEvent("Backspace", true)) - store.dispatch(createKeyEvent(0)) - expect(fetchApi).not.toBeCalled() - }) - - it('should close modal', () => { - store.getState().ui.modal.activeModal = true - store.dispatch(createKeyEvent("Escape")) - expect(store.getActions()).toEqual([{type: modalActions.HIDE_MODAL}]) - }) - -}) + store.dispatch(createKeyEvent("x")); + expect(fetchApi).toHaveBeenCalledWith("/flows/1/kill", { + method: "POST", + }); + }); + + it("should handle clear action", () => { + const store = makeStore(); + store.dispatch(createKeyEvent("z")); + expect(fetchApi).toHaveBeenCalledWith("/clear", { method: "POST" }); + }); + + it("should stop on some action with no flow is selected", () => { + const store = makeStore(); + store.dispatch(flowsActions.select(undefined)); + store.dispatch(createKeyEvent("ArrowLeft")); + store.dispatch(createKeyEvent("Tab")); + store.dispatch(createKeyEvent("ArrowRight")); + store.dispatch(createKeyEvent("D")); + expect(fetchApi).not.toHaveBeenCalled(); + }); + + it("should do nothing when Ctrl and undefined key is pressed ", () => { + const store = makeStore(); + store.dispatch(createKeyEvent("Backspace", true)); + store.dispatch(createKeyEvent(0)); + expect(fetchApi).not.toHaveBeenCalled(); + }); + + it("should close modal", () => { + const store = makeStore(); + store.dispatch(modalActions.setActiveModal("OptionModal")); + expect(store.getState().ui.modal.activeModal).toEqual("OptionModal"); + store.dispatch(createKeyEvent("Escape")); + expect(store.getState().ui.modal.activeModal).toEqual(undefined); + }); +}); diff --git a/web/src/js/__tests__/ducks/ui/modalSpec.tsx b/web/src/js/__tests__/ducks/ui/modalSpec.tsx index b6ef5e6924..1a31a33670 100644 --- a/web/src/js/__tests__/ducks/ui/modalSpec.tsx +++ b/web/src/js/__tests__/ducks/ui/modalSpec.tsx @@ -1,24 +1,20 @@ -import reduceModal, * as ModalActions from '../../../ducks/ui/modal' +import reduceModal, * as ModalActions from "../../../ducks/ui/modal"; -describe('modal reducer', () => { +describe("modal reducer", () => { + it("should return the initial state", () => { + expect(reduceModal(undefined, {})).toEqual({ activeModal: undefined }); + }); - it('should return the initial state', () => { - expect(reduceModal(undefined, {})).toEqual( - { activeModal: undefined } - ) - }) + it("should handle setActiveModal action", () => { + const state = reduceModal( + undefined, + ModalActions.setActiveModal("foo"), + ); + expect(state).toEqual({ activeModal: "foo" }); + }); - it('should handle setActiveModal action', () => { - let state = reduceModal(undefined, ModalActions.setActiveModal('foo')) - expect(state).toEqual( - { activeModal: 'foo' } - ) - }) - - it('should handle hideModal action', () => { - let state = reduceModal(undefined, ModalActions.hideModal()) - expect(state).toEqual( - { activeModal: undefined } - ) - }) -}) + it("should handle hideModal action", () => { + const state = reduceModal(undefined, ModalActions.hideModal()); + expect(state).toEqual({ activeModal: undefined }); + }); +}); diff --git a/web/src/js/__tests__/ducks/ui/optionEditorSpec.tsx b/web/src/js/__tests__/ducks/ui/optionEditorSpec.tsx index 4d9f46d168..45047f0e9e 100644 --- a/web/src/js/__tests__/ducks/ui/optionEditorSpec.tsx +++ b/web/src/js/__tests__/ducks/ui/optionEditorSpec.tsx @@ -1,33 +1,59 @@ -import reduceOptionsEditor, * as optionsEditorActions from '../../../ducks/ui/optionsEditor' -import { HIDE_MODAL } from '../../../ducks/ui/modal' -import {OptionsState} from "../../../ducks/_options_gen"; +import reduceOptionsEditor, * as optionsEditorActions from "../../../ducks/ui/optionsEditor"; +import { HIDE_MODAL } from "../../../ducks/ui/modal"; -describe('optionsEditor reducer', () => { +describe("optionsEditor reducer", () => { + it("should return initial state", () => { + expect(reduceOptionsEditor(undefined, {})).toEqual({}); + }); - it('should return initial state', () => { - expect(reduceOptionsEditor(undefined, {})).toEqual({}) - }) + it("should handle option update start", () => { + const state = reduceOptionsEditor( + undefined, + optionsEditorActions.startUpdate("foo", "bar"), + ); + expect(state).toEqual({ + foo: { error: false, isUpdating: true, value: "bar" }, + }); + }); - it('should handle option update start', () => { - let state = reduceOptionsEditor(undefined, optionsEditorActions.startUpdate('foo', 'bar')) - expect(state).toEqual({ foo: {error: false, isUpdating: true, value: 'bar'}}) - }) + it("should handle option update success", () => { + expect( + reduceOptionsEditor( + undefined, + optionsEditorActions.updateSuccess("foo"), + ), + ).toEqual({ foo: undefined }); + }); - it('should handle option update success', () => { - expect(reduceOptionsEditor(undefined, optionsEditorActions.updateSuccess('foo'))).toEqual({foo: undefined}) - }) - - it('should handle option update error', () => { - let state = reduceOptionsEditor(undefined, optionsEditorActions.startUpdate('foo', 'bar')) - state = reduceOptionsEditor(state, optionsEditorActions.updateError('foo', 'errorMsg')) - expect(state).toEqual({ foo: {error: 'errorMsg', isUpdating: false, value: 'bar'}}) + it("should handle option update error", () => { + let state = reduceOptionsEditor( + undefined, + optionsEditorActions.startUpdate("foo", "bar"), + ); + state = reduceOptionsEditor( + state, + optionsEditorActions.updateError("foo", "errorMsg"), + ); + expect(state).toEqual({ + foo: { error: "errorMsg", isUpdating: false, value: "bar" }, + }); // boolean type - state = reduceOptionsEditor(undefined, optionsEditorActions.startUpdate('foo', true)) - state = reduceOptionsEditor(state, optionsEditorActions.updateError('foo', 'errorMsg')) - expect(state).toEqual({ foo: {error: 'errorMsg', isUpdating: false, value: false}}) - }) + state = reduceOptionsEditor( + undefined, + optionsEditorActions.startUpdate("foo", true), + ); + state = reduceOptionsEditor( + state, + optionsEditorActions.updateError("foo", "errorMsg"), + ); + expect(state).toEqual({ + foo: { error: "errorMsg", isUpdating: false, value: false }, + }); + }); - it('should handle hide modal', () => { - expect(reduceOptionsEditor(undefined, {type: HIDE_MODAL})).toEqual({}) - }) -}) + it("should handle hide modal", () => { + expect(reduceOptionsEditor(undefined, { type: HIDE_MODAL })).toEqual( + {}, + ); + }); +}); diff --git a/web/src/js/__tests__/ducks/utils/storeSpec.tsx b/web/src/js/__tests__/ducks/utils/storeSpec.tsx index e156315958..48cd9b692b 100644 --- a/web/src/js/__tests__/ducks/utils/storeSpec.tsx +++ b/web/src/js/__tests__/ducks/utils/storeSpec.tsx @@ -1,188 +1,214 @@ -import * as storeActions from '../../../ducks/utils/store' -import {Item, reduce} from '../../../ducks/utils/store' +import * as storeActions from "../../../ducks/utils/store"; +import { Item, reduce } from "../../../ducks/utils/store"; -describe('store reducer', () => { - it('should return initial state', () => { +describe("store reducer", () => { + it("should return initial state", () => { expect(reduce(undefined, {})).toEqual({ byId: {}, list: [], listIndex: {}, view: [], viewIndex: {}, - }) - }) - - it('should handle add action', () => { - let a = {id: "1"}, - b = {id: "9"}, - state = reduce(undefined, {}) - expect(state = reduce(state, storeActions.add(a))).toEqual({ - byId: {"1": a}, - listIndex: {"1": 0}, + }); + }); + + it("should handle add action", () => { + const a = { id: "1" }; + const b = { id: "9" }; + let state = reduce(undefined, {}); + expect((state = reduce(state, storeActions.add(a)))).toEqual({ + byId: { "1": a }, + listIndex: { "1": 0 }, list: [a], view: [a], - viewIndex: {"1": 0}, - }) + viewIndex: { "1": 0 }, + }); - expect(state = reduce(state, storeActions.add(b))).toEqual({ - byId: {"1": a, 9: b}, - listIndex: {"1": 0, "9": 1}, + expect((state = reduce(state, storeActions.add(b)))).toEqual({ + byId: { "1": a, 9: b }, + listIndex: { "1": 0, "9": 1 }, list: [a, b], view: [a, b], - viewIndex: {"1": 0, "9": 1}, - }) + viewIndex: { "1": 0, "9": 1 }, + }); // add item and sort them - let c = {id: "0"} - expect(reduce(state, storeActions.add(c, undefined, - (a, b) => { - return a.id > b.id ? 1 : -1 - }))).toEqual({ - byId: {...state.byId, "0": c}, + const c = { id: "0" }; + expect( + reduce( + state, + storeActions.add(c, undefined, (a, b) => { + return a.id > b.id ? 1 : -1; + }), + ), + ).toEqual({ + byId: { ...state.byId, "0": c }, list: [...state.list, c], - listIndex: {...state.listIndex, "0": 2}, + listIndex: { ...state.listIndex, "0": 2 }, view: [c, ...state.view], - viewIndex: {"0": 0, "1": 1, "9": 2} - - }) - }) + viewIndex: { "0": 0, "1": 1, "9": 2 }, + }); + }); - it('should not add the item with duplicated id', () => { - let a = {id: "1"}, - state = reduce(undefined, storeActions.add(a)) - expect(reduce(state, storeActions.add(a))).toEqual(state) - }) + it("should not add the item with duplicated id", () => { + const a = { id: "1" }; + const state = reduce(undefined, storeActions.add(a)); + expect(reduce(state, storeActions.add(a))).toEqual(state); + }); - it('should handle update action', () => { + it("should handle update action", () => { interface TItem extends Item { - foo: string + foo: string; } - let a: TItem = {id: "1", foo: "foo"}, - updated = {...a, foo: "bar"}, - state = reduce(undefined, storeActions.add(a)) + const a: TItem = { id: "1", foo: "foo" }; + const updated = { ...a, foo: "bar" }; + const state = reduce(undefined, storeActions.add(a)); expect(reduce(state, storeActions.update(updated))).toEqual({ - byId: {1: updated}, + byId: { 1: updated }, list: [updated], - listIndex: {1: 0}, + listIndex: { 1: 0 }, view: [updated], - viewIndex: {1: 0}, - }) - }) - - it('should handle update action with filter', () => { - let a = {id: "0"}, b = {id: "1"}, - state = reduce(undefined, storeActions.receive([a, b])) - state = reduce(state, storeActions.update(b, - item => { - return item.id !== "1" - })) + viewIndex: { 1: 0 }, + }); + }); + + it("should handle update action with filter", () => { + const a = { id: "0" }; + const b = { id: "1" }; + let state = reduce(undefined, storeActions.receive([a, b])); + state = reduce( + state, + storeActions.update(b, (item) => { + return item.id !== "1"; + }), + ); expect(state).toEqual({ - byId: {"0": a, "1": b}, + byId: { "0": a, "1": b }, list: [a, b], - listIndex: {"0": 0, "1": 1}, + listIndex: { "0": 0, "1": 1 }, view: [a], - viewIndex: {"0": 0} - }) - expect(reduce(state, storeActions.update(b, - item => { - return item.id !== "0" - }))).toEqual({ - byId: {"0": a, "1": b}, + viewIndex: { "0": 0 }, + }); + expect( + reduce( + state, + storeActions.update(b, (item) => { + return item.id !== "0"; + }), + ), + ).toEqual({ + byId: { "0": a, "1": b }, list: [a, b], - listIndex: {"0": 0, "1": 1}, + listIndex: { "0": 0, "1": 1 }, view: [a, b], - viewIndex: {"0": 0, "1": 1} - }) - }) - - it('should handle update action with sort', () => { - let a = {id: "2"}, - b = {id: "3"}, - state = reduce(undefined, storeActions.receive([a, b])) - expect(reduce(state, storeActions.update(b, undefined, - (a, b) => { - return b.id > a.id ? 1 : -1 - }))).toEqual({ + viewIndex: { "0": 0, "1": 1 }, + }); + }); + + it("should handle update action with sort", () => { + const a = { id: "2" }; + const b = { id: "3" }; + const state = reduce(undefined, storeActions.receive([a, b])); + expect( + reduce( + state, + storeActions.update(b, undefined, (a, b) => { + return b.id > a.id ? 1 : -1; + }), + ), + ).toEqual({ // sort by id in descending order - byId: {"2": a, "3": b}, + byId: { "2": a, "3": b }, list: [a, b], - listIndex: {"2": 0, "3": 1}, + listIndex: { "2": 0, "3": 1 }, view: [b, a], - viewIndex: {"2": 1, "3": 0}, - }) - - let state1 = reduce(undefined, storeActions.receive([b, a])) - expect(reduce(state1, storeActions.update(b, undefined, - (a, b) => { - return a.id > b.id ? 1 : -1 - }))).toEqual({ + viewIndex: { "2": 1, "3": 0 }, + }); + + const state1 = reduce(undefined, storeActions.receive([b, a])); + expect( + reduce( + state1, + storeActions.update(b, undefined, (a, b) => { + return a.id > b.id ? 1 : -1; + }), + ), + ).toEqual({ // sort by id in ascending order - byId: {"2": a, "3": b}, + byId: { "2": a, "3": b }, list: [b, a], - listIndex: {"2": 1, "3": 0}, + listIndex: { "2": 1, "3": 0 }, view: [a, b], - viewIndex: {"2": 0, "3": 1}, - }) - }) - - it('should set filter', () => { - let a = {id: "1"}, - b = {id: "2"}, - state = reduce(undefined, storeActions.receive([a, b])) - expect(reduce(state, storeActions.setFilter( - item => { - return item.id !== "1" - } - ))).toEqual({ - byId: {"1": a, "2": b}, + viewIndex: { "2": 0, "3": 1 }, + }); + }); + + it("should set filter", () => { + const a = { id: "1" }; + const b = { id: "2" }; + const state = reduce(undefined, storeActions.receive([a, b])); + expect( + reduce( + state, + storeActions.setFilter((item) => { + return item.id !== "1"; + }), + ), + ).toEqual({ + byId: { "1": a, "2": b }, list: [a, b], - listIndex: {"1": 0, "2": 1}, + listIndex: { "1": 0, "2": 1 }, view: [b], - viewIndex: {"2": 0}, - }) - }) - - it('should set sort', () => { - let a = {id: "1"}, - b = {id: "2"}, - state = reduce(undefined, storeActions.receive([a, b])) - expect(reduce(state, storeActions.setSort( - (a, b) => { - return b.id > a.id ? 1 : -1 - } - ))).toEqual({ - byId: {1: a, 2: b}, + viewIndex: { "2": 0 }, + }); + }); + + it("should set sort", () => { + const a = { id: "1" }; + const b = { id: "2" }; + const state = reduce(undefined, storeActions.receive([a, b])); + expect( + reduce( + state, + storeActions.setSort((a, b) => { + return b.id > a.id ? 1 : -1; + }), + ), + ).toEqual({ + byId: { 1: a, 2: b }, list: [a, b], - listIndex: {1: 0, 2: 1}, + listIndex: { 1: 0, 2: 1 }, view: [b, a], - viewIndex: {1: 1, 2: 0}, - }) - }) - - it('should handle remove action', () => { - let a = {id: "1"}, b = {id: "2"}, - state = reduce(undefined, storeActions.receive([a, b])) + viewIndex: { 1: 1, 2: 0 }, + }); + }); + + it("should handle remove action", () => { + const a = { id: "1" }; + const b = { id: "2" }; + const state = reduce(undefined, storeActions.receive([a, b])); expect(reduce(state, storeActions.remove("1"))).toEqual({ - byId: {"2": b}, + byId: { "2": b }, list: [b], - listIndex: {"2": 0}, + listIndex: { "2": 0 }, view: [b], - viewIndex: {"2": 0}, - }) + viewIndex: { "2": 0 }, + }); - expect(reduce(state, storeActions.remove("3"))).toEqual(state) - }) + expect(reduce(state, storeActions.remove("3"))).toEqual(state); + }); - it('should handle receive list', () => { - let a = {id: "1"}, b = {id: "2"}, - list = [a, b] + it("should handle receive list", () => { + const a = { id: "1" }; + const b = { id: "2" }; + const list = [a, b]; expect(reduce(undefined, storeActions.receive(list))).toEqual({ - byId: {"1": a, "2": b}, + byId: { "1": a, "2": b }, list: [a, b], - listIndex: {"1": 0, "2": 1}, + listIndex: { "1": 0, "2": 1 }, view: [a, b], - viewIndex: {"1": 0, "2": 1}, - }) - }) -}) + viewIndex: { "1": 0, "2": 1 }, + }); + }); +}); diff --git a/web/src/js/__tests__/flow/utilsSpec.tsx b/web/src/js/__tests__/flow/utilsSpec.tsx index e8b5b00052..4c9d08a1a7 100644 --- a/web/src/js/__tests__/flow/utilsSpec.tsx +++ b/web/src/js/__tests__/flow/utilsSpec.tsx @@ -1,95 +1,107 @@ -import * as utils from '../../flow/utils' -import {TFlow, TTCPFlow, TUDPFlow} from "../ducks/tutils"; -import {TDNSFlow, THTTPFlow} from "../ducks/_tflow"; -import {HTTPFlow} from "../../flow"; +import * as utils from "../../flow/utils"; +import { TFlow, TTCPFlow, TUDPFlow } from "../ducks/tutils"; +import { TDNSFlow, THTTPFlow } from "../ducks/_tflow"; +import { HTTPFlow } from "../../flow"; -describe('MessageUtils', () => { - it('should be possible to get first header', () => { - let tflow = TFlow(); - expect(utils.MessageUtils.get_first_header(tflow.request, /header/)).toEqual("qvalue") - expect(utils.MessageUtils.get_first_header(tflow.request, /123/)).toEqual(undefined) - }) +describe("MessageUtils", () => { + it("should be possible to get first header", () => { + const tflow = TFlow(); + expect( + utils.MessageUtils.get_first_header(tflow.request, /header/), + ).toEqual("qvalue"); + expect( + utils.MessageUtils.get_first_header(tflow.request, /123/), + ).toEqual(undefined); + }); - it('should be possible to get Content-Type', () => { - let tflow = TFlow(); + it("should be possible to get Content-Type", () => { + const tflow = TFlow(); tflow.request.headers = [["Content-Type", "text/html"]]; - expect(utils.MessageUtils.getContentType(tflow.request)).toEqual("text/html"); - }) + expect(utils.MessageUtils.getContentType(tflow.request)).toEqual( + "text/html", + ); + }); - it('should be possible to match header', () => { - let h1 = ["foo", "bar"], - msg = {headers : [h1]} - expect(utils.MessageUtils.match_header(msg, /foo/i)).toEqual(h1) - expect(utils.MessageUtils.match_header(msg, /123/i)).toBeFalsy() - }) + it("should be possible to match header", () => { + const h1 = ["foo", "bar"]; + const msg = { headers: [h1] }; + expect(utils.MessageUtils.match_header(msg, /foo/i)).toEqual(h1); + expect(utils.MessageUtils.match_header(msg, /123/i)).toBeFalsy(); + }); - it('should be possible to get content URL', () => { + it("should be possible to get content URL", () => { const flow = TFlow(); // request - let view = "bar"; - expect(utils.MessageUtils.getContentURL(flow, flow.request, view)).toEqual( - "./flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/request/content/bar.json" - ) - expect(utils.MessageUtils.getContentURL(flow, flow.request, '')).toEqual( - "./flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/request/content.data" - ) + const view = "bar"; + expect( + utils.MessageUtils.getContentURL(flow, flow.request, view), + ).toEqual( + "./flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/request/content/bar.json", + ); + expect( + utils.MessageUtils.getContentURL(flow, flow.request, ""), + ).toEqual( + "./flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/request/content.data", + ); // response - expect(utils.MessageUtils.getContentURL(flow, flow.response, view)).toEqual( - "./flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/response/content/bar.json" - ) - }) -}) + expect( + utils.MessageUtils.getContentURL(flow, flow.response, view), + ).toEqual( + "./flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/response/content/bar.json", + ); + }); +}); -describe('RequestUtils', () => { - it('should be possible prettify url', () => { - let flow = TFlow(); +describe("RequestUtils", () => { + it("should be possible prettify url", () => { + const flow = TFlow(); expect(utils.RequestUtils.pretty_url(flow.request)).toEqual( - "http://address:22/path" - ) - }) -}) + "http://address:22/path", + ); + }); +}); -describe('parseUrl', () => { - it('should be possible to parse url', () => { - let url = "http://foo:4444/bar" +describe("parseUrl", () => { + it("should be possible to parse url", () => { + const url = "http://foo:4444/bar"; expect(utils.parseUrl(url)).toEqual({ port: 4444, - scheme: 'http', - host: 'foo', - path: '/bar' - }) + scheme: "http", + host: "foo", + path: "/bar", + }); - expect(utils.parseUrl("foo:foo")).toBeFalsy() - }) -}) + expect(utils.parseUrl("foo:foo")).toBeFalsy(); + }); +}); -describe('isValidHttpVersion', () => { - it('should be possible to validate http version', () => { - expect(utils.isValidHttpVersion("HTTP/1.1")).toBeTruthy() - expect(utils.isValidHttpVersion("HTTP//1")).toBeFalsy() - }) -}) +describe("isValidHttpVersion", () => { + it("should be possible to validate http version", () => { + expect(utils.isValidHttpVersion("HTTP/1.1")).toBeTruthy(); + expect(utils.isValidHttpVersion("HTTP//1")).toBeFalsy(); + }); +}); -it('should be possible to get a start time', () => { +it("should be possible to get a start time", () => { expect(utils.startTime(THTTPFlow())).toEqual(946681200); expect(utils.startTime(TTCPFlow())).toEqual(946681200); expect(utils.startTime(TUDPFlow())).toEqual(946681200); expect(utils.startTime(TDNSFlow())).toEqual(946681200); -}) +}); -it('should be possible to get an end time', () => { - let f: HTTPFlow = THTTPFlow(); +it("should be possible to get an end time", () => { + const f: HTTPFlow = THTTPFlow(); expect(utils.endTime(f)).toEqual(946681205); f.websocket = undefined; expect(utils.endTime(f)).toEqual(946681203); expect(utils.endTime(TTCPFlow())).toEqual(946681205); expect(utils.endTime(TUDPFlow())).toEqual(946681204.5); expect(utils.endTime(TDNSFlow())).toEqual(946681201); -}) +}); -it('should be possible to get a total size', () => { +it("should be possible to get a total size", () => { expect(utils.getTotalSize(THTTPFlow())).toEqual(43); expect(utils.getTotalSize(TTCPFlow())).toEqual(12); expect(utils.getTotalSize(TUDPFlow())).toEqual(12); expect(utils.getTotalSize(TDNSFlow())).toEqual(8); -}) +}); diff --git a/web/src/js/__tests__/modes/dnsSpec.ts b/web/src/js/__tests__/modes/dnsSpec.ts new file mode 100644 index 0000000000..786e7d2d77 --- /dev/null +++ b/web/src/js/__tests__/modes/dnsSpec.ts @@ -0,0 +1,31 @@ +import { ModesState } from "../../ducks/modes"; +import { parseSpec } from "../../modes"; +import { getSpec, parseRaw, DnsState } from "../../modes/dns"; + +describe("getSpec dns mode", () => { + it("should return the correct mode config", () => { + const modes = { + dns: [ + { + active: true, + listen_host: "localhost", + listen_port: 8082, + }, + ], + } as ModesState; + const mode = getSpec(modes.dns[0]); + expect(mode).toBe("dns@localhost:8082"); + }); +}); + +describe("parseRaw dns mode", () => { + it("should parse", () => { + const parsed = parseRaw(parseSpec("dns@localhost:8082")); + expect(parsed).toEqual({ + active: true, + ui_id: parsed.ui_id, + listen_host: "localhost", + listen_port: 8082, + } as DnsState); + }); +}); diff --git a/web/src/js/__tests__/modes/indexSpec.ts b/web/src/js/__tests__/modes/indexSpec.ts new file mode 100644 index 0000000000..31ac205400 --- /dev/null +++ b/web/src/js/__tests__/modes/indexSpec.ts @@ -0,0 +1,57 @@ +import { includeListenAddress, parseSpec } from "../../modes"; + +describe("includeListenAddress", () => { + it("should keep mode as-is if not port is specified", () => { + const mode = {}; + const result = includeListenAddress("regular", mode); + expect(result).toEqual("regular"); + }); + + it("should return array with mode name and listen_port if mode is active with listen_port", () => { + const mode = { + listen_port: 8080, + }; + const result = includeListenAddress("regular", mode); + expect(result).toEqual("regular@8080"); + }); + + it("should return array with mode name, listen_host, and listen_port if mode is active with listen_host and listen_port", () => { + const mode = { + listen_host: "localhost", + listen_port: 8080, + }; + const result = includeListenAddress("regular", mode); + expect(result).toEqual("regular@localhost:8080"); + }); +}); + +describe("parseSpec", () => { + it("should parse regular mode with host and port", () => { + const modeConfig = "regular@localhost:8081"; + const result = parseSpec(modeConfig); + expect(result).toEqual({ + name: "regular", + full_spec: "regular@localhost:8081", + data: "", + listen_host: "localhost", + listen_port: 8081, + }); + }); + + it("should parse local mode with data", () => { + const modeConfig = "local:curl,http"; + const result = parseSpec(modeConfig); + expect(result).toEqual({ + name: "local", + data: "curl,http", + full_spec: "local:curl,http", + listen_host: undefined, + listen_port: undefined, + }); + }); + + it("should throw an error for invalid port", () => { + const modeConfig = "regular@99999"; + expect(() => parseSpec(modeConfig)).toThrow("invalid port: 99999"); + }); +}); diff --git a/web/src/js/__tests__/modes/localSpec.ts b/web/src/js/__tests__/modes/localSpec.ts new file mode 100644 index 0000000000..77b5440a24 --- /dev/null +++ b/web/src/js/__tests__/modes/localSpec.ts @@ -0,0 +1,29 @@ +import { ModesState } from "../../ducks/modes"; +import { parseSpec } from "../../modes"; +import { getSpec, LocalState, parseRaw } from "../../modes/local"; + +describe("getSpec local mode", () => { + it("should return the correct mode config", () => { + const modes = { + local: [ + { + active: true, + selectedProcesses: "curl,http", + }, + ], + } as ModesState; + const mode = getSpec(modes.local[0]); + expect(mode).toBe("local:curl,http"); + }); +}); + +describe("parseRaw local mode", () => { + it("should parse", () => { + const parsed = parseRaw(parseSpec("local:curl,http")); + expect(parsed).toEqual({ + active: true, + ui_id: parsed.ui_id, + selectedProcesses: "curl,http", + } as LocalState); + }); +}); diff --git a/web/src/js/__tests__/modes/regularSpec.ts b/web/src/js/__tests__/modes/regularSpec.ts new file mode 100644 index 0000000000..40e38fa75b --- /dev/null +++ b/web/src/js/__tests__/modes/regularSpec.ts @@ -0,0 +1,31 @@ +import { ModesState } from "../../ducks/modes"; +import { parseSpec } from "../../modes"; +import { getSpec, parseRaw, RegularState } from "../../modes/regular"; + +describe("getSpec regular mode", () => { + it("should return the correct mode config", () => { + const modes = { + regular: [ + { + active: true, + listen_host: "localhost", + listen_port: 8082, + }, + ], + } as ModesState; + const mode = getSpec(modes.regular[0]); + expect(mode).toBe("regular@localhost:8082"); + }); +}); + +describe("parseRaw regular mode", () => { + it("should parse", () => { + const parsed = parseRaw(parseSpec("regular@localhost:8082")); + expect(parsed).toEqual({ + active: true, + ui_id: parsed.ui_id, + listen_host: "localhost", + listen_port: 8082, + } as RegularState); + }); +}); diff --git a/web/src/js/__tests__/modes/reverseSpec.ts b/web/src/js/__tests__/modes/reverseSpec.ts new file mode 100644 index 0000000000..aa88c4e79c --- /dev/null +++ b/web/src/js/__tests__/modes/reverseSpec.ts @@ -0,0 +1,38 @@ +import { ReverseProxyProtocols } from "../../backends/consts"; +import { ModesState } from "../../ducks/modes"; +import { parseSpec } from "../../modes"; +import { getSpec, parseRaw, ReverseState } from "../../modes/reverse"; + +describe("getSpec reverse mode", () => { + it("should return the correct mode config", () => { + const modes = { + reverse: [ + { + active: true, + protocol: ReverseProxyProtocols.HTTPS, + destination: "example.com:8085", + listen_host: "localhost", + listen_port: 8082, + }, + ], + } as ModesState; + const mode = getSpec(modes.reverse[0]); + expect(mode).toBe("reverse:https://example.com:8085@localhost:8082"); + }); +}); + +describe("parseRaw reverse mode", () => { + it("should parse", () => { + const parsed = parseRaw( + parseSpec("reverse:https://example.com:8085@localhost:8082"), + ); + expect(parsed).toEqual({ + active: true, + ui_id: parsed.ui_id, + protocol: ReverseProxyProtocols.HTTPS, + destination: "example.com:8085", + listen_host: "localhost", + listen_port: 8082, + } as ReverseState); + }); +}); diff --git a/web/src/js/__tests__/modes/socksSpec.ts b/web/src/js/__tests__/modes/socksSpec.ts new file mode 100644 index 0000000000..f0b4f79fa4 --- /dev/null +++ b/web/src/js/__tests__/modes/socksSpec.ts @@ -0,0 +1,31 @@ +import { ModesState } from "../../ducks/modes"; +import { parseSpec } from "../../modes"; +import { getSpec, parseRaw, SocksState } from "../../modes/socks"; + +describe("getSpec socks mode", () => { + it("should return the correct mode config", () => { + const modes = { + socks: [ + { + active: true, + listen_host: "localhost", + listen_port: 8082, + }, + ], + } as ModesState; + const mode = getSpec(modes.socks[0]); + expect(mode).toBe("socks5@localhost:8082"); + }); +}); + +describe("parseRaw socks mode", () => { + it("should parse", () => { + const parsed = parseRaw(parseSpec("socks5@localhost:8082")); + expect(parsed).toEqual({ + active: true, + ui_id: parsed.ui_id, + listen_host: "localhost", + listen_port: 8082, + } as SocksState); + }); +}); diff --git a/web/src/js/__tests__/modes/transparentSpec.ts b/web/src/js/__tests__/modes/transparentSpec.ts new file mode 100644 index 0000000000..1a9815d1a1 --- /dev/null +++ b/web/src/js/__tests__/modes/transparentSpec.ts @@ -0,0 +1,31 @@ +import { ModesState } from "../../ducks/modes"; +import { parseSpec } from "../../modes"; +import { getSpec, parseRaw, TransparentState } from "../../modes/transparent"; + +describe("getSpec transparent mode", () => { + it("should return the correct mode config", () => { + const modes = { + transparent: [ + { + active: true, + listen_host: "localhost", + listen_port: 8082, + }, + ], + } as ModesState; + const mode = getSpec(modes.transparent[0]); + expect(mode).toBe("transparent@localhost:8082"); + }); +}); + +describe("parseRaw transparent mode", () => { + it("should parse", () => { + const parsed = parseRaw(parseSpec("transparent@localhost:8082")); + expect(parsed).toEqual({ + active: true, + ui_id: parsed.ui_id, + listen_host: "localhost", + listen_port: 8082, + } as TransparentState); + }); +}); diff --git a/web/src/js/__tests__/modes/upstreamSpec.ts b/web/src/js/__tests__/modes/upstreamSpec.ts new file mode 100644 index 0000000000..05d6efa740 --- /dev/null +++ b/web/src/js/__tests__/modes/upstreamSpec.ts @@ -0,0 +1,35 @@ +import { ModesState } from "../../ducks/modes"; +import { parseSpec } from "../../modes"; +import { getSpec, parseRaw, UpstreamState } from "../../modes/upstream"; + +describe("getSpec upstream mode", () => { + it("should return the correct mode config", () => { + const modes = { + upstream: [ + { + active: true, + destination: "https://example.com:8085", + listen_host: "localhost", + listen_port: 8082, + }, + ], + } as ModesState; + const mode = getSpec(modes.upstream[0]); + expect(mode).toBe("upstream:https://example.com:8085@localhost:8082"); + }); +}); + +describe("parseRaw upstream mode", () => { + it("should parse", () => { + const parsed = parseRaw( + parseSpec("upstream:https://example.com:8085@localhost:8082"), + ); + expect(parsed).toEqual({ + active: true, + ui_id: parsed.ui_id, + destination: "https://example.com:8085", + listen_host: "localhost", + listen_port: 8082, + } as UpstreamState); + }); +}); diff --git a/web/src/js/__tests__/modes/wireguardSpec.ts b/web/src/js/__tests__/modes/wireguardSpec.ts new file mode 100644 index 0000000000..ef1d983263 --- /dev/null +++ b/web/src/js/__tests__/modes/wireguardSpec.ts @@ -0,0 +1,36 @@ +import { ModesState } from "../../ducks/modes"; +import { parseSpec } from "../../modes"; +import { getSpec, parseRaw, WireguardState } from "../../modes/wireguard"; + +describe("getSpec wireguard mode", () => { + it("should return the correct mode config", () => { + const modes = { + wireguard: [ + { + active: true, + listen_host: "localhost", + listen_port: 8082, + }, + { + active: true, + file_path: "~/test", + }, + ], + } as ModesState; + expect(getSpec(modes.wireguard[0])).toBe("wireguard@localhost:8082"); + expect(getSpec(modes.wireguard[1])).toBe("wireguard:~/test"); + }); +}); + +describe("parseRaw wireguard mode", () => { + it("should parse", () => { + const parsed = parseRaw(parseSpec("wireguard@localhost:8082")); + expect(parsed).toEqual({ + active: true, + ui_id: parsed.ui_id, + file_path: "", + listen_host: "localhost", + listen_port: 8082, + } as WireguardState); + }); +}); diff --git a/web/src/js/__tests__/test-utils.tsx b/web/src/js/__tests__/test-utils.tsx index b479b089ef..b68b481e9b 100644 --- a/web/src/js/__tests__/test-utils.tsx +++ b/web/src/js/__tests__/test-utils.tsx @@ -1,31 +1,19 @@ -import * as React from "react" -import {render as rtlRender} from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import "@testing-library/jest-dom" -import {Provider} from 'react-redux' -// Import your own reducer -import {createAppStore} from '../ducks' -import {testState} from "./ducks/tutils"; +import * as React from "react"; +import { render as rtlRender } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import "@testing-library/jest-dom"; +import { Provider } from "react-redux"; +import { TStore } from "./ducks/tutils"; // re-export everything -export { - waitFor, fireEvent, act, screen -} from '@testing-library/react' -export { - userEvent -} +export { waitFor, fireEvent, act, screen } from "@testing-library/react"; +export { userEvent }; -export function render( - ui, - { - store = createAppStore(testState), - ...renderOptions - } = {} -) { - function Wrapper({children}) { - return {children} +export function render(ui, { store = TStore(), ...renderOptions } = {}) { + function Wrapper({ children }: { children: React.ReactNode }) { + return {children}; } - const ret = rtlRender(ui, {wrapper: Wrapper, ...renderOptions}) - return {...ret, store} + const ret = rtlRender(ui, { wrapper: Wrapper, ...renderOptions }); + return { ...ret, store }; } diff --git a/web/src/js/__tests__/urlStateSpec.tsx b/web/src/js/__tests__/urlStateSpec.tsx index 31a0d8c539..9f24feca02 100644 --- a/web/src/js/__tests__/urlStateSpec.tsx +++ b/web/src/js/__tests__/urlStateSpec.tsx @@ -1,103 +1,115 @@ -import initialize from '../urlState' -import { updateStoreFromUrl, updateUrlFromStore } from '../urlState' +import initialize, { + updateStoreFromUrl, + updateUrlFromStore, +} from "../urlState"; -import reduceFlows from '../ducks/flows' -import reduceUI from '../ducks/ui/index' -import reduceEventLog from '../ducks/eventLog' -import reduceCommandBar from '../ducks/commandBar' -import * as flowsActions from '../ducks/flows' +import reduceFlows, * as flowsActions from "../ducks/flows"; +import reduceUI from "../ducks/ui/index"; +import reduceEventLog from "../ducks/eventLog"; +import reduceCommandBar from "../ducks/commandBar"; -import configureStore from 'redux-mock-store' +import configureStore from "redux-mock-store"; -const mockStore = configureStore() -history.replaceState = jest.fn() +const mockStore = configureStore(); +history.replaceState = jest.fn(); -describe('updateStoreFromUrl', () => { - - it('should handle search query', () => { - window.location.hash = "#/flows?s=foo" - let store = mockStore() - updateStoreFromUrl(store) - expect(store.getActions()).toEqual([{ filter: "foo", type: "FLOWS_SET_FILTER" }]) - }) +describe("updateStoreFromUrl", () => { + it("should handle search query", () => { + window.location.hash = "#/flows?s=foo"; + const store = mockStore(); + updateStoreFromUrl(store); + expect(store.getActions()).toEqual([ + { filter: "foo", type: "FLOWS_SET_FILTER" }, + ]); + }); - it('should handle highlight query', () => { - window.location.hash = "#/flows?h=foo" - let store = mockStore() - updateStoreFromUrl(store) - expect(store.getActions()).toEqual([{ highlight: "foo", type: "FLOWS_SET_HIGHLIGHT" }]) - }) + it("should handle highlight query", () => { + window.location.hash = "#/flows?h=foo"; + const store = mockStore(); + updateStoreFromUrl(store); + expect(store.getActions()).toEqual([ + { highlight: "foo", type: "FLOWS_SET_HIGHLIGHT" }, + ]); + }); - it('should handle show event log', () => { - window.location.hash = "#/flows?e=true" - let initialState = { eventLog: reduceEventLog(undefined, {}) }, - store = mockStore(initialState) - updateStoreFromUrl(store) - expect(store.getActions()).toEqual([{ type: "EVENTS_TOGGLE_VISIBILITY" }]) - }) + it("should handle show event log", () => { + window.location.hash = "#/flows?e=true"; + const initialState = { eventLog: reduceEventLog(undefined, {}) }; + const store = mockStore(initialState); + updateStoreFromUrl(store); + expect(store.getActions()).toEqual([ + { type: "EVENTS_TOGGLE_VISIBILITY" }, + ]); + }); - it('should handle unimplemented query argument', () => { - window.location.hash = "#/flows?foo=bar" - console.error = jest.fn() - let store = mockStore() - updateStoreFromUrl(store) - expect(console.error).toBeCalledWith("unimplemented query arg: foo=bar") - }) + it("should handle unimplemented query argument", () => { + window.location.hash = "#/flows?foo=bar"; + console.error = jest.fn(); + const store = mockStore(); + updateStoreFromUrl(store); + expect(console.error).toBeCalledWith( + "unimplemented query arg: foo=bar", + ); + }); - it('should select flow and tab', () => { - window.location.hash = "#/flows/123/request" - let store = mockStore() - updateStoreFromUrl(store) + it("should select flow and tab", () => { + window.location.hash = "#/flows/123/request"; + const store = mockStore(); + updateStoreFromUrl(store); expect(store.getActions()).toEqual([ { flowIds: ["123"], - type: "FLOWS_SELECT" + type: "FLOWS_SELECT", }, { tab: "request", - type: "UI_FLOWVIEW_SET_TAB" - } - ]) - }) -}) + type: "UI_FLOWVIEW_SET_TAB", + }, + ]); + }); +}); -describe('updateUrlFromStore', () => { - let initialState = { - flows: reduceFlows(undefined, {type: "other"}), - ui: reduceUI(undefined, {type: "other"}), - eventLog: reduceEventLog(undefined, {type: "other"}), - commandBar: reduceCommandBar(undefined, {type: "other"}), - } +describe("updateUrlFromStore", () => { + const initialState = { + flows: reduceFlows(undefined, { type: "other" }), + ui: reduceUI(undefined, { type: "other" }), + eventLog: reduceEventLog(undefined, { type: "other" }), + commandBar: reduceCommandBar(undefined, { type: "other" }), + }; - it('should update initial url', () => { - let store = mockStore(initialState) - updateUrlFromStore(store) - expect(history.replaceState).toBeCalledWith(undefined, '', '/#/flows') - }) + it("should update initial url", () => { + const store = mockStore(initialState); + updateUrlFromStore(store); + expect(history.replaceState).toBeCalledWith(undefined, "", "/#/flows"); + }); - it('should update url', () => { - let flows = reduceFlows(undefined, flowsActions.select("123")), - state = { - ...initialState, - flows: reduceFlows(flows, flowsActions.setFilter('~u foo')) - }, - store = mockStore(state) - updateUrlFromStore(store) - expect(history.replaceState).toBeCalledWith(undefined, '', '/#/flows/123/request?s=~u%20foo') - }) -}) + it("should update url", () => { + const flows = reduceFlows(undefined, flowsActions.select("123")); + const state = { + ...initialState, + flows: reduceFlows(flows, flowsActions.setFilter("~u foo")), + }; + const store = mockStore(state); + updateUrlFromStore(store); + expect(history.replaceState).toBeCalledWith( + undefined, + "", + "/#/flows/123/request?s=~u%20foo", + ); + }); +}); -describe('initialize', () => { - let initialState = { - flows: reduceFlows(undefined, {type: "other"}), - ui: reduceUI(undefined, {type: "other"}), - eventLog: reduceEventLog(undefined, {type: "other"}), - commandBar: reduceCommandBar(undefined, {type: "other"}), - } +describe("initialize", () => { + const initialState = { + flows: reduceFlows(undefined, { type: "other" }), + ui: reduceUI(undefined, { type: "other" }), + eventLog: reduceEventLog(undefined, { type: "other" }), + commandBar: reduceCommandBar(undefined, { type: "other" }), + }; - it('should handle initial state', () => { - let store = mockStore(initialState) - initialize(store) - store.dispatch({ type: "foo" }) - }) -}) + it("should handle initial state", () => { + const store = mockStore(initialState); + initialize(store); + store.dispatch({ type: "foo" }); + }); +}); diff --git a/web/src/js/__tests__/utilsSpec.tsx b/web/src/js/__tests__/utilsSpec.tsx index 124954af32..db03e9f587 100644 --- a/web/src/js/__tests__/utilsSpec.tsx +++ b/web/src/js/__tests__/utilsSpec.tsx @@ -1,85 +1,87 @@ -import * as utils from '../utils' -import {enableFetchMocks} from "jest-fetch-mock"; +import * as utils from "../utils"; +import { enableFetchMocks } from "jest-fetch-mock"; enableFetchMocks(); -describe('formatSize', () => { - it('should return 0 when 0 byte', () => { - expect(utils.formatSize(0)).toEqual('0') - }) +describe("formatSize", () => { + it("should return 0 when 0 byte", () => { + expect(utils.formatSize(0)).toEqual("0"); + }); - it('should return formatted size', () => { - expect(utils.formatSize(27104011)).toEqual("25.8mb") - expect(utils.formatSize(1023)).toEqual("1023b") - }) -}) + it("should return formatted size", () => { + expect(utils.formatSize(27104011)).toEqual("25.8mb"); + expect(utils.formatSize(1023)).toEqual("1023b"); + }); +}); -describe('formatTimeDelta', () => { - it('should return formatted time', () => { - expect(utils.formatTimeDelta(3600100)).toEqual("1h") - }) -}) +describe("formatTimeDelta", () => { + it("should return formatted time", () => { + expect(utils.formatTimeDelta(3600100)).toEqual("1h"); + }); +}); -describe('formatTimeStamp', () => { - it('should return formatted time', () => { - expect(utils.formatTimeStamp(1483228800, {milliseconds: false})).toEqual("2017-01-01 00:00:00") - expect(utils.formatTimeStamp(1483228800, {milliseconds: true})).toEqual("2017-01-01 00:00:00.000") - }) -}) +describe("formatTimeStamp", () => { + it("should return formatted time", () => { + expect( + utils.formatTimeStamp(1483228800, { milliseconds: false }), + ).toEqual("2017-01-01 00:00:00"); + expect( + utils.formatTimeStamp(1483228800, { milliseconds: true }), + ).toEqual("2017-01-01 00:00:00.000"); + }); +}); -describe('formatAddress', () => { - it('should return formatted addresses', () => { - expect(utils.formatAddress(["127.0.0.1", 8080])).toEqual("127.0.0.1:8080"); +describe("formatAddress", () => { + it("should return formatted addresses", () => { + expect(utils.formatAddress(["127.0.0.1", 8080])).toEqual( + "127.0.0.1:8080", + ); expect(utils.formatAddress(["::1", 8080])).toEqual("[::1]:8080"); - }) -}) + }); +}); -describe('reverseString', () => { - it('should return reversed string', () => { - let str1 = "abc", str2 = "xyz" - expect(utils.reverseString(str1) > utils.reverseString(str2)).toBeTruthy() - }) -}) +describe("reverseString", () => { + it("should return reversed string", () => { + const str1 = "abc"; + const str2 = "xyz"; + expect( + utils.reverseString(str1) > utils.reverseString(str2), + ).toBeTruthy(); + }); +}); -describe('fetchApi', () => { - it('should handle fetch operation', () => { - utils.fetchApi('http://foo/bar', {method: "POST"}) - expect(fetchMock.mock.calls[0][0]).toEqual( - "http://foo/bar" - ) - fetchMock.mockClear() +describe("fetchApi", () => { + it("should handle fetch operation", () => { + utils.fetchApi("http://foo/bar", { method: "POST" }); + expect(fetchMock.mock.calls[0][0]).toEqual("http://foo/bar"); + fetchMock.mockClear(); - utils.fetchApi('http://foo?bar=1', {method: "POST"}) - expect(fetchMock.mock.calls[0][0]).toEqual( - "http://foo?bar=1" - ) + utils.fetchApi("http://foo?bar=1", { method: "POST" }); + expect(fetchMock.mock.calls[0][0]).toEqual("http://foo?bar=1"); + }); - }) - - it('should be possible to do put request', () => { - fetchMock.mockClear() - utils.fetchApi.put("http://foo", [1, 2, 3], {}) - expect(fetchMock.mock.calls[0]).toEqual( - [ - "http://foo", - { - body: "[1,2,3]", - credentials: "same-origin", - headers: { - "Content-Type": "application/json", - "X-XSRFToken": undefined, - }, - method: "PUT" + it("should be possible to do put request", () => { + fetchMock.mockClear(); + utils.fetchApi.put("http://foo", [1, 2, 3], {}); + expect(fetchMock.mock.calls[0]).toEqual([ + "http://foo", + { + body: "[1,2,3]", + credentials: "same-origin", + headers: { + "Content-Type": "application/json", + "X-XSRFToken": undefined, }, - ] - ) - }) -}) + method: "PUT", + }, + ]); + }); +}); -describe('getDiff', () => { - it('should return json object including only the changed keys value pairs', () => { - let obj1 = {a: 1, b: {foo: 1}, c: [3]}, - obj2 = {a: 1, b: {foo: 2}, c: [4]} - expect(utils.getDiff(obj1, obj2)).toEqual({b: {foo: 2}, c: [4]}) - }) -}) +describe("getDiff", () => { + it("should return json object including only the changed keys value pairs", () => { + const obj1 = { a: 1, b: { foo: 1 }, c: [3] }; + const obj2 = { a: 1, b: { foo: 2 }, c: [4] }; + expect(utils.getDiff(obj1, obj2)).toEqual({ b: { foo: 2 }, c: [4] }); + }); +}); diff --git a/web/src/js/app.tsx b/web/src/js/app.tsx index 69fd66788a..1e8c96aadb 100644 --- a/web/src/js/app.tsx +++ b/web/src/js/app.tsx @@ -1,34 +1,34 @@ -import * as React from "react" -import {render} from 'react-dom' -import {Provider} from 'react-redux' +import * as React from "react"; +import { createRoot } from "react-dom/client"; +import { Provider } from "react-redux"; -import ProxyApp from './components/ProxyApp' -import {add as addLog} from './ducks/eventLog' -import useUrlState from './urlState' -import WebSocketBackend from './backends/websocket' -import StaticBackend from './backends/static' -import {store} from "./ducks"; +import ProxyApp from "./components/ProxyApp"; +import { add as addLog } from "./ducks/eventLog"; +import useUrlState from "./urlState"; +import WebSocketBackend from "./backends/websocket"; +import StaticBackend from "./backends/static"; +import { store } from "./ducks"; - -useUrlState(store) -// @ts-ignore +useUrlState(store); +// @ts-expect-error custom property on window if (window.MITMWEB_STATIC) { - // @ts-ignore - window.backend = new StaticBackend(store) + // @ts-expect-error new property on window for debugging + window.backend = new StaticBackend(store); } else { - // @ts-ignore - window.backend = new WebSocketBackend(store) + // @ts-expect-error new property on window for debugging + window.backend = new WebSocketBackend(store); } -window.addEventListener('error', (e: ErrorEvent) => { - store.dispatch(addLog(`${e.message}\n${e.error.stack}`)) -}) +window.addEventListener("error", (e: ErrorEvent) => { + store.dispatch(addLog(`${e.message}\n${e.error.stack}`)); +}); -document.addEventListener('DOMContentLoaded', () => { - render( +document.addEventListener("DOMContentLoaded", () => { + const container = document.getElementById("mitmproxy"); + const root = createRoot(container!); + root.render( - + , - document.getElementById("mitmproxy") - ) -}) + ); +}); diff --git a/web/src/js/backends/consts.ts b/web/src/js/backends/consts.ts new file mode 100644 index 0000000000..133a47f9b3 --- /dev/null +++ b/web/src/js/backends/consts.ts @@ -0,0 +1,12 @@ +/** Auto-generated by web/gen/backend_consts.py */ +export enum ReverseProxyProtocols { + HTTP = "http", + HTTPS = "https", + HTTP3 = "http3", + TLS = "tls", + DTLS = "dtls", + TCP = "tcp", + UDP = "udp", + DNS = "dns", + QUIC = "quic", +} diff --git a/web/src/js/backends/static.tsx b/web/src/js/backends/static.tsx index 79d537ba77..992174b56b 100644 --- a/web/src/js/backends/static.tsx +++ b/web/src/js/backends/static.tsx @@ -2,36 +2,34 @@ * This backend uses the REST API only to host static instances, * without any Websocket connection. */ -import {fetchApi} from "../utils" -import {Store} from "redux"; -import {RootState} from "../ducks"; +import { fetchApi } from "../utils"; +import { Store } from "redux"; +import { RootState } from "../ducks"; export default class StaticBackend { - - store: Store + store: Store; constructor(store) { - this.store = store - this.onOpen() + this.store = store; + this.onOpen(); } onOpen() { - this.fetchData("flows") - this.fetchData("options") + this.fetchData("flows"); + this.fetchData("options"); // this.fetchData("events") # TODO: Add events log to static viewer. } fetchData(resource) { fetchApi(`./${resource}`) - .then(res => res.json()) - .then(json => { - this.receive(resource, json) - }) + .then((res) => res.json()) + .then((json) => { + this.receive(resource, json); + }); } receive(resource, data) { - let type = `${resource}_RECEIVE`.toUpperCase() - this.store.dispatch({type, cmd: "receive", resource, data}) + const type = `${resource}_RECEIVE`.toUpperCase(); + this.store.dispatch({ type, cmd: "receive", resource, data }); } - } diff --git a/web/src/js/backends/websocket.tsx b/web/src/js/backends/websocket.tsx index 64cf284841..34a3d44b9a 100644 --- a/web/src/js/backends/websocket.tsx +++ b/web/src/js/backends/websocket.tsx @@ -3,92 +3,110 @@ * from the REST API and live updates delivered via a WebSocket connection. * An alternative backend may use the REST API only to host static instances. */ -import {fetchApi} from "../utils" -import * as connectionActions from "../ducks/connection" -import {Store} from "redux"; -import {RootState} from "../ducks"; +import { fetchApi } from "../utils"; +import * as connectionActions from "../ducks/connection"; +import { Store } from "redux"; +import { RootState } from "../ducks"; +import { PayloadAction } from "@reduxjs/toolkit"; +import { BackendState } from "../ducks/backendState"; -const CMD_RESET = 'reset' +const CMD_RESET = "reset"; export default class WebsocketBackend { - activeFetches: { - flows?: [] - events?: [] - options?: [] - } - store: Store - socket: WebSocket + flows?: []; + events?: []; + options?: []; + }; + store: Store; + socket: WebSocket; constructor(store) { - this.activeFetches = {} - this.store = store - this.connect() + this.activeFetches = {}; + this.store = store; + this.connect(); } connect() { - this.socket = new WebSocket(location.origin.replace('http', 'ws') + '/updates') - this.socket.addEventListener('open', () => this.onOpen()) - this.socket.addEventListener('close', event => this.onClose(event)) - this.socket.addEventListener('message', msg => this.onMessage(JSON.parse(msg.data))) - this.socket.addEventListener('error', error => this.onError(error)) + this.socket = new WebSocket( + location.origin.replace("http", "ws") + + location.pathname.replace(/\/$/, "") + + "/updates", + ); + this.socket.addEventListener("open", () => this.onOpen()); + this.socket.addEventListener("close", (event) => this.onClose(event)); + this.socket.addEventListener("message", (msg) => + this.onMessage(JSON.parse(msg.data)), + ); + this.socket.addEventListener("error", (error) => this.onError(error)); } onOpen() { - this.fetchData("state") - this.fetchData("flows") - this.fetchData("events") - this.fetchData("options") - this.store.dispatch(connectionActions.startFetching()) + this.fetchData("state"); + this.fetchData("flows"); + this.fetchData("events"); + this.fetchData("options"); + this.store.dispatch(connectionActions.startFetching()); } fetchData(resource) { - let queue = [] - this.activeFetches[resource] = queue + const queue = []; + this.activeFetches[resource] = queue; fetchApi(`./${resource}`) - .then(res => res.json()) - .then(json => { + .then((res) => res.json()) + .then((json) => { // Make sure that we are not superseded yet by the server sending a RESET. if (this.activeFetches[resource] === queue) - this.receive(resource, json) - }) + this.receive(resource, json); + }); } onMessage(msg) { - if (msg.cmd === CMD_RESET) { - return this.fetchData(msg.resource) + return this.fetchData(msg.resource); } if (msg.resource in this.activeFetches) { - this.activeFetches[msg.resource].push(msg) + this.activeFetches[msg.resource].push(msg); } else { - let type = `${msg.resource}_${msg.cmd}`.toUpperCase() - this.store.dispatch({type, ...msg}) + const type = `${msg.resource}_${msg.cmd}`.toUpperCase(); + this.store.dispatch({ type, ...msg }); } } receive(resource, data) { - let type = `${resource}_RECEIVE`.toUpperCase() - this.store.dispatch({type, cmd: "receive", resource, data}) - let queue = this.activeFetches[resource] - delete this.activeFetches[resource] - queue.forEach(msg => this.onMessage(msg)) + const type = `${resource}_RECEIVE`.toUpperCase(); + if (resource === "state") { + this.store.dispatch({ + type, + payload: data, + } as PayloadAction); + } else { + // deprecated: these should be converted to payload actions as well. + this.store.dispatch({ type, cmd: "receive", resource, data }); + } + const queue = this.activeFetches[resource]; + delete this.activeFetches[resource]; + queue.forEach((msg) => this.onMessage(msg)); if (Object.keys(this.activeFetches).length === 0) { // We have fetched the last resource - this.store.dispatch(connectionActions.connectionEstablished()) + this.store.dispatch(connectionActions.connectionEstablished()); } } onClose(closeEvent) { - this.store.dispatch(connectionActions.connectionError( - `Connection closed at ${new Date().toUTCString()} with error code ${closeEvent.code}.` - )) - console.error("websocket connection closed", closeEvent) + this.store.dispatch( + connectionActions.connectionError( + `Connection closed at ${new Date().toUTCString()} with error code ${ + closeEvent.code + }.`, + ), + ); + console.error("websocket connection closed", closeEvent); } - onError(error) { + onError(...args) { // FIXME - console.error("websocket connection errored", arguments) + console.error("websocket connection errored", args); } } diff --git a/web/src/js/components/CaptureSetup.tsx b/web/src/js/components/CaptureSetup.tsx deleted file mode 100644 index 63e7a488e3..0000000000 --- a/web/src/js/components/CaptureSetup.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import * as React from "react"; -import {useEffect, useRef} from "react"; -import {useAppSelector} from "../ducks"; -import {ServerInfo} from "../ducks/backendState"; -import {formatAddress} from "../utils"; -import QRCode from 'qrcode'; - -export default function CaptureSetup() { - const servers = useAppSelector(state => state.backendState.servers); - - let configure_action_text; - if (servers.length === 0) { - configure_action_text = ""; - } else if (servers.length === 1) { - configure_action_text = "Configure your client to use the following proxy server:"; - } else { - configure_action_text = "Configure your client to use one of the following proxy servers:"; - } - - return
    - -

    mitmproxy is running.

    -

    - No flows have been recorded yet.
    - {configure_action_text} -

    -
      - {servers.map((server, i) =>
    • )} -
    - {/* -

    You can also start additional servers:

    -
      -
    • TODO
    • -
    - */} - -
    -} - -export function ServerDescription( - { - description, - listen_addrs, - last_exception, - is_running, - full_spec, - wireguard_conf, - }: ServerInfo -) { - const qrCode = useRef(null); - useEffect(() => { - if (wireguard_conf && qrCode.current) - QRCode.toCanvas(qrCode.current, wireguard_conf, {margin: 0, scale: 3}); - }, [wireguard_conf]); - - let listen_str; - const all_same_port = listen_addrs.length === 1 || (listen_addrs.length === 2 && listen_addrs[0][1] === listen_addrs[1][1]); - const unbound = listen_addrs.every(addr => ["::", "0.0.0.0"].includes(addr[0])); - if (all_same_port && unbound) { - listen_str = formatAddress(["*", listen_addrs[0][1]]); - } else { - listen_str = listen_addrs.map(formatAddress).join(" and "); - } - description = description[0].toUpperCase() + description.substr(1); - let desc, icon; - if (last_exception) { - icon = "fa-exclamation text-error" - desc = <>{description} ({full_spec}):
    {last_exception}; - } else if (!is_running) { - icon = "fa-pause text-warning" - desc = <>{description} ({full_spec}) - } else { - icon = "fa-check text-success" - desc = `${description} listening at ${listen_str}.` - - if (wireguard_conf) { - desc = <> - {desc} -
    -
    {wireguard_conf}
    - -
    - ; - } - - } - return <>{desc}; -} diff --git a/web/src/js/components/CommandBar.tsx b/web/src/js/components/CommandBar.tsx index e6e3cd9019..78a74454b7 100644 --- a/web/src/js/components/CommandBar.tsx +++ b/web/src/js/components/CommandBar.tsx @@ -1,154 +1,196 @@ -import React, {useEffect, useRef, useState} from 'react' -import classnames from 'classnames' -import {fetchApi, runCommand} from '../utils' -import Filt from '../filt/command' +import React, { useEffect, useRef, useState } from "react"; +import classnames from "classnames"; +import { fetchApi, runCommand } from "../utils"; +import Filt from "../filt/command"; type CommandParameter = { - name: string - type: string - kind: string -} + name: string; + type: string; + kind: string; +}; type Command = { - help?: string - parameters: CommandParameter[] - return_type: string | undefined - signature_help: string -} + help?: string; + parameters: CommandParameter[]; + return_type: string | undefined; + signature_help: string; +}; type AllCommands = { - [name: string]: Command -} + [name: string]: Command; +}; type CommandHelpProps = { - nextArgs: string[], - currentArg: number, - help: string, - description: string, - availableCommands: string[], -} + nextArgs: string[]; + currentArg: number; + help: string; + description: string; + availableCommands: string[]; +}; type CommandResult = { - command: string, - result: string, -} + command: string; + result: string; +}; type ResultProps = { - results: CommandResult[], -} + results: CommandResult[]; +}; -function getAvailableCommands(commands: AllCommands, input: string = ""): string[] { - if (!commands) return [] - let availableCommands: string[] = [] - for (const [command, args] of Object.entries(commands)) { +function getAvailableCommands( + commands: AllCommands, + input: string = "", +): string[] { + if (!commands) return []; + const availableCommands: string[] = []; + for (const command of Object.keys(commands)) { if (command.startsWith(input)) { - availableCommands.push(command) + availableCommands.push(command); } } - return availableCommands + return availableCommands; } -export function Results({results}: ResultProps) { +export function Results({ results }: ResultProps) { const resultElement = useRef(null!); useEffect(() => { if (resultElement) { - resultElement.current.addEventListener('DOMNodeInserted', (event) => { - const target = event.currentTarget as Element; - target.scroll({top: target.scrollHeight, behavior: 'auto'}); - }); + resultElement.current.addEventListener( + "DOMNodeInserted", + (event) => { + const target = event.currentTarget as Element; + target.scroll({ + top: target.scrollHeight, + behavior: "auto", + }); + }, + ); } - }, []) + }, []); return (
    {results.map((result, i) => (
    -
    $ {result.command}
    +
    + $ {result.command} +
    {result.result}
    ))}
    - ) + ); } -export function CommandHelp({nextArgs, currentArg, help, description, availableCommands}: CommandHelpProps) { - let argumentSuggestion: JSX.Element[] = [] +export function CommandHelp({ + nextArgs, + currentArg, + help, + description, + availableCommands, +}: CommandHelpProps) { + const argumentSuggestion: JSX.Element[] = []; for (let i: number = 0; i < nextArgs.length; i++) { if (i == currentArg) { - argumentSuggestion.push({nextArgs[i]}) - continue + argumentSuggestion.push({nextArgs[i]}); + continue; } - argumentSuggestion.push({nextArgs[i]} ) + argumentSuggestion.push({nextArgs[i]} ); } - return (
    -
    -
    - {argumentSuggestion.length > 0 &&
    Argument suggestion: {argumentSuggestion}
    } - {help?.includes("->") &&
    Signature help: {help}
    } - {description &&
    # {description}
    } -
    Available Commands:

    {JSON.stringify(availableCommands)}

    + return ( +
    +
    +
    + {argumentSuggestion.length > 0 && ( +
    + Argument suggestion:{" "} + {argumentSuggestion} +
    + )} + {help?.includes("->") && ( +
    + Signature help: + {help} +
    + )} + {description &&
    # {description}
    } +
    + Available Commands: +

    + {JSON.stringify(availableCommands)} +

    +
    +
    -
    ) + ); } export default function CommandBar() { - const [input, setInput] = useState("") - const [originalInput, setOriginalInput] = useState("") - const [currentCompletion, setCurrentCompletion] = useState(0) - const [completionCandidate, setCompletionCandidate] = useState([]) - - const [availableCommands, setAvailableCommands] = useState([]) - const [allCommands, setAllCommands] = useState({}) - const [nextArgs, setNextArgs] = useState([]) - const [currentArg, setCurrentArg] = useState(0) - const [signatureHelp, setSignatureHelp] = useState("") - const [description, setDescription] = useState("") - - const [results, setResults] = useState([]) - const [history, setHistory] = useState([]) + const [input, setInput] = useState(""); + const [originalInput, setOriginalInput] = useState(""); + const [currentCompletion, setCurrentCompletion] = useState(0); + const [completionCandidate, setCompletionCandidate] = useState( + [], + ); + + const [availableCommands, setAvailableCommands] = useState([]); + const [allCommands, setAllCommands] = useState({}); + const [nextArgs, setNextArgs] = useState([]); + const [currentArg, setCurrentArg] = useState(0); + const [signatureHelp, setSignatureHelp] = useState(""); + const [description, setDescription] = useState(""); + + const [results, setResults] = useState([]); + const [history, setHistory] = useState([]); const [currentPos, setCurrentPos] = useState(undefined); useEffect(() => { - fetchApi('/commands', {method: 'GET'}) - .then(response => response.json()) + fetchApi("/commands", { method: "GET" }) + .then((response) => response.json()) .then((data: AllCommands) => { - setAllCommands(data) - setCompletionCandidate(getAvailableCommands(data)) - setAvailableCommands(Object.keys(data)) - }).catch(e => console.error(e)) - }, []) + setAllCommands(data); + setCompletionCandidate(getAvailableCommands(data)); + setAvailableCommands(Object.keys(data)); + }) + .catch((e) => console.error(e)); + }, []); useEffect(() => { - runCommand("commands.history.get").then((ret) => { - setHistory(ret.value); - }).catch(e => console.error(e)) - }, []) + runCommand("commands.history.get") + .then((ret) => { + setHistory(ret.value); + }) + .catch((e) => console.error(e)); + }, []); const parseCommand = (originalInput: string, input: string) => { - const parts: string[] = Filt.parse(input) - const originalParts: string[] = Filt.parse(originalInput) + const parts: string[] = Filt.parse(input); + const originalParts: string[] = Filt.parse(originalInput); - setSignatureHelp(allCommands[parts[0]]?.signature_help) - setDescription(allCommands[parts[0]]?.help || "") + setSignatureHelp(allCommands[parts[0]]?.signature_help); + setDescription(allCommands[parts[0]]?.help || ""); - setCompletionCandidate(getAvailableCommands(allCommands, originalParts[0])) - setAvailableCommands(getAvailableCommands(allCommands, parts[0])) + setCompletionCandidate( + getAvailableCommands(allCommands, originalParts[0]), + ); + setAvailableCommands(getAvailableCommands(allCommands, parts[0])); - const nextArgs: string[] = allCommands[parts[0]]?.parameters.map(p => p.name) + const nextArgs: string[] = allCommands[parts[0]]?.parameters.map( + (p) => p.name, + ); if (nextArgs) { - setNextArgs([parts[0], ...nextArgs]) - setCurrentArg(parts.length - 1) + setNextArgs([parts[0], ...nextArgs]); + setCurrentArg(parts.length - 1); } - } + }; const onChange = (e) => { - setInput(e.target.value) - setOriginalInput(e.target.value) - setCurrentCompletion(0) - } + setInput(e.target.value); + setOriginalInput(e.target.value); + setCurrentCompletion(0); + }; const onKeyDown = (e) => { if (e.key === "Enter") { @@ -157,32 +199,40 @@ export default function CommandBar() { setHistory([...history, input]); runCommand("commands.history.add", input).catch(() => 0); - fetchApi.post(`/commands/${cmd}`, {arguments: args}) - .then(response => response.json()) - .then(data => { - setCurrentPos(undefined) - setNextArgs([]) - setResults([...results, { - command: input, - result: JSON.stringify(data.value || data.error) - }]) - }).catch(e => { - setCurrentPos(undefined) - setNextArgs([]) - setResults([...results, { - command: input, - result: e.toString() - }]); - }) + fetchApi + .post(`/commands/${cmd}`, { arguments: args }) + .then((response) => response.json()) + .then((data) => { + setCurrentPos(undefined); + setNextArgs([]); + setResults([ + ...results, + { + command: input, + result: JSON.stringify(data.value || data.error), + }, + ]); + }) + .catch((e) => { + setCurrentPos(undefined); + setNextArgs([]); + setResults([ + ...results, + { + command: input, + result: e.toString(), + }, + ]); + }); - setSignatureHelp("") - setDescription("") + setSignatureHelp(""); + setDescription(""); - setInput("") - setOriginalInput("") + setInput(""); + setOriginalInput(""); - setCurrentCompletion(0) - setCompletionCandidate(availableCommands) + setCurrentCompletion(0); + setCompletionCandidate(availableCommands); } if (e.key === "ArrowUp") { let nextPos; @@ -191,52 +241,57 @@ export default function CommandBar() { } else { nextPos = Math.max(0, currentPos - 1); } - setInput(history[nextPos]) - setOriginalInput(history[nextPos]) - setCurrentPos(nextPos) + setInput(history[nextPos]); + setOriginalInput(history[nextPos]); + setCurrentPos(nextPos); } if (e.key === "ArrowDown") { if (currentPos === undefined) { - return + return; } else if (currentPos == history.length - 1) { setInput(""); setOriginalInput(""); setCurrentPos(undefined); } else { const nextPos = currentPos + 1; - setInput(history[nextPos]) - setOriginalInput(history[nextPos]) - setCurrentPos(nextPos) + setInput(history[nextPos]); + setOriginalInput(history[nextPos]); + setCurrentPos(nextPos); } } if (e.key === "Tab") { - setInput(completionCandidate[currentCompletion]) - setCurrentCompletion((currentCompletion + 1) % completionCandidate.length) - e.preventDefault() + setInput(completionCandidate[currentCompletion]); + setCurrentCompletion( + (currentCompletion + 1) % completionCandidate.length, + ); + e.preventDefault(); } - e.stopPropagation() - } + e.stopPropagation(); + }; const onKeyUp = (e) => { if (!input) { - setAvailableCommands(Object.keys(allCommands)) - return + setAvailableCommands(Object.keys(allCommands)); + return; } - parseCommand(originalInput, input) - e.stopPropagation() - } + parseCommand(originalInput, input); + e.stopPropagation(); + }; return (
    -
    - Command Result -
    - - -
    +
    Command Result
    + + +
    - +
    - ) + ); } diff --git a/web/src/js/components/EventLog.jsx b/web/src/js/components/EventLog.jsx deleted file mode 100644 index 40fe900ed3..0000000000 --- a/web/src/js/components/EventLog.jsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { connect } from 'react-redux' -import { toggleFilter, toggleVisibility } from '../ducks/eventLog' -import ToggleButton from './common/ToggleButton' -import EventList from './EventLog/EventList' - -export class PureEventLog extends Component { - - static propTypes = { - filters: PropTypes.object.isRequired, - events: PropTypes.array.isRequired, - toggleFilter: PropTypes.func.isRequired, - close: PropTypes.func.isRequired, - defaultHeight: PropTypes.number, - } - - static defaultProps = { - defaultHeight: 200, - } - - constructor(props, context) { - super(props, context) - - this.state = { height: this.props.defaultHeight } - - this.onDragStart = this.onDragStart.bind(this) - this.onDragMove = this.onDragMove.bind(this) - this.onDragStop = this.onDragStop.bind(this) - } - - onDragStart(event) { - event.preventDefault() - this.dragStart = this.state.height + event.pageY - window.addEventListener('mousemove', this.onDragMove) - window.addEventListener('mouseup', this.onDragStop) - window.addEventListener('dragend', this.onDragStop) - } - - onDragMove(event) { - event.preventDefault() - this.setState({ height: this.dragStart - event.pageY }) - } - - onDragStop(event) { - event.preventDefault() - window.removeEventListener('mousemove', this.onDragMove) - } - - render() { - const { height } = this.state - const { filters, events, toggleFilter, close } = this.props - - return ( -
    -
    - Eventlog -
    - {['debug', 'info', 'web', 'warn', 'error'].map(type => ( - toggleFilter(type)}/> - ))} - -
    -
    - -
    - ) - } -} - -export default connect( - state => ({ - filters: state.eventLog.filters, - events: state.eventLog.view, - }), - { - close: toggleVisibility, - toggleFilter: toggleFilter, - } -)(PureEventLog) diff --git a/web/src/js/components/EventLog.tsx b/web/src/js/components/EventLog.tsx new file mode 100644 index 0000000000..50e5a70db7 --- /dev/null +++ b/web/src/js/components/EventLog.tsx @@ -0,0 +1,94 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { + EventLogItem, + LogLevel, + toggleFilter, + toggleVisibility, +} from "../ducks/eventLog"; +import ToggleButton from "./common/ToggleButton"; +import EventList from "./EventLog/EventList"; +import { RootState } from "../ducks"; + +type EventLogState = { + height: number; +}; + +type EventLogProps = { + events: EventLogItem[]; + filters: { [level in LogLevel]: boolean }; + toggleFilter: (filter: LogLevel) => any; + close: () => any; + defaultHeight: number; +}; + +export class PureEventLog extends Component { + static defaultProps = { + defaultHeight: 200, + }; + private dragStart: number; + + constructor(props, context) { + super(props, context); + + this.state = { height: this.props.defaultHeight }; + + this.onDragStart = this.onDragStart.bind(this); + this.onDragMove = this.onDragMove.bind(this); + this.onDragStop = this.onDragStop.bind(this); + } + + onDragStart(event: React.MouseEvent) { + event.preventDefault(); + this.dragStart = this.state.height + event.pageY; + window.addEventListener("mousemove", this.onDragMove); + window.addEventListener("mouseup", this.onDragStop); + window.addEventListener("dragend", this.onDragStop); + } + + onDragMove(event: MouseEvent) { + event.preventDefault(); + this.setState({ height: this.dragStart - event.pageY }); + } + + onDragStop(event: MouseEvent | DragEvent) { + event.preventDefault(); + window.removeEventListener("mousemove", this.onDragMove); + } + + render() { + const { height } = this.state; + const { filters, events, toggleFilter, close } = this.props; + + return ( +
    +
    + Eventlog +
    + {Object.values(LogLevel).map((type) => ( + toggleFilter(type)} + /> + ))} + +
    +
    + +
    + ); + } +} + +export default connect( + (state: RootState) => ({ + filters: state.eventLog.filters, + events: state.eventLog.view, + }), + { + close: toggleVisibility, + toggleFilter: toggleFilter, + }, +)(PureEventLog); diff --git a/web/src/js/components/EventLog/EventList.tsx b/web/src/js/components/EventLog/EventList.tsx index 2b25a31aee..6fb05af997 100644 --- a/web/src/js/components/EventLog/EventList.tsx +++ b/web/src/js/components/EventLog/EventList.tsx @@ -1,108 +1,121 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import ReactDOM from 'react-dom' -import shallowEqual from 'shallowequal' -import AutoScroll from '../helpers/AutoScroll' -import {calcVScroll, VScroll} from '../helpers/VirtualScroll' -import {EventLogItem} from "../../ducks/eventLog"; - +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import * as autoscroll from "../helpers/AutoScroll"; +import { calcVScroll, VScroll } from "../helpers/VirtualScroll"; +import { EventLogItem } from "../../ducks/eventLog"; +import { shallowEqual } from "react-redux"; type EventLogListProps = { - events: EventLogItem[] - rowHeight: number -} + events: EventLogItem[]; + rowHeight: number; +}; type EventLogListState = { - vScroll: VScroll -} - -class EventLogList extends Component { + vScroll: VScroll; +}; +export default class EventLogList extends Component< + EventLogListProps, + EventLogListState +> { static propTypes = { events: PropTypes.array.isRequired, rowHeight: PropTypes.number, - } + }; static defaultProps = { rowHeight: 18, - } + }; - heights: {[id: string]: number} + heights: { [id: string]: number }; + + viewport = React.createRef(); constructor(props) { - super(props) + super(props); - this.heights = {} - this.state = { vScroll: calcVScroll() } + this.heights = {}; + this.state = { vScroll: calcVScroll() }; - this.onViewportUpdate = this.onViewportUpdate.bind(this) + this.onViewportUpdate = this.onViewportUpdate.bind(this); } componentDidMount() { - window.addEventListener('resize', this.onViewportUpdate) - this.onViewportUpdate() + window.addEventListener("resize", this.onViewportUpdate); + this.onViewportUpdate(); } componentWillUnmount() { - window.removeEventListener('resize', this.onViewportUpdate) + window.removeEventListener("resize", this.onViewportUpdate); } - componentDidUpdate() { - this.onViewportUpdate() + getSnapshotBeforeUpdate() { + return autoscroll.isAtBottom(this.viewport); + } + + componentDidUpdate(prevProps, prevState, snapshot) { + if (snapshot) { + autoscroll.adjustScrollTop(this.viewport); + } + this.onViewportUpdate(); } onViewportUpdate() { - const viewport = ReactDOM.findDOMNode(this) + const viewport = this.viewport.current!; const vScroll = calcVScroll({ itemCount: this.props.events.length, rowHeight: this.props.rowHeight, viewportTop: viewport.scrollTop, viewportHeight: viewport.offsetHeight, - itemHeights: this.props.events.map(entry => this.heights[entry.id]), - }) + itemHeights: this.props.events.map( + (entry) => this.heights[entry.id], + ), + }); if (!shallowEqual(this.state.vScroll, vScroll)) { - this.setState({vScroll}) + this.setState({ vScroll }); } } setHeight(id, node) { if (node && !this.heights[id]) { - const height = node.offsetHeight + const height = node.offsetHeight; if (this.heights[id] !== height) { - this.heights[id] = height - this.onViewportUpdate() + this.heights[id] = height; + this.onViewportUpdate(); } } } render() { - const { vScroll } = this.state - const { events } = this.props + const { vScroll } = this.state; + const { events } = this.props; return ( -
    -                
    - {events.slice(vScroll.start, vScroll.end).map(event => ( -
    this.setHeight(event.id, node)}> - +
    +                
    + {events.slice(vScroll.start, vScroll.end).map((event) => ( +
    this.setHeight(event.id, node)} + > + {event.message}
    ))} -
    +
    - ) + ); } } -function LogIcon({ event }) { - const icon = { - web: 'html5', - debug: 'bug', - warn: 'exclamation-triangle', - error: 'ban' - }[event.level] || 'info' - return +function LogIcon({ event }: { event: EventLogItem }) { + const icon = + { + web: "html5", + debug: "bug", + warn: "exclamation-triangle", + error: "ban", + }[event.level] || "info"; + return ; } - -export default AutoScroll(EventLogList) diff --git a/web/src/js/components/FlowTable.jsx b/web/src/js/components/FlowTable.jsx deleted file mode 100644 index eb5d4adb96..0000000000 --- a/web/src/js/components/FlowTable.jsx +++ /dev/null @@ -1,140 +0,0 @@ -import * as React from "react" -import PropTypes from 'prop-types' -import ReactDOM from 'react-dom' -import { connect } from 'react-redux' -import shallowEqual from 'shallowequal' -import AutoScroll from './helpers/AutoScroll' -import { calcVScroll } from './helpers/VirtualScroll' -import FlowTableHead from './FlowTable/FlowTableHead' -import FlowRow from './FlowTable/FlowRow' -import Filt from "../filt/filt" - - -class FlowTable extends React.Component { - - static propTypes = { - flows: PropTypes.array.isRequired, - rowHeight: PropTypes.number, - highlight: PropTypes.string, - selected: PropTypes.object, - } - - static defaultProps = { - rowHeight: 32, - } - - constructor(props, context) { - super(props, context) - - this.state = { vScroll: calcVScroll() } - this.onViewportUpdate = this.onViewportUpdate.bind(this) - } - - UNSAFE_componentWillMount() { - window.addEventListener('resize', this.onViewportUpdate) - } - - componentDidMount() { - this.onViewportUpdate(); - } - - UNSAFE_componentWillUnmount() { - window.removeEventListener('resize', this.onViewportUpdate) - } - - componentDidUpdate() { - this.onViewportUpdate() - - if (!this.shouldScrollIntoView) { - return - } - - this.shouldScrollIntoView = false - - const { rowHeight, flows, selected } = this.props - const viewport = ReactDOM.findDOMNode(this) - const head = ReactDOM.findDOMNode(this.refs.head) - - const headHeight = head ? head.offsetHeight : 0 - - const rowTop = (flows.indexOf(selected) * rowHeight) + headHeight - const rowBottom = rowTop + rowHeight - - const viewportTop = viewport.scrollTop - const viewportHeight = viewport.offsetHeight - - // Account for pinned thead - if (rowTop - headHeight < viewportTop) { - viewport.scrollTop = rowTop - headHeight - } else if (rowBottom > viewportTop + viewportHeight) { - viewport.scrollTop = rowBottom - viewportHeight - } - } - - UNSAFE_componentWillReceiveProps(nextProps) { - if (nextProps.selected && nextProps.selected !== this.props.selected) { - this.shouldScrollIntoView = true - } - } - - onViewportUpdate() { - const viewport = ReactDOM.findDOMNode(this) - const viewportTop = viewport.scrollTop || 0 - - const vScroll = calcVScroll({ - viewportTop, - viewportHeight: viewport.offsetHeight || 0, - itemCount: this.props.flows.length, - rowHeight: this.props.rowHeight, - }) - - if (this.state.viewportTop !== viewportTop || !shallowEqual(this.state.vScroll, vScroll)) { - // the next rendered state may only have much lower number of rows compared to what the current - // viewportHeight anticipates. To make sure that we update (almost) at once, we already constrain - // the maximum viewportTop value. See https://github.com/mitmproxy/mitmproxy/pull/5658 for details. - let newViewportTop = Math.min(viewportTop, vScroll.end * this.props.rowHeight); - this.setState({ - vScroll, - viewportTop: newViewportTop - }); - } - } - - render() { - const { vScroll, viewportTop } = this.state - const { flows, selected, highlight } = this.props - const isHighlighted = highlight ? Filt.parse(highlight) : () => false - - return ( -
    - - - - - - - {flows.slice(vScroll.start, vScroll.end).map(flow => ( - - ))} - - -
    -
    - ) - } -} - -export const PureFlowTable = AutoScroll(FlowTable) - -export default connect( - state => ({ - flows: state.flows.view, - highlight: state.flows.highlight, - selected: state.flows.byId[state.flows.selected[0]], - }) -)(PureFlowTable) diff --git a/web/src/js/components/FlowTable.tsx b/web/src/js/components/FlowTable.tsx new file mode 100644 index 0000000000..1b49639659 --- /dev/null +++ b/web/src/js/components/FlowTable.tsx @@ -0,0 +1,156 @@ +import * as React from "react"; +import { connect, shallowEqual } from "react-redux"; +import * as autoscroll from "./helpers/AutoScroll"; +import { calcVScroll, VScroll } from "./helpers/VirtualScroll"; +import FlowTableHead from "./FlowTable/FlowTableHead"; +import FlowRow from "./FlowTable/FlowRow"; +import Filt from "../filt/filt"; +import { Flow } from "../flow"; +import { RootState } from "../ducks"; + +type FlowTableProps = { + flows: Flow[]; + rowHeight: number; + highlight: string; + selected: Flow; +}; + +type FlowTableState = { + vScroll: VScroll; + viewportTop: number; +}; + +export class PureFlowTable extends React.Component< + FlowTableProps, + FlowTableState +> { + static defaultProps = { + rowHeight: 32, + }; + private viewport = React.createRef(); + private head = React.createRef(); + + constructor(props, context) { + super(props, context); + + this.state = { + vScroll: calcVScroll(), + viewportTop: 0, + }; + this.onViewportUpdate = this.onViewportUpdate.bind(this); + } + + componentDidMount() { + window.addEventListener("resize", this.onViewportUpdate); + this.onViewportUpdate(); + } + + componentWillUnmount() { + window.removeEventListener("resize", this.onViewportUpdate); + } + + getSnapshotBeforeUpdate() { + return autoscroll.isAtBottom(this.viewport); + } + + componentDidUpdate(prevProps, prevState, snapshot) { + if (snapshot) { + autoscroll.adjustScrollTop(this.viewport); + } + this.onViewportUpdate(); + + const selectedNewFlow = + this.props.selected && this.props.selected !== prevProps.selected; + if (selectedNewFlow) { + const { rowHeight, flows, selected } = this.props; + const viewport = this.viewport.current!; + const head = this.head.current; + + const headHeight = head ? head.offsetHeight : 0; + + const rowTop = flows.indexOf(selected) * rowHeight + headHeight; + const rowBottom = rowTop + rowHeight; + + const viewportTop = viewport.scrollTop; + const viewportHeight = viewport.offsetHeight; + + // Account for pinned thead + if (rowTop - headHeight < viewportTop) { + viewport.scrollTop = rowTop - headHeight; + } else if (rowBottom > viewportTop + viewportHeight) { + viewport.scrollTop = rowBottom - viewportHeight; + } + this.onViewportUpdate(); + } + } + + onViewportUpdate() { + const viewport = this.viewport.current!; + const viewportTop = viewport.scrollTop || 0; + + const vScroll = calcVScroll({ + viewportTop, + viewportHeight: viewport.offsetHeight || 0, + itemCount: this.props.flows.length, + rowHeight: this.props.rowHeight, + }); + + if ( + this.state.viewportTop !== viewportTop || + !shallowEqual(this.state.vScroll, vScroll) + ) { + // the next rendered state may only have much lower number of rows compared to what the current + // viewportHeight anticipates. To make sure that we update (almost) at once, we already constrain + // the maximum viewportTop value. See https://github.com/mitmproxy/mitmproxy/pull/5658 for details. + const newViewportTop = Math.min( + viewportTop, + vScroll.end * this.props.rowHeight, + ); + this.setState({ + vScroll, + viewportTop: newViewportTop, + }); + } + } + + render() { + const { vScroll, viewportTop } = this.state; + const { flows, selected, highlight } = this.props; + const isHighlighted = highlight ? Filt.parse(highlight) : () => false; + + return ( +
    + + + + + + + {flows.slice(vScroll.start, vScroll.end).map((flow) => ( + + ))} + + +
    +
    + ); + } +} + +export default connect((state: RootState) => ({ + flows: state.flows.view, + highlight: state.flows.highlight, + selected: state.flows.byId[state.flows.selected[0]], +}))(PureFlowTable); diff --git a/web/src/js/components/FlowTable/FlowColumns.tsx b/web/src/js/components/FlowTable/FlowColumns.tsx index 61664c8919..4d2d35f75f 100644 --- a/web/src/js/components/FlowTable/FlowColumns.tsx +++ b/web/src/js/components/FlowTable/FlowColumns.tsx @@ -1,240 +1,211 @@ -import React, {ReactElement, useState} from 'react' -import {useDispatch} from 'react-redux' -import classnames from 'classnames' -import {canReplay, endTime, getTotalSize, RequestUtils, ResponseUtils, startTime} from '../../flow/utils' -import {formatSize, formatTimeDelta, formatTimeStamp} from '../../utils' +import React, { ReactElement } from "react"; +import { useAppDispatch, useAppSelector } from "../../ducks"; +import classnames from "classnames"; +import { + canReplay, + endTime, + getTotalSize, + startTime, + sortFunctions, + getIcon, + mainPath, + statusCode, + getMethod, + getVersion, +} from "../../flow/utils"; +import { formatSize, formatTimeDelta, formatTimeStamp } from "../../utils"; import * as flowActions from "../../ducks/flows"; -import {Flow} from "../../flow"; - +import { Flow } from "../../flow"; type FlowColumnProps = { - flow: Flow -} + flow: Flow; +}; interface FlowColumn { (props: FlowColumnProps): JSX.Element; headerName: string; // Shown in the UI - sortKey: (flow: Flow) => any; } -export const tls: FlowColumn = ({flow}) => { +export const tls: FlowColumn = ({ flow }) => { return ( - - ) -} -tls.headerName = '' -tls.sortKey = flow => flow.type === "http" && flow.request.scheme + + ); +}; +tls.headerName = ""; -export const icon: FlowColumn = ({flow}) => { +export const index: FlowColumn = ({ flow }) => { + const index = useAppSelector((state) => state.flows.listIndex[flow.id]); + return {index + 1}; +}; +index.headerName = "#"; + +export const icon: FlowColumn = ({ flow }) => { return ( -
    +
    - ) -} -icon.headerName = '' -icon.sortKey = flow => getIcon(flow) - -const getIcon = (flow: Flow): string => { - if (flow.type !== "http") { - return `resource-icon-${flow.type}` - } - if (flow.websocket) { - return 'resource-icon-websocket' - } - if (!flow.response) { - return 'resource-icon-plain' - } - - var contentType = ResponseUtils.getContentType(flow.response) || '' - - if (flow.response.status_code === 304) { - return 'resource-icon-not-modified' - } - if (300 <= flow.response.status_code && flow.response.status_code < 400) { - return 'resource-icon-redirect' - } - if (contentType.indexOf('image') >= 0) { - return 'resource-icon-image' - } - if (contentType.indexOf('javascript') >= 0) { - return 'resource-icon-js' - } - if (contentType.indexOf('css') >= 0) { - return 'resource-icon-css' - } - if (contentType.indexOf('html') >= 0) { - return 'resource-icon-document' - } - - return 'resource-icon-plain' -} - -const mainPath = (flow: Flow): string => { - switch (flow.type) { - case "http": - return RequestUtils.pretty_url(flow.request) - case "tcp": - case "udp": - return `${flow.client_conn.peername.join(':')} ↔ ${flow.server_conn?.address?.join(':')}` - case "dns": - return `${flow.request.questions.map(q => `${q.name} ${q.type}`).join(", ")} = ${(flow.response?.answers.map(q => q.data).join(", ") ?? "...") || "?"}` - } -} + ); +}; +icon.headerName = ""; -export const path: FlowColumn = ({flow}) => { +export const path: FlowColumn = ({ flow }) => { let err; if (flow.error) { if (flow.error.msg === "Connection killed.") { - err = + err = ; } else { - err = + err = ; } } return ( {flow.is_replay === "request" && ( - - )} - {flow.intercepted && ( - + )} + {flow.intercepted && } {err} {flow.marked} {mainPath(flow)} - ) + ); }; -path.headerName = 'Path' -path.sortKey = flow => mainPath(flow) - -export const method: FlowColumn = ({flow}) => {method.sortKey(flow)} -method.headerName = 'Method' -method.sortKey = flow => { - switch (flow.type) { - case "http": return flow.websocket ? (flow.client_conn.tls_established ? "WSS" : "WS") : flow.request.method - case "dns": return flow.request.op_code - default: return flow.type.toUpperCase() - } -} +path.headerName = "Path"; -export const status: FlowColumn = ({flow}) => { - let color = 'darkred' +export const method: FlowColumn = ({ flow }) => ( + {getMethod(flow)} +); +method.headerName = "Method"; + +export const version: FlowColumn = ({ flow }) => ( + {getVersion(flow)} +); +version.headerName = "Version"; + +export const status: FlowColumn = ({ flow }) => { + let color = "darkred"; if ((flow.type !== "http" && flow.type != "dns") || !flow.response) - return + return ; if (100 <= flow.response.status_code && flow.response.status_code < 200) { - color = 'green' - } else if (200 <= flow.response.status_code && flow.response.status_code < 300) { - color = 'darkgreen' - } else if (300 <= flow.response.status_code && flow.response.status_code < 400) { - color = 'lightblue' - } else if (400 <= flow.response.status_code && flow.response.status_code < 500) { - color = 'red' - } else if (500 <= flow.response.status_code && flow.response.status_code < 600) { - color = 'red' + color = "green"; + } else if ( + 200 <= flow.response.status_code && + flow.response.status_code < 300 + ) { + color = "darkgreen"; + } else if ( + 300 <= flow.response.status_code && + flow.response.status_code < 400 + ) { + color = "lightblue"; + } else if ( + 400 <= flow.response.status_code && + flow.response.status_code < 500 + ) { + color = "red"; + } else if ( + 500 <= flow.response.status_code && + flow.response.status_code < 600 + ) { + color = "red"; } return ( - {status.sortKey(flow)} - ) -} -status.headerName = 'Status' -status.sortKey = flow => { - switch (flow.type) { - case "http": return flow.response?.status_code - case "dns": return flow.response?.response_code - default: return undefined - } -} - -export const size: FlowColumn = ({flow}) => { - return ( - {formatSize(getTotalSize(flow))} - ) + + {statusCode(flow)} + + ); }; -size.headerName = 'Size' -size.sortKey = flow => getTotalSize(flow) +status.headerName = "Status"; +export const size: FlowColumn = ({ flow }) => { + return {formatSize(getTotalSize(flow))}; +}; +size.headerName = "Size"; -export const time: FlowColumn = ({flow}) => { - const start = startTime(flow), end = endTime(flow); +export const time: FlowColumn = ({ flow }) => { + const start = startTime(flow); + const end = endTime(flow); return ( - {start && end ? ( - formatTimeDelta(1000 * (end - start)) - ) : ( - '...' - )} + {start && end ? formatTimeDelta(1000 * (end - start)) : "..."} - ) -} -time.headerName = 'Time' -time.sortKey = flow => { - const start = startTime(flow), end = endTime(flow); - return start && end && end - start; -} + ); +}; +time.headerName = "Time"; -export const timestamp: FlowColumn = ({flow}) => { +export const timestamp: FlowColumn = ({ flow }) => { const start = startTime(flow); return ( - {start ? ( - formatTimeStamp(start) - ) : ( - '...' - )} + {start ? formatTimeStamp(start) : "..."} - ) -} -timestamp.headerName = 'Start time' -timestamp.sortKey = flow => startTime(flow) - -const markers = { - ":red_circle:": "🔴", - ":orange_circle:": "🟠", - ":yellow_circle:": "🟡", - ":green_circle:": "🟢", - ":large_blue_circle:": "🔵", - ":purple_circle:": "🟣", - ":brown_circle:": "🟤", -} + ); +}; +timestamp.headerName = "Start time"; -export const quickactions: FlowColumn = ({flow}) => { - const dispatch = useDispatch() - let [open, setOpen] = useState(false) +export const quickactions: FlowColumn = ({ flow }) => { + const dispatch = useAppDispatch(); let resume_or_replay: ReactElement | null = null; if (flow.intercepted) { - resume_or_replay = dispatch(flowActions.resume(flow))}> - - ; + resume_or_replay = ( + dispatch(flowActions.resume(flow))} + > + + + ); } else if (canReplay(flow)) { - resume_or_replay = dispatch(flowActions.replay(flow))}> - - ; + resume_or_replay = ( + dispatch(flowActions.replay(flow))} + > + + + ); } return ( - 0}> + {resume_or_replay ?
    {resume_or_replay}
    : <>} - ) -} + ); +}; +quickactions.headerName = ""; -quickactions.headerName = '' -quickactions.sortKey = flow => 0; +export const comment: FlowColumn = ({ flow }) => { + const text = flow.comment; + return {text}; +}; +comment.headerName = "Comment"; -export default { +const FlowColumns: { [key in keyof typeof sortFunctions]: FlowColumn } = { + // parsed by web/gen/web_columns icon, + index, method, + version, path, quickactions, size, status, time, timestamp, - tls + tls, + comment, }; +export default FlowColumns; diff --git a/web/src/js/components/FlowTable/FlowRow.tsx b/web/src/js/components/FlowTable/FlowRow.tsx index 8e2d6ac0d4..ddb583bf36 100644 --- a/web/src/js/components/FlowTable/FlowRow.tsx +++ b/web/src/js/components/FlowTable/FlowRow.tsx @@ -1,45 +1,56 @@ -import React, {useCallback} from 'react' -import classnames from 'classnames' -import {Flow} from "../../flow"; -import {useAppDispatch, useAppSelector} from "../../ducks"; -import {select} from '../../ducks/flows' +import React, { useCallback } from "react"; +import classnames from "classnames"; +import { Flow } from "../../flow"; +import { useAppDispatch, useAppSelector } from "../../ducks"; +import { select } from "../../ducks/flows"; import * as columns from "./FlowColumns"; type FlowRowProps = { - flow: Flow - selected: boolean - highlighted: boolean -} + flow: Flow; + selected: boolean; + highlighted: boolean; +}; -export default React.memo(function FlowRow({flow, selected, highlighted}: FlowRowProps) { - const dispatch = useAppDispatch(), - displayColumnNames = useAppSelector(state => state.options.web_columns), - className = classnames({ - 'selected': selected, - 'highlighted': highlighted, - 'intercepted': flow.intercepted, - 'has-request': flow.type === "http" && flow.request, - 'has-response': flow.type === "http" && flow.response, - }) +export default React.memo(function FlowRow({ + flow, + selected, + highlighted, +}: FlowRowProps) { + const dispatch = useAppDispatch(); + const displayColumnNames = useAppSelector( + (state) => state.options.web_columns, + ); + const className = classnames({ + selected: selected, + highlighted: highlighted, + intercepted: flow.intercepted, + "has-request": flow.type === "http" && flow.request, + "has-response": flow.type === "http" && flow.response, + }); - const onClick = useCallback(e => { - // a bit of a hack to disable row selection for quickactions. - let node = e.target; - while (node.parentNode) { - if (node.classList.contains("col-quickactions")) - return - node = node.parentNode; - } - dispatch(select(flow.id)); - }, [flow]); + const onClick = useCallback( + (e) => { + // a bit of a hack to disable row selection for quickactions. + let node = e.target; + while (node.parentNode) { + if (node.classList.contains("col-quickactions")) return; + node = node.parentNode; + } + dispatch(select(flow.id)); + }, + [flow], + ); - const displayColumns = displayColumnNames.map(x => columns[x]).filter(x => x).concat(columns.quickactions); + const displayColumns = displayColumnNames + .map((x) => columns[x]) + .filter((x) => x) + .concat(columns.quickactions); return ( - {displayColumns.map(Column => ( - + {displayColumns.map((Column) => ( + ))} - ) -}) + ); +}); diff --git a/web/src/js/components/FlowTable/FlowTableHead.tsx b/web/src/js/components/FlowTable/FlowTableHead.tsx index e06dbd0f09..584d21a58e 100644 --- a/web/src/js/components/FlowTable/FlowTableHead.tsx +++ b/web/src/js/components/FlowTable/FlowTableHead.tsx @@ -1,30 +1,47 @@ -import * as React from "react" -import classnames from 'classnames' -import * as columns from './FlowColumns' +import * as React from "react"; +import classnames from "classnames"; +import * as columns from "./FlowColumns"; -import {setSort} from '../../ducks/flows' -import {useAppDispatch, useAppSelector} from "../../ducks"; +import { setSort } from "../../ducks/flows"; +import { useAppDispatch, useAppSelector } from "../../ducks"; export default React.memo(function FlowTableHead() { - const dispatch = useAppDispatch(), - sortDesc = useAppSelector(state => state.flows.sort.desc), - sortColumn = useAppSelector(state => state.flows.sort.column), - displayColumnNames = useAppSelector(state => state.options.web_columns); + const dispatch = useAppDispatch(); + const sortDesc = useAppSelector((state) => state.flows.sort.desc); + const sortColumn = useAppSelector((state) => state.flows.sort.column); + const displayColumnNames = useAppSelector( + (state) => state.options.web_columns, + ); - const sortType = sortDesc ? 'sort-desc' : 'sort-asc' - const displayColumns = displayColumnNames.map(x => columns[x]).filter(x => x).concat(columns.quickactions); + const sortType = sortDesc ? "sort-desc" : "sort-asc"; + const displayColumns = displayColumnNames + .map((x) => columns[x]) + .filter((x) => x) + .concat(columns.quickactions); return ( - {displayColumns.map(Column => ( - ( + dispatch(setSort( - Column.name === sortColumn && sortDesc ? undefined : Column.name, - Column.name !== sortColumn ? false : !sortDesc))}> + onClick={() => + dispatch( + setSort( + Column.name === sortColumn && sortDesc + ? undefined + : Column.name, + Column.name !== sortColumn ? false : !sortDesc, + ), + ) + } + > {Column.headerName} ))} - ) -}) + ); +}); diff --git a/web/src/js/components/FlowView.tsx b/web/src/js/components/FlowView.tsx index 4c69efa3e5..9fef0290ca 100644 --- a/web/src/js/components/FlowView.tsx +++ b/web/src/js/components/FlowView.tsx @@ -1,24 +1,30 @@ -import * as React from "react" -import {FunctionComponent} from "react" -import {Request, Response} from './FlowView/HttpMessages' -import {Request as DnsRequest, Response as DnsResponse} from './FlowView/DnsMessages' -import Connection from './FlowView/Connection' -import Error from "./FlowView/Error" -import Timing from "./FlowView/Timing" -import WebSocket from "./FlowView/WebSocket" - -import {selectTab} from '../ducks/ui/flow' -import {useAppDispatch, useAppSelector} from "../ducks"; -import {Flow} from "../flow"; +import * as React from "react"; +import { FunctionComponent } from "react"; +import { Request, Response } from "./FlowView/HttpMessages"; +import { + Request as DnsRequest, + Response as DnsResponse, +} from "./FlowView/DnsMessages"; +import Connection from "./FlowView/Connection"; +import Error from "./FlowView/Error"; +import Timing from "./FlowView/Timing"; +import WebSocket from "./FlowView/WebSocket"; +import Comment from "./FlowView/Comment"; +import { selectTab } from "../ducks/ui/flow"; +import { useAppDispatch, useAppSelector } from "../ducks"; +import { Flow } from "../flow"; import classnames from "classnames"; import TcpMessages from "./FlowView/TcpMessages"; import UdpMessages from "./FlowView/UdpMessages"; +import * as flowsActions from "../ducks/flows"; type TabProps = { - flow: Flow -} + flow: Flow; +}; -export const allTabs: { [name: string]: FunctionComponent & { displayName: string } } = { +export const allTabs: { + [name: string]: FunctionComponent & { displayName: string }; +} = { request: Request, response: Response, error: Error, @@ -29,45 +35,55 @@ export const allTabs: { [name: string]: FunctionComponent & { displayN udpmessages: UdpMessages, dnsrequest: DnsRequest, dnsresponse: DnsResponse, -} + comment: Comment, +}; export function tabsForFlow(flow: Flow): string[] { let tabs; switch (flow.type) { case "http": - tabs = ['request', 'response', 'websocket'].filter(k => flow[k]) - break + tabs = ["request", "response", "websocket"].filter((k) => flow[k]); + break; case "tcp": - tabs = ["tcpmessages"] - break + tabs = ["tcpmessages"]; + break; case "udp": - tabs = ["udpmessages"] - break + tabs = ["udpmessages"]; + break; case "dns": - tabs = ['request', 'response'].filter(k => flow[k]).map(s => "dns" + s) - break + tabs = ["request", "response"] + .filter((k) => flow[k]) + .map((s) => "dns" + s); + break; } - if (flow.error) - tabs.push("error") - tabs.push("connection") - tabs.push("timing") + if (flow.error) tabs.push("error"); + tabs.push("connection"); + tabs.push("timing"); + tabs.push("comment"); return tabs; } export default function FlowView() { - const dispatch = useAppDispatch(), - flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]]), - tabs = tabsForFlow(flow); + const dispatch = useAppDispatch(); + const flow = useAppSelector( + (state) => state.flows.byId[state.flows.selected[0]], + ); + let active = useAppSelector((state) => state.ui.flow.tab); + + if (flow == undefined) { + return <>; + } + + const tabs = tabsForFlow(flow); - let active = useAppSelector(state => state.ui.flow.tab) if (tabs.indexOf(active) < 0) { - if (active === 'response' && flow.error) { - active = 'error' - } else if (active === 'error' && "response" in flow) { - active = 'response' + if (active === "response" && flow.error) { + active = "error"; + } else if (active === "error" && "response" in flow) { + active = "response"; } else { - active = tabs[0] + active = tabs[0]; } } const Tab = allTabs[active]; @@ -75,17 +91,28 @@ export default function FlowView() { return ( - ) + ); } diff --git a/web/src/js/components/FlowView/Comment.tsx b/web/src/js/components/FlowView/Comment.tsx new file mode 100644 index 0000000000..0f3c7c66e3 --- /dev/null +++ b/web/src/js/components/FlowView/Comment.tsx @@ -0,0 +1,24 @@ +import { Flow } from "../../flow"; +import * as React from "react"; +import ValueEditor from "../editors/ValueEditor"; +import { useAppDispatch } from "../../ducks"; +import * as flowActions from "../../ducks/flows"; + +export default function Comment({ flow }: { flow: Flow }) { + const dispatch = useAppDispatch(); + + return ( +
    +

    Comment

    + { + dispatch(flowActions.update(flow, { comment })); + }} + placeholder="empty" + /> +
    + ); +} +Comment.displayName = "Comment"; diff --git a/web/src/js/components/FlowView/Connection.tsx b/web/src/js/components/FlowView/Connection.tsx index 48493ca12e..63a99753e5 100644 --- a/web/src/js/components/FlowView/Connection.tsx +++ b/web/src/js/components/FlowView/Connection.tsx @@ -1,152 +1,170 @@ -import * as React from "react" -import {formatTimeStamp} from '../../utils' -import {Client, Flow, Server} from '../../flow' - +import * as React from "react"; +import { formatTimeStamp } from "../../utils"; +import { Client, Flow, Server } from "../../flow"; type ConnectionInfoProps = { - conn: Client | Server -} + conn: Client | Server; +}; -export function ConnectionInfo({conn}: ConnectionInfoProps) { +export function ConnectionInfo({ conn }: ConnectionInfoProps) { let address_info: JSX.Element | null = null; if ("address" in conn) { // Server - address_info = <> - - Address: - {conn.address?.join(':')} - - {conn.peername && ( - - Resolved address: - {conn.peername.join(':')} - - )} - {conn.sockname && ( + address_info = ( + <> - Source address: - {conn.sockname.join(':')} + Address: + {conn.address?.join(":")} - )} - ; + {conn.peername && ( + + Resolved address: + {conn.peername.join(":")} + + )} + {conn.sockname && ( + + Source address: + {conn.sockname.join(":")} + + )} + + ); } else { // Client if (conn.peername?.[0]) { - address_info = <> - - Address: - {conn.peername?.join(':')} - - + address_info = ( + <> + + Address: + {conn.peername?.join(":")} + + + ); } - } return ( - {address_info} - {conn.sni ? ( - - - - - ): null} - {conn.alpn ? ( - - - - - ) : null} - {conn.tls_version ? ( - - - - - ): null} - {conn.cipher ? ( - - - - - ): null} + {address_info} + {conn.sni ? ( + + + + + ) : null} + {conn.alpn ? ( + + + + + ) : null} + {conn.tls_version ? ( + + + + + ) : null} + {conn.cipher ? ( + + + + + ) : null}
    SNI:{conn.sni}
    ALPN:{conn.alpn}
    TLS Version:{conn.tls_version}
    TLS Cipher:{conn.cipher}
    + SNI: + {conn.sni}
    + ALPN: + {conn.alpn}
    TLS Version:{conn.tls_version}
    TLS Cipher:{conn.cipher}
    - ) + ); } function attrList(data: [string, string][]): JSX.Element { - return
    - {data.map(([k, v]) => - -
    {k}
    -
    {v}
    -
    - )} -
    + return ( +
    + {data.map(([k, v]) => ( + +
    {k}
    +
    {v}
    +
    + ))} +
    + ); } -export function CertificateInfo({flow}: { flow: Flow }): JSX.Element { +export function CertificateInfo({ flow }: { flow: Flow }): JSX.Element { const cert = flow.server_conn?.cert; - if (!cert) - return <>; + if (!cert) return <>; - return <> -

    Server Certificate

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Type{cert.keyinfo[0]}, {cert.keyinfo[1]} bits
    SHA256 digest{cert.sha256}
    Valid from{formatTimeStamp(cert.notbefore, {milliseconds: false})}
    Valid to{formatTimeStamp(cert.notafter, {milliseconds: false})}
    Subject Alternative Names{cert.altnames.join(", ")}
    Subject{attrList(cert.subject)}
    Issuer{attrList(cert.issuer)}
    Serial{cert.serial}
    - + return ( + <> +

    Server Certificate

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Type + {cert.keyinfo[0]}, {cert.keyinfo[1]} bits +
    SHA256 digest{cert.sha256}
    Valid from + {formatTimeStamp(cert.notbefore, { + milliseconds: false, + })} +
    Valid to + {formatTimeStamp(cert.notafter, { + milliseconds: false, + })} +
    Subject Alternative Names{cert.altnames.join(", ")}
    Subject{attrList(cert.subject)}
    Issuer{attrList(cert.issuer)}
    Serial{cert.serial}
    + + ); } -export default function Connection({flow}: { flow: Flow }) { +export default function Connection({ flow }: { flow: Flow }) { return (

    Client Connection

    - + - { - flow.server_conn?.address && + {flow.server_conn?.address && ( <>

    Server Connection

    - + - } + )} - +
    - ) + ); } -Connection.displayName = "Connection" +Connection.displayName = "Connection"; diff --git a/web/src/js/components/FlowView/DnsMessages.tsx b/web/src/js/components/FlowView/DnsMessages.tsx index 8cc51ca1bc..c3fc74e7a6 100644 --- a/web/src/js/components/FlowView/DnsMessages.tsx +++ b/web/src/js/components/FlowView/DnsMessages.tsx @@ -1,108 +1,116 @@ -import * as React from "react" +import * as React from "react"; -import {useAppSelector} from "../../ducks"; -import {DNSFlow, DNSMessage, DNSResourceRecord} from '../../flow' +import { useAppSelector } from "../../ducks"; +import { DNSFlow, DNSMessage, DNSResourceRecord } from "../../flow"; const Summary: React.FC<{ - message: DNSMessage -}> = ({message}) => ( + message: DNSMessage; +}> = ({ message }) => (
    {message.query ? message.op_code : message.response_code}   {message.truncation ? "(Truncated)" : ""}
    -) +); const Questions: React.FC<{ - message: DNSMessage -}> = ({message}) => ( + message: DNSMessage; +}> = ({ message }) => ( <>
    {message.recursion_desired ? "Recursive " : ""}Question
    - - - - - + + + + + - {message.questions.map((question, index) => ( - - - - - - ))} + {message.questions.map((question, index) => ( + + + + + + ))}
    NameTypeClass
    NameTypeClass
    {question.name}{question.type}{question.class}
    {question.name}{question.type}{question.class}
    -) +); const ResourceRecords: React.FC<{ - name: string - values: DNSResourceRecord[] -}> = ({name, values}) => ( + name: string; + values: DNSResourceRecord[]; +}> = ({ name, values }) => ( <>
    {name}
    - {values.length > 0 - ? + {values.length > 0 ? ( +
    - - - - - - - + + + + + + + - {values.map((rr, index) => ( - - - - - - - - ))} + {values.map((rr, index) => ( + + + + + + + + ))}
    NameTypeClassTTLData
    NameTypeClassTTLData
    {rr.name}{rr.type}{rr.class}{rr.ttl}{rr.data}
    {rr.name}{rr.type}{rr.class}{rr.ttl}{rr.data}
    - : "—" - } + ) : ( + "—" + )} -) +); const Message: React.FC<{ - type: "request" | "response" - message: DNSMessage -}> = ({type, message}) => ( + type: "request" | "response"; + message: DNSMessage; +}> = ({ type, message }) => (
    - +
    - -
    + +
    -
    - -
    - + name={`${message.authoritative_answer ? "Authoritative " : ""}${ + message.recursion_available ? "Recursive " : "" + }Answer`} + values={message.answers} + /> +
    + +
    +
    -) +); export function Request() { - const flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]]) as DNSFlow; - return ; + const flow = useAppSelector( + (state) => state.flows.byId[state.flows.selected[0]], + ) as DNSFlow; + return ; } -Request.displayName = "Request" +Request.displayName = "Request"; export function Response() { - const flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]]) as DNSFlow & { response: DNSMessage } - return ; + const flow = useAppSelector( + (state) => state.flows.byId[state.flows.selected[0]], + ) as DNSFlow & { response: DNSMessage }; + return ; } -Response.displayName = "Response" +Response.displayName = "Response"; diff --git a/web/src/js/components/FlowView/Error.tsx b/web/src/js/components/FlowView/Error.tsx index e99e2fa327..9eb4589dc3 100644 --- a/web/src/js/components/FlowView/Error.tsx +++ b/web/src/js/components/FlowView/Error.tsx @@ -1,12 +1,12 @@ -import {HTTPFlow} from "../../flow"; -import {formatTimeStamp} from "../../utils"; +import { HTTPFlow } from "../../flow"; +import { formatTimeStamp } from "../../utils"; import * as React from "react"; type ErrorProps = { - flow: HTTPFlow & { error: Error } -} + flow: HTTPFlow & { error: Error }; +}; -export default function Error({flow}: ErrorProps) { +export default function Error({ flow }: ErrorProps) { return (
    @@ -16,6 +16,6 @@ export default function Error({flow}: ErrorProps) {
    - ) + ); } Error.displayName = "Error"; diff --git a/web/src/js/components/FlowView/HttpMessages.tsx b/web/src/js/components/FlowView/HttpMessages.tsx index 1eaab2f478..82012e9edc 100644 --- a/web/src/js/components/FlowView/HttpMessages.tsx +++ b/web/src/js/components/FlowView/HttpMessages.tsx @@ -1,21 +1,25 @@ -import * as React from "react" - -import {isValidHttpVersion, MessageUtils, parseUrl, RequestUtils} from '../../flow/utils' -import ValidateEditor from '../editors/ValidateEditor' -import ValueEditor from '../editors/ValueEditor' - -import {useAppDispatch, useAppSelector} from "../../ducks"; -import {HTTPFlow, HTTPMessage, HTTPResponse} from '../../flow' -import * as flowActions from '../../ducks/flows' +import * as React from "react"; + +import { + isValidHttpVersion, + MessageUtils, + parseUrl, + RequestUtils, +} from "../../flow/utils"; +import ValidateEditor from "../editors/ValidateEditor"; +import ValueEditor from "../editors/ValueEditor"; + +import { useAppDispatch, useAppSelector } from "../../ducks"; +import { HTTPFlow, HTTPMessage, HTTPResponse } from "../../flow"; +import * as flowActions from "../../ducks/flows"; import KeyValueListEditor from "../editors/KeyValueListEditor"; import HttpMessage from "../contentviews/HttpMessage"; - type RequestLineProps = { - flow: HTTPFlow, -} + flow: HTTPFlow; +}; -function RequestLine({flow}: RequestLineProps) { +function RequestLine({ flow }: RequestLineProps) { const dispatch = useAppDispatch(); return ( @@ -23,67 +27,97 @@ function RequestLine({flow}: RequestLineProps) {
    dispatch(flowActions.update(flow, {request: {method}}))} - isValid={method => method.length > 0} + onEditDone={(method) => + dispatch( + flowActions.update(flow, { request: { method } }), + ) + } + isValid={(method) => method.length > 0} />   dispatch(flowActions.update(flow, {request: {path: '', ...parseUrl(url)}}))} - isValid={url => !!parseUrl(url)?.host} + onEditDone={(url) => + dispatch( + flowActions.update(flow, { + request: { path: "", ...parseUrl(url) }, + }), + ) + } + isValid={(url) => !!parseUrl(url)?.host} />   dispatch(flowActions.update(flow, {request: {http_version}}))} + onEditDone={(http_version) => + dispatch( + flowActions.update(flow, { + request: { http_version }, + }), + ) + } isValid={isValidHttpVersion} />
    - ) + ); } - type ResponseLineProps = { - flow: HTTPFlow & { response: HTTPResponse }, -} + flow: HTTPFlow & { response: HTTPResponse }; +}; -function ResponseLine({flow}: ResponseLineProps) { +function ResponseLine({ flow }: ResponseLineProps) { const dispatch = useAppDispatch(); return (
    dispatch(flowActions.update(flow, {response: {http_version: nextVer}}))} + onEditDone={(nextVer) => + dispatch( + flowActions.update(flow, { + response: { http_version: nextVer }, + }), + ) + } isValid={isValidHttpVersion} />   dispatch(flowActions.update(flow, {response: {code: parseInt(code)}}))} - isValid={code => /^\d+$/.test(code)} + content={flow.response.status_code + ""} + onEditDone={(code) => + dispatch( + flowActions.update(flow, { + response: { code: parseInt(code) }, + }), + ) + } + isValid={(code) => /^\d+$/.test(code)} /> - {flow.response.http_version !== "HTTP/2.0" && - <>  - dispatch(flowActions.update(flow, {response: {msg}}))} - /> - - } - + {flow.response.http_version !== "HTTP/2.0" && ( + <> +   + + dispatch( + flowActions.update(flow, { response: { msg } }), + ) + } + /> + + )}
    - ) + ); } - type HeadersProps = { - flow: HTTPFlow, - message: HTTPMessage -} + flow: HTTPFlow; + message: HTTPMessage; +}; -function Headers({flow, message}: HeadersProps) { +function Headers({ flow, message }: HeadersProps) { const dispatch = useAppDispatch(); const part = flow.request === message ? "request" : "response"; @@ -91,58 +125,73 @@ function Headers({flow, message}: HeadersProps) { dispatch(flowActions.update(flow, {[part]: {headers}}))} + onChange={(headers) => + dispatch(flowActions.update(flow, { [part]: { headers } })) + } /> ); } type TrailersProps = { - flow: HTTPFlow, - message: HTTPMessage -} + flow: HTTPFlow; + message: HTTPMessage; +}; -function Trailers({flow, message}: TrailersProps) { +function Trailers({ flow, message }: TrailersProps) { const dispatch = useAppDispatch(); const part = flow.request === message ? "request" : "response"; - const hasTrailers = !!MessageUtils.get_first_header(message, /^trailer$/i) + const hasTrailers = !!MessageUtils.get_first_header(message, /^trailer$/i); - if (!hasTrailers) - return null; + if (!hasTrailers) return null; - return <> -
    -
    HTTP Trailers
    - dispatch(flowActions.update(flow, {[part]: {trailers}}))} - /> - ; + return ( + <> +
    +
    HTTP Trailers
    + + dispatch(flowActions.update(flow, { [part]: { trailers } })) + } + /> + + ); } -const Message = React.memo(function Message({flow, message}: { flow: HTTPFlow, message: HTTPMessage }) { +const Message = React.memo(function Message({ + flow, + message, +}: { + flow: HTTPFlow; + message: HTTPMessage; +}) { const part = flow.request === message ? "request" : "response"; const FirstLine = flow.request === message ? RequestLine : ResponseLine; return (
    - - -
    - - + + +
    + +
    - ) + ); }); export function Request() { - const flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]]) as HTTPFlow; - return ; + const flow = useAppSelector( + (state) => state.flows.byId[state.flows.selected[0]], + ) as HTTPFlow; + return ; } -Request.displayName = "Request" +Request.displayName = "Request"; export function Response() { - const flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]]) as HTTPFlow & { response: HTTPResponse } - return ; + const flow = useAppSelector( + (state) => state.flows.byId[state.flows.selected[0]], + ) as HTTPFlow & { response: HTTPResponse }; + return ; } -Response.displayName = "Response" +Response.displayName = "Response"; diff --git a/web/src/js/components/FlowView/Messages.tsx b/web/src/js/components/FlowView/Messages.tsx index 89ae95cdaa..f135a6a878 100644 --- a/web/src/js/components/FlowView/Messages.tsx +++ b/web/src/js/components/FlowView/Messages.tsx @@ -1,59 +1,88 @@ -import {Flow, MessagesMeta} from "../../flow"; -import {useAppDispatch, useAppSelector} from "../../ducks"; +import { Flow, MessagesMeta } from "../../flow"; +import { useAppDispatch, useAppSelector } from "../../ducks"; import * as React from "react"; -import {useCallback, useMemo, useState} from "react"; -import {ContentViewData, useContent} from "../contentviews/useContent"; -import {MessageUtils} from "../../flow/utils"; +import { useCallback, useMemo, useState } from "react"; +import { ContentViewData, useContent } from "../contentviews/useContent"; +import { MessageUtils } from "../../flow/utils"; import ViewSelector from "../contentviews/ViewSelector"; -import {setContentViewFor} from "../../ducks/ui/flow"; -import {formatTimeStamp} from "../../utils"; +import { setContentViewFor } from "../../ducks/ui/flow"; +import { formatTimeStamp } from "../../utils"; import LineRenderer from "../contentviews/LineRenderer"; type MessagesPropTypes = { - flow: Flow - messages_meta: MessagesMeta -} + flow: Flow; + messages_meta: MessagesMeta; +}; -export default function Messages({flow, messages_meta}: MessagesPropTypes) { +export default function Messages({ flow, messages_meta }: MessagesPropTypes) { const dispatch = useAppDispatch(); - const contentView = useAppSelector(state => state.ui.flow.contentViewFor[flow.id + "messages"] || "Auto"); - let [maxLines, setMaxLines] = useState(useAppSelector(state => state.options.content_view_lines_cutoff)); - const showMore = useCallback(() => setMaxLines(Math.max(1024, maxLines * 2)), [maxLines]); + const contentView = useAppSelector( + (state) => state.ui.flow.contentViewFor[flow.id + "messages"] || "Auto", + ); + const [maxLines, setMaxLines] = useState( + useAppSelector((state) => state.options.content_view_lines_cutoff), + ); + const showMore = useCallback( + () => setMaxLines(Math.max(1024, maxLines * 2)), + [maxLines], + ); const content = useContent( MessageUtils.getContentURL(flow, "messages", contentView, maxLines + 1), - flow.id + messages_meta.count + flow.id + messages_meta.count, ); - const messages = useMemo(() => { - if (content) { - try { - return JSON.parse(content) - } catch (e) { - const err: ContentViewData = {"description": "Network Error", lines: [[["error", `${content}`]]]}; - return err; + const messages = + useMemo(() => { + if (content) { + try { + return JSON.parse(content); + } catch (e) { + const err: ContentViewData[] = [ + { + description: "Network Error", + lines: [[["error", `${content}`]]], + }, + ]; + return err; + } } - } - }, [content]) || []; + }, [content]) || []; + + let remainingLines = maxLines; return (
    {messages_meta.count} Messages
    - dispatch(setContentViewFor(flow.id + "messages", cv))}/> + + dispatch(setContentViewFor(flow.id + "messages", cv)) + } + />
    {messages.map((d: ContentViewData, i) => { - const className = `fa fa-fw fa-arrow-${d.from_client ? "right text-primary" : "left text-danger"}`; - const renderer =
    - - - {d.timestamp && formatTimeStamp(d.timestamp)} - - -
    ; - maxLines -= d.lines.length; + const className = `fa fa-fw fa-arrow-${ + d.from_client ? "right text-primary" : "left text-danger" + }`; + const renderer = ( +
    + + + + {d.timestamp && formatTimeStamp(d.timestamp)} + + + +
    + ); + remainingLines -= d.lines.length; return renderer; })}
    - ) + ); } diff --git a/web/src/js/components/FlowView/TcpMessages.tsx b/web/src/js/components/FlowView/TcpMessages.tsx index ce04beb884..9391dd2bc0 100644 --- a/web/src/js/components/FlowView/TcpMessages.tsx +++ b/web/src/js/components/FlowView/TcpMessages.tsx @@ -1,14 +1,12 @@ -import {TCPFlow} from "../../flow"; +import { TCPFlow } from "../../flow"; import * as React from "react"; import Messages from "./Messages"; - -export default function TcpMessages({flow}: { flow: TCPFlow }) { +export default function TcpMessages({ flow }: { flow: TCPFlow }) { return (
    -

    TCP Data

    - +
    - ) + ); } -TcpMessages.displayName = "TCP Messages" +TcpMessages.displayName = "Stream Data"; diff --git a/web/src/js/components/FlowView/Timing.tsx b/web/src/js/components/FlowView/Timing.tsx index 864d3a4943..656ee37610 100644 --- a/web/src/js/components/FlowView/Timing.tsx +++ b/web/src/js/components/FlowView/Timing.tsx @@ -1,14 +1,14 @@ -import {Flow} from "../../flow"; +import { Flow } from "../../flow"; import * as React from "react"; -import {formatTimeDelta, formatTimeStamp} from "../../utils"; +import { formatTimeDelta, formatTimeStamp } from "../../utils"; export type TimeStampProps = { - t: number, - deltaTo?: number, - title: string, -} + t: number; + deltaTo?: number; + title: string; +}; -export function TimeStamp({t, deltaTo, title}: TimeStampProps) { +export function TimeStamp({ t, deltaTo, title }: TimeStampProps) { return t ? ( {title}: @@ -22,68 +22,79 @@ export function TimeStamp({t, deltaTo, title}: TimeStampProps) { ) : ( - - ) + + ); } -export default function Timing({flow}: { flow: Flow }) { +export default function Timing({ flow }: { flow: Flow }) { let ref: number; if (flow.type === "http") { - ref = flow.request.timestamp_start + ref = flow.request.timestamp_start; } else { - ref = flow.client_conn.timestamp_start + ref = flow.client_conn.timestamp_start; } const timestamps: Partial[] = [ { title: "Server conn. initiated", t: flow.server_conn?.timestamp_start, - deltaTo: ref - }, { + deltaTo: ref, + }, + { title: "Server conn. TCP handshake", t: flow.server_conn?.timestamp_tcp_setup, - deltaTo: ref - }, { + deltaTo: ref, + }, + { title: "Server conn. TLS handshake", t: flow.server_conn?.timestamp_tls_setup, - deltaTo: ref - }, { + deltaTo: ref, + }, + { title: "Server conn. closed", t: flow.server_conn?.timestamp_end, - deltaTo: ref - }, { + deltaTo: ref, + }, + { title: "Client conn. established", t: flow.client_conn.timestamp_start, - deltaTo: flow.type === "http" ? ref : undefined - }, { + deltaTo: flow.type === "http" ? ref : undefined, + }, + { title: "Client conn. TLS handshake", t: flow.client_conn.timestamp_tls_setup, - deltaTo: ref - }, { + deltaTo: ref, + }, + { title: "Client conn. closed", t: flow.client_conn.timestamp_end, - deltaTo: ref + deltaTo: ref, }, - ] + ]; if (flow.type === "http") { - timestamps.push(...[ - { - title: "First request byte", - t: flow.request.timestamp_start - }, { - title: "Request complete", - t: flow.request.timestamp_end, - deltaTo: ref - }, { - title: "First response byte", - t: flow.response?.timestamp_start, - deltaTo: ref - }, { - title: "Response complete", - t: flow.response?.timestamp_end, - deltaTo: ref - } - ]); + timestamps.push( + ...[ + { + title: "First request byte", + t: flow.request.timestamp_start, + }, + { + title: "Request complete", + t: flow.request.timestamp_end, + deltaTo: ref, + }, + { + title: "First response byte", + t: flow.response?.timestamp_start, + deltaTo: ref, + }, + { + title: "Response complete", + t: flow.response?.timestamp_end, + deltaTo: ref, + }, + ], + ); } return ( @@ -91,13 +102,20 @@ export default function Timing({flow}: { flow: Flow }) {

    Timing

    - {timestamps - .filter((v): v is TimeStampProps => !!v.t) - .sort((a, b) => a.t - b.t) - .map(props => )} + {timestamps + .filter((v): v is TimeStampProps => !!v.t) + .sort((a, b) => a.t - b.t) + .map(({ title, t, deltaTo }) => ( + + ))}
    - ) + ); } -Timing.displayName = "Timing" +Timing.displayName = "Timing"; diff --git a/web/src/js/components/FlowView/UdpMessages.tsx b/web/src/js/components/FlowView/UdpMessages.tsx index d40900406f..79e7c3b79c 100644 --- a/web/src/js/components/FlowView/UdpMessages.tsx +++ b/web/src/js/components/FlowView/UdpMessages.tsx @@ -1,14 +1,12 @@ -import {UDPFlow} from "../../flow"; +import { UDPFlow } from "../../flow"; import * as React from "react"; import Messages from "./Messages"; - -export default function UdpMessages({flow}: { flow: UDPFlow }) { +export default function UdpMessages({ flow }: { flow: UDPFlow }) { return (
    -

    UDP Data

    - +
    - ) + ); } -UdpMessages.displayName = "UDP Messages" +UdpMessages.displayName = "Datagrams"; diff --git a/web/src/js/components/FlowView/WebSocket.tsx b/web/src/js/components/FlowView/WebSocket.tsx index eecea1db14..67ea6917b4 100644 --- a/web/src/js/components/FlowView/WebSocket.tsx +++ b/web/src/js/components/FlowView/WebSocket.tsx @@ -1,32 +1,39 @@ -import {HTTPFlow, WebSocketData} from "../../flow"; +import { HTTPFlow, WebSocketData } from "../../flow"; import * as React from "react"; -import {formatTimeStamp} from "../../utils"; +import { formatTimeStamp } from "../../utils"; import Messages from "./Messages"; - -export default function WebSocket({flow}: { flow: HTTPFlow & { websocket: WebSocketData } }) { +export default function WebSocket({ + flow, +}: { + flow: HTTPFlow & { websocket: WebSocketData }; +}) { return (

    WebSocket

    - - + +
    - ) + ); } -WebSocket.displayName = "WebSocket" - +WebSocket.displayName = "WebSocket"; -function CloseSummary({websocket}: { websocket: WebSocketData }) { - if (!websocket.timestamp_end) - return null; - const reason = websocket.close_reason ? `(${websocket.close_reason})` : "" - return
    - -   - Closed by {websocket.closed_by_client ? "client" : "server"} with code {websocket.close_code} {reason}. - - - {formatTimeStamp(websocket.timestamp_end)} - -
    +function CloseSummary({ websocket }: { websocket: WebSocketData }) { + if (!websocket.timestamp_end) return null; + const reason = websocket.close_reason ? `(${websocket.close_reason})` : ""; + return ( +
    + +   Closed by {websocket.closed_by_client + ? "client" + : "server"}{" "} + with code {websocket.close_code} {reason}. + + {formatTimeStamp(websocket.timestamp_end)} + +
    + ); } diff --git a/web/src/js/components/Footer.tsx b/web/src/js/components/Footer.tsx index a6f91a537a..d9cacd8251 100644 --- a/web/src/js/components/Footer.tsx +++ b/web/src/js/components/Footer.tsx @@ -1,14 +1,28 @@ -import * as React from 'react' -import {formatSize} from '../utils' -import HideInStatic from '../components/common/HideInStatic' -import {useAppSelector} from "../ducks"; +import * as React from "react"; +import { formatSize } from "../utils"; +import HideInStatic from "../components/common/HideInStatic"; +import { useAppSelector } from "../ducks"; export default function Footer() { - const version = useAppSelector(state => state.backendState.version); - let { - mode, intercept, showhost, upstream_cert, rawtcp, http2, websocket, anticache, anticomp, - stickyauth, stickycookie, stream_large_bodies, listen_host, listen_port, server, ssl_insecure - } = useAppSelector(state => state.options); + const version = useAppSelector((state) => state.backendState.version); + const { + mode, + intercept, + showhost, + upstream_cert, + rawtcp, + http2, + websocket, + anticache, + anticomp, + stickyauth, + stickycookie, + stream_large_bodies, + listen_host, + listen_port, + server, + ssl_insecure, + } = useAppSelector((state) => state.options); return (
    @@ -16,54 +30,56 @@ export default function Footer() { {mode.join(",")} )} {intercept && ( - Intercept: {intercept} + + Intercept: {intercept} + )} {ssl_insecure && ( ssl_insecure )} - {showhost && ( - showhost - )} + {showhost && showhost} {!upstream_cert && ( no-upstream-cert )} - {!rawtcp && ( - no-raw-tcp - )} - {!http2 && ( - no-http2 - )} + {!rawtcp && no-raw-tcp} + {!http2 && no-http2} {!websocket && ( no-websocket )} {anticache && ( anticache )} - {anticomp && ( - anticomp - )} + {anticomp && anticomp} {stickyauth && ( - stickyauth: {stickyauth} + + stickyauth: {stickyauth} + )} {stickycookie && ( - stickycookie: {stickycookie} + + stickycookie: {stickycookie} + )} {stream_large_bodies && ( - stream: {formatSize(stream_large_bodies)} + + stream: {formatSize(stream_large_bodies)} + )}
    - { - server && ( - - {listen_host || "*"}:{listen_port || 8080} - ) - } + {server && ( + + {listen_host || "*"}:{listen_port || 8080} + + )} - mitmproxy {version} - + mitmproxy {version} +
    - ) + ); } diff --git a/web/src/js/components/Header.tsx b/web/src/js/components/Header.tsx index c00d55ed88..fe06472207 100644 --- a/web/src/js/components/Header.tsx +++ b/web/src/js/components/Header.tsx @@ -1,64 +1,91 @@ -import React, {useState} from 'react' -import classnames from 'classnames' -import StartMenu from './Header/StartMenu' -import OptionMenu from './Header/OptionMenu' -import FileMenu from './Header/FileMenu' -import FlowMenu from './Header/FlowMenu' -import ConnectionIndicator from "./Header/ConnectionIndicator" -import HideInStatic from './common/HideInStatic' -import {useAppSelector} from "../ducks"; +import React, { useEffect, useState } from "react"; +import classnames from "classnames"; +import FileMenu from "./Header/FileMenu"; +import ConnectionIndicator from "./Header/ConnectionIndicator"; +import HideInStatic from "./common/HideInStatic"; +import CaptureMenu from "./Header/CaptureMenu"; +import { useAppDispatch, useAppSelector } from "../ducks"; +import FlowListMenu from "./Header/FlowListMenu"; +import OptionMenu from "./Header/OptionMenu"; +import FlowMenu from "./Header/FlowMenu"; +import { Menu } from "./ProxyApp"; +import { shallowEqual } from "react-redux"; +import { Tab, setCurrent } from "../ducks/ui/tabs"; -interface Menu { - (): JSX.Element; - - title: string; -} +const tabs: { [key in Tab]: Menu } = { + [Tab.Capture]: CaptureMenu, + [Tab.FlowList]: FlowListMenu, + [Tab.Options]: OptionMenu, + [Tab.Flow]: FlowMenu, +}; export default function Header() { - const selectedFlows = useAppSelector(state => state.flows.selected.filter(id => id in state.flows.byId)), - [ActiveMenu, setActiveMenu] = useState(() => StartMenu), - [wasFlowSelected, setWasFlowSelected] = useState(false); + const dispatch = useAppDispatch(); + const currentTab = useAppSelector((state) => state.ui.tabs.current); + const selectedFlows = useAppSelector( + (state) => state.flows.selected.filter((id) => id in state.flows.byId), + shallowEqual, + ); + const [wasFlowSelected, setWasFlowSelected] = useState(false); + const hasFlows = useAppSelector((state) => state.flows.list.length > 0); + const isInitialTab = useAppSelector((state) => state.ui.tabs.isInitial); - let entries: Menu[] = [StartMenu, OptionMenu]; + const entries: Tab[] = [Tab.Capture, Tab.FlowList, Tab.Options]; if (selectedFlows.length > 0) { - if (!wasFlowSelected) { - setActiveMenu(() => FlowMenu); - setWasFlowSelected(true); - } - entries.push(FlowMenu) - } else { - if (wasFlowSelected) { - setWasFlowSelected(false); + entries.push(Tab.Flow); + } + + // Switch to "Flow List" when the first flow appears. + useEffect(() => { + if (hasFlows && isInitialTab) { + dispatch(setCurrent(Tab.FlowList)); } - if (ActiveMenu === FlowMenu) { - setActiveMenu(() => StartMenu) + }, [hasFlows]); + + // Switch to "Flow" tab if we just selected a new flow. + useEffect(() => { + if (selectedFlows.length > 0 && !wasFlowSelected) { + // User just clicked on a flow without having previously selected one. + dispatch(setCurrent(Tab.Flow)); + setWasFlowSelected(true); + } else if (selectedFlows.length === 0) { + if (wasFlowSelected) { + setWasFlowSelected(false); + } + if (currentTab === Tab.Flow) { + dispatch(setCurrent(Tab.FlowList)); + } } - } + }, [selectedFlows, wasFlowSelected, currentTab]); - function handleClick(active: Menu, e) { - e.preventDefault() - setActiveMenu(() => active) + function handleClick(tab: Tab, e: React.MouseEvent) { + e.preventDefault(); + dispatch(setCurrent(tab)); } + const ActiveMenu = tabs[currentTab]; + return (
    - +
    - ) + ); } diff --git a/web/src/js/components/Header/CaptureMenu.tsx b/web/src/js/components/Header/CaptureMenu.tsx new file mode 100644 index 0000000000..b0a4699770 --- /dev/null +++ b/web/src/js/components/Header/CaptureMenu.tsx @@ -0,0 +1,8 @@ +import * as React from "react"; + +CaptureMenu.title = "Capture"; + +export default function CaptureMenu() { + //empty component since we don't have an header menu for capture, but we just display the title + return <>; +} diff --git a/web/src/js/components/Header/ConnectionIndicator.tsx b/web/src/js/components/Header/ConnectionIndicator.tsx index e3246d6784..84702678d1 100644 --- a/web/src/js/components/Header/ConnectionIndicator.tsx +++ b/web/src/js/components/Header/ConnectionIndicator.tsx @@ -1,27 +1,37 @@ import * as React from "react"; -import {ConnectionState} from "../../ducks/connection" -import {useAppSelector} from "../../ducks"; +import { ConnectionState } from "../../ducks/connection"; +import { useAppSelector } from "../../ducks"; - -export default React.memo(function ConnectionIndicator() { - - const connState = useAppSelector(state => state.connection.state), - message = useAppSelector(state => state.connection.message) +export default React.memo(function ConnectionIndicator(): React.ReactElement { + const connState = useAppSelector((state) => state.connection.state); + const message = useAppSelector((state) => state.connection.message); switch (connState) { case ConnectionState.INIT: - return connecting…; + return ( + connecting… + ); case ConnectionState.FETCHING: - return fetching data…; + return ( + + fetching data… + + ); case ConnectionState.ESTABLISHED: - return connected; + return ( + + connected + + ); case ConnectionState.ERROR: - return connection lost; + return ( + + connection lost + + ); case ConnectionState.OFFLINE: - return offline; - default: - const exhaustiveCheck: never = connState; - throw "unknown connection state"; + return ( + offline + ); } -}) +}); diff --git a/web/src/js/components/Header/FileMenu.tsx b/web/src/js/components/Header/FileMenu.tsx index e5fdbe9ba1..0b3c760a87 100644 --- a/web/src/js/components/Header/FileMenu.tsx +++ b/web/src/js/components/Header/FileMenu.tsx @@ -1,47 +1,61 @@ -import * as React from "react" -import {useDispatch} from 'react-redux' -import FileChooser from '../common/FileChooser' -import Dropdown, {Divider, MenuItem} from '../common/Dropdown' -import * as flowsActions from '../../ducks/flows' +import * as React from "react"; +import FileChooser from "../common/FileChooser"; +import Dropdown, { Divider, MenuItem } from "../common/Dropdown"; +import * as flowsActions from "../../ducks/flows"; import HideInStatic from "../common/HideInStatic"; -import {useAppSelector} from "../../ducks"; - +import { useAppDispatch, useAppSelector } from "../../ducks"; export default React.memo(function FileMenu() { - const dispatch = useDispatch(), filter = useAppSelector(state => state.flows.filter); + const dispatch = useAppDispatch(); + const filter = useAppSelector((state) => state.flows.filter); return ( - +
  • e.stopPropagation() + (e) => e.stopPropagation() } - onOpenFile={file => { + onOpenFile={(file) => { dispatch(flowsActions.upload(file)); document.body.click(); // "restart" event propagation }} />
  • - location.replace('/flows/dump')}> -  Save + location.replace("/flows/dump")}> + +  Save - location.replace('/flows/dump?filter=' + filter)}> -  Save filtered + location.replace("/flows/dump?filter=" + filter)} + > + +  Save filtered - confirm('Delete all flows?') && dispatch(flowsActions.clear())}> -  Clear All + + confirm("Delete all flows?") && + dispatch(flowsActions.clear()) + } + > + +  Clear All - +
  • - -  Install Certificates... + + +  Install Certificates...
  • - ) + ); }); diff --git a/web/src/js/components/Header/FilterDocs.tsx b/web/src/js/components/Header/FilterDocs.tsx index 5f34b05993..af350d566a 100644 --- a/web/src/js/components/Header/FilterDocs.tsx +++ b/web/src/js/components/Header/FilterDocs.tsx @@ -1,64 +1,79 @@ -import React, { Component } from 'react' +import React, { Component } from "react"; import { fetchApi } from "../../utils"; type FilterDocsProps = { - selectHandler: (cmd: string) => void, -} + selectHandler: (cmd: string) => void; +}; type FilterDocsStates = { - doc: {commands: string[][]} -} - -export default class FilterDocs extends Component { + doc: { commands: string[][] }; +}; +export default class FilterDocs extends Component< + FilterDocsProps, + FilterDocsStates +> { // @todo move to redux - static xhr: Promise | null - static doc: {commands: string[][]} + static xhr: Promise | null; + static doc: { commands: string[][] }; constructor(props, context) { - super(props, context) - this.state = { doc: FilterDocs.doc } + super(props, context); + this.state = { doc: FilterDocs.doc }; } componentDidMount() { if (!FilterDocs.xhr) { - FilterDocs.xhr = fetchApi('/filter-help').then(response => response.json()) + FilterDocs.xhr = fetchApi("/filter-help").then((response) => + response.json(), + ); FilterDocs.xhr.catch(() => { - FilterDocs.xhr = null - }) + FilterDocs.xhr = null; + }); } if (!this.state.doc) { - FilterDocs.xhr.then(doc => { - FilterDocs.doc = doc - this.setState({ doc }) - }) + FilterDocs.xhr.then((doc) => { + FilterDocs.doc = doc; + this.setState({ doc }); + }); } } render() { - const { doc } = this.state + const { doc } = this.state; return !doc ? ( - + ) : ( - {doc.commands.map(cmd => ( - this.props.selectHandler(cmd[0].split(" ")[0] + " ")}> - + {doc.commands.map((cmd) => ( + + this.props.selectHandler( + cmd[0].split(" ")[0] + " ", + ) + } + > + ))}
    {cmd[0].replace(' ', '\u00a0')}
    {cmd[0].replace(" ", "\u00a0")} {cmd[1]}
    - - -   mitmproxy docs + + +   mitmproxy docs +
    - ) + ); } } diff --git a/web/src/js/components/Header/FilterInput.tsx b/web/src/js/components/Header/FilterInput.tsx index ecafe29dd6..7d406d9fcc 100644 --- a/web/src/js/components/Header/FilterInput.tsx +++ b/web/src/js/components/Header/FilterInput.tsx @@ -1,127 +1,138 @@ -import React, {Component} from 'react' -import ReactDOM from 'react-dom' -import classnames from 'classnames' -import Filt from '../../filt/filt' -import FilterDocs from './FilterDocs' +import React, { Component } from "react"; +import classnames from "classnames"; +import Filt from "../../filt/filt"; +import FilterDocs from "./FilterDocs"; type FilterInputProps = { - type: string - color: any - placeholder: string - value: string - onChange: (value) => { type: string, filter?: string, highlight?: string } | void -} + type: string; + color: any; + placeholder: string; + value: string; + onChange: (value) => void; +}; type FilterInputState = { - value: string - focus: boolean - mousefocus: boolean -} + value: string; + focus: boolean; + mousefocus: boolean; +}; -export default class FilterInput extends Component { +export default class FilterInput extends Component< + FilterInputProps, + FilterInputState +> { + inputRef = React.createRef(); constructor(props, context) { - super(props, context) + super(props, context); // Consider both focus and mouseover for showing/hiding the tooltip, // because onBlur of the input is triggered before the click on the tooltip // finalized, hiding the tooltip just as the user clicks on it. - this.state = {value: this.props.value, focus: false, mousefocus: false} - - this.onChange = this.onChange.bind(this) - this.onFocus = this.onFocus.bind(this) - this.onBlur = this.onBlur.bind(this) - this.onKeyDown = this.onKeyDown.bind(this) - this.onMouseEnter = this.onMouseEnter.bind(this) - this.onMouseLeave = this.onMouseLeave.bind(this) - this.selectFilter = this.selectFilter.bind(this) + this.state = { + value: this.props.value, + focus: false, + mousefocus: false, + }; + + this.onChange = this.onChange.bind(this); + this.onFocus = this.onFocus.bind(this); + this.onBlur = this.onBlur.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.onMouseEnter = this.onMouseEnter.bind(this); + this.onMouseLeave = this.onMouseLeave.bind(this); + this.selectFilter = this.selectFilter.bind(this); } UNSAFE_componentWillReceiveProps(nextProps) { - this.setState({value: nextProps.value}) + this.setState({ value: nextProps.value }); } isValid(filt) { try { if (filt) { - Filt.parse(filt) + Filt.parse(filt); } - return true + return true; } catch (e) { - return false + return false; } } getDesc() { if (!this.state.value) { - return + return ; } try { - return Filt.parse(this.state.value).desc + return Filt.parse(this.state.value).desc; } catch (e) { - return '' + e + return "" + e; } } onChange(e) { - const value = e.target.value - this.setState({value}) + const value = e.target.value; + this.setState({ value }); // Only propagate valid filters upwards. if (this.isValid(value)) { - this.props.onChange(value) + this.props.onChange(value); } } onFocus() { - this.setState({focus: true}) + this.setState({ focus: true }); } onBlur() { - this.setState({focus: false}) + this.setState({ focus: false }); } onMouseEnter() { - this.setState({mousefocus: true}) + this.setState({ mousefocus: true }); } onMouseLeave() { - this.setState({mousefocus: false}) + this.setState({ mousefocus: false }); } onKeyDown(e) { if (e.key === "Escape" || e.key === "Enter") { - this.blur() + this.blur(); // If closed using ESC/ENTER, hide the tooltip. - this.setState({mousefocus: false}) + this.setState({ mousefocus: false }); } - e.stopPropagation() + e.stopPropagation(); } selectFilter(cmd) { - this.setState({value: cmd}) - ReactDOM.findDOMNode(this.refs.input).focus() + this.setState({ value: cmd }); + this.inputRef.current?.focus(); } blur() { - ReactDOM.findDOMNode(this.refs.input).blur() + this.inputRef.current?.blur(); } select() { - ReactDOM.findDOMNode(this.refs.input).select() + this.inputRef.current?.select(); } render() { - const {type, color, placeholder} = this.props - const {value, focus, mousefocus} = this.state + const { type, color, placeholder } = this.props; + const { value, focus, mousefocus } = this.state; return ( -
    +
    - + {(focus || mousefocus) && ( -
    -
    -
    - {this.getDesc()} -
    +
    +
    +
    {this.getDesc()}
    )}
    - ) + ); } } diff --git a/web/src/js/components/Header/FlowListMenu.tsx b/web/src/js/components/Header/FlowListMenu.tsx new file mode 100644 index 0000000000..362392bcb1 --- /dev/null +++ b/web/src/js/components/Header/FlowListMenu.tsx @@ -0,0 +1,87 @@ +import * as React from "react"; +import FilterInput from "./FilterInput"; +import * as flowsActions from "../../ducks/flows"; +import { setFilter, setHighlight } from "../../ducks/flows"; +import Button from "../common/Button"; +import { update as updateOptions } from "../../ducks/options"; +import { useAppDispatch, useAppSelector } from "../../ducks"; + +FlowListMenu.title = "Flow List"; + +export default function FlowListMenu() { + return ( +
    +
    +
    + + +
    +
    Find
    +
    + +
    +
    + + +
    +
    Intercept
    +
    +
    + ); +} + +function InterceptInput() { + const dispatch = useAppDispatch(); + const value = useAppSelector((state) => state.options.intercept); + return ( + dispatch(updateOptions("intercept", val))} + /> + ); +} + +function FlowFilterInput() { + const dispatch = useAppDispatch(); + const value = useAppSelector((state) => state.flows.filter); + return ( + dispatch(setFilter(value))} + /> + ); +} + +function HighlightInput() { + const dispatch = useAppDispatch(); + const value = useAppSelector((state) => state.flows.highlight); + return ( + dispatch(setHighlight(value))} + /> + ); +} + +export function ResumeAll() { + const dispatch = useAppDispatch(); + return ( + + ); +} diff --git a/web/src/js/components/Header/FlowMenu.tsx b/web/src/js/components/Header/FlowMenu.tsx index 9d6e6cceb1..0b893d72ce 100644 --- a/web/src/js/components/Header/FlowMenu.tsx +++ b/web/src/js/components/Header/FlowMenu.tsx @@ -1,53 +1,66 @@ import * as React from "react"; -import Button from "../common/Button" -import {canReplay, MessageUtils} from "../../flow/utils" +import Button from "../common/Button"; +import { canReplay, MessageUtils } from "../../flow/utils"; import HideInStatic from "../common/HideInStatic"; -import {useAppDispatch, useAppSelector} from "../../ducks"; -import * as flowActions from "../../ducks/flows" +import { useAppDispatch, useAppSelector } from "../../ducks"; +import * as flowActions from "../../ducks/flows"; import { duplicate as duplicateFlow, kill as killFlow, remove as removeFlow, replay as replayFlow, resume as resumeFlow, - revert as revertFlow -} from "../../ducks/flows" -import Dropdown, {MenuItem} from "../common/Dropdown"; -import {copy} from "../../flow/export"; -import {Flow} from "../../flow"; + revert as revertFlow, +} from "../../ducks/flows"; +import Dropdown, { MenuItem } from "../common/Dropdown"; +import { copy } from "../../flow/export"; +import { Flow } from "../../flow"; -FlowMenu.title = 'Flow' +FlowMenu.title = "Flow"; export default function FlowMenu(): JSX.Element { - const dispatch = useAppDispatch(), - flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]]) + const dispatch = useAppDispatch(); + const flow = useAppSelector( + (state) => state.flows.byId[state.flows.selected[0]], + ); - if (!flow) - return
    + if (!flow) return
    ; return (
    - - - - - +
    Flow Modification
    @@ -55,8 +68,8 @@ export default function FlowMenu(): JSX.Element {
    - - + +
    Export
    @@ -64,12 +77,20 @@ export default function FlowMenu(): JSX.Element {
    - -
    @@ -77,60 +98,118 @@ export default function FlowMenu(): JSX.Element {
    - ) + ); } // Reference: https://stackoverflow.com/a/63627688/9921431 const openInNewTab = (url) => { - const newWindow = window.open(url, '_blank', 'noopener,noreferrer') - if (newWindow) newWindow.opener = null -} + const newWindow = window.open(url, "_blank", "noopener,noreferrer"); + if (newWindow) newWindow.opener = null; +}; -function DownloadButton({flow}: { flow: Flow }) { +function DownloadButton({ flow }: { flow: Flow }) { if (flow.type !== "http") - return ; + return ( + + ); if (flow.request.contentLength && !flow.response?.contentLength) { - return + return ( + + ); } if (flow.response) { const response = flow.response; if (!flow.request.contentLength && flow.response.contentLength) { - return + return ( + + ); } if (flow.request.contentLength && flow.response.contentLength) { - return 1}>Download▾ - } options={{"placement": "bottom-start"}}> - openInNewTab(MessageUtils.getContentURL(flow, flow.request))}>Download - request - openInNewTab(MessageUtils.getContentURL(flow, response))}>Download - response - + return ( + 1}> + Download▾ + + } + options={{ placement: "bottom-start" }} + > + + openInNewTab( + MessageUtils.getContentURL(flow, flow.request), + ) + } + > + Download request + + + openInNewTab( + MessageUtils.getContentURL(flow, response), + ) + } + > + Download response + + + ); } } return null; } -function ExportButton({flow}: { flow: Flow }) { - return 1} - disabled={flow.type !== "http"}>Export▾ - } options={{"placement": "bottom-start"}}> - copy(flow, "raw_request")}>Copy raw request - copy(flow, "raw_response")}>Copy raw response - copy(flow, "raw")}>Copy raw request and response - copy(flow, "curl")}>Copy as cURL - copy(flow, "httpie")}>Copy as HTTPie - +function ExportButton({ flow }: { flow: Flow }) { + return ( + 1} + disabled={flow.type !== "http"} + > + Export▾ + + } + options={{ placement: "bottom-start" }} + > + copy(flow, "raw_request")}> + Copy raw request + + copy(flow, "raw_response")}> + Copy raw response + + copy(flow, "raw")}> + Copy raw request and response + + copy(flow, "curl")}>Copy as cURL + copy(flow, "httpie")}> + Copy as HTTPie + + + ); } - const markers = { ":red_circle:": "🔴", ":orange_circle:": "🟠", @@ -139,21 +218,41 @@ const markers = { ":large_blue_circle:": "🔵", ":purple_circle:": "🟣", ":brown_circle:": "🟤", -} +}; -function MarkButton({flow}: { flow: Flow }) { +function MarkButton({ flow }: { flow: Flow }) { const dispatch = useAppDispatch(); - return 1}>Mark▾ - } options={{"placement": "bottom-start"}}> - dispatch(flowActions.update(flow, {marked: ""}))}>⚪ (no - marker) - {Object.entries(markers).map(([name, sym]) => + return ( + 1} + > + Mark▾ + + } + options={{ placement: "bottom-start" }} + > dispatch(flowActions.update(flow, {marked: name}))}> - {sym} {name.replace(/[:_]/g, " ")} + onClick={() => + dispatch(flowActions.update(flow, { marked: "" })) + } + > + ⚪ (no marker) - )} - + {Object.entries(markers).map(([name, sym]) => ( + + dispatch(flowActions.update(flow, { marked: name })) + } + > + {sym} {name.replace(/[:_]/g, " ")} + + ))} + + ); } diff --git a/web/src/js/components/Header/MenuToggle.tsx b/web/src/js/components/Header/MenuToggle.tsx index da0b89539a..747efac59d 100644 --- a/web/src/js/components/Header/MenuToggle.tsx +++ b/web/src/js/components/Header/MenuToggle.tsx @@ -1,38 +1,34 @@ import * as React from "react"; -import {useDispatch} from "react-redux" -import * as eventLogActions from "../../ducks/eventLog" -import * as commandBarActions from "../../ducks/commandBar" -import {useAppDispatch, useAppSelector} from "../../ducks" -import * as optionsActions from "../../ducks/options" - +import * as eventLogActions from "../../ducks/eventLog"; +import * as commandBarActions from "../../ducks/commandBar"; +import { useAppDispatch, useAppSelector } from "../../ducks"; +import * as optionsActions from "../../ducks/options"; type MenuToggleProps = { - value: boolean - onChange: (e: React.ChangeEvent) => void - children: React.ReactNode -} + value: boolean; + onChange: (e: React.ChangeEvent) => void; + children: React.ReactNode; +}; -export function MenuToggle({value, onChange, children}: MenuToggleProps) { +export function MenuToggle({ value, onChange, children }: MenuToggleProps) { return (
    - ) + ); } type OptionsToggleProps = { - name: optionsActions.Option, - children: React.ReactNode -} + name: optionsActions.Option; + children: React.ReactNode; +}; -export function OptionsToggle({name, children}: OptionsToggleProps) { - const dispatch = useAppDispatch(), - value = useAppSelector(state => state.options[name]); +export function OptionsToggle({ name, children }: OptionsToggleProps) { + const dispatch = useAppDispatch(); + const value = useAppSelector((state) => state.options[name]); return ( {children} - ) + ); } - export function EventlogToggle() { - const dispatch = useDispatch(), - visible = useAppSelector(state => state.eventLog.visible); + const dispatch = useAppDispatch(); + const visible = useAppSelector((state) => state.eventLog.visible); return ( Display Event Log - ) + ); } export function CommandBarToggle() { - const dispatch = useDispatch(), - visible = useAppSelector(state => state.commandBar.visible); + const dispatch = useAppDispatch(); + const visible = useAppSelector((state) => state.commandBar.visible); return ( Display Command Bar - ) + ); } diff --git a/web/src/js/components/Header/OptionMenu.tsx b/web/src/js/components/Header/OptionMenu.tsx index 2932a1254c..2cfb0bcb3e 100644 --- a/web/src/js/components/Header/OptionMenu.tsx +++ b/web/src/js/components/Header/OptionMenu.tsx @@ -1,24 +1,27 @@ import * as React from "react"; -import {CommandBarToggle, EventlogToggle, OptionsToggle} from "./MenuToggle" -import Button from "../common/Button" -import DocsLink from "../common/DocsLink" +import { CommandBarToggle, EventlogToggle, OptionsToggle } from "./MenuToggle"; +import Button from "../common/Button"; +import DocsLink from "../common/DocsLink"; import HideInStatic from "../common/HideInStatic"; -import * as modalActions from "../../ducks/ui/modal" +import * as modalActions from "../../ducks/ui/modal"; import { useAppDispatch } from "../../ducks"; -OptionMenu.title = 'Options' +OptionMenu.title = "Options"; export default function OptionMenu() { - const dispatch = useAppDispatch() - const openOptions = () => modalActions.setActiveModal('OptionModal') + const dispatch = useAppDispatch(); + const openOptions = () => modalActions.setActiveModal("OptionModal"); return (
    -
    @@ -28,13 +31,14 @@ export default function OptionMenu() {
    - Strip cache headers + Strip cache headers{" "} + Use host header for display - Don't verify server certificates + Don't verify server certificates
    Quick Options
    @@ -43,11 +47,11 @@ export default function OptionMenu() {
    - - + +
    View Options
    - ) + ); } diff --git a/web/src/js/components/Header/StartMenu.tsx b/web/src/js/components/Header/StartMenu.tsx deleted file mode 100644 index 0cc363482e..0000000000 --- a/web/src/js/components/Header/StartMenu.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import * as React from "react"; -import FilterInput from "./FilterInput" -import * as flowsActions from "../../ducks/flows" -import {setFilter, setHighlight} from "../../ducks/flows" -import Button from "../common/Button" -import {update as updateOptions} from "../../ducks/options"; -import {useAppDispatch, useAppSelector} from "../../ducks"; - -StartMenu.title = "Start" - -export default function StartMenu() { - return ( -
    -
    -
    - - -
    -
    Find
    -
    - -
    -
    - - -
    -
    Intercept
    -
    -
    - ) -} - -function InterceptInput() { - const dispatch = useAppDispatch(), - value = useAppSelector(state => state.options.intercept) - return dispatch(updateOptions("intercept", val))} - /> -} - -function FlowFilterInput() { - const dispatch = useAppDispatch(), - value = useAppSelector(state => state.flows.filter) - return dispatch(setFilter(value))} - /> -} - -function HighlightInput() { - const dispatch = useAppDispatch(), - value = useAppSelector(state => state.flows.highlight) - return dispatch(setHighlight(value))} - /> -} - - -export function ResumeAll() { - const dispatch = useAppDispatch(); - return ( - - ) -} diff --git a/web/src/js/components/MainView.tsx b/web/src/js/components/MainView.tsx index 7c8644af70..9c639f5dfb 100644 --- a/web/src/js/components/MainView.tsx +++ b/web/src/js/components/MainView.tsx @@ -1,19 +1,30 @@ -import * as React from "react" -import Splitter from './common/Splitter' -import FlowTable from './FlowTable' -import FlowView from './FlowView' -import {useAppSelector} from "../ducks"; -import CaptureSetup from "./CaptureSetup"; +import * as React from "react"; +import Splitter from "./common/Splitter"; +import FlowTable from "./FlowTable"; +import FlowView from "./FlowView"; +import { useAppSelector } from "../ducks"; +import CaptureSetup from "./Modes/CaptureSetup"; +import Modes from "./Modes"; +import { Tab } from "../ducks/ui/tabs"; export default function MainView() { - const hasSelection = useAppSelector(state => !!state.flows.byId[state.flows.selected[0]]) - const hasFlows = useAppSelector(state => state.flows.list.length > 0); + const hasSelection = useAppSelector( + (state) => !!state.flows.byId[state.flows.selected[0]], + ); + const hasFlows = useAppSelector((state) => state.flows.list.length > 0); + const currentTab = useAppSelector((state) => state.ui.tabs.current); + return (
    - {hasFlows ? : } - - {hasSelection && } - {hasSelection && } + {currentTab === Tab.Capture ? ( + + ) : ( + <> + {hasFlows ? : } + {hasSelection && } + {hasSelection && } + + )}
    - ) + ); } diff --git a/web/src/js/components/Modal/Modal.tsx b/web/src/js/components/Modal/Modal.tsx index d9a475eb91..ec66376968 100644 --- a/web/src/js/components/Modal/Modal.tsx +++ b/web/src/js/components/Modal/Modal.tsx @@ -1,13 +1,14 @@ -import * as React from "react" -import ModalList from './ModalList' +import * as React from "react"; +import ModalList from "./ModalList"; import { useAppSelector } from "../../ducks"; - export default function PureModal() { - const activeModal : string = useAppSelector(state => state.ui.modal.activeModal) - const ActiveModal:(() => JSX.Element) | undefined= ModalList.find(m => m.name === activeModal ) + const activeModal: string = useAppSelector( + (state) => state.ui.modal.activeModal, + ); + const ActiveModal: (() => JSX.Element) | undefined = ModalList.find( + (m) => m.name === activeModal, + ); - return( - activeModal&&ActiveModal!==undefined ? :
    - ) + return activeModal && ActiveModal !== undefined ? :
    ; } diff --git a/web/src/js/components/Modal/ModalLayout.tsx b/web/src/js/components/Modal/ModalLayout.tsx index dd21aed081..87fe155ae3 100644 --- a/web/src/js/components/Modal/ModalLayout.tsx +++ b/web/src/js/components/Modal/ModalLayout.tsx @@ -1,20 +1,24 @@ -import * as React from "react" +import * as React from "react"; type ModalLayoutProps = { - children: React.ReactNode, -} + children: React.ReactNode; +}; -export default function ModalLayout ({ children}: ModalLayoutProps ) { +export default function ModalLayout({ children }: ModalLayoutProps) { return (
    -
    - - ) + ); } diff --git a/web/src/js/components/Modal/ModalList.tsx b/web/src/js/components/Modal/ModalList.tsx index e1f25199a1..27209d5325 100644 --- a/web/src/js/components/Modal/ModalList.tsx +++ b/web/src/js/components/Modal/ModalList.tsx @@ -1,13 +1,13 @@ -import * as React from "react" -import ModalLayout from './ModalLayout' -import OptionContent from './OptionModal' +import * as React from "react"; +import ModalLayout from "./ModalLayout"; +import OptionContent from "./OptionModal"; function OptionModal() { return ( - + - ) + ); } -export default [ OptionModal ] +export default [OptionModal]; diff --git a/web/src/js/components/Modal/Option.jsx b/web/src/js/components/Modal/Option.jsx deleted file mode 100644 index 844fec8593..0000000000 --- a/web/src/js/components/Modal/Option.jsx +++ /dev/null @@ -1,150 +0,0 @@ -import React, {Component} from "react" -import PropTypes from "prop-types" -import {connect} from "react-redux" -import {update as updateOptions} from "../../ducks/options" -import classnames from 'classnames' - -const stopPropagation = e => { - if (e.key !== "Escape") { - e.stopPropagation() - } -} - -BooleanOption.propTypes = { - value: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired, -} - -function BooleanOption({value, onChange, ...props}) { - return ( -
    - -
    - ) -} - -StringOption.propTypes = { - value: PropTypes.string, - onChange: PropTypes.func.isRequired, -} - -function StringOption({value, onChange, ...props}) { - return ( - onChange(e.target.value)} - {...props} - /> - ) -} - -function Optional(Component) { - return function ({onChange, ...props}) { - return onChange(x ? x : null)} - {...props} - /> - } -} - -NumberOption.propTypes = { - value: PropTypes.number.isRequired, - onChange: PropTypes.func.isRequired, -} - -function NumberOption({value, onChange, ...props}) { - return ( - onChange(parseInt(e.target.value))} - {...props} - /> - ) -} - -ChoicesOption.propTypes = { - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, -} - -export function ChoicesOption({value, onChange, choices, ...props}) { - return ( - - ) -} - -StringSequenceOption.propTypes = { - value: PropTypes.arrayOf(PropTypes.string).isRequired, - onChange: PropTypes.func.isRequired, -} - -function StringSequenceOption({value, onChange, ...props}) { - const height = Math.max(value.length, 1) - return