diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..bc3c076f39 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +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 diff --git a/.github/node-version.txt b/.github/node-version.txt new file mode 100644 index 0000000000..8351c19397 --- /dev/null +++ b/.github/node-version.txt @@ -0,0 +1 @@ +14 diff --git a/.github/python-version.txt b/.github/python-version.txt new file mode 100644 index 0000000000..2c0733315e --- /dev/null +++ b/.github/python-version.txt @@ -0,0 +1 @@ +3.11 diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml new file mode 100644 index 0000000000..8ee552c49e --- /dev/null +++ b/.github/workflows/autofix.yml @@ -0,0 +1,30 @@ +name: autofix.ci + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + autofix: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: install-pinned/ruff@0e35bc58bd73769469284df9e1f8898daeea8768 + - run: ruff --fix-only . + - run: ruff format . + + - name: Run prettier + run: | + npm ci + npm run prettier + working-directory: web + + - uses: mhils/add-pr-ref-in-changelog@main + + - uses: autofix-ci/action@d3e591514b99d0fca6779455ff8338516663f7cc diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index af6ac43378..5e7e68e4b3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,66 +1,42 @@ name: CI -on: [ push, pull_request ] +on: + push: + branches: + - '**' + - '!dependabot/**' + 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@v2 - with: - persist-credentials: false - - 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@v2 - with: - persist-credentials: false - - uses: actions/setup-python@v2 - with: - python-version: '3.11' - - run: pip install tox - - run: tox -e flake8 + lint: + uses: mhils/workflows/.github/workflows/python-tox.yml@main + with: + cmd: tox -e lint + filename-matching: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - persist-credentials: false - - uses: actions/setup-python@v2 - with: - python-version: '3.11' - - run: pip install tox - - run: tox -e filename_matching + uses: mhils/workflows/.github/workflows/python-tox.yml@main + with: + cmd: tox -e filename_matching + mypy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - persist-credentials: false - - uses: actions/setup-python@v2 - with: - python-version: '3.11' - - run: pip install tox - - run: tox -e mypy + uses: mhils/workflows/.github/workflows/python-tox.yml@main + with: + cmd: tox -e mypy + individual-coverage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - persist-credentials: false - fetch-depth: 0 - - uses: actions/setup-python@v2 - with: - python-version: '3.9' # there's a weird bug on 3.10 where some lines are not counted as covered. - - run: pip install tox - - run: tox -e individual_coverage + uses: mhils/workflows/.github/workflows/python-tox.yml@main + with: + cmd: tox -e individual_coverage + test: strategy: fail-fast: false @@ -73,15 +49,15 @@ jobs: - os: macos-latest py: "3.11" - os: ubuntu-latest - py: 3.9 + py: "3.10" runs-on: ${{ matrix.os }} steps: - run: printenv - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: persist-credentials: false fetch-depth: 0 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: python-version: ${{ matrix.py }} - run: pip install tox @@ -94,62 +70,86 @@ 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: mhils/better-codecov-action@main with: - file: ./coverage.xml - name: ${{ matrix.os }} + arguments: '--file ./coverage.xml --name ${{ matrix.os }}' build: strategy: fail-fast: false matrix: include: - - image: macos-10.15 + - image: macos-12 platform: macos - image: windows-2019 platform: windows - - image: ubuntu-18.04 # Old Ubuntu version for old glibc + - image: ubuntu-20.04 # Oldest available version so we get oldest glibc possible. platform: linux runs-on: ${{ matrix.image }} env: - CI_BUILD_WHEEL: ${{ matrix.platform == 'linux' }} - CI_BUILD_PYINSTALLER: 1 - CI_BUILD_WININSTALLER: ${{ matrix.platform == 'windows' }} CI_BUILD_KEY: ${{ secrets.CI_BUILD_KEY }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: persist-credentials: false fetch-depth: 0 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: - python-version: '3.11' + python-version-file: .github/python-version.txt - if: matrix.platform == 'windows' - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: release/installbuilder/setup key: installbuilder - - run: pip install -e .[dev] - - run: python release/cibuild.py build - # artifacts must have different names, see https://github.com/actions/upload-artifact/issues/24 - - uses: actions/upload-artifact@v2 + - run: pip install .[dev] # pyinstaller 5.9 does not like pyproject.toml + editable installs. + + # macOS x64. Due to GHA limitations, we are currently building the Apple Silicon app bundle outside of CI. + - if: 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@5565bb656f60c98c8fc515f3444dd8db73545dc2 + with: + keychain: ${{ runner.temp }}/temp + p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }} + p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + - if: 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 standalone-binaries wheel + + # Windows + - if: matrix.platform == 'windows' + run: python -u release/build.py standalone-binaries + - if: matrix.platform == 'windows' && github.repository == 'mitmproxy/mitmproxy' && + (github.ref == 'refs/heads/citest' || startsWith(github.ref, 'refs/tags/')) + run: python -u release/build.py --dirty installbuilder-installer msix-installer + + - uses: actions/upload-artifact@v3 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 test-web-ui: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: persist-credentials: false - - run: git rev-parse --abbrev-ref HEAD - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v4 with: - node-version: '14' + node-version-file: .github/node-version.txt - name: Cache Node.js modules - uses: actions/cache@v2 + uses: actions/cache@v3 with: # npm cache files are stored in `~/.npm` on Linux/macOS path: ~/.npm @@ -161,100 +161,148 @@ 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: mhils/better-codecov-action@main with: - file: ./web/coverage/coverage-final.json - name: web + arguments: '--file ./web/coverage/coverage-final.json' docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: persist-credentials: false - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: - python-version: '3.11' + python-version-file: .github/python-version.txt - run: | wget -q https://github.com/gohugoio/hugo/releases/download/v0.92.1/hugo_extended_0.92.1_Linux-64bit.deb echo "a9440adfd3ecce40089def287dee4e42ffae252ba08c77d1ac575b880a079ce6 hugo_extended_0.92.1_Linux-64bit.deb" | sha256sum -c sudo dpkg -i hugo*.deb - run: pip install -e .[dev] - run: ./docs/build.py - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: docs path: docs/public + # For releases, also build the archive version of the docs. + - if: startsWith(github.ref, 'refs/tags/') + run: ./docs/build.py + env: + DOCS_ARCHIVE: true + - if: startsWith(github.ref, 'refs/tags/') + uses: actions/upload-artifact@v3 + with: + name: docs-archive + path: docs/public + + check: + if: always() + needs: + - lint + - filename-matching + - mypy + - individual-coverage + - test + - build + - test-web-ui + - docs + uses: mhils/workflows/.github/workflows/alls-green.yml@main + with: + jobs: ${{ toJSON(needs) }} # Separate from everything else because slow. build-and-deploy-docker: if: github.repository == 'mitmproxy/mitmproxy' && ( - github.ref == 'refs/heads/main' || - github.ref == 'refs/heads/dockertest' || - startsWith(github.ref, 'refs/tags/') + github.ref == 'refs/heads/main' + || github.ref == 'refs/heads/citest' + || startsWith(github.ref, 'refs/tags/') ) environment: deploy-docker - needs: - - test - - test-web-ui - - build - - docs + needs: check runs-on: ubuntu-latest env: - CI_BUILD_DOCKER: 1 DOCKER_USERNAME: mitmbot DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: persist-credentials: false - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: - python-version: '3.11' - - uses: actions/download-artifact@v2 + python-version-file: .github/python-version.txt + - uses: actions/download-artifact@v3 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: pip install -e .[dev] - - run: python release/cibuild.py build - - run: python release/cibuild.py upload + - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 + - uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v1.6.0 + - run: python release/build-and-deploy-docker.py 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' && github.event_name == 'push' - environment: deploy - needs: - - test - - test-web-ui - - build - - docs + 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: check runs-on: ubuntu-latest env: - TWINE_USERNAME: mitmproxy + # PyPI and MSFT keys are only available for the deploy-release environment + # The AWS access key for snapshots is scoped to branches/* as well. + TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-west-2 + MSFT_APP_ID: 9NWNDLQMNZD7 + 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@v2 + - uses: actions/checkout@v4 with: persist-credentials: false - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: - python-version: '3.11' + python-version-file: .github/python-version.txt - run: sudo apt-get update - - run: sudo apt-get install -y twine awscli - - uses: actions/download-artifact@v2 + - run: sudo apt-get install -y awscli + - if: startsWith(github.ref, 'refs/tags/') + run: sudo apt-get install -y twine + + - uses: actions/download-artifact@v3 + with: + name: docs + path: docs/public + - if: startsWith(github.ref, 'refs/tags/') + uses: actions/download-artifact@v3 + with: + name: docs-archive + path: docs/archive + - uses: actions/download-artifact@v3 with: + name: binaries.windows path: release/dist - - run: mv release/dist/docs docs/public - # move artifacts from their subfolders into release/dist - - run: find release/dist -mindepth 2 -type f -exec mv {} release/dist \; - # and then delete the empty folders - - run: find release/dist -type d -empty -delete + - uses: actions/download-artifact@v3 + with: + name: binaries.linux + path: release/dist + - uses: actions/download-artifact@v3 + with: + name: binaries.macos + path: release/dist + - run: ls docs/public - run: ls release/dist + - run: ./release/deploy.py + + - name: Deploy to Microsoft Store (test flight) + if: github.ref == 'refs/heads/citest' + run: ./release/deploy-microsoft-store.py release/dist/*.msix + env: + MSFT_APP_FLIGHT: 174ca570-8cae-4444-9858-c07293f1f13a + - name: Deploy to Microsoft Store + if: startsWith(github.ref, 'refs/tags/') + run: ./release/deploy-microsoft-store.py release/dist/*.msix diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..69db1f7eed --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,36 @@ +name: Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Version (major.minor.patch)' + required: true + type: string + skip-branch-status-check: + description: 'Skip CI status check.' + default: false + required: false + type: boolean + +permissions: + actions: write + contents: write + +jobs: + release: + environment: deploy-release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_PUSH_TOKEN }} # this token works to push to the protected main branch. + - uses: actions/setup-node@v4 + with: + node-version-file: .github/node-version.txt + - uses: actions/setup-python@v4 + with: + python-version-file: .github/python-version.txt + - run: ./release/release.py ${{ inputs.version }} ${{ inputs.skip-branch-status-check }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # this token works with the GraphQL API diff --git a/.gitignore b/.gitignore index 786f221855..2a6f5eb373 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,7 @@ MANIFEST .tox*/ build/ dist/ -mitmproxy/contrib/kaitaistruct/*.ksy +mitmproxy/contrib/kaitaistruct/**/*.ksy .pytest_cache __pycache__ .hypothesis/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e575e51d4..14f237559f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,249 @@ # Release History + + ## Unreleased: mitmproxy next +* Improved handling for `--allow-hosts`/`--ignore-hosts` options in WireGuard mode (#5930). + ([#6513](https://github.com/mitmproxy/mitmproxy/pull/6513), @dsphper) +* DNS resolution is now exempted from `--ignore-hosts` in WireGuard Mode. + ([#6513](https://github.com/mitmproxy/mitmproxy/pull/6513), @dsphper) +* 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) +* 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) + + +## 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. + +## 02 November 2022: mitmproxy 9.0.1 + +* The precompiled binaries now ship with OpenSSL 3.0.7, which resolves CVE-2022-3602 and CVE-2022-3786. +* Performance and stability improvements for WireGuard mode. + ([#5694](https://github.com/mitmproxy/mitmproxy/issues/5694), @mhils, @decathorpe) +* Fix a bug where the standalone Linux binaries would require libffi to be installed. + ([#5699](https://github.com/mitmproxy/mitmproxy/issues/5699), @mhils) +* Hard exit when mitmproxy cannot write logs, fixes endless loop when parent process exits. + ([#4669](https://github.com/mitmproxy/mitmproxy/issues/4669), @Prinzhorn) +* Fix a permission error affecting the Docker images. + ([#5700](https://github.com/mitmproxy/mitmproxy/issues/5700), @mhils) + + +## 28 October 2022: mitmproxy 9.0.0 + +### Major Features + +* Add Raw UDP support. + ([#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. + ([#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). + +### Deprecations + +* Deprecate `add_log` event hook. Users should use the builtin `logging` module instead. + 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) +* Deprecate `mitmproxy.ctx.log` in favor of Python's builtin `logging` module. + 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. + ([#5678](https://github.com/mitmproxy/mitmproxy/issues/5678), @mhils) +* One mitmproxy instance can now spawn multiple proxy servers. + ([#5393](https://github.com/mitmproxy/mitmproxy/pull/5393), @mhils) +* Add syntax highlighting to JSON and msgpack content view. + ([#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 + upstream connections to fetch TLS certificate details. + ([#5487](https://github.com/mitmproxy/mitmproxy/pull/5487), @mhils) +* Fix order of event hooks on startup. + ([#5376](https://github.com/mitmproxy/mitmproxy/issues/5376), @meitinger) +* Include server information in bind/listen errors. + ([#5495](https://github.com/mitmproxy/mitmproxy/pull/5495), @meitinger) +* Include information about lazy connection_strategy in related errors. + ([#5465](https://github.com/mitmproxy/mitmproxy/pull/5465), @meitinger, @mhils) +* Fix `tls_version_server_min` and `tls_version_server_max` options. + ([#5546](https://github.com/mitmproxy/mitmproxy/issues/5546), @mhils) +* Added Magisk module generation for Android onboarding. + ([#5547](https://github.com/mitmproxy/mitmproxy/pull/5547), @jorants) +* Update Linux binary builder to Ubuntu 20.04, bumping the minimum glibc version to 2.31. + ([#5547](https://github.com/mitmproxy/mitmproxy/pull/5547), @jorants) +* Add "Save filtered" button in mitmweb. + ([#5531](https://github.com/mitmproxy/mitmproxy/pull/5531), @rnbwdsh, @mhils) +* Render application/prpc content as gRPC/Protocol Buffers + ([#5568](https://github.com/mitmproxy/mitmproxy/pull/5568), @selfisekai) +* Mitmweb now supports `content_view_lines_cutoff`. + ([#5548](https://github.com/mitmproxy/mitmproxy/pull/5548), @sanlengjingvv) +* Fix a mitmweb crash when scrolling down the flow list. + ([#5507](https://github.com/mitmproxy/mitmproxy/pull/5507), @LIU-shuyi) +* Add HTTP/3 binary frame content view. + ([#5582](https://github.com/mitmproxy/mitmproxy/pull/5582), @mhils) +* Fix mitmweb not properly opening a browser and being stuck on some Linux. + ([#5522](https://github.com/mitmproxy/mitmproxy/issues/5522), @Prinzhorn) +* Fix race condition when updating mitmweb WebSocket connections that are closing. + ([#5405](https://github.com/mitmproxy/mitmproxy/issues/5405), [#5686](https://github.com/mitmproxy/mitmproxy/issues/5686), @mhils) +* Fix mitmweb crash when using filters. + ([#5658](https://github.com/mitmproxy/mitmproxy/issues/5658), [#5661](https://github.com/mitmproxy/mitmproxy/issues/5661), @LIU-shuyi, @mhils) +* Fix missing default port when starting a browser. + ([#5687](https://github.com/mitmproxy/mitmproxy/issues/5687), @rbdixon) +* Add docs for transparent mode on Windows. + ([#5402](https://github.com/mitmproxy/mitmproxy/issues/5402), @stephenspol) + ## 28 June 2022: mitmproxy 8.1.1 * Support specifying the local address for outgoing connections @@ -15,6 +257,8 @@ * Remove overambitious assertions in the HTTP state machine, fix some error handling. ([#5383](https://github.com/mitmproxy/mitmproxy/issues/5383), @mhils) +* Use default_factory for parser_options. + ([#5474](https://github.com/mitmproxy/mitmproxy/issues/5474), @rathann) ## 15 May 2022: mitmproxy 8.1.0 @@ -63,6 +307,8 @@ ([#5339](https://github.com/mitmproxy/mitmproxy/issues/5339), @mhils) * Fix handling of multiple Cookie headers when proxying HTTP/2 to HTTP/1 ([#5337](https://github.com/mitmproxy/mitmproxy/issues/5337), @rinsuki) +* Improve http_manipulate_cookies.py example. + ([#5578](https://github.com/mitmproxy/mitmproxy/issues/5578), @insilications) ## 19 March 2022: mitmproxy 8.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9f0496090a..6a421768a4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,8 +14,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 @@ -79,7 +78,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% @@ -92,7 +91,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 a92e0b0b76..f23dbd7c9e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # 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) @@ -26,7 +28,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! @@ -39,6 +41,6 @@ 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! +Also, please feel free to join our developer Slack! However, please note that the primary purpose of our Slack is direct communication between maintainers and contributors. **If you have questions where the answer might be valuable to others, please use [GitHub Discussions](https://github.com/mitmproxy/mitmproxy/discussions) and not Slack.** [![Slack Developer Chat](https://shields.mitmproxy.org/badge/slack-mitmproxy-E01563.svg)](http://slack.mitmproxy.org/) diff --git a/codecov.yml b/codecov.yml index 11f75c939d..899cd7e308 100644 --- a/codecov.yml +++ b/codecov.yml @@ -7,7 +7,8 @@ coverage: target: auto threshold: 0.1% paths: - - "!web/" + - "mitmproxy/" + - "examples/addons/" web: target: auto threshold: 0.1% 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 0c23fbd86c..be3f1e0b04 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, 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__).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,21 +34,15 @@ 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") if types: print(f"from typing import {', '.join(sorted(types))}") - print("from mitmproxy import ctx") + print("import logging") for imp in sorted(imports): print(f"import {imp}") print() @@ -54,14 +60,15 @@ 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( - f' ctx.log(f"{hook.name}: {" ".join("{" + p.name + "=}" for p in params)}")' + f' logging.info(f"{hook.name}: {" ".join("{" + p.name + "=}" for p in params)}")' ) else: - print(f' ctx.log("{hook.name}")') + print(f' logging.info("{hook.name}")') print("") @@ -128,6 +135,26 @@ def category(name: str, desc: str, hooks: list[type[hooks.Hook]]) -> None: ], ) + category( + "UDP", + "", + [ + udp.UdpStartHook, + udp.UdpMessageHook, + udp.UdpEndHook, + udp.UdpErrorHook, + ], + ) + + category( + "QUIC", + "", + [ + quic.QuicStartClientHook, + quic.QuicStartServerHook, + ], + ) + category( "TLS", "", diff --git a/docs/scripts/api-render.py b/docs/scripts/api-render.py index 10299e7c3c..0a224eb77f 100644 --- a/docs/scripts/api-render.py +++ b/docs/scripts/api-render.py @@ -32,9 +32,11 @@ "mitmproxy.http", "mitmproxy.net.server_spec", "mitmproxy.proxy.context", + "mitmproxy.proxy.mode_specs", "mitmproxy.proxy.server_hooks", "mitmproxy.tcp", "mitmproxy.tls", + "mitmproxy.udp", "mitmproxy.websocket", here / ".." / "src" / "generated" / "events.py", ] @@ -51,7 +53,7 @@ if isinstance(module, Path): continue filename = f"api/{module.replace('.', '/')}.html" - (api_content / f"{module}.md").write_text( + (api_content / f"{module}.md").write_bytes( textwrap.dedent( f""" --- @@ -65,7 +67,7 @@ {{{{< readfile file="/generated/{filename}" >}}}} """ - ) + ).encode() ) (here / ".." / "src" / "content" / "addons-api.md").touch() 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 2ebca9d94b..7fd2327f67 100644 --- a/docs/src/content/addons-api-changelog.md +++ b/docs/src/content/addons-api-changelog.md @@ -10,6 +10,31 @@ 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 + +We've deprecated mitmproxy's homegrown logging system in favor of Python's builtin `logging` module. +This means that addons should now use standard logging functionality instead of `mitmproxy.ctx.log`: + +```python +# Deprecated: +from mitmproxy import ctx +ctx.log.info("hello world") + +# New: +import logging +logging.info("hello world") +``` + + +Accordingly, the `add_log` event has been deprecated. Developers who rely on log entries should register their own +`logging.Handler` instead. An example for this can be found in the `EventStore` addon. + ## mitmproxy 7.0 #### Connection Events diff --git a/docs/src/content/addons-options.md b/docs/src/content/addons-options.md index f5c35f8c7b..e2d5b1be68 100644 --- a/docs/src/content/addons-options.md +++ b/docs/src/content/addons-options.md @@ -95,4 +95,4 @@ The following types are supported for options. - Primitive types - `str`, `int`, `float`, `bool`. - Optional values, annotated using `typing.Optional`. -- Sequences of values, annotated using `typing.Sequence`. +- Sequences of values, annotated using `collections.abc.Sequence`. diff --git a/docs/src/content/api/mitmproxy.proxy.mode_specs.md b/docs/src/content/api/mitmproxy.proxy.mode_specs.md new file mode 100644 index 0000000000..54fffef716 --- /dev/null +++ b/docs/src/content/api/mitmproxy.proxy.mode_specs.md @@ -0,0 +1,11 @@ + +--- +title: "mitmproxy.proxy.mode_specs" +url: "api/mitmproxy/proxy/mode_specs.html" + +menu: + addons: + parent: 'Event Hooks & API' +--- + +{{< readfile file="/generated/api/mitmproxy/proxy/mode_specs.html" >}} diff --git a/docs/src/content/api/mitmproxy.udp.md b/docs/src/content/api/mitmproxy.udp.md new file mode 100644 index 0000000000..a18e7f55eb --- /dev/null +++ b/docs/src/content/api/mitmproxy.udp.md @@ -0,0 +1,11 @@ + +--- +title: "mitmproxy.udp" +url: "api/mitmproxy/udp.html" + +menu: + addons: + parent: 'Event Hooks & API' +--- + +{{< readfile file="/generated/api/mitmproxy/udp.html" >}} diff --git a/docs/src/content/concepts-certificates.md b/docs/src/content/concepts-certificates.md index 78f5e77f74..e65b704aa5 100644 --- a/docs/src/content/concepts-certificates.md +++ b/docs/src/content/concepts-certificates.md @@ -62,6 +62,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) @@ -108,6 +109,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.* diff --git a/docs/src/content/concepts-modes.md b/docs/src/content/concepts-modes.md index a54a3d6c8d..a8b6f8fed4 100644 --- a/docs/src/content/concepts-modes.md +++ b/docs/src/content/concepts-modes.md @@ -9,9 +9,11 @@ menu: - [Regular](#regular-proxy) (the default) - [Transparent](#transparent-proxy) +- [WireGuard](#wireguard-transparent-proxy) - [Reverse Proxy](#reverse-proxy) - [Upstream Proxy](#upstream-proxy) - [SOCKS Proxy](#socks-proxy) +- [DNS Server](#dns-server) Now, which one should you pick? Use this flow chart: @@ -137,19 +139,115 @@ most cases, the configuration will look like this: {{< figure src="/schematics/proxy-modes-transparent-3.png" >}} +## WireGuard (transparent proxy) + +```shell +mitmdump --mode wireguard +``` + +The WireGuard mode works in the same way as transparent mode, except that setup +and routing client traffic to mitmproxy are different. In this mode, mitmproxy +runs an internal WireGuard server, which devices can be connected to by using +standard WireGuard client applications: + +1. Start `mitmweb --mode wireguard`. +2. Install a WireGuard client on target device. +3. Import the WireGuard client configuration provided by mitmproxy. + +No additional routing configuration is required for this mode, since WireGuard +operates by exchanging UDP packets between client and server (with encrypted IP +packets as their payload) instead of routing IP packets directly. Additionally, +the WireGuard server runs entirely in userspace, so no administrative privileges +are necessary for setup or operation of mitmproxy in this mode. + +### Configuration + +#### WireGuard server + +By default, the WireGuard server will listen on port `51820/udp`, the default +port for WireGuard servers. This can be changed by setting the `listen_port` +option or by specifying an explicit port (`--mode wireguard@51821`). + +The encryption keys for WireGuard connections are stored in +`~/.mitmproxy/wireguard.conf`. It is possible to specify a custom path with +`--mode wireguard:path`. New keys will be generated automatically if the +specified file does not yet exist. For example, to connect two clients +simultaneously, you can run +`mitmdump --mode wireguard:wg-keys-1.conf --mode wireguard:wg-keys-2.conf@51821`. + +#### WireGuard clients + +It is possible to limit the IP addresses for which traffic is sent over the +WireGuard tunnel to specific ranges. In this case, the `AllowedIPs` setting +in the WireGuard client configuration can be changed from `0.0.0.0/0` (i.e +"route *all* IPv4 traffic through the WireGuard tunnel") to the desired ranges +of IP addresses (this setting allows multiple, comma-separated values). + +For more complex network layouts it might also be necessary to override the +automatically detected `Endpoint` IP address (i.e. the address of the host on +which mitmproxy and its WireGuard server are running). + +### Limitations + +#### Transparently proxying mitmproxy host traffic + +With the current implementation, it is not possible to proxy all traffic of the +host that mitmproxy itself is running on, since this would result in outgoing +WireGuard packets being sent over the WireGuard tunnel themselves. + +#### Limited support for IPv6 traffic + +The WireGuard server internal to mitmproxy supports receiving IPv6 packets from +client devices, but support for proxying IPv6 packets themselves is still +limited. For this reason, the `AllowedIPs` setting in generated WireGuard client +configurations does not list any IPv6 addresses yet. To enable the incomplete +support for IPv6 traffic, `::/0` (i.e. "route *all* IPv6 traffic through the +WireGuard tunnel") or other IPv6 address ranges can be added to the list of +allowed IP addresses. + ## Reverse Proxy ```shell 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" >}} -There are various use-cases: +### Listen Port + +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 @@ -162,16 +260,24 @@ 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 + `--mode reverse:dtls://example.com:1234` to use UDP and DTLS respectively instead. + Incoming client connections can either use (D)TLS themselves or raw TCP/UDP. + In case you want to inspect raw traffic only for some hosts and HTTP for + others, have a look at the [tcp_hosts]({{< relref "concepts-options" >}}#tcp_hosts) + and [udp_hosts]({{< relref "concepts-options" >}}#udp_hosts) options. +- Say you want to capture DNS traffic to Google's Public DNS server? Then you + can spawn a reverse instance with `--mode reverse:dns://8.8.8.8`. In case + you want to resolve queries locally (ie. using the resolve capabilities + provided and configured by your operating system), use [DNS Server](#dns-server) + mode instead. ### Host Header @@ -228,3 +334,21 @@ mitmdump --mode socks5 In this mode, mitmproxy acts as a SOCKS5 proxy. This is similar to the regular proxy mode, but using SOCKS5 instead of HTTP for connection establishment with the proxy. + + +## DNS Server + +```shell +mitmdump --mode dns +``` + +This mode will listen for incoming DNS queries and use the resolve +capabilities of your operation system to return an answer. +By default port 53 will be used. To specify a different port, say 5353, +use `--mode dns@5353`. + +Since the lookup API is limited to turning host names into IP addresses +and vice-versa, only A, AAAA, PTR and CNAME queries are supported. +You can, however, use reverse mode to specify an upstream server and +unlock all query types. For example, to use Google's Public DNS server +specify `--mode reverse:dns://8.8.8.8`. 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 f15caa6f78..e01fa8ad27 100644 --- a/docs/src/content/concepts-protocols.md +++ b/docs/src/content/concepts-protocols.md @@ -11,19 +11,15 @@ 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, which is particularly robust to HTTP syntax +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 errors. Protocol violations are often deliberately forwarded or inserted at the proxy. ##### Known Limitations -- Trailers: mitmproxy currently does not support HTTP trailers, but we are happy to accept contributions. - -##### RFCs - -- [RFC7230: HTTP/1.1: Message Syntax and Routing](http://tools.ietf.org/html/rfc7230) -- [RFC7231: HTTP/1.1: Semantics and Content](http://tools.ietf.org/html/rfc7231) +- Trailers: mitmproxy currently does not support trailers with HTTP/1.x, but we are happy to accept contributions. ## HTTP/2 @@ -32,15 +28,22 @@ server does not speak HTTP/2, mitmproxy seamlessly translates messages to HTTP/1 ##### Known Limitations -- *Trailers*: mitmproxy currently does not support HTTP trailers, but we are happy to accept contributions. - *Priority Information*: mitmproxy currently ignores HTTP/2 PRIORITY frames. This does not affect the transmitted contents, but potentially affects the order in which messages are sent. - *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). -##### RFCs +## 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 -- [RFC7540: Hypertext Transfer Protocol Version 2 (HTTP/2)](http://tools.ietf.org/html/rfc7540) +- *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 @@ -49,19 +52,23 @@ for message compression. ##### Known Limitations -- *User Interface*: WebSocket messages are currently logged to the event log, but not displayed in the console or web - interface. We would welcome contributions that fix this issue. - *Replay*: Client or server replay is not possible yet. - *Ping*: mitmproxy will forward PING and PONG frames, but not store them. The payload is only logged to the event log. - *Unknown Extensions*: Unknown WebSocket extensions will cause a warning message to be logged, but are otherwise passed through as-is. This may lead to noncompliant behavior. -##### RFCs +## DNS -- [RFC6455: The WebSocket Protocol](http://tools.ietf.org/html/rfc6455) -- [RFC7692: Compression Extensions for WebSocket](http://tools.ietf.org/html/rfc7692) +DNS support in mitmproxy is based on a custom DNS implementation. -## Generic TCP Proxy +##### Known Limitations + +- *Replay*: Client or server replay is not possible yet. +- mitmproxy current does not support DNS over TCP. +- We have not started any work on DoT/DoH/DoQ (DNS-over-TLS/HTTPS/QUIC) yet. Contributions are welcome. +- We have not started any work on stripping ESNI or HTTPS RR records yet. Contributions are welcome. + +## Generic TCP/TLS Proxy Mitmproxy can also act as a generic TCP proxy. In this mode, mitmproxy will still detect the presence of TLS at the beginning of a connection and perform a man-in-the-middle attack if necessary, but otherwise forward messages @@ -73,3 +80,16 @@ Users can explicitly opt into generic TCP proxying by setting the [`tcp_hosts` o - *Replay*: Client or server replay is not possible yet. - *Opportunistic TLS*: mitmproxy will not detect when a plaintext protocol upgrades to TLS (STARTTLS). + + +## Generic UDP/DTLS Proxy + +Mitmproxy can also act as a generic UDP proxy. In this mode, mitmproxy will still detect the presence of DTLS at the +beginning of a connection and perform a man-in-the-middle attack if necessary, but otherwise forward messages +unmodified. + +Users can explicitly opt into generic UDP proxying by setting the [`udp_hosts` option]({{< relref concepts-options >}}). + +##### Known Limitations + +- *Replay*: Client or server replay is not possible yet. 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 205ddf68a7..a5f9f84e15 100644 --- a/docs/src/content/howto-transparent.md +++ b/docs/src/content/howto-transparent.md @@ -12,6 +12,13 @@ network layer, without any client configuration being required. This makes transparent proxying ideal for those situations where you can't change client behaviour - proxy-oblivious mobile applications being a common example. +{{% note %}} +The new [WireGuard mode]({{< relref "concepts-modes" >}}#wireguard-transparent-proxy) +provides an alternative implementation for transparent proxying. It is much +easier to set up, as it does not require setting up IP forwarding or modifying +routing rules. +{{% /note %}} + To set up transparent proxying, we need two new components. The first is a redirection mechanism that transparently reroutes a TCP connection destined for a server on the Internet to a listening proxy server. This usually takes the @@ -268,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: @@ -276,11 +286,63 @@ Follow steps **3-5** above. This will redirect the packets from all users other sudo -u nobody mitmproxy --mode transparent --showhost ``` +## Windows + +All commands will need to be run on Windows 10 or above with elevated privileges. PowerShell should be run as Administrator. + +### 1. Enable IP routing. + +```batch +reg add HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters /v IPEnableRouter /D 1 /t REG_DWORD /f +``` + +This enables your Windows to be able act as an IP router. The RemoteAccess service can now be enabled. + +```batch +sc config RemoteAccess start= demand +``` + +This command enables the IP routing service. The `demand` option allows the service to manually be started. Alternatively, +you can replace `demand` with `auto` to enable IP routing on startup. + +```batch +sc start RemoteAccess +``` + +Starts the RemoteAccess service. Windows can now route IP's! + +### 2. Block outgoing ICMP redirect. + +```batch +netsh advfirewall firewall add rule name="Don't send ICMP redirects" dir=out protocol=icmpv4:5,any action=block +``` + +Command above puts a rule in the advanced firewall to not redirect any ICMP packets. + +If your test device is on the same physical network, your machine shouldn't inform the device that +there's a shorter route available by skipping the proxy. + +### 3. Fire up mitmproxy. + +You probably want a command like this: + +```batch +mitmproxy --mode transparent --showhost +``` + +The `--mode transparent` option turns on transparent mode, and the `--showhost` argument tells +mitmproxy to use the value of the Host header for URL display. + +### 4. Finally, configure your test device. + +Set the test device up to use the host on which mitmproxy is running as the default gateway and +[install the mitmproxy certificate authority on the test device]({{< relref "concepts-certificates" >}}). + ## "Full" transparent mode on Linux {{% note %}} This feature is currently unavailable in mitmproxy 7 and above -(#4914). +([#4914](https://github.com/mitmproxy/mitmproxy/discussions/4914)). {{% /note %}} By default mitmproxy will use its own local IP address for its server-side 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-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/docs/src/layouts/shortcodes/note.html b/docs/src/layouts/shortcodes/note.html index f3db72c4b4..b7c3451129 100644 --- a/docs/src/layouts/shortcodes/note.html +++ b/docs/src/layouts/shortcodes/note.html @@ -1 +1 @@ -
{{.Inner}}
+
{{.Inner | markdownify }}
diff --git a/examples/addons/anatomy.py b/examples/addons/anatomy.py index f86b3b916d..011ccd0f1f 100644 --- a/examples/addons/anatomy.py +++ b/examples/addons/anatomy.py @@ -3,7 +3,7 @@ Run as follows: mitmproxy -s anatomy.py """ -from mitmproxy import ctx +import logging class Counter: @@ -12,7 +12,7 @@ def __init__(self): def request(self, flow): self.num = self.num + 1 - ctx.log.info("We've seen %d flows" % self.num) + logging.info("We've seen %d flows" % self.num) addons = [Counter()] diff --git a/examples/addons/commands-flows.py b/examples/addons/commands-flows.py index 053d11d424..9cc196ca9b 100644 --- a/examples/addons/commands-flows.py +++ b/examples/addons/commands-flows.py @@ -1,10 +1,11 @@ """Handle flows as command arguments.""" +import logging from collections.abc import Sequence from mitmproxy import command -from mitmproxy import ctx from mitmproxy import flow from mitmproxy import http +from mitmproxy.log import ALERT class MyAddon: @@ -13,7 +14,7 @@ def addheader(self, flows: Sequence[flow.Flow]) -> None: for f in flows: if isinstance(f, http.HTTPFlow): f.request.headers["myheader"] = "value" - ctx.log.alert("done") + logging.log(ALERT, "done") addons = [MyAddon()] diff --git a/examples/addons/commands-paths.py b/examples/addons/commands-paths.py index 80987e27b6..e80ace5cb8 100644 --- a/examples/addons/commands-paths.py +++ b/examples/addons/commands-paths.py @@ -1,11 +1,12 @@ """Handle file paths as command arguments.""" +import logging from collections.abc import Sequence from mitmproxy import command -from mitmproxy import ctx from mitmproxy import flow from mitmproxy import http from mitmproxy import types +from mitmproxy.log import ALERT class MyAddon: @@ -24,7 +25,7 @@ def histogram( for cnt, dom in sorted((v, k) for (k, v) in totals.items()): fp.write(f"{cnt}: {dom}\n") - ctx.log.alert("done") + logging.log(ALERT, "done") addons = [MyAddon()] diff --git a/examples/addons/commands-simple.py b/examples/addons/commands-simple.py index 153a7db58b..86750a4b92 100644 --- a/examples/addons/commands-simple.py +++ b/examples/addons/commands-simple.py @@ -1,6 +1,7 @@ """Add a custom command to mitmproxy's command prompt.""" +import logging + from mitmproxy import command -from mitmproxy import ctx class MyAddon: @@ -10,7 +11,7 @@ def __init__(self): @command.command("myaddon.inc") def inc(self) -> None: self.num += 1 - ctx.log.info(f"num = {self.num}") + logging.info(f"num = {self.num}") addons = [MyAddon()] diff --git a/examples/addons/contentview-custom-grpc.py b/examples/addons/contentview-custom-grpc.py index c84da91c89..74ffd14b46 100644 --- a/examples/addons/contentview-custom-grpc.py +++ b/examples/addons/contentview-custom-grpc.py @@ -4,7 +4,10 @@ """ 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 +71,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 +103,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..03d2a87a79 100644 --- a/examples/addons/contentview.py +++ b/examples/addons/contentview.py @@ -5,10 +5,10 @@ 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 +18,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 +29,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 +43,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..f11eb7c582 100644 --- a/examples/addons/duplicate-modify-replay.py +++ b/examples/addons/duplicate-modify-replay.py @@ -10,6 +10,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 13b54dbe34..fe499228f9 100644 --- a/examples/addons/filter-flows.py +++ b/examples/addons/filter-flows.py @@ -1,25 +1,29 @@ """ Use mitmproxy's filter pattern in scripts. """ +from __future__ import annotations + +import logging + from mitmproxy import flowfilter -from mitmproxy import ctx, http +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): - ctx.log.info("Flow matches filter:") - ctx.log.info(flow) + logging.info("Flow matches filter:") + logging.info(flow) addons = [Filter()] diff --git a/examples/addons/http-modify-form.py b/examples/addons/http-modify-form.py index b4ad178fdb..d3c73a72a1 100644 --- a/examples/addons/http-modify-form.py +++ b/examples/addons/http-modify-form.py @@ -9,4 +9,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-reply-from-proxy.py b/examples/addons/http-reply-from-proxy.py index 3ce35c5a4f..e6470a3da4 100644 --- a/examples/addons/http-reply-from-proxy.py +++ b/examples/addons/http-reply-from-proxy.py @@ -1,4 +1,4 @@ -"""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..9c59a338ed 100644 --- a/examples/addons/http-stream-modify.py +++ b/examples/addons/http-stream-modify.py @@ -7,10 +7,10 @@ - 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..2ac026cc0d 100644 --- a/examples/addons/http-trailers.py +++ b/examples/addons/http-trailers.py @@ -6,7 +6,6 @@ headers by name, so the receiving endpoint can wait and read them after the body. """ - from mitmproxy import http from mitmproxy.http import Headers @@ -34,6 +33,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/io-read-saved-flows.py b/examples/addons/io-read-saved-flows.py index f6a177be4c..32da842d34 100644 --- a/examples/addons/io-read-saved-flows.py +++ b/examples/addons/io-read-saved-flows.py @@ -2,11 +2,13 @@ """ 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..a348749de2 100644 --- a/examples/addons/io-write-flow-file.py +++ b/examples/addons/io-write-flow-file.py @@ -11,7 +11,8 @@ import sys from typing import BinaryIO -from mitmproxy import io, http +from mitmproxy import http +from mitmproxy import io class Writer: diff --git a/examples/addons/log-events.py b/examples/addons/log-events.py index c31d6bc50c..3ecfb8f982 100644 --- a/examples/addons/log-events.py +++ b/examples/addons/log-events.py @@ -1,8 +1,17 @@ """Post messages to mitmproxy's event log.""" -from mitmproxy import ctx +import logging +from mitmproxy.addonmanager import Loader +from mitmproxy.log import ALERT -def load(l): - ctx.log.info("This is some informative text.") - ctx.log.warn("This is a warning.") - ctx.log.error("This is an error.") +logger = logging.getLogger(__name__) + + +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 2ee0929808..8f7eab50d1 100644 --- a/examples/addons/nonblocking.py +++ b/examples/addons/nonblocking.py @@ -1,27 +1,26 @@ """ -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 -from mitmproxy import ctx +# 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): - ctx.log.info(f"handle request: {flow.request.host}{flow.request.path}") - await asyncio.sleep(5) - ctx.log.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): - # This is ugly in mitmproxy's UI, but you don't want to use mitmproxy.ctx.log from a different thread. - print(f"handle request: {flow.request.host}{flow.request.path}") - time.sleep(5) - print(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-simple.py b/examples/addons/options-simple.py index 6014bc8f94..6895a23be0 100644 --- a/examples/addons/options-simple.py +++ b/examples/addons/options-simple.py @@ -3,7 +3,7 @@ Usage: - mitmproxy -s options-simple.py --set addheader true + 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 b0314f14de..13629eeff9 100644 --- a/examples/addons/shutdown.py +++ b/examples/addons/shutdown.py @@ -8,11 +8,14 @@ and then send a HTTP request to trigger the shutdown: curl --proxy localhost:8080 http://example.com/path """ -from mitmproxy import ctx, http +import logging + +from mitmproxy import ctx +from mitmproxy import http def request(flow: http.HTTPFlow) -> None: # a random condition to make this example a bit more interactive if flow.request.pretty_url == "http://example.com/path": - ctx.log.info("Shutting down everything...") + logging.info("Shutting down everything...") ctx.master.shutdown() diff --git a/examples/addons/tcp-simple.py b/examples/addons/tcp-simple.py index 8fc3bccc8e..ed90ba48f1 100644 --- a/examples/addons/tcp-simple.py +++ b/examples/addons/tcp-simple.py @@ -10,15 +10,16 @@ mitmdump --rawtcp --tcp-hosts ".*" -s examples/tcp-simple.py """ -from mitmproxy.utils import strutils -from mitmproxy import ctx +import logging + from mitmproxy import tcp +from mitmproxy.utils import strutils def tcp_message(flow: tcp.TCPFlow): message = flow.messages[-1] message.content = message.content.replace(b"foo", b"bar") - ctx.log.info( + logging.info( f"tcp_message[from_client={message.from_client}), content={strutils.bytes_to_escaped_str(message.content)}]" ) diff --git a/examples/addons/websocket-inject-message.py b/examples/addons/websocket-inject-message.py index a0b73d24c2..715f66e4d8 100644 --- a/examples/addons/websocket-inject-message.py +++ b/examples/addons/websocket-inject-message.py @@ -5,8 +5,8 @@ """ import asyncio -from mitmproxy import ctx, http - +from mitmproxy import ctx +from mitmproxy import http # Simple example: Inject a message as a response to an event @@ -33,5 +33,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 03ad63d21d..8ced159106 100644 --- a/examples/addons/websocket-simple.py +++ b/examples/addons/websocket-simple.py @@ -1,6 +1,8 @@ """Process individual messages from a WebSocket connection.""" +import logging import re -from mitmproxy import ctx, http + +from mitmproxy import http def websocket_message(flow: http.HTTPFlow): @@ -10,9 +12,9 @@ def websocket_message(flow: http.HTTPFlow): # was the message sent from the client or server? if message.from_client: - ctx.log.info(f"Client sent a message: {message.content!r}") + logging.info(f"Client sent a message: {message.content!r}") else: - ctx.log.info(f"Server sent a message: {message.content!r}") + logging.info(f"Server sent a message: {message.content!r}") # manipulate the message content message.content = re.sub(rb"^Hello", b"HAPPY", message.content) diff --git a/examples/addons/wsgi-flask-app.py b/examples/addons/wsgi-flask-app.py index 4f117f05ab..7feab9d58b 100644 --- a/examples/addons/wsgi-flask-app.py +++ b/examples/addons/wsgi-flask-app.py @@ -6,6 +6,7 @@ a single simplest-possible page. """ from flask import Flask + from mitmproxy.addons import asgiapp app = Flask("proxapp") @@ -24,5 +25,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..153818d03b 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.focus.flow], marker) diff --git a/examples/contrib/block_dns_over_https.py b/examples/contrib/block_dns_over_https.py index 4d527af39d..2933ce6ce4 100644 --- a/examples/contrib/block_dns_over_https.py +++ b/examples/contrib/block_dns_over_https.py @@ -4,85 +4,255 @@ 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) """ - -from mitmproxy import ctx +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) @@ -96,9 +266,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 @@ -110,7 +280,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): @@ -128,12 +298,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 @@ -147,9 +317,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 @@ -172,7 +342,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, ] @@ -180,6 +350,9 @@ def request(flow): for check in doh_request_detection_checks: is_doh = check(flow) if is_doh: - ctx.log.warn("[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..0a0d540673 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 = 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 ea36e8aa23..31e0887fc6 100644 --- a/examples/contrib/custom_next_layer.py +++ b/examples/contrib/custom_next_layer.py @@ -8,8 +8,11 @@ - 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(): @@ -19,7 +22,7 @@ def running(): def next_layer(nextlayer: layer.NextLayer): - ctx.log( + logging.info( f"{nextlayer.context=}\n" f"{nextlayer.data_client()[:70]=}\n" f"{nextlayer.data_server()[:70]=}\n" diff --git a/examples/contrib/dns_spoofing.py b/examples/contrib/dns_spoofing.py index 63222eae2d..75c80e8eba 100644 --- a/examples/contrib/dns_spoofing.py +++ b/examples/contrib/dns_spoofing.py @@ -35,7 +35,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 8f70ede7f7..455597f186 100644 --- a/examples/contrib/har_dump.py +++ b/examples/contrib/har_dump.py @@ -1,221 +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 json -import base64 -import zlib -import os - -from datetime import datetime -from datetime import timezone - -import mitmproxy - -from mitmproxy import connection -from mitmproxy import version -from mitmproxy import ctx -from mitmproxy.utils import strutils -from mitmproxy.net.http import cookies - -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 - }, - "entries": [] - } - }) - - -def response(flow: mitmproxy.http.HTTPFlow): - """ - Called when a server response has been received. - """ - - # -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.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) - - -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 == '-': - mitmproxy.ctx.log(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) - - mitmproxy.ctx.log("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 f420dc411b..184b0e8166 100644 --- a/examples/contrib/http_manipulate_cookies.py +++ b/examples/contrib/http_manipulate_cookies.py @@ -15,8 +15,8 @@ """ import json -from mitmproxy import http +from mitmproxy import http PATH_TO_COOKIES = "./cookies.json" # insert your path to the cookie file here FILTER_COOKIES = { @@ -27,7 +27,7 @@ # -- Helper functions -- -def load_json_cookies() -> list[dict[str, str]]: +def load_json_cookies() -> list[dict[str, str | None]]: """ Load a particular json file containing a list of cookies. """ @@ -38,24 +38,30 @@ def load_json_cookies() -> list[dict[str, str]]: # NOTE: or just hardcode the cookies as [{"name": "", "value": ""}] -def stringify_cookies(cookies: list[dict]) -> 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']}" 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, str]]: +def parse_cookies(cookie_string: str) -> list[dict[str, str | None]]: """ Parses a cookie string into a list of cookie dicts. """ - cookies = [] - for c in cookie_string.split(";"): - c = c.strip() - if c: - k, v = c.split("=", 1) - cookies.append({"name": k, "value": v}) - return cookies + 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 + ] + ] # -- Main interception functionality -- @@ -77,14 +83,19 @@ def request(flow: http.HTTPFlow) -> None: def response(flow: http.HTTPFlow) -> None: """Remove a specific cookie from every response.""" - set_cookies_str = flow.response.headers.get("set-cookie", "") - # NOTE: use safe attribute access (.get), in some cases there might not be a set-cookie header + set_cookies_str = flow.response.headers.get_all("set-cookie") + # NOTE: According to docs, for use with the "Set-Cookie" and "Cookie" headers, either use `Response.cookies` or see `Headers.get_all`. + set_cookies_str_modified: list[str] = [] if set_cookies_str: - resp_cookies = parse_cookies(set_cookies_str) + for cookie in set_cookies_str: + resp_cookies = parse_cookies(cookie) + + # remove the cookie we want to remove + resp_cookies = [c for c in resp_cookies if c["name"] not in FILTER_COOKIES] - # remove the cookie we want to remove - resp_cookies = [c for c in resp_cookies if c["name"] not in FILTER_COOKIES] + # append the modified cookies to the list that will change the requested cookies + set_cookies_str_modified.append(stringify_cookies(resp_cookies)) # modify the request with the combined cookies - flow.response.headers["set-cookie"] = stringify_cookies(resp_cookies) + flow.response.headers.set_all("set-cookie", set_cookies_str_modified) diff --git a/examples/contrib/httpdump.py b/examples/contrib/httpdump.py index c9e9eede31..532ed1e5d5 100644 --- a/examples/contrib/httpdump.py +++ b/examples/contrib/httpdump.py @@ -9,12 +9,14 @@ # remember to add your own mitmproxy authorative 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 os +import logging import mimetypes +import os from pathlib import Path +from mitmproxy import ctx from mitmproxy import flowfilter -from mitmproxy import ctx, http +from mitmproxy import http class HTTPDump: @@ -22,16 +24,16 @@ def load(self, loader): self.filter = ctx.options.dumper_filter loader.add_option( - name = "dumper_folder", - typespec = str, - default = "httpdump", - help = "content dump destination folder", + name="dumper_folder", + typespec=str, + default="httpdump", + help="content dump destination folder", ) loader.add_option( - name = "open_browser", - typespec = bool, - default = True, - help = "open integrated browser at start" + name="open_browser", + typespec=bool, + default=True, + help="open integrated browser at start", ) def running(self): @@ -66,7 +68,7 @@ def dump(self, flow: http.HTTPFlow): if flow.response.content: with open(filepath, "wb") as f: f.write(flow.response.content) - ctx.log.info(f"Saved! {filepath}") + logging.info(f"Saved! {filepath}") addons = [HTTPDump()] diff --git a/examples/contrib/jsondump.py b/examples/contrib/jsondump.py index ab808a2d76..cfde9b75c1 100644 --- a/examples/contrib/jsondump.py +++ b/examples/contrib/jsondump.py @@ -30,10 +30,13 @@ dump_destination: "/user/rastley/output.log" EOF """ -from threading import Lock, Thread -from queue import Queue import base64 import json +import logging +from queue import Queue +from threading import Lock +from threading import Thread + import requests from mitmproxy import ctx @@ -48,6 +51,7 @@ class JSONDumper: for out-of-the-box Elasticsearch support, and then either writes the result to a file or sends it to a URL. """ + def __init__(self): self.outfile = None self.transformations = None @@ -63,76 +67,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'), - ), - 'ip': ( - ('server_conn', 'source_address'), - ('server_conn', 'ip_address'), - ('server_conn', 'address'), - ('client_conn', 'address'), + "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"), ), - 'ws_messages': ( - ('messages', ), + "ip": ( + ("server_conn", "source_address"), + ("server_conn", "ip_address"), + ("server_conn", "address"), + ("client_conn", "address"), ), - 'headers': ( - ('request', 'headers'), - ('response', 'headers'), + "ws_messages": (("messages",),), + "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): @@ -153,8 +158,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): @@ -172,8 +179,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: @@ -188,14 +195,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, _): """ @@ -204,18 +218,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 - ctx.log.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) - ctx.log.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() - ctx.log.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 6d99269d3b..f7de3b63ce 100644 --- a/examples/contrib/ntlm_upstream_proxy.py +++ b/examples/contrib/ntlm_upstream_proxy.py @@ -1,31 +1,38 @@ import base64 import binascii +import logging import socket -from typing import Any, Optional +from typing import Any +from typing import Optional -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: - ctx.log.info("NTLMUpstreamAuth loader") + logging.info("NTLMUpstreamAuth loader") loader.add_option( name="upstream_ntlm_auth", typespec=Optional[str], @@ -33,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", @@ -41,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", @@ -49,7 +56,7 @@ def load(self, loader: addonmanager.Loader) -> None: default=None, help=""" upstream poxy address. - """ + """, ) loader.add_option( name="upstream_ntlm_compatibility", @@ -58,22 +65,26 @@ 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) - """ + """, ) - ctx.log.debug("AddOn: NTLM Upstream Authentication - Loaded") + 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: - ctx.log.error("failed to build connect flow") + logging.error("failed to build connect flow") raise flow.request.content = b"" # we should send empty content for handshake header_name, header_value = connect_header @@ -84,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: - ctx.log.error("Failed to extract challenge_message") + 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: @@ -118,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 @@ -130,27 +151,26 @@ def patched_receive_handshake_data(self, data) -> layer.CommandGenerator[tuple[b HttpUpstreamProxy.receive_handshake_data = patched_receive_handshake_data def done(self): - ctx.log.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() ntlm_compatibility: int = ctx.options.upstream_ntlm_compatibility username, password = tuple(auth.split(":")) workstation = socket.gethostname().upper() - ctx.log.debug(f'\nntlm context with the details: "{domain}\\{username}", *****') - self.ctx_log = ctx.log + logging.debug(f'\nntlm context with the details: "{domain}\\{username}", *****') self.preferred_type = preferred_type self.ntlm_context = ntlm.NtlmContext( username=username, @@ -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}' - self.ctx_log.debug( - f'{self.preferred_type} Authentication, negotiate message: {negotiate_message_base_64_final}' + 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}" ) 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: - self.ctx_log.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')) - self.ctx_log.debug( - f'{self.preferred_type} Authentication, response to challenge message: {negotiate_message_base_64}' + 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}" ) 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 e258f963e1..6407705964 100644 --- a/examples/contrib/save_streamed_data.py +++ b/examples/contrib/save_streamed_data.py @@ -18,16 +18,16 @@ 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 +from pathlib import Path from typing import Optional from mitmproxy import ctx -from datetime import datetime -from pathlib import Path -import os class StreamSaver: - TAG = "save_streamed_data: " def __init__(self, flow, direction): @@ -58,11 +58,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 @@ -70,43 +72,45 @@ def __call__(self, data): if not parent.exists(): parent.mkdir(parents=True, exist_ok=True) except OSError: - ctx.log.error(f"{self.TAG}Failed to create directory: {parent}") + logging.error(f"{self.TAG}Failed to create directory: {parent}") try: self.fh = open(self.path, "wb", buffering=0) except OSError: - ctx.log.error(f"{self.TAG}Failed to open for writing: {self.path}") + logging.error(f"{self.TAG}Failed to open for writing: {self.path}") if self.fh: try: self.fh.write(data) except OSError: - ctx.log.error(f"{self.TAG}Failed to write to: {self.path}") + logging.error(f"{self.TAG}Failed to write to: {self.path}") return 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 c60361698b..73d775d2be 100644 --- a/examples/contrib/search.py +++ b/examples/contrib/search.py @@ -1,23 +1,21 @@ +import logging import re from collections.abc import Sequence - from json import dumps -from mitmproxy import command, ctx, 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 @@ -44,17 +42,17 @@ def _search(self, try: self.exp = re.compile(regex) except re.error as e: - ctx.log.error(e) + logging.error(e) return 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 @@ -63,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): @@ -72,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..6b88c39565 100644 --- a/examples/contrib/sslstrip.py +++ b/examples/contrib/sslstrip.py @@ -12,15 +12,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 +31,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..5cb319ef65 100644 --- a/examples/contrib/suppress_error_responses.py +++ b/examples/contrib/suppress_error_responses.py @@ -10,7 +10,7 @@ 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..abb85c2903 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,7 +36,7 @@ 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: @@ -45,24 +45,28 @@ def test_contentencode(self, tmpdir): 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 25237c4f80..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,130 +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') - def logger(self, monkeypatch): - class Logger(): - def __init__(self): - self.args = [] - - def info(self, str): - self.args.append(str) - - def error(self, str): - self.args.append(str) - - logger = Logger() - monkeypatch.setattr("mitmproxy.ctx.log", logger) - yield logger - - @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() @@ -379,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 811d56abfe..d0237d7937 100644 --- a/examples/contrib/tls_passthrough.py +++ b/examples/contrib/tls_passthrough.py @@ -15,11 +15,16 @@ // 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 @@ -53,6 +58,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] @@ -61,6 +67,7 @@ class ProbabilisticStrategy(TlsStrategy): """ Fixed probability that we intercept a given connection. """ + def __init__(self, p: float): self.p = p super().__init__() @@ -72,9 +79,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.", ) @@ -87,21 +96,28 @@ 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): - ctx.log(f"TLS passthrough: {human.format_address(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 - ctx.log(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 - ctx.log(f"TLS handshake failed: {human.format_address(server_address)}") + 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 368662cfbd..eab9510a07 100644 --- a/examples/contrib/xss_scanner.py +++ b/examples/contrib/xss_scanner.py @@ -34,18 +34,17 @@ Line: 1029zxcs'd"aoso[sb]po(pc)se;sl/bsl\eq=3847asd """ - -from html.parser import HTMLParser -from typing import NamedTuple, Optional, Union -from urllib.parse import urlparse +import logging import re import socket +from html.parser import HTMLParser +from typing import NamedTuple +from typing import Optional +from urllib.parse import urlparse import requests from mitmproxy import http -from mitmproxy import ctx - # The actual payload is put between a frontWall and a backWall to make it easy # to locate the payload with regular expressions @@ -84,15 +83,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""" - def getValue(attrs: list[tuple[str, str]], attrName: str) -> Optional[str]: + """Look for unclaimed URLs in script tags and log them if found""" + + def getValue(attrs: list[tuple[str, str]], attrName: str) -> str | None: for name, value in attrs: if attrName == name: return value @@ -102,9 +102,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() @@ -115,17 +121,21 @@ def handle_starttag(self, tag, attrs): try: socket.gethostbyname(domain) except socket.gaierror: - ctx.log.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") @@ -133,31 +143,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() @@ -166,75 +187,112 @@ 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 - ctx.log.error("===== XSS Found ====") - ctx.log.error("XSS URL: %s" % xss_info.url) - ctx.log.error("Injection Point: %s" % xss_info.injection_point) - ctx.log.error("Suggested Exploit: %s" % xss_info.exploit) - ctx.log.error("Line: %s" % xss_info.line) + logging.error("===== XSS Found ====") + logging.error("XSS URL: %s" % xss_info.url) + logging.error("Injection Point: %s" % xss_info.injection_point) + logging.error("Suggested Exploit: %s" % xss_info.exploit) + 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 - ctx.log.error("===== SQLi Found =====") - ctx.log.error("SQLi URL: %s" % sqli_info.url) - ctx.log.error("Injection Point: %s" % sqli_info.injection_point) - ctx.log.error("Regex used: %s" % sqli_info.regex) - ctx.log.error("Suspected DBMS: %s" % sqli_info.dbms) + logging.error("===== SQLi Found =====") + logging.error("SQLi URL: %s" % sqli_info.url) + logging.error("Injection Point: %s" % sqli_info.injection_point) + logging.error("Regex used: %s" % sqli_info.regex) + logging.error("Suspected DBMS: %s" % sqli_info.dbms) 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 @@ -246,25 +304,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: @@ -275,12 +335,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 +351,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,21 +364,23 @@ 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() parser.feed(html) return parser.injectJSHandler + # 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) @@ -334,64 +399,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 27c3a8a560..5f918b75b7 100644 --- a/mitmproxy/addonmanager.py +++ b/mitmproxy/addonmanager.py @@ -1,17 +1,20 @@ import contextlib 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 +from typing import Any -from mitmproxy import hooks from mitmproxy import exceptions from mitmproxy import flow -from . import ctx +from mitmproxy import hooks + +logger = logging.getLogger(__name__) def _get_name(itm): @@ -48,8 +51,11 @@ def safecall(): etype, value, tb = sys.exc_info() tb = cut_traceback(tb, "invoke_addon_sync") tb = cut_traceback(tb, "invoke_addon") - ctx.log.error( - "Addon error: %s" % "".join(traceback.format_exception(etype, value, tb)) + assert etype + assert value + logger.error( + f"Addon error: {value}", + exc_info=(etype, value, tb), ) @@ -67,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. @@ -76,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 = ( @@ -88,7 +95,7 @@ def add_option( if same_signature: return else: - ctx.log.warn("Over-riding existing option %s" % name) + logger.warning("Over-riding existing option %s" % name) self.master.options.add_option(name, typespec, default, help, choices) def add_command(self, path: str, func: Callable) -> None: @@ -128,7 +135,7 @@ def __init__(self, master): self.master = master master.options.changed.connect(self._configure_all) - def _configure_all(self, options, updated): + def _configure_all(self, updated): self.trigger(hooks.ConfigureHook(updated)) def clear(self): @@ -160,25 +167,26 @@ def register(self, addon): """ api_changes = { # mitmproxy 6 -> mitmproxy 7 - "clientconnect": "client_connected", - "clientdisconnect": "client_disconnected", - "serverconnect": "server_connect and server_connected", - "serverdisconnect": "server_disconnected", + "clientconnect": f"The clientconnect event has been removed, use client_connected instead", + "clientdisconnect": f"The clientdisconnect event has been removed, use client_disconnected instead", + "serverconnect": "The serverconnect event has been removed, use server_connect and server_connected instead", + "serverdisconnect": f"The serverdisconnect event has been removed, use server_disconnected instead", + # mitmproxy 8 -> mitmproxy 9 + "add_log": "The add_log event has been deprecated, use Python's builtin logging module instead", } for a in traverse([addon]): - for old, new in api_changes.items(): + for old, msg in api_changes.items(): if hasattr(a, old): - ctx.log.warn( - f"The {old} event has been removed, use {new} instead. " - f"For more details, see https://docs.mitmproxy.org/stable/addons-events/." + logger.warning( + f"{msg}. For more details, see https://docs.mitmproxy.org/dev/addons-api-changelog/." ) name = _get_name(a) if name in self.lookup: 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..fd36bb0cdd 100644 --- a/mitmproxy/addons/__init__.py +++ b/mitmproxy/addons/__init__.py @@ -11,19 +11,20 @@ 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 tlsconfig from mitmproxy.addons import upstream_auth @@ -56,6 +57,7 @@ def default_addons(): stickyauth.StickyAuth(), stickycookie.StickyCookie(), save.Save(), + savehar.SaveHar(), tlsconfig.TlsConfig(), upstream_auth.UpstreamAuth(), ] diff --git a/mitmproxy/addons/asgiapp.py b/mitmproxy/addons/asgiapp.py index c0bafa10ac..785d7982b7 100644 --- a/mitmproxy/addons/asgiapp.py +++ b/mitmproxy/addons/asgiapp.py @@ -1,10 +1,14 @@ import asyncio -import traceback +import logging 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__) class ASGIApp: @@ -16,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 @@ -26,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 @@ -38,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) @@ -120,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 @@ -131,8 +137,8 @@ async def send(event): await app(scope, receive, send) if not sent_response: raise RuntimeError(f"no response sent.") - except Exception: - ctx.log.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 45301c6545..8f6e37c195 100644 --- a/mitmproxy/addons/block.py +++ b/mitmproxy/addons/block.py @@ -1,4 +1,6 @@ import ipaddress +import logging + from mitmproxy import ctx @@ -33,13 +35,13 @@ def client_connected(self, client): return if ctx.options.block_private and address.is_private: - ctx.log.warn( + logging.warning( f"Client connection from {client.peername[0]} killed by block_private option." ) client.error = "Connection killed by block_private." if ctx.options.block_global and address.is_global: - ctx.log.warn( + logging.warning( f"Client connection from {client.peername[0]} killed by block_global option." ) client.error = "Connection killed by block_global." diff --git a/mitmproxy/addons/blocklist.py b/mitmproxy/addons/blocklist.py index 4945fa4997..6c99bca067 100644 --- a/mitmproxy/addons/blocklist.py +++ b/mitmproxy/addons/blocklist.py @@ -1,7 +1,11 @@ from collections.abc import Sequence from typing import NamedTuple -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 @@ -36,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): diff --git a/mitmproxy/addons/browser.py b/mitmproxy/addons/browser.py index 68d974eb0d..4c3a6bc274 100644 --- a/mitmproxy/addons/browser.py +++ b/mitmproxy/addons/browser.py @@ -1,13 +1,14 @@ +import logging 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]: +def get_chrome_executable() -> str | None: for browser in ( "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", # https://stackoverflow.com/questions/40674914/google-chrome-path-in-windows-10 @@ -27,7 +28,7 @@ def get_chrome_executable() -> Optional[str]: return None -def get_chrome_flatpak() -> Optional[str]: +def get_chrome_flatpak() -> str | None: if shutil.which("flatpak"): for browser in ( "com.google.Chrome", @@ -48,7 +49,7 @@ def get_chrome_flatpak() -> Optional[str]: return None -def get_browser_cmd() -> Optional[list[str]]: +def get_browser_cmd() -> list[str] | None: if browser := get_chrome_executable(): return [browser] elif browser := get_chrome_flatpak(): @@ -68,11 +69,13 @@ def start(self) -> None: running proxy. """ if len(self.browser) > 0: - ctx.log.alert("Starting additional browser") + logging.log(ALERT, "Starting additional browser") cmd = get_browser_cmd() if not cmd: - ctx.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() @@ -83,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 + ctx.options.listen_host or "127.0.0.1", + ctx.options.listen_port or "8080", ), "--disable-fre", "--no-default-browser-check", diff --git a/mitmproxy/addons/clientplayback.py b/mitmproxy/addons/clientplayback.py index fb6dfad1d1..53b202505a 100644 --- a/mitmproxy/addons/clientplayback.py +++ b/mitmproxy/addons/clientplayback.py @@ -1,8 +1,12 @@ +from __future__ import annotations + import asyncio +import logging import time -import traceback from collections.abc import Sequence -from typing import Optional, cast +from types import TracebackType +from typing import cast +from typing import Literal import mitmproxy.types from mitmproxy import command @@ -11,16 +15,23 @@ from mitmproxy import flow from mitmproxy import http from mitmproxy import io +from mitmproxy.connection import ConnectionState +from mitmproxy.connection import Server from mitmproxy.hooks import UpdateHook -from mitmproxy.net import server_spec +from mitmproxy.log import ALERT from mitmproxy.options import Options +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.layers.http import HTTPMode -from mitmproxy.proxy import commands, events, layers, server -from mitmproxy.connection import ConnectionState, Server from mitmproxy.proxy.layer import CommandGenerator +from mitmproxy.proxy.layers.http import HTTPMode +from mitmproxy.proxy.mode_specs import UpstreamMode from mitmproxy.utils import asyncio_utils +logger = logging.getLogger(__name__) + class MockServer(layers.http.HttpConnection): """ @@ -68,7 +79,7 @@ def _handle_event(self, event: events.Event) -> CommandGenerator[None]: ): pass else: # pragma: no cover - ctx.log(f"Unexpected event during replay: {event}") + logger.warning(f"Unexpected event during replay: {event}") class ReplayHandler(server.ConnectionHandler): @@ -79,16 +90,18 @@ 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" - if options.mode.startswith("upstream:"): - context.server.via = flow.server_conn.via = server_spec.parse_with_mode( - options.mode - )[1] + 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. + context.server.via = flow.server_conn.via = (mode.scheme, mode.address) super().__init__(context) - if options.mode.startswith("upstream:"): + if options.mode and options.mode[0].startswith("upstream:"): self.layer = layers.HttpLayer(context, HTTPMode.upstream) else: self.layer = layers.HttpLayer(context, HTTPMode.transparent) @@ -100,8 +113,16 @@ async def replay(self) -> None: self.server_event(events.Start()) await self.done.wait() - def log(self, message: str, level: str = "info") -> None: - ctx.log(f"[replay] {message}", level) + 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}") async def handle_hook(self, hook: commands.StartHook) -> None: (data,) = hook.args() @@ -122,15 +143,17 @@ 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.playback_task = asyncio_utils.create_task( @@ -146,21 +169,23 @@ 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: - ctx.log( - f"Client replay has crashed!\n{traceback.format_exc()}", "error" - ) + 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: @@ -228,7 +253,7 @@ def stop_replay(self) -> None: updated.append(f) ctx.master.addons.trigger(UpdateHook(updated)) - ctx.log.alert("Client replay queue cleared.") + logger.log(ALERT, "Client replay queue cleared.") @command.command("replay.client") def start_replay(self, flows: Sequence[flow.Flow]) -> None: @@ -239,7 +264,7 @@ def start_replay(self, flows: Sequence[flow.Flow]) -> None: for f in flows: err = self.check(f) if err: - ctx.log.warn(err) + logger.warning(err) continue http_flow = cast(http.HTTPFlow, f) diff --git a/mitmproxy/addons/command_history.py b/mitmproxy/addons/command_history.py index 96c71387a1..507b60e500 100644 --- a/mitmproxy/addons/command_history.py +++ b/mitmproxy/addons/command_history.py @@ -1,3 +1,4 @@ +import logging import os import pathlib from collections.abc import Sequence @@ -44,7 +45,7 @@ def done(self): try: self.history_file.write_text(history_str) except Exception as e: - ctx.log.alert(f"Failed writing to {self.history_file}: {e}") + logging.warning(f"Failed writing to {self.history_file}: {e}") @command.command("commands.history.add") def add_command(self, command: str) -> None: @@ -57,7 +58,7 @@ def add_command(self, command: str) -> None: with self.history_file.open("a") as f: f.write(f"{command}\n") except Exception as e: - ctx.log.alert(f"Failed writing to {self.history_file}: {e}") + logging.warning(f"Failed writing to {self.history_file}: {e}") self.set_filter("") @@ -72,7 +73,7 @@ def clear_history(self): try: self.history_file.unlink() except Exception as e: - ctx.log.alert(f"Failed deleting {self.history_file}: {e}") + logging.warning(f"Failed deleting {self.history_file}: {e}") self.history = [] self.set_filter("") 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 230afac4a0..5cd3f79adb 100644 --- a/mitmproxy/addons/core.py +++ b/mitmproxy/addons/core.py @@ -1,18 +1,19 @@ +import logging import os from collections.abc import Sequence -from typing import Union -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 import platform -from mitmproxy.net import server_spec +from mitmproxy.log import ALERT from mitmproxy.net.http import status_codes -import mitmproxy.types +from mitmproxy.utils import emoji +logger = logging.getLogger(__name__) CONF_DIR = "~/.mitmproxy" LISTEN_PORT = 8080 @@ -25,20 +26,6 @@ def configure(self, updated): raise exceptions.OptionsError( "add_upstream_certs_to_client_chain requires the upstream_cert option to be enabled." ) - if "mode" in updated: - mode = opts.mode - if mode.startswith("reverse:") or mode.startswith("upstream:"): - try: - server_spec.parse_with_mode(mode) - except ValueError as e: - raise exceptions.OptionsError(str(e)) from e - elif mode == "transparent": - if not platform.original_addr: - raise exceptions.OptionsError( - "Transparent mode not supported on this platform." - ) - elif mode not in ["regular", "socks5"]: - raise exceptions.OptionsError("Invalid mode specification: %s" % mode) if "client_certs" in updated: if opts.client_certs: client_certs = os.path.expanduser(opts.client_certs) @@ -112,7 +99,7 @@ def kill(self, flows: Sequence[flow.Flow]) -> None: if f.killable: f.kill() updated.append(f) - ctx.log.alert("Killed %s flows." % len(updated)) + logger.log(ALERT, "Killed %s flows." % len(updated)) ctx.master.addons.trigger(hooks.UpdateHook(updated)) # FIXME: this will become view.revert later @@ -126,7 +113,7 @@ def revert(self, flows: Sequence[flow.Flow]) -> None: if f.modified(): f.revert() updated.append(f) - ctx.log.alert("Reverted %s flows." % len(updated)) + logger.log(ALERT, "Reverted %s flows." % len(updated)) ctx.master.addons.trigger(hooks.UpdateHook(updated)) @command.command("flow.set.options") @@ -146,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 @@ -192,7 +179,7 @@ def flow_set(self, flows: Sequence[flow.Flow], attr: str, value: str) -> None: updated.append(f) ctx.master.addons.trigger(hooks.UpdateHook(updated)) - ctx.log.alert(f"Set {attr} on {len(updated)} flows.") + logger.log(ALERT, f"Set {attr} on {len(updated)} flows.") @command.command("flow.decode") def decode(self, flows: Sequence[flow.Flow], part: str) -> None: @@ -207,7 +194,7 @@ def decode(self, flows: Sequence[flow.Flow], part: str) -> None: p.decode() updated.append(f) ctx.master.addons.trigger(hooks.UpdateHook(updated)) - ctx.log.alert("Decoded %s flows." % len(updated)) + logger.log(ALERT, "Decoded %s flows." % len(updated)) @command.command("flow.encode.toggle") def encode_toggle(self, flows: Sequence[flow.Flow], part: str) -> None: @@ -226,7 +213,7 @@ def encode_toggle(self, flows: Sequence[flow.Flow], part: str) -> None: p.decode() updated.append(f) ctx.master.addons.trigger(hooks.UpdateHook(updated)) - ctx.log.alert("Toggled encoding on %s flows." % len(updated)) + logger.log(ALERT, "Toggled encoding on %s flows." % len(updated)) @command.command("flow.encode") @command.argument("encoding", type=mitmproxy.types.Choice("flow.encode.options")) @@ -249,7 +236,7 @@ def encode( p.encode(encoding) updated.append(f) ctx.master.addons.trigger(hooks.UpdateHook(updated)) - ctx.log.alert("Encoded %s flows." % len(updated)) + logger.log(ALERT, "Encoded %s flows." % len(updated)) @command.command("flow.encode.options") def encode_options(self) -> Sequence[str]: diff --git a/mitmproxy/addons/cut.py b/mitmproxy/addons/cut.py index 1c8a171ee6..c15ac3f539 100644 --- a/mitmproxy/addons/cut.py +++ b/mitmproxy/addons/cut.py @@ -1,17 +1,20 @@ -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 ctx -from mitmproxy import certs -import mitmproxy.types +from mitmproxy.log import ALERT -import pyperclip +logger = logging.getLogger(__name__) def headername(spec: str): @@ -24,7 +27,7 @@ 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: path = cut.split(".") current: Any = f for i, spec in enumerate(path): @@ -82,7 +85,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 @@ -117,7 +120,7 @@ def save( fp.write(v) else: fp.write(v.encode("utf8")) - ctx.log.alert("Saved single cut.") + logger.log(ALERT, "Saved single cut.") else: with open( path, "a" if append else "w", newline="", encoding="utf8" @@ -126,11 +129,12 @@ def save( for f in flows: vals = [extract_str(c, f) for c in cuts] writer.writerow(vals) - ctx.log.alert( - "Saved %s cuts over %d flows as CSV." % (len(cuts), len(flows)) + logger.log( + ALERT, + "Saved %s cuts over %d flows as CSV." % (len(cuts), len(flows)), ) except OSError as e: - ctx.log.error(str(e)) + logger.error(str(e)) @command.command("cut.clip") def clip( @@ -143,19 +147,19 @@ 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]) fp.write(v) - ctx.log.alert("Clipped single cut.") + logger.log(ALERT, "Clipped single cut.") else: writer = csv.writer(fp) for f in flows: vals = [extract_str(c, f) for c in cuts] writer.writerow(vals) - ctx.log.alert("Clipped %s cuts as CSV." % len(cuts)) + logger.log(ALERT, "Clipped %s cuts as CSV." % len(cuts)) try: pyperclip.copy(fp.getvalue()) except pyperclip.PyperclipException as e: - ctx.log.error(str(e)) + logger.error(str(e)) diff --git a/mitmproxy/addons/disable_h2c.py b/mitmproxy/addons/disable_h2c.py index b2652f4849..595ce2b3f0 100644 --- a/mitmproxy/addons/disable_h2c.py +++ b/mitmproxy/addons/disable_h2c.py @@ -1,4 +1,4 @@ -import mitmproxy +import logging class DisableH2C: @@ -16,7 +16,7 @@ class DisableH2C: def process_flow(self, f): if f.request.headers.get("upgrade", "") == "h2c": - mitmproxy.ctx.log.warn( + logging.warning( "HTTP/2 cleartext connections (h2c upgrade requests) are currently not supported." ) del f.request.headers["upgrade"] @@ -32,7 +32,7 @@ def process_flow(self, f): ) if is_connection_preface: f.kill() - mitmproxy.ctx.log.warn( + logging.warning( "Initiating HTTP/2 connections with prior knowledge are currently not supported." ) diff --git a/mitmproxy/addons/dns_resolver.py b/mitmproxy/addons/dns_resolver.py index e1af088b3e..63718050eb 100644 --- a/mitmproxy/addons/dns_resolver.py +++ b/mitmproxy/addons/dns_resolver.py @@ -1,8 +1,11 @@ import asyncio import ipaddress import socket -from typing import Callable, Iterable, Union -from mitmproxy import ctx, dns +from collections.abc import Callable +from collections.abc import Iterable + +from mitmproxy import dns +from mitmproxy.proxy import mode_specs IP4_PTR_SUFFIX = ".in-addr.arpa" IP6_PTR_SUFFIX = ".ip6.arpa" @@ -20,7 +23,7 @@ async def resolve_question_by_name( question: dns.Question, loop: asyncio.AbstractEventLoop, family: socket.AddressFamily, - ip: Callable[[str], Union[ipaddress.IPv4Address, ipaddress.IPv6Address]], + ip: Callable[[str], ipaddress.IPv4Address | ipaddress.IPv6Address], ) -> Iterable[dns.ResourceRecord]: try: addrinfos = await loop.getaddrinfo(host=question.name, port=0, family=family) @@ -30,7 +33,7 @@ async def resolve_question_by_name( 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) + raise ResolveError(dns.response_codes.SERVFAIL) # pragma: no cover return map( lambda addrinfo: dns.ResourceRecord( name=question.name, @@ -47,7 +50,7 @@ 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]]], + sockaddr: Callable[[list[str]], tuple[str, int] | tuple[str, int, int, int]], ) -> Iterable[dns.ResourceRecord]: try: addr = sockaddr(question.name[: -len(suffix)].split(".")[::-1]) @@ -138,12 +141,19 @@ async def resolve_message( class DnsResolver: async def dns_request(self, flow: dns.DNSFlow) -> None: should_resolve = ( - flow.live + ( + isinstance(flow.client_conn.proxy_mode, mode_specs.DnsMode) + or ( + isinstance(flow.client_conn.proxy_mode, mode_specs.WireGuardMode) + and flow.server_conn.address == ("10.0.0.53", 53) + ) + ) + and flow.live and not flow.response and not flow.error - and ctx.options.dns_mode == "regular" ) if should_resolve: + # TODO: We need to handle overly long responses here. flow.response = await resolve_message( flow.request, asyncio.get_running_loop() ) diff --git a/mitmproxy/addons/dumper.py b/mitmproxy/addons/dumper.py index 7da08ecf4c..b76c34832a 100644 --- a/mitmproxy/addons/dumper.py +++ b/mitmproxy/addons/dumper.py @@ -1,7 +1,11 @@ +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 @@ -13,20 +17,25 @@ from mitmproxy import flowfilter from mitmproxy import http from mitmproxy.contrib import click as miniclick -from mitmproxy.tcp import TCPFlow, TCPMessage +from mitmproxy.net.dns import response_codes +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), @@ -35,8 +44,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) @@ -93,7 +102,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) @@ -101,19 +110,19 @@ 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, WebSocketMessage], - flow: Union[http.HTTPFlow, TCPFlow], + 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 ) if error: - ctx.log.debug(error) + logging.debug(error) if ctx.options.flow_detail == 3: lines_to_echo = itertools.islice(lines, 70) @@ -200,7 +209,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, "") @@ -311,34 +320,60 @@ def websocket_end(self, f: http.HTTPFlow): def format_websocket_error(self, websocket: WebSocketData) -> str: try: - ret = CloseReason(websocket.close_code).name + ret = CloseReason(websocket.close_code).name # type: ignore except ValueError: ret = f"UNKNOWN_ERROR={websocket.close_code}" if websocket.close_reason: ret += f" (reason: {websocket.close_reason})" return ret - def tcp_error(self, f): + def _proto_error(self, f): if self.match(f): self.echo( - f"Error in TCP connection to {human.format_address(f.server_conn.address)}: {f.error}", + f"Error in {f.type.upper()} connection to {human.format_address(f.server_conn.address)}: {f.error}", fg="red", ) - def tcp_message(self, f): + def tcp_error(self, f): + self._proto_error(f) + + def udp_error(self, f): + self._proto_error(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 == "QUIC": + 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} tcp {direction} {server}".format( + "{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=flow_type, ) ) if ctx.options.flow_detail >= 3: self._echo_message(message, f) + def tcp_message(self, f): + self._proto_message(f) + + def udp_message(self, f): + self._proto_message(f) + def _echo_dns_query(self, f: dns.DNSFlow) -> None: client = self._fmt_client(f) opcode = dns.op_codes.to_str(f.request.op_code) @@ -346,8 +381,8 @@ def _echo_dns_query(self, f: dns.DNSFlow) -> None: desc = f"DNS {opcode} ({type})" desc_color = { - "DNS QUERY (A)": "green", - "DNS QUERY (AAAA)": "magenta", + "A": "green", + "AAAA": "magenta", }.get(type, "red") desc = self.style(desc, fg=desc_color) @@ -360,9 +395,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 6669ee435e..9b6eff66a1 100644 --- a/mitmproxy/addons/errorcheck.py +++ b/mitmproxy/addons/errorcheck.py @@ -1,4 +1,5 @@ import asyncio +import logging import sys from mitmproxy import log @@ -7,23 +8,41 @@ class ErrorCheck: """Monitor startup for error log entries, and terminate immediately if there are some.""" - def __init__(self, log_to_stderr: bool = False): - self.has_errored: list[str] = [] - 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 add_log(self, e: log.LogEntry): - if e.level == "error": - self.has_errored.append(e.msg) + def __init__(self, repeat_errors_on_stderr: bool = False) -> None: + self.repeat_errors_on_stderr = repeat_errors_on_stderr - async def running(self): - # don't run immediately, wait for all logging tasks to finish. - asyncio.create_task(self._shutdown_if_errored()) + self.logger = ErrorCheckHandler() + self.logger.install() + + def finish(self): + self.logger.uninstall() - async def _shutdown_if_errored(self): - if self.has_errored: - if self.log_to_stderr: - plural = "s" if len(self.has_errored) > 1 else "" - msg = "\n".join(self.has_errored) - print(f"Error{plural} on startup: {msg}", file=sys.stderr) + 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: + plural = "s" if len(self.logger.has_errored) > 1 else "" + if self.repeat_errors_on_stderr: + msg = "\n".join(self.logger.format(r) for r in self.logger.has_errored) + print(f"Error{plural} logged during startup:\n{msg}", 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) -> None: + super().__init__(logging.ERROR) + self.has_errored: list[logging.LogRecord] = [] + + def emit(self, record: logging.LogRecord) -> None: + self.has_errored.append(record) diff --git a/mitmproxy/addons/eventstore.py b/mitmproxy/addons/eventstore.py index 07dbdf06bb..b474a5f7c0 100644 --- a/mitmproxy/addons/eventstore.py +++ b/mitmproxy/addons/eventstore.py @@ -1,25 +1,33 @@ +import asyncio import collections -from typing import Optional - -import blinker +import logging +from collections.abc import Callable 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 = blinker.Signal() - self.sig_refresh = blinker.Signal() + self.sig_add = signals.SyncSignal(lambda entry: None) + self.sig_refresh = signals.SyncSignal(lambda: None) - @property - def size(self) -> Optional[int]: - return self.data.maxlen + self.logger = CallbackLogger(self._add_log) + self.logger.install() - def add_log(self, entry: LogEntry) -> None: + def done(self): + self.logger.uninstall() + + def _add_log(self, entry: LogEntry) -> None: self.data.append(entry) - self.sig_add.send(self, entry=entry) + self.sig_add.send(entry) + + @property + def size(self) -> int | None: + return self.data.maxlen @command.command("eventstore.clear") def clear(self) -> None: @@ -27,4 +35,22 @@ def clear(self) -> None: Clear the event log. """ self.data.clear() - self.sig_refresh.send(self) + self.sig_refresh.send() + + +class CallbackLogger(log.MitmLogHandler): + def __init__( + self, + callback: Callable[[LogEntry], None], + ): + super().__init__() + self.callback = callback + self.event_loop = asyncio.get_running_loop() + self.formatter = log.MitmFormatter(colorize=False) + + def emit(self, record: logging.LogRecord) -> None: + entry = LogEntry( + msg=self.format(record), + level=log.LOGGING_LEVELS_TO_LOGENTRY.get(record.levelno, "error"), + ) + self.event_loop.call_soon_threadsafe(self.callback, entry) diff --git a/mitmproxy/addons/export.py b/mitmproxy/addons/export.py index 731c4f3420..fb5ce2d5db 100644 --- a/mitmproxy/addons/export.py +++ b/mitmproxy/addons/export.py @@ -1,14 +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 @@ -138,7 +141,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, @@ -185,7 +188,7 @@ def file(self, format: str, flow: flow.Flow, path: mitmproxy.types.Path) -> None else: fp.write(v.encode("utf-8")) except OSError as e: - ctx.log.error(str(e)) + logging.error(str(e)) @command.command("export.clip") def clip(self, format: str, f: flow.Flow) -> None: @@ -195,7 +198,7 @@ def clip(self, format: str, f: flow.Flow) -> None: try: pyperclip.copy(self.export_str(format, f)) except pyperclip.PyperclipException as e: - ctx.log.error(str(e)) + logging.error(str(e)) @command.command("export") def export_str(self, format: str, f: flow.Flow) -> str: diff --git a/mitmproxy/addons/intercept.py b/mitmproxy/addons/intercept.py index d021dc2f4c..dff16e459d 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") @@ -49,6 +50,9 @@ def response(self, f): def tcp_message(self, f): self.process_flow(f) + def udp_message(self, f): + self.process_flow(f) + def dns_request(self, f): self.process_flow(f) diff --git a/mitmproxy/addons/keepserving.py b/mitmproxy/addons/keepserving.py index 5a149fdec1..efe296ab36 100644 --- a/mitmproxy/addons/keepserving.py +++ b/mitmproxy/addons/keepserving.py @@ -1,8 +1,13 @@ +from __future__ import annotations + import asyncio + from mitmproxy import ctx class KeepServing: + _watch_task: asyncio.Task | None = None + def load(self, loader): loader.add_option( "keepserving", @@ -39,4 +44,4 @@ 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.get_running_loop().create_task(self.watch()) diff --git a/mitmproxy/addons/maplocal.py b/mitmproxy/addons/maplocal.py index 4ced741ce3..5b1abd0b53 100644 --- a/mitmproxy/addons/maplocal.py +++ b/mitmproxy/addons/maplocal.py @@ -1,3 +1,4 @@ +import logging import mimetypes import re import urllib.parse @@ -7,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 @@ -75,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): @@ -133,7 +138,7 @@ def request(self, flow: http.HTTPFlow) -> None: try: contents = local_file.read_bytes() except OSError as e: - ctx.log.warn(f"Could not read file: {e}") + logging.warning(f"Could not read file: {e}") continue flow.response = http.Response.make(200, contents, headers) @@ -141,6 +146,6 @@ def request(self, flow: http.HTTPFlow) -> None: return if all_candidates: flow.response = http.Response.make(404) - ctx.log.info( + logging.info( f"None of the local file candidates exist: {', '.join(str(x) for x in all_candidates)}" ) 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 c8e3760a17..657dbe07ce 100644 --- a/mitmproxy/addons/modifybody.py +++ b/mitmproxy/addons/modifybody.py @@ -1,12 +1,18 @@ +import logging 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): @@ -34,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 @@ -50,7 +68,7 @@ def run(self, flow): try: replacement = spec.read_replacement() except OSError as e: - ctx.log.warn(f"Could not read replacement file: {e}") + logging.warning(f"Could not read replacement file: {e}") continue if flow.response: flow.response.content = re.sub( diff --git a/mitmproxy/addons/modifyheaders.py b/mitmproxy/addons/modifyheaders.py index d571d4a10a..a7e45b0ddd 100644 --- a/mitmproxy/addons/modifyheaders.py +++ b/mitmproxy/addons/modifyheaders.py @@ -1,9 +1,13 @@ +import logging import re from collections.abc import Sequence 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 @@ -50,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): @@ -106,7 +110,7 @@ def run(self, flow: http.HTTPFlow, hdrs: Headers) -> None: try: replacement = spec.read_replacement() except OSError as e: - ctx.log.warn(f"Could not read replacement file: {e}") + logging.warning(f"Could not read replacement file: {e}") continue else: if replacement: diff --git a/mitmproxy/addons/next_layer.py b/mitmproxy/addons/next_layer.py index e2e57e96b9..ce63f81390 100644 --- a/mitmproxy/addons/next_layer.py +++ b/mitmproxy/addons/next_layer.py @@ -8,28 +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 struct +import sys +from collections.abc import Iterable from collections.abc import Sequence -from typing import Any, Iterable, Optional, Union +from typing import Any +from typing import cast -from mitmproxy import ctx, 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 mitmproxy import ctx +from mitmproxy import dns +from mitmproxy import exceptions +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, 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 mitmproxy.proxy.layers.tls import dtls_parse_client_hello +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 @@ -39,16 +72,25 @@ 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] = () + 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: self.tcp_hosts = [ re.compile(x, re.IGNORECASE) for x in ctx.options.tcp_hosts ] + if "udp_hosts" in updated: + self.udp_hosts = [ + 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( @@ -61,33 +103,130 @@ def configure(self, updated): re.compile(x, re.IGNORECASE) for x in ctx.options.allow_hosts ] - def ignore_connection( - self, server_address: Optional[connection.Address], data_client: bytes - ) -> Optional[bool]: + 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.info( + f"Deferring layer decision, not enough data: {nextlayer.data_client().hex()}" + ) + + 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): + return ( + layers.TCPLayer(context, ignore=True) + if tcp_based + else layers.UDPLayer(context, ignore=True) + ) + + # 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): + 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) Is it DNS? + if udp_based: + try: + # TODO: DNS over TCP + dns.Message.unpack(data_client) # TODO: perf + except struct.error: + pass + else: + return layers.DNSLayer(context) + # 5b) We have no other specialized layers for UDP, so we fall back to raw forwarding. + if udp_based: + return layers.UDPLayer(context) + # 5b) 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 ( + # the first three bytes should be the HTTP verb, so A-Za-z is expected. + len(data_client) < 3 + or not data_client[:3].isalpha() + # a server greeting would be uncharacteristic. + or data_server + ) + 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, + context: Context, + data_client: 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_record_magic(data_client): - try: - ch = parse_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 and (peername := context.server.peername[0]): + hostnames.append(peername) + if context.server.address and (server_address := context.server.address[0]): + hostnames.append(server_address) + if ( + client_hello := self._get_client_hello(context, data_client) + ) and client_hello.sni: + hostnames.append(client_hello.sni) + # If the client data is not a TLS record, try to extract the domain from the HTTP request + elif host := self._extract_http1_host_header(data_client): + hostnames.append(host) if not hostnames: return False @@ -106,99 +245,145 @@ def ignore_connection( else: # pragma: no cover raise AssertionError() - 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(), - ) + @staticmethod + def _extract_http1_host_header(data_client: bytes) -> str: + pattern = rb"Host:\s+(.+?)\r\n" + match = re.search(pattern, data_client) + return match.group(1).decode() if match else "" - def _next_layer( - self, context: context.Context, data_client: bytes, data_server: bytes - ) -> Optional[layer.Layer]: - if len(context.layers) == 0: - return self.make_top_layer(context) + def _get_client_hello( + self, context: Context, data_client: bytes + ) -> ClientHello | None: + """ + Try to read a TLS/DTLS/QUIC ClientHello from data_client. - 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. - # helper function to quickly check if the existing layer stack matches a particular configuration. - def s(*layers): - return stack_match(context, layers) + 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(data_client) + except ValueError: + pass - # 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: - return None - - # 2. Check for TLS - client_tls = is_tls_record_magic(data_client) - if client_tls: - # client tls usually requires a server tls layer as parent layer, except: - # - a secure web proxy doesn't have a server part. - # - reverse proxy mode manages this itself. - if ( - s(modes.HttpProxy) - or s(modes.ReverseProxy) - or s(modes.ReverseProxy, layers.ServerTLSLayer) - ): - return layers.ClientTLSLayer(context) - else: - # We already assign the next layer here os that ServerTLSLayer - # knows that it can safely wait for a ClientHello. - ret = layers.ServerTLSLayer(context) - ret.child_layer = layers.ClientTLSLayer(context) - return ret + 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) - # 3. Setup the HTTP layer for a regular HTTP proxy or an upstream 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) - ): - if ctx.options.mode == "regular": - return layers.HttpLayer(context, HTTPMode.regular) - else: - return layers.HttpLayer(context, HTTPMode.upstream) + def _setup_reverse_proxy(self, context: Context, data_client: bytes) -> Layer: + spec = cast(mode_specs.ReverseMode, context.client.proxy_mode) + stack = tunnel.LayerStack() - # 4. Check for --tcp - if any( - (context.server.address and rex.search(context.server.address[0])) - or (context.client.sni and rex.search(context.client.sni)) - for rex in self.tcp_hosts - ): - return layers.TCPLayer(context) + match spec.scheme: + case "http": + if starts_like_tls_record(data_client): + stack /= ClientTLSLayer(context) + stack /= HttpLayer(context, HTTPMode.transparent) + case "https": + stack /= ServerTLSLayer(context) + if starts_like_tls_record(data_client): + stack /= ClientTLSLayer(context) + stack /= HttpLayer(context, HTTPMode.transparent) - # 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) + 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) - # 6. Assume HTTP by default. - return layers.HttpLayer(context, HTTPMode.transparent) + 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) - def make_top_layer(self, context: context.Context) -> layer.Layer: - if ctx.options.mode == "regular" or ctx.options.mode.startswith("upstream:"): - return layers.modes.HttpProxy(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) - elif ctx.options.mode == "transparent": - return layers.modes.TransparentProxy(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) - elif ctx.options.mode.startswith("reverse:"): - return layers.modes.ReverseProxy(context) + case _: # pragma: no cover + assert_never(spec.scheme) - elif ctx.options.mode == "socks5": - return layers.modes.Socks5Proxy(context) + return stack[0] - else: # pragma: no cover - raise AssertionError("Unknown mode.") + def _setup_explicit_http_proxy(self, 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) + + return stack[0] + + def _is_destination_in_hosts( + self, 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 + ) + + +def _starts_like_quic(data_client: bytes) -> bool: + # FIXME: handle clienthellos distributed over multiple packets? + # FIXME: perf + try: + quic_parse_client_hello(data_client) + except ValueError: + return False + else: + return True 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 380633c95f..a4aed19858 100644 --- a/mitmproxy/addons/onboardingapp/__init__.py +++ b/mitmproxy/addons/onboardingapp/__init__.py @@ -1,8 +1,11 @@ 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__) # will be overridden in the addon, setting this here so that the Flask app can be run standalone. @@ -29,6 +32,24 @@ def cer(): return read_cert("cer", "application/x-x509-ca-cert") +@app.route("/cert/magisk") +def magisk(): + filename = CONF_BASENAME + f"-magisk-module.zip" + p = os.path.join(app.config["CONFDIR"], filename) + p = os.path.expanduser(p) + + if not os.path.exists(p): + write_magisk_module(p) + + with open(p, "rb") as f: + cert = f.read() + + return cert, { + "Content-Type": "application/zip", + "Content-Disposition": f"attachment; filename={filename}", + } + + def read_cert(ext, content_type): filename = CONF_BASENAME + f"-ca-cert.{ext}" p = os.path.join(app.config["CONFDIR"], filename) @@ -38,5 +59,5 @@ def read_cert(ext, content_type): return cert, { "Content-Type": content_type, - "Content-Disposition": f"inline; filename={filename}", + "Content-Disposition": f"attachment; filename={filename}", } diff --git a/mitmproxy/addons/onboardingapp/templates/index.html b/mitmproxy/addons/onboardingapp/templates/index.html index 0da178624a..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
    @@ -93,6 +98,9 @@
    Android 10+
    patch most apps manually (Android network security config).

    +

    + Alternatively, if you have rooted the device and have Magisk installed, you can install this Magisk module via the Magisk Manager app. +

    {% endcall %} {% call entry('Firefox (does not use the OS root certificates)', 'firefox-browser') %} 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 494ecbe99b..3dcd986194 100644 --- a/mitmproxy/addons/proxyauth.py +++ b/mitmproxy/addons/proxyauth.py @@ -2,17 +2,20 @@ 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 +from mitmproxy.proxy import mode_specs from mitmproxy.proxy.layers import modes REALM = "mitmproxy" @@ -21,7 +24,7 @@ class ProxyAuth: validator: Validator | None = None - def __init__(self): + def __init__(self) -> None: self.authenticated: MutableMapping[ connection.Client, tuple[str, str] ] = weakref.WeakKeyDictionary() @@ -37,32 +40,26 @@ 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. """, ) def configure(self, updated): - if "proxyauth" not in updated: - return - auth = ctx.options.proxyauth - if auth: - if ctx.options.mode == "transparent": - raise exceptions.OptionsError( - "Proxy Authentication not supported in transparent mode." - ) - - if auth == "any": - self.validator = AcceptAll() - elif auth.startswith("@"): - self.validator = Htpasswd(auth) - elif ctx.options.proxyauth.startswith("ldap"): - self.validator = Ldap(auth) - elif ":" in ctx.options.proxyauth: - self.validator = SingleUser(auth) + if "proxyauth" in updated: + auth = ctx.options.proxyauth + if auth: + if auth == "any": + self.validator = AcceptAll() + elif auth.startswith("@"): + self.validator = Htpasswd(auth) + elif ctx.options.proxyauth.startswith("ldap"): + self.validator = Ldap(auth) + elif ":" in ctx.options.proxyauth: + self.validator = SingleUser(auth) + else: + raise exceptions.OptionsError("Invalid proxyauth specification.") else: - raise exceptions.OptionsError("Invalid proxyauth specification.") - else: - self.validator = None + self.validator = None def socks5_auth(self, data: modes.Socks5AuthData) -> None: if self.validator and self.validator(data.username, data.password): @@ -93,8 +90,11 @@ def authenticate_http(self, f: http.HTTPFlow) -> bool: username = None password = None is_valid = False + + is_proxy = is_http_proxy(f) + auth_header = http_auth_header(is_proxy) try: - auth_value = f.request.headers.get(self.http_auth_header, "") + auth_value = f.request.headers.get(auth_header, "") scheme, username, password = parse_http_basic_auth(auth_value) is_valid = self.validator(username, password) except Exception: @@ -102,47 +102,50 @@ def authenticate_http(self, f: http.HTTPFlow) -> bool: if is_valid: f.metadata["proxyauth"] = (username, password) - del f.request.headers[self.http_auth_header] + del f.request.headers[auth_header] return True else: - f.response = self.make_auth_required_response() + f.response = make_auth_required_response(is_proxy) return False - def make_auth_required_response(self) -> http.Response: - if self.is_http_proxy: - status_code = status_codes.PROXY_AUTH_REQUIRED - headers = {"Proxy-Authenticate": f'Basic realm="{REALM}"'} - else: - status_code = status_codes.UNAUTHORIZED - headers = {"WWW-Authenticate": f'Basic realm="{REALM}"'} - - reason = http.status_codes.RESPONSES[status_code] - return http.Response.make( - status_code, - ( - f"" - f"{status_code} {reason}" - f"

    {status_code} {reason}

    " - f"" - ), - headers, - ) - @property - def http_auth_header(self) -> str: - if self.is_http_proxy: - return "Proxy-Authorization" - else: - return "Authorization" +def make_auth_required_response(is_proxy: bool) -> http.Response: + if is_proxy: + status_code = status_codes.PROXY_AUTH_REQUIRED + headers = {"Proxy-Authenticate": f'Basic realm="{REALM}"'} + else: + status_code = status_codes.UNAUTHORIZED + headers = {"WWW-Authenticate": f'Basic realm="{REALM}"'} + + reason = http.status_codes.RESPONSES[status_code] + return http.Response.make( + status_code, + ( + f"" + f"{status_code} {reason}" + f"

    {status_code} {reason}

    " + f"" + ), + headers, + ) - @property - def is_http_proxy(self) -> bool: - """ - Returns: - - True, if authentication is done as if mitmproxy is a proxy - - False, if authentication is done as if mitmproxy is an HTTP server - """ - return ctx.options.mode == "regular" or ctx.options.mode.startswith("upstream:") + +def http_auth_header(is_proxy: bool) -> str: + if is_proxy: + return "Proxy-Authorization" + else: + return "Authorization" + + +def is_http_proxy(f: http.HTTPFlow) -> bool: + """ + Returns: + - 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) + ) def mkauth(username: str, password: str, scheme: str = "basic") -> str: @@ -210,6 +213,7 @@ class Ldap(Validator): conn: ldap3.Connection server: ldap3.Server dn_subtree: str + filter_key: str def __init__(self, proxyauth: str): ( @@ -219,6 +223,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) @@ -226,7 +231,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: ( @@ -242,6 +247,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": @@ -249,14 +264,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 f512de7e81..42334faaca 100644 --- a/mitmproxy/addons/proxyserver.py +++ b/mitmproxy/addons/proxyserver.py @@ -1,112 +1,137 @@ +""" +This addon is responsible for starting/stopping the proxy server sockets/instances specified by the mode option. +""" +from __future__ import annotations + import asyncio -from asyncio import base_events +import collections import ipaddress -import re -import struct +import logging +from collections.abc import Iterable +from collections.abc import Iterator +from contextlib import contextmanager from typing import Optional -from mitmproxy import ( - command, - ctx, - exceptions, - flow, - http, - log, - master, - options, - platform, - tcp, - websocket, -) +from wsproto.frame_protocol import Opcode + +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.net import udp -from mitmproxy.proxy import commands, events, layers, server_hooks -from mitmproxy.proxy import server +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.utils import asyncio_utils, human -from wsproto.frame_protocol import Opcode +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 human +from mitmproxy.utils import signals +logger = logging.getLogger(__name__) -class ProxyConnectionHandler(server.LiveConnectionHandler): - master: master.Master - - def __init__(self, master, r, w, options, timeout=None): - self.master = master - super().__init__(r, w, options) - self.log_prefix = f"{human.format_address(self.client.peername)}: " - if timeout is not None: - self.timeout_watchdog.CONNECTION_TIMEOUT = timeout - - async def handle_hook(self, hook: commands.StartHook) -> None: - with self.timeout_watchdog.disarm(): - # We currently only support single-argument hooks. - (data,) = hook.args() - await self.master.addons.handle_lifecycle(hook) - if isinstance(data, flow.Flow): - await data.wait_for_resume() - - def log(self, message: str, level: str = "info") -> None: - x = log.LogEntry(self.log_prefix + message, level) - asyncio_utils.create_task( - self.master.addons.handle_lifecycle(log.AddLogHook(x)), - name="ProxyConnectionHandler.log", - ) +class Servers: + def __init__(self, manager: ServerManager): + self.changed = signals.AsyncSignal(lambda: None) + self._instances: dict[mode_specs.ProxyMode, ServerInstance] = dict() + self._lock = asyncio.Lock() + self._manager = manager + + @property + def is_updating(self) -> bool: + return self._lock.locked() + + async def update(self, modes: Iterable[mode_specs.ProxyMode]) -> bool: + all_ok = True + + async with self._lock: + new_instances: dict[mode_specs.ProxyMode, ServerInstance] = {} + + start_tasks = [] + if ctx.options.server: + # Create missing modes and keep existing ones. + for spec in modes: + if spec in self._instances: + instance = self._instances[spec] + else: + instance = ServerInstance.make(spec, self._manager) + start_tasks.append(instance.start()) + new_instances[spec] = instance + + # Shutdown modes that have been removed from the list. + stop_tasks = [ + s.stop() + for spec, s in self._instances.items() + if spec not in new_instances + ] + + self._instances = new_instances + # Notify listeners about the new not-yet-started servers. + await self.changed.send() + + # We first need to free ports before starting new servers. + for ret in await asyncio.gather(*stop_tasks, return_exceptions=True): + if ret: + all_ok = False + logger.error(str(ret)) + for ret in await asyncio.gather(*start_tasks, return_exceptions=True): + if ret: + all_ok = False + logger.error(str(ret)) + + await self.changed.send() + return all_ok + + def __len__(self) -> int: + return len(self._instances) + + def __iter__(self) -> Iterator[ServerInstance]: + return iter(self._instances.values()) + + def __getitem__(self, mode: str | mode_specs.ProxyMode) -> ServerInstance: + if isinstance(mode, str): + mode = mode_specs.ProxyMode.parse(mode) + return self._instances[mode] -class Proxyserver: + +class Proxyserver(ServerManager): """ This addon runs the actual proxy server. """ - tcp_server: Optional[base_events.Server] - dns_server: Optional[udp.UdpServer] - connect_addr: Optional[Address] - listen_port: int - dns_reverse_addr: Optional[tuple[str, int]] - master: master.Master - options: options.Options + connections: dict[tuple | str, ProxyConnectionHandler] + servers: Servers + is_running: bool - _connections: dict[tuple, ProxyConnectionHandler] + _connect_addr: Address | None = None + _update_task: asyncio.Task | None = None def __init__(self): - self._lock = asyncio.Lock() - self.tcp_server = None - self.dns_server = None - self.connect_addr = None - self.dns_reverse_addr = None + self.connections = {} + self.servers = Servers(self) self.is_running = False - self._connections = {} def __repr__(self): - return f"ProxyServer({'running' if self.running_servers else 'stopped'}, {len(self._connections)} active conns)" - - @property - def _server_desc(self): - yield "Proxy", self.tcp_server, lambda x: setattr( - self, "tcp_server", x - ), ctx.options.server, lambda: asyncio.start_server( - self.handle_tcp_connection, - self.options.listen_host, - self.options.listen_port, - ) - yield "DNS", self.dns_server, lambda x: setattr( - self, "dns_server", x - ), ctx.options.dns_server, lambda: udp.start_server( - self.handle_dns_datagram, - self.options.dns_listen_host or "127.0.0.1", - self.options.dns_listen_port, - transparent=self.options.dns_mode == "transparent", - ) + return f"Proxyserver({len(self.connections)} active conns)" - @property - def running_servers(self): - return tuple( - instance - for _, instance, _, _, _ in self._server_desc - if instance is not None - ) + @contextmanager + def register_connection( + self, connection_id: tuple | str, handler: ProxyConnectionHandler + ): + self.connections[connection_id] = handler + try: + yield + finally: + del self.connections[connection_id] def load(self, loader): loader.add_option( @@ -125,8 +150,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( @@ -178,32 +204,11 @@ def load(self, loader): None, """Set the local IP address that mitmproxy should use when connecting to upstream servers.""", ) - loader.add_option( - "dns_server", bool, False, """Start a DNS server. Disabled by default.""" - ) - loader.add_option( - "dns_listen_host", str, "", """Address to bind DNS server to.""" - ) - loader.add_option("dns_listen_port", int, 53, """DNS server service port.""") - loader.add_option( - "dns_mode", - str, - "regular", - """ - One of "regular", "reverse:[:]" or "transparent". - regular....: requests will be resolved using the local resolver - reverse....: forward queries to another DNS server - transparent: transparent mode - """, - ) - async def running(self): - self.master = ctx.master - self.options = ctx.options + def running(self): self.is_running = True - await self.refresh_server() - def configure(self, updated): + def configure(self, updated) -> None: if "stream_large_bodies" in updated: try: human.parse_size(ctx.options.stream_large_bodies) @@ -222,163 +227,86 @@ def configure(self, updated): ) if "connect_addr" in updated: try: - self.connect_addr = (str(ipaddress.ip_address(ctx.options.connect_addr)), 0) if ctx.options.connect_addr else None + if ctx.options.connect_addr: + self._connect_addr = ( + str(ipaddress.ip_address(ctx.options.connect_addr)), + 0, + ) + else: + self._connect_addr = None except ValueError: raise exceptions.OptionsError( - f"Invalid connection address {ctx.options.connect_addr!r}, specify a valid IP address." + f"Invalid value for connect_addr: {ctx.options.connect_addr!r}. Specify a valid IP address." ) - - if "dns_mode" in updated: - m = re.match( - r"^(regular|reverse:(?P[^:]+)(:(?P\d+))?|transparent)$", - ctx.options.dns_mode, - ) - if not m: - raise exceptions.OptionsError( - f"Invalid DNS mode {ctx.options.dns_mode!r}." - ) - if m["host"]: + if "mode" in updated or "server" in updated: + # Make sure that all modes are syntactically valid... + modes: list[mode_specs.ProxyMode] = [] + for mode in ctx.options.mode: try: - self.dns_reverse_addr = ( - str(ipaddress.ip_address(m["host"])), - int(m["port"]) if m["port"] is not None else 53, - ) - except ValueError: + modes.append(mode_specs.ProxyMode.parse(mode)) + except ValueError as e: raise exceptions.OptionsError( - f"Invalid DNS reverse mode, expected 'reverse:ip[:port]' got {ctx.options.dns_mode!r}." + f"Invalid proxy mode specification: {mode} ({e})" ) - else: - self.dns_reverse_addr = None - if "mode" in updated and ctx.options.mode == "transparent": # pragma: no cover - platform.init_transparent_mode() - if self.is_running and any( - x in updated - for x in [ - "server", - "listen_host", - "listen_port", - "dns_server", - "dns_mode", - "dns_listen_host", - "dns_listen_port", + + # ...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 ] - ): - asyncio.create_task(self.refresh_server()) + 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}" + ) - async def refresh_server(self): - async with self._lock: - await self.shutdown_server() - if ctx.options.server and not ctx.master.addons.get("nextlayer"): - ctx.log.warn("Warning: Running proxyserver without nextlayer addon!") - for name, instance, set_instance, enabled, start in self._server_desc: - if instance is None and enabled: - try: - instance = await start() - except OSError as e: - ctx.log.error(str(e)) - else: - set_instance(instance) - # TODO: This is a bit confusing currently for `-p 0`. - addrs = { - f"{human.format_address(s.getsockname())}" - for s in instance.sockets - } - ctx.log.info( - f"{name} server listening at {' and '.join(addrs)}" - ) - - async def shutdown_server(self): - for name, instance, set_instance, _, _ in self._server_desc: - if instance is not None: - ctx.log.info(f"Stopping {name} server...") - try: - instance.close() - await instance.wait_closed() - except OSError as e: - ctx.log.error(str(e)) + if ctx.options.mode and not ctx.master.addons.get("nextlayer"): + logger.warning("Warning: Running proxyserver without nextlayer addon!") + if any(isinstance(m, mode_specs.TransparentMode) for m in modes): + if platform.original_addr: + platform.init_transparent_mode() else: - set_instance(None) - - async def handle_connection(self, connection_id: tuple): - handler = self._connections[connection_id] - task = asyncio.current_task() - assert task - asyncio_utils.set_task_debug_info( - task, - name=f"Proxyserver.handle_connection", - client=handler.client.peername, - ) - try: - await handler.handle_client() - finally: - del self._connections[connection_id] - - async def handle_tcp_connection( - self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter - ) -> None: - connection_id = ( - "tcp", - writer.get_extra_info("peername"), - writer.get_extra_info("sockname"), - ) - self._connections[connection_id] = ProxyConnectionHandler( - self.master, reader, writer, self.options + raise exceptions.OptionsError( + "Transparent mode not supported on this platform." + ) + + if self.is_running: + self._update_task = asyncio.create_task(self.servers.update(modes)) + + async def setup_servers(self) -> bool: + """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] ) - await self.handle_connection(connection_id) - - def handle_dns_datagram( - self, - transport: asyncio.DatagramTransport, - data: bytes, - remote_addr: Address, - local_addr: Address, - ) -> None: - try: - dns_id = struct.unpack_from("!H", data, 0) - except struct.error: - ctx.log.info( - f"Invalid DNS datagram received from {human.format_address(remote_addr)}." - ) - return - connection_id = ("udp", dns_id, remote_addr, local_addr) - if connection_id not in self._connections: - reader = udp.DatagramReader() - writer = udp.DatagramWriter(transport, remote_addr, reader) - handler = ProxyConnectionHandler( - self.master, reader, writer, self.options, 20 - ) - handler.layer = layers.DNSLayer(handler.layer.context) - handler.layer.context.server.address = ( - local_addr - if self.options.dns_mode == "transparent" - else self.dns_reverse_addr - ) - handler.layer.context.server.transport_protocol = "udp" - self._connections[connection_id] = handler - asyncio.create_task(self.handle_connection(connection_id)) - else: - handler = self._connections[connection_id] - client_reader = handler.transports[handler.client].reader - assert isinstance(client_reader, udp.DatagramReader) - reader = client_reader - reader.feed_data(data, remote_addr) + + def listen_addrs(self) -> list[Address]: + return [addr for server in self.servers for addr in server.listen_addrs] def inject_event(self, event: events.MessageInjected): - connection_id = ( - "tcp", - event.flow.client_conn.peername, - event.flow.client_conn.sockname, - ) - if connection_id not in self._connections: + 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) + self.connections[connection_id].server_event(event) @command.command("inject.websocket") def inject_websocket( self, flow: Flow, to_client: bool, message: bytes, is_text: bool = True ): if not isinstance(flow, http.HTTPFlow) or not flow.websocket: - ctx.log.warn("Cannot inject WebSocket messages into non-WebSocket flows.") + logger.warning("Cannot inject WebSocket messages into non-WebSocket flows.") msg = websocket.WebSocketMessage( Opcode.TEXT if is_text else Opcode.BINARY, not to_client, message @@ -387,37 +315,49 @@ def inject_websocket( try: self.inject_event(event) except ValueError as e: - ctx.log.warn(str(e)) + logger.warning(str(e)) @command.command("inject.tcp") def inject_tcp(self, flow: Flow, to_client: bool, message: bytes): if not isinstance(flow, tcp.TCPFlow): - ctx.log.warn("Cannot inject TCP messages into non-TCP flows.") + logger.warning("Cannot inject TCP messages into non-TCP flows.") event = TcpMessageInjected(flow, tcp.TCPMessage(not to_client, message)) try: self.inject_event(event) except ValueError as e: - ctx.log.warn(str(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, ctx: server_hooks.ServerConnectionHookData): + def server_connect(self, data: server_hooks.ServerConnectionHookData): return # :) - assert ctx.server.address - # FIXME: Move this to individual proxy modes. - self_connect = ctx.server.address[1] in ( - self.options.dns_listen_port, - self.options.listen_port, - ) and ctx.server.address[0] in ( - "localhost", - "127.0.0.1", - "::1", - self.options.listen_host, - self.options.dns_listen_host, - ) - if self_connect: - ctx.server.error = ( - "Request destination unknown. " - "Unable to figure out where this request should be forwarded to." - ) - if ctx.server.sockname is None: - ctx.server.sockname = self.connect_addr + if data.server.sockname is None: + data.server.sockname = self._connect_addr + + # Prevent mitmproxy from recursively connecting to itself. + assert data.server.address + connect_host, connect_port, *_ = data.server.address + + for server in self.servers: + 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 server.mode.transport_protocol == data.server.transport_protocol + ) + if self_connect: + data.server.error = ( + "Request destination unknown. " + "Unable to figure out where this request should be forwarded to." + ) + return diff --git a/mitmproxy/addons/readfile.py b/mitmproxy/addons/readfile.py index 4ff0048801..63f7b1dbde 100644 --- a/mitmproxy/addons/readfile.py +++ b/mitmproxy/addons/readfile.py @@ -1,13 +1,15 @@ import asyncio +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 class ReadFile: @@ -17,7 +19,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.") @@ -46,9 +48,9 @@ async def load_flows(self, fo: BinaryIO) -> int: cnt += 1 except (OSError, exceptions.FlowReadException) as e: if cnt: - ctx.log.warn("Flow file corrupted - loaded %i flows." % cnt) + logging.warning("Flow file corrupted - loaded %i flows." % cnt) else: - ctx.log.error("Flow file corrupted.") + logging.error("Flow file corrupted.") raise exceptions.FlowReadException(str(e)) from e else: return cnt @@ -59,25 +61,24 @@ async def load_flows_from_path(self, path: str) -> int: with open(path, "rb") as f: return await self.load_flows(f) except OSError as e: - ctx.log.error(f"Cannot load flows: {e}") + 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 + self._read_task = None 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) class ReadFileStdin(ReadFile): diff --git a/mitmproxy/addons/save.py b/mitmproxy/addons/save.py index 32d3b4bd41..0103bde60a 100644 --- a/mitmproxy/addons/save.py +++ b/mitmproxy/addons/save.py @@ -1,13 +1,15 @@ +import logging import os.path import sys from collections.abc import Sequence 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 +from mitmproxy import command from mitmproxy import ctx from mitmproxy import dns from mitmproxy import exceptions @@ -15,6 +17,9 @@ 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 @lru_cache @@ -36,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( @@ -75,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() @@ -136,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 - ctx.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: @@ -148,6 +160,16 @@ def tcp_end(self, flow: tcp.TCPFlow): def tcp_error(self, flow: tcp.TCPFlow): self.tcp_end(flow) + def udp_start(self, flow: udp.UDPFlow): + if self.stream: + self.active_flows.add(flow) + + def udp_end(self, flow: udp.UDPFlow): + self.save_flow(flow) + + def udp_error(self, flow: udp.UDPFlow): + self.udp_end(flow) + def websocket_end(self, flow: http.HTTPFlow): self.save_flow(flow) diff --git a/mitmproxy/addons/savehar.py b/mitmproxy/addons/savehar.py new file mode 100644 index 0000000000..5e3f22ad48 --- /dev/null +++ b/mitmproxy/addons/savehar.py @@ -0,0 +1,304 @@ +"""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 = None + servers_seen.add(flow.server_conn) + else: + connect_time = None + ssl_time = None + + 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: + 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 + 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 flow.response.content and strutils.is_mostly_bin(flow.response.content): + response["content"]["text"] = base64.b64encode( + flow.response.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 + + 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": flow.request.pretty_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 50b939c7e5..a9b864cc8e 100644 --- a/mitmproxy/addons/script.py +++ b/mitmproxy/addons/script.py @@ -1,24 +1,26 @@ import asyncio -import os -import importlib.util import importlib.machinery +import importlib.util +import logging +import os 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] ) @@ -36,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)) - ) - ctx.log.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 @@ -76,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: @@ -103,7 +108,7 @@ def addons(self): return [self.ns] if self.ns else [] def loadscript(self): - ctx.log.info("Loading script %s" % self.path) + logger.info("Loading script %s" % self.path) if self.ns: ctx.master.addons.remove(self.ns) self.ns = None @@ -116,19 +121,19 @@ 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 + last_mtime = 0.0 while True: try: mtime = os.stat(self.fullpath).st_mtime except FileNotFoundError: - ctx.log.info("Removing script %s" % self.path) + logger.info("Removing script %s" % self.path) scripts = list(ctx.options.scripts) scripts.remove(self.path) ctx.options.update(scripts=scripts) @@ -162,7 +167,7 @@ def script_run(self, flows: Sequence[flow.Flow], path: mtypes.Path) -> None: simulated. Note that the load event is not invoked. """ if not os.path.isfile(path): - ctx.log.error("No such script: %s" % path) + logger.error("No such script: %s" % path) return mod = load_script(path) if mod: @@ -184,7 +189,7 @@ def configure(self, updated): for a in self.addons[:]: if a.path not in ctx.options.scripts: - ctx.log.info("Un-loading script: %s" % a.path) + logger.info("Un-loading script: %s" % a.path) ctx.master.addons.remove(a) self.addons.remove(a) diff --git a/mitmproxy/addons/server_side_events.py b/mitmproxy/addons/server_side_events.py index 111244d3a9..18d740d4c7 100644 --- a/mitmproxy/addons/server_side_events.py +++ b/mitmproxy/addons/server_side_events.py @@ -1,4 +1,6 @@ -from mitmproxy import ctx, http +import logging + +from mitmproxy import http class ServerSideEvents: @@ -15,7 +17,7 @@ def response(self, flow: http.HTTPFlow): "text/event-stream" ) if is_sse and not flow.response.stream: - ctx.log.warn( + logging.warning( "mitmproxy currently does not support server side events. As a workaround, you can enable response " "streaming for such flows: https://github.com/mitmproxy/mitmproxy/issues/4469" ) diff --git a/mitmproxy/addons/serverplayback.py b/mitmproxy/addons/serverplayback.py index 3924db67df..077eade378 100644 --- a/mitmproxy/addons/serverplayback.py +++ b/mitmproxy/addons/serverplayback.py @@ -1,15 +1,21 @@ 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__) + class ServerPlayback: flowmap: dict[Hashable, list[http.HTTPFlow]] @@ -24,10 +30,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, """ @@ -35,6 +50,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, @@ -109,6 +132,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), []) @@ -183,14 +213,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 ) @@ -209,6 +239,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: @@ -227,10 +266,21 @@ def request(self, f: http.HTTPFlow) -> None: response.refresh() f.response = response f.is_replay = "response" - elif ctx.options.server_replay_kill_extra: - ctx.log.warn( + 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..2164ac55f0 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/termlog.py b/mitmproxy/addons/termlog.py index faaed6b808..f6d0038643 100644 --- a/mitmproxy/addons/termlog.py +++ b/mitmproxy/addons/termlog.py @@ -1,44 +1,56 @@ +from __future__ import annotations + +import asyncio +import logging import sys -from typing import IO, Optional +from typing import IO from mitmproxy import ctx from mitmproxy import log -from mitmproxy.contrib import click as miniclick from mitmproxy.utils import vt_codes -LOG_COLORS = {"error": "red", "warn": "yellow", "alert": "magenta"} - class TermLog: - def __init__( - self, - out: Optional[IO[str]] = None, - err: Optional[IO[str]] = None, - ): - self.out_file: IO[str] = out or sys.stdout - self.out_has_vt_codes = vt_codes.ensure_supported(self.out_file) - self.err_file: IO[str] = err or sys.stderr - self.err_has_vt_codes = vt_codes.ensure_supported(self.err_file) + _teardown_task: asyncio.Task | None = None + + def __init__(self, out: IO[str] | None = None): + self.logger = TermLogHandler(out) + self.logger.install() def load(self, loader): loader.add_option( - "termlog_verbosity", str, "info", "Log verbosity.", choices=log.LogTierOrder + "termlog_verbosity", str, "info", "Log verbosity.", choices=log.LogLevels ) + self.logger.setLevel(logging.INFO) + + 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. + self._teardown_task = asyncio.create_task(t) + except RuntimeError: + # no event loop, we're in a test. + asyncio.run(t) + + async def _teardown(self): + self.logger.uninstall() + + +class TermLogHandler(log.MitmLogHandler): + 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) + self.formatter = log.MitmFormatter(self.has_vt_codes) - def add_log(self, e: log.LogEntry): - if log.log_tier(ctx.options.termlog_verbosity) >= log.log_tier(e.level): - if e.level == "error": - f = self.err_file - has_vt_codes = self.err_has_vt_codes - else: - f = self.out_file - has_vt_codes = self.out_has_vt_codes - - msg = e.msg - if has_vt_codes: - msg = miniclick.style( - e.msg, - fg=LOG_COLORS.get(e.level), - dim=(e.level == "debug"), - ) - print(msg, file=f) + def emit(self, record: logging.LogRecord) -> None: + try: + print(self.format(record), file=self.file) + except OSError: + # We cannot print, exit immediately. + # See https://github.com/mitmproxy/mitmproxy/issues/4669 + sys.exit(1) diff --git a/mitmproxy/addons/tlsconfig.py b/mitmproxy/addons/tlsconfig.py index 0cb7492e28..e044d1d1c8 100644 --- a/mitmproxy/addons/tlsconfig.py +++ b/mitmproxy/addons/tlsconfig.py @@ -1,14 +1,26 @@ import ipaddress +import logging import os +import ssl from pathlib import Path -from typing import Any, Optional, TypedDict +from typing import Any +from typing import TypedDict +from aioquic.h3.connection import H3_ALPN +from aioquic.tls import CipherSuite +from OpenSSL import crypto from OpenSSL import SSL -from mitmproxy import certs, ctx, exceptions, connection, tls + +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 # We manually need to specify this, otherwise OpenSSL may select a non-HTTP2 cipher by default. @@ -51,8 +63,8 @@ class AppData(TypedDict): - client_alpn: Optional[bytes] - server_alpn: Optional[bytes] + client_alpn: bytes | None + server_alpn: bytes | None http2: bool @@ -131,17 +143,29 @@ 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`).', + ) def tls_clienthello(self, tls_clienthello: tls.ClientHelloData): conn_context = tls_clienthello.context - tls_clienthello.establish_server_tls_first = conn_context.server.tls and ( - ctx.options.connection_strategy == "eager" - or ctx.options.add_upstream_certs_to_client_chain - or ctx.options.upstream_cert + tls_clienthello.establish_server_tls_first = ( + conn_context.server.tls and ctx.options.connection_strategy == "eager" ) def tls_start_client(self, tls_start: tls.TlsData) -> None: - """Establish TLS between client and proxy.""" + """Establish TLS or DTLS between client and proxy.""" if tls_start.ssl_conn is not None: return # a user addon has already provided the pyOpenSSL context. @@ -164,11 +188,13 @@ 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, 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), - cert=entry.cert, - key=entry.privatekey, + ecdh_curve=ctx.options.tls_ecdh_curve_client, chain_file=entry.chain_file, request_client_cert=False, alpn_select_callback=alpn_select_callback, @@ -177,13 +203,18 @@ 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) + ) + # 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, # but the complexity outweighs the benefits for now. 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 @@ -197,7 +228,7 @@ def tls_start_client(self, tls_start: tls.TlsData) -> None: tls_start.ssl_conn.set_accept_state() def tls_start_server(self, tls_start: tls.TlsData) -> None: - """Establish TLS between proxy and server.""" + """Establish TLS or DTLS between proxy and server.""" if tls_start.ssl_conn is not None: return # a user addon has already provided the pyOpenSSL context. @@ -240,7 +271,7 @@ def tls_start_server(self, tls_start: tls.TlsData) -> None: # don't assign to client.cipher_list, doesn't need to be stored. cipher_list = server.cipher_list or DEFAULT_CIPHERS - 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): @@ -252,13 +283,18 @@ def tls_start_server(self, tls_start: tls.TlsData) -> None: client_cert = p ssl_ctx = net_tls.create_proxy_server_context( - min_version=net_tls.Version[ctx.options.tls_version_client_min], - max_version=net_tls.Version[ctx.options.tls_version_client_max], + 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) @@ -278,7 +314,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", @@ -293,54 +331,157 @@ 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(): - ctx.log.warn( - "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(): + 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.", + ) - 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, - ) - except ValueError as e: - raise exceptions.OptionsError( - f"Invalid certificate format for {cert}: {e}" - ) from e + 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 def get_cert(self, conn_context: context.Context) -> certs.CertStoreEntry: """ @@ -348,7 +489,7 @@ def get_cert(self, conn_context: context.Context) -> certs.CertStoreEntry: our certificate should have and then fetches a matching cert from the certstore. """ altnames: list[str] = [] - organization: Optional[str] = None + organization: str | None = None # Use upstream certificate if available. if ctx.options.upstream_cert and conn_context.server.certificate_list: @@ -359,17 +500,16 @@ def get_cert(self, conn_context: context.Context) -> certs.CertStoreEntry: 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]) - - # 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: + else: 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(conn_context.server.address[0]) + # only keep first occurrence of each hostname altnames = list(dict.fromkeys(altnames)) diff --git a/mitmproxy/addons/upstream_auth.py b/mitmproxy/addons/upstream_auth.py index 0c1cb1d625..655ca7d752 100644 --- a/mitmproxy/addons/upstream_auth.py +++ b/mitmproxy/addons/upstream_auth.py @@ -1,10 +1,11 @@ -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 @@ -26,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( @@ -52,7 +53,10 @@ def http_connect_upstream(self, f: http.HTTPFlow): def requestheaders(self, f: http.HTTPFlow): if self.auth: - if ctx.options.mode.startswith("upstream") 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 ctx.options.mode.startswith("reverse"): + 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 51f9862805..6c0c286cd4 100644 --- a/mitmproxy/addons/view.py +++ b/mitmproxy/addons/view.py @@ -9,26 +9,31 @@ 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 blinker 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 - +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, @@ -56,7 +61,7 @@ def refresh(self, f): self.view._view.remove(f) self.view.settings[f][k] = new self.view._view.add(f) - self.view.sig_view_refresh.send(self.view) + self.view.sig_view_refresh.send() def _key(self): return "_order_%s" % id(self) @@ -83,8 +88,8 @@ class OrderRequestMethod(_OrderKey): def generate(self, f: mitmproxy.flow.Flow) -> str: if isinstance(f, http.HTTPFlow): return f.request.method - elif isinstance(f, tcp.TCPFlow): - return "TCP" + elif isinstance(f, (tcp.TCPFlow, udp.UDPFlow)): + return f.type.upper() elif isinstance(f, dns.DNSFlow): return dns.op_codes.to_str(f.request.op_code) else: @@ -95,7 +100,7 @@ class OrderRequestURL(_OrderKey): def generate(self, f: mitmproxy.flow.Flow) -> str: if isinstance(f, http.HTTPFlow): return f.request.url - elif isinstance(f, tcp.TCPFlow): + elif isinstance(f, (tcp.TCPFlow, udp.UDPFlow)): return human.format_address(f.server_conn.address) elif isinstance(f, dns.DNSFlow): return f.request.questions[0].name if f.request.questions else "" @@ -112,7 +117,7 @@ def generate(self, f: mitmproxy.flow.Flow) -> int: if f.response and f.response.raw_content: size += len(f.response.raw_content) return size - elif isinstance(f, tcp.TCPFlow): + elif isinstance(f, (tcp.TCPFlow, udp.UDPFlow)): size = 0 for message in f.messages: size += len(message.content) @@ -131,10 +136,20 @@ def generate(self, f: mitmproxy.flow.Flow) -> int: ] +def _signal_with_flow(flow: mitmproxy.flow.Flow) -> 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 @@ -146,7 +161,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 @@ -155,19 +170,19 @@ def __init__(self): # The sig_view* signals broadcast events that affect the view. That is, # an update to a flow in the store but not in the view does not trigger # a signal. All signals are called after the view has been updated. - self.sig_view_update = blinker.Signal() - self.sig_view_add = blinker.Signal() - self.sig_view_remove = blinker.Signal() + self.sig_view_update = signals.SyncSignal(_signal_with_flow) + self.sig_view_add = signals.SyncSignal(_signal_with_flow) + self.sig_view_remove = signals.SyncSignal(_sig_view_remove) # Signals that the view should be refreshed completely - self.sig_view_refresh = blinker.Signal() + self.sig_view_refresh = signals.SyncSignal(lambda: None) # The sig_store* signals broadcast events that affect the underlying # store. If a flow is removed from just the view, sig_view_remove is # triggered. If it is removed from the store while it is also in the # view, both sig_store_remove and sig_view_remove are triggered. - self.sig_store_remove = blinker.Signal() + self.sig_store_remove = signals.SyncSignal(_signal_with_flow) # Signals that the store should be refreshed completely - self.sig_store_refresh = blinker.Signal() + self.sig_store_refresh = signals.SyncSignal(lambda: None) self.focus = Focus(self) self.settings = Settings(self) @@ -219,7 +234,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)) @@ -240,7 +255,7 @@ def _refilter(self): continue if self.filter(i): self._base_add(i) - self.sig_view_refresh.send(self) + self.sig_view_refresh.send() """ View API """ @@ -297,7 +312,7 @@ def order_options(self) -> Sequence[str]: @command.command("view.order.reverse") def set_reversed(self, boolean: bool) -> None: self.order_reversed = boolean - self.sig_view_refresh.send(self) + self.sig_view_refresh.send() @command.command("view.order.set") def set_order(self, order_key: str) -> None: @@ -306,9 +321,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 @@ -337,7 +352,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() @@ -349,8 +364,8 @@ def clear(self) -> None: """ self._store.clear() self._view.clear() - self.sig_view_refresh.send(self) - self.sig_store_refresh.send(self) + self.sig_view_refresh.send() + self.sig_store_refresh.send() @command.command("view.clear_unmarked") def clear_not_marked(self) -> None: @@ -362,7 +377,7 @@ def clear_not_marked(self) -> None: self._store.pop(flow.id) self._refilter() - self.sig_store_refresh.send(self) + self.sig_store_refresh.send() # View Settings @command.command("view.settings.getval") @@ -409,7 +424,7 @@ def duplicate(self, flows: Sequence[mitmproxy.flow.Flow]) -> None: if dups: self.add(dups) self.focus.flow = dups[0] - ctx.log.alert("Duplicated %s flows" % len(dups)) + logging.log(ALERT, "Duplicated %s flows" % len(dups)) @command.command("view.flows.remove") def remove(self, flows: Sequence[mitmproxy.flow.Flow]) -> None: @@ -425,11 +440,11 @@ def remove(self, flows: Sequence[mitmproxy.flow.Flow]) -> None: # sorting key, and we cannot reconstruct the index from that. idx = self._view.index(f) self._view.remove(f) - self.sig_view_remove.send(self, flow=f, index=idx) + self.sig_view_remove.send(flow=f, index=idx) del self._store[f.id] - self.sig_store_remove.send(self, flow=f) + self.sig_store_remove.send(flow=f) if len(flows) > 1: - ctx.log.alert("Removed %s flows" % len(flows)) + logging.log(ALERT, "Removed %s flows" % len(flows)) @command.command("view.flows.resolve") def resolve(self, flow_spec: str) -> Sequence[mitmproxy.flow.Flow]: @@ -465,8 +480,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 @@ -486,9 +505,9 @@ def load_file(self, path: mitmproxy.types.Path) -> None: # .newid() method or something. self.add([i.copy()]) except OSError as e: - ctx.log.error(e.strerror) + logging.error(e.strerror) except exceptions.FlowReadException as e: - ctx.log.error(str(e)) + logging.error(str(e)) def add(self, flows: Sequence[mitmproxy.flow.Flow]) -> None: """ @@ -502,9 +521,9 @@ def add(self, flows: Sequence[mitmproxy.flow.Flow]) -> None: self._base_add(f) if self.focus_follow: self.focus.flow = f - self.sig_view_add.send(self, 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. @@ -592,6 +611,18 @@ def tcp_error(self, f): def tcp_end(self, f): self.update([f]) + def udp_start(self, f): + self.add([f]) + + def udp_message(self, f): + self.update([f]) + + def udp_error(self, f): + self.update([f]) + + def udp_end(self, f): + self.update([f]) + def dns_request(self, f): self.add([f]) @@ -612,14 +643,14 @@ def update(self, flows: Sequence[mitmproxy.flow.Flow]) -> None: self._base_add(f) if self.focus_follow: self.focus.flow = f - self.sig_view_add.send(self, flow=f) + self.sig_view_add.send(flow=f) else: # This is a tad complicated. The sortedcontainers # implementation assumes that the order key is stable. If # it changes mid-way Very Bad Things happen. We detect when # this happens, and re-fresh the item. self.order_key.refresh(f) - self.sig_view_update.send(self, flow=f) + self.sig_view_update.send(flow=f) else: try: idx = self._view.index(f) @@ -627,7 +658,7 @@ def update(self, flows: Sequence[mitmproxy.flow.Flow]) -> None: pass # The value was not in the view else: self._view.remove(f) - self.sig_view_remove.send(self, flow=f, index=idx) + self.sig_view_remove.send(flow=f, index=idx) class Focus: @@ -637,8 +668,8 @@ class Focus: def __init__(self, v: View) -> None: self.view = v - self._flow: Optional[mitmproxy.flow.Flow] = None - self.sig_change = blinker.Signal() + self._flow: mitmproxy.flow.Flow | None = None + self.sig_change = signals.SyncSignal(lambda: None) if len(self.view): self.flow = self.view[0] v.sig_view_add.connect(self._sig_view_add) @@ -646,18 +677,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(self) + 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 @@ -671,21 +702,21 @@ def index(self, idx): def _nearest(self, f, v): return min(v._bisect(f), len(v) - 1) - def _sig_view_remove(self, view, flow, index): - if len(view) == 0: + def _sig_view_remove(self, flow, index): + if len(self.view) == 0: self.flow = None elif flow is self.flow: self.index = min(index, len(self.view) - 1) - def _sig_view_refresh(self, view): - if len(view) == 0: + def _sig_view_refresh(self): + if len(self.view) == 0: self.flow = None elif self.flow is None: - self.flow = view[0] - elif self.flow not in view: - self.flow = view[self._nearest(self.flow, view)] + self.flow = self.view[0] + elif self.flow not in self.view: + self.flow = self.view[self._nearest(self.flow, self.view)] - def _sig_view_add(self, view, flow): + def _sig_view_add(self, flow): # We only have to act if we don't have a focus element if not self.flow: self.flow = flow @@ -709,11 +740,11 @@ def __getitem__(self, f: mitmproxy.flow.Flow) -> dict: raise KeyError return self._values.setdefault(f.id, {}) - def _sig_store_remove(self, view, flow): + def _sig_store_remove(self, flow): if flow.id in self._values: del self._values[flow.id] - def _sig_store_refresh(self, view): + def _sig_store_refresh(self): for fid in list(self._values.keys()): - if fid not in view._store: + if fid not in self.view._store: del self._values[fid] diff --git a/mitmproxy/certs.py b/mitmproxy/certs.py index dc3787a8b7..d994fbb6b6 100644 --- a/mitmproxy/certs.py +++ b/mitmproxy/certs.py @@ -6,15 +6,22 @@ import sys 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.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 # Default expiry must not be too long: https://github.com/mitmproxy/mitmproxy/issues/815 @@ -125,19 +132,19 @@ 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 @@ -160,12 +167,8 @@ def altnames(self) -> list[str]: 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 @@ -225,9 +228,9 @@ def create_ca( def dummy_cert( privkey: rsa.RSAPrivateKey, cacert: x509.Certificate, - commonname: Optional[str], + commonname: str | None, sans: list[str], - organization: Optional[str] = None, + organization: str | None = None, ) -> Cert: """ Generates a dummy certificate. @@ -267,6 +270,7 @@ def dummy_cert( try: ip = ipaddress.ip_address(x) except ValueError: + x = x.encode("idna").decode() ss.append(x509.DNSName(x)) else: ss.append(x509.IPAddress(ip)) @@ -274,6 +278,19 @@ def dummy_cert( builder = builder.add_extension( x509.SubjectAlternativeName(ss), critical=not is_valid_commonname ) + + # we just use the same key as the CA for these certs, so put that in the SKI extension + builder = builder.add_extension( + x509.SubjectKeyIdentifier.from_public_key(privkey.public_key()), + critical=False, + ) + # add authority key identifier for the cacert issuing cert for greater acceptance by + # client TLS libraries (such as OpenSSL 3.x) + builder = builder.add_extension( + x509.AuthorityKeyIdentifier.from_issuer_public_key(cacert.public_key()), + critical=False, + ) + cert = builder.sign(private_key=privkey, algorithm=hashes.SHA256()) # type: ignore return Cert(cert) @@ -282,7 +299,8 @@ 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) @@ -305,12 +323,24 @@ 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 = ( + [ + Cert.from_pem(chunk) + for chunk in re.split( + rb"(?=-----BEGIN( [A-Z]+)+-----)", + self.default_chain_file.read_bytes(), + ) + if chunk.startswith(b"-----BEGIN CERTIFICATE-----") + ] + if self.default_chain_file + else [default_ca] + ) self.dhparams = dhparams self.certs = {} self.expire_queue = [] @@ -330,7 +360,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 +378,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,7 +392,7 @@ 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) @@ -368,7 +400,7 @@ def from_files( certs = re.split(rb"(?=-----BEGIN CERTIFICATE-----)", raw) ca = Cert.from_pem(certs[1]) if len(certs) > 2: - chain_file: Optional[Path] = ca_file + chain_file: Path | None = ca_file else: chain_file = None return cls(key, ca, chain_file, dh) @@ -444,7 +476,7 @@ 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) @@ -453,7 +485,7 @@ def add_cert_file( except ValueError: key = self.default_privatekey - self.add_cert(CertStoreEntry(cert, key, path), spec) + self.add_cert(CertStoreEntry(cert, key, path, [cert]), spec) def add_cert(self, entry: CertStoreEntry, *names: str) -> None: """ @@ -481,9 +513,9 @@ def asterisk_forms(dn: str) -> list[str]: def get_cert( self, - commonname: Optional[str], + commonname: str | None, sans: list[str], - organization: Optional[str] = None, + organization: str | None = None, ) -> CertStoreEntry: """ commonname: Common name for the generated certificate. Must be a @@ -516,6 +548,7 @@ 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.expire(entry) @@ -523,7 +556,7 @@ def get_cert( 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 db36702880..5b12497e13 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -3,19 +3,26 @@ """ import functools import inspect +import logging 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: @@ -58,13 +65,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() @@ -87,7 +94,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 @@ -177,7 +184,7 @@ def collect_commands(self, addon): try: self.add(o.command_name, o) except exceptions.CommandError as e: - self.master.log.warn( + logging.warning( f"Could not load command {o.command_name}: {e}" ) @@ -192,14 +199,16 @@ def parse_partial( Parse a possibly partial command. Return a sequence of ParseResults and a sequence of remainder type help items. """ - parts: list[str] = 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( @@ -237,7 +246,7 @@ def parse_partial( if to: try: to.parse(self, expected.type, part) - except exceptions.TypeError: + except ValueError: valid = False else: valid = True @@ -300,11 +309,11 @@ def parsearg(manager: CommandManager, spec: str, argtype: type) -> Any: raise exceptions.CommandError(f"Unsupported argument type: {argtype}") try: return t.parse(manager, argtype, spec) - except exceptions.TypeError as e: + except ValueError as e: 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 6ed08602f6..dfa474d0d1 100644 --- a/mitmproxy/connection.py +++ b/mitmproxy/connection.py @@ -1,13 +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.net import server_spec +from mitmproxy.proxy import mode_specs from mitmproxy.utils import human @@ -28,8 +33,12 @@ class ConnectionState(Flag): # 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. @@ -37,20 +46,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. @@ -81,27 +94,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: str | 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 @@ -123,19 +136,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.", @@ -145,6 +162,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.""" @@ -153,28 +172,18 @@ 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. """ - timestamp_start: float - """*Timestamp:* TCP SYN received""" + proxy_mode: mode_specs.ProxyMode = field( + default=mode_specs.ProxyMode.parse("regular") + ) + """The proxy server type this client has been connecting to.""" - def __init__( - self, - peername: Address, - sockname: Address, - timestamp_start: float, - *, - transport_protocol: TransportProtocol = "tcp", - ): - 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 + timestamp_start: float = field(default_factory=time.time) + """*Timestamp:* TCP SYN received""" def __str__(self): if self.alpn: @@ -183,67 +192,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, - } - - @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"] + 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 @@ -265,7 +216,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.", @@ -275,7 +226,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.", @@ -300,34 +251,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')}" @@ -339,7 +297,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"): @@ -353,65 +313,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.", @@ -421,7 +324,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 b6cc2d9412..ec2313751f 100644 --- a/mitmproxy/contentviews/__init__.py +++ b/mitmproxy/contentviews/__init__.py @@ -12,46 +12,55 @@ `base.View`. """ import traceback -from typing import Union -from typing import Optional - -import blinker +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 -from . import ( - auto, - raw, - hex, - json, - xml_html, - wbxml, - javascript, - css, - urlencoded, - multipart, - image, - query, - protobuf, - msgpack, - graphql, - grpc, -) -from .base import View, KEY_MAX, format_text, format_dict, TViewResult -from ..http import HTTPFlow -from ..tcp import TCPMessage, TCPFlow -from ..websocket import WebSocketMessage views: list[View] = [] -on_add = blinker.Signal() + +def _update(view: View) -> None: + ... + + +on_add = signals.SyncSignal(_update) """A new contentview has been added.""" -on_remove = blinker.Signal() +on_remove = signals.SyncSignal(_update) """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 @@ -79,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) @@ -89,8 +98,8 @@ def safe_to_print(lines, encoding="utf8"): def get_message_content_view( viewname: str, - message: Union[http.Message, TCPMessage, WebSocketMessage], - flow: Union[HTTPFlow, TCPFlow], + message: http.Message | TCPMessage | UDPMessage | WebSocketMessage, + flow: flow.Flow, ): """ Like get_content_view, but also handles message encoding. @@ -100,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: @@ -124,12 +133,22 @@ def get_message_content_view( if ct := http.parse_content_type(ctype): content_type = f"{ct[0]}/{ct[1]}" + tcp_message = None + if isinstance(message, TCPMessage): + tcp_message = message + + udp_message = None + if isinstance(message, UDPMessage): + udp_message = message + description, lines, error = get_content_view( viewmode, content, content_type=content_type, flow=flow, http_message=http_message, + tcp_message=tcp_message, + udp_message=udp_message, ) if enc: @@ -138,30 +157,15 @@ def get_message_content_view( return description, lines, error -def get_tcp_content_view( - viewname: str, - data: bytes, - flow: TCPFlow, -): - viewmode = get(viewname) - if not viewmode: - viewmode = get("auto") - - # https://github.com/mitmproxy/mitmproxy/pull/3970#issuecomment-623024447 - assert viewmode - - description, lines, error = get_content_view(viewmode, data, flow=flow) - - return description, lines, error - - def get_content_view( viewmode: View, 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, + tcp_message: tcp.TCPMessage | None = None, + udp_message: udp.UDPMessage | None = None, ): """ Args: @@ -176,7 +180,12 @@ def get_content_view( """ try: ret = viewmode( - data, content_type=content_type, flow=flow, http_message=http_message + data, + content_type=content_type, + flow=flow, + http_message=http_message, + tcp_message=tcp_message, + udp_message=udp_message, ) if ret is None: ret = ( @@ -186,6 +195,8 @@ def get_content_view( content_type=content_type, flow=flow, http_message=http_message, + tcp_message=tcp_message, + udp_message=udp_message, )[1], ) desc, content = ret @@ -196,7 +207,12 @@ def get_content_view( raw = get("Raw") assert raw content = raw( - data, content_type=content_type, flow=flow, http_message=http_message + data, + content_type=content_type, + flow=flow, + http_message=http_message, + tcp_message=tcp_message, + udp_message=udp_message, )[1] error = f"{getattr(viewmode, 'name')} content viewer failed: \n{traceback.format_exc()}" @@ -206,7 +222,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()) @@ -220,6 +237,9 @@ def get_content_view( add(protobuf.ViewProtobuf()) add(msgpack.ViewMsgPack()) add(grpc.ViewGrpcProtobuf()) +add(mqtt.ViewMQTT()) +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..5263b75f5f 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]): @@ -46,7 +45,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 5c73220c83..4c0307b315 100644 --- a/mitmproxy/contentviews/grpc.py +++ b/mitmproxy/contentviews/grpc.py @@ -1,11 +1,18 @@ from __future__ import annotations +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, ctx, 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 @@ -258,8 +265,9 @@ def read_packed_fields( packed_field: ProtoParser.Field, ) -> list[ProtoParser.Field]: if not isinstance(packed_field.wire_value, bytes): - ctx.log(type(packed_field.wire_value)) - raise ValueError("can not unpack field with data other than bytes") + 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 @@ -526,7 +536,7 @@ def apply_rules(self, only_first_hit=True): self.preferred_decoding = decoding self.try_unpack = as_packed except Exception as e: - ctx.log.warn(e) + logging.warning(e) def _gen_tag_str(self): tags = self.parent_tags[:] @@ -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) @@ -966,6 +978,9 @@ class ViewGrpcProtobuf(base.View): ] __content_types_grpc = [ "application/grpc", + # seems specific to chromium infra tooling + # https://chromium.googlesource.com/infra/luci/luci-go/+/refs/heads/main/grpc/prpc/ + "application/prpc", ] # first value serves as default algorithm for compressed messages, if 'grpc-encoding' header is missing @@ -973,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() @@ -1059,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( @@ -1084,7 +1100,7 @@ def __call__( # hook to log exception tracebacks on iterators # import traceback - # ctx.log.warn("gRPC contentview: {}".format(traceback.format_exc())) + # logging.warning("gRPC contentview: {}".format(traceback.format_exc())) raise e return title, text_iter @@ -1098,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 new file mode 100644 index 0000000000..d1c93915f5 --- /dev/null +++ b/mitmproxy/contentviews/http3.py @@ -0,0 +1,160 @@ +from collections import defaultdict +from collections.abc import Iterator +from dataclasses import dataclass +from dataclasses import field + +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 ..proxy.layers.http import is_h3_alpn +from . import base +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 + + def pretty(self): + frame_name = f"0x{self.type:x} Frame" + if self.type == 0: + frame_name = "DATA Frame" + elif self.type == 1: + try: + hdrs = pylsqpack.Decoder(4096, 16).feed_header(0, self.data)[1] + return [[("header", "HEADERS Frame")], *base.format_pairs(hdrs)] + except Exception as e: + frame_name = f"HEADERS Frame (error: {e})" + elif self.type == 4: + settings = [] + try: + s = parse_settings(self.data) + except Exception as e: + frame_name = f"SETTINGS Frame (error: {e})" + else: + for k, v in s.items(): + try: + key = Setting(k).name + except ValueError: + key = f"0x{k:x}" + settings.append((key, f"0x{v:x}")) + return [[("header", "SETTINGS Frame")], *base.format_pairs(settings)] + return [ + [("header", frame_name)], + *ViewHexDump._format(self.data), + ] + + +@dataclass(frozen=True) +class StreamType: + """Representation of an HTTP/3 stream types.""" + + type: int + + def pretty(self): + stream_type = { + 0x00: "Control Stream", + 0x01: "Push Stream", + 0x02: "QPACK Encoder Stream", + 0x03: "QPACK Decoder Stream", + }.get(self.type, f"0x{self.type:x} Stream") + return [[("header", stream_type)]] + + +@dataclass +class ConnectionState: + message_count: int = 0 + frames: dict[int, list[Frame | StreamType]] = field(default_factory=dict) + client_buf: bytearray = field(default_factory=bytearray) + server_buf: bytearray = field(default_factory=bytearray) + + +class ViewHttp3(base.View): + name = "HTTP/3 Frames" + + 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, + ): + assert isinstance(flow, tcp.TCPFlow) + assert tcp_message + + state = self.connections[flow] + + for message in flow.messages[state.message_count :]: + if message.from_client: + buf = state.client_buf + else: + buf = state.server_buf + buf += message.content + + if state.message_count == 0 and flow.metadata["quic_is_unidirectional"]: + h3_buf = Buffer(data=bytes(buf[:8])) + stream_type = h3_buf.pull_uint_var() + consumed = h3_buf.tell() + del buf[:consumed] + state.frames[0] = [StreamType(stream_type)] + + while True: + h3_buf = Buffer(data=bytes(buf[:16])) + try: + frame_type = h3_buf.pull_uint_var() + frame_size = h3_buf.pull_uint_var() + except BufferReadError: + break + + consumed = h3_buf.tell() + + if len(buf) < consumed + frame_size: + break + + 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] + + 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=})") + else: + return "HTTP/3", fmt_frames(frames) + + def render_priority( + 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)) + ) + + +def fmt_frames(frames: list[Frame | StreamType]) -> Iterator[base.TViewLine]: + for i, frame in enumerate(frames): + if i > 0: + yield [("text", "")] + yield from frame.pretty() 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..021cac9920 100644 --- a/mitmproxy/contentviews/image/view.py +++ b/mitmproxy/contentviews/image/view.py @@ -1,9 +1,8 @@ import imghdr -from typing import Optional +from . import image_parser from mitmproxy.contentviews import base from mitmproxy.coretypes import multidict -from . import image_parser def test_ico(h, f): @@ -36,7 +35,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 a75ba83bb4..1b0e22a1e6 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 @@ -28,11 +28,18 @@ def format_json(data: Any) -> Iterator[base.TViewLine]: yield current_line current_line = [] if re.match(r'\s*"', chunk): - current_line.append(("json_string", chunk)) + 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)) elif re.match(r"\s*\d", chunk): - current_line.append(("json_number", chunk)) + current_line.append(("Token_Literal_Number", chunk)) elif re.match(r"\s*(true|null|false)", chunk): - current_line.append(("json_boolean", chunk)) + current_line.append(("Token_Keyword_Constant", chunk)) else: current_line.append(("text", chunk)) yield current_line @@ -47,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 new file mode 100644 index 0000000000..ad40482601 --- /dev/null +++ b/mitmproxy/contentviews/mqtt.py @@ -0,0 +1,277 @@ +import struct + +from mitmproxy.contentviews import base +from mitmproxy.utils import strutils + +# from https://github.com/nikitastupin/mitmproxy-mqtt-script + + +class MQTTControlPacket: + # Packet types + ( + CONNECT, + CONNACK, + PUBLISH, + PUBACK, + PUBREC, + PUBREL, + PUBCOMP, + SUBSCRIBE, + SUBACK, + UNSUBSCRIBE, + UNSUBACK, + PINGREQ, + PINGRESP, + DISCONNECT, + ) = range(1, 15) + + # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Table_2.1_- + Names = [ + "reserved", + "CONNECT", + "CONNACK", + "PUBLISH", + "PUBACK", + "PUBREC", + "PUBREL", + "PUBCOMP", + "SUBSCRIBE", + "SUBACK", + "UNSUBSCRIBE", + "UNSUBACK", + "PINGREQ", + "PINGRESP", + "DISCONNECT", + "reserved", + ] + + PACKETS_WITH_IDENTIFIER = [ + PUBACK, + PUBREC, + PUBREL, + PUBCOMP, + SUBSCRIBE, + SUBACK, + UNSUBSCRIBE, + UNSUBACK, + ] + + def __init__(self, packet): + self._packet = packet + # Fixed header + # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718020 + self.packet_type = self._parse_packet_type() + self.packet_type_human = self.Names[self.packet_type] + self.dup, self.qos, self.retain = self._parse_flags() + self.remaining_length = self._parse_remaining_length() + # Variable header & Payload + # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718024 + # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718026 + if self.packet_type == self.CONNECT: + self._parse_connect_variable_headers() + self._parse_connect_payload() + elif self.packet_type == self.PUBLISH: + self._parse_publish_variable_headers() + self._parse_publish_payload() + elif self.packet_type == self.SUBSCRIBE: + self._parse_subscribe_variable_headers() + self._parse_subscribe_payload() + elif self.packet_type == self.SUBACK: + pass + elif self.packet_type == self.UNSUBSCRIBE: + pass + else: + self.payload = None + + 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']} +Will Topic: {self.payload.get('WillTopic')} +Will Message: {strutils.bytes_to_escaped_str(self.payload.get('WillMessage', b'None'))} +User Name: {self.payload.get('UserName')} +Password: {strutils.bytes_to_escaped_str(self.payload.get('Password', b'None'))} +""" + elif self.packet_type == self.SUBSCRIBE: + 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) + + s += f" '{payload}' to topic '{topic_name}'" + elif self.packet_type in [self.PINGREQ, self.PINGRESP]: + pass + else: + s = f"Packet type {self.Names[self.packet_type]} is not supported yet!" + + return s + + def _parse_length_prefixed_bytes(self, offset): + field_length_bytes = self._packet[offset : offset + 2] + field_length = struct.unpack("!H", field_length_bytes)[0] + + field_content_bytes = self._packet[offset + 2 : offset + 2 + field_length] + + return field_length + 2, field_content_bytes + + def _parse_publish_variable_headers(self): + offset = len(self._packet) - self.remaining_length + + field_length, field_content_bytes = self._parse_length_prefixed_bytes(offset) + self.topic_name = field_content_bytes + + if self.qos in [0x01, 0x02]: + offset += field_length + self.packet_identifier = self._packet[offset : offset + 2] + + def _parse_publish_payload(self): + fixed_header_length = len(self._packet) - self.remaining_length + variable_header_length = 2 + len(self.topic_name) + + if self.qos in [0x01, 0x02]: + variable_header_length += 2 + + offset = fixed_header_length + variable_header_length + + self.payload = self._packet[offset:] + + def _parse_subscribe_variable_headers(self): + self._parse_packet_identifier() + + def _parse_subscribe_payload(self): + offset = len(self._packet) - self.remaining_length + 2 + + self.topic_filters = {} + + while len(self._packet) - offset > 0: + field_length, topic_filter_bytes = self._parse_length_prefixed_bytes(offset) + offset += field_length + + qos = self._packet[offset : offset + 1] + offset += 1 + + topic_filter = topic_filter_bytes.decode("utf-8") + self.topic_filters[topic_filter] = {"qos": qos} + + # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718030 + def _parse_connect_variable_headers(self): + offset = len(self._packet) - self.remaining_length + + self.variable_headers = {} + self.connect_flags = {} + + self.variable_headers["ProtocolName"] = self._packet[offset : offset + 6] + self.variable_headers["ProtocolLevel"] = self._packet[offset + 6 : offset + 7] + self.variable_headers["ConnectFlags"] = self._packet[offset + 7 : offset + 8] + self.variable_headers["KeepAlive"] = self._packet[offset + 8 : offset + 10] + # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc385349229 + self.connect_flags["CleanSession"] = bool( + self.variable_headers["ConnectFlags"][0] & 0x02 + ) + self.connect_flags["Will"] = bool( + self.variable_headers["ConnectFlags"][0] & 0x04 + ) + self.will_qos = (self.variable_headers["ConnectFlags"][0] >> 3) & 0x03 + self.connect_flags["WillRetain"] = bool( + self.variable_headers["ConnectFlags"][0] & 0x20 + ) + self.connect_flags["Password"] = bool( + self.variable_headers["ConnectFlags"][0] & 0x40 + ) + self.connect_flags["UserName"] = bool( + self.variable_headers["ConnectFlags"][0] & 0x80 + ) + + # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718031 + def _parse_connect_payload(self): + fields = [] + offset = len(self._packet) - self.remaining_length + 10 + + while len(self._packet) - offset > 0: + field_length, field_content = self._parse_length_prefixed_bytes(offset) + fields.append(field_content) + offset += field_length + + self.payload = {} + + for f in fields: + # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc385349242 + if "ClientId" not in self.payload: + self.payload["ClientId"] = f.decode("utf-8") + # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc385349243 + elif self.connect_flags["Will"] and "WillTopic" not in self.payload: + 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 + self.payload["UserName"] = f.decode("utf-8") + 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}") + + def _parse_packet_type(self): + return self._packet[0] >> 4 + + # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718022 + def _parse_flags(self): + dup = None + qos = None + retain = None + + if self.packet_type == self.PUBLISH: + dup = (self._packet[0] >> 3) & 0x01 + qos = (self._packet[0] >> 1) & 0x03 + retain = self._packet[0] & 0x01 + + return dup, qos, retain + + # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Table_2.4_Size + def _parse_remaining_length(self): + multiplier = 1 + value = 0 + i = 1 + + while True: + encodedByte = self._packet[i] + value += (encodedByte & 127) * multiplier + multiplier *= 128 + + if multiplier > 128 * 128 * 128: + raise ValueError("Malformed Remaining Length") + + if encodedByte & 128 == 0: + break + + i += 1 + + return value + + # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Table_2.5_- + def _parse_packet_identifier(self): + offset = len(self._packet) - self.remaining_length + self.packet_identifier = self._packet[offset : offset + 2] + + +class ViewMQTT(base.View): + name = "MQTT" + + def __call__(self, data, **metadata): + mqtt_packet = MQTTControlPacket(data) + text = mqtt_packet.pprint() + return "MQTT", base.format_text(text) + + def render_priority( + 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 01f74e49ad..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,23 +14,71 @@ def parse_msgpack(s: bytes) -> Any: return PARSE_ERROR -def pretty(value, htchar=" ", lfchar="\n", indent=0): - nlch = lfchar + htchar * (indent + 1) - if type(value) is dict: - items = [ - nlch + repr(key) + ": " + pretty(value[key], htchar, lfchar, indent + 1) - for key in value - ] - return "{%s}" % (",".join(items) + lfchar + htchar * indent) - elif type(value) is list: - items = [nlch + pretty(item, htchar, lfchar, indent + 1) for item in value] - return "[%s]" % (",".join(items) + lfchar + htchar * indent) - else: - return repr(value) +def format_msgpack( + data: Any, output=None, indent_count: int = 0 +) -> list[base.TViewLine]: + if output is None: + output = [[]] + + indent = ("text", " " * indent_count) + + 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 isinstance(data, bool): + token = [("Token_Keyword_Constant", repr(data))] + output[-1] += token + + return output + + elif isinstance(data, float | int): + token = [("Token_Literal_Number", repr(data))] + output[-1] += token + + return output + elif isinstance(data, dict): + output[-1] += [("text", "{")] + for key in data: + output.append( + [ + indent, + ("text", " "), + ("Token_Name_Tag", f'"{key}"'), + ("text", ": "), + ] + ) + format_msgpack(data[key], output, indent_count + 1) + + if key != list(data)[-1]: + output[-1] += [("text", ",")] + + output.append([indent, ("text", "}")]) + + return output + + elif isinstance(data, list): + output[-1] += [("text", "[")] + + for count, item in enumerate(data): + output.append([indent, ("text", " ")]) + format_msgpack(item, output, indent_count + 1) + if count != len(data) - 1: + output[-1] += [("text", ",")] + + output.append([indent, ("text", "]")]) + + return output + + else: + token = [("text", repr(data))] + output[-1] += token -def format_msgpack(data): - return base.format_text(pretty(data)) + return output class ViewMsgPack(base.View): @@ -47,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..6114fff653 100644 --- a/mitmproxy/contentviews/multipart.py +++ b/mitmproxy/contentviews/multipart.py @@ -1,8 +1,6 @@ -from typing import Optional - +from . import base from mitmproxy.coretypes import multidict from mitmproxy.net.http import multipart -from . import base class ViewMultipart(base.View): @@ -13,14 +11,14 @@ 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, **metadata): 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 758e25d437..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 @@ -22,15 +22,26 @@ def write_buf(out, field_tag, body, indent_level): out.write(" " * indent_level + "}\n") +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 + ): + raise ValueError("Not a protobuf.") + return buf.pairs + + def format_pbuf(raw): out = io.StringIO() stack = [] try: - buf = google_protobuf.GoogleProtobuf(KaitaiStream(io.BytesIO(raw))) - except: + pairs = _parse_proto(raw) + except Exception: return False - stack.extend([(pair, 0) for pair in buf.pairs[::-1]]) + stack.extend([(pair, 0) for pair in pairs[::-1]]) while len(stack): pair, indent_level = stack.pop() @@ -48,10 +59,10 @@ def format_pbuf(raw): body = pair.value try: - next_buf = google_protobuf.GoogleProtobuf(KaitaiStream(io.BytesIO(body))) - stack.extend([(pair, indent_level + 2) for pair in next_buf.pairs[::-1]]) + 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: @@ -86,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/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 new file mode 100644 index 0000000000..43a77cf972 --- /dev/null +++ b/mitmproxy/contrib/kaitaistruct/dtls_client_hello.ksy @@ -0,0 +1,140 @@ +meta: + id: dtls_client_hello + endian: be + license: MIT + +seq: + - id: version + type: version + + - id: random + type: random + + - id: session_id + type: session_id + + - id: cookie + type: cookie + + - id: cipher_suites + type: cipher_suites + + - id: compression_methods + type: compression_methods + + - id: extensions + type: extensions + if: _io.eof == false + +types: + version: + seq: + - id: major + type: u1 + + - id: minor + type: u1 + + random: + seq: + - id: gmt_unix_time + type: u4 + + - id: random + size: 28 + + session_id: + seq: + - id: len + type: u1 + + - id: sid + size: len + + cookie: + seq: + - id: len + type: u1 + + - id: cookie + size: len + + cipher_suites: + seq: + - id: len + type: u2 + + - id: cipher_suites + type: u2 + repeat: expr + repeat-expr: len/2 + + compression_methods: + seq: + - id: len + type: u1 + + - id: compression_methods + size: len + + extensions: + seq: + - id: len + type: u2 + + - id: extensions + type: extension + repeat: eos + + extension: + seq: + - id: type + type: u2 + + - id: len + type: u2 + + - id: body + size: len + type: + switch-on: type + cases: + 0: sni + 16: alpn + + sni: + seq: + - id: list_length + type: u2 + + - id: server_names + type: server_name + repeat: eos + + server_name: + seq: + - id: name_type + type: u1 + + - id: length + type: u2 + + - id: host_name + size: length + + alpn: + seq: + - id: ext_len + type: u2 + + - id: alpn_protocols + type: protocol + repeat: eos + + protocol: + seq: + - id: strlen + type: u1 + + - id: name + size: strlen diff --git a/mitmproxy/contrib/kaitaistruct/dtls_client_hello.py b/mitmproxy/contrib/kaitaistruct/dtls_client_hello.py new file mode 100644 index 0000000000..b581a08a7f --- /dev/null +++ b/mitmproxy/contrib/kaitaistruct/dtls_client_hello.py @@ -0,0 +1,202 @@ +# 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 + + +if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): + raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) + +class DtlsClientHello(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.version = DtlsClientHello.Version(self._io, self, self._root) + self.random = DtlsClientHello.Random(self._io, self, self._root) + self.session_id = DtlsClientHello.SessionId(self._io, self, self._root) + self.cookie = DtlsClientHello.Cookie(self._io, self, self._root) + self.cipher_suites = DtlsClientHello.CipherSuites(self._io, self, self._root) + self.compression_methods = DtlsClientHello.CompressionMethods(self._io, self, self._root) + if self._io.is_eof() == False: + self.extensions = DtlsClientHello.Extensions(self._io, self, self._root) + + + class ServerName(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.name_type = self._io.read_u1() + self.length = self._io.read_u2be() + self.host_name = self._io.read_bytes(self.length) + + + class Random(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.gmt_unix_time = self._io.read_u4be() + self.random = self._io.read_bytes(28) + + + class SessionId(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.len = self._io.read_u1() + self.sid = self._io.read_bytes(self.len) + + + class Sni(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.list_length = self._io.read_u2be() + self.server_names = [] + i = 0 + while not self._io.is_eof(): + self.server_names.append(DtlsClientHello.ServerName(self._io, self, self._root)) + i += 1 + + + + class CipherSuites(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.len = self._io.read_u2be() + self.cipher_suites = [] + for i in range(self.len // 2): + self.cipher_suites.append(self._io.read_u2be()) + + + + class CompressionMethods(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.len = self._io.read_u1() + self.compression_methods = self._io.read_bytes(self.len) + + + class Alpn(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.ext_len = self._io.read_u2be() + self.alpn_protocols = [] + i = 0 + while not self._io.is_eof(): + self.alpn_protocols.append(DtlsClientHello.Protocol(self._io, self, self._root)) + i += 1 + + + + class Extensions(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.len = self._io.read_u2be() + self.extensions = [] + i = 0 + while not self._io.is_eof(): + self.extensions.append(DtlsClientHello.Extension(self._io, self, self._root)) + i += 1 + + + + class Version(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.major = self._io.read_u1() + self.minor = self._io.read_u1() + + + class Cookie(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.len = self._io.read_u1() + self.cookie = self._io.read_bytes(self.len) + + + class Protocol(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.strlen = self._io.read_u1() + self.name = self._io.read_bytes(self.strlen) + + + class Extension(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.type = self._io.read_u2be() + self.len = self._io.read_u2be() + _on = self.type + if _on == 0: + self._raw_body = self._io.read_bytes(self.len) + _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) + self.body = DtlsClientHello.Sni(_io__raw_body, self, self._root) + elif _on == 16: + self._raw_body = self._io.read_bytes(self.len) + _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) + self.body = DtlsClientHello.Alpn(_io__raw_body, self, self._root) + else: + self.body = self._io.read_bytes(self.len) + + + diff --git a/mitmproxy/contrib/kaitaistruct/exif.py b/mitmproxy/contrib/kaitaistruct/exif.py index d72034f430..464849e420 100644 --- a/mitmproxy/contrib/kaitaistruct/exif.py +++ b/mitmproxy/contrib/kaitaistruct/exif.py @@ -1,26 +1,654 @@ # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild -import array -import struct -import zlib +import kaitaistruct +from kaitaistruct import KaitaiStream, KaitaiStruct from enum import Enum -from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO - -# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 - -from .exif_le import ExifLe -from .exif_be import ExifBe +if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): + raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) class Exif(KaitaiStruct): def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.endianness = self._io.read_u2le() - _on = self.endianness - if _on == 18761: - self.body = ExifLe(self._io) - elif _on == 19789: - self.body = ExifBe(self._io) + self.body = Exif.ExifBody(self._io, self, self._root) + + class ExifBody(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + _on = self._root.endianness + if _on == 18761: + self._is_le = True + elif _on == 19789: + self._is_le = False + if not hasattr(self, '_is_le'): + raise kaitaistruct.UndecidedEndiannessError("/types/exif_body") + elif self._is_le == True: + self._read_le() + elif self._is_le == False: + self._read_be() + + def _read_le(self): + self.version = self._io.read_u2le() + self.ifd0_ofs = self._io.read_u4le() + + def _read_be(self): + self.version = self._io.read_u2be() + self.ifd0_ofs = self._io.read_u4be() + + class Ifd(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None, _is_le=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._is_le = _is_le + self._read() + + def _read(self): + if not hasattr(self, '_is_le'): + raise kaitaistruct.UndecidedEndiannessError("/types/exif_body/types/ifd") + elif self._is_le == True: + self._read_le() + elif self._is_le == False: + self._read_be() + + def _read_le(self): + self.num_fields = self._io.read_u2le() + self.fields = [] + for i in range(self.num_fields): + self.fields.append(Exif.ExifBody.IfdField(self._io, self, self._root, self._is_le)) + + self.next_ifd_ofs = self._io.read_u4le() + + def _read_be(self): + self.num_fields = self._io.read_u2be() + self.fields = [] + for i in range(self.num_fields): + self.fields.append(Exif.ExifBody.IfdField(self._io, self, self._root, self._is_le)) + + self.next_ifd_ofs = self._io.read_u4be() + + @property + def next_ifd(self): + if hasattr(self, '_m_next_ifd'): + return self._m_next_ifd + + if self.next_ifd_ofs != 0: + _pos = self._io.pos() + self._io.seek(self.next_ifd_ofs) + if self._is_le: + self._m_next_ifd = Exif.ExifBody.Ifd(self._io, self, self._root, self._is_le) + else: + self._m_next_ifd = Exif.ExifBody.Ifd(self._io, self, self._root, self._is_le) + self._io.seek(_pos) + + return getattr(self, '_m_next_ifd', None) + + + class IfdField(KaitaiStruct): + + class FieldTypeEnum(Enum): + byte = 1 + ascii_string = 2 + word = 3 + dword = 4 + rational = 5 + undefined = 7 + slong = 9 + srational = 10 + + class TagEnum(Enum): + image_width = 256 + image_height = 257 + bits_per_sample = 258 + compression = 259 + photometric_interpretation = 262 + thresholding = 263 + cell_width = 264 + cell_length = 265 + fill_order = 266 + document_name = 269 + image_description = 270 + make = 271 + model = 272 + strip_offsets = 273 + orientation = 274 + samples_per_pixel = 277 + rows_per_strip = 278 + strip_byte_counts = 279 + min_sample_value = 280 + max_sample_value = 281 + x_resolution = 282 + y_resolution = 283 + planar_configuration = 284 + page_name = 285 + x_position = 286 + y_position = 287 + free_offsets = 288 + free_byte_counts = 289 + gray_response_unit = 290 + gray_response_curve = 291 + t4_options = 292 + t6_options = 293 + resolution_unit = 296 + page_number = 297 + color_response_unit = 300 + transfer_function = 301 + software = 305 + modify_date = 306 + artist = 315 + host_computer = 316 + predictor = 317 + white_point = 318 + primary_chromaticities = 319 + color_map = 320 + halftone_hints = 321 + tile_width = 322 + tile_length = 323 + tile_offsets = 324 + tile_byte_counts = 325 + bad_fax_lines = 326 + clean_fax_data = 327 + consecutive_bad_fax_lines = 328 + sub_ifd = 330 + ink_set = 332 + ink_names = 333 + numberof_inks = 334 + dot_range = 336 + target_printer = 337 + extra_samples = 338 + sample_format = 339 + s_min_sample_value = 340 + s_max_sample_value = 341 + transfer_range = 342 + clip_path = 343 + x_clip_path_units = 344 + y_clip_path_units = 345 + indexed = 346 + jpeg_tables = 347 + opi_proxy = 351 + global_parameters_ifd = 400 + profile_type = 401 + fax_profile = 402 + coding_methods = 403 + version_year = 404 + mode_number = 405 + decode = 433 + default_image_color = 434 + t82_options = 435 + jpeg_tables2 = 437 + jpeg_proc = 512 + thumbnail_offset = 513 + thumbnail_length = 514 + jpeg_restart_interval = 515 + jpeg_lossless_predictors = 517 + jpeg_point_transforms = 518 + jpegq_tables = 519 + jpegdc_tables = 520 + jpegac_tables = 521 + y_cb_cr_coefficients = 529 + y_cb_cr_sub_sampling = 530 + y_cb_cr_positioning = 531 + reference_black_white = 532 + strip_row_counts = 559 + application_notes = 700 + uspto_miscellaneous = 999 + related_image_file_format = 4096 + related_image_width = 4097 + related_image_height = 4098 + rating = 18246 + xp_dip_xml = 18247 + stitch_info = 18248 + rating_percent = 18249 + sony_raw_file_type = 28672 + light_falloff_params = 28722 + chromatic_aberration_corr_params = 28725 + distortion_corr_params = 28727 + image_id = 32781 + wang_tag1 = 32931 + wang_annotation = 32932 + wang_tag3 = 32933 + wang_tag4 = 32934 + image_reference_points = 32953 + region_xform_tack_point = 32954 + warp_quadrilateral = 32955 + affine_transform_mat = 32956 + matteing = 32995 + data_type = 32996 + image_depth = 32997 + tile_depth = 32998 + image_full_width = 33300 + image_full_height = 33301 + texture_format = 33302 + wrap_modes = 33303 + fov_cot = 33304 + matrix_world_to_screen = 33305 + matrix_world_to_camera = 33306 + model2 = 33405 + cfa_repeat_pattern_dim = 33421 + cfa_pattern2 = 33422 + battery_level = 33423 + kodak_ifd = 33424 + copyright = 33432 + exposure_time = 33434 + f_number = 33437 + md_file_tag = 33445 + md_scale_pixel = 33446 + md_color_table = 33447 + md_lab_name = 33448 + md_sample_info = 33449 + md_prep_date = 33450 + md_prep_time = 33451 + md_file_units = 33452 + pixel_scale = 33550 + advent_scale = 33589 + advent_revision = 33590 + uic1_tag = 33628 + uic2_tag = 33629 + uic3_tag = 33630 + uic4_tag = 33631 + iptc_naa = 33723 + intergraph_packet_data = 33918 + intergraph_flag_registers = 33919 + intergraph_matrix = 33920 + ingr_reserved = 33921 + model_tie_point = 33922 + site = 34016 + color_sequence = 34017 + it8_header = 34018 + raster_padding = 34019 + bits_per_run_length = 34020 + bits_per_extended_run_length = 34021 + color_table = 34022 + image_color_indicator = 34023 + background_color_indicator = 34024 + image_color_value = 34025 + background_color_value = 34026 + pixel_intensity_range = 34027 + transparency_indicator = 34028 + color_characterization = 34029 + hc_usage = 34030 + trap_indicator = 34031 + cmyk_equivalent = 34032 + sem_info = 34118 + afcp_iptc = 34152 + pixel_magic_jbig_options = 34232 + jpl_carto_ifd = 34263 + model_transform = 34264 + wb_grgb_levels = 34306 + leaf_data = 34310 + photoshop_settings = 34377 + exif_offset = 34665 + icc_profile = 34675 + tiff_fx_extensions = 34687 + multi_profiles = 34688 + shared_data = 34689 + t88_options = 34690 + image_layer = 34732 + geo_tiff_directory = 34735 + geo_tiff_double_params = 34736 + geo_tiff_ascii_params = 34737 + jbig_options = 34750 + exposure_program = 34850 + spectral_sensitivity = 34852 + gps_info = 34853 + iso = 34855 + opto_electric_conv_factor = 34856 + interlace = 34857 + time_zone_offset = 34858 + self_timer_mode = 34859 + sensitivity_type = 34864 + standard_output_sensitivity = 34865 + recommended_exposure_index = 34866 + iso_speed = 34867 + iso_speed_latitudeyyy = 34868 + iso_speed_latitudezzz = 34869 + fax_recv_params = 34908 + fax_sub_address = 34909 + fax_recv_time = 34910 + fedex_edr = 34929 + leaf_sub_ifd = 34954 + exif_version = 36864 + date_time_original = 36867 + create_date = 36868 + google_plus_upload_code = 36873 + offset_time = 36880 + offset_time_original = 36881 + offset_time_digitized = 36882 + components_configuration = 37121 + compressed_bits_per_pixel = 37122 + shutter_speed_value = 37377 + aperture_value = 37378 + brightness_value = 37379 + exposure_compensation = 37380 + max_aperture_value = 37381 + subject_distance = 37382 + metering_mode = 37383 + light_source = 37384 + flash = 37385 + focal_length = 37386 + flash_energy = 37387 + spatial_frequency_response = 37388 + noise = 37389 + focal_plane_x_resolution = 37390 + focal_plane_y_resolution = 37391 + focal_plane_resolution_unit = 37392 + image_number = 37393 + security_classification = 37394 + image_history = 37395 + subject_area = 37396 + exposure_index = 37397 + tiff_ep_standard_id = 37398 + sensing_method = 37399 + cip3_data_file = 37434 + cip3_sheet = 37435 + cip3_side = 37436 + sto_nits = 37439 + maker_note = 37500 + user_comment = 37510 + sub_sec_time = 37520 + sub_sec_time_original = 37521 + sub_sec_time_digitized = 37522 + ms_document_text = 37679 + ms_property_set_storage = 37680 + ms_document_text_position = 37681 + image_source_data = 37724 + ambient_temperature = 37888 + humidity = 37889 + pressure = 37890 + water_depth = 37891 + acceleration = 37892 + camera_elevation_angle = 37893 + xp_title = 40091 + xp_comment = 40092 + xp_author = 40093 + xp_keywords = 40094 + xp_subject = 40095 + flashpix_version = 40960 + color_space = 40961 + exif_image_width = 40962 + exif_image_height = 40963 + related_sound_file = 40964 + interop_offset = 40965 + samsung_raw_pointers_offset = 40976 + samsung_raw_pointers_length = 40977 + samsung_raw_byte_order = 41217 + samsung_raw_unknown = 41218 + flash_energy2 = 41483 + spatial_frequency_response2 = 41484 + noise2 = 41485 + focal_plane_x_resolution2 = 41486 + focal_plane_y_resolution2 = 41487 + focal_plane_resolution_unit2 = 41488 + image_number2 = 41489 + security_classification2 = 41490 + image_history2 = 41491 + subject_location = 41492 + exposure_index2 = 41493 + tiff_ep_standard_id2 = 41494 + sensing_method2 = 41495 + file_source = 41728 + scene_type = 41729 + cfa_pattern = 41730 + custom_rendered = 41985 + exposure_mode = 41986 + white_balance = 41987 + digital_zoom_ratio = 41988 + focal_length_in35mm_format = 41989 + scene_capture_type = 41990 + gain_control = 41991 + contrast = 41992 + saturation = 41993 + sharpness = 41994 + device_setting_description = 41995 + subject_distance_range = 41996 + image_unique_id = 42016 + owner_name = 42032 + serial_number = 42033 + lens_info = 42034 + lens_make = 42035 + lens_model = 42036 + lens_serial_number = 42037 + gdal_metadata = 42112 + gdal_no_data = 42113 + gamma = 42240 + expand_software = 44992 + expand_lens = 44993 + expand_film = 44994 + expand_filter_lens = 44995 + expand_scanner = 44996 + expand_flash_lamp = 44997 + pixel_format = 48129 + transformation = 48130 + uncompressed = 48131 + image_type = 48132 + image_width2 = 48256 + image_height2 = 48257 + width_resolution = 48258 + height_resolution = 48259 + image_offset = 48320 + image_byte_count = 48321 + alpha_offset = 48322 + alpha_byte_count = 48323 + image_data_discard = 48324 + alpha_data_discard = 48325 + oce_scanjob_desc = 50215 + oce_application_selector = 50216 + oce_id_number = 50217 + oce_image_logic = 50218 + annotations = 50255 + print_im = 50341 + original_file_name = 50547 + uspto_original_content_type = 50560 + dng_version = 50706 + dng_backward_version = 50707 + unique_camera_model = 50708 + localized_camera_model = 50709 + cfa_plane_color = 50710 + cfa_layout = 50711 + linearization_table = 50712 + black_level_repeat_dim = 50713 + black_level = 50714 + black_level_delta_h = 50715 + black_level_delta_v = 50716 + white_level = 50717 + default_scale = 50718 + default_crop_origin = 50719 + default_crop_size = 50720 + color_matrix1 = 50721 + color_matrix2 = 50722 + camera_calibration1 = 50723 + camera_calibration2 = 50724 + reduction_matrix1 = 50725 + reduction_matrix2 = 50726 + analog_balance = 50727 + as_shot_neutral = 50728 + as_shot_white_xy = 50729 + baseline_exposure = 50730 + baseline_noise = 50731 + baseline_sharpness = 50732 + bayer_green_split = 50733 + linear_response_limit = 50734 + camera_serial_number = 50735 + dng_lens_info = 50736 + chroma_blur_radius = 50737 + anti_alias_strength = 50738 + shadow_scale = 50739 + sr2_private = 50740 + maker_note_safety = 50741 + raw_image_segmentation = 50752 + calibration_illuminant1 = 50778 + calibration_illuminant2 = 50779 + best_quality_scale = 50780 + raw_data_unique_id = 50781 + alias_layer_metadata = 50784 + original_raw_file_name = 50827 + original_raw_file_data = 50828 + active_area = 50829 + masked_areas = 50830 + as_shot_icc_profile = 50831 + as_shot_pre_profile_matrix = 50832 + current_icc_profile = 50833 + current_pre_profile_matrix = 50834 + colorimetric_reference = 50879 + s_raw_type = 50885 + panasonic_title = 50898 + panasonic_title2 = 50899 + camera_calibration_sig = 50931 + profile_calibration_sig = 50932 + profile_ifd = 50933 + as_shot_profile_name = 50934 + noise_reduction_applied = 50935 + profile_name = 50936 + profile_hue_sat_map_dims = 50937 + profile_hue_sat_map_data1 = 50938 + profile_hue_sat_map_data2 = 50939 + profile_tone_curve = 50940 + profile_embed_policy = 50941 + profile_copyright = 50942 + forward_matrix1 = 50964 + forward_matrix2 = 50965 + preview_application_name = 50966 + preview_application_version = 50967 + preview_settings_name = 50968 + preview_settings_digest = 50969 + preview_color_space = 50970 + preview_date_time = 50971 + raw_image_digest = 50972 + original_raw_file_digest = 50973 + sub_tile_block_size = 50974 + row_interleave_factor = 50975 + profile_look_table_dims = 50981 + profile_look_table_data = 50982 + opcode_list1 = 51008 + opcode_list2 = 51009 + opcode_list3 = 51022 + noise_profile = 51041 + time_codes = 51043 + frame_rate = 51044 + t_stop = 51058 + reel_name = 51081 + original_default_final_size = 51089 + original_best_quality_size = 51090 + original_default_crop_size = 51091 + camera_label = 51105 + profile_hue_sat_map_encoding = 51107 + profile_look_table_encoding = 51108 + baseline_exposure_offset = 51109 + default_black_render = 51110 + new_raw_image_digest = 51111 + raw_to_preview_gain = 51112 + default_user_crop = 51125 + padding = 59932 + offset_schema = 59933 + owner_name2 = 65000 + serial_number2 = 65001 + lens = 65002 + kdc_ifd = 65024 + raw_file = 65100 + converter = 65101 + white_balance2 = 65102 + exposure = 65105 + shadows = 65106 + brightness = 65107 + contrast2 = 65108 + saturation2 = 65109 + sharpness2 = 65110 + smoothness = 65111 + moire_filter = 65112 + def __init__(self, _io, _parent=None, _root=None, _is_le=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._is_le = _is_le + self._read() + + def _read(self): + if not hasattr(self, '_is_le'): + raise kaitaistruct.UndecidedEndiannessError("/types/exif_body/types/ifd_field") + elif self._is_le == True: + self._read_le() + elif self._is_le == False: + self._read_be() + + def _read_le(self): + self.tag = KaitaiStream.resolve_enum(Exif.ExifBody.IfdField.TagEnum, self._io.read_u2le()) + self.field_type = KaitaiStream.resolve_enum(Exif.ExifBody.IfdField.FieldTypeEnum, self._io.read_u2le()) + self.length = self._io.read_u4le() + self.ofs_or_data = self._io.read_u4le() + + def _read_be(self): + self.tag = KaitaiStream.resolve_enum(Exif.ExifBody.IfdField.TagEnum, self._io.read_u2be()) + self.field_type = KaitaiStream.resolve_enum(Exif.ExifBody.IfdField.FieldTypeEnum, self._io.read_u2be()) + self.length = self._io.read_u4be() + self.ofs_or_data = self._io.read_u4be() + + @property + def type_byte_length(self): + if hasattr(self, '_m_type_byte_length'): + return self._m_type_byte_length + + self._m_type_byte_length = (2 if self.field_type == Exif.ExifBody.IfdField.FieldTypeEnum.word else (4 if self.field_type == Exif.ExifBody.IfdField.FieldTypeEnum.dword else 1)) + return getattr(self, '_m_type_byte_length', None) + + @property + def byte_length(self): + if hasattr(self, '_m_byte_length'): + return self._m_byte_length + + self._m_byte_length = (self.length * self.type_byte_length) + return getattr(self, '_m_byte_length', None) + + @property + def is_immediate_data(self): + if hasattr(self, '_m_is_immediate_data'): + return self._m_is_immediate_data + + self._m_is_immediate_data = self.byte_length <= 4 + return getattr(self, '_m_is_immediate_data', None) + + @property + def data(self): + if hasattr(self, '_m_data'): + return self._m_data + + if not (self.is_immediate_data): + io = self._root._io + _pos = io.pos() + io.seek(self.ofs_or_data) + if self._is_le: + self._m_data = io.read_bytes(self.byte_length) + else: + self._m_data = io.read_bytes(self.byte_length) + io.seek(_pos) + + return getattr(self, '_m_data', None) + + + @property + def ifd0(self): + if hasattr(self, '_m_ifd0'): + return self._m_ifd0 + + _pos = self._io.pos() + self._io.seek(self.ifd0_ofs) + if self._is_le: + self._m_ifd0 = Exif.ExifBody.Ifd(self._io, self, self._root, self._is_le) + else: + self._m_ifd0 = Exif.ExifBody.Ifd(self._io, self, self._root, self._is_le) + self._io.seek(_pos) + return getattr(self, '_m_ifd0', None) + + + diff --git a/mitmproxy/contrib/kaitaistruct/exif_be.py b/mitmproxy/contrib/kaitaistruct/exif_be.py deleted file mode 100644 index 145a28056a..0000000000 --- a/mitmproxy/contrib/kaitaistruct/exif_be.py +++ /dev/null @@ -1,582 +0,0 @@ -# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild - -from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO -from enum import Enum - - -# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 - -class ExifBe(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.version = self._io.read_u2be() - self.ifd0_ofs = self._io.read_u4be() - - class Ifd(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.num_fields = self._io.read_u2be() - self.fields = [None] * (self.num_fields) - for i in range(self.num_fields): - self.fields[i] = self._root.IfdField(self._io, self, self._root) - - self.next_ifd_ofs = self._io.read_u4be() - - @property - def next_ifd(self): - if hasattr(self, '_m_next_ifd'): - return self._m_next_ifd if hasattr(self, '_m_next_ifd') else None - - if self.next_ifd_ofs != 0: - _pos = self._io.pos() - self._io.seek(self.next_ifd_ofs) - self._m_next_ifd = self._root.Ifd(self._io, self, self._root) - self._io.seek(_pos) - - return self._m_next_ifd if hasattr(self, '_m_next_ifd') else None - - - class IfdField(KaitaiStruct): - - class FieldTypeEnum(Enum): - byte = 1 - ascii_string = 2 - word = 3 - dword = 4 - rational = 5 - undefined = 7 - slong = 9 - srational = 10 - - class TagEnum(Enum): - image_width = 256 - image_height = 257 - bits_per_sample = 258 - compression = 259 - photometric_interpretation = 262 - thresholding = 263 - cell_width = 264 - cell_length = 265 - fill_order = 266 - document_name = 269 - image_description = 270 - make = 271 - model = 272 - strip_offsets = 273 - orientation = 274 - samples_per_pixel = 277 - rows_per_strip = 278 - strip_byte_counts = 279 - min_sample_value = 280 - max_sample_value = 281 - x_resolution = 282 - y_resolution = 283 - planar_configuration = 284 - page_name = 285 - x_position = 286 - y_position = 287 - free_offsets = 288 - free_byte_counts = 289 - gray_response_unit = 290 - gray_response_curve = 291 - t4_options = 292 - t6_options = 293 - resolution_unit = 296 - page_number = 297 - color_response_unit = 300 - transfer_function = 301 - software = 305 - modify_date = 306 - artist = 315 - host_computer = 316 - predictor = 317 - white_point = 318 - primary_chromaticities = 319 - color_map = 320 - halftone_hints = 321 - tile_width = 322 - tile_length = 323 - tile_offsets = 324 - tile_byte_counts = 325 - bad_fax_lines = 326 - clean_fax_data = 327 - consecutive_bad_fax_lines = 328 - sub_ifd = 330 - ink_set = 332 - ink_names = 333 - numberof_inks = 334 - dot_range = 336 - target_printer = 337 - extra_samples = 338 - sample_format = 339 - s_min_sample_value = 340 - s_max_sample_value = 341 - transfer_range = 342 - clip_path = 343 - x_clip_path_units = 344 - y_clip_path_units = 345 - indexed = 346 - jpeg_tables = 347 - opi_proxy = 351 - global_parameters_ifd = 400 - profile_type = 401 - fax_profile = 402 - coding_methods = 403 - version_year = 404 - mode_number = 405 - decode = 433 - default_image_color = 434 - t82_options = 435 - jpeg_tables2 = 437 - jpeg_proc = 512 - thumbnail_offset = 513 - thumbnail_length = 514 - jpeg_restart_interval = 515 - jpeg_lossless_predictors = 517 - jpeg_point_transforms = 518 - jpegq_tables = 519 - jpegdc_tables = 520 - jpegac_tables = 521 - y_cb_cr_coefficients = 529 - y_cb_cr_sub_sampling = 530 - y_cb_cr_positioning = 531 - reference_black_white = 532 - strip_row_counts = 559 - application_notes = 700 - uspto_miscellaneous = 999 - related_image_file_format = 4096 - related_image_width = 4097 - related_image_height = 4098 - rating = 18246 - xp_dip_xml = 18247 - stitch_info = 18248 - rating_percent = 18249 - sony_raw_file_type = 28672 - light_falloff_params = 28722 - chromatic_aberration_corr_params = 28725 - distortion_corr_params = 28727 - image_id = 32781 - wang_tag1 = 32931 - wang_annotation = 32932 - wang_tag3 = 32933 - wang_tag4 = 32934 - image_reference_points = 32953 - region_xform_tack_point = 32954 - warp_quadrilateral = 32955 - affine_transform_mat = 32956 - matteing = 32995 - data_type = 32996 - image_depth = 32997 - tile_depth = 32998 - image_full_width = 33300 - image_full_height = 33301 - texture_format = 33302 - wrap_modes = 33303 - fov_cot = 33304 - matrix_world_to_screen = 33305 - matrix_world_to_camera = 33306 - model2 = 33405 - cfa_repeat_pattern_dim = 33421 - cfa_pattern2 = 33422 - battery_level = 33423 - kodak_ifd = 33424 - copyright = 33432 - exposure_time = 33434 - f_number = 33437 - md_file_tag = 33445 - md_scale_pixel = 33446 - md_color_table = 33447 - md_lab_name = 33448 - md_sample_info = 33449 - md_prep_date = 33450 - md_prep_time = 33451 - md_file_units = 33452 - pixel_scale = 33550 - advent_scale = 33589 - advent_revision = 33590 - uic1_tag = 33628 - uic2_tag = 33629 - uic3_tag = 33630 - uic4_tag = 33631 - iptc_naa = 33723 - intergraph_packet_data = 33918 - intergraph_flag_registers = 33919 - intergraph_matrix = 33920 - ingr_reserved = 33921 - model_tie_point = 33922 - site = 34016 - color_sequence = 34017 - it8_header = 34018 - raster_padding = 34019 - bits_per_run_length = 34020 - bits_per_extended_run_length = 34021 - color_table = 34022 - image_color_indicator = 34023 - background_color_indicator = 34024 - image_color_value = 34025 - background_color_value = 34026 - pixel_intensity_range = 34027 - transparency_indicator = 34028 - color_characterization = 34029 - hc_usage = 34030 - trap_indicator = 34031 - cmyk_equivalent = 34032 - sem_info = 34118 - afcp_iptc = 34152 - pixel_magic_jbig_options = 34232 - jpl_carto_ifd = 34263 - model_transform = 34264 - wb_grgb_levels = 34306 - leaf_data = 34310 - photoshop_settings = 34377 - exif_offset = 34665 - icc_profile = 34675 - tiff_fx_extensions = 34687 - multi_profiles = 34688 - shared_data = 34689 - t88_options = 34690 - image_layer = 34732 - geo_tiff_directory = 34735 - geo_tiff_double_params = 34736 - geo_tiff_ascii_params = 34737 - jbig_options = 34750 - exposure_program = 34850 - spectral_sensitivity = 34852 - gps_info = 34853 - iso = 34855 - opto_electric_conv_factor = 34856 - interlace = 34857 - time_zone_offset = 34858 - self_timer_mode = 34859 - sensitivity_type = 34864 - standard_output_sensitivity = 34865 - recommended_exposure_index = 34866 - iso_speed = 34867 - iso_speed_latitudeyyy = 34868 - iso_speed_latitudezzz = 34869 - fax_recv_params = 34908 - fax_sub_address = 34909 - fax_recv_time = 34910 - fedex_edr = 34929 - leaf_sub_ifd = 34954 - exif_version = 36864 - date_time_original = 36867 - create_date = 36868 - google_plus_upload_code = 36873 - offset_time = 36880 - offset_time_original = 36881 - offset_time_digitized = 36882 - components_configuration = 37121 - compressed_bits_per_pixel = 37122 - shutter_speed_value = 37377 - aperture_value = 37378 - brightness_value = 37379 - exposure_compensation = 37380 - max_aperture_value = 37381 - subject_distance = 37382 - metering_mode = 37383 - light_source = 37384 - flash = 37385 - focal_length = 37386 - flash_energy = 37387 - spatial_frequency_response = 37388 - noise = 37389 - focal_plane_x_resolution = 37390 - focal_plane_y_resolution = 37391 - focal_plane_resolution_unit = 37392 - image_number = 37393 - security_classification = 37394 - image_history = 37395 - subject_area = 37396 - exposure_index = 37397 - tiff_ep_standard_id = 37398 - sensing_method = 37399 - cip3_data_file = 37434 - cip3_sheet = 37435 - cip3_side = 37436 - sto_nits = 37439 - maker_note = 37500 - user_comment = 37510 - sub_sec_time = 37520 - sub_sec_time_original = 37521 - sub_sec_time_digitized = 37522 - ms_document_text = 37679 - ms_property_set_storage = 37680 - ms_document_text_position = 37681 - image_source_data = 37724 - ambient_temperature = 37888 - humidity = 37889 - pressure = 37890 - water_depth = 37891 - acceleration = 37892 - camera_elevation_angle = 37893 - xp_title = 40091 - xp_comment = 40092 - xp_author = 40093 - xp_keywords = 40094 - xp_subject = 40095 - flashpix_version = 40960 - color_space = 40961 - exif_image_width = 40962 - exif_image_height = 40963 - related_sound_file = 40964 - interop_offset = 40965 - samsung_raw_pointers_offset = 40976 - samsung_raw_pointers_length = 40977 - samsung_raw_byte_order = 41217 - samsung_raw_unknown = 41218 - flash_energy2 = 41483 - spatial_frequency_response2 = 41484 - noise2 = 41485 - focal_plane_x_resolution2 = 41486 - focal_plane_y_resolution2 = 41487 - focal_plane_resolution_unit2 = 41488 - image_number2 = 41489 - security_classification2 = 41490 - image_history2 = 41491 - subject_location = 41492 - exposure_index2 = 41493 - tiff_ep_standard_id2 = 41494 - sensing_method2 = 41495 - file_source = 41728 - scene_type = 41729 - cfa_pattern = 41730 - custom_rendered = 41985 - exposure_mode = 41986 - white_balance = 41987 - digital_zoom_ratio = 41988 - focal_length_in35mm_format = 41989 - scene_capture_type = 41990 - gain_control = 41991 - contrast = 41992 - saturation = 41993 - sharpness = 41994 - device_setting_description = 41995 - subject_distance_range = 41996 - image_unique_id = 42016 - owner_name = 42032 - serial_number = 42033 - lens_info = 42034 - lens_make = 42035 - lens_model = 42036 - lens_serial_number = 42037 - gdal_metadata = 42112 - gdal_no_data = 42113 - gamma = 42240 - expand_software = 44992 - expand_lens = 44993 - expand_film = 44994 - expand_filter_lens = 44995 - expand_scanner = 44996 - expand_flash_lamp = 44997 - pixel_format = 48129 - transformation = 48130 - uncompressed = 48131 - image_type = 48132 - image_width2 = 48256 - image_height2 = 48257 - width_resolution = 48258 - height_resolution = 48259 - image_offset = 48320 - image_byte_count = 48321 - alpha_offset = 48322 - alpha_byte_count = 48323 - image_data_discard = 48324 - alpha_data_discard = 48325 - oce_scanjob_desc = 50215 - oce_application_selector = 50216 - oce_id_number = 50217 - oce_image_logic = 50218 - annotations = 50255 - print_im = 50341 - original_file_name = 50547 - uspto_original_content_type = 50560 - dng_version = 50706 - dng_backward_version = 50707 - unique_camera_model = 50708 - localized_camera_model = 50709 - cfa_plane_color = 50710 - cfa_layout = 50711 - linearization_table = 50712 - black_level_repeat_dim = 50713 - black_level = 50714 - black_level_delta_h = 50715 - black_level_delta_v = 50716 - white_level = 50717 - default_scale = 50718 - default_crop_origin = 50719 - default_crop_size = 50720 - color_matrix1 = 50721 - color_matrix2 = 50722 - camera_calibration1 = 50723 - camera_calibration2 = 50724 - reduction_matrix1 = 50725 - reduction_matrix2 = 50726 - analog_balance = 50727 - as_shot_neutral = 50728 - as_shot_white_xy = 50729 - baseline_exposure = 50730 - baseline_noise = 50731 - baseline_sharpness = 50732 - bayer_green_split = 50733 - linear_response_limit = 50734 - camera_serial_number = 50735 - dng_lens_info = 50736 - chroma_blur_radius = 50737 - anti_alias_strength = 50738 - shadow_scale = 50739 - sr2_private = 50740 - maker_note_safety = 50741 - raw_image_segmentation = 50752 - calibration_illuminant1 = 50778 - calibration_illuminant2 = 50779 - best_quality_scale = 50780 - raw_data_unique_id = 50781 - alias_layer_metadata = 50784 - original_raw_file_name = 50827 - original_raw_file_data = 50828 - active_area = 50829 - masked_areas = 50830 - as_shot_icc_profile = 50831 - as_shot_pre_profile_matrix = 50832 - current_icc_profile = 50833 - current_pre_profile_matrix = 50834 - colorimetric_reference = 50879 - s_raw_type = 50885 - panasonic_title = 50898 - panasonic_title2 = 50899 - camera_calibration_sig = 50931 - profile_calibration_sig = 50932 - profile_ifd = 50933 - as_shot_profile_name = 50934 - noise_reduction_applied = 50935 - profile_name = 50936 - profile_hue_sat_map_dims = 50937 - profile_hue_sat_map_data1 = 50938 - profile_hue_sat_map_data2 = 50939 - profile_tone_curve = 50940 - profile_embed_policy = 50941 - profile_copyright = 50942 - forward_matrix1 = 50964 - forward_matrix2 = 50965 - preview_application_name = 50966 - preview_application_version = 50967 - preview_settings_name = 50968 - preview_settings_digest = 50969 - preview_color_space = 50970 - preview_date_time = 50971 - raw_image_digest = 50972 - original_raw_file_digest = 50973 - sub_tile_block_size = 50974 - row_interleave_factor = 50975 - profile_look_table_dims = 50981 - profile_look_table_data = 50982 - opcode_list1 = 51008 - opcode_list2 = 51009 - opcode_list3 = 51022 - noise_profile = 51041 - time_codes = 51043 - frame_rate = 51044 - t_stop = 51058 - reel_name = 51081 - original_default_final_size = 51089 - original_best_quality_size = 51090 - original_default_crop_size = 51091 - camera_label = 51105 - profile_hue_sat_map_encoding = 51107 - profile_look_table_encoding = 51108 - baseline_exposure_offset = 51109 - default_black_render = 51110 - new_raw_image_digest = 51111 - raw_to_preview_gain = 51112 - default_user_crop = 51125 - padding = 59932 - offset_schema = 59933 - owner_name2 = 65000 - serial_number2 = 65001 - lens = 65002 - kdc_ifd = 65024 - raw_file = 65100 - converter = 65101 - white_balance2 = 65102 - exposure = 65105 - shadows = 65106 - brightness = 65107 - contrast2 = 65108 - saturation2 = 65109 - sharpness2 = 65110 - smoothness = 65111 - moire_filter = 65112 - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.tag = self._root.IfdField.TagEnum(self._io.read_u2be()) - self.field_type = self._root.IfdField.FieldTypeEnum(self._io.read_u2be()) - self.length = self._io.read_u4be() - self.ofs_or_data = self._io.read_u4be() - - @property - def type_byte_length(self): - if hasattr(self, '_m_type_byte_length'): - return self._m_type_byte_length if hasattr(self, '_m_type_byte_length') else None - - self._m_type_byte_length = (2 if self.field_type == self._root.IfdField.FieldTypeEnum.word else (4 if self.field_type == self._root.IfdField.FieldTypeEnum.dword else 1)) - return self._m_type_byte_length if hasattr(self, '_m_type_byte_length') else None - - @property - def byte_length(self): - if hasattr(self, '_m_byte_length'): - return self._m_byte_length if hasattr(self, '_m_byte_length') else None - - self._m_byte_length = (self.length * self.type_byte_length) - return self._m_byte_length if hasattr(self, '_m_byte_length') else None - - @property - def is_immediate_data(self): - if hasattr(self, '_m_is_immediate_data'): - return self._m_is_immediate_data if hasattr(self, '_m_is_immediate_data') else None - - self._m_is_immediate_data = self.byte_length <= 4 - return self._m_is_immediate_data if hasattr(self, '_m_is_immediate_data') else None - - @property - def data(self): - if hasattr(self, '_m_data'): - return self._m_data if hasattr(self, '_m_data') else None - - if not (self.is_immediate_data): - io = self._root._io - _pos = io.pos() - io.seek(self.ofs_or_data) - self._m_data = io.read_bytes(self.byte_length) - io.seek(_pos) - - return self._m_data if hasattr(self, '_m_data') else None - - - @property - def ifd0(self): - if hasattr(self, '_m_ifd0'): - return self._m_ifd0 if hasattr(self, '_m_ifd0') else None - - _pos = self._io.pos() - self._io.seek(self.ifd0_ofs) - self._m_ifd0 = self._root.Ifd(self._io, self, self._root) - self._io.seek(_pos) - return self._m_ifd0 if hasattr(self, '_m_ifd0') else None - - diff --git a/mitmproxy/contrib/kaitaistruct/exif_le.py b/mitmproxy/contrib/kaitaistruct/exif_le.py deleted file mode 100644 index 3c7e43ae29..0000000000 --- a/mitmproxy/contrib/kaitaistruct/exif_le.py +++ /dev/null @@ -1,582 +0,0 @@ -# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild - -from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO -from enum import Enum - - -# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 - -class ExifLe(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.version = self._io.read_u2le() - self.ifd0_ofs = self._io.read_u4le() - - class Ifd(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.num_fields = self._io.read_u2le() - self.fields = [None] * (self.num_fields) - for i in range(self.num_fields): - self.fields[i] = self._root.IfdField(self._io, self, self._root) - - self.next_ifd_ofs = self._io.read_u4le() - - @property - def next_ifd(self): - if hasattr(self, '_m_next_ifd'): - return self._m_next_ifd if hasattr(self, '_m_next_ifd') else None - - if self.next_ifd_ofs != 0: - _pos = self._io.pos() - self._io.seek(self.next_ifd_ofs) - self._m_next_ifd = self._root.Ifd(self._io, self, self._root) - self._io.seek(_pos) - - return self._m_next_ifd if hasattr(self, '_m_next_ifd') else None - - - class IfdField(KaitaiStruct): - - class FieldTypeEnum(Enum): - byte = 1 - ascii_string = 2 - word = 3 - dword = 4 - rational = 5 - undefined = 7 - slong = 9 - srational = 10 - - class TagEnum(Enum): - image_width = 256 - image_height = 257 - bits_per_sample = 258 - compression = 259 - photometric_interpretation = 262 - thresholding = 263 - cell_width = 264 - cell_length = 265 - fill_order = 266 - document_name = 269 - image_description = 270 - make = 271 - model = 272 - strip_offsets = 273 - orientation = 274 - samples_per_pixel = 277 - rows_per_strip = 278 - strip_byte_counts = 279 - min_sample_value = 280 - max_sample_value = 281 - x_resolution = 282 - y_resolution = 283 - planar_configuration = 284 - page_name = 285 - x_position = 286 - y_position = 287 - free_offsets = 288 - free_byte_counts = 289 - gray_response_unit = 290 - gray_response_curve = 291 - t4_options = 292 - t6_options = 293 - resolution_unit = 296 - page_number = 297 - color_response_unit = 300 - transfer_function = 301 - software = 305 - modify_date = 306 - artist = 315 - host_computer = 316 - predictor = 317 - white_point = 318 - primary_chromaticities = 319 - color_map = 320 - halftone_hints = 321 - tile_width = 322 - tile_length = 323 - tile_offsets = 324 - tile_byte_counts = 325 - bad_fax_lines = 326 - clean_fax_data = 327 - consecutive_bad_fax_lines = 328 - sub_ifd = 330 - ink_set = 332 - ink_names = 333 - numberof_inks = 334 - dot_range = 336 - target_printer = 337 - extra_samples = 338 - sample_format = 339 - s_min_sample_value = 340 - s_max_sample_value = 341 - transfer_range = 342 - clip_path = 343 - x_clip_path_units = 344 - y_clip_path_units = 345 - indexed = 346 - jpeg_tables = 347 - opi_proxy = 351 - global_parameters_ifd = 400 - profile_type = 401 - fax_profile = 402 - coding_methods = 403 - version_year = 404 - mode_number = 405 - decode = 433 - default_image_color = 434 - t82_options = 435 - jpeg_tables2 = 437 - jpeg_proc = 512 - thumbnail_offset = 513 - thumbnail_length = 514 - jpeg_restart_interval = 515 - jpeg_lossless_predictors = 517 - jpeg_point_transforms = 518 - jpegq_tables = 519 - jpegdc_tables = 520 - jpegac_tables = 521 - y_cb_cr_coefficients = 529 - y_cb_cr_sub_sampling = 530 - y_cb_cr_positioning = 531 - reference_black_white = 532 - strip_row_counts = 559 - application_notes = 700 - uspto_miscellaneous = 999 - related_image_file_format = 4096 - related_image_width = 4097 - related_image_height = 4098 - rating = 18246 - xp_dip_xml = 18247 - stitch_info = 18248 - rating_percent = 18249 - sony_raw_file_type = 28672 - light_falloff_params = 28722 - chromatic_aberration_corr_params = 28725 - distortion_corr_params = 28727 - image_id = 32781 - wang_tag1 = 32931 - wang_annotation = 32932 - wang_tag3 = 32933 - wang_tag4 = 32934 - image_reference_points = 32953 - region_xform_tack_point = 32954 - warp_quadrilateral = 32955 - affine_transform_mat = 32956 - matteing = 32995 - data_type = 32996 - image_depth = 32997 - tile_depth = 32998 - image_full_width = 33300 - image_full_height = 33301 - texture_format = 33302 - wrap_modes = 33303 - fov_cot = 33304 - matrix_world_to_screen = 33305 - matrix_world_to_camera = 33306 - model2 = 33405 - cfa_repeat_pattern_dim = 33421 - cfa_pattern2 = 33422 - battery_level = 33423 - kodak_ifd = 33424 - copyright = 33432 - exposure_time = 33434 - f_number = 33437 - md_file_tag = 33445 - md_scale_pixel = 33446 - md_color_table = 33447 - md_lab_name = 33448 - md_sample_info = 33449 - md_prep_date = 33450 - md_prep_time = 33451 - md_file_units = 33452 - pixel_scale = 33550 - advent_scale = 33589 - advent_revision = 33590 - uic1_tag = 33628 - uic2_tag = 33629 - uic3_tag = 33630 - uic4_tag = 33631 - iptc_naa = 33723 - intergraph_packet_data = 33918 - intergraph_flag_registers = 33919 - intergraph_matrix = 33920 - ingr_reserved = 33921 - model_tie_point = 33922 - site = 34016 - color_sequence = 34017 - it8_header = 34018 - raster_padding = 34019 - bits_per_run_length = 34020 - bits_per_extended_run_length = 34021 - color_table = 34022 - image_color_indicator = 34023 - background_color_indicator = 34024 - image_color_value = 34025 - background_color_value = 34026 - pixel_intensity_range = 34027 - transparency_indicator = 34028 - color_characterization = 34029 - hc_usage = 34030 - trap_indicator = 34031 - cmyk_equivalent = 34032 - sem_info = 34118 - afcp_iptc = 34152 - pixel_magic_jbig_options = 34232 - jpl_carto_ifd = 34263 - model_transform = 34264 - wb_grgb_levels = 34306 - leaf_data = 34310 - photoshop_settings = 34377 - exif_offset = 34665 - icc_profile = 34675 - tiff_fx_extensions = 34687 - multi_profiles = 34688 - shared_data = 34689 - t88_options = 34690 - image_layer = 34732 - geo_tiff_directory = 34735 - geo_tiff_double_params = 34736 - geo_tiff_ascii_params = 34737 - jbig_options = 34750 - exposure_program = 34850 - spectral_sensitivity = 34852 - gps_info = 34853 - iso = 34855 - opto_electric_conv_factor = 34856 - interlace = 34857 - time_zone_offset = 34858 - self_timer_mode = 34859 - sensitivity_type = 34864 - standard_output_sensitivity = 34865 - recommended_exposure_index = 34866 - iso_speed = 34867 - iso_speed_latitudeyyy = 34868 - iso_speed_latitudezzz = 34869 - fax_recv_params = 34908 - fax_sub_address = 34909 - fax_recv_time = 34910 - fedex_edr = 34929 - leaf_sub_ifd = 34954 - exif_version = 36864 - date_time_original = 36867 - create_date = 36868 - google_plus_upload_code = 36873 - offset_time = 36880 - offset_time_original = 36881 - offset_time_digitized = 36882 - components_configuration = 37121 - compressed_bits_per_pixel = 37122 - shutter_speed_value = 37377 - aperture_value = 37378 - brightness_value = 37379 - exposure_compensation = 37380 - max_aperture_value = 37381 - subject_distance = 37382 - metering_mode = 37383 - light_source = 37384 - flash = 37385 - focal_length = 37386 - flash_energy = 37387 - spatial_frequency_response = 37388 - noise = 37389 - focal_plane_x_resolution = 37390 - focal_plane_y_resolution = 37391 - focal_plane_resolution_unit = 37392 - image_number = 37393 - security_classification = 37394 - image_history = 37395 - subject_area = 37396 - exposure_index = 37397 - tiff_ep_standard_id = 37398 - sensing_method = 37399 - cip3_data_file = 37434 - cip3_sheet = 37435 - cip3_side = 37436 - sto_nits = 37439 - maker_note = 37500 - user_comment = 37510 - sub_sec_time = 37520 - sub_sec_time_original = 37521 - sub_sec_time_digitized = 37522 - ms_document_text = 37679 - ms_property_set_storage = 37680 - ms_document_text_position = 37681 - image_source_data = 37724 - ambient_temperature = 37888 - humidity = 37889 - pressure = 37890 - water_depth = 37891 - acceleration = 37892 - camera_elevation_angle = 37893 - xp_title = 40091 - xp_comment = 40092 - xp_author = 40093 - xp_keywords = 40094 - xp_subject = 40095 - flashpix_version = 40960 - color_space = 40961 - exif_image_width = 40962 - exif_image_height = 40963 - related_sound_file = 40964 - interop_offset = 40965 - samsung_raw_pointers_offset = 40976 - samsung_raw_pointers_length = 40977 - samsung_raw_byte_order = 41217 - samsung_raw_unknown = 41218 - flash_energy2 = 41483 - spatial_frequency_response2 = 41484 - noise2 = 41485 - focal_plane_x_resolution2 = 41486 - focal_plane_y_resolution2 = 41487 - focal_plane_resolution_unit2 = 41488 - image_number2 = 41489 - security_classification2 = 41490 - image_history2 = 41491 - subject_location = 41492 - exposure_index2 = 41493 - tiff_ep_standard_id2 = 41494 - sensing_method2 = 41495 - file_source = 41728 - scene_type = 41729 - cfa_pattern = 41730 - custom_rendered = 41985 - exposure_mode = 41986 - white_balance = 41987 - digital_zoom_ratio = 41988 - focal_length_in35mm_format = 41989 - scene_capture_type = 41990 - gain_control = 41991 - contrast = 41992 - saturation = 41993 - sharpness = 41994 - device_setting_description = 41995 - subject_distance_range = 41996 - image_unique_id = 42016 - owner_name = 42032 - serial_number = 42033 - lens_info = 42034 - lens_make = 42035 - lens_model = 42036 - lens_serial_number = 42037 - gdal_metadata = 42112 - gdal_no_data = 42113 - gamma = 42240 - expand_software = 44992 - expand_lens = 44993 - expand_film = 44994 - expand_filter_lens = 44995 - expand_scanner = 44996 - expand_flash_lamp = 44997 - pixel_format = 48129 - transformation = 48130 - uncompressed = 48131 - image_type = 48132 - image_width2 = 48256 - image_height2 = 48257 - width_resolution = 48258 - height_resolution = 48259 - image_offset = 48320 - image_byte_count = 48321 - alpha_offset = 48322 - alpha_byte_count = 48323 - image_data_discard = 48324 - alpha_data_discard = 48325 - oce_scanjob_desc = 50215 - oce_application_selector = 50216 - oce_id_number = 50217 - oce_image_logic = 50218 - annotations = 50255 - print_im = 50341 - original_file_name = 50547 - uspto_original_content_type = 50560 - dng_version = 50706 - dng_backward_version = 50707 - unique_camera_model = 50708 - localized_camera_model = 50709 - cfa_plane_color = 50710 - cfa_layout = 50711 - linearization_table = 50712 - black_level_repeat_dim = 50713 - black_level = 50714 - black_level_delta_h = 50715 - black_level_delta_v = 50716 - white_level = 50717 - default_scale = 50718 - default_crop_origin = 50719 - default_crop_size = 50720 - color_matrix1 = 50721 - color_matrix2 = 50722 - camera_calibration1 = 50723 - camera_calibration2 = 50724 - reduction_matrix1 = 50725 - reduction_matrix2 = 50726 - analog_balance = 50727 - as_shot_neutral = 50728 - as_shot_white_xy = 50729 - baseline_exposure = 50730 - baseline_noise = 50731 - baseline_sharpness = 50732 - bayer_green_split = 50733 - linear_response_limit = 50734 - camera_serial_number = 50735 - dng_lens_info = 50736 - chroma_blur_radius = 50737 - anti_alias_strength = 50738 - shadow_scale = 50739 - sr2_private = 50740 - maker_note_safety = 50741 - raw_image_segmentation = 50752 - calibration_illuminant1 = 50778 - calibration_illuminant2 = 50779 - best_quality_scale = 50780 - raw_data_unique_id = 50781 - alias_layer_metadata = 50784 - original_raw_file_name = 50827 - original_raw_file_data = 50828 - active_area = 50829 - masked_areas = 50830 - as_shot_icc_profile = 50831 - as_shot_pre_profile_matrix = 50832 - current_icc_profile = 50833 - current_pre_profile_matrix = 50834 - colorimetric_reference = 50879 - s_raw_type = 50885 - panasonic_title = 50898 - panasonic_title2 = 50899 - camera_calibration_sig = 50931 - profile_calibration_sig = 50932 - profile_ifd = 50933 - as_shot_profile_name = 50934 - noise_reduction_applied = 50935 - profile_name = 50936 - profile_hue_sat_map_dims = 50937 - profile_hue_sat_map_data1 = 50938 - profile_hue_sat_map_data2 = 50939 - profile_tone_curve = 50940 - profile_embed_policy = 50941 - profile_copyright = 50942 - forward_matrix1 = 50964 - forward_matrix2 = 50965 - preview_application_name = 50966 - preview_application_version = 50967 - preview_settings_name = 50968 - preview_settings_digest = 50969 - preview_color_space = 50970 - preview_date_time = 50971 - raw_image_digest = 50972 - original_raw_file_digest = 50973 - sub_tile_block_size = 50974 - row_interleave_factor = 50975 - profile_look_table_dims = 50981 - profile_look_table_data = 50982 - opcode_list1 = 51008 - opcode_list2 = 51009 - opcode_list3 = 51022 - noise_profile = 51041 - time_codes = 51043 - frame_rate = 51044 - t_stop = 51058 - reel_name = 51081 - original_default_final_size = 51089 - original_best_quality_size = 51090 - original_default_crop_size = 51091 - camera_label = 51105 - profile_hue_sat_map_encoding = 51107 - profile_look_table_encoding = 51108 - baseline_exposure_offset = 51109 - default_black_render = 51110 - new_raw_image_digest = 51111 - raw_to_preview_gain = 51112 - default_user_crop = 51125 - padding = 59932 - offset_schema = 59933 - owner_name2 = 65000 - serial_number2 = 65001 - lens = 65002 - kdc_ifd = 65024 - raw_file = 65100 - converter = 65101 - white_balance2 = 65102 - exposure = 65105 - shadows = 65106 - brightness = 65107 - contrast2 = 65108 - saturation2 = 65109 - sharpness2 = 65110 - smoothness = 65111 - moire_filter = 65112 - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.tag = self._root.IfdField.TagEnum(self._io.read_u2le()) - self.field_type = self._root.IfdField.FieldTypeEnum(self._io.read_u2le()) - self.length = self._io.read_u4le() - self.ofs_or_data = self._io.read_u4le() - - @property - def type_byte_length(self): - if hasattr(self, '_m_type_byte_length'): - return self._m_type_byte_length if hasattr(self, '_m_type_byte_length') else None - - self._m_type_byte_length = (2 if self.field_type == self._root.IfdField.FieldTypeEnum.word else (4 if self.field_type == self._root.IfdField.FieldTypeEnum.dword else 1)) - return self._m_type_byte_length if hasattr(self, '_m_type_byte_length') else None - - @property - def byte_length(self): - if hasattr(self, '_m_byte_length'): - return self._m_byte_length if hasattr(self, '_m_byte_length') else None - - self._m_byte_length = (self.length * self.type_byte_length) - return self._m_byte_length if hasattr(self, '_m_byte_length') else None - - @property - def is_immediate_data(self): - if hasattr(self, '_m_is_immediate_data'): - return self._m_is_immediate_data if hasattr(self, '_m_is_immediate_data') else None - - self._m_is_immediate_data = self.byte_length <= 4 - return self._m_is_immediate_data if hasattr(self, '_m_is_immediate_data') else None - - @property - def data(self): - if hasattr(self, '_m_data'): - return self._m_data if hasattr(self, '_m_data') else None - - if not (self.is_immediate_data): - io = self._root._io - _pos = io.pos() - io.seek(self.ofs_or_data) - self._m_data = io.read_bytes(self.byte_length) - io.seek(_pos) - - return self._m_data if hasattr(self, '_m_data') else None - - - @property - def ifd0(self): - if hasattr(self, '_m_ifd0'): - return self._m_ifd0 if hasattr(self, '_m_ifd0') else None - - _pos = self._io.pos() - self._io.seek(self.ifd0_ofs) - self._m_ifd0 = self._root.Ifd(self._io, self, self._root) - self._io.seek(_pos) - return self._m_ifd0 if hasattr(self, '_m_ifd0') else None - - diff --git a/mitmproxy/contrib/kaitaistruct/gif.py b/mitmproxy/contrib/kaitaistruct/gif.py index a85ff0a9c0..09fa564816 100644 --- a/mitmproxy/contrib/kaitaistruct/gif.py +++ b/mitmproxy/contrib/kaitaistruct/gif.py @@ -1,16 +1,32 @@ # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild -import array -import struct -import zlib -from enum import Enum - +import kaitaistruct from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO +from enum import Enum -# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 +if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): + raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) class Gif(KaitaiStruct): + """GIF (Graphics Interchange Format) is an image file format, developed + in 1987. It became popular in 1990s as one of the main image formats + used in World Wide Web. + + GIF format allows encoding of palette-based images up to 256 colors + (each of the colors can be chosen from a 24-bit RGB + colorspace). Image data stream uses LZW (Lempel-Ziv-Welch) lossless + compression. + + Over the years, several version of the format were published and + several extensions to it were made, namely, a popular Netscape + extension that allows to store several images in one file, switching + between them, which produces crude form of animation. + + Structurally, format consists of several mandatory headers and then + a stream of blocks follows. Blocks can carry additional + metainformation or image data. + """ class BlockType(Enum): extension = 33 @@ -25,27 +41,39 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self - self.hdr = self._root.Header(self._io, self, self._root) - self.logical_screen_descriptor = self._root.LogicalScreenDescriptorStruct(self._io, self, self._root) + self._read() + + def _read(self): + self.hdr = Gif.Header(self._io, self, self._root) + self.logical_screen_descriptor = Gif.LogicalScreenDescriptorStruct(self._io, self, self._root) if self.logical_screen_descriptor.has_color_table: self._raw_global_color_table = self._io.read_bytes((self.logical_screen_descriptor.color_table_size * 3)) - io = KaitaiStream(BytesIO(self._raw_global_color_table)) - self.global_color_table = self._root.ColorTable(io, self, self._root) + _io__raw_global_color_table = KaitaiStream(BytesIO(self._raw_global_color_table)) + self.global_color_table = Gif.ColorTable(_io__raw_global_color_table, self, self._root) self.blocks = [] + i = 0 while True: - _ = self._root.Block(self._io, self, self._root) + _ = Gif.Block(self._io, self, self._root) self.blocks.append(_) - if ((self._io.is_eof()) or (_.block_type == self._root.BlockType.end_of_file)) : + if ((self._io.is_eof()) or (_.block_type == Gif.BlockType.end_of_file)) : break + i += 1 class ImageData(KaitaiStruct): + """ + .. seealso:: + - section 22 - https://www.w3.org/Graphics/GIF/spec-gif89a.txt + """ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.lzw_min_code_size = self._io.read_u1() - self.subblocks = self._root.Subblocks(self._io, self, self._root) + self.subblocks = Gif.Subblocks(self._io, self, self._root) class ColorTableEntry(KaitaiStruct): @@ -53,16 +81,26 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.red = self._io.read_u1() self.green = self._io.read_u1() self.blue = self._io.read_u1() class LogicalScreenDescriptorStruct(KaitaiStruct): + """ + .. seealso:: + - section 18 - https://www.w3.org/Graphics/GIF/spec-gif89a.txt + """ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.screen_width = self._io.read_u2le() self.screen_height = self._io.read_u2le() self.flags = self._io.read_u1() @@ -72,18 +110,18 @@ def __init__(self, _io, _parent=None, _root=None): @property def has_color_table(self): if hasattr(self, '_m_has_color_table'): - return self._m_has_color_table if hasattr(self, '_m_has_color_table') else None + return self._m_has_color_table self._m_has_color_table = (self.flags & 128) != 0 - return self._m_has_color_table if hasattr(self, '_m_has_color_table') else None + return getattr(self, '_m_has_color_table', None) @property def color_table_size(self): if hasattr(self, '_m_color_table_size'): - return self._m_color_table_size if hasattr(self, '_m_color_table_size') else None + return self._m_color_table_size self._m_color_table_size = (2 << (self.flags & 7)) - return self._m_color_table_size if hasattr(self, '_m_color_table_size') else None + return getattr(self, '_m_color_table_size', None) class LocalImageDescriptor(KaitaiStruct): @@ -91,6 +129,9 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.left = self._io.read_u2le() self.top = self._io.read_u2le() self.width = self._io.read_u2le() @@ -98,42 +139,42 @@ def __init__(self, _io, _parent=None, _root=None): self.flags = self._io.read_u1() if self.has_color_table: self._raw_local_color_table = self._io.read_bytes((self.color_table_size * 3)) - io = KaitaiStream(BytesIO(self._raw_local_color_table)) - self.local_color_table = self._root.ColorTable(io, self, self._root) + _io__raw_local_color_table = KaitaiStream(BytesIO(self._raw_local_color_table)) + self.local_color_table = Gif.ColorTable(_io__raw_local_color_table, self, self._root) - self.image_data = self._root.ImageData(self._io, self, self._root) + self.image_data = Gif.ImageData(self._io, self, self._root) @property def has_color_table(self): if hasattr(self, '_m_has_color_table'): - return self._m_has_color_table if hasattr(self, '_m_has_color_table') else None + return self._m_has_color_table self._m_has_color_table = (self.flags & 128) != 0 - return self._m_has_color_table if hasattr(self, '_m_has_color_table') else None + return getattr(self, '_m_has_color_table', None) @property def has_interlace(self): if hasattr(self, '_m_has_interlace'): - return self._m_has_interlace if hasattr(self, '_m_has_interlace') else None + return self._m_has_interlace self._m_has_interlace = (self.flags & 64) != 0 - return self._m_has_interlace if hasattr(self, '_m_has_interlace') else None + return getattr(self, '_m_has_interlace', None) @property def has_sorted_color_table(self): if hasattr(self, '_m_has_sorted_color_table'): - return self._m_has_sorted_color_table if hasattr(self, '_m_has_sorted_color_table') else None + return self._m_has_sorted_color_table self._m_has_sorted_color_table = (self.flags & 32) != 0 - return self._m_has_sorted_color_table if hasattr(self, '_m_has_sorted_color_table') else None + return getattr(self, '_m_has_sorted_color_table', None) @property def color_table_size(self): if hasattr(self, '_m_color_table_size'): - return self._m_color_table_size if hasattr(self, '_m_color_table_size') else None + return self._m_color_table_size self._m_color_table_size = (2 << (self.flags & 7)) - return self._m_color_table_size if hasattr(self, '_m_color_table_size') else None + return getattr(self, '_m_color_table_size', None) class Block(KaitaiStruct): @@ -141,60 +182,92 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self - self.block_type = self._root.BlockType(self._io.read_u1()) + self._read() + + def _read(self): + self.block_type = KaitaiStream.resolve_enum(Gif.BlockType, self._io.read_u1()) _on = self.block_type - if _on == self._root.BlockType.extension: - self.body = self._root.Extension(self._io, self, self._root) - elif _on == self._root.BlockType.local_image_descriptor: - self.body = self._root.LocalImageDescriptor(self._io, self, self._root) + if _on == Gif.BlockType.extension: + self.body = Gif.Extension(self._io, self, self._root) + elif _on == Gif.BlockType.local_image_descriptor: + self.body = Gif.LocalImageDescriptor(self._io, self, self._root) class ColorTable(KaitaiStruct): + """ + .. seealso:: + - section 19 - https://www.w3.org/Graphics/GIF/spec-gif89a.txt + """ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.entries = [] + i = 0 while not self._io.is_eof(): - self.entries.append(self._root.ColorTableEntry(self._io, self, self._root)) + self.entries.append(Gif.ColorTableEntry(self._io, self, self._root)) + i += 1 class Header(KaitaiStruct): + """ + .. seealso:: + - section 17 - https://www.w3.org/Graphics/GIF/spec-gif89a.txt + """ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self - self.magic = self._io.ensure_fixed_contents(struct.pack('3b', 71, 73, 70)) + self._read() + + def _read(self): + self.magic = self._io.read_bytes(3) + if not self.magic == b"\x47\x49\x46": + raise kaitaistruct.ValidationNotEqualError(b"\x47\x49\x46", self.magic, self._io, u"/types/header/seq/0") self.version = (self._io.read_bytes(3)).decode(u"ASCII") class ExtGraphicControl(KaitaiStruct): + """ + .. seealso:: + - section 23 - https://www.w3.org/Graphics/GIF/spec-gif89a.txt + """ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self - self.block_size = self._io.ensure_fixed_contents(struct.pack('1b', 4)) + self._read() + + def _read(self): + self.block_size = self._io.read_bytes(1) + if not self.block_size == b"\x04": + raise kaitaistruct.ValidationNotEqualError(b"\x04", self.block_size, self._io, u"/types/ext_graphic_control/seq/0") self.flags = self._io.read_u1() self.delay_time = self._io.read_u2le() self.transparent_idx = self._io.read_u1() - self.terminator = self._io.ensure_fixed_contents(struct.pack('1b', 0)) + self.terminator = self._io.read_bytes(1) + if not self.terminator == b"\x00": + raise kaitaistruct.ValidationNotEqualError(b"\x00", self.terminator, self._io, u"/types/ext_graphic_control/seq/4") @property def transparent_color_flag(self): if hasattr(self, '_m_transparent_color_flag'): - return self._m_transparent_color_flag if hasattr(self, '_m_transparent_color_flag') else None + return self._m_transparent_color_flag self._m_transparent_color_flag = (self.flags & 1) != 0 - return self._m_transparent_color_flag if hasattr(self, '_m_transparent_color_flag') else None + return getattr(self, '_m_transparent_color_flag', None) @property def user_input_flag(self): if hasattr(self, '_m_user_input_flag'): - return self._m_user_input_flag if hasattr(self, '_m_user_input_flag') else None + return self._m_user_input_flag self._m_user_input_flag = (self.flags & 2) != 0 - return self._m_user_input_flag if hasattr(self, '_m_user_input_flag') else None + return getattr(self, '_m_user_input_flag', None) class Subblock(KaitaiStruct): @@ -202,8 +275,26 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self - self.num_bytes = self._io.read_u1() - self.bytes = self._io.read_bytes(self.num_bytes) + self._read() + + def _read(self): + self.len_bytes = self._io.read_u1() + self.bytes = self._io.read_bytes(self.len_bytes) + + + class ApplicationId(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.len_bytes = self._io.read_u1() + if not self.len_bytes == 11: + raise kaitaistruct.ValidationNotEqualError(11, self.len_bytes, self._io, u"/types/application_id/seq/0") + self.application_identifier = (self._io.read_bytes(8)).decode(u"ASCII") + self.application_auth_code = self._io.read_bytes(3) class ExtApplication(KaitaiStruct): @@ -211,13 +302,18 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self - self.application_id = self._root.Subblock(self._io, self, self._root) + self._read() + + def _read(self): + self.application_id = Gif.ApplicationId(self._io, self, self._root) self.subblocks = [] + i = 0 while True: - _ = self._root.Subblock(self._io, self, self._root) + _ = Gif.Subblock(self._io, self, self._root) self.subblocks.append(_) - if _.num_bytes == 0: + if _.len_bytes == 0: break + i += 1 class Subblocks(KaitaiStruct): @@ -225,12 +321,17 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.entries = [] + i = 0 while True: - _ = self._root.Subblock(self._io, self, self._root) + _ = Gif.Subblock(self._io, self, self._root) self.entries.append(_) - if _.num_bytes == 0: + if _.len_bytes == 0: break + i += 1 class Extension(KaitaiStruct): @@ -238,16 +339,19 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self - self.label = self._root.ExtensionLabel(self._io.read_u1()) + self._read() + + def _read(self): + self.label = KaitaiStream.resolve_enum(Gif.ExtensionLabel, self._io.read_u1()) _on = self.label - if _on == self._root.ExtensionLabel.application: - self.body = self._root.ExtApplication(self._io, self, self._root) - elif _on == self._root.ExtensionLabel.comment: - self.body = self._root.Subblocks(self._io, self, self._root) - elif _on == self._root.ExtensionLabel.graphic_control: - self.body = self._root.ExtGraphicControl(self._io, self, self._root) + if _on == Gif.ExtensionLabel.application: + self.body = Gif.ExtApplication(self._io, self, self._root) + elif _on == Gif.ExtensionLabel.comment: + self.body = Gif.Subblocks(self._io, self, self._root) + elif _on == Gif.ExtensionLabel.graphic_control: + self.body = Gif.ExtGraphicControl(self._io, self, self._root) else: - self.body = self._root.Subblocks(self._io, self, self._root) + self.body = Gif.Subblocks(self._io, self, self._root) diff --git a/mitmproxy/contrib/kaitaistruct/google_protobuf.py b/mitmproxy/contrib/kaitaistruct/google_protobuf.py index 09a51949e2..48f5e0ec9b 100644 --- a/mitmproxy/contrib/kaitaistruct/google_protobuf.py +++ b/mitmproxy/contrib/kaitaistruct/google_protobuf.py @@ -1,13 +1,14 @@ # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild - -from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO +import kaitaistruct +from kaitaistruct import KaitaiStream, KaitaiStruct from enum import Enum -# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 +if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): + raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) -from .vlq_base128_le import VlqBase128Le +from . import vlq_base128_le class GoogleProtobuf(KaitaiStruct): """Google Protocol Buffers (AKA protobuf) is a popular data serialization scheme used for communication protocols, data storage, @@ -48,8 +49,10 @@ def __init__(self, _io, _parent=None, _root=None): def _read(self): self.pairs = [] + i = 0 while not self._io.is_eof(): - self.pairs.append(self._root.Pair(self._io, self, self._root)) + self.pairs.append(GoogleProtobuf.Pair(self._io, self, self._root)) + i += 1 class Pair(KaitaiStruct): @@ -69,15 +72,15 @@ def __init__(self, _io, _parent=None, _root=None): self._read() def _read(self): - self.key = VlqBase128Le(self._io) + self.key = vlq_base128_le.VlqBase128Le(self._io) _on = self.wire_type - if _on == self._root.Pair.WireTypes.varint: - self.value = VlqBase128Le(self._io) - elif _on == self._root.Pair.WireTypes.len_delimited: - self.value = self._root.DelimitedBytes(self._io, self, self._root) - elif _on == self._root.Pair.WireTypes.bit_64: + if _on == GoogleProtobuf.Pair.WireTypes.varint: + self.value = vlq_base128_le.VlqBase128Le(self._io) + elif _on == GoogleProtobuf.Pair.WireTypes.len_delimited: + self.value = GoogleProtobuf.DelimitedBytes(self._io, self, self._root) + elif _on == GoogleProtobuf.Pair.WireTypes.bit_64: self.value = self._io.read_u8le() - elif _on == self._root.Pair.WireTypes.bit_32: + elif _on == GoogleProtobuf.Pair.WireTypes.bit_32: self.value = self._io.read_u4le() @property @@ -91,10 +94,10 @@ def wire_type(self): arbitrary bytes from UTF-8 encoded strings, etc. """ if hasattr(self, '_m_wire_type'): - return self._m_wire_type if hasattr(self, '_m_wire_type') else None + return self._m_wire_type - self._m_wire_type = self._root.Pair.WireTypes((self.key.value & 7)) - return self._m_wire_type if hasattr(self, '_m_wire_type') else None + self._m_wire_type = KaitaiStream.resolve_enum(GoogleProtobuf.Pair.WireTypes, (self.key.value & 7)) + return getattr(self, '_m_wire_type', None) @property def field_tag(self): @@ -102,10 +105,10 @@ def field_tag(self): field name in a `.proto` file by this field tag. """ if hasattr(self, '_m_field_tag'): - return self._m_field_tag if hasattr(self, '_m_field_tag') else None + return self._m_field_tag self._m_field_tag = (self.key.value >> 3) - return self._m_field_tag if hasattr(self, '_m_field_tag') else None + return getattr(self, '_m_field_tag', None) class DelimitedBytes(KaitaiStruct): @@ -116,7 +119,7 @@ def __init__(self, _io, _parent=None, _root=None): self._read() def _read(self): - self.len = VlqBase128Le(self._io) + self.len = vlq_base128_le.VlqBase128Le(self._io) self.body = self._io.read_bytes(self.len.value) diff --git a/mitmproxy/contrib/kaitaistruct/ico.py b/mitmproxy/contrib/kaitaistruct/ico.py index c8395dc4f4..39b34ae49a 100644 --- a/mitmproxy/contrib/kaitaistruct/ico.py +++ b/mitmproxy/contrib/kaitaistruct/ico.py @@ -1,10 +1,11 @@ # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild -from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO -import struct +import kaitaistruct +from kaitaistruct import KaitaiStruct -# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 +if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): + raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) class Ico(KaitaiStruct): """Microsoft Windows uses specific file format to store applications @@ -13,7 +14,7 @@ class Ico(KaitaiStruct): contained inside). .. seealso:: - Source - https://msdn.microsoft.com/en-us/library/ms997538.aspx + Source - https://docs.microsoft.com/en-us/previous-versions/ms997538(v=msdn.10) """ def __init__(self, _io, _parent=None, _root=None): self._io = _io @@ -22,11 +23,13 @@ def __init__(self, _io, _parent=None, _root=None): self._read() def _read(self): - self.magic = self._io.ensure_fixed_contents(struct.pack('4b', 0, 0, 1, 0)) + self.magic = self._io.read_bytes(4) + if not self.magic == b"\x00\x00\x01\x00": + raise kaitaistruct.ValidationNotEqualError(b"\x00\x00\x01\x00", self.magic, self._io, u"/seq/0") self.num_images = self._io.read_u2le() - self.images = [None] * (self.num_images) + self.images = [] for i in range(self.num_images): - self.images[i] = self._root.IconDirEntry(self._io, self, self._root) + self.images.append(Ico.IconDirEntry(self._io, self, self._root)) class IconDirEntry(KaitaiStruct): @@ -40,7 +43,9 @@ def _read(self): self.width = self._io.read_u1() self.height = self._io.read_u1() self.num_colors = self._io.read_u1() - self.reserved = self._io.ensure_fixed_contents(struct.pack('1b', 0)) + self.reserved = self._io.read_bytes(1) + if not self.reserved == b"\x00": + raise kaitaistruct.ValidationNotEqualError(b"\x00", self.reserved, self._io, u"/types/icon_dir_entry/seq/3") self.num_planes = self._io.read_u2le() self.bpp = self._io.read_u2le() self.len_img = self._io.read_u4le() @@ -53,13 +58,13 @@ def img(self): relevant parser, if needed to parse image data further. """ if hasattr(self, '_m_img'): - return self._m_img if hasattr(self, '_m_img') else None + return self._m_img _pos = self._io.pos() self._io.seek(self.ofs_img) self._m_img = self._io.read_bytes(self.len_img) self._io.seek(_pos) - return self._m_img if hasattr(self, '_m_img') else None + return getattr(self, '_m_img', None) @property def png_header(self): @@ -67,22 +72,22 @@ def png_header(self): embedded PNG file. """ if hasattr(self, '_m_png_header'): - return self._m_png_header if hasattr(self, '_m_png_header') else None + return self._m_png_header _pos = self._io.pos() self._io.seek(self.ofs_img) self._m_png_header = self._io.read_bytes(8) self._io.seek(_pos) - return self._m_png_header if hasattr(self, '_m_png_header') else None + return getattr(self, '_m_png_header', None) @property def is_png(self): """True if this image is in PNG format.""" if hasattr(self, '_m_is_png'): - return self._m_is_png if hasattr(self, '_m_is_png') else None + return self._m_is_png - self._m_is_png = self.png_header == struct.pack('8b', -119, 80, 78, 71, 13, 10, 26, 10) - return self._m_is_png if hasattr(self, '_m_is_png') else None + self._m_is_png = self.png_header == b"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A" + return getattr(self, '_m_is_png', None) diff --git a/mitmproxy/contrib/kaitaistruct/jpeg.py b/mitmproxy/contrib/kaitaistruct/jpeg.py index 7799152912..60a45bfbf9 100644 --- a/mitmproxy/contrib/kaitaistruct/jpeg.py +++ b/mitmproxy/contrib/kaitaistruct/jpeg.py @@ -1,18 +1,32 @@ # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild -import array -import struct -import zlib -from enum import Enum - +import kaitaistruct from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO +from enum import Enum -# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 - -from .exif import Exif +if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): + raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) +from . import exif class Jpeg(KaitaiStruct): + """JPEG File Interchange Format, or JFIF, or, more colloquially known + as just "JPEG" or "JPG", is a popular 2D bitmap image file format, + offering lossy compression which works reasonably well with + photographic images. + + Format is organized as a container format, serving multiple + "segments", each starting with a magic and a marker. JFIF standard + dictates order and mandatory apperance of segments: + + * SOI + * APP0 (with JFIF magic) + * APP0 (with JFXX magic, optional) + * everything else + * SOS + * JPEG-compressed stream + * EOI + """ class ComponentId(Enum): y = 1 @@ -24,9 +38,14 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.segments = [] + i = 0 while not self._io.is_eof(): - self.segments.append(self._root.Segment(self._io, self, self._root)) + self.segments.append(Jpeg.Segment(self._io, self, self._root)) + i += 1 class Segment(KaitaiStruct): @@ -69,33 +88,38 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self - self.magic = self._io.ensure_fixed_contents(struct.pack('1b', -1)) - self.marker = self._root.Segment.MarkerEnum(self._io.read_u1()) - if ((self.marker != self._root.Segment.MarkerEnum.soi) and (self.marker != self._root.Segment.MarkerEnum.eoi)) : + self._read() + + def _read(self): + self.magic = self._io.read_bytes(1) + if not self.magic == b"\xFF": + raise kaitaistruct.ValidationNotEqualError(b"\xFF", self.magic, self._io, u"/types/segment/seq/0") + self.marker = KaitaiStream.resolve_enum(Jpeg.Segment.MarkerEnum, self._io.read_u1()) + if ((self.marker != Jpeg.Segment.MarkerEnum.soi) and (self.marker != Jpeg.Segment.MarkerEnum.eoi)) : self.length = self._io.read_u2be() - if ((self.marker != self._root.Segment.MarkerEnum.soi) and (self.marker != self._root.Segment.MarkerEnum.eoi)) : + if ((self.marker != Jpeg.Segment.MarkerEnum.soi) and (self.marker != Jpeg.Segment.MarkerEnum.eoi)) : _on = self.marker - if _on == self._root.Segment.MarkerEnum.sos: + if _on == Jpeg.Segment.MarkerEnum.app1: self._raw_data = self._io.read_bytes((self.length - 2)) - io = KaitaiStream(BytesIO(self._raw_data)) - self.data = self._root.SegmentSos(io, self, self._root) - elif _on == self._root.Segment.MarkerEnum.app1: + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = Jpeg.SegmentApp1(_io__raw_data, self, self._root) + elif _on == Jpeg.Segment.MarkerEnum.app0: self._raw_data = self._io.read_bytes((self.length - 2)) - io = KaitaiStream(BytesIO(self._raw_data)) - self.data = self._root.SegmentApp1(io, self, self._root) - elif _on == self._root.Segment.MarkerEnum.sof0: + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = Jpeg.SegmentApp0(_io__raw_data, self, self._root) + elif _on == Jpeg.Segment.MarkerEnum.sof0: self._raw_data = self._io.read_bytes((self.length - 2)) - io = KaitaiStream(BytesIO(self._raw_data)) - self.data = self._root.SegmentSof0(io, self, self._root) - elif _on == self._root.Segment.MarkerEnum.app0: + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = Jpeg.SegmentSof0(_io__raw_data, self, self._root) + elif _on == Jpeg.Segment.MarkerEnum.sos: self._raw_data = self._io.read_bytes((self.length - 2)) - io = KaitaiStream(BytesIO(self._raw_data)) - self.data = self._root.SegmentApp0(io, self, self._root) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = Jpeg.SegmentSos(_io__raw_data, self, self._root) else: self.data = self._io.read_bytes((self.length - 2)) - if self.marker == self._root.Segment.MarkerEnum.sos: + if self.marker == Jpeg.Segment.MarkerEnum.sos: self.image_data = self._io.read_bytes_full() @@ -105,10 +129,13 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.num_components = self._io.read_u1() - self.components = [None] * (self.num_components) + self.components = [] for i in range(self.num_components): - self.components[i] = self._root.SegmentSos.Component(self._io, self, self._root) + self.components.append(Jpeg.SegmentSos.Component(self._io, self, self._root)) self.start_spectral_selection = self._io.read_u1() self.end_spectral = self._io.read_u1() @@ -119,7 +146,10 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self - self.id = self._root.ComponentId(self._io.read_u1()) + self._read() + + def _read(self): + self.id = KaitaiStream.resolve_enum(Jpeg.ComponentId, self._io.read_u1()) self.huffman_table = self._io.read_u1() @@ -129,10 +159,13 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.magic = (self._io.read_bytes_term(0, False, True, True)).decode(u"ASCII") _on = self.magic if _on == u"Exif": - self.body = self._root.ExifInJpeg(self._io, self, self._root) + self.body = Jpeg.ExifInJpeg(self._io, self, self._root) class SegmentSof0(KaitaiStruct): @@ -140,13 +173,16 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.bits_per_sample = self._io.read_u1() self.image_height = self._io.read_u2be() self.image_width = self._io.read_u2be() self.num_components = self._io.read_u1() - self.components = [None] * (self.num_components) + self.components = [] for i in range(self.num_components): - self.components[i] = self._root.SegmentSof0.Component(self._io, self, self._root) + self.components.append(Jpeg.SegmentSof0.Component(self._io, self, self._root)) class Component(KaitaiStruct): @@ -154,25 +190,28 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self - self.id = self._root.ComponentId(self._io.read_u1()) + self._read() + + def _read(self): + self.id = KaitaiStream.resolve_enum(Jpeg.ComponentId, self._io.read_u1()) self.sampling_factors = self._io.read_u1() self.quantization_table_id = self._io.read_u1() @property def sampling_x(self): if hasattr(self, '_m_sampling_x'): - return self._m_sampling_x if hasattr(self, '_m_sampling_x') else None + return self._m_sampling_x self._m_sampling_x = ((self.sampling_factors & 240) >> 4) - return self._m_sampling_x if hasattr(self, '_m_sampling_x') else None + return getattr(self, '_m_sampling_x', None) @property def sampling_y(self): if hasattr(self, '_m_sampling_y'): - return self._m_sampling_y if hasattr(self, '_m_sampling_y') else None + return self._m_sampling_y self._m_sampling_y = (self.sampling_factors & 15) - return self._m_sampling_y if hasattr(self, '_m_sampling_y') else None + return getattr(self, '_m_sampling_y', None) @@ -181,10 +220,15 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self - self.extra_zero = self._io.ensure_fixed_contents(struct.pack('1b', 0)) + self._read() + + def _read(self): + self.extra_zero = self._io.read_bytes(1) + if not self.extra_zero == b"\x00": + raise kaitaistruct.ValidationNotEqualError(b"\x00", self.extra_zero, self._io, u"/types/exif_in_jpeg/seq/0") self._raw_data = self._io.read_bytes_full() - io = KaitaiStream(BytesIO(self._raw_data)) - self.data = Exif(io) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = exif.Exif(_io__raw_data) class SegmentApp0(KaitaiStruct): @@ -197,12 +241,18 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.magic = (self._io.read_bytes(5)).decode(u"ASCII") self.version_major = self._io.read_u1() self.version_minor = self._io.read_u1() - self.density_units = self._root.SegmentApp0.DensityUnit(self._io.read_u1()) + self.density_units = KaitaiStream.resolve_enum(Jpeg.SegmentApp0.DensityUnit, self._io.read_u1()) self.density_x = self._io.read_u2be() self.density_y = self._io.read_u2be() self.thumbnail_x = self._io.read_u1() self.thumbnail_y = self._io.read_u1() self.thumbnail = self._io.read_bytes(((self.thumbnail_x * self.thumbnail_y) * 3)) + + + diff --git a/mitmproxy/contrib/kaitaistruct/make.sh b/mitmproxy/contrib/kaitaistruct/make.sh index ab4b08446f..5dcbd089c1 100755 --- a/mitmproxy/contrib/kaitaistruct/make.sh +++ b/mitmproxy/contrib/kaitaistruct/make.sh @@ -1,13 +1,14 @@ #!/usr/bin/env bash -wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/image/exif_be.ksy -wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/image/exif_le.ksy +set -e + wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/image/exif.ksy wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/image/gif.ksy wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/image/jpeg.ksy wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/image/png.ksy wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/image/ico.ksy -wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/common/vlq_base128_le.ksy +wget -N -P common/ https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/common/vlq_base128_le.ksy wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/serialization/google_protobuf.ksy +wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/network/tls_client_hello.ksy -kaitai-struct-compiler --target python --opaque-types=true ./*.ksy +kaitai-struct-compiler --target python --opaque-types=true -I . --python-package . ./*.ksy diff --git a/mitmproxy/contrib/kaitaistruct/png.py b/mitmproxy/contrib/kaitaistruct/png.py index 799749da39..c88892e846 100644 --- a/mitmproxy/contrib/kaitaistruct/png.py +++ b/mitmproxy/contrib/kaitaistruct/png.py @@ -1,16 +1,36 @@ # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild -import array -import struct -import zlib -from enum import Enum - +import kaitaistruct from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO +from enum import Enum +import zlib -# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 +if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): + raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) class Png(KaitaiStruct): + """Test files for APNG can be found at the following locations: + + * + * + """ + + class PhysUnit(Enum): + unknown = 0 + meter = 1 + + class BlendOpValues(Enum): + source = 0 + over = 1 + + class CompressionMethods(Enum): + zlib = 0 + + class DisposeOpValues(Enum): + none = 0 + background = 1 + previous = 2 class ColorType(Enum): greyscale = 0 @@ -18,31 +38,41 @@ class ColorType(Enum): indexed = 3 greyscale_alpha = 4 truecolor_alpha = 6 - - class PhysUnit(Enum): - unknown = 0 - meter = 1 def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self - self.magic = self._io.ensure_fixed_contents(struct.pack('8b', -119, 80, 78, 71, 13, 10, 26, 10)) - self.ihdr_len = self._io.ensure_fixed_contents(struct.pack('4b', 0, 0, 0, 13)) - self.ihdr_type = self._io.ensure_fixed_contents(struct.pack('4b', 73, 72, 68, 82)) - self.ihdr = self._root.IhdrChunk(self._io, self, self._root) + self._read() + + def _read(self): + self.magic = self._io.read_bytes(8) + if not self.magic == b"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A": + raise kaitaistruct.ValidationNotEqualError(b"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A", self.magic, self._io, u"/seq/0") + self.ihdr_len = self._io.read_u4be() + if not self.ihdr_len == 13: + raise kaitaistruct.ValidationNotEqualError(13, self.ihdr_len, self._io, u"/seq/1") + self.ihdr_type = self._io.read_bytes(4) + if not self.ihdr_type == b"\x49\x48\x44\x52": + raise kaitaistruct.ValidationNotEqualError(b"\x49\x48\x44\x52", self.ihdr_type, self._io, u"/seq/2") + self.ihdr = Png.IhdrChunk(self._io, self, self._root) self.ihdr_crc = self._io.read_bytes(4) self.chunks = [] + i = 0 while True: - _ = self._root.Chunk(self._io, self, self._root) + _ = Png.Chunk(self._io, self, self._root) self.chunks.append(_) if ((_.type == u"IEND") or (self._io.is_eof())) : break + i += 1 class Rgb(KaitaiStruct): def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.r = self._io.read_u1() self.g = self._io.read_u1() self.b = self._io.read_u1() @@ -53,59 +83,78 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.len = self._io.read_u4be() self.type = (self._io.read_bytes(4)).decode(u"UTF-8") _on = self.type if _on == u"iTXt": self._raw_body = self._io.read_bytes(self.len) - io = KaitaiStream(BytesIO(self._raw_body)) - self.body = self._root.InternationalTextChunk(io, self, self._root) + _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) + self.body = Png.InternationalTextChunk(_io__raw_body, self, self._root) elif _on == u"gAMA": self._raw_body = self._io.read_bytes(self.len) - io = KaitaiStream(BytesIO(self._raw_body)) - self.body = self._root.GamaChunk(io, self, self._root) + _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) + self.body = Png.GamaChunk(_io__raw_body, self, self._root) elif _on == u"tIME": self._raw_body = self._io.read_bytes(self.len) - io = KaitaiStream(BytesIO(self._raw_body)) - self.body = self._root.TimeChunk(io, self, self._root) + _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) + self.body = Png.TimeChunk(_io__raw_body, self, self._root) elif _on == u"PLTE": self._raw_body = self._io.read_bytes(self.len) - io = KaitaiStream(BytesIO(self._raw_body)) - self.body = self._root.PlteChunk(io, self, self._root) + _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) + self.body = Png.PlteChunk(_io__raw_body, self, self._root) elif _on == u"bKGD": self._raw_body = self._io.read_bytes(self.len) - io = KaitaiStream(BytesIO(self._raw_body)) - self.body = self._root.BkgdChunk(io, self, self._root) + _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) + self.body = Png.BkgdChunk(_io__raw_body, self, self._root) elif _on == u"pHYs": self._raw_body = self._io.read_bytes(self.len) - io = KaitaiStream(BytesIO(self._raw_body)) - self.body = self._root.PhysChunk(io, self, self._root) + _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) + self.body = Png.PhysChunk(_io__raw_body, self, self._root) + elif _on == u"fdAT": + self._raw_body = self._io.read_bytes(self.len) + _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) + self.body = Png.FrameDataChunk(_io__raw_body, self, self._root) elif _on == u"tEXt": self._raw_body = self._io.read_bytes(self.len) - io = KaitaiStream(BytesIO(self._raw_body)) - self.body = self._root.TextChunk(io, self, self._root) + _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) + self.body = Png.TextChunk(_io__raw_body, self, self._root) elif _on == u"cHRM": self._raw_body = self._io.read_bytes(self.len) - io = KaitaiStream(BytesIO(self._raw_body)) - self.body = self._root.ChrmChunk(io, self, self._root) + _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) + self.body = Png.ChrmChunk(_io__raw_body, self, self._root) + elif _on == u"acTL": + self._raw_body = self._io.read_bytes(self.len) + _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) + self.body = Png.AnimationControlChunk(_io__raw_body, self, self._root) elif _on == u"sRGB": self._raw_body = self._io.read_bytes(self.len) - io = KaitaiStream(BytesIO(self._raw_body)) - self.body = self._root.SrgbChunk(io, self, self._root) + _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) + self.body = Png.SrgbChunk(_io__raw_body, self, self._root) elif _on == u"zTXt": self._raw_body = self._io.read_bytes(self.len) - io = KaitaiStream(BytesIO(self._raw_body)) - self.body = self._root.CompressedTextChunk(io, self, self._root) + _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) + self.body = Png.CompressedTextChunk(_io__raw_body, self, self._root) + elif _on == u"fcTL": + self._raw_body = self._io.read_bytes(self.len) + _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) + self.body = Png.FrameControlChunk(_io__raw_body, self, self._root) else: self.body = self._io.read_bytes(self.len) self.crc = self._io.read_bytes(4) class BkgdIndexed(KaitaiStruct): + """Background chunk for images with indexed palette.""" def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.palette_index = self._io.read_u1() @@ -114,71 +163,105 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.x_int = self._io.read_u4be() self.y_int = self._io.read_u4be() @property def x(self): if hasattr(self, '_m_x'): - return self._m_x if hasattr(self, '_m_x') else None + return self._m_x self._m_x = (self.x_int / 100000.0) - return self._m_x if hasattr(self, '_m_x') else None + return getattr(self, '_m_x', None) @property def y(self): if hasattr(self, '_m_y'): - return self._m_y if hasattr(self, '_m_y') else None + return self._m_y self._m_y = (self.y_int / 100000.0) - return self._m_y if hasattr(self, '_m_y') else None + return getattr(self, '_m_y', None) class BkgdGreyscale(KaitaiStruct): + """Background chunk for greyscale images.""" def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.value = self._io.read_u2be() class ChrmChunk(KaitaiStruct): + """ + .. seealso:: + Source - https://www.w3.org/TR/PNG/#11cHRM + """ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self - self.white_point = self._root.Point(self._io, self, self._root) - self.red = self._root.Point(self._io, self, self._root) - self.green = self._root.Point(self._io, self, self._root) - self.blue = self._root.Point(self._io, self, self._root) + self._read() + + def _read(self): + self.white_point = Png.Point(self._io, self, self._root) + self.red = Png.Point(self._io, self, self._root) + self.green = Png.Point(self._io, self, self._root) + self.blue = Png.Point(self._io, self, self._root) class IhdrChunk(KaitaiStruct): + """ + .. seealso:: + Source - https://www.w3.org/TR/PNG/#11IHDR + """ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.width = self._io.read_u4be() self.height = self._io.read_u4be() self.bit_depth = self._io.read_u1() - self.color_type = self._root.ColorType(self._io.read_u1()) + self.color_type = KaitaiStream.resolve_enum(Png.ColorType, self._io.read_u1()) self.compression_method = self._io.read_u1() self.filter_method = self._io.read_u1() self.interlace_method = self._io.read_u1() class PlteChunk(KaitaiStruct): + """ + .. seealso:: + Source - https://www.w3.org/TR/PNG/#11PLTE + """ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.entries = [] + i = 0 while not self._io.is_eof(): - self.entries.append(self._root.Rgb(self._io, self, self._root)) + self.entries.append(Png.Rgb(self._io, self, self._root)) + i += 1 class SrgbChunk(KaitaiStruct): + """ + .. seealso:: + Source - https://www.w3.org/TR/PNG/#11sRGB + """ class Intent(Enum): perceptual = 0 @@ -189,101 +272,250 @@ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self - self.render_intent = self._root.SrgbChunk.Intent(self._io.read_u1()) + self._read() + + def _read(self): + self.render_intent = KaitaiStream.resolve_enum(Png.SrgbChunk.Intent, self._io.read_u1()) class CompressedTextChunk(KaitaiStruct): + """Compressed text chunk effectively allows to store key-value + string pairs in PNG container, compressing "value" part (which + can be quite lengthy) with zlib compression. + + .. seealso:: + Source - https://www.w3.org/TR/PNG/#11zTXt + """ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.keyword = (self._io.read_bytes_term(0, False, True, True)).decode(u"UTF-8") - self.compression_method = self._io.read_u1() + self.compression_method = KaitaiStream.resolve_enum(Png.CompressionMethods, self._io.read_u1()) self._raw_text_datastream = self._io.read_bytes_full() self.text_datastream = zlib.decompress(self._raw_text_datastream) + class FrameDataChunk(KaitaiStruct): + """ + .. seealso:: + Source - https://wiki.mozilla.org/APNG_Specification#.60fdAT.60:_The_Frame_Data_Chunk + """ + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.sequence_number = self._io.read_u4be() + self.frame_data = self._io.read_bytes_full() + + class BkgdTruecolor(KaitaiStruct): + """Background chunk for truecolor images.""" def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.red = self._io.read_u2be() self.green = self._io.read_u2be() self.blue = self._io.read_u2be() class GamaChunk(KaitaiStruct): + """ + .. seealso:: + Source - https://www.w3.org/TR/PNG/#11gAMA + """ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.gamma_int = self._io.read_u4be() @property def gamma_ratio(self): if hasattr(self, '_m_gamma_ratio'): - return self._m_gamma_ratio if hasattr(self, '_m_gamma_ratio') else None + return self._m_gamma_ratio self._m_gamma_ratio = (100000.0 / self.gamma_int) - return self._m_gamma_ratio if hasattr(self, '_m_gamma_ratio') else None + return getattr(self, '_m_gamma_ratio', None) class BkgdChunk(KaitaiStruct): + """Background chunk stores default background color to display this + image against. Contents depend on `color_type` of the image. + + .. seealso:: + Source - https://www.w3.org/TR/PNG/#11bKGD + """ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): _on = self._root.ihdr.color_type - if _on == self._root.ColorType.greyscale_alpha: - self.bkgd = self._root.BkgdGreyscale(self._io, self, self._root) - elif _on == self._root.ColorType.indexed: - self.bkgd = self._root.BkgdIndexed(self._io, self, self._root) - elif _on == self._root.ColorType.greyscale: - self.bkgd = self._root.BkgdGreyscale(self._io, self, self._root) - elif _on == self._root.ColorType.truecolor_alpha: - self.bkgd = self._root.BkgdTruecolor(self._io, self, self._root) - elif _on == self._root.ColorType.truecolor: - self.bkgd = self._root.BkgdTruecolor(self._io, self, self._root) + if _on == Png.ColorType.indexed: + self.bkgd = Png.BkgdIndexed(self._io, self, self._root) + elif _on == Png.ColorType.truecolor_alpha: + self.bkgd = Png.BkgdTruecolor(self._io, self, self._root) + elif _on == Png.ColorType.greyscale_alpha: + self.bkgd = Png.BkgdGreyscale(self._io, self, self._root) + elif _on == Png.ColorType.truecolor: + self.bkgd = Png.BkgdTruecolor(self._io, self, self._root) + elif _on == Png.ColorType.greyscale: + self.bkgd = Png.BkgdGreyscale(self._io, self, self._root) class PhysChunk(KaitaiStruct): + """"Physical size" chunk stores data that allows to translate + logical pixels into physical units (meters, etc) and vice-versa. + + .. seealso:: + Source - https://www.w3.org/TR/PNG/#11pHYs + """ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.pixels_per_unit_x = self._io.read_u4be() self.pixels_per_unit_y = self._io.read_u4be() - self.unit = self._root.PhysUnit(self._io.read_u1()) + self.unit = KaitaiStream.resolve_enum(Png.PhysUnit, self._io.read_u1()) + + + class FrameControlChunk(KaitaiStruct): + """ + .. seealso:: + Source - https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk + """ + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.sequence_number = self._io.read_u4be() + self.width = self._io.read_u4be() + if not self.width >= 1: + raise kaitaistruct.ValidationLessThanError(1, self.width, self._io, u"/types/frame_control_chunk/seq/1") + if not self.width <= self._root.ihdr.width: + raise kaitaistruct.ValidationGreaterThanError(self._root.ihdr.width, self.width, self._io, u"/types/frame_control_chunk/seq/1") + self.height = self._io.read_u4be() + if not self.height >= 1: + raise kaitaistruct.ValidationLessThanError(1, self.height, self._io, u"/types/frame_control_chunk/seq/2") + if not self.height <= self._root.ihdr.height: + raise kaitaistruct.ValidationGreaterThanError(self._root.ihdr.height, self.height, self._io, u"/types/frame_control_chunk/seq/2") + self.x_offset = self._io.read_u4be() + if not self.x_offset <= (self._root.ihdr.width - self.width): + raise kaitaistruct.ValidationGreaterThanError((self._root.ihdr.width - self.width), self.x_offset, self._io, u"/types/frame_control_chunk/seq/3") + self.y_offset = self._io.read_u4be() + if not self.y_offset <= (self._root.ihdr.height - self.height): + raise kaitaistruct.ValidationGreaterThanError((self._root.ihdr.height - self.height), self.y_offset, self._io, u"/types/frame_control_chunk/seq/4") + self.delay_num = self._io.read_u2be() + self.delay_den = self._io.read_u2be() + self.dispose_op = KaitaiStream.resolve_enum(Png.DisposeOpValues, self._io.read_u1()) + self.blend_op = KaitaiStream.resolve_enum(Png.BlendOpValues, self._io.read_u1()) + + @property + def delay(self): + """Time to display this frame, in seconds.""" + if hasattr(self, '_m_delay'): + return self._m_delay + + self._m_delay = (self.delay_num / (100.0 if self.delay_den == 0 else self.delay_den)) + return getattr(self, '_m_delay', None) class InternationalTextChunk(KaitaiStruct): + """International text chunk effectively allows to store key-value string pairs in + PNG container. Both "key" (keyword) and "value" (text) parts are + given in pre-defined subset of iso8859-1 without control + characters. + + .. seealso:: + Source - https://www.w3.org/TR/PNG/#11iTXt + """ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.keyword = (self._io.read_bytes_term(0, False, True, True)).decode(u"UTF-8") self.compression_flag = self._io.read_u1() - self.compression_method = self._io.read_u1() + self.compression_method = KaitaiStream.resolve_enum(Png.CompressionMethods, self._io.read_u1()) self.language_tag = (self._io.read_bytes_term(0, False, True, True)).decode(u"ASCII") self.translated_keyword = (self._io.read_bytes_term(0, False, True, True)).decode(u"UTF-8") self.text = (self._io.read_bytes_full()).decode(u"UTF-8") class TextChunk(KaitaiStruct): + """Text chunk effectively allows to store key-value string pairs in + PNG container. Both "key" (keyword) and "value" (text) parts are + given in pre-defined subset of iso8859-1 without control + characters. + + .. seealso:: + Source - https://www.w3.org/TR/PNG/#11tEXt + """ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.keyword = (self._io.read_bytes_term(0, False, True, True)).decode(u"iso8859-1") self.text = (self._io.read_bytes_full()).decode(u"iso8859-1") + class AnimationControlChunk(KaitaiStruct): + """ + .. seealso:: + Source - https://wiki.mozilla.org/APNG_Specification#.60acTL.60:_The_Animation_Control_Chunk + """ + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.num_frames = self._io.read_u4be() + self.num_plays = self._io.read_u4be() + + class TimeChunk(KaitaiStruct): + """Time chunk stores time stamp of last modification of this image, + up to 1 second precision in UTC timezone. + + .. seealso:: + Source - https://www.w3.org/TR/PNG/#11tIME + """ def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.year = self._io.read_u2be() self.month = self._io.read_u1() self.day = self._io.read_u1() diff --git a/mitmproxy/contrib/kaitaistruct/tls_client_hello.ksy b/mitmproxy/contrib/kaitaistruct/tls_client_hello.ksy index 921c11b5bb..56461d8a30 100644 --- a/mitmproxy/contrib/kaitaistruct/tls_client_hello.ksy +++ b/mitmproxy/contrib/kaitaistruct/tls_client_hello.ksy @@ -1,39 +1,37 @@ meta: id: tls_client_hello + xref: + rfc: 5246 # TLS 1.2 + wikidata: Q206494 # TLS + license: MIT endian: be seq: - id: version type: version - + - id: random type: random - + - id: session_id type: session_id - + - id: cipher_suites type: cipher_suites - + - id: compression_methods type: compression_methods - - - id: extensions - size: 0 - repeat: expr - repeat-expr: 0 - if: _io.eof == true - id: extensions type: extensions if: _io.eof == false - + types: version: seq: - id: major type: u1 - + - id: minor type: u1 @@ -41,7 +39,7 @@ types: seq: - id: gmt_unix_time type: u4 - + - id: random size: 28 @@ -49,15 +47,15 @@ types: seq: - id: len type: u1 - + - id: sid size: len - + cipher_suites: seq: - id: len type: u2 - + - id: cipher_suites type: u2 repeat: expr @@ -67,7 +65,7 @@ types: seq: - id: len type: u1 - + - id: compression_methods size: len @@ -79,23 +77,23 @@ types: - id: extensions type: extension repeat: eos - + extension: seq: - id: type type: u2 - + - id: len type: u2 - + - id: body size: len - type: + type: switch-on: type cases: 0: sni 16: alpn - + sni: seq: - id: list_length @@ -104,12 +102,12 @@ types: - id: server_names type: server_name repeat: eos - + server_name: seq: - id: name_type type: u1 - + - id: length type: u2 @@ -129,6 +127,6 @@ types: seq: - id: strlen type: u1 - + - id: name size: strlen diff --git a/mitmproxy/contrib/kaitaistruct/tls_client_hello.py b/mitmproxy/contrib/kaitaistruct/tls_client_hello.py index 097b6641e9..fdcd7fe5a7 100644 --- a/mitmproxy/contrib/kaitaistruct/tls_client_hello.py +++ b/mitmproxy/contrib/kaitaistruct/tls_client_hello.py @@ -1,137 +1,189 @@ # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild -import array -import struct -import zlib -from enum import Enum - +import kaitaistruct from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO -# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 +if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): + raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) class TlsClientHello(KaitaiStruct): def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self - self.version = self._root.Version(self._io, self, self._root) - self.random = self._root.Random(self._io, self, self._root) - self.session_id = self._root.SessionId(self._io, self, self._root) - self.cipher_suites = self._root.CipherSuites(self._io, self, self._root) - self.compression_methods = self._root.CompressionMethods(self._io, self, self._root) - if self._io.is_eof() == True: - self.extensions = [None] * (0) - for i in range(0): - self.extensions[i] = self._io.read_bytes(0) - + self._read() + + def _read(self): + self.version = TlsClientHello.Version(self._io, self, self._root) + self.random = TlsClientHello.Random(self._io, self, self._root) + self.session_id = TlsClientHello.SessionId(self._io, self, self._root) + self.cipher_suites = TlsClientHello.CipherSuites(self._io, self, self._root) + self.compression_methods = TlsClientHello.CompressionMethods(self._io, self, self._root) if self._io.is_eof() == False: - self.extensions = self._root.Extensions(self._io, self, self._root) + self.extensions = TlsClientHello.Extensions(self._io, self, self._root) + class ServerName(KaitaiStruct): def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.name_type = self._io.read_u1() self.length = self._io.read_u2be() self.host_name = self._io.read_bytes(self.length) + class Random(KaitaiStruct): def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.gmt_unix_time = self._io.read_u4be() self.random = self._io.read_bytes(28) + class SessionId(KaitaiStruct): def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.len = self._io.read_u1() self.sid = self._io.read_bytes(self.len) + class Sni(KaitaiStruct): def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.list_length = self._io.read_u2be() self.server_names = [] + i = 0 while not self._io.is_eof(): - self.server_names.append(self._root.ServerName(self._io, self, self._root)) + self.server_names.append(TlsClientHello.ServerName(self._io, self, self._root)) + i += 1 + + class CipherSuites(KaitaiStruct): def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.len = self._io.read_u2be() - self.cipher_suites = [None] * (self.len // 2) + self.cipher_suites = [] for i in range(self.len // 2): - self.cipher_suites[i] = self._io.read_u2be() + self.cipher_suites.append(self._io.read_u2be()) + + class CompressionMethods(KaitaiStruct): def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.len = self._io.read_u1() self.compression_methods = self._io.read_bytes(self.len) + class Alpn(KaitaiStruct): def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.ext_len = self._io.read_u2be() self.alpn_protocols = [] + i = 0 while not self._io.is_eof(): - self.alpn_protocols.append(self._root.Protocol(self._io, self, self._root)) + self.alpn_protocols.append(TlsClientHello.Protocol(self._io, self, self._root)) + i += 1 + + class Extensions(KaitaiStruct): def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.len = self._io.read_u2be() self.extensions = [] + i = 0 while not self._io.is_eof(): - self.extensions.append(self._root.Extension(self._io, self, self._root)) + self.extensions.append(TlsClientHello.Extension(self._io, self, self._root)) + i += 1 + + class Version(KaitaiStruct): def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.major = self._io.read_u1() self.minor = self._io.read_u1() + class Protocol(KaitaiStruct): def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.strlen = self._io.read_u1() self.name = self._io.read_bytes(self.strlen) + class Extension(KaitaiStruct): def __init__(self, _io, _parent=None, _root=None): self._io = _io self._parent = _parent self._root = _root if _root else self + self._read() + + def _read(self): self.type = self._io.read_u2be() self.len = self._io.read_u2be() _on = self.type if _on == 0: self._raw_body = self._io.read_bytes(self.len) - io = KaitaiStream(BytesIO(self._raw_body)) - self.body = self._root.Sni(io, self, self._root) + _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) + self.body = TlsClientHello.Sni(_io__raw_body, self, self._root) elif _on == 16: self._raw_body = self._io.read_bytes(self.len) - io = KaitaiStream(BytesIO(self._raw_body)) - self.body = self._root.Alpn(io, self, self._root) + _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) + self.body = TlsClientHello.Alpn(_io__raw_body, self, self._root) else: self.body = self._io.read_bytes(self.len) + + + diff --git a/mitmproxy/contrib/kaitaistruct/vlq_base128_le.py b/mitmproxy/contrib/kaitaistruct/vlq_base128_le.py index 5fb63bfed9..78f28c28d3 100644 --- a/mitmproxy/contrib/kaitaistruct/vlq_base128_le.py +++ b/mitmproxy/contrib/kaitaistruct/vlq_base128_le.py @@ -1,13 +1,15 @@ # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild -from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO +import kaitaistruct +from kaitaistruct import KaitaiStruct -# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 +if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): + raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) class VlqBase128Le(KaitaiStruct): - """A variable-length unsigned integer using base128 encoding. 1-byte groups - consists of 1-bit flag of continuation and 7-bit value, and are ordered + """A variable-length unsigned/signed integer using base128 encoding. 1-byte groups + consist of 1-bit flag of continuation and 7-bit value chunk, and are ordered "least significant group first", i.e. in "little-endian" manner. This particular encoding is specified and used in: @@ -17,10 +19,10 @@ class VlqBase128Le(KaitaiStruct): * Google Protocol Buffers, where it's called "Base 128 Varints". https://developers.google.com/protocol-buffers/docs/encoding?csw=1#varints * Apache Lucene, where it's called "VInt" - http://lucene.apache.org/core/3_5_0/fileformats.html#VInt + https://lucene.apache.org/core/3_5_0/fileformats.html#VInt * Apache Avro uses this as a basis for integer encoding, adding ZigZag on top of it for signed ints - http://avro.apache.org/docs/current/spec.html#binary_encode_primitive + https://avro.apache.org/docs/current/spec.html#binary_encode_primitive More information on this encoding is available at https://en.wikipedia.org/wiki/LEB128 @@ -34,15 +36,16 @@ def __init__(self, _io, _parent=None, _root=None): def _read(self): self.groups = [] + i = 0 while True: - _ = self._root.Group(self._io, self, self._root) + _ = VlqBase128Le.Group(self._io, self, self._root) self.groups.append(_) if not (_.has_next): break + i += 1 class Group(KaitaiStruct): - """One byte group, clearly divided into 7-bit "value" and 1-bit "has continuation - in the next byte" flag. + """One byte group, clearly divided into 7-bit "value" chunk and 1-bit "continuation" flag. """ def __init__(self, _io, _parent=None, _root=None): self._io = _io @@ -57,36 +60,56 @@ def _read(self): def has_next(self): """If true, then we have more bytes to read.""" if hasattr(self, '_m_has_next'): - return self._m_has_next if hasattr(self, '_m_has_next') else None + return self._m_has_next self._m_has_next = (self.b & 128) != 0 - return self._m_has_next if hasattr(self, '_m_has_next') else None + return getattr(self, '_m_has_next', None) @property def value(self): - """The 7-bit (base128) numeric value of this group.""" + """The 7-bit (base128) numeric value chunk of this group.""" if hasattr(self, '_m_value'): - return self._m_value if hasattr(self, '_m_value') else None + return self._m_value self._m_value = (self.b & 127) - return self._m_value if hasattr(self, '_m_value') else None + return getattr(self, '_m_value', None) @property def len(self): if hasattr(self, '_m_len'): - return self._m_len if hasattr(self, '_m_len') else None + return self._m_len self._m_len = len(self.groups) - return self._m_len if hasattr(self, '_m_len') else None + return getattr(self, '_m_len', None) @property def value(self): - """Resulting value as normal integer.""" + """Resulting unsigned value as normal integer.""" if hasattr(self, '_m_value'): - return self._m_value if hasattr(self, '_m_value') else None + return self._m_value self._m_value = (((((((self.groups[0].value + ((self.groups[1].value << 7) if self.len >= 2 else 0)) + ((self.groups[2].value << 14) if self.len >= 3 else 0)) + ((self.groups[3].value << 21) if self.len >= 4 else 0)) + ((self.groups[4].value << 28) if self.len >= 5 else 0)) + ((self.groups[5].value << 35) if self.len >= 6 else 0)) + ((self.groups[6].value << 42) if self.len >= 7 else 0)) + ((self.groups[7].value << 49) if self.len >= 8 else 0)) - return self._m_value if hasattr(self, '_m_value') else None + return getattr(self, '_m_value', None) + + @property + def sign_bit(self): + if hasattr(self, '_m_sign_bit'): + return self._m_sign_bit + + self._m_sign_bit = (1 << ((7 * self.len) - 1)) + return getattr(self, '_m_sign_bit', None) + + @property + def value_signed(self): + """ + .. seealso:: + Source - https://graphics.stanford.edu/~seander/bithacks.html#VariableSignExtend + """ + if hasattr(self, '_m_value_signed'): + return self._m_value_signed + + self._m_value_signed = ((self.value ^ self.sign_bit) - self.sign_bit) + return getattr(self, '_m_value_signed', None) 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/basethread.py b/mitmproxy/coretypes/basethread.py deleted file mode 100644 index 9a3c64bdc4..0000000000 --- a/mitmproxy/coretypes/basethread.py +++ /dev/null @@ -1,11 +0,0 @@ -import time -import threading - - -class BaseThread(threading.Thread): - def __init__(self, name, *args, **kwargs): - super().__init__(name=name, *args, **kwargs) - self._thread_started = time.time() - - def _threadinfo(self): - return "%s - age: %is" % (self.name, int(time.time() - self._thread_started)) 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..ca1c31fa0f 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,145 @@ 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 = {} + 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 = _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}" + ) + + +V = TypeVar("V") + + +def _process(attr_val: typing.Any, attr_type: type[V], attr_name: str, make: bool) -> V: + 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: type[U], attr_name: str) -> U: + """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: type[U], attr_name: str) -> U: + """Get the state of the object given as val.""" + return _process(value, attr_type, attr_name, False) diff --git a/mitmproxy/dns.py b/mitmproxy/dns.py index 4bfe25f7b2..5372f8dff0 100644 --- a/mitmproxy/dns.py +++ b/mitmproxy/dns.py @@ -1,32 +1,33 @@ from __future__ import annotations -from dataclasses import dataclass + import itertools import random import struct -from ipaddress import IPv4Address, IPv6Address import time +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 op_codes +from mitmproxy.net.dns import response_codes +from mitmproxy.net.dns import types # 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 +44,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 +54,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 +64,7 @@ def __str__(self) -> str: return self.domain_name if self.type == types.TXT: return self.text - except: + except Exception: return f"0x{self.data.hex()} (invalid {types.to_str(self.type)} data)" return f"0x{self.data.hex()}" @@ -150,7 +145,7 @@ def TXT(cls, name: str, text: str, *, ttl: int = DEFAULT_TTL) -> ResourceRecord: # 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 +189,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( @@ -465,9 +437,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 bf15b6e291..40b4767b9e 100644 --- a/mitmproxy/eventsequence.py +++ b/mitmproxy/eventsequence.py @@ -1,10 +1,13 @@ -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 from mitmproxy import hooks from mitmproxy import http from mitmproxy import tcp +from mitmproxy import udp from mitmproxy.proxy import layers TEventGenerator = Iterator[hooks.Hook] @@ -42,6 +45,19 @@ def _iterate_tcp(f: tcp.TCPFlow) -> TEventGenerator: yield layers.tcp.TcpEndHook(f) +def _iterate_udp(f: udp.UDPFlow) -> TEventGenerator: + messages = f.messages + f.messages = [] + yield layers.udp.UdpStartHook(f) + while messages: + f.messages.append(messages.pop(0)) + yield layers.udp.UdpMessageHook(f) + if f.error: + yield layers.udp.UdpErrorHook(f) + else: + yield layers.udp.UdpEndHook(f) + + def _iterate_dns(f: dns.DNSFlow) -> TEventGenerator: if f.request: yield layers.dns.DnsRequestHook(f) @@ -54,6 +70,7 @@ def _iterate_dns(f: dns.DNSFlow) -> TEventGenerator: _iterate_map: dict[type[flow.Flow], Callable[[Any], TEventGenerator]] = { http.HTTPFlow: _iterate_http, tcp.TCPFlow: _iterate_tcp, + udp.UDPFlow: _iterate_udp, dns.DNSFlow: _iterate_dns, } diff --git a/mitmproxy/exceptions.py b/mitmproxy/exceptions.py index 30005970af..bfb9e4c1e3 100644 --- a/mitmproxy/exceptions.py +++ b/mitmproxy/exceptions.py @@ -49,7 +49,3 @@ class AddonHalt(MitmproxyException): """ Raised by addons to signal that no further handlers should handle this event. """ - - -class TypeError(MitmproxyException): - pass diff --git a/mitmproxy/flow.py b/mitmproxy/flow.py index e8284aebf3..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. @@ -57,6 +49,7 @@ class Flow(stateobject.StateObject): See also: - mitmproxy.http.HTTPFlow - mitmproxy.tcp.TCPFlow + - mitmproxy.udp.UDPFlow """ client_conn: connection.Client @@ -72,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 @@ -95,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. @@ -129,59 +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"]] = {} + __types: dict[str, type[Flow]] = {} - @classmethod - @property - def type(cls) -> str: - """The flow type, for example `http`, `tcp`, or `dns`.""" - return cls.__name__.removesuffix("Flow").lower() + 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 7e3dfac92a..52db22be03 100644 --- a/mitmproxy/flowfilter.py +++ b/mitmproxy/flowfilter.py @@ -32,15 +32,20 @@ ~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 +from mitmproxy import dns +from mitmproxy import flow +from mitmproxy import http +from mitmproxy import tcp +from mitmproxy import udp def only(*types): @@ -120,6 +125,15 @@ def __call__(self, f): return True +class FUDP(_Action): + code = "udp" + help = "Match UDP flows" + + @only(udp.UDPFlow) + def __call__(self, f): + return True + + class FDNS(_Action): code = "dns" help = "Match DNS flows" @@ -276,22 +290,28 @@ class FBod(_Rex): help = "Body" flags = re.DOTALL - @only(http.HTTPFlow, tcp.TCPFlow, dns.DNSFlow) + @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): + 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): @@ -306,17 +326,20 @@ class FBodRequest(_Rex): help = "Request body" flags = re.DOTALL - @only(http.HTTPFlow, tcp.TCPFlow, dns.DNSFlow) + @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): + elif isinstance(f, (tcp.TCPFlow, udp.UDPFlow)): for msg in f.messages: if msg.from_client and self.re.search(msg.content): return True @@ -330,17 +353,20 @@ class FBodResponse(_Rex): help = "Response body" flags = re.DOTALL - @only(http.HTTPFlow, tcp.TCPFlow, dns.DNSFlow) + @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): + elif isinstance(f, (tcp.TCPFlow, udp.UDPFlow)): for msg in f.messages: if not msg.from_client and self.re.search(msg.content): return True @@ -537,6 +563,7 @@ def __call__(self, f): FReq, FResp, FTCP, + FUDP, FDNS, FWebSocket, FAll, @@ -634,7 +661,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..986e6b8983 100644 --- a/mitmproxy/http.py +++ b/mitmproxy/http.py @@ -1,26 +1,24 @@ 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 @@ -35,6 +33,7 @@ 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,7 +402,7 @@ def get_content(self, strict: bool = True) -> Optional[bytes]: else: return self.raw_content - def _get_content_type_charset(self) -> Optional[str]: + def _get_content_type_charset(self) -> str | None: ct = parse_content_type(self.headers.get("content-type", "")) if ct: return ct[2].get("charset") @@ -433,7 +436,7 @@ def _guess_encoding(self, content: bytes = b"") -> str: 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 @@ -453,7 +456,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. @@ -481,14 +484,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 +556,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 +611,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 +689,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 +700,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 +722,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 +744,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 +758,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 +785,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 +828,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 +963,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 +989,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 +1013,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 +1033,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 +1049,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 +1092,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 +1164,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 +1182,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 +1242,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 +1253,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()} @@ -441,12 +484,13 @@ def convert_unicode(data: dict) -> dict: 14: convert_14_15, 15: convert_15_16, 16: convert_16_17, + 17: convert_17_18, + 18: convert_18_19, + 19: convert_19_20, } -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..cf2dc0ce3a --- /dev/null +++ b/mitmproxy/io/har.py @@ -0,0 +1,125 @@ +"""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 + +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"] + 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) + if content_encoding == "base64": + response_content = base64.b64decode(response_content) + response_headers = fix_headers(request_json["response"]["headers"]) + + new_flow.response = http.Response.make( + response_code, response_content, response_headers + ) + + # Change time to match HAR file + new_flow.request.timestamp_start = timestamp_start + new_flow.request.timestamp_end = timestamp_end + + new_flow.response.timestamp_start = timestamp_start + new_flow.response.timestamp_end = timestamp_end + + new_flow.client_conn.timestamp_start = timestamp_start + new_flow.client_conn.timestamp_end = timestamp_end + + 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..e3dd640e46 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,54 @@ 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(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..db071eac8c 100644 --- a/mitmproxy/io/tnetstring.py +++ b/mitmproxy/io/tnetstring.py @@ -39,9 +39,9 @@ :License: MIT """ - 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 +138,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() @@ -210,11 +210,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: diff --git a/mitmproxy/log.py b/mitmproxy/log.py index 2456be3cf8..221b12a932 100644 --- a/mitmproxy/log.py +++ b/mitmproxy/log.py @@ -1,6 +1,100 @@ +from __future__ import annotations + +import logging +import os +import warnings from dataclasses import dataclass from mitmproxy import hooks +from mitmproxy import master +from mitmproxy.contrib import click as miniclick +from mitmproxy.utils import human + +ALERT = logging.INFO + 1 +""" +The ALERT logging level has the same urgency as info, but +signals to interactive tools that the user's attention should be +drawn to the output even if they're not currently looking at the +event log. +""" +logging.addLevelName(ALERT, "ALERT") + +LogLevels = [ + "error", + "warn", + "info", + "alert", + "debug", +] + +LOG_COLORS = {logging.ERROR: "red", logging.WARNING: "yellow", ALERT: "magenta"} + + +class MitmFormatter(logging.Formatter): + def __init__(self, colorize: bool): + super().__init__() + self.colorize = colorize + time = "[%s]" + client = "[%s]" + if colorize: + time = miniclick.style(time, fg="cyan", dim=True) + client = miniclick.style(client, fg="yellow", dim=True) + + self.with_client = f"{time}{client} %s" + self.without_client = f"{time} %s" + + default_time_format = "%H:%M:%S" + 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, + fg=LOG_COLORS.get(record.levelno), + # dim=(record.levelno <= logging.DEBUG) + ) + if client := getattr(record, "client", None): + client = human.format_address(client) + return self.with_client % (time, client, message) + else: + return self.without_client % (time, message) + + +class MitmLogHandler(logging.Handler): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._initiated_in_test = os.environ.get("PYTEST_CURRENT_TEST") + + def filter(self, record: logging.LogRecord) -> bool: + # We can't remove stale handlers here because that would modify .handlers during iteration! + return bool( + super().filter(record) + and ( + not self._initiated_in_test + or self._initiated_in_test == os.environ.get("PYTEST_CURRENT_TEST") + ) + ) + + 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 + ): + h.uninstall() + + logging.getLogger().addHandler(self) + + def uninstall(self) -> None: + logging.getLogger().removeHandler(self) + + +# everything below is deprecated! class LogEntry: @@ -22,6 +116,8 @@ def __repr__(self): class Log: """ The central logger, exposed to scripts as mitmproxy.ctx.log. + + Deprecated: Please use the standard Python logging module instead. """ def __init__(self, master): @@ -31,13 +127,23 @@ def debug(self, txt): """ Log with level debug. """ - self(txt, "debug") + warnings.warn( + "mitmproxy's ctx.log.debug() is deprecated. Please use the standard Python logging module instead.", + DeprecationWarning, + stacklevel=2, + ) + logging.getLogger().debug(txt) def info(self, txt): """ Log with level info. """ - self(txt, "info") + warnings.warn( + "mitmproxy's ctx.log.info() is deprecated. Please use the standard Python logging module instead.", + DeprecationWarning, + stacklevel=2, + ) + logging.getLogger().info(txt) def alert(self, txt): """ @@ -46,30 +152,81 @@ def alert(self, txt): drawn to the output even if they're not currently looking at the event log. """ - self(txt, "alert") + warnings.warn( + "mitmproxy's ctx.log.alert() is deprecated. Please use the standard Python logging module instead.", + DeprecationWarning, + stacklevel=2, + ) + logging.getLogger().log(ALERT, txt) def warn(self, txt): """ Log with level warn. """ - self(txt, "warn") + warnings.warn( + "mitmproxy's ctx.log.warn() is deprecated. Please use the standard Python logging module instead.", + DeprecationWarning, + stacklevel=2, + ) + logging.getLogger().warning(txt) def error(self, txt): """ Log with level error. """ - self(txt, "error") + warnings.warn( + "mitmproxy's ctx.log.error() is deprecated. Please use the standard Python logging module instead.", + DeprecationWarning, + stacklevel=2, + ) + logging.getLogger().error(txt) def __call__(self, text, level="info"): + warnings.warn( + "mitmproxy's ctx.log() is deprecated. Please use the standard Python logging module instead.", + DeprecationWarning, + stacklevel=2, + ) + logging.getLogger().log(level=logging.getLevelName(level.upper()), msg=text) + + +LOGGING_LEVELS_TO_LOGENTRY = { + logging.ERROR: "error", + logging.WARNING: "warn", + logging.INFO: "info", + ALERT: "alert", + logging.DEBUG: "debug", +} + + +class LegacyLogEvents(MitmLogHandler): + """Emit deprecated `add_log` events from stdlib logging.""" + + def __init__( + self, + master: master.Master, + ): + super().__init__() + self.master = master + self.formatter = MitmFormatter(colorize=False) + + def emit(self, record: logging.LogRecord) -> None: + entry = LogEntry( + msg=self.format(record), + level=LOGGING_LEVELS_TO_LOGENTRY.get(record.levelno, "error"), + ) self.master.event_loop.call_soon_threadsafe( self.master.addons.trigger, - AddLogHook(LogEntry(text, level)), + AddLogHook(entry), ) @dataclass class AddLogHook(hooks.Hook): """ + **Deprecated:** Starting with mitmproxy 9, users should use the standard Python logging module instead, for example + by calling `logging.getLogger().addHandler()`. + Called whenever a new log entry is created through the mitmproxy context. Be careful not to log from this event, which will cause an infinite loop! @@ -78,14 +235,9 @@ class AddLogHook(hooks.Hook): entry: LogEntry -LogTierOrder = [ - "error", - "warn", - "info", - "alert", - "debug", -] - - def log_tier(level): + """ + Comparison method for "old" LogEntry log tiers. + Ideally you should use the standard Python logging module instead. + """ return dict(error=0, warn=1, info=2, alert=2, debug=3).get(level) diff --git a/mitmproxy/master.py b/mitmproxy/master.py index ec10032c0f..58da37bd17 100644 --- a/mitmproxy/master.py +++ b/mitmproxy/master.py @@ -1,15 +1,17 @@ import asyncio -import traceback -from typing import Optional +import logging -from mitmproxy import addonmanager, hooks +from . import ctx as mitmproxy_ctx +from .proxy.mode_specs import ReverseMode +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 mitmproxy.net import server_spec -from . import ctx as mitmproxy_ctx + +logger = logging.getLogger(__name__) class Master: @@ -19,22 +21,26 @@ class Master: event_loop: asyncio.AbstractEventLoop - def __init__(self, opts, event_loop: Optional[asyncio.AbstractEventLoop] = None): + def __init__( + self, + opts: options.Options, + event_loop: asyncio.AbstractEventLoop | None = None, + ): self.options: options.Options = opts or options.Options() self.commands = command.CommandManager(self) self.addons = addonmanager.AddonManager(self) - self.log = log.Log(self) + + self.log = log.Log(self) # deprecated, do not use. + self._legacy_log_events = log.LegacyLogEvents(self) + self._legacy_log_events.install() # We expect an active event loop here already because some addons # 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: - self.should_exit = asyncio.Event(loop=self.event_loop) + self.should_exit = asyncio.Event() mitmproxy_ctx.master = self - mitmproxy_ctx.log = self.log + mitmproxy_ctx.log = self.log # deprecated, do not use. mitmproxy_ctx.options = self.options async def run(self) -> None: @@ -43,8 +49,20 @@ async def run(self) -> None: try: self.should_exit.clear() - # Handle scheduled tasks (configure()) first. - await asyncio.sleep(0) + if ec := self.addons.get("errorcheck"): + await ec.shutdown_if_errored() + if ps := self.addons.get("proxyserver"): + # 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 ec := self.addons.get("errorcheck"): + await ec.shutdown_if_errored() + ec.finish() await self.running() try: await self.should_exit.wait() @@ -66,23 +84,19 @@ async def running(self) -> None: async def done(self) -> None: await self.addons.trigger_event(hooks.DoneHook()) + self._legacy_log_events.uninstall() - def _asyncio_exception_handler(self, loop, context): + def _asyncio_exception_handler(self, loop, context) -> None: try: exc: Exception = context["exception"] except KeyError: - self.log.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 - self.log.error( - "\n".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) - + "\nPlease lodge a bug report at:" - + "\n\thttps://github.com/mitmproxy/mitmproxy/issues" + logger.error( + "Unhandled error in task.", + exc_info=(type(exc), exc, exc.__traceback__), ) async def load_flow(self, f): @@ -90,14 +104,19 @@ async def load_flow(self, f): Loads a flow """ - if isinstance(f, http.HTTPFlow): - if self.options.mode.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. - _, upstream_spec = server_spec.parse_with_mode(self.options.mode) - f.request.host, f.request.port = upstream_spec.address - f.request.scheme = upstream_spec.scheme + 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 for e in eventsequence.iterate(f): await self.addons.handle_lifecycle(e) 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..d99f03c64a 100644 --- a/mitmproxy/net/dns/domain_names.py +++ b/mitmproxy/net/dns/domain_names.py @@ -1,7 +1,6 @@ import struct from typing import Optional - _LABEL_SIZE = struct.Struct("!B") _POINTER_OFFSET = struct.Struct("!H") _POINTER_INDICATOR = 0b11000000 diff --git a/mitmproxy/net/encoding.py b/mitmproxy/net/encoding.py index 32e61b62c6..17a48f1caa 100644 --- a/mitmproxy/net/encoding.py +++ b/mitmproxy/net/encoding.py @@ -1,13 +1,12 @@ """ Utility functions for decoding response bodies. """ - import codecs import collections import gzip import zlib from io import BytesIO -from typing import Union, overload +from typing import overload import brotli import zstandard as zstd @@ -31,13 +30,13 @@ 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 @@ -87,7 +86,7 @@ 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: ... @@ -97,8 +96,8 @@ 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 @@ -159,7 +158,8 @@ def decode_gzip(content: bytes) -> bytes: def encode_gzip(content: bytes) -> bytes: s = BytesIO() - gf = gzip.GzipFile(fileobj=s, mode="wb") + # set mtime to 0 so that gzip encoding is deterministic. + gf = gzip.GzipFile(fileobj=s, mode="wb", mtime=0) gf.write(content) gf.close() return s.getvalue() @@ -184,7 +184,7 @@ def decode_zstd(content: bytes) -> bytes: 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.decompress(content, max_output_size=10 * 2**20) 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..e87efc5032 100644 --- a/mitmproxy/net/http/headers.py +++ b/mitmproxy/net/http/headers.py @@ -1,8 +1,7 @@ import collections -from typing import Optional -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 +32,4 @@ 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}" diff --git a/mitmproxy/net/http/http1/__init__.py b/mitmproxy/net/http/http1/__init__.py index 3049e02fb9..022e03fa22 100644 --- a/mitmproxy/net/http/http1/__init__.py +++ b/mitmproxy/net/http/http1/__init__.py @@ -1,18 +1,13 @@ -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 +from .read import validate_headers __all__ = [ "read_request_head", diff --git a/mitmproxy/net/http/http1/read.py b/mitmproxy/net/http/http1/read.py index 1da4583e1d..774dc83f1f 100644 --- a/mitmproxy/net/http/http1/read.py +++ b/mitmproxy/net/http/http1/read.py @@ -1,8 +1,10 @@ import re import time -from typing import Iterable, Optional +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 @@ -53,7 +55,7 @@ def validate_headers(headers: Headers) -> None: te_found = False cl_found = False - for (name, value) in headers.fields: + 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 " @@ -75,8 +77,8 @@ def validate_headers(headers: Headers) -> None: 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: @@ -214,7 +216,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 +225,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/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..6a83f8fb79 100644 --- a/mitmproxy/net/http/user_agents.py +++ b/mitmproxy/net/http/user_agents.py @@ -2,9 +2,7 @@ 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/local_ip.py b/mitmproxy/net/local_ip.py new file mode 100644 index 0000000000..bc3087263c --- /dev/null +++ b/mitmproxy/net/local_ip.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import socket + + +def get_local_ip(reachable: str = "8.8.8.8") -> str | None: + """ + Get the default local outgoing IPv4 address without sending any packets. + This will fail if the target address is known to be unreachable. + 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) + try: + s.connect((reachable, 80)) + return s.getsockname()[0] + except OSError: + return None + finally: + s.close() + + +def get_local_ip6(reachable: str = "2001:4860:4860::8888") -> str | None: + """ + Get the default local outgoing IPv6 address without sending any packets. + 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) + try: + s.connect((reachable, 80)) + return s.getsockname()[0] + except OSError: + return None + finally: + s.close() diff --git a/mitmproxy/net/server_spec.py b/mitmproxy/net/server_spec.py index ee95ad6471..f0d5edd609 100644 --- a/mitmproxy/net/server_spec.py +++ b/mitmproxy/net/server_spec.py @@ -1,17 +1,16 @@ """ Server specs are used to describe an upstream proxy or server. """ -import functools import re -from typing import Literal, NamedTuple +from functools import cache +from typing import Literal from mitmproxy.net import check - -class ServerSpec(NamedTuple): - scheme: Literal["http", "https"] - address: tuple[str, int] - +ServerSpec = tuple[ + Literal["http", "https", "http3", "tls", "dtls", "tcp", "udp", "dns", "quic"], + tuple[str, int], +] server_spec_re = re.compile( r""" @@ -26,8 +25,8 @@ class ServerSpec(NamedTuple): ) -@functools.lru_cache -def parse(server_spec: str) -> ServerSpec: +@cache +def parse(server_spec: str, default_scheme: str) -> ServerSpec: """ Parses a server mode specification, e.g.: @@ -45,8 +44,18 @@ def parse(server_spec: str) -> ServerSpec: if m.group("scheme"): scheme = m.group("scheme") else: - scheme = "https" if m.group("port") in ("443", None) else "http" - if scheme not in ("http", "https"): + scheme = default_scheme + if scheme not in ( + "http", + "https", + "http3", + "tls", + "dtls", + "tcp", + "udp", + "dns", + "quic", + ): raise ValueError(f"Invalid server scheme: {scheme}") host = m.group("host") @@ -59,19 +68,17 @@ def parse(server_spec: str) -> ServerSpec: if m.group("port"): port = int(m.group("port")) else: - port = {"http": 80, "https": 443}[scheme] + try: + port = { + "http": 80, + "https": 443, + "quic": 443, + "http3": 443, + "dns": 53, + }[scheme] + except KeyError: + raise ValueError(f"Port specification missing.") if not check.is_valid_port(port): raise ValueError(f"Invalid port: {port}") - return ServerSpec(scheme, (host, port)) # type: ignore - - -def parse_with_mode(mode: str) -> tuple[str, ServerSpec]: - """ - Parse a proxy mode specification, which is usually just `(reverse|upstream):server-spec`. - - *Raises:* - - ValueError, if the specification is invalid. - """ - mode, server_spec = mode.split(":", maxsplit=1) - return mode, parse(server_spec) + return scheme, (host, port) # type: ignore diff --git a/mitmproxy/net/tls.py b/mitmproxy/net/tls.py index e2f7e35224..21b4754a48 100644 --- a/mitmproxy/net/tls.py +++ b/mitmproxy/net/tls.py @@ -1,23 +1,34 @@ import os import threading +from collections.abc import Callable +from collections.abc import Iterable from enum import Enum 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 cryptography.hazmat.primitives.asymmetric import rsa -from OpenSSL import SSL, crypto 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 try: @@ -50,7 +61,7 @@ class Verify(Enum): 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. @@ -71,7 +82,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 @@ -87,7 +98,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) @@ -102,6 +114,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: @@ -119,19 +138,23 @@ def _create_ssl_context( @lru_cache(256) 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.TLS_CLIENT_METHOD, + method=method, min_version=min_version, max_version=max_version, cipher_list=cipher_list, + ecdh_curve=ecdh_curve, ) context.set_verify(verify.value, None) @@ -152,32 +175,34 @@ 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 @lru_cache(256) def create_client_proxy_context( *, + method: Method, min_version: Version, max_version: Version, - cipher_list: Optional[tuple[str, ...]], - cert: certs.Cert, - key: rsa.RSAPrivateKey, - 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, ) -> SSL.Context: context: SSL.Context = _create_ssl_context( - method=Method.TLS_SERVER_METHOD, + method=method, min_version=min_version, max_version=max_version, cipher_list=cipher_list, + ecdh_curve=ecdh_curve, ) - context.use_certificate(cert.to_pyopenssl()) - context.use_privatekey(crypto.PKey.from_cryptography_key(key)) if chain_file is not None: try: context.load_verify_locations(str(chain_file), None) @@ -222,15 +247,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 index c70647800e..37892c997f 100644 --- a/mitmproxy/net/udp.py +++ b/mitmproxy/net/udp.py @@ -1,14 +1,19 @@ from __future__ import annotations import asyncio -import ipaddress +import logging import socket -import struct -from typing import Any, Callable, Optional, Union, cast -from mitmproxy import ctx +from collections.abc import Callable +from typing import Any +from typing import cast +from typing import Union + +import mitmproxy_rs + from mitmproxy.connection import Address from mitmproxy.utils import human +logger = logging.getLogger(__name__) MAX_DATAGRAM_SIZE = 65535 - 20 @@ -21,78 +26,13 @@ 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`. -In the case of transparent server, the last argument is the original destination address. """ # to make mypy happy SockAddress = Union[tuple[str, int], tuple[str, int, int, int]] -class TransparentSocket(socket.socket): - SOL_IP = getattr(socket, "SOL_IP", 0) - IP_TRANSPARENT = getattr(socket, "IP_TRANSPARENT", 19) - IP_RECVORIGDSTADDR = getattr(socket, "IP_RECVORIGDSTADDR", 20) - - def __init__(self, family: socket.AddressFamily, local_addr: SockAddress) -> None: - self._recvmsg = getattr(self, "recvmsg") - if not self._recvmsg: - raise NotImplementedError( - "Transparent UDP sockets are only supporting on platforms providing recvmsg." - ) - super().__init__( - family=family, type=socket.SOCK_DGRAM, proto=socket.IPPROTO_UDP - ) - try: - self.setblocking(False) - self.setsockopt( - TransparentSocket.SOL_IP, TransparentSocket.IP_TRANSPARENT, 1 - ) - self.setsockopt( - TransparentSocket.SOL_IP, TransparentSocket.IP_RECVORIGDSTADDR, 1 - ) - self.bind(local_addr) - except: - self.close() - raise - - @staticmethod - def _unpack_addr(sockaddr_in: bytes) -> SockAddress: - """Converts a native sockaddr into a python tuple.""" - - (family,) = struct.unpack_from("h", sockaddr_in, 0) - if family == socket.AF_INET: - port, in4_addr, _ = struct.unpack_from("!H4s8s", sockaddr_in, 2) - return str(ipaddress.IPv4Address(in4_addr)), port - elif family == socket.AF_INET6: - port, flowinfo, in6_addr, scopeid = struct.unpack_from( - "!HL16sL", sockaddr_in, 2 - ) - return str(ipaddress.IPv6Address(in6_addr)), port, flowinfo, scopeid - else: - raise NotImplementedError(f"family {family} not implemented") - - def recvfrom( - self, bufsize: int, flags: int = 0 - ) -> tuple[bytes, tuple[SockAddress, SockAddress]]: - """Same as recvfrom, but always returns source and destination addresses.""" - - data, ancdata, _, client_addr = self._recvmsg( - bufsize, socket.CMSG_SPACE(1024), flags - ) - for cmsg_level, cmsg_type, cmsg_data in ancdata: - if ( - cmsg_level == TransparentSocket.SOL_IP - and cmsg_type == TransparentSocket.IP_RECVORIGDSTADDR - ): - server_addr = TransparentSocket._unpack_addr(cmsg_data) - break - else: - raise OSError("recvmsg did not return th original destination address") - return data, (client_addr, server_addr) - - class DrainableDatagramProtocol(asyncio.DatagramProtocol): - _loop: asyncio.AbstractEventLoop _closed: asyncio.Event _paused: int @@ -120,7 +60,7 @@ def connection_made(self, transport: asyncio.BaseTransport) -> None: def connection_lost(self, exc: Exception | None) -> None: self._closed.set() if exc: - ctx.log.warn(f"Connection lost on {self!r}: {exc!r}") + logger.warning(f"Connection lost on {self!r}: {exc!r}") # pragma: no cover def pause_writing(self) -> None: self._paused = self._paused + 1 @@ -137,7 +77,7 @@ async def drain(self) -> None: await self._can_write.wait() def error_received(self, exc: Exception) -> None: - ctx.log.warn(f"Send/receive on {self!r} failed: {exc!r}") + logger.warning(f"Send/receive on {self!r} failed: {exc!r}") # pragma: no cover async def wait_closed(self) -> None: await self._closed.wait() @@ -148,87 +88,37 @@ class UdpServer(DrainableDatagramProtocol): # _datagram_received_cb: DatagramReceivedCallback _transport: asyncio.DatagramTransport | None - _transparent_transports: dict[Address, asyncio.DatagramTransport] | None _local_addr: Address | None def __init__( self, datagram_received_cb: DatagramReceivedCallback, loop: asyncio.AbstractEventLoop | None, - transparent: bool, ) -> None: super().__init__(loop) self._datagram_received_cb = datagram_received_cb self._transport = None - self._transparent_transports = {} if transparent else 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) - async def _datagram_received_for_new_transparent_addr( - self, data: bytes, remote_addr: Address, local_addr: Address - ) -> None: - assert self._sock is not None - assert self._transparent_transports is not None - sock = socket.socket( - family=self._sock.family, type=socket.SOCK_DGRAM, proto=socket.IPPROTO_UDP - ) - try: - sock.setblocking(False) - sock.setsockopt( - TransparentSocket.SOL_IP, TransparentSocket.IP_TRANSPARENT, 1 - ) - sock.shutdown(socket.SHUT_RD) - sock.bind(local_addr) - except: - sock.close() - raise - transport, _ = await self._loop.create_datagram_endpoint( - lambda: self, sock=sock - ) - self._transparent_transports[local_addr] = cast( - asyncio.DatagramTransport, transport - ) - self._datagram_received_cb( - self._transparent_transports[local_addr], data, remote_addr, local_addr - ) - def datagram_received(self, data: bytes, addr: Any) -> None: assert self._transport is not None - if self._transparent_transports is None: - assert self._local_addr is not None - self._datagram_received_cb(self._transport, data, addr, self._local_addr) - else: - remote_addr, local_addr = addr - if local_addr in self._transparent_transports: - self._datagram_received_cb( - self._transparent_transports[local_addr], - data, - remote_addr, - local_addr, - ) - else: - self._loop.create_task( - self._datagram_received_for_new_transparent_addr( - data, remote_addr, local_addr - ) - ) + 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() - if self._transparent_transports is not None: - for transport in self._transparent_transports.values(): - transport.close() class DatagramReader: - - _packets: asyncio.Queue + _packets: asyncio.Queue[bytes] _eof: bool def __init__(self) -> None: @@ -238,14 +128,14 @@ def __init__(self) -> None: def feed_data(self, data: bytes, remote_addr: Address) -> None: assert len(data) <= MAX_DATAGRAM_SIZE if self._eof: - ctx.log.info( + logger.info( f"Received UDP packet from {human.format_address(remote_addr)} after EOF." ) else: try: self._packets.put_nowait(data) except asyncio.QueueFull: - ctx.log.debug( + logger.debug( f"Dropped UDP packet from {human.format_address(remote_addr)}." ) @@ -264,19 +154,22 @@ async def read(self, n: int) -> bytes: except asyncio.QueueEmpty: return b"" else: - return await self._packets.get() + try: + return await self._packets.get() + except RuntimeError: # pragma: no cover + # event loop got closed + return b"" class DatagramWriter: - - _transport: asyncio.DatagramTransport + _transport: asyncio.DatagramTransport | mitmproxy_rs.DatagramTransport _remote_addr: Address _reader: DatagramReader | None _closed: asyncio.Event | None def __init__( self, - transport: asyncio.DatagramTransport, + transport: asyncio.DatagramTransport | mitmproxy_rs.DatagramTransport, remote_addr: Address, reader: DatagramReader | None = None, ) -> None: @@ -286,20 +179,24 @@ def __init__( """ self._transport = transport self._remote_addr = remote_addr - proto = transport.get_protocol() - assert isinstance(proto, DrainableDatagramProtocol) - self._reader = reader - self._closed = asyncio.Event() if reader is not None else None + if reader is not None: + self._reader = reader + self._closed = asyncio.Event() + else: + self._reader = None + self._closed = None @property - def _protocol(self) -> DrainableDatagramProtocol: - return cast(DrainableDatagramProtocol, self._transport.get_protocol()) + def _protocol( + self, + ) -> DrainableDatagramProtocol | mitmproxy_rs.DatagramTransport: + 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 NotImplementedError("UDP does not support half-closing.") + raise OSError("UDP does not support half-closing.") def get_extra_info(self, name: str, default: Any = None) -> Any: if name == "peername": @@ -312,9 +209,15 @@ def close(self) -> None: self._transport.close() else: self._closed.set() - if self._reader is not None: + 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() @@ -346,39 +249,21 @@ async def start_server( datagram_received_cb: DatagramReceivedCallback, host: str, port: int, - *, - transparent: bool = False, ) -> UdpServer: """UDP variant of asyncio.start_server.""" + assert host, "Cannot bind to an empty host for UDP sockets on Windows or Ubuntu." loop = asyncio.get_running_loop() - - if transparent: - addrinfos = await loop.getaddrinfo(host, port) - exception = OSError(f"getaddrinfo for host '{host}' failed") - for family, _, _, _, addr in addrinfos: - try: - sock = TransparentSocket(family=family, local_addr=addr) - except OSError as exc: - exception = exc - else: - break - else: - raise exception - else: - sock = None - _, protocol = await loop.create_datagram_endpoint( - lambda: UdpServer(datagram_received_cb, loop, transparent), + lambda: UdpServer(datagram_received_cb, loop), local_addr=(host, port), - sock=sock, ) assert isinstance(protocol, UdpServer) return protocol async def open_connection( - host: str, port: int, *, local_addr: Optional[Address] = None + host: str, port: int, *, local_addr: Address | None = None ) -> tuple[DatagramReader, DatagramWriter]: """UDP variant of asyncio.open_connection.""" diff --git a/mitmproxy/options.py b/mitmproxy/options.py index c9aa9c3335..6ef97db44a 100644 --- a/mitmproxy/options.py +++ b/mitmproxy/options.py @@ -5,7 +5,6 @@ CONF_DIR = "~/.mitmproxy" CONF_BASENAME = "mitmproxy" -LISTEN_PORT = 8080 CONTENT_VIEW_LINES_CUTOFF = 512 KEY_SIZE = 2048 @@ -91,16 +90,34 @@ 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 to.") - self.add_option("listen_port", int, LISTEN_PORT, "Proxy service port.") self.add_option( - "mode", + "listen_host", str, - "regular", + "", + "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], + ["regular"], """ - Mode can be "regular", "transparent", "socks5", "reverse:SPEC", - or "upstream:SPEC". For reverse and upstream proxy modes, SPEC - is host specification in the form of "http[s]://host[:port]". + The proxy server type(s) to spawn. Can be passed multiple times. + + Mitmproxy supports "regular" (HTTP), "transparent", "socks5", "reverse:SPEC", + "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 + which upstream server to use. """, ) self.add_option( @@ -114,7 +131,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", @@ -126,6 +143,12 @@ 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( "websocket", bool, @@ -171,6 +194,16 @@ def __init__(self, **kwargs) -> None: The communication contents are printed to the log in verbose mode. """, ) + self.add_option( + "udp_hosts", + Sequence[str], + [], + """ + Generic UDP SSL proxy mode for all hosts that match the pattern. + Similar to --ignore-hosts, but SSL connections are intercepted. + The communication contents are printed to the log in verbose mode. + """, + ) self.add_option( "content_view_lines_cutoff", int, diff --git a/mitmproxy/optmanager.py b/mitmproxy/optmanager.py index 6264ce6f9e..b116f31d13 100644 --- a/mitmproxy/optmanager.py +++ b/mitmproxy/optmanager.py @@ -1,18 +1,23 @@ +from __future__ import annotations + import contextlib import copy -from collections.abc import Sequence -from dataclasses import dataclass -import functools -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 blinker -import blinker._saferef import ruamel.yaml from mitmproxy import exceptions +from mitmproxy.utils import signals from mitmproxy.utils import typecheck """ @@ -28,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 @@ -83,11 +88,19 @@ class _UnconvertedStrings: val: list[str] +def _sig_changed_spec(updated: set[str]) -> None: # pragma: no cover + ... # expected function signature for OptManager.changed receivers. + + +def _sig_errored_spec(exc: Exception) -> None: # pragma: no cover + ... # expected function signature for OptManager.errored receivers. + + class OptManager: """ OptManager is the base class from which Options objects are derived. - .changed is a blinker Signal that triggers whenever options are + .changed is a Signal that triggers whenever options are updated. If any handler in the chain raises an exceptions.OptionsError exception, all changes are rolled back, the exception is suppressed, and the .errored signal is notified. @@ -96,10 +109,12 @@ class OptManager: mutation doesn't change the option state inadvertently. """ - def __init__(self): + def __init__(self) -> None: self.deferred: dict[str, Any] = {} - self.changed = blinker.Signal() - self.errored = blinker.Signal() + self.changed = signals.SyncSignal(_sig_changed_spec) + self.changed.connect(self._notify_subscribers) + self.errored = signals.SyncSignal(_sig_errored_spec) + self._subscriptions: list[tuple[weakref.ref[Callable], set[str]]] = [] # Options must be the last attribute here - after that, we raise an # error for attribute assignment to unknown options. self._options: dict[str, Any] = {} @@ -107,13 +122,13 @@ 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(self, updated={name}) + self.changed.send(updated={name}) @contextlib.contextmanager def rollback(self, updated, reraise=False): @@ -122,10 +137,10 @@ def rollback(self, updated, reraise=False): yield except exceptions.OptionsError as e: # Notify error handlers - self.errored.send(self, exc=e) + self.errored.send(exc=e) # Rollback self.__dict__["_options"] = old - self.changed.send(self, updated=updated) + self.changed.send(updated=updated) if reraise: raise e @@ -141,23 +156,22 @@ def subscribe(self, func, opts): if i not in self._options: raise exceptions.OptionsError("No such option: %s" % i) - # We reuse blinker's safe reference functionality to cope with weakrefs - # to bound methods. - func = blinker._saferef.safe_ref(func) + self._subscriptions.append((signals.make_weak_ref(func), set(opts))) - @functools.wraps(func) - def _call(options, updated): - if updated.intersection(set(opts)): - f = func() - if f: - f(options, updated) - else: - self.changed.disconnect(_call) + def _notify_subscribers(self, updated) -> None: + cleanup = False + for ref, opts in self._subscriptions: + callback = ref() + if callback is not None: + if opts & updated: + callback(self, updated) + else: + cleanup = True - # Our wrapper function goes out of scope immediately, so we have to set - # weakrefs to false. This means we need to keep our own weakref, and - # clean up the hook when it's gone. - self.changed.connect(_call, weak=False) + if cleanup: + self.__dict__["_subscriptions"] = [ + (ref, opts) for (ref, opts) in self._subscriptions if ref() is not None + ] def __eq__(self, other): if isinstance(other, OptManager): @@ -202,7 +216,7 @@ def reset(self): """ for o in self._options.values(): o.reset() - self.changed.send(self, updated=set(self._options.keys())) + self.changed.send(updated=set(self._options.keys())) def update_known(self, **kwargs): """ @@ -220,7 +234,7 @@ def update_known(self, **kwargs): with self.rollback(updated, reraise=True): for k, v in known.items(): self._options[k].set(v) - self.changed.send(self, updated=updated) + self.changed.send(updated=updated) return unknown def update_defer(self, **kwargs): @@ -358,7 +372,7 @@ 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: @@ -401,9 +415,9 @@ 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 @@ -482,14 +496,15 @@ def dump_defaults(opts, out: TextIO): return ruamel.yaml.YAML().dump(s, out) -def dump_dicts(opts, keys: list[str] = None): +def dump_dicts(opts, keys: Iterable[str] | None = None) -> dict: """ Dumps the options into a list of dict object. Return: A list like: { "anticache": { type: "bool", default: false, value: true, help: "help text"} } """ options_dict = {} - keys = keys if keys else opts.keys() + if keys is None: + keys = opts.keys() for k in sorted(keys): o = opts._options[k] t = typecheck.typespec_to_str(o.typespec) @@ -515,7 +530,7 @@ def parse(text): 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.") @@ -526,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}") @@ -579,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: @@ -595,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 969c655415..53539d4b77 100644 --- a/mitmproxy/platform/windows.py +++ b/mitmproxy/platform/windows.py @@ -1,9 +1,8 @@ -import collections +from __future__ import annotations + import collections.abc import contextlib -import ctypes import ctypes.wintypes -import io import json import os import re @@ -12,11 +11,16 @@ import threading import time from collections.abc import Callable -from typing import Any, ClassVar, Optional +from typing import Any +from typing import cast +from typing import ClassVar +from typing import IO -import pydivert import pydivert.consts +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 @@ -25,20 +29,20 @@ # Resolver -def read(rfile: io.BufferedReader) -> Any: +def read(rfile: IO[bytes]) -> 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: IO[bytes]) -> 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): @@ -82,7 +86,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) @@ -94,7 +100,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) @@ -123,6 +131,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_ = [ @@ -152,6 +161,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_ = [ @@ -203,7 +213,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, @@ -226,7 +236,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, @@ -249,32 +259,6 @@ def _refresh_ipv6(self): ) -def get_local_ip() -> Optional[str]: - # Auto-Detect local IP. This is required as re-injecting to 127.0.0.1 does not work. - # https://stackoverflow.com/questions/166506/finding-local-ip-addresses-using-pythons-stdlib - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - s.connect(("8.8.8.8", 80)) - return s.getsockname()[0] - except OSError: - return None - finally: - s.close() - - -def get_local_ip6(reachable: str) -> Optional[str]: - # The same goes for IPv6, with the added difficulty that .connect() fails if - # the target network is not reachable. - s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) - try: - s.connect((reachable, 80)) - return s.getsockname()[0] - except OSError: - return None - finally: - s.close() - - class Redirect(threading.Thread): daemon = True windivert: pydivert.WinDivert @@ -299,7 +283,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 @@ -309,7 +293,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. @@ -417,9 +401,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 @@ -433,7 +417,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 = ( @@ -442,7 +426,7 @@ def __init__( ) self.ipv4_address = get_local_ip() - self.ipv6_address = get_local_ip6("2001:4860:4860::8888") + self.ipv6_address = get_local_ip6() # print(f"IPv4: {self.ipv4_address}, IPv6: {self.ipv6_address}") self.client_server_map = ClientServerMap() 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 388abf9fe8..e6749dccda 100644 --- a/mitmproxy/proxy/commands.py +++ b/mitmproxy/proxy/commands.py @@ -6,10 +6,14 @@ The counterpart to commands are events. """ -from typing import Literal, Union, TYPE_CHECKING +import logging +import warnings +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 @@ -74,7 +78,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): @@ -92,6 +96,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. @@ -119,26 +125,34 @@ def __new__(cls, *args, **kwargs): return super().__new__(cls, *args, **kwargs) -class GetSocket(ConnectionCommand): - """ - Get the underlying socket. - This should really never be used, but is required to implement transparent mode. +class Log(Command): """ + Log a message. - blocking = True - + Layers could technically call `logging.log` directly, but the use of a command allows us to + write more expressive playbook tests. Put differently, by using commands we can assert that + a specific log message is a direct consequence of a particular I/O event. + 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. + """ -class Log(Command): message: str - level: str + level: int def __init__( self, message: str, - level: Literal["error", "warn", "info", "alert", "debug"] = "info", + level: int = logging.INFO, ): + if isinstance(level, str): # pragma: no cover + warnings.warn( + "commands.Log() now expects an integer log level, not a string.", + DeprecationWarning, + stacklevel=2, + ) + level = getattr(logging, level.upper()) self.message = message self.level = level def __repr__(self): - return f"Log({self.message!r}, {self.level!r})" + return f"Log({self.message!r}, {logging.getLevelName(self.level).lower()})" 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 b767483f0e..c0518631c7 100644 --- a/mitmproxy/proxy/events.py +++ b/mitmproxy/proxy/events.py @@ -3,14 +3,17 @@ Events represent the only way for layers to receive new data from sockets. The counterpart to events are commands. """ -import socket +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: @@ -48,7 +51,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): @@ -73,7 +76,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) @@ -81,7 +84,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: @@ -102,7 +105,7 @@ def __repr__(self): @dataclass(repr=False) class OpenConnectionCompleted(CommandCompleted): command: commands.OpenConnection - reply: Optional[str] + reply: str | None """error message""" @@ -112,12 +115,6 @@ class HookCompleted(CommandCompleted): reply: None = None -@dataclass(repr=False) -class GetSocketCompleted(CommandCompleted): - command: commands.GetSocket - reply: socket.socket - - T = TypeVar("T") diff --git a/mitmproxy/proxy/layer.py b/mitmproxy/proxy/layer.py index c1a1c3d0f4..27328c4fc7 100644 --- a/mitmproxy/proxy/layer.py +++ b/mitmproxy/proxy/layer.py @@ -4,12 +4,20 @@ import collections import textwrap from abc import abstractmethod +from collections.abc import Callable +from collections.abc import Generator from dataclasses import dataclass -from typing import Any, ClassVar, Generator, NamedTuple, Optional, TypeVar +from logging import DEBUG +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") @@ -19,6 +27,10 @@ """ +MAX_LOG_STATEMENT_SIZE = 512 +"""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. @@ -51,7 +63,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. @@ -61,7 +73,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. @@ -89,15 +101,16 @@ 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 - return commands.Log(textwrap.indent(message, self.debug), "debug") + assert self.debug is not None + return commands.Log(textwrap.indent(message, self.debug), DEBUG) @property def stack_pos(self) -> str: @@ -232,7 +245,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] @@ -246,7 +259,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)}" @@ -284,7 +297,7 @@ def _ask(self): # Has an addon decided on the next layer yet? if self.layer: if self.debug: - yield commands.Log(f"{self.debug}[nextlayer] {self.layer!r}", "debug") + yield commands.Log(f"{self.debug}[nextlayer] {self.layer!r}", DEBUG) for e in self.events: yield from self.layer.handle_event(e) self.events.clear() @@ -295,8 +308,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 55553b258c..e21ba60e08 100644 --- a/mitmproxy/proxy/layers/__init__.py +++ b/mitmproxy/proxy/layers/__init__.py @@ -1,16 +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, ServerTLSLayer +from .tls import ClientTLSLayer +from .tls import ServerTLSLayer +from .udp import UDPLayer 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 d76c1abfd8..e2e5c701bb 100644 --- a/mitmproxy/proxy/layers/dns.py +++ b/mitmproxy/proxy/layers/dns.py @@ -1,9 +1,11 @@ -from dataclasses import dataclass import struct +from dataclasses import dataclass -from mitmproxy import dns, flow -from mitmproxy import connection -from mitmproxy.proxy import commands, events, layer +from mitmproxy import dns +from mitmproxy import flow as mflow +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 @@ -40,40 +42,43 @@ class DNSLayer(layer.Layer): Layer that handles resolving DNS queries. """ - flow: dns.DNSFlow + flows: dict[int, dns.DNSFlow] def __init__(self, context: Context): super().__init__(context) - self.flow = dns.DNSFlow(self.context.client, self.context.server, live=True) - - def handle_request(self, msg: dns.Message) -> layer.CommandGenerator[None]: - self.flow.request = msg # if already set, continue and query upstream again - yield DnsRequestHook( - self.flow - ) # give hooks a chance to change the request or produce a response - if self.flow.response: - yield from self.handle_response(self.flow.response) - elif not self.flow.server_conn.address: - yield from self.handle_error("No hook has set a response.") + self.flows = {} + + 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 not self.context.server.address: + yield from self.handle_error( + flow, "No hook has set a response and there is no upstream server." + ) else: - if ( - self.flow.server_conn.state is connection.ConnectionState.CLOSED - ): # we need an upstream connection - err = yield commands.OpenConnection(self.flow.server_conn) + if not self.context.server.connected: + err = yield commands.OpenConnection(self.context.server) if err: - yield from self.handle_error(str(err)) - return # cannot recover from this - yield commands.SendData(self.flow.server_conn, self.flow.request.packed) - - def handle_response(self, msg: dns.Message) -> layer.CommandGenerator[None]: - self.flow.response = msg - yield DnsResponseHook(self.flow) - if self.flow.response: # allows the response hook to suppress an answer - yield commands.SendData(self.context.client, self.flow.response.packed) - - def handle_error(self, err: str) -> layer.CommandGenerator[None]: - self.flow.error = flow.Error(err) - yield DnsErrorHook(self.flow) + yield from self.handle_error(flow, str(err)) + # cannot recover from this + return + yield commands.SendData(self.context.server, flow.request.packed) + + 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) + + def handle_error(self, flow: dns.DNSFlow, err: str) -> layer.CommandGenerator[None]: + flow.error = mflow.Error(err) + yield DnsErrorHook(flow) @expect(events.Start) def state_start(self, _) -> layer.CommandGenerator[None]: @@ -90,18 +95,28 @@ def state_query(self, event: events.Event) -> layer.CommandGenerator[None]: msg = dns.Message.unpack(event.data) 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(msg) + yield from self.handle_request(flow, msg) else: - yield from self.handle_response(msg) + yield from self.handle_response(flow, msg) elif isinstance(event, events.ConnectionClosed): - other_conn = self.flow.server_conn if from_client else self.context.client - if other_conn.state is not connection.ConnectionState.CLOSED: + other_conn = self.context.server if from_client else self.context.client + if other_conn.connected: yield commands.CloseConnection(other_conn) self._handle_event = self.state_done - self.flow.live = False + for flow in self.flows.values(): + flow.live = False else: raise AssertionError(f"Unexpected event: {event}") diff --git a/mitmproxy/proxy/layers/http/__init__.py b/mitmproxy/proxy/layers/http/__init__.py index 08d6d7fb3b..419d2971b3 100644 --- a/mitmproxy/proxy/layers/http/__init__.py +++ b/mitmproxy/proxy/layers/http/__init__.py @@ -2,46 +2,65 @@ import enum import time from dataclasses import dataclass -from typing import Optional, Union +from functools import cached_property +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 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.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.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 class HTTPMode(enum.Enum): @@ -50,7 +69,7 @@ class HTTPMode(enum.Enum): upstream = 3 -def validate_request(mode: HTTPMode, request: http.Request) -> Optional[str]: +def validate_request(mode: HTTPMode, request: http.Request) -> 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": @@ -61,6 +80,10 @@ def validate_request(mode: HTTPMode, request: http.Request) -> Optional[str]: 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): """ @@ -70,7 +93,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) @@ -81,13 +105,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""" @@ -98,7 +123,7 @@ class RegisterHttpConnection(HttpCommand): """ connection: Connection - err: Optional[str] + err: str | None @dataclass @@ -122,15 +147,16 @@ class HttpStream(layer.Layer): response_body_buf: bytes flow: http.HTTPFlow stream_id: StreamId - child_layer: Optional[layer.Layer] = None + child_layer: layer.Layer | None = None - @property - def mode(self): + @cached_property + 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"" @@ -215,14 +241,16 @@ 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 = "" # update host header in reverse proxy mode if ( - self.context.options.mode.startswith("reverse:") + isinstance(self.context.client.proxy_mode, ReverseMode) and not self.context.options.keep_host_header ): assert self.context.server.address @@ -271,7 +299,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): @@ -478,10 +506,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) @@ -490,14 +519,14 @@ def flow_done(self): else: yield commands.Log( f"Sent HTTP 101 response, but no protocol is enabled to upgrade to.", - "warn", + WARNING, ) yield commands.CloseConnection(self.context.client) self.client_state = self.server_state = self.state_errored return if self.debug: yield commands.Log( - f"{self.debug}[http] upgrading to {self.child_layer}", "debug" + f"{self.debug}[http] upgrading to {self.child_layer}", DEBUG ) yield from self.child_layer.handle_event(events.Start()) self._handle_event = self.passthrough @@ -523,7 +552,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) @@ -621,7 +650,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) @@ -659,6 +688,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( @@ -670,6 +700,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 @@ -690,7 +721,9 @@ def handle_connect_regular(self): if err: self.flow.response = http.Response.make( 502, - f"Cannot connect to {human.format_address(self.context.server.address)}: {err}", + 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.", ) self.child_layer = layer.NextLayer(self.context) yield from self.handle_connect_finish() @@ -776,7 +809,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 @@ -819,28 +853,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: - self.context.server.via = server_spec.parse_with_mode( - self.context.options.mode - )[1] - elif isinstance(event, events.Wakeup): - stream = self.command_sources.pop(event.command) - yield from self.event_to_child(stream, event) + 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.CommandCompleted): stream = self.command_sources.pop(event.command) yield from self.event_to_child(stream, event) @@ -873,10 +907,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()) @@ -893,7 +930,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): @@ -991,12 +1028,12 @@ 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 - assert event.via.scheme in ("http", "https") # We always send a CONNECT request, *except* for plaintext absolute-form HTTP requests in upstream mode. send_connect = event.tls or self.mode != HTTPMode.upstream stack /= _upstream_proxy.HttpUpstreamProxy.make(context, send_connect) @@ -1007,10 +1044,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) @@ -1024,7 +1073,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: @@ -1054,13 +1103,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/_http1.py b/mitmproxy/proxy/layers/http/_http1.py index 4cab5bd9b6..a4932b8cd8 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): @@ -141,7 +149,7 @@ def done(self, event: events.ConnectionEvent) -> layer.CommandGenerator[None]: def make_pipe(self) -> layer.CommandGenerator[None]: self.state = self.passthrough if self.buf: - already_received = self.buf.maybe_extract_at_most(len(self.buf)) + already_received = self.buf.maybe_extract_at_most(len(self.buf)) or b"" # Some clients send superfluous newlines after CONNECT, we want to eat those. already_received = already_received.lstrip(b"\r\n") if already_received: @@ -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,7 +256,11 @@ 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): @@ -264,11 +279,10 @@ def read_headers( if isinstance(event, events.DataReceived): request_head = self.buf.maybe_extract_lines() if request_head: - request_head = [ - bytes(x) for x in request_head - ] # TODO: Make url.parse compatible with bytearrays try: - self.request = http1.read_request_head(request_head) + 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) @@ -332,7 +346,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 @@ -340,7 +354,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() @@ -370,7 +384,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}") @@ -384,15 +398,14 @@ 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: - response_head = [ - bytes(x) for x in response_head - ] # TODO: Make url.parse compatible with bytearrays try: - self.response = http1.read_response_head(response_head) + 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( @@ -450,7 +463,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 a014c3efbb..0843850193 100644 --- a/mitmproxy/proxy/layers/http/_http2.py +++ b/mitmproxy/proxy/layers/http/_http2.py @@ -2,7 +2,9 @@ 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 @@ -13,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): @@ -59,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( - f"{human.format_address(self.context.client.peername)}: " - f"{self.__class__.__name__}" + self.context.client.peername, self.__class__.__name__ ) self.h2_conf.validate_inbound_headers = ( self.context.options.validate_inbound_headers @@ -171,10 +183,10 @@ def _handle_event(self, event: Event) -> CommandGenerator[None]: for h2_event in events: if self.debug: - yield Log(f"{self.debug}[h2] {h2_event}", "debug") + yield Log(f"{self.debug}[h2] {h2_event}", DEBUG) if (yield from self.handle_h2_event(h2_event)): if self.debug: - yield Log(f"{self.debug}[h2] done", "debug") + yield Log(f"{self.debug}[h2] done", DEBUG) return data_to_send = self.h2_conn.data_to_send() @@ -255,12 +267,16 @@ def handle_h2_event(self, event: h2.events.Event) -> CommandGenerator[bool]: elif isinstance(event, h2.events.PushedStreamReceived): yield Log( "Received HTTP/2 push promise, even though we signalled no support.", - "error", + ERROR, ) elif isinstance(event, h2.events.UnknownFrameReceived): # 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 @@ -311,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, @@ -328,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()) @@ -402,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.""" @@ -468,7 +518,7 @@ def _handle_event2(self, event: Event) -> CommandGenerator[None]: if data is not None: yield Log( f"Send HTTP/2 keep-alive PING to {human.format_address(self.conn.peername)}", - "debug", + DEBUG, ) yield SendData(self.conn, data) time_until_next_ping = self.context.options.http2_ping_keepalive - ( @@ -483,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 @@ -550,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, "") @@ -577,7 +608,7 @@ def split_pseudo_headers( ) -> 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}") @@ -640,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..51b04f5bc6 --- /dev/null +++ b/mitmproxy/proxy/layers/http/_http3.py @@ -0,0 +1,298 @@ +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 StreamReset +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.layers.quic import StopQuicStream +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 + ) + self._stream_protocol_errors: dict[int, int] = {} + + 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)): + code = { + status_codes.CLIENT_CLOSED_REQUEST: H3ErrorCode.H3_REQUEST_CANCELLED.value, + }.get(event.code, H3ErrorCode.H3_INTERNAL_ERROR.value) + self._stream_protocol_errors[event.stream_id] = code + 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: + self.h3_conn.reset_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) + if event.stream_id in self._stream_protocol_errors: + # we already reset or ended the stream, tell the peer to stop + # (this is a noop if the peer already did the same) + yield StopQuicStream( + self.conn, + event.stream_id, + self._stream_protocol_errors[event.stream_id], + ) + else: + for h3_event in h3_events: + if isinstance(h3_event, StreamReset): + if h3_event.push_id is None: + err_str = error_code_to_str(h3_event.error_code) + err_code = { + H3ErrorCode.H3_REQUEST_CANCELLED.value: status_codes.CLIENT_CLOSED_REQUEST, + }.get(h3_event.error_code, self.ReceiveProtocolError.code) + yield ReceiveHttp( + self.ReceiveProtocolError( + h3_event.stream_id, + f"stream reset by client ({err_str})", + code=err_code, + ) + ) + elif isinstance(h3_event, DataReceived): + if h3_event.push_id is None: + 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): + if h3_event.push_id is None: + 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): + if h3_event.push_id is None: + 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 + # we don't support push + pass + 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(push_id=None): + 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 abcce65798..60a88f6002 100644 --- a/mitmproxy/proxy/layers/http/_http_h2.py +++ b/mitmproxy/proxy/layers/http/_http_h2.py @@ -1,5 +1,6 @@ import collections -from typing import Dict, List, NamedTuple, Tuple +import logging +from typing import NamedTuple import h2.config import h2.connection @@ -8,17 +9,27 @@ import h2.settings import h2.stream +logger = logging.getLogger(__name__) + class H2ConnectionLogger(h2.config.DummyLogger): - def __init__(self, name: str): + def __init__(self, peername: tuple, conn_type: str): super().__init__() - self.name = name + self.peername = peername + self.conn_type = conn_type def debug(self, fmtstr, *args): - print(f"{self.name} h2 (debug): {fmtstr % args}") + logger.debug( + f"{self.conn_type} {fmtstr}", *args, extra={"client": self.peername} + ) def trace(self, fmtstr, *args): - print(f"{self.name} h2 (trace): {fmtstr % args}") + logger.log( + logging.DEBUG - 1, + f"{self.conn_type} {fmtstr}", + *args, + extra={"client": self.peername}, + ) class SendH2Data(NamedTuple): @@ -34,7 +45,7 @@ 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) @@ -79,7 +90,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 @@ -159,7 +170,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..1d1ea907d6 --- /dev/null +++ b/mitmproxy/proxy/layers/http/_http_h3.py @@ -0,0 +1,289 @@ +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.connection import StreamType +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 ResetQuicStream +from mitmproxy.proxy.layers.quic import SendQuicStreamData + + +@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." + + push_id: int | None = None + "The Push ID or `None` if this is not a push." + + +@dataclass +class StreamReset(H3Event): + """ + The StreamReset event is fired whenever a stream is reset by the peer. + """ + + stream_id: int + "The ID of the stream that was reset." + + error_code: int + """The error code indicating why the stream was reset.""" + + push_id: int | None = None + "The Push ID or `None` if this is not a push." + + +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 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) + super().__init__(self._mock, enable_webtransport) # type: ignore + + 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, event.push_id + ) + 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, push_id: int | None) -> Iterable[int]: + """Iterates over all non-special open streams, optionally for a given push id.""" + + return ( + stream.stream_id + for stream in self._stream.values() + if ( + stream.push_id == push_id + and stream.stream_type == (None if push_id is None else StreamType.PUSH) + 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 [] + + # treat reset events similar to data events with end_stream=True + # We can receive multiple reset events as long as the final size does not change. + elif isinstance(event, QuicStreamReset): + stream = self._get_or_create_stream(event.stream_id) + stream.ended = True + stream.headers_recv_state = HeadersState.AFTER_TRAILERS + return [StreamReset(event.stream_id, event.error_code, stream.push_id)] + + # convert data events from the QUIC layer back to aioquic events + elif isinstance(event, QuicStreamDataReceived): + if 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 reset_stream(self, stream_id: int, error_code: int) -> None: + """Resets a stream that hasn't been ended locally yet.""" + + # set the header state and queue a reset event + stream = self._get_or_create_stream(stream_id) + stream.headers_send_state = HeadersState.AFTER_TRAILERS + self._quic.reset_stream(stream_id, 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", + "StreamReset", + "TrailersReceived", +] diff --git a/mitmproxy/proxy/layers/http/_upstream_proxy.py b/mitmproxy/proxy/layers/http/_upstream_proxy.py index cdbde2d67d..3220cf9307 100644 --- a/mitmproxy/proxy/layers/http/_upstream_proxy.py +++ b/mitmproxy/proxy/layers/http/_upstream_proxy.py @@ -1,13 +1,17 @@ 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 @@ -26,16 +30,16 @@ def __init__( @classmethod def make(cls, ctx: context.Context, send_connect: bool) -> tunnel.LayerStack: - spec = ctx.server.via - assert spec - assert spec.scheme in ("http", "https") + assert ctx.server.via + scheme, address = ctx.server.via + assert scheme in ("http", "https") - http_proxy = connection.Server(spec.address) + http_proxy = connection.Server(address=address) stack = tunnel.LayerStack() - if spec.scheme == "https": + if scheme == "https": http_proxy.alpn_offers = tls.HTTP1_ALPNS - http_proxy.sni = spec.address[0] + http_proxy.sni = address[0] stack /= tls.ServerTLSLayer(ctx, http_proxy) stack /= cls(ctx, http_proxy, send_connect) @@ -46,7 +50,9 @@ 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() + ) flow.request = http.Request( host=self.conn.address[0], port=self.conn.address[1], @@ -67,17 +73,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: - response_head = [ - bytes(x) for x in response_head - ] # TODO: Make url.parse compatible with bytearrays try: - response = http1.read_response_head(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}") @@ -90,7 +93,7 @@ def receive_handshake_data( else: proxyaddr = human.format_address(self.tunnel_connection.address) raw_resp = b"\n".join(response_head) - yield commands.Log(f"{proxyaddr}: {raw_resp!r}", level="debug") + yield commands.Log(f"{proxyaddr}: {raw_resp!r}", DEBUG) return ( False, f"Upstream proxy {proxyaddr} refused HTTP CONNECT request: {response.status_code} {response.reason}", diff --git a/mitmproxy/proxy/layers/modes.py b/mitmproxy/proxy/layers/modes.py index cf9bf960a5..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, platform -from mitmproxy.net import server_spec -from mitmproxy.proxy import commands, events, layer +from mitmproxy 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.layers import 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) @@ -20,15 +29,24 @@ def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: yield from child_layer.handle_event(event) +class HttpUpstreamProxy(layer.Layer): + @expect(events.Start) + def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: + child_layer = layer.NextLayer(self.context) + self._handle_event = child_layer.handle_event + yield from child_layer.handle_event(event) + + class DestinationKnown(layer.Layer, metaclass=ABCMeta): """Base layer for layers that gather connection destination info and then delegate.""" 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 + and self.context.server.transport_protocol == "tcp" ): err = yield commands.OpenConnection(self.context.server) if err: @@ -47,15 +65,21 @@ def done(self, _) -> layer.CommandGenerator[None]: class ReverseProxy(DestinationKnown): @expect(events.Start) def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: - spec = server_spec.parse_with_mode(self.context.options.mode)[1] + spec = self.context.client.proxy_mode + assert isinstance(spec, ReverseMode) self.context.server.address = spec.address - if spec.scheme not in ("http", "tcp"): - if not self.context.options.keep_host_header: - self.context.server.sni = spec.address[0] - self.child_layer = tls.ServerTLSLayer(self.context) - else: - self.child_layer = layer.NextLayer(self.context) + 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: @@ -65,15 +89,8 @@ 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 platform.original_addr is not None - socket = yield commands.GetSocket(self.context.client) - try: - self.context.server.address = platform.original_addr(socket) - except Exception as e: - yield commands.Log(f"Transparent mode failure: {e!r}") - + assert self.context.server.address, "No server address set." self.child_layer = layer.NextLayer(self.context) - err = yield from self.finish_start() if err: yield commands.CloseConnection(self.context.client) @@ -119,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( @@ -147,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 @@ -187,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 @@ -218,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.py b/mitmproxy/proxy/layers/quic.py new file mode 100644 index 0000000000..b3428fd2d7 --- /dev/null +++ b/mitmproxy/proxy/layers/quic.py @@ -0,0 +1,1256 @@ +from __future__ import annotations + +import time +from collections.abc import Callable +from dataclasses import dataclass +from dataclasses import field +from logging import DEBUG +from logging import ERROR +from logging import WARNING +from ssl import VerifyMode + +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 QuicConnectionError +from aioquic.quic.connection import QuicConnectionState +from aioquic.quic.connection import QuicErrorCode +from aioquic.quic.connection import stream_is_client_initiated +from aioquic.quic.connection import stream_is_unidirectional +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 aioquic.quic.packet import QuicProtocolVersion +from aioquic.tls import CipherSuite +from aioquic.tls import HandshakeType +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 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.modes import TransparentProxy +from mitmproxy.proxy.layers.tcp import TCPLayer +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 ClientHello +from mitmproxy.tls import ClientHelloData +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 + + +@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.""" + + +@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.""" + + +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 + + +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 StopQuicStream(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 + + +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 + + +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, + ) + + +@dataclass +class QuicClientHello(Exception): + """Helper error only used in `quic_parse_client_hello`.""" + + data: bytes + + +def quic_parse_client_hello(data: bytes) -> ClientHello: + """Helper function that parses a client hello packet.""" + + # ensure the first packet is indeed the initial one + buffer = QuicBuffer(data=data) + 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="", + ), + 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: + quic.receive_datagram(data, ("0.0.0.0", 0), now=0) + 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 + raise ValueError("No ClientHello returned.") + + +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, ignore: 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 + + # ignored connections will be assigned a TCPLayer immediately + super().__init__(context) + self.child_layer = ( + TCPLayer(context, ignore=True) + if ignore + 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. + """ + + ignore: bool + """Indicates whether traffic should be routed as-is.""" + 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, ignore: bool = False) -> None: + super().__init__(context) + self.ignore = ignore + self.datagram_layer = ( + UDPLayer(self.context.fork(), ignore=True) + if ignore + 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(), self.ignore, 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 = 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 StopQuicStream( + 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]: + yield from () + + +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 + timer = self._wakeup_commands.pop(event.command) + if self.quic._state is not QuicConnectionState.TERMINATED: + self.quic.handle_timer(now=max(timer, self._time())) + 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): + self.quic.reset_stream(command.stream_id, command.error_code) + elif isinstance(command, StopQuicStream): + # 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 + for data, addr in self.quic.datagrams_to_send(now=self._time()): + assert addr == self.conn.peername + yield commands.SendData(self.tunnel_connection, data) + + # request a new wakeup if all pending requests trigger at a later time + timer = self.quic.get_timer() + if timer is not None and not any( + existing <= timer for existing in self._wakeup_commands.values() + ): + command = commands.RequestWakeup(timer - self._time()) + 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 = "QUIC" + + # 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.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.""" + + 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 + ) + + def start_handshake(self) -> layer.CommandGenerator[None]: + yield from () + + def receive_handshake_data( + self, data: bytes + ) -> layer.CommandGenerator[tuple[bool, str | None]]: + if isinstance(self.context.layers[0], TransparentProxy): # pragma: no cover + yield commands.Log( + f"Swallowing QUIC handshake because HTTP/3 does not support transparent mode yet.", + DEBUG, + ) + return False, 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 + supported_versions = [ + version.value + for version in QuicProtocolVersion + if version is not QuicProtocolVersion.NEGOTIATION + ] + if header.version is not None and header.version not in supported_versions: + yield commands.SendData( + self.tunnel_connection, + encode_quic_version_negotiation( + source_cid=header.destination_cid, + destination_cid=header.source_cid, + supported_versions=supported_versions, + ), + ) + 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()})", + ) + + # extract the client hello + try: + client_hello = quic_parse_client_hello(data) + except ValueError as e: + return False, f"Cannot parse ClientHello: {str(e)} ({data.hex()})" + + # 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()) + yield from parent_layer.handle_event( + events.DataReceived(self.context.client, data) + ) + 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 + return (yield from super().receive_handshake_data(data)) + + 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 + ) 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 580a39bbb5..ab8ea154ce 100644 --- a/mitmproxy/proxy/layers/tls.py +++ b/mitmproxy/proxy/layers/tls.py @@ -1,31 +1,32 @@ import struct import time +from collections.abc import Iterator from dataclasses import dataclass -from typing import Iterator, Literal, 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.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 -from mitmproxy.tls import ClientHello, ClientHelloData, TlsData +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. @@ -37,7 +38,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: @@ -51,7 +52,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. @@ -66,7 +67,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. @@ -88,6 +89,73 @@ def parse_client_hello(data: bytes) -> Optional[ClientHello]: return None +def dtls_handshake_record_contents(data: bytes) -> Iterator[bytes]: + """ + Returns a generator that yields the bytes contained in each handshake record. + This will raise an error on the first non-handshake record, so fully exhausting this + generator is a bad idea. + """ + offset = 0 + while True: + # DTLS includes two new fields, totaling 8 bytes, between Version and Length + if len(data) < offset + 13: + return + record_header = data[offset : offset + 13] + 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] + if record_size == 0: + raise ValueError("Record must not be empty.") + offset += 13 + + if len(data) < offset + record_size: + return + record_body = data[offset : offset + record_size] + yield record_body + offset += record_size + + +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. + """ + client_hello = b"" + for d in dtls_handshake_record_contents(data): + 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 + ) + if len(client_hello) >= client_hello_size: + return client_hello[:client_hello_size] + return None + + +def dtls_parse_client_hello(data: bytes) -> ClientHello | None: + """ + Check if the supplied bytes contain a full ClientHello message, + and if so, parse it. + + Returns: + - A ClientHello object on success + - None, if the TLS record is not complete + + Raises: + - A ValueError, if the passed ClientHello is invalid + """ + # Check if ClientHello is complete + client_hello = get_dtls_client_hello(data) + if client_hello: + try: + return ClientHello(client_hello[12:], dtls=True) + except EOFError as e: + raise ValueError("Invalid ClientHello") from e + return None + + HTTP1_ALPNS = (b"http/1.1", b"http/1.0", b"http/0.9") HTTP_ALPNS = (b"h2",) + HTTP1_ALPNS @@ -167,7 +235,7 @@ class TlsFailedServerHook(StartHook): data: TlsData -class _TLSLayer(tunnel.TunnelLayer): +class TLSLayer(tunnel.TunnelLayer): tls: SSL.Connection = None # type: ignore """The OpenSSL connection object""" @@ -181,19 +249,29 @@ 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): + return self.conn.transport_protocol == "udp" + + @property + def proto_name(self): + return "DTLS" if self.is_dtls else "TLS" def start_tls(self) -> layer.CommandGenerator[None]: assert not self.tls - tls_start = TlsData(self.conn, self.context) + tls_start = TlsData(self.conn, self.context, is_dtls=self.is_dtls) if self.conn == self.context.client: yield TlsStartClientHook(tls_start) else: yield TlsStartServerHook(tls_start) if not tls_start.ssl_conn: yield commands.Log( - "No TLS context was provided, failing connection.", "error" + f"No {self.proto_name} context was provided, failing connection.", ERROR ) yield commands.CloseConnection(self.conn) return @@ -211,7 +289,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) @@ -234,7 +312,9 @@ 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"), @@ -286,7 +366,7 @@ def receive_handshake_data( self.conn.tls_version = self.tls.get_protocol_version_name() if self.debug: yield commands.Log( - f"{self.debug}[tls] tls established: {self.conn}", "debug" + f"{self.debug}[tls] tls established: {self.conn}", DEBUG ) if self.conn == self.context.client: yield TlsEstablishedClientHook( @@ -328,9 +408,8 @@ def receive_data(self, data: bytes) -> layer.CommandGenerator[None]: # which upon mistrusting a certificate still completes the handshake # and then sends an alert in the next packet. At this point we have unfortunately # already fired out `tls_established_client` hook. - yield commands.Log(f"TLS Error: {e}", "warn") + yield commands.Log(f"TLS Error: {e}", WARNING) break - if plaintext: yield from self.event_to_child( events.DataReceived(self.conn, bytes(plaintext)) @@ -338,9 +417,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}", level="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]: @@ -357,21 +434,21 @@ 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): +class ServerTLSLayer(TLSLayer): """ This layer establishes TLS for a single server connection. """ 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]: @@ -408,11 +485,11 @@ def event_to_child(self, event: events.Event) -> layer.CommandGenerator[None]: yield from super().event_to_child(event) def on_handshake_error(self, err: str) -> layer.CommandGenerator[None]: - yield commands.Log(f"Server TLS handshake failed. {err}", level="warn") + yield commands.Log(f"Server TLS handshake failed. {err}", level=WARNING) yield from super().on_handshake_error(err) -class ClientTLSLayer(_TLSLayer): +class ClientTLSLayer(TLSLayer): """ This layer establishes TLS on a single client connection. @@ -461,12 +538,15 @@ 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) try: - client_hello = parse_client_hello(self.recv_buffer) + if self.is_dtls: + client_hello = dtls_parse_client_hello(self.recv_buffer) + else: + client_hello = parse_client_hello(self.recv_buffer) except ValueError: return False, f"Cannot parse ClientHello: {self.recv_buffer.hex()}" @@ -484,14 +564,17 @@ 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 ) - self.child_layer = tcp.TCPLayer(self.context, ignore=True) + if self.is_dtls: + self.child_layer = udp.UDPLayer(self.context, ignore=True) + else: + self.child_layer = tcp.TCPLayer(self.context, ignore=True) yield from self.event_to_child( events.DataReceived(self.context.client, bytes(self.recv_buffer)) ) @@ -504,8 +587,10 @@ def receive_handshake_data( err = yield from self.start_server_tls() if err: yield commands.Log( - f"Unable to establish TLS connection with server ({err}). " - f"Trying to establish TLS with client anyway." + f"Unable to establish {self.proto_name} connection with server ({err}). " + f"Trying to establish {self.proto_name} 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." ) yield from self.start_tls() @@ -516,13 +601,13 @@ 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. """ if not self.server_tls_available: - return "No server TLS available." + return f"No server {self.proto_name} available." err = yield commands.OpenConnection(self.context.server) return err @@ -531,7 +616,7 @@ def on_handshake_error(self, err: str) -> layer.CommandGenerator[None]: dest = self.conn.sni else: dest = human.format_address(self.context.server.address) - level: Literal["warn", "info"] = "warn" + level: int = WARNING if err.startswith("Cannot parse ClientHello"): pass elif ( @@ -556,7 +641,7 @@ def on_handshake_error(self, err: str) -> layer.CommandGenerator[None]: f"The client disconnected during the handshake. If this happens consistently for {dest}, " f"this may indicate that the client does not trust the proxy's certificate." ) - level = "info" + level = INFO elif err == "connection closed early": pass else: @@ -568,10 +653,12 @@ 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): +class MockTLSLayer(TLSLayer): """Mock layer to disable actual TLS and use cleartext in tests. Use like so: @@ -579,4 +666,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 new file mode 100644 index 0000000000..fd7e65227f --- /dev/null +++ b/mitmproxy/proxy/layers/udp.py @@ -0,0 +1,134 @@ +from dataclasses import dataclass + +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 + + +@dataclass +class UdpStartHook(StartHook): + """ + A UDP connection has started. + """ + + flow: udp.UDPFlow + + +@dataclass +class UdpMessageHook(StartHook): + """ + A UDP connection has received a message. The most recent message + will be flow.messages[-1]. The message is user-modifiable. + """ + + flow: udp.UDPFlow + + +@dataclass +class UdpEndHook(StartHook): + """ + A UDP connection has ended. + """ + + flow: udp.UDPFlow + + +@dataclass +class UdpErrorHook(StartHook): + """ + A UDP error has occurred. + + Every UDP flow will receive either a udp_error or a udp_end event, but not both. + """ + + flow: udp.UDPFlow + + +class UdpMessageInjected(MessageInjected[udp.UDPMessage]): + """ + The user has injected a custom UDP message. + """ + + +class UDPLayer(layer.Layer): + """ + Simple UDP layer that just relays messages right now. + """ + + flow: udp.UDPFlow | None + + def __init__(self, context: Context, ignore: bool = False): + super().__init__(context) + if ignore: + self.flow = None + else: + self.flow = udp.UDPFlow(self.context.client, self.context.server, True) + + @expect(events.Start) + def start(self, _) -> layer.CommandGenerator[None]: + if self.flow: + yield UdpStartHook(self.flow) + + if self.context.server.timestamp_start is None: + err = yield commands.OpenConnection(self.context.server) + if err: + if self.flow: + self.flow.error = flow.Error(str(err)) + yield UdpErrorHook(self.flow) + yield commands.CloseConnection(self.context.client) + self._handle_event = self.done + return + self._handle_event = self.relay_messages + + _handle_event = start + + @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( + self.context.client + if event.message.from_client + else self.context.server, + event.message.content, + ) + + assert isinstance(event, events.ConnectionEvent) + + from_client = event.connection == self.context.client + send_to: Connection + if from_client: + send_to = self.context.server + else: + send_to = self.context.client + + if isinstance(event, events.DataReceived): + if self.flow: + udp_message = udp.UDPMessage(from_client, event.data) + self.flow.messages.append(udp_message) + yield UdpMessageHook(self.flow) + yield commands.SendData(send_to, udp_message.content) + else: + 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 + else: + raise AssertionError(f"Unexpected event: {event}") + + @expect(events.DataReceived, events.ConnectionClosed, UdpMessageInjected) + def done(self, _) -> layer.CommandGenerator[None]: + yield from () 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 new file mode 100644 index 0000000000..6e57a5f77c --- /dev/null +++ b/mitmproxy/proxy/mode_servers.py @@ -0,0 +1,548 @@ +""" +This module defines "server instances", which manage +the TCP/UDP servers spawned by mitmproxy as specified by the proxy mode. + +Example: + + mode = ProxyMode.parse("reverse:https://example.com") + inst = ServerInstance.make(mode, manager_that_handles_callbacks) + 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 +from abc import abstractmethod +from contextlib import contextmanager +from pathlib import Path +from typing import cast +from typing import ClassVar +from typing import Generic +from typing import get_args +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 +from mitmproxy.net import udp +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 + +logger = logging.getLogger(__name__) + + +class ProxyConnectionHandler(server.LiveConnectionHandler): + master: Master + + def __init__(self, master, r, w, options, mode): + self.master = master + super().__init__(r, w, options, mode) + self.log_prefix = f"{human.format_address(self.client.peername)}: " + + async def handle_hook(self, hook: commands.StartHook) -> None: + with self.timeout_watchdog.disarm(): + # We currently only support single-argument hooks. + (data,) = hook.args() + await self.master.addons.handle_lifecycle(hook) + if isinstance(data, flow.Flow): + await data.wait_for_resume() # pragma: no cover + + +M = TypeVar("M", bound=mode_specs.ProxyMode) + + +class ServerManager(typing.Protocol): + # 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 | 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 + + 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] # type: ignore + if not isinstance(mode, TypeVar): + assert issubclass(mode, mode_specs.ProxyMode) + assert mode.type_name not in ServerInstance.__modes + ServerInstance.__modes[mode.type_name] = cls + + @classmethod + def make( + cls, + mode: mode_specs.ProxyMode | str, + manager: ServerManager, + ) -> Self: + if isinstance(mode, str): + mode = mode_specs.ProxyMode.parse(mode) + inst = ServerInstance.__modes[mode.type_name](mode, manager) + + if not isinstance(inst, cls): + raise ValueError(f"{mode!r} is not a spec for a {cls.__name__} server.") + + return inst + + @property + @abstractmethod + def is_running(self) -> bool: + pass + + 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: + pass + + @property + @abstractmethod + def listen_addrs(self) -> tuple[Address, ...]: + pass + + @abstractmethod + def make_top_layer(self, context: Context) -> Layer: + pass + + def to_json(self) -> dict: + return { + "type": self.mode.type_name, + "description": self.mode.description, + "full_spec": self.mode.full_spec, + "is_running": self.is_running, + "last_exception": str(self.last_exception) if self.last_exception else None, + "listen_addrs": self.listen_addrs, + } + + async def handle_tcp_connection( + self, + reader: asyncio.StreamReader | mitmproxy_rs.TcpStream, + writer: asyncio.StreamWriter | mitmproxy_rs.TcpStream, + ) -> None: + 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): + 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}") + return + else: + handler.layer.context.client.sockname = original_dst + handler.layer.context.server.address = original_dst + elif isinstance(self.mode, (mode_specs.WireGuardMode, mode_specs.LocalMode)): + handler.layer.context.server.address = writer.get_extra_info( + "destination_address", handler.layer.context.client.sockname + ) + + with self.manager.register_connection(handler.layer.context.client.id, handler): + await handler.handle_client() + + def handle_udp_datagram( + self, + transport: asyncio.DatagramTransport | mitmproxy_rs.DatagramTransport, + data: bytes, + remote_addr: Address, + local_addr: Address, + ) -> None: + # temporary workaround: we don't have a client uuid here. + connection_id = (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 + ) + 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, mode_specs.LocalMode)): + 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 + t = asyncio.create_task(self.handle_udp_connection(connection_id, handler)) + # assign it somewhere so that it does not get garbage-collected. + handler._handle_udp_task = t # type: ignore + 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): + await handler.handle_client() + + +class AsyncioServerInstance(ServerInstance[M], metaclass=ABCMeta): + _servers: list[asyncio.Server | udp.UdpServer] + + def __init__(self, *args, **kwargs) -> None: + self._servers = [] + super().__init__(*args, **kwargs) + + @property + def is_running(self) -> bool: + return bool(self._servers) + + @property + def listen_addrs(self) -> tuple[Address, ...]: + return tuple( + sock.getsockname() for serv in self._servers for sock in serv.sockets + ) + + 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) + try: + self._servers = await self.listen(host, port) + except OSError as 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 + 2}`." + raise OSError(e.errno, message, e.filename) from e + + async def _stop(self) -> None: + assert self._servers + try: + 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 | udp.UdpServer]: + if self.mode.transport_protocol == "tcp": + # 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), + # so we try to cover the 99% case and then give up and fall back to what asyncio does. + if port == 0: + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(("", 0)) + fixed_port = s.getsockname()[1] + s.close() + return [ + await asyncio.start_server( + self.handle_tcp_connection, host, fixed_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 (non-dual-stack) socket, so we spawn two servers instead. + if not host: + ipv4 = await udp.start_server( + self.handle_udp_datagram, + "0.0.0.0", + port, + ) + try: + ipv6 = await udp.start_server( + self.handle_udp_datagram, + "::", + port or ipv4.sockets[0].getsockname()[1], + ) + except Exception: # pragma: no cover + logger.debug("Failed to listen on '::', listening on IPv4 only.") + return [ipv4] + else: # pragma: no cover + return [ipv4, ipv6] + return [ + await udp.start_server( + self.handle_udp_datagram, + host, + port, + ) + ] + else: + raise AssertionError(self.mode.transport_protocol) + + +class WireGuardServerInstance(ServerInstance[mode_specs.WireGuardMode]): + _server: mitmproxy_rs.WireGuardServer | None = None + + server_key: str + client_key: str + + def make_top_layer(self, context: Context) -> Layer: + return layers.modes.TransparentProxy(context) + + @property + def is_running(self) -> bool: + return self._server is not 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) + + if self.mode.data: + conf_path = Path(self.mode.data).expanduser() + else: + conf_path = Path(ctx.options.confdir).expanduser() / "wireguard.conf" + + if not conf_path.exists(): + conf_path.parent.mkdir(parents=True, exist_ok=True) + conf_path.write_text( + json.dumps( + { + "server_key": mitmproxy_rs.genkey(), + "client_key": mitmproxy_rs.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 = mitmproxy_rs.pubkey(self.client_key) + _ = mitmproxy_rs.pubkey(self.server_key) + + self._server = await mitmproxy_rs.start_wireguard_server( + host, + port, + self.server_key, + [p], + self.wg_handle_tcp_connection, + self.handle_udp_datagram, + ) + + conf = self.client_conf() + assert conf + 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() + port = self.mode.listen_port(ctx.options.listen_port) + return textwrap.dedent( + f""" + [Interface] + PrivateKey = {self.client_key} + Address = 10.0.0.1/32 + DNS = 10.0.0.53 + + [Peer] + PublicKey = {mitmproxy_rs.pubkey(self.server_key)} + AllowedIPs = 0.0.0.0/0 + Endpoint = {host}:{port} + """ + ).strip() + + def to_json(self) -> dict: + return {"wireguard_conf": self.client_conf(), **super().to_json()} + + async def _stop(self) -> None: + assert self._server is not None + try: + self._server.close() + await self._server.wait_closed() + finally: + self._server = None + + async def wg_handle_tcp_connection(self, stream: mitmproxy_rs.TcpStream) -> None: + await self.handle_tcp_connection(stream, stream) + + +class LocalRedirectorInstance(ServerInstance[mode_specs.LocalMode]): + _server: ClassVar[mitmproxy_rs.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 is_running(self) -> bool: + return self._instance is not None + + def make_top_layer(self, context: Context) -> Layer: + return layers.modes.TransparentProxy(context) + + @classmethod + async def redirector_handle_tcp_connection( + cls, stream: mitmproxy_rs.TcpStream + ) -> None: + if cls._instance is not None: + await cls._instance.handle_tcp_connection(stream, stream) + + @classmethod + def redirector_handle_datagram( + cls, + transport: mitmproxy_rs.DatagramTransport, + data: bytes, + remote_addr: Address, + local_addr: Address, + ) -> None: + if cls._instance is not None: + cls._instance.handle_udp_datagram( + transport=transport, + data=data, + remote_addr=remote_addr, + local_addr=local_addr, + ) + + async def _start(self) -> None: + if self._instance: + raise RuntimeError("Cannot spawn more than one local redirector.") + + if self.mode.data.startswith("!"): + spec = f"{self.mode.data},{os.getpid()}" + elif self.mode.data: + spec = self.mode.data + 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.start_local_redirector( + cls.redirector_handle_tcp_connection, + cls.redirector_handle_datagram, + ) + 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]): + def make_top_layer(self, context: Context) -> Layer: + return layers.modes.HttpProxy(context) + + +class UpstreamInstance(AsyncioServerInstance[mode_specs.UpstreamMode]): + def make_top_layer(self, context: Context) -> Layer: + return layers.modes.HttpUpstreamProxy(context) + + +class TransparentInstance(AsyncioServerInstance[mode_specs.TransparentMode]): + def make_top_layer(self, context: Context) -> Layer: + return layers.modes.TransparentProxy(context) + + +class ReverseInstance(AsyncioServerInstance[mode_specs.ReverseMode]): + def make_top_layer(self, context: Context) -> Layer: + return layers.modes.ReverseProxy(context) + + +class Socks5Instance(AsyncioServerInstance[mode_specs.Socks5Mode]): + def make_top_layer(self, context: Context) -> Layer: + return layers.modes.Socks5Proxy(context) + + +class DnsInstance(AsyncioServerInstance[mode_specs.DnsMode]): + def make_top_layer(self, context: Context) -> Layer: + return layers.DNSLayer(context) + + +# 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 new file mode 100644 index 0000000000..f9a7542142 --- /dev/null +++ b/mitmproxy/proxy/mode_specs.py @@ -0,0 +1,309 @@ +""" +This module is responsible for parsing proxy mode specifications such as +`"regular"`, `"reverse:https://example.com"`, or `"socks5@1234"`. The general syntax is + + mode [: mode_configuration] [@ [listen_addr:]listen_port] + +For a full example, consider `reverse:https://example.com@127.0.0.1:443`. +This would spawn a reverse proxy on port 443 bound to localhost. +The mode is `reverse`, and the mode data is `https://example.com`. +Examples: + + mode = ProxyMode.parse("regular@1234") + assert mode.listen_port == 1234 + assert isinstance(mode, RegularMode) + + ProxyMode.parse("reverse:example.com@invalid-port") # ValueError + + RegularMode.parse("regular") # ok + RegularMode.parse("socks5") # ValueError + +""" +from __future__ import annotations + +import dataclasses +import sys +from abc import ABCMeta +from abc import abstractmethod +from dataclasses import dataclass +from functools import cache +from typing import ClassVar +from typing import Literal + +import mitmproxy_rs + +from mitmproxy.coretypes.serializable import Serializable +from mitmproxy.net import server_spec + +if sys.version_info < (3, 11): + from typing_extensions import Self # pragma: no cover +else: + from typing import Self + + +@dataclass(frozen=True) # type: ignore +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 + """The (raw) mode data, i.e. the part after the mode name.""" + custom_listen_host: str | None + """A custom listen host, if specified in the spec.""" + 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__ + """The unique name for this proxy mode, e.g. "regular" or "reverse".""" + __types: ClassVar[dict[str, type[ProxyMode]]] = {} + + def __init_subclass__(cls, **kwargs): + cls.type_name = cls.__name__.removesuffix("Mode").lower() + assert cls.type_name not in ProxyMode.__types + ProxyMode.__types[cls.type_name] = cls + + def __repr__(self): + return f"ProxyMode.parse({self.full_spec!r})" + + @abstractmethod + def __post_init__(self) -> None: + """Validation of data happens here.""" + + @property + @abstractmethod + def description(self) -> str: + """The mode description that will be used in server logs and UI.""" + + @property + def default_port(self) -> int: + """ + Default listen port of servers for this mode, see `ProxyMode.listen_port()`. + """ + return 8080 + + @property + @abstractmethod + def transport_protocol(self) -> Literal["tcp", "udp"] | None: + """The transport protocol used by this mode's server.""" + + @classmethod + @cache + def parse(cls, spec: str) -> Self: + """ + Parse a proxy mode specification and return the corresponding `ProxyMode` instance. + """ + head, _, listen_at = spec.rpartition("@") + if not head: + head = listen_at + listen_at = "" + + mode, _, data = head.partition(":") + + if listen_at: + if ":" in listen_at: + host, _, port_str = listen_at.rpartition(":") + else: + host = None + port_str = listen_at + try: + port = int(port_str) + if port < 0 or 65535 < port: + raise ValueError + except ValueError: + raise ValueError(f"invalid port: {port_str}") + else: + host = None + port = None + + try: + mode_cls = ProxyMode.__types[mode.lower()] + except KeyError: + raise ValueError(f"unknown mode") + + if not issubclass(mode_cls, cls): + 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 + ) + + def listen_host(self, default: str | None = None) -> str: + """ + Return the address a server for this mode should listen on. This can be either directly + specified in the spec or taken from a user-configured global default (`options.listen_host`). + By default, return an empty string to listen on all hosts. + """ + if self.custom_listen_host is not None: + return self.custom_listen_host + elif default is not None: + return default + else: + return "" + + def listen_port(self, default: int | None = None) -> int: + """ + 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`. + """ + if self.custom_listen_port is not None: + return self.custom_listen_port + elif default is not None: + return default + else: + return self.default_port + + @classmethod + def from_state(cls, state): + return ProxyMode.parse(state) + + def get_state(self): + return self.full_spec + + def set_state(self, state): + if state != self.full_spec: + raise dataclasses.FrozenInstanceError("Proxy modes are immutable.") + + +TCP: Literal["tcp", "udp"] = "tcp" +UDP: Literal["tcp", "udp"] = "udp" + + +def _check_empty(data): + if data: + raise ValueError("mode takes no arguments") + + +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 + + def __post_init__(self) -> None: + _check_empty(self.data) + + +class TransparentMode(ProxyMode): + """A transparent proxy, see https://docs.mitmproxy.org/dev/howto-transparent/""" + + description = "Transparent Proxy" + transport_protocol = TCP + + def __post_init__(self) -> None: + _check_empty(self.data) + + +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"] + address: tuple[str, int] + + # noinspection PyDataclass + def __post_init__(self) -> None: + scheme, self.address = server_spec.parse(self.data, default_scheme="http") + if scheme != "http" and scheme != "https": + raise ValueError("invalid upstream proxy scheme") + self.scheme = scheme + + +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", "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 ("http3", "dtls", "udp", "dns", "quic"): + self.transport_protocol = UDP + self.description = f"{self.description} to {self.data}" + + @property + def default_port(self) -> int: + if self.scheme == "dns": + return 53 + return super().default_port + + +class Socks5Mode(ProxyMode): + """A SOCKSv5 proxy.""" + + description = "SOCKS v5 proxy" + default_port = 1080 + transport_protocol = TCP + + def __post_init__(self) -> None: + _check_empty(self.data) + + +class DnsMode(ProxyMode): + """A DNS server.""" + + description = "DNS server" + default_port = 53 + transport_protocol = UDP + + 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 = None + + def __post_init__(self) -> None: + # should not raise + mitmproxy_rs.LocalRedirector.describe_spec(self.data) + + +class OsProxyMode(ProxyMode): # pragma: no cover + """Deprecated alias for LocalMode""" + + description = "Deprecated alias for LocalMode" + transport_protocol = 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 20fd4233bf..c990261924 100644 --- a/mitmproxy/proxy/server.py +++ b/mitmproxy/proxy/server.py @@ -9,24 +9,41 @@ 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_rs from OpenSSL import SSL -from mitmproxy import http, options as moptions, tls + +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.net import udp +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, 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__) + class TimeoutWatchdog: last_activity: float @@ -72,9 +89,13 @@ def disarm(self): @dataclass class ConnectionIO: - handler: Optional[asyncio.Task] = None - reader: Optional[Union[asyncio.StreamReader, udp.DatagramReader]] = None - writer: Optional[Union[asyncio.StreamWriter, udp.DatagramWriter]] = None + handler: asyncio.Task | None = None + reader: None | ( + asyncio.StreamReader | udp.DatagramReader | mitmproxy_rs.TcpStream + ) = None + writer: None | ( + asyncio.StreamWriter | udp.DatagramWriter | mitmproxy_rs.TcpStream + ) = None class ConnectionHandler(metaclass=abc.ABCMeta): @@ -82,14 +103,16 @@ class ConnectionHandler(metaclass=abc.ABCMeta): timeout_watchdog: TimeoutWatchdog client: Client max_conns: collections.defaultdict[Address, asyncio.Semaphore] - layer: layer.Layer + 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 @@ -127,6 +150,12 @@ async def handle_client(self) -> None: 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"connection handler has crashed: {e}", + logging.ERROR, + exc_info=(type(e), e, e.__traceback__), + ) watch.cancel() while self.wakeup_timer: @@ -138,14 +167,14 @@ async def handle_client(self) -> None: await self.handle_hook(server_hooks.ClientDisconnectedHook(self.client)) if self.transports: - self.log("closing transports...", "debug") + self.log("closing transports...", logging.DEBUG) for io in self.transports.values(): if io.handler: io.handler.cancel("client disconnected") await asyncio.wait( [x.handler for x in self.transports.values() if x.handler] ) - self.log("transports closed!", "debug") + self.log("transports closed!", logging.DEBUG) async def open_connection(self, command: commands.OpenConnection) -> None: if not command.connection.address: @@ -171,8 +200,8 @@ async def open_connection(self, command: commands.OpenConnection) -> None: return async with self.max_conns[command.connection.address]: - reader: Union[asyncio.StreamReader, udp.DatagramReader] - writer: Union[asyncio.StreamWriter, udp.DatagramWriter] + reader: asyncio.StreamReader | udp.DatagramReader + writer: asyncio.StreamWriter | udp.DatagramWriter try: command.connection.timestamp_start = time.time() if command.connection.transport_protocol == "tcp": @@ -298,7 +327,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() @@ -321,15 +350,23 @@ async def hook_task(self, hook: commands.StartHook) -> None: async def handle_hook(self, hook: commands.StartHook) -> None: pass - def log(self, message: str, level: str = "info") -> None: - print(message) + 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}, 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( @@ -354,26 +391,27 @@ def server_event(self, event: events.Event) -> None: elif isinstance(command, commands.SendData): writer = self.transports[command.connection].writer assert writer - writer.write(command.data) - elif isinstance(command, commands.CloseConnection): + 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.GetSocket): - writer = self.transports[command.connection].writer - assert writer - socket = writer.get_extra_info("socket") - self.server_event(events.GetSocketCompleted(command, socket)) + elif isinstance(command, commands.CloseConnection): + self.close_connection(command.connection, False) elif isinstance(command, commands.StartHook): - asyncio_utils.create_task( + 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!\n{traceback.format_exc()}", level="error") + self.log(f"mitmproxy has crashed!", logging.ERROR, exc_info=True) def close_connection( self, connection: Connection, half_close: bool = False @@ -381,11 +419,12 @@ def close_connection( if half_close: if not connection.state & ConnectionState.CAN_WRITE: return - self.log(f"half-closing {connection}", "debug") + self.log(f"half-closing {connection}", logging.DEBUG) try: writer = self.transports[connection].writer assert writer - writer.write_eof() + if not writer.is_closing(): + writer.write_eof() except OSError: # if we can't write to the socket anymore we presume it completely dead. connection.state = ConnectionState.CLOSED @@ -403,14 +442,26 @@ def close_connection( class LiveConnectionHandler(ConnectionHandler, metaclass=abc.ABCMeta): def __init__( self, - reader: asyncio.StreamReader, - writer: asyncio.StreamWriter, + reader: asyncio.StreamReader | mitmproxy_rs.TcpStream, + writer: asyncio.StreamWriter | mitmproxy_rs.TcpStream, options: moptions.Options, + mode: mode_specs.ProxyMode, ) -> None: + # mitigate impact of https://github.com/mitmproxy/mitmproxy/issues/6204: + # For UDP, we don't get an accurate sockname from the transport when binding to all interfaces, + # however we would later need that to generate matching certificates. + # Until this is fixed properly, we can at least make the localhost case work. + sockname = writer.get_extra_info("sockname") + if sockname == "::": + sockname = "::1" + elif sockname == "0.0.0.0": + sockname = "127.0.0.1" client = Client( - writer.get_extra_info("peername"), - writer.get_extra_info("sockname"), - time.time(), + peername=writer.get_extra_info("peername"), + sockname=sockname, + timestamp_start=time.time(), + proxy_mode=mode, + state=ConnectionState.OPEN, ) context = Context(client, options) super().__init__(context) @@ -424,18 +475,14 @@ class SimpleConnectionHandler(LiveConnectionHandler): # pragma: no cover hook_handlers: dict[str, Callable] - def __init__(self, reader, writer, options, hooks): - super().__init__(reader, writer, options) + def __init__(self, reader, writer, options, mode, hooks): + super().__init__(reader, writer, options, mode) self.hook_handlers = hooks async def handle_hook(self, hook: commands.StartHook) -> None: if hook.name in self.hook_handlers: self.hook_handlers[hook.name](*hook.args()) - def log(self, message: str, level: str = "info"): - if "Hook" not in message: - pass # print(message, file=sys.stderr if level in ("error", "warn") else sys.stdout) - if __name__ == "__main__": # pragma: no cover # simple standalone implementation for testing. @@ -459,7 +506,6 @@ def log(self, message: str, level: str = "info"): to the reverse proxy target. """, ) - opts.mode = "reverse:http://127.0.0.1:3000/" async def handle(reader, writer): layer_stack = [ @@ -472,9 +518,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: @@ -517,6 +563,7 @@ def tls_start_server(tls_start: tls.TlsData): reader, writer, opts, + mode_specs.ProxyMode.parse("reverse:http://127.0.0.1:3000/"), { "next_layer": next_layer, "request": request, diff --git a/mitmproxy/proxy/server_hooks.py b/mitmproxy/proxy/server_hooks.py index 22e1e5418b..a00c6ca195 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 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/script/concurrent.py b/mitmproxy/script/concurrent.py index 9d9546568d..587a65e78e 100644 --- a/mitmproxy/script/concurrent.py +++ b/mitmproxy/script/concurrent.py @@ -2,9 +2,9 @@ This module provides a @concurrent decorator primitive to offload computations from mitmproxy's main master thread. """ - 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 a52bb5eef5..82ee2de304 100644 --- a/mitmproxy/test/taddons.py +++ b/mitmproxy/test/taddons.py @@ -1,59 +1,12 @@ import asyncio -import sys import mitmproxy.master import mitmproxy.options -from mitmproxy import addonmanager, hooks, log from mitmproxy import command from mitmproxy import eventsequence -from mitmproxy.addons import script, core - - -class TestAddons(addonmanager.AddonManager): - def __init__(self, master): - super().__init__(master) - - def trigger(self, event: hooks.Hook): - if isinstance(event, log.AddLogHook): - self.master.logs.append(event.entry) - super().trigger(event) - - -class RecordingMaster(mitmproxy.master.Master): - def __init__(self, *args, **kwargs): - try: - loop = asyncio.get_running_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - super().__init__(*args, **kwargs, event_loop=loop) - self.addons = TestAddons(self) - self.logs = [] - - def dump_log(self, outf=sys.stdout): - for i in self.logs: - print(f"{i.level}: {i.msg}", file=outf) - - def has_log(self, txt, level=None): - for i in self.logs: - if level and i.level != level: - continue - if txt.lower() in i.msg.lower(): - return True - return False - - async def await_log(self, txt, level=None, timeout=1): - # start with a sleep(0), which lets all other coroutines advance. - # often this is enough to not sleep at all. - await asyncio.sleep(0) - for i in range(int(timeout / 0.01)): - if self.has_log(txt, level): - return True - else: - await asyncio.sleep(0.01) - raise AssertionError(f"Did not find log entry {txt!r} in {self.logs}.") - - def clear(self): - self.logs = [] +from mitmproxy import hooks +from mitmproxy.addons import core +from mitmproxy.addons import script class context: @@ -64,8 +17,13 @@ class context: """ def __init__(self, *addons, options=None, loadcore=True): + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + options = options or mitmproxy.options.Options() - self.master = RecordingMaster(options) + self.master = mitmproxy.master.Master(options, event_loop=loop) self.options = self.master.options if loadcore: diff --git a/mitmproxy/test/tflow.py b/mitmproxy/test/tflow.py index 571a22375d..84654deb7d 100644 --- a/mitmproxy/test/tflow.py +++ b/mitmproxy/test/tflow.py @@ -1,15 +1,20 @@ import uuid -from typing import Optional, Union + +from wsproto.frame_protocol import Opcode from mitmproxy import connection from mitmproxy import dns from mitmproxy import flow from mitmproxy import http from mitmproxy import tcp +from mitmproxy import udp from mitmproxy import websocket -from mitmproxy.test.tutils import tdnsreq, tdnsresp -from mitmproxy.test.tutils import treq, tresp -from wsproto.frame_protocol import Opcode +from mitmproxy.connection import ConnectionState +from mitmproxy.proxy.mode_specs import ProxyMode +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( @@ -35,6 +40,29 @@ def ttcpflow( return f +def tudpflow( + client_conn=True, server_conn=True, messages=True, err=None +) -> udp.UDPFlow: + if client_conn is True: + client_conn = tclient_conn() + if server_conn is True: + server_conn = tserver_conn() + if messages is True: + messages = [ + udp.UDPMessage(True, b"hello", 946681204.2), + udp.UDPMessage(False, b"it's me", 946681204.5), + ] + if err is True: + err = terr() + + f = udp.UDPFlow(client_conn, server_conn) + f.timestamp_created = client_conn.timestamp_start + f.messages = messages + f.error = err + f.live = True + return f + + def twebsocketflow( messages=True, err=None, close_code=None, close_reason="" ) -> http.HTTPFlow: @@ -93,16 +121,17 @@ 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.""" if client_conn is None: client_conn = tclient_conn() + client_conn.proxy_mode = ProxyMode.parse("dns") client_conn.transport_protocol = "udp" if server_conn is None: server_conn = tserver_conn() @@ -129,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.""" @@ -185,57 +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=[], - ) + 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 @@ -269,6 +291,8 @@ def tflows() -> list[flow.Flow]: tflow(ws=True), ttcpflow(), ttcpflow(err=True), + tudpflow(), + tudpflow(err=True), tdnsflow(resp=True), tdnsflow(err=True), ] diff --git a/mitmproxy/tls.py b/mitmproxy/tls.py index f26e34816b..de0aa340e6 100644 --- a/mitmproxy/tls.py +++ b/mitmproxy/tls.py @@ -1,11 +1,11 @@ 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 dtls_client_hello from mitmproxy.contrib.kaitaistruct import tls_client_hello from mitmproxy.net import check from mitmproxy.proxy import context @@ -18,12 +18,17 @@ class ClientHello: _raw_bytes: bytes - def __init__(self, raw_client_hello: bytes): + def __init__(self, raw_client_hello: bytes, dtls: bool = False): """Create a TLS ClientHello object from raw bytes.""" self._raw_bytes = raw_client_hello - self._client_hello = tls_client_hello.TlsClientHello( - KaitaiStream(io.BytesIO(raw_client_hello)) - ) + if dtls: + self._client_hello = dtls_client_hello.DtlsClientHello( + KaitaiStream(io.BytesIO(raw_client_hello)) + ) + else: + self._client_hello = tls_client_hello.TlsClientHello( + KaitaiStream(io.BytesIO(raw_client_hello)) + ) def raw_bytes(self, wrap_in_record: bool = True) -> bytes: """ @@ -37,6 +42,9 @@ def raw_bytes(self, wrap_in_record: bool = True) -> bytes: A future implementation may return not just the exact ClientHello, but also the exact record(s) as seen on the wire. """ + if isinstance(self._client_hello, dtls_client_hello.DtlsClientHello): + raise NotImplementedError + if wrap_in_record: return ( # record layer @@ -59,13 +67,13 @@ 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. """ - if self._client_hello.extensions: - for extension in self._client_hello.extensions.extensions: + if ext := getattr(self._client_hello, "extensions", None): + for extension in ext.extensions: is_valid_sni_extension = ( extension.type == 0x00 and len(extension.body.server_names) == 1 @@ -82,8 +90,8 @@ def alpn_protocols(self) -> list[bytes]: The application layer protocols offered by the client as part of the [ALPN](https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation) TLS extension. """ - if self._client_hello.extensions: - for extension in self._client_hello.extensions.extensions: + if ext := getattr(self._client_hello, "extensions", None): + for extension in ext.extensions: if extension.type == 0x10: return list(x.name for x in extension.body.alpn_protocols) return [] @@ -92,8 +100,8 @@ def alpn_protocols(self) -> list[bytes]: def extensions(self) -> list[tuple[int, bytes]]: """The raw list of extensions in the form of `(extension_type, raw_bytes)` tuples.""" ret = [] - if self._client_hello.extensions: - for extension in self._client_hello.extensions.extensions: + if ext := getattr(self._client_hello, "extensions", None): + for extension in ext.extensions: body = getattr(extension, "_raw_body", extension.body) ret.append((extension.type, body)) return ret @@ -133,8 +141,12 @@ 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. """ + is_dtls: bool = False + """ + If set to `True`, indicates that it is a DTLS event. + """ diff --git a/mitmproxy/tools/cmdline.py b/mitmproxy/tools/cmdline.py index 7f902a88a8..efd3868340 100644 --- a/mitmproxy/tools/cmdline.py +++ b/mitmproxy/tools/cmdline.py @@ -74,7 +74,6 @@ def common_options(parser, opts): opts.make_parser(group, "certs", metavar="SPEC") opts.make_parser(group, "cert_passphrase", metavar="PASS") opts.make_parser(group, "ssl_insecure", short="k") - opts.make_parser(group, "key_size", metavar="KEY_SIZE") # Client replay group = parser.add_argument_group("Client Replay") @@ -84,7 +83,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 @@ -141,7 +141,6 @@ def mitmweb(opts): opts.make_parser(group, "web_open_browser") opts.make_parser(group, "web_port", metavar="PORT") opts.make_parser(group, "web_host", metavar="HOST") - opts.make_parser(group, "web_columns") common_options(parser, opts) group = parser.add_argument_group( 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 bc99db94c4..fea007fe9a 100644 --- a/mitmproxy/tools/console/commandexecutor.py +++ b/mitmproxy/tools/console/commandexecutor.py @@ -1,9 +1,8 @@ +import logging from collections.abc import Sequence from mitmproxy import exceptions from mitmproxy import flow -from mitmproxy import ctx - from mitmproxy.tools.console import overlay from mitmproxy.tools.console import signals @@ -12,12 +11,12 @@ class CommandExecutor: def __init__(self, master): self.master = master - def __call__(self, cmd): + def __call__(self, cmd: str) -> None: if cmd.strip(): try: ret = self.master.commands.execute(cmd) except exceptions.CommandError as e: - ctx.log.error(str(e)) + logging.error(str(e)) else: if ret is not None: if type(ret) == Sequence[flow.Flow]: diff --git a/mitmproxy/tools/console/commands.py b/mitmproxy/tools/console/commands.py index 3b8f60f3d5..ee10492549 100644 --- a/mitmproxy/tools/console/commands.py +++ b/mitmproxy/tools/console/commands.py @@ -1,14 +1,15 @@ -import urwid -import blinker import textwrap +import urwid + from mitmproxy import command from mitmproxy.tools.console import layoutwidget from mitmproxy.tools.console import signals +from mitmproxy.utils import signals as utils_signals HELP_HEIGHT = 5 -command_focus_change = blinker.Signal() +command_focus_change = utils_signals.SyncSignal(lambda text: None) class CommandItem(urwid.WidgetWrap): @@ -63,7 +64,7 @@ def _get(self, pos): def get_focus(self): return self.focus_obj, self.index - def set_focus(self, index): + def set_focus(self, index: int) -> None: cmd = self.cmds[index] self.index = index self.focus_obj = self._get(self.index) @@ -88,7 +89,7 @@ def __init__(self, master): self.walker = CommandListWalker(master) super().__init__(self.walker) - def keypress(self, size, key): + def keypress(self, size: int, key: str): if key == "m_select": foc, idx = self.get_focus() signals.status_prompt_command.send(partial=foc.cmd.name + " ") @@ -125,6 +126,8 @@ 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 18c2abf270..bd5b8f89f5 100644 --- a/mitmproxy/tools/console/common.py +++ b/mitmproxy/tools/console/common.py @@ -1,21 +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 import dns -from mitmproxy.dns import DNSFlow +from mitmproxy.udp import UDPFlow +from mitmproxy.utils import emoji +from mitmproxy.utils import human # Detect Windows Subsystem for Linux and Windows IS_WINDOWS_OR_WSL = ( @@ -31,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, @@ -116,7 +116,9 @@ def fcol(s: str, attr: str) -> tuple[str, int, urwid.Text]: "ws": "scheme_ws", "wss": "scheme_wss", "tcp": "scheme_tcp", + "udp": "scheme_udp", "dns": "scheme_dns", + "quic": "scheme_quic", } HTTP_REQUEST_METHOD_STYLES = { "GET": "method_get", @@ -240,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 @@ -266,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 ( @@ -356,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: @@ -374,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: @@ -402,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 = [] @@ -490,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, @@ -500,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( @@ -546,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" @@ -604,17 +605,18 @@ def format_http_flow_table( @lru_cache(maxsize=800) -def format_tcp_flow( +def format_message_flow( *, render_mode: RenderMode, focused: bool, timestamp_start: float, marked: str, + protocol: str, 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)}" @@ -633,9 +635,9 @@ def format_tcp_flow( items.append(fcol(" ", "focus")) if render_mode is RenderMode.TABLE: - items.append(fcol("TCP ", SCHEME_STYLES["tcp"])) + items.append(fcol(fixlen(protocol.upper(), 5), SCHEME_STYLES[protocol])) else: - items.append(fcol("TCP", SCHEME_STYLES["tcp"])) + items.append(fcol(protocol.upper(), SCHEME_STYLES[protocol])) items.append(("weight", 1.0, truncated_plain(conn, "text", "left"))) if error_message: @@ -665,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 = [] @@ -745,14 +747,14 @@ 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: error_message = None - if isinstance(f, TCPFlow): + if isinstance(f, (TCPFlow, UDPFlow)): total_size = 0 for message in f.messages: total_size += len(message.content) @@ -760,11 +762,16 @@ def format_flow( duration = f.messages[-1].timestamp - f.client_conn.timestamp_start else: duration = None - return format_tcp_flow( + if f.client_conn.tls_version == "QUIC": + 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=protocol, client_address=f.client_conn.peername, server_address=f.server_conn.address, total_size=total_size, @@ -774,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( @@ -806,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 f0eebbf08c..6810bc42a5 100644 --- a/mitmproxy/tools/console/consoleaddons.py +++ b/mitmproxy/tools/console/consoleaddons.py @@ -1,8 +1,10 @@ import csv +import logging 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 @@ -11,11 +13,16 @@ from mitmproxy import http from mitmproxy import log from mitmproxy import tcp +from mitmproxy import udp +from mitmproxy.log import ALERT from mitmproxy.tools.console import keymap from mitmproxy.tools.console import overlay from mitmproxy.tools.console import signals from mitmproxy.utils import strutils +logger = logging.getLogger(__name__) + + console_palettes = [ "lowlight", "lowdark", @@ -62,7 +69,7 @@ def load(self, loader): str, "info", "EventLog verbosity.", - choices=log.LogTierOrder, + choices=log.LogLevels, ) loader.add_option( "console_layout", @@ -131,6 +138,13 @@ def panes_next(self) -> None: """ self.master.window.switch() + @command.command("console.panes.prev") + def panes_prev(self) -> None: + """ + Go to the previous layout pane. + """ + return self.panes_next() + @command.command("console.options.reset.focus") def options_reset_current(self) -> None: """ @@ -231,7 +245,7 @@ def callback(opt): try: self.master.commands.call_strings(cmd, repl) except exceptions.CommandError as e: - ctx.log.error(str(e)) + logger.error(str(e)) self.master.overlay(overlay.Chooser(self.master, prompt, choices, "", callback)) @@ -256,7 +270,7 @@ def callback(opt): try: self.master.commands.call_strings(subcmd, repl) except exceptions.CommandError as e: - ctx.log.error(str(e)) + logger.error(str(e)) self.master.overlay(overlay.Chooser(self.master, prompt, choices, "", callback)) @@ -297,7 +311,7 @@ def view_options(self) -> None: @command.command("console.view.eventlog") def view_eventlog(self) -> None: - """View the options editor.""" + """View the event log.""" self.master.switch_view("eventlog") @command.command("console.view.help") @@ -308,10 +322,10 @@ def view_help(self) -> None: @command.command("console.view.flow") def view_flow(self, flow: flow.Flow) -> None: """View a flow.""" - if isinstance(flow, (http.HTTPFlow, tcp.TCPFlow, dns.DNSFlow)): + if isinstance(flow, (http.HTTPFlow, tcp.TCPFlow, udp.UDPFlow, dns.DNSFlow)): self.master.switch_view("flowview") else: - ctx.log.warn(f"No detail view for {type(flow).__name__}.") + logger.warning(f"No detail view for {type(flow).__name__}.") @command.command("console.exit") def exit(self) -> None: @@ -324,7 +338,7 @@ def view_pop(self) -> None: Pop a view off the console stack. At the top level, this prompts the user to exit mitmproxy. """ - signals.pop_view_state.send(self) + signals.pop_view_state.send() @command.command("console.bodyview") @command.argument("part", type=mitmproxy.types.Choice("console.bodyview.options")) @@ -363,6 +377,8 @@ def edit_focus_options(self) -> Sequence[str]: if isinstance(flow, tcp.TCPFlow): focus_options = ["tcp-message"] + elif isinstance(flow, udp.UDPFlow): + focus_options = ["udp-message"] elif isinstance(flow, http.HTTPFlow): focus_options = [ "cookies", @@ -447,7 +463,7 @@ def edit_focus(self, flow_part: str) -> None: self.master.commands.call_strings( "console.command", ["flow.set", "@focus", flow_part] ) - elif flow_part == "tcp-message": + elif flow_part in ["tcp-message", "udp-message"]: message = flow.messages[-1] c = self.master.spawn_editor(message.content or b"") message.content = c.rstrip(b"\n") @@ -507,9 +523,9 @@ def grideditor_save(self, path: mitmproxy.types.Path) -> None: writer.writerow( [strutils.always_str(x) or "" for x in row] # type: ignore ) - ctx.log.alert("Saved %s rows as CSV." % (len(rows))) + logger.log(ALERT, "Saved %s rows as CSV." % (len(rows))) except OSError as e: - ctx.log.error(str(e)) + logger.error(str(e)) @command.command("console.grideditor.editor") def grideditor_editor(self) -> None: @@ -539,7 +555,7 @@ def flowview_mode_set(self, mode: str) -> None: "view.settings.setval", ["@focus", f"flowview_mode_{idx}", mode] ) except exceptions.CommandError as e: - ctx.log.error(str(e)) + logger.error(str(e)) @command.command("console.flowview.mode.options") def flowview_mode_options(self) -> Sequence[str]: @@ -644,8 +660,8 @@ def key_edit_focus(self) -> None: def running(self): self.started = True - def update(self, flows): + def update(self, flows) -> None: if not flows: - signals.update_settings.send(self) + signals.update_settings.send() for f in flows: - signals.flow_change.send(self, flow=f) + signals.flow_change.send(flow=f) diff --git a/mitmproxy/tools/console/defaultkeys.py b/mitmproxy/tools/console/defaultkeys.py index e20a967605..41ac65bc56 100644 --- a/mitmproxy/tools/console/defaultkeys.py +++ b/mitmproxy/tools/console/defaultkeys.py @@ -1,4 +1,7 @@ -def map(km): +from mitmproxy.tools.console.keymap import Keymap + + +def map(km: Keymap) -> None: km.add(":", "console.command ", ["commonkey", "global"], "Command prompt") km.add( ";", @@ -14,9 +17,11 @@ def map(km): 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("shift tab", "console.panes.next", ["global"], "Focus next layout pane") km.add("ctrl right", "console.panes.next", ["global"], "Focus next layout pane") + km.add("ctrl left", "console.panes.prev", ["global"], "Focus previous layout pane") + km.add("shift tab", "console.panes.next", ["global"], "Focus next layout pane") km.add("P", "console.view.flow @focus", ["global"], "View flow details") km.add("?", "console.view.pop", ["help"], "Exit help") @@ -78,7 +83,12 @@ def map(km): "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 ", diff --git a/mitmproxy/tools/console/eventlog.py b/mitmproxy/tools/console/eventlog.py index 246de37c88..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): @@ -42,7 +43,7 @@ def keypress(self, size, key): self.set_focus(0) return super().keypress(size, key) - def add_event(self, event_store, entry: log.LogEntry): + def add_event(self, entry: log.LogEntry): if log.log_tier(self.master.options.console_eventlog_verbosity) < log.log_tier( entry.level ): @@ -56,7 +57,7 @@ def add_event(self, event_store, entry: log.LogEntry): if self.master.options.console_focus_follow: self.walker.set_focus(len(self.walker) - 1) - def refresh_events(self, *_): + def refresh_events(self, *_) -> None: self.walker.clear() for event in self.master.events.data: - self.add_event(None, event) + self.add_event(event) diff --git a/mitmproxy/tools/console/flowdetailview.py b/mitmproxy/tools/console/flowdetailview.py index 56161d50ce..b8b2745549 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 diff --git a/mitmproxy/tools/console/flowlist.py b/mitmproxy/tools/console/flowlist.py index 8c72a0a3bc..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 @@ -105,5 +104,5 @@ def keypress(self, size, key): def view_changed(self): self.body.view_changed() - def set_flowlist_layout(self, opts, updated): + def set_flowlist_layout(self, *_) -> None: self.master.ui.clear() diff --git a/mitmproxy/tools/console/flowview.py b/mitmproxy/tools/console/flowview.py index 96701f5026..191edd8c88 100644 --- a/mitmproxy/tools/console/flowview.py +++ b/mitmproxy/tools/console/flowview.py @@ -1,7 +1,7 @@ +import logging import math import sys from functools import lru_cache -from typing import Optional import urwid @@ -12,6 +12,7 @@ from mitmproxy import dns from mitmproxy import http from mitmproxy import tcp +from mitmproxy import udp from mitmproxy.tools.console import common from mitmproxy.tools.console import flowdetailview from mitmproxy.tools.console import layoutwidget @@ -87,7 +88,12 @@ def focus_changed(self): ] elif isinstance(f, tcp.TCPFlow): self.tabs = [ - (self.tab_tcp_stream, self.view_tcp_stream), + (self.tab_tcp_stream, self.view_message_stream), + (self.tab_details, self.view_details), + ] + elif isinstance(f, udp.UDPFlow): + self.tabs = [ + (self.tab_udp_stream, self.view_message_stream), (self.tab_details, self.view_details), ] elif isinstance(f, dns.DNSFlow): @@ -135,6 +141,9 @@ def tab_dns_response(self) -> str: def tab_tcp_stream(self): return "TCP Stream" + def tab_udp_stream(self): + return "UDP Stream" + def tab_websocket_messages(self): return "WebSocket Messages" @@ -235,42 +244,27 @@ def view_websocket_messages(self): return searchable.Searchable(widget_lines) - def view_tcp_stream(self) -> urwid.Widget: + def view_message_stream(self) -> urwid.Widget: flow = self.flow - assert isinstance(flow, tcp.TCPFlow) + assert isinstance(flow, (tcp.TCPFlow, udp.UDPFlow)) if not flow.messages: return searchable.Searchable([urwid.Text(("highlight", "No messages."))]) viewmode = self.master.commands.call("console.flowview.mode") - # Merge adjacent TCP "messages". For detailed explanation of this code block see: - # https://github.com/mitmproxy/mitmproxy/pull/3970/files/469bd32582f764f9a29607efa4f5b04bd87961fb#r418670880 - from_client = None - messages = [] - for message in flow.messages: - if message.from_client is not from_client: - messages.append(message.content) - from_client = message.from_client - else: - messages[-1] += message.content - widget_lines = [] - - from_client = flow.messages[0].from_client - for m in messages: - _, lines, _ = contentviews.get_tcp_content_view(viewmode, m, flow) + 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)) @@ -318,7 +312,7 @@ def _get_content_view(self, viewmode, max_lines, _): viewmode, message, self.flow ) if error: - self.master.log.debug(error) + logging.debug(error) # Give hint that you have to tab for the response. if description == "No content" and isinstance(message, http.Request): description = "No request content" @@ -330,7 +324,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)) @@ -422,7 +416,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 c4fdb5a617..8510e91996 100644 --- a/mitmproxy/tools/console/grideditor/base.py +++ b/mitmproxy/tools/console/grideditor/base.py @@ -1,19 +1,36 @@ 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 +75,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] = [] @@ -117,11 +134,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() @@ -135,11 +152,11 @@ def get_current_value(self): if self.lst: return self.lst[self.focus][0][self.focus_col] - def set_current_value(self, val): + def set_current_value(self, val) -> None: errors = self.lst[self.focus][1] emsg = self.editor.is_error(self.focus_col, val) if emsg: - signals.status_message.send(message=emsg, expire=5) + signals.status_message.send(message=emsg) errors.add(self.focus_col) else: errors.discard(self.focus_col) @@ -150,7 +167,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 +197,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 +259,7 @@ def __init__(self, lw): class BaseGridEditor(urwid.WidgetWrap): title: str = "" - keyctx = "grideditor" + keyctx: ClassVar[str] = "grideditor" def __init__( self, @@ -252,7 +269,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 @@ -353,7 +370,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 +405,7 @@ def cmd_spawn_editor(self): class GridEditor(BaseGridEditor): title = "" columns: Sequence[Column] = () - keyctx = "grideditor" + keyctx: ClassVar[str] = "grideditor" def __init__( self, @@ -396,7 +413,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 +425,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 @@ -430,9 +447,9 @@ def set_data(self, vals, flow): """ raise NotImplementedError - def set_data_update(self, vals, flow): + def set_data_update(self, vals, flow) -> None: self.set_data(vals, flow) - signals.flow_change.send(self, flow=flow) + signals.flow_change.send(flow=flow) def key_responder(self): return self._w diff --git a/mitmproxy/tools/console/grideditor/col_bytes.py b/mitmproxy/tools/console/grideditor/col_bytes.py index 02f1f955b0..9af1a3544d 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 @@ -44,5 +45,5 @@ def get_data(self) -> bytes: try: return strutils.escaped_str_to_bytes(txt) except ValueError: - signals.status_message.send(self, message="Invalid data.", expire=1000) + signals.status_message.send(message="Invalid data.") raise diff --git a/mitmproxy/tools/console/grideditor/col_subgrid.py b/mitmproxy/tools/console/grideditor/col_subgrid.py index 02465647cd..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): @@ -18,11 +19,9 @@ def Display(self, data): def blank(self): return [] - def keypress(self, key, editor): + def keypress(self, key: str, editor): if key in "rRe": - signals.status_message.send( - self, message="Press enter to edit this field.", expire=1000 - ) + 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 f4ff5ee2ad..04dbb5ab04 100644 --- a/mitmproxy/tools/console/grideditor/col_text.py +++ b/mitmproxy/tools/console/grideditor/col_text.py @@ -4,7 +4,6 @@ In a nutshell, text columns are actually a proxy class for byte columns, which just encode/decodes contents. """ - from mitmproxy.tools.console import signals from mitmproxy.tools.console.grideditor import col_bytes @@ -28,14 +27,14 @@ 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: - signals.status_message.send(self, message="Invalid encoding.", expire=1000) + signals.status_message.send(message="Invalid encoding.") raise diff --git a/mitmproxy/tools/console/grideditor/col_viewany.py b/mitmproxy/tools/console/grideditor/col_viewany.py index 2801587c04..b6ffe1f442 100644 --- a/mitmproxy/tools/console/grideditor/col_viewany.py +++ b/mitmproxy/tools/console/grideditor/col_viewany.py @@ -4,6 +4,7 @@ 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 580f1de8f7..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): @@ -176,7 +174,7 @@ def __init__(self, master, name, vals): self.name = name super().__init__(master, [[i] for i in vals], self.callback) - def callback(self, vals): + def callback(self, vals) -> None: try: setattr(self.master.options, self.name, [i[0] for i in vals]) except exceptions.OptionsError as v: @@ -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 821e99dda0..5cb3819b58 100644 --- a/mitmproxy/tools/console/keybindings.py +++ b/mitmproxy/tools/console/keybindings.py @@ -1,13 +1,15 @@ -import urwid -import blinker import textwrap + +import urwid + from mitmproxy.tools.console import layoutwidget from mitmproxy.tools.console import signals +from mitmproxy.utils import signals as utils_signals HELP_HEIGHT = 5 -keybinding_focus_change = blinker.Signal() +keybinding_focus_change = utils_signals.SyncSignal(lambda text: None) class KeyItem(urwid.WidgetWrap): @@ -46,7 +48,7 @@ def __init__(self, master): self.set_focus(0) signals.keybindings_change.connect(self.sig_modified) - def sig_modified(self, sender): + def sig_modified(self): self.bindings = list(self.master.keymap.list("all")) self.set_focus(min(self.index, len(self.bindings) - 1)) self._modified() @@ -129,6 +131,7 @@ def sig_mod(self, txt): class KeyBindings(urwid.Pile, layoutwidget.LayoutWidget): title = "Key Bindings" keyctx = "keybindings" + focus_position: int def __init__(self, master): oh = KeyHelp(master) diff --git a/mitmproxy/tools/console/keymap.py b/mitmproxy/tools/console/keymap.py index 160501064c..514066a9bc 100644 --- a/mitmproxy/tools/console/keymap.py +++ b/mitmproxy/tools/console/keymap.py @@ -1,16 +1,17 @@ +import logging import os +from collections import defaultdict from collections.abc import Sequence -from typing import Optional +from functools import cache -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): @@ -59,6 +60,11 @@ def keyspec(self): """ return self.key.replace("space", " ") + def key_short(self) -> str: + return ( + self.key.replace("enter", "⏎").replace("right", "→").replace("space", "␣") + ) + def sortkey(self): return self.key + ",".join(self.contexts) @@ -66,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): @@ -78,6 +82,10 @@ def _check_contexts(self, contexts): if c not in Contexts: raise ValueError("Unsupported context: %s" % c) + def _on_change(self) -> None: + signals.keybindings_change.send() + self.binding_for_help.cache_clear() + def add(self, key: str, command: str, contexts: Sequence[str], help="") -> None: """ Add a key to the key map. @@ -96,7 +104,7 @@ def add(self, key: str, command: str, contexts: Sequence[str], help="") -> None: b = Binding(key=key, command=command, contexts=contexts, help=help) self.bindings.append(b) self.bind(b) - signals.keybindings_change.send(self) + self._on_change() def remove(self, key: str, contexts: Sequence[str]) -> None: """ @@ -111,7 +119,7 @@ def remove(self, key: str, contexts: Sequence[str]) -> None: if b.contexts: self.bindings.append(b) self.bind(b) - signals.keybindings_change.send(self) + self._on_change() def bind(self, binding: Binding) -> None: for c in binding.contexts: @@ -124,12 +132,20 @@ def unbind(self, binding: Binding) -> None: for c in binding.contexts: del self.keys[c][binding.keyspec()] 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) -> Binding | None: + for b in self.bindings: + if b.help == help: + return b + return None + def list(self, context: str) -> Sequence[Binding]: b = [x for x in self.bindings if context in x.contexts or context == "all"] single = [x for x in b if len(x.key.split()) == 1] @@ -138,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 @@ -170,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 @@ -181,9 +202,9 @@ 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: - ctx.log.error(e) + logging.error(e) def load_path(self, km, p): if os.path.exists(p) and os.path.isfile(p): 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 38761ba40a..ee303ff5d2 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,21 +8,21 @@ import subprocess import sys import tempfile -import contextlib import threading - -from tornado.platform.asyncio import AddThreadSelectorEventLoop +from typing import TypeVar 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 @@ -30,9 +30,11 @@ from mitmproxy.tools.console import signals from mitmproxy.tools.console import window +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() @@ -44,8 +46,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(), @@ -53,31 +53,30 @@ 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) - signals.update_settings.send(self) + signals.update_settings.send() - def options_error(self, opts, exc): + def options_error(self, exc) -> None: signals.status_message.send(message=str(exc), expire=1) - def prompt_for_exit(self): + def prompt_for_exit(self) -> None: signals.status_prompt_onekey.send( - self, prompt="Quit", - keys=( + keys=[ ("yes", "y"), ("no", "n"), - ), + ], callback=self.quit, ) - def sig_add_log(self, event_store, entry: log.LogEntry): + def sig_add_log(self, entry: log.LogEntry): if log.log_tier(self.options.console_eventlog_verbosity) < log.log_tier( entry.level ): @@ -91,9 +90,9 @@ def sig_add_log(self, event_store, entry: log.LogEntry): expire=5, ) - def sig_call_in(self, sender, seconds, callback, args=()): + def sig_call_in(self, seconds, callback): def cb(*_): - return callback(*args) + return callback() self.loop.set_alarm_in(seconds, cb) @@ -121,7 +120,7 @@ def get_editor(self) -> str: else: return "vi" - def spawn_editor(self, data): + def spawn_editor(self, data: T) -> T: text = not isinstance(data, bytes) fd, name = tempfile.mkstemp("", "mitmproxy", text=text) with open(fd, "w" if text else "wb") as f: @@ -132,7 +131,7 @@ def spawn_editor(self, data): 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: @@ -167,7 +166,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) ) @@ -175,10 +174,10 @@ def spawn_external_viewer(self, data, contenttype): t = threading.Timer(1.0, os.unlink, args=[name]) t.start() - def set_palette(self, opts, updated): + def set_palette(self, *_) -> None: self.ui.register_palette( - palettes.palettes[opts.console_palette].palette( - opts.console_palette_transparent + palettes.palettes[self.options.console_palette].palette( + self.options.console_palette_transparent ) ) self.ui.clear() @@ -209,14 +208,13 @@ async def running(self) -> None: signals.call_in.connect(self.sig_call_in) self.ui = window.Screen() self.ui.set_terminal_properties(256) - self.set_palette(self.options, None) + self.set_palette(None) self.options.subscribe( self.set_palette, ["console_palette", "console_palette_transparent"] ) 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( @@ -238,9 +236,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 b922ee32f0..8aca078bac 100644 --- a/mitmproxy/tools/console/options.py +++ b/mitmproxy/tools/console/options.py @@ -1,16 +1,17 @@ -from collections.abc import Sequence +from __future__ import annotations -import urwid -import blinker -import textwrap import pprint +import textwrap +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 @@ -27,9 +28,6 @@ def fcol(s, width, attr): return ("fixed", width, urwid.Text((attr, s))) -option_focus_change = blinker.Signal() - - class OptionItem(urwid.WidgetWrap): def __init__(self, walker, opt, focused, namewidth, editing): self.walker, self.opt, self.focused = walker, opt, focused @@ -88,8 +86,9 @@ def keypress(self, size, key): class OptionListWalker(urwid.ListWalker): - def __init__(self, master): + def __init__(self, master, help_widget: OptionHelp): self.master = master + self.help_widget = help_widget self.index = 0 self.focusobj = None @@ -134,7 +133,7 @@ def set_focus(self, index): opt = self.master.options._options[name] self.index = index self.focus_obj = self._get(self.index, self.editing) - option_focus_change.send(opt.help) + self.help_widget.update_help_text(opt.help) self._modified() def get_next(self, pos): @@ -157,9 +156,9 @@ def positions(self, reverse=False): class OptionsList(urwid.ListBox): - def __init__(self, master): + def __init__(self, master, help_widget: OptionHelp): self.master = master - self.walker = OptionListWalker(master) + self.walker = OptionListWalker(master, help_widget) super().__init__(self.walker) def save_config(self, path): @@ -228,7 +227,6 @@ def __init__(self, master): self.master = master super().__init__(self.widget("")) self.set_active(False) - option_focus_change.connect(self.sig_mod) def set_active(self, val): h = urwid.Text("Option Help") @@ -239,7 +237,7 @@ 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): + def update_help_text(self, txt: str) -> None: self.set_body(self.widget(txt)) @@ -247,9 +245,11 @@ class Options(urwid.Pile, layoutwidget.LayoutWidget): title = "Options" keyctx = "options" + focus_position: int + def __init__(self, master): oh = OptionHelp(master) - self.optionslist = OptionsList(master) + self.optionslist = OptionsList(master, oh) super().__init__( [ self.optionslist, diff --git a/mitmproxy/tools/console/overlay.py b/mitmproxy/tools/console/overlay.py index d303e28a7e..17b55bc10a 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): @@ -124,14 +124,14 @@ def keypress(self, size, key): choice = self.walker.choice_by_shortcut(key) if choice: self.callback(choice) - signals.pop_view_state.send(self) + signals.pop_view_state.send() return if key == "m_select": self.callback(self.choices[self.walker.index]) - signals.pop_view_state.send(self) + signals.pop_view_state.send() return elif key in ["q", "esc"]: - signals.pop_view_state.send(self) + signals.pop_view_state.send() return binding = self.master.keymap.get("global", key) diff --git a/mitmproxy/tools/console/palettes.py b/mitmproxy/tools/console/palettes.py index 85cec35cfb..a6b5d2e73c 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: @@ -37,7 +39,9 @@ class Palette: "scheme_ws", "scheme_wss", "scheme_tcp", + "scheme_udp", "scheme_dns", + "scheme_quic", "scheme_other", "url_punctuation", "url_domain", @@ -68,10 +72,11 @@ class Palette: "mark", # Hex view "offset", - # JSON view - "json_string", - "json_number", - "json_boolean", + # JSON/msgpack view + "Token_Name_Tag", + "Token_Literal_String", + "Token_Literal_Number", + "Token_Keyword_Constant", # TCP flow details "from_client", "to_client", @@ -86,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"): @@ -98,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): @@ -174,7 +180,9 @@ class LowDark(Palette): scheme_ws=("brown", "default"), scheme_wss=("dark magenta", "default"), 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"), @@ -205,10 +213,11 @@ class LowDark(Palette): mark=("light red", "default"), # Hex view offset=("dark cyan", "default"), - # JSON view - json_string=("dark blue", "default"), - json_number=("light magenta", "default"), - json_boolean=("dark magenta", "default"), + # JSON/msgpack view + Token_Name_Tag=("dark green", "default"), + Token_Literal_String=("dark blue", "default"), + Token_Literal_Number=("light magenta", "default"), + Token_Keyword_Constant=("dark magenta", "default"), # TCP flow details from_client=("light blue", "default"), to_client=("light red", "default"), @@ -272,7 +281,9 @@ class LowLight(Palette): scheme_ws=("brown", "default"), scheme_wss=("light magenta", "default"), 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"), @@ -303,10 +314,11 @@ class LowLight(Palette): mark=("dark red", "default"), # Hex view offset=("dark blue", "default"), - # JSON view - json_string=("dark blue", "default"), - json_number=("light magenta", "default"), - json_boolean=("dark magenta", "default"), + # JSON/msgpack view + Token_Name_Tag=("dark green", "default"), + Token_Literal_String=("dark blue", "default"), + Token_Literal_Number=("light magenta", "default"), + Token_Keyword_Constant=("dark magenta", "default"), # TCP flow details from_client=("dark blue", "default"), to_client=("dark red", "default"), @@ -391,7 +403,9 @@ class SolarizedLight(LowLight): scheme_ws=(sol_orange, "default"), scheme_wss=("light magenta", "default"), 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"), @@ -423,10 +437,11 @@ class SolarizedLight(LowLight): ), # Hex view offset=(sol_cyan, "default"), - # JSON view - json_string=(sol_cyan, "default"), - json_number=(sol_blue, "default"), - json_boolean=(sol_magenta, "default"), + # JSON/msgpack view + Token_Name_Tag=(sol_green, "default"), + Token_Literal_String=(sol_cyan, "default"), + Token_Literal_Number=(sol_blue, "default"), + Token_Keyword_Constant=(sol_magenta, "default"), # TCP flow details from_client=(sol_blue, "default"), to_client=(sol_red, "default"), @@ -502,10 +517,11 @@ class SolarizedDark(LowDark): ), # Hex view offset=(sol_cyan, "default"), - # JSON view - json_string=(sol_cyan, "default"), - json_number=(sol_blue, "default"), - json_boolean=(sol_magenta, "default"), + # JSON/msgpack view + Token_Name_Tag=(sol_green, "default"), + Token_Literal_String=(sol_cyan, "default"), + Token_Literal_Number=(sol_blue, "default"), + Token_Keyword_Constant=(sol_magenta, "default"), # TCP flow details from_client=(sol_blue, "default"), to_client=(sol_red, "default"), diff --git a/mitmproxy/tools/console/quickhelp.py b/mitmproxy/tools/console/quickhelp.py new file mode 100644 index 0000000000..d6c959ad9a --- /dev/null +++ b/mitmproxy/tools/console/quickhelp.py @@ -0,0 +1,189 @@ +""" +This module is reponsible for drawing the quick key help at the bottom of mitmproxy. +""" +from dataclasses import dataclass +from typing import Union + +import urwid + +from mitmproxy import flow +from mitmproxy.http import HTTPFlow +from mitmproxy.tools.console.eventlog import EventLog +from mitmproxy.tools.console.flowlist import FlowListBox +from mitmproxy.tools.console.flowview import FlowView +from mitmproxy.tools.console.grideditor.base import FocusEditor +from mitmproxy.tools.console.help import HelpView +from mitmproxy.tools.console.keybindings import KeyBindings +from mitmproxy.tools.console.keymap import Keymap +from mitmproxy.tools.console.options import Options + + +@dataclass +class BasicKeyHelp: + """Quick help for urwid-builtin keybindings (i.e. those keys that do not appear in the keymap)""" + + key: str + + +HelpItems = dict[str, Union[str, BasicKeyHelp]] +""" +A mapping from the short text that should be displayed in the help bar to the full help text provided for the key +binding. The order of the items in the dictionary determines the order in which they are displayed in the help bar. + +Some help items explain builtin urwid functionality, so there is no key binding for them. In this case, the value +is a BasicKeyHelp object. +""" + + +@dataclass +class QuickHelp: + top_label: str + top_items: HelpItems + bottom_label: str + bottom_items: HelpItems + + def make_rows(self, keymap: Keymap) -> tuple[urwid.Columns, urwid.Columns]: + top = _make_row(self.top_label, self.top_items, keymap) + bottom = _make_row(self.bottom_label, self.bottom_items, keymap) + return top, bottom + + +def make( + widget: type[urwid.Widget], + focused_flow: flow.Flow | None, + is_root_widget: bool, +) -> QuickHelp: + top_label = "" + top_items: HelpItems = {} + if widget in (FlowListBox, FlowView): + top_label = "Flow:" + if focused_flow: + if widget == FlowListBox: + top_items["Select"] = "Select" + else: + top_items["Edit"] = "Edit a flow component" + top_items |= { + "Duplicate": "Duplicate flow", + "Replay": "Replay this flow", + "Export": "Export this flow to file", + "Delete": "Delete flow from view", + } + if widget == FlowListBox: + if focused_flow.marked: + top_items["Unmark"] = "Toggle mark on this flow" + else: + top_items["Mark"] = "Toggle mark on this flow" + if focused_flow.intercepted: + top_items["Resume"] = "Resume this intercepted flow" + if focused_flow.modified(): + top_items["Restore"] = "Revert changes to this flow" + if isinstance(focused_flow, HTTPFlow) and focused_flow.response: + top_items["Save body"] = "Save response body to file" + if widget == FlowView: + top_items |= { + "Next flow": "Go to next flow", + "Prev flow": "Go to previous flow", + } + else: + top_items |= { + "Load flows": "Load flows from file", + "Create new": "Create a new flow", + } + elif widget == KeyBindings: + top_label = "Keybindings:" + top_items |= { + "Add": "Add a key binding", + "Edit": "Edit the currently focused key binding", + "Delete": "Unbind the currently focused key binding", + "Execute": "Execute the currently focused key binding", + } + elif widget == Options: + top_label = "Options:" + top_items |= { + "Edit": BasicKeyHelp("⏎"), + "Reset": "Reset this option", + "Reset all": "Reset all options", + "Load file": "Load from file", + "Save file": "Save to file", + } + elif widget == HelpView: + top_label = "Help:" + top_items |= { + "Scroll down": BasicKeyHelp("↓"), + "Scroll up": BasicKeyHelp("↑"), + "Exit help": "Exit help", + "Next tab": BasicKeyHelp("tab"), + } + elif widget == EventLog: + top_label = "Events:" + top_items |= { + "Scroll down": BasicKeyHelp("↓"), + "Scroll up": BasicKeyHelp("↑"), + "Clear": "Clear", + } + elif issubclass(widget, FocusEditor): + top_label = f"Edit:" + top_items |= { + "Start edit": BasicKeyHelp("⏎"), + "Stop edit": BasicKeyHelp("esc"), + "Add row": "Add a row after cursor", + "Delete row": "Delete this row", + } + else: + pass + + bottom_label = "Proxy:" + bottom_items: HelpItems = { + "Help": "View help", + } + if is_root_widget: + bottom_items["Quit"] = "Exit the current view" + else: + bottom_items["Back"] = "Exit the current view" + bottom_items |= { + "Events": "View event log", + "Options": "View options", + "Intercept": "Set intercept", + "Filter": "Set view filter", + } + if focused_flow: + bottom_items |= { + "Save flows": "Save listed flows to file", + "Clear list": "Clear flow list", + } + bottom_items |= { + "Layout": "Cycle to next layout", + "Switch": "Focus next layout pane", + "Follow new": "Set focus follow", + } + + label_len = max(len(top_label), len(bottom_label), 8) + 1 + top_label = top_label.ljust(label_len) + bottom_label = bottom_label.ljust(label_len) + + return QuickHelp(top_label, top_items, bottom_label, bottom_items) + + +def _make_row(label: str, items: HelpItems, keymap: Keymap) -> urwid.Columns: + cols = [ + (len(label), urwid.Text(label)), + ] + for short, long in items.items(): + if isinstance(long, BasicKeyHelp): + key_short = long.key + else: + b = keymap.binding_for_help(long) + if b is None: + continue + key_short = b.key_short() + txt = urwid.Text( + [ + ("heading_inactive", key_short), + " ", + short, + ], + wrap="clip", + ) + cols.append((14, txt)) + + return urwid.Columns(cols) diff --git a/mitmproxy/tools/console/searchable.py b/mitmproxy/tools/console/searchable.py index e71fe862a9..15d4e72771 100644 --- a/mitmproxy/tools/console/searchable.py +++ b/mitmproxy/tools/console/searchable.py @@ -22,7 +22,7 @@ def __init__(self, contents): self.search_term = None self.last_search = None - def keypress(self, size, key): + def keypress(self, size, key: str): if key == "/": signals.status_prompt.send( prompt="Search for", text="", callback=self.set_search @@ -63,7 +63,7 @@ def get_text(self, w): else: return None - def find_next(self, backwards): + def find_next(self, backwards: bool): if not self.search_term: if self.last_search: self.search_term = self.last_search diff --git a/mitmproxy/tools/console/signals.py b/mitmproxy/tools/console/signals.py index 9c44b361c3..38c3f5cfac 100644 --- a/mitmproxy/tools/console/signals.py +++ b/mitmproxy/tools/console/signals.py @@ -1,39 +1,71 @@ -import blinker +from __future__ import annotations + +from collections.abc import Callable +from typing import Union + +from mitmproxy.utils import signals + +StatusMessage = Union[tuple[str, str], str] # Show a status message in the action bar -status_message = blinker.Signal() +# Instead of using this signal directly, consider emitting a log event. +def _status_message(message: StatusMessage, expire: int = 5) -> None: + ... + + +status_message = signals.SyncSignal(_status_message) + # Prompt for input -status_prompt = blinker.Signal() +def _status_prompt( + prompt: str, text: str | None, callback: Callable[[str], None] +) -> None: + ... + + +status_prompt = signals.SyncSignal(_status_prompt) -# Prompt for a path -status_prompt_path = blinker.Signal() # Prompt for a single keystroke -status_prompt_onekey = blinker.Signal() +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 -status_prompt_command = blinker.Signal() +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 -call_in = blinker.Signal() +def _call_in(seconds: float, callback: Callable[[], None]) -> None: + ... + + +call_in = signals.SyncSignal(_call_in) # Focus the body, footer or header of the main window -focus = blinker.Signal() +focus = signals.SyncSignal(lambda section: None) # Fired when settings change -update_settings = blinker.Signal() +update_settings = signals.SyncSignal(lambda: None) # Fired when a flow changes -flow_change = blinker.Signal() - -# Fired when the flow list or focus changes -flowlist_change = blinker.Signal() +flow_change = signals.SyncSignal(lambda flow: None) # Pop and push view state onto a stack -pop_view_state = blinker.Signal() -push_view_state = blinker.Signal() +pop_view_state = signals.SyncSignal(lambda: None) + +# Fired when the window state changes +window_refresh = signals.SyncSignal(lambda: None) # Fired when the key bindings change -keybindings_change = blinker.Signal() +keybindings_change = signals.SyncSignal(lambda: None) diff --git a/mitmproxy/tools/console/statusbar.py b/mitmproxy/tools/console/statusbar.py index 8ab3a13392..71b5d41d96 100644 --- a/mitmproxy/tools/console/statusbar.py +++ b/mitmproxy/tools/console/statusbar.py @@ -1,141 +1,142 @@ -import os.path -from typing import Optional +from __future__ import annotations + +from collections.abc import Callable +from functools import lru_cache import urwid import mitmproxy.tools.console.master 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 - - -class PromptPath: - def __init__(self, callback, args): - self.callback, self.args = callback, args - - def __call__(self, pth): - if not pth: - return - pth = os.path.expanduser(pth) - try: - return self.callback(pth, *self.args) - except OSError as v: - signals.status_message.send(message=v.strerror) - - -class PromptStub: - def __init__(self, callback, args): - self.callback, self.args = callback, args - - def __call__(self, txt): - return self.callback(txt, *self.args) +from mitmproxy.utils import human + + +@lru_cache +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. + """ + if isinstance(msg, tuple): + disp_attr, msg_text = msg + elif isinstance(msg, str): + msg_text = msg + disp_attr = "" + else: + raise AssertionError(f"Unexpected message type: {type(msg)}") + msg_end = "\u2026" # unicode ellipsis for the end of shortened message + prompt = "(more in eventlog)" + + msg_lines = msg_text.split("\n") + first_line = msg_lines[0] + if len(msg_lines) > 1: + # First line of messages with a few lines must end with prompt. + line_length = len(first_line) + len(prompt) + else: + line_length = len(first_line) + + if line_length > max_width: + shortening_index = max(0, max_width - len(prompt) - len(msg_end)) + first_line = first_line[:shortening_index] + msg_end + else: + if len(msg_lines) == 1: + prompt = "" + + return [(disp_attr, first_line), ("warn", prompt)] class ActionBar(urwid.WidgetWrap): - def __init__(self, master): + def __init__(self, master: mitmproxy.tools.console.master.ConsoleMaster) -> None: self.master = master - urwid.WidgetWrap.__init__(self, None) - self.clear() + self.top = urwid.WidgetWrap(urwid.Text("")) + self.bottom = urwid.WidgetWrap(urwid.Text("")) + super().__init__(urwid.Pile([self.top, self.bottom])) + self.show_quickhelp() signals.status_message.connect(self.sig_message) signals.status_prompt.connect(self.sig_prompt) signals.status_prompt_onekey.connect(self.sig_prompt_onekey) signals.status_prompt_command.connect(self.sig_prompt_command) + signals.window_refresh.connect(self.sig_update) + master.view.focus.sig_change.connect(self.sig_update) + master.view.sig_view_update.connect(self.sig_update) - self.prompting = None + self.prompting: Callable[[str], None] | None = None - self.onekey = False + self.onekey: set[str] | None = None - def sig_message(self, sender, message, expire=1): + 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: if self.prompting: return cols, _ = self.master.ui.get_cols_rows() - w = urwid.Text(self.shorten_message(message, cols)) - self._w = w + w = urwid.Text(shorten_message(message, cols)) + self.top._w = w + self.bottom._w = urwid.Text("") if expire: - def cb(*args): - if w == self._w: - self.clear() + def cb(): + if w == self.top._w: + self.show_quickhelp() signals.call_in.send(seconds=expire, callback=cb) - def prep_prompt(self, p): - return p.strip() + ": " - - @staticmethod - def shorten_message(msg, max_width): - """ - Shorten message so that it fits into a single line in the statusbar. - """ - if isinstance(msg, tuple): - disp_attr, msg_text = msg - elif isinstance(msg, str): - disp_attr, msg_text = None, msg - else: - return msg - msg_end = "\u2026" # unicode ellipsis for the end of shortened message - prompt = "(more in eventlog)" - - msg_lines = msg_text.split("\n") - first_line = msg_lines[0] - if len(msg_lines) > 1: - # First line of messages with a few lines must end with prompt. - line_length = len(first_line) + len(prompt) - else: - line_length = len(first_line) - - if line_length > max_width: - shortening_index = max(0, max_width - len(prompt) - len(msg_end)) - first_line = first_line[:shortening_index] + msg_end - else: - if len(msg_lines) == 1: - prompt = "" - - return [(disp_attr, first_line), ("warn", prompt)] - - def sig_prompt(self, sender, prompt, text, callback, args=()): - signals.focus.send(self, section="footer") - self._w = urwid.Edit(self.prep_prompt(prompt), text or "") - self.prompting = PromptStub(callback, args) - - def sig_prompt_command( - self, sender, partial: str = "", cursor: Optional[int] = None - ): - signals.focus.send(self, section="footer") - self._w = commander.CommandEdit( + 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: int | None = None) -> None: + signals.focus.send(section="footer") + self.top._w = commander.CommandEdit( self.master, partial, ) if cursor is not None: - self._w.cbuf.cursor = cursor + self.top._w.cbuf.cursor = cursor + self.bottom._w = urwid.Text("") self.prompting = self.execute_command - def execute_command(self, txt): + def execute_command(self, txt: str) -> None: if txt.strip(): self.master.commands.call("commands.history.add", txt) execute = commandexecutor.CommandExecutor(self.master) execute(txt) - def sig_prompt_onekey(self, sender, prompt, keys, callback, args=()): + 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. """ - signals.focus.send(self, section="footer") - prompt = [prompt, " ("] + signals.focus.send(section="footer") + parts = [prompt, " ("] mkup = [] for i, e in enumerate(keys): mkup.extend(common.highlight_key(e[0], e[1])) if i < len(keys) - 1: mkup.append(",") - prompt.extend(mkup) - prompt.append(")? ") + parts.extend(mkup) + parts.append(")? ") self.onekey = {i[1] for i in keys} - self._w = urwid.Edit(prompt, "") - self.prompting = PromptStub(callback, args) + self.top._w = urwid.Edit(parts, "") + self.bottom._w = urwid.Text("") + self.prompting = callback - def selectable(self): + def selectable(self) -> bool: return True def keypress(self, size, k): @@ -148,28 +149,37 @@ def keypress(self, size, k): elif k in self.onekey: self.prompt_execute(k) elif k == "enter": - text = self._w.get_edit_text() + text = self.top._w.get_edit_text() self.prompt_execute(text) else: if common.is_keypress(k): - self._w.keypress(size, k) + self.top._w.keypress(size, k) else: return k - def clear(self): - self._w = urwid.Text("") - self.prompting = None - - def prompt_done(self): + def show_quickhelp(self) -> None: + if w := self.master.window: + s = w.focus_stack() + focused_widget = type(s.top_widget()) + is_top_widget = len(s.stack) == 1 + else: # on startup + focused_widget = flowlist.FlowListBox + is_top_widget = True + focused_flow = self.master.view.focus.flow + qh = quickhelp.make(focused_widget, focused_flow, is_top_widget) + self.top._w, self.bottom._w = qh.make_rows(self.master.keymap) + + def prompt_done(self) -> None: self.prompting = None - self.onekey = False - signals.status_message.send(message="") - signals.focus.send(self, section="body") + self.onekey = None + self.show_quickhelp() + signals.focus.send(section="body") - def prompt_execute(self, txt): - p = self.prompting + def prompt_execute(self, txt) -> None: + callback = self.prompting + assert callback is not None self.prompt_done() - msg = p(txt) + msg = callback(txt) if msg: signals.status_message.send(message=msg, expire=1) @@ -178,31 +188,30 @@ class StatusBar(urwid.WidgetWrap): REFRESHTIME = 0.5 # Timed refresh time in seconds keyctx = "" - def __init__(self, master: "mitmproxy.tools.console.master.ConsoleMaster") -> None: + def __init__(self, master: mitmproxy.tools.console.master.ConsoleMaster) -> None: self.master = master self.ib = urwid.WidgetWrap(urwid.Text("")) self.ab = ActionBar(self.master) super().__init__(urwid.Pile([self.ib, self.ab])) signals.flow_change.connect(self.sig_update) signals.update_settings.connect(self.sig_update) - signals.flowlist_change.connect(self.sig_update) master.options.changed.connect(self.sig_update) master.view.focus.sig_change.connect(self.sig_update) master.view.sig_view_add.connect(self.sig_update) self.refresh() - def refresh(self): + def refresh(self) -> None: self.redraw() signals.call_in.send(seconds=self.REFRESHTIME, callback=self.refresh) - def sig_update(self, sender, flow=None, updated=None): + def sig_update(self, *args, **kwargs) -> None: self.redraw() def keypress(self, *args, **kwargs): return self.ab.keypress(*args, **kwargs) - def get_status(self): - r = [] + def get_status(self) -> list[tuple[str, str] | str]: + r: list[tuple[str, str] | str] = [] sreplay = self.master.commands.call("replay.server.count") creplay = self.master.commands.call("replay.client.count") @@ -269,8 +278,6 @@ def get_status(self): 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: @@ -281,8 +288,11 @@ def get_status(self): if opts: r.append("[%s]" % (":".join(opts))) - if self.master.options.mode != "regular": - r.append("[%s]" % self.master.options.mode) + if self.master.options.mode != ["regular"]: + if len(self.master.options.mode) == 1: + r.append(f"[{self.master.options.mode[0]}]") + else: + r.append(f"[modes:{len(self.master.options.mode)}]") if self.master.options.scripts: r.append("[scripts:%s]" % len(self.master.options.scripts)) @@ -291,9 +301,9 @@ def get_status(self): return r - def redraw(self): + 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 @@ -307,15 +317,18 @@ def redraw(self): if self.master.commands.execute("view.properties.marked"): marked = "M" - t = [ - ("heading", (f"{arrow} {marked} [{offset}/{fc}]").ljust(11)), + t: list[tuple[str, str] | str] = [ + ("heading", f"{arrow} {marked} [{offset}/{fc}]".ljust(11)), ] - if self.master.options.server: - host = self.master.options.listen_host - if host == "0.0.0.0" or host == "": - host = "*" - boundaddr = f"[{host}:{self.master.options.listen_port}]" + 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()) @@ -330,5 +343,5 @@ def redraw(self): ) self.ib._w = status - def selectable(self): + def selectable(self) -> bool: return True diff --git a/mitmproxy/tools/console/window.py b/mitmproxy/tools/console/window.py index 90094e082e..6c2e24926b 100644 --- a/mitmproxy/tools/console/window.py +++ b/mitmproxy/tools/console/window.py @@ -1,7 +1,8 @@ -import os import re import urwid + +from mitmproxy import flow from mitmproxy.tools.console import commands from mitmproxy.tools.console import common from mitmproxy.tools.console import eventlog @@ -15,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): @@ -146,17 +142,17 @@ def __init__(self, master): signals.focus.connect(self.sig_focus) signals.flow_change.connect(self.flow_changed) signals.pop_view_state.connect(self.pop) - signals.push_view_state.connect(self.push) - self.master.options.subscribe(self.configure, ["console_layout"]) - self.master.options.subscribe(self.configure, ["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")] def focus_stack(self): return self.stacks[self.pane] - def configure(self, otions, updated): + def configure(self, options, updated): self.refresh() def refresh(self): @@ -190,8 +186,9 @@ def wrapped(idx): ) self.body = urwid.AttrWrap(w, "background") + signals.window_refresh.send() - def flow_changed(self, sender, flow): + def flow_changed(self, flow: flow.Flow) -> None: if self.master.view.focus.flow: if flow.id == self.master.view.focus.flow.id: self.focus_changed() @@ -227,7 +224,7 @@ def push(self, wname): self.view_changed() self.focus_changed() - def pop(self, *args, **kwargs): + def pop(self) -> None: """ Pop a window from the currently focused stack. If there is only one window on the stack, this prompts for exit. @@ -270,7 +267,7 @@ def current_window(self, keyctx): if t.keyctx == keyctx: return t - def sig_focus(self, sender, section): + def sig_focus(self, section): self.focus_position = section def switch(self): @@ -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..1f6d62f4db 100644 --- a/mitmproxy/tools/dump.py +++ b/mitmproxy/tools/dump.py @@ -1,17 +1,22 @@ 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 +from mitmproxy.addons import termlog class DumpMaster(master.Master): def __init__( self, options: options.Options, + loop=None, with_termlog=True, with_dumper=True, ) -> None: - super().__init__(options) + super().__init__(options, event_loop=loop) if with_termlog: self.addons.add(termlog.TermLog()) self.addons.add(*addons.default_addons()) diff --git a/mitmproxy/tools/main.py b/mitmproxy/tools/main.py index 76bd4389be..cc3c9ddeeb 100644 --- a/mitmproxy/tools/main.py +++ b/mitmproxy/tools/main.py @@ -1,16 +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): @@ -26,11 +33,10 @@ def process_options(parser, opts, args): args.termlog_verbosity = "debug" args.flow_detail = 2 - adict = {} - for n in dir(args): - if n in opts: - adict[n] = getattr(args, n) - opts.merge(adict) + adict = { + key: val for key, val in vars(args).items() if key in opts and val is not None + } + opts.update(**adict) T = TypeVar("T", bound=master.Master) @@ -40,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 @@ -48,6 +54,13 @@ def run( """ async def main() -> T: + logging.getLogger().setLevel(logging.DEBUG) + 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() @@ -84,7 +97,7 @@ async def main() -> T: sys.exit(0) if extra: if args.filter_args: - master.log.info( + logging.info( f"Only processing flows that match \"{' & '.join(args.filter_args)}\"" ) opts.update(**extra(args)) @@ -114,14 +127,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): @@ -138,7 +151,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 dbb7490789..f031d6787f 100644 --- a/mitmproxy/tools/web/app.py +++ b/mitmproxy/tools/web/app.py @@ -1,13 +1,16 @@ +from __future__ import annotations + import asyncio import hashlib import json import logging import os.path import re +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 @@ -15,7 +18,9 @@ import mitmproxy.flow import mitmproxy.tools.web.master -from mitmproxy import certs, command, contentviews +from mitmproxy import certs +from mitmproxy import command +from mitmproxy import contentviews from mitmproxy import flowfilter from mitmproxy import http from mitmproxy import io @@ -24,13 +29,16 @@ from mitmproxy import version from mitmproxy.dns import DNSFlow from mitmproxy.http import HTTPFlow -from mitmproxy.tcp import TCPFlow, TCPMessage +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 -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] @@ -101,8 +109,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) @@ -162,7 +170,7 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict: "close_reason": flow.websocket.close_reason, "timestamp_end": flow.websocket.timestamp_end, } - elif isinstance(flow, TCPFlow): + elif isinstance(flow, (TCPFlow, UDPFlow)): f["messages_meta"] = { "contentLength": sum(len(x.content) for x in flow.messages), "count": len(flow.messages), @@ -189,9 +197,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): @@ -235,11 +243,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 @@ -273,13 +281,26 @@ def get(self): class WebSocketEventBroadcaster(tornado.websocket.WebSocketHandler): # raise an error if inherited class doesn't specify its own instance. - connections: ClassVar[set] + connections: ClassVar[set[WebSocketEventBroadcaster]] + _send_tasks: ClassVar[set[asyncio.Task]] = set() def open(self): self.connections.add(self) def on_close(self): - self.connections.remove(self) + self.connections.discard(self) + + @classmethod + def send(cls, conn: WebSocketEventBroadcaster, message: bytes) -> None: + async def wrapper(): + try: + await conn.write_message(message) + except tornado.websocket.WebSocketClosedError: + cls.connections.discard(conn) + + t = asyncio.create_task(wrapper()) + cls._send_tasks.add(t) + t.add_done_callback(cls._send_tasks.remove) @classmethod def broadcast(cls, **kwargs): @@ -288,10 +309,7 @@ def broadcast(cls, **kwargs): ) for conn in cls.connections: - try: - conn.write_message(message) - except Exception: # pragma: no cover - logging.error("Error sending message", exc_info=True) + cls.send(conn, message) class ClientConnection(WebSocketEventBroadcaster): @@ -304,23 +322,35 @@ 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") - bio = BytesIO() - fw = io.FlowWriter(bio) - for f in self.view: - fw.add(f) - - self.write(bio.getvalue()) - bio.close() - - def post(self): + 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 + + def match(_) -> bool: + return True + + with BytesIO() as bio: + fw = io.FlowWriter(bio) + for f in self.view: + if match(f): + fw.add(f) + self.write(bio.getvalue()) + + 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() @@ -366,7 +396,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: @@ -454,13 +484,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] @@ -477,15 +507,15 @@ class FlowContentView(RequestHandler): def message_to_json( self, viewname: str, - message: Union[http.Message, TCPMessage, WebSocketMessage], - flow: Union[HTTPFlow, TCPFlow], - 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 ) if error: - self.master.log.error(error) + logging.error(error) if max_lines: lines = islice(lines, max_lines) @@ -494,9 +524,9 @@ 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)) + assert isinstance(flow, (HTTPFlow, TCPFlow, UDPFlow)) if self.request.arguments.get("lines"): max_lines = int(self.request.arguments["lines"][0]) @@ -504,9 +534,10 @@ 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): + elif isinstance(flow, (TCPFlow, UDPFlow)): messages = flow.messages else: raise APIError(400, f"This flow has no messages.") @@ -529,7 +560,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": [ @@ -603,22 +634,31 @@ def get(self): ) -class Conf(RequestHandler): +class State(RequestHandler): def get(self): - conf = { - "static": False, - "version": version.VERSION, - "contentViews": [v.name for v in contentviews.views if v.name != "Query"], - } - self.write(f"MITMWEB_CONF = {json.dumps(conf)};") - self.set_header("content-type", "application/javascript") + self.write( + { + "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], + } + ) + + +class GZipContentAndFlowFiles(tornado.web.GZipContentEncoding): + CONTENT_TYPES = { + "application/octet-stream", + *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__( @@ -629,6 +669,7 @@ def __init__( cookie_secret=os.urandom(256), debug=debug, autoreload=False, + transforms=[GZipContentAndFlowFiles], ) self.add_handlers("dns-rebind-protection", [(r"/.*", DnsRebind)]) @@ -664,6 +705,6 @@ def __init__( (r"/clear", ClearAll), (r"/options(?:\.json)?", Options), (r"/options/save", SaveOptions), - (r"/conf\.js", Conf), + (r"/state(?:\.json)?", State), ], ) diff --git a/mitmproxy/tools/web/master.py b/mitmproxy/tools/web/master.py index 1937fc311a..520cf4d4ea 100644 --- a/mitmproxy/tools/web/master.py +++ b/mitmproxy/tools/web/master.py @@ -1,22 +1,32 @@ +import errno +import logging + import tornado.httpserver import tornado.ioloop from mitmproxy import addons +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.contrib.tornado import patch_tornado -from mitmproxy.tools.web import app, webaddons, static_viewer +from mitmproxy.addons.proxyserver import Proxyserver +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) self.view = view.View() self.view.sig_view_add.connect(self._sig_view_add) self.view.sig_view_remove.connect(self._sig_view_remove) @@ -42,47 +52,63 @@ def __init__(self, options, with_termlog=True): errorcheck.ErrorCheck(), ) self.app = app.Application(self, self.options.web_debug) + self.proxyserver: Proxyserver = self.addons.get("proxyserver") + self.proxyserver.servers.changed.connect(self._sig_servers_changed) - def _sig_view_add(self, view, flow): + def _sig_view_add(self, flow: flow.Flow) -> None: app.ClientConnection.broadcast( resource="flows", cmd="add", data=app.flow_to_json(flow) ) - def _sig_view_update(self, view, flow): + def _sig_view_update(self, flow: flow.Flow) -> None: app.ClientConnection.broadcast( resource="flows", cmd="update", data=app.flow_to_json(flow) ) - def _sig_view_remove(self, view, flow, index): + def _sig_view_remove(self, flow: flow.Flow, index: int) -> None: app.ClientConnection.broadcast(resource="flows", cmd="remove", data=flow.id) - def _sig_view_refresh(self, view): + def _sig_view_refresh(self) -> None: app.ClientConnection.broadcast(resource="flows", cmd="reset") - def _sig_events_add(self, event_store, entry: log.LogEntry): + def _sig_events_add(self, entry: log.LogEntry) -> None: app.ClientConnection.broadcast( resource="events", cmd="add", data=app.logentry_to_json(entry) ) - def _sig_events_refresh(self, event_store): + def _sig_events_refresh(self) -> None: app.ClientConnection.broadcast(resource="events", cmd="reset") - def _sig_options_update(self, options, updated): - options_dict = optmanager.dump_dicts(options, updated) + def _sig_options_update(self, updated: set[str]) -> None: + options_dict = optmanager.dump_dicts(self.options, updated) app.ClientConnection.broadcast( resource="options", cmd="update", data=options_dict ) + def _sig_servers_changed(self) -> None: + app.ClientConnection.broadcast( + resource="state", + cmd="update", + data={"servers": [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.listen(self.options.web_port, self.options.web_host) - - self.log.info( + 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 + 2}`." + raise OSError(e.errno, message, e.filename) from e + + logger.info( f"Web server listening at http://{self.options.web_host}:{self.options.web_port}/", ) diff --git a/mitmproxy/tools/web/static/app.css b/mitmproxy/tools/web/static/app.css index f729f67b2d..1c9b4eac2e 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-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}.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} +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{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-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 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} /*# sourceMappingURL=app.css.map */ diff --git a/mitmproxy/tools/web/static/app.js b/mitmproxy/tools/web/static/app.js index afb0f93e15..11a11b85eb 100644 --- a/mitmproxy/tools/web/static/app.js +++ b/mitmproxy/tools/web/static/app.js @@ -1,66 +1,73 @@ -(()=>{var XM=Object.create;var Ec=Object.defineProperty,QM=Object.defineProperties,ZM=Object.getOwnPropertyDescriptor,JM=Object.getOwnPropertyDescriptors,eA=Object.getOwnPropertyNames,sv=Object.getOwnPropertySymbols,tA=Object.getPrototypeOf,F0=Object.prototype.hasOwnProperty,EC=Object.prototype.propertyIsEnumerable;var I0=(e,t,n)=>t in e?Ec(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,Pe=(e,t)=>{for(var n in t||(t={}))F0.call(t,n)&&I0(e,n,t[n]);if(sv)for(var n of sv(t))EC.call(t,n)&&I0(e,n,t[n]);return e},It=(e,t)=>QM(e,JM(t)),TC=e=>Ec(e,"__esModule",{value:!0}),o=(e,t)=>Ec(e,"name",{value:t,configurable:!0});var Ds=(e,t)=>{var n={};for(var l in e)F0.call(e,l)&&t.indexOf(l)<0&&(n[l]=e[l]);if(e!=null&&sv)for(var l of sv(e))t.indexOf(l)<0&&EC.call(e,l)&&(n[l]=e[l]);return n};var ur=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),kC=(e,t)=>{TC(e);for(var n in t)Ec(e,n,{get:t[n],enumerable:!0})},rA=(e,t,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let l of eA(t))!F0.call(e,l)&&l!=="default"&&Ec(e,l,{get:()=>t[l],enumerable:!(n=ZM(t,l))||n.enumerable});return e},pe=e=>rA(TC(Ec(e!=null?XM(tA(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var Tc=(e,t,n)=>(I0(e,typeof t!="symbol"?t+"":t,n),n);var Na=(e,t,n)=>new Promise((l,d)=>{var m=_=>{try{x(n.next(_))}catch(O){d(O)}},p=_=>{try{x(n.throw(_))}catch(O){d(O)}},x=_=>_.done?l(_.value):Promise.resolve(_.value).then(m,p);x((n=n.apply(e,t)).next())});var H0=ur((t2,LC)=>{"use strict";var OC=Object.getOwnPropertySymbols,nA=Object.prototype.hasOwnProperty,iA=Object.prototype.propertyIsEnumerable;function oA(e){if(e==null)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}o(oA,"toObject");function sA(){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(m){return t[m]});if(l.join("")!=="0123456789")return!1;var d={};return"abcdefghijklmnopqrst".split("").forEach(function(m){d[m]=m}),Object.keys(Object.assign({},d)).join("")==="abcdefghijklmnopqrst"}catch(m){return!1}}o(sA,"shouldUseNative");LC.exports=sA()?Object.assign:function(e,t){for(var n,l=oA(e),d,m=1;m{"use strict";var W0=H0(),kc=60103,NC=60106;bt.Fragment=60107;bt.StrictMode=60108;bt.Profiler=60114;var PC=60109,MC=60110,AC=60112;bt.Suspense=60113;var DC=60115,RC=60116;typeof Symbol=="function"&&Symbol.for&&(yo=Symbol.for,kc=yo("react.element"),NC=yo("react.portal"),bt.Fragment=yo("react.fragment"),bt.StrictMode=yo("react.strict_mode"),bt.Profiler=yo("react.profiler"),PC=yo("react.provider"),MC=yo("react.context"),AC=yo("react.forward_ref"),bt.Suspense=yo("react.suspense"),DC=yo("react.memo"),RC=yo("react.lazy"));var yo,FC=typeof Symbol=="function"&&Symbol.iterator;function lA(e){return e===null||typeof e!="object"?null:(e=FC&&e[FC]||e["@@iterator"],typeof e=="function"?e:null)}o(lA,"y");function Td(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n{"use strict";VC.exports=qC()});var ZC=ur(Dt=>{"use strict";var Lc,kd,uv,q0;typeof performance=="object"&&typeof performance.now=="function"?(KC=performance,Dt.unstable_now=function(){return KC.now()}):(V0=Date,GC=V0.now(),Dt.unstable_now=function(){return V0.now()-GC});var KC,V0,GC;typeof window=="undefined"||typeof MessageChannel!="function"?(Nc=null,K0=null,G0=o(function(){if(Nc!==null)try{var e=Dt.unstable_now();Nc(!0,e),Nc=null}catch(t){throw setTimeout(G0,0),t}},"w"),Lc=o(function(e){Nc!==null?setTimeout(Lc,0,e):(Nc=e,setTimeout(G0,0))},"f"),kd=o(function(e,t){K0=setTimeout(e,t)},"g"),uv=o(function(){clearTimeout(K0)},"h"),Dt.unstable_shouldYield=function(){return!1},q0=Dt.unstable_forceFrameRate=function(){}):(YC=window.setTimeout,XC=window.clearTimeout,typeof console!="undefined"&&(QC=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 QC!="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")),Od=!1,Ld=null,fv=-1,Y0=5,X0=0,Dt.unstable_shouldYield=function(){return Dt.unstable_now()>=X0},q0=o(function(){},"k"),Dt.unstable_forceFrameRate=function(e){0>e||125>>1,d=e[l];if(d!==void 0&&0dv(p,n))_!==void 0&&0>dv(_,p)?(e[l]=_,e[x]=n,l=x):(e[l]=p,e[m]=n,l=m);else if(_!==void 0&&0>dv(_,n))e[l]=_,e[x]=n,l=x;else break e}}return t}return null}o(pv,"K");function dv(e,t){var n=e.sortIndex-t.sortIndex;return n!==0?n:e.id-t.id}o(dv,"I");var Rs=[],Pa=[],pA=1,wo=null,jn=3,hv=!1,Zu=!1,Nd=!1;function J0(e){for(var t=Qo(Pa);t!==null;){if(t.callback===null)pv(Pa);else if(t.startTime<=e)pv(Pa),t.sortIndex=t.expirationTime,Z0(Rs,t);else break;t=Qo(Pa)}}o(J0,"T");function ew(e){if(Nd=!1,J0(e),!Zu)if(Qo(Rs)!==null)Zu=!0,Lc(tw);else{var t=Qo(Pa);t!==null&&kd(ew,t.startTime-e)}}o(ew,"U");function tw(e,t){Zu=!1,Nd&&(Nd=!1,uv()),hv=!0;var n=jn;try{for(J0(t),wo=Qo(Rs);wo!==null&&(!(wo.expirationTime>t)||e&&!Dt.unstable_shouldYield());){var l=wo.callback;if(typeof l=="function"){wo.callback=null,jn=wo.priorityLevel;var d=l(wo.expirationTime<=t);t=Dt.unstable_now(),typeof d=="function"?wo.callback=d:wo===Qo(Rs)&&pv(Rs),J0(t)}else pv(Rs);wo=Qo(Rs)}if(wo!==null)var m=!0;else{var p=Qo(Pa);p!==null&&kd(ew,p.startTime-t),m=!1}return m}finally{wo=null,jn=n,hv=!1}}o(tw,"V");var dA=q0;Dt.unstable_IdlePriority=5;Dt.unstable_ImmediatePriority=1;Dt.unstable_LowPriority=4;Dt.unstable_NormalPriority=3;Dt.unstable_Profiling=null;Dt.unstable_UserBlockingPriority=2;Dt.unstable_cancelCallback=function(e){e.callback=null};Dt.unstable_continueExecution=function(){Zu||hv||(Zu=!0,Lc(tw))};Dt.unstable_getCurrentPriorityLevel=function(){return jn};Dt.unstable_getFirstCallbackNode=function(){return Qo(Rs)};Dt.unstable_next=function(e){switch(jn){case 1:case 2:case 3:var t=3;break;default:t=jn}var n=jn;jn=t;try{return e()}finally{jn=n}};Dt.unstable_pauseExecution=function(){};Dt.unstable_requestPaint=dA;Dt.unstable_runWithPriority=function(e,t){switch(e){case 1:case 2:case 3:case 4:case 5:break;default:e=3}var n=jn;jn=e;try{return t()}finally{jn=n}};Dt.unstable_scheduleCallback=function(e,t,n){var l=Dt.unstable_now();switch(typeof n=="object"&&n!==null?(n=n.delay,n=typeof n=="number"&&0l?(e.sortIndex=n,Z0(Pa,e),Qo(Rs)===null&&e===Qo(Pa)&&(Nd?uv():Nd=!0,kd(ew,n-l))):(e.sortIndex=d,Z0(Rs,e),Zu||hv||(Zu=!0,Lc(tw))),e};Dt.unstable_wrapCallback=function(e){var t=jn;return function(){var n=jn;jn=t;try{return e.apply(this,arguments)}finally{jn=n}}}});var e_=ur((o2,JC)=>{"use strict";JC.exports=ZC()});var BE=ur(Eo=>{"use strict";var mv=De(),fr=H0(),wn=e_();function we(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;nt}return!1}o(gA,"na");function fi(e,t,n,l,d,m,p){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=m,this.removeEmptyString=p}o(fi,"B");var Pn={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){Pn[e]=new fi(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];Pn[t]=new fi(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){Pn[e]=new fi(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){Pn[e]=new fi(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){Pn[e]=new fi(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){Pn[e]=new fi(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){Pn[e]=new fi(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){Pn[e]=new fi(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){Pn[e]=new fi(e,5,!1,e.toLowerCase(),null,!1,!1)});var rw=/[\-:]([a-z])/g;function nw(e){return e[1].toUpperCase()}o(nw,"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(rw,nw);Pn[t]=new fi(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(rw,nw);Pn[t]=new fi(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(rw,nw);Pn[t]=new fi(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){Pn[e]=new fi(e,1,!1,e.toLowerCase(),null,!1,!1)});Pn.xlinkHref=new fi("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){Pn[e]=new fi(e,1,!1,e.toLowerCase(),null,!0,!0)});function iw(e,t,n,l){var d=Pn.hasOwnProperty(t)?Pn[t]:null,m=d!==null?d.type===0:l?!1:!(!(2x||d[p]!==m[x])return` -`+d[p].replace(" at new "," at ");while(1<=p&&0<=x);break}}}finally{hw=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Fd(e):""}o(wv,"Pa");function yA(e){switch(e.tag){case 5:return Fd(e.type);case 16:return Fd("Lazy");case 13:return Fd("Suspense");case 19:return Fd("SuspenseList");case 0:case 2:case 15:return e=wv(e.type,!1),e;case 11:return e=wv(e.type.render,!1),e;case 22:return e=wv(e.type._render,!1),e;case 1:return e=wv(e.type,!0),e;default:return""}}o(yA,"Qa");function Mc(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 Ma:return"Fragment";case tf:return"Portal";case Ad:return"Profiler";case ow:return"StrictMode";case Dd:return"Suspense";case gv:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case lw:return(e.displayName||"Context")+".Consumer";case sw:return(e._context.displayName||"Context")+".Provider";case vv:var t=e.render;return t=t.displayName||t.name||"",e.displayName||(t!==""?"ForwardRef("+t+")":"ForwardRef");case yv:return Mc(e.type);case uw:return Mc(e._render);case aw:t=e._payload,e=e._init;try{return Mc(e(t))}catch(n){}}return null}o(Mc,"Ra");function Aa(e){switch(typeof e){case"boolean":case"number":case"object":case"string":case"undefined":return e;default:return""}}o(Aa,"Sa");function l_(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}o(l_,"Ta");function wA(e){var t=l_(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,m=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return d.call(this)},set:function(p){l=""+p,m.call(this,p)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return l},setValue:function(p){l=""+p},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}o(wA,"Ua");function xv(e){e._valueTracker||(e._valueTracker=wA(e))}o(xv,"Va");function a_(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),l="";return e&&(l=l_(e)?e.checked?"true":"false":e.value),e=l,e!==n?(t.setValue(e),!0):!1}o(a_,"Wa");function Sv(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(Sv,"Xa");function mw(e,t){var n=t.checked;return fr({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}o(mw,"Ya");function u_(e,t){var n=t.defaultValue==null?"":t.defaultValue,l=t.checked!=null?t.checked:t.defaultChecked;n=Aa(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(u_,"Za");function f_(e,t){t=t.checked,t!=null&&iw(e,"checked",t,!1)}o(f_,"$a");function vw(e,t){f_(e,t);var n=Aa(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")?gw(e,t.type,n):t.hasOwnProperty("defaultValue")&&gw(e,t.type,Aa(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}o(vw,"ab");function c_(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(c_,"cb");function gw(e,t,n){(t!=="number"||Sv(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}o(gw,"bb");function xA(e){var t="";return mv.Children.forEach(e,function(n){n!=null&&(t+=n)}),t}o(xA,"db");function yw(e,t){return e=fr({children:void 0},t),(t=xA(t.children))&&(e.children=t),e}o(yw,"eb");function Ac(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:Aa(n)}}o(p_,"hb");function d_(e,t){var n=Aa(t.value),l=Aa(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(d_,"ib");function h_(e){var t=e.textContent;t===e._wrapperState.initialValue&&t!==""&&t!==null&&(e.value=t)}o(h_,"jb");var xw={html:"http://www.w3.org/1999/xhtml",mathml:"http://www.w3.org/1998/Math/MathML",svg:"http://www.w3.org/2000/svg"};function m_(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(m_,"lb");function Sw(e,t){return e==null||e==="http://www.w3.org/1999/xhtml"?m_(t):e==="http://www.w3.org/2000/svg"&&t==="foreignObject"?"http://www.w3.org/1999/xhtml":e}o(Sw,"mb");var Cv,v_=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!==xw.svg||"innerHTML"in e)e.innerHTML=t;else{for(Cv=Cv||document.createElement("div"),Cv.innerHTML=""+t.valueOf().toString()+"",t=Cv.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Id(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}o(Id,"pb");var Hd={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},SA=["Webkit","ms","Moz","O"];Object.keys(Hd).forEach(function(e){SA.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Hd[t]=Hd[e]})});function g_(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Hd.hasOwnProperty(e)&&Hd[e]?(""+t).trim():t+"px"}o(g_,"sb");function y_(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var l=n.indexOf("--")===0,d=g_(n,t[n],l);n==="float"&&(n="cssFloat"),l?e.setProperty(n,d):e[n]=d}}o(y_,"tb");var CA=fr({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 Cw(e,t){if(t){if(CA[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(Cw,"vb");function _w(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(_w,"wb");function bw(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}o(bw,"xb");var Ew=null,Dc=null,Rc=null;function w_(e){if(e=rh(e)){if(typeof Ew!="function")throw Error(we(280));var t=e.stateNode;t&&(t=zv(t),Ew(e.stateNode,e.type,t))}}o(w_,"Bb");function x_(e){Dc?Rc?Rc.push(e):Rc=[e]:Dc=e}o(x_,"Eb");function S_(){if(Dc){var e=Dc,t=Rc;if(Rc=Dc=null,w_(e),t)for(e=0;el?0:1<n;n++)t.push(e);return t}o(Hw,"Zc");function Lv(e,t,n){e.pendingLanes|=t;var l=t-1;e.suspendedLanes&=l,e.pingedLanes&=l,e=e.eventTimes,t=31-Ia(t),e[t]=n}o(Lv,"$c");var Ia=Math.clz32?Math.clz32:HA,FA=Math.log,IA=Math.LN2;function HA(e){return e===0?32:31-(FA(e)/IA|0)|0}o(HA,"ad");var WA=wn.unstable_UserBlockingPriority,BA=wn.unstable_runWithPriority,Nv=!0;function UA(e,t,n,l){rf||kw();var d=Ww,m=rf;rf=!0;try{C_(d,e,t,n,l)}finally{(rf=m)||Lw()}}o(UA,"gd");function $A(e,t,n,l){BA(WA,Ww.bind(null,e,t,n,l))}o($A,"id");function Ww(e,t,n,l){if(Nv){var d;if((d=(t&4)==0)&&0=Yd),G_=String.fromCharCode(32),Y_=!1;function X_(e,t){switch(e){case"keyup":return cD.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}o(X_,"ge");function Q_(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}o(Q_,"he");var Uc=!1;function dD(e,t){switch(e){case"compositionend":return Q_(t);case"keypress":return t.which!==32?null:(Y_=!0,G_);case"textInput":return e=t.data,e===G_&&Y_?null:e;default:return null}}o(dD,"je");function hD(e,t){if(Uc)return e==="compositionend"||!Kw&&X_(e,t)?(e=$_(),Pv=Uw=Ha=null,Uc=!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=nb(n)}}o(ib,"Le");function ob(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?ob(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}o(ob,"Me");function sb(){for(var e=window,t=Sv();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=Sv(e.document)}return t}o(sb,"Ne");function Yw(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(Yw,"Oe");var bD=Ll&&"documentMode"in document&&11>=document.documentMode,$c=null,Xw=null,Jd=null,Qw=!1;function lb(e,t,n){var l=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Qw||$c==null||$c!==Sv(l)||(l=$c,"selectionStart"in l&&Yw(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}),Jd&&Zd(Jd,l)||(Jd=l,l=Wv(Xw,"onSelect"),0Kc||(e.current=i1[Kc],i1[Kc]=null,Kc--)}o(ir,"H");function xr(e,t){Kc++,i1[Kc]=e.current,e.current=t}o(xr,"I");var Ua={},qn=Ba(Ua),Ni=Ba(!1),sf=Ua;function Gc(e,t){var n=e.type.contextTypes;if(!n)return Ua;var l=e.stateNode;if(l&&l.__reactInternalMemoizedUnmaskedChildContext===t)return l.__reactInternalMemoizedMaskedChildContext;var d={},m;for(m in n)d[m]=t[m];return l&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=d),d}o(Gc,"Ef");function Pi(e){return e=e.childContextTypes,e!=null}o(Pi,"Ff");function jv(){ir(Ni),ir(qn)}o(jv,"Gf");function Cb(e,t,n){if(qn.current!==Ua)throw Error(we(168));xr(qn,t),xr(Ni,n)}o(Cb,"Hf");function _b(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,Mc(t)||"Unknown",d));return fr({},n,l)}o(_b,"If");function qv(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Ua,sf=qn.current,xr(qn,e),xr(Ni,Ni.current),!0}o(qv,"Jf");function bb(e,t,n){var l=e.stateNode;if(!l)throw Error(we(169));n?(e=_b(e,t,sf),l.__reactInternalMemoizedMergedChildContext=e,ir(Ni),ir(qn),xr(qn,e)):ir(Ni),xr(Ni,n)}o(bb,"Kf");var o1=null,lf=null,kD=wn.unstable_runWithPriority,s1=wn.unstable_scheduleCallback,l1=wn.unstable_cancelCallback,OD=wn.unstable_shouldYield,Eb=wn.unstable_requestPaint,a1=wn.unstable_now,LD=wn.unstable_getCurrentPriorityLevel,Vv=wn.unstable_ImmediatePriority,Tb=wn.unstable_UserBlockingPriority,kb=wn.unstable_NormalPriority,Ob=wn.unstable_LowPriority,Lb=wn.unstable_IdlePriority,u1={},ND=Eb!==void 0?Eb:function(){},Nl=null,Kv=null,f1=!1,Nb=a1(),Vn=1e4>Nb?a1:function(){return a1()-Nb};function Yc(){switch(LD()){case Vv:return 99;case Tb:return 98;case kb:return 97;case Ob:return 96;case Lb:return 95;default:throw Error(we(332))}}o(Yc,"eg");function Pb(e){switch(e){case 99:return Vv;case 98:return Tb;case 97:return kb;case 96:return Ob;case 95:return Lb;default:throw Error(we(332))}}o(Pb,"fg");function af(e,t){return e=Pb(e),kD(e,t)}o(af,"gg");function nh(e,t,n){return e=Pb(e),s1(e,t,n)}o(nh,"hg");function Is(){if(Kv!==null){var e=Kv;Kv=null,l1(e)}Mb()}o(Is,"ig");function Mb(){if(!f1&&Nl!==null){f1=!0;var e=0;try{var t=Nl;af(99,function(){for(;ede?(ge=ie,ie=null):ge=ie.sibling;var xe=U(F,ie,R[de],K);if(xe===null){ie===null&&(ie=ge);break}e&&ie&&xe.alternate===null&&t(F,ie),M=m(xe,M,de),ue===null?V=xe:ue.sibling=xe,ue=xe,ie=ge}if(de===R.length)return n(F,ie),V;if(ie===null){for(;dede?(ge=ie,ie=null):ge=ie.sibling;var qe=U(F,ie,xe.value,K);if(qe===null){ie===null&&(ie=ge);break}e&&ie&&qe.alternate===null&&t(F,ie),M=m(qe,M,de),ue===null?V=qe:ue.sibling=qe,ue=qe,ie=ge}if(xe.done)return n(F,ie),V;if(ie===null){for(;!xe.done;de++,xe=R.next())xe=Y(F,xe.value,K),xe!==null&&(M=m(xe,M,de),ue===null?V=xe:ue.sibling=xe,ue=xe);return V}for(ie=l(F,ie);!xe.done;de++,xe=R.next())xe=X(ie,F,de,xe.value,K),xe!==null&&(e&&xe.alternate!==null&&ie.delete(xe.key===null?de:xe.key),M=m(xe,M,de),ue===null?V=xe:ue.sibling=xe,ue=xe);return e&&ie.forEach(function(et){return t(F,et)}),V}return o(Q,"w"),function(F,M,R,K){var V=typeof R=="object"&&R!==null&&R.type===Ma&&R.key===null;V&&(R=R.props.children);var ue=typeof R=="object"&&R!==null;if(ue)switch(R.$$typeof){case Md:e:{for(ue=R.key,V=M;V!==null;){if(V.key===ue){switch(V.tag){case 7:if(R.type===Ma){n(F,V.sibling),M=d(V,R.props.children),M.return=F,F=M;break e}break;default:if(V.elementType===R.type){n(F,V.sibling),M=d(V,R.props),M.ref=oh(F,V,R),M.return=F,F=M;break e}}n(F,V);break}else t(F,V);V=V.sibling}R.type===Ma?(M=op(R.props.children,F.mode,K,R.key),M.return=F,F=M):(K=yg(R.type,R.key,R.props,null,F.mode,K),K.ref=oh(F,M,R),K.return=F,F=K)}return p(F);case tf:e:{for(V=R.key;M!==null;){if(M.key===V)if(M.tag===4&&M.stateNode.containerInfo===R.containerInfo&&M.stateNode.implementation===R.implementation){n(F,M.sibling),M=d(M,R.children||[]),M.return=F,F=M;break e}else{n(F,M);break}else t(F,M);M=M.sibling}M=Q1(R,F.mode,K),M.return=F,F=M}return p(F)}if(typeof R=="string"||typeof R=="number")return R=""+R,M!==null&&M.tag===6?(n(F,M.sibling),M=d(M,R),M.return=F,F=M):(n(F,M),M=X1(R,F.mode,K),M.return=F,F=M),p(F);if(Jv(R))return te(F,M,R,K);if(Rd(R))return Q(F,M,R,K);if(ue&&eg(F,R),typeof R=="undefined"&&!V)switch(F.tag){case 1:case 22:case 0:case 11:case 15:throw Error(we(152,Mc(F.type)||"Component"))}return n(F,M)}}o(Ub,"Sg");var tg=Ub(!0),$b=Ub(!1),sh={},Hs=Ba(sh),lh=Ba(sh),ah=Ba(sh);function uf(e){if(e===sh)throw Error(we(174));return e}o(uf,"dh");function m1(e,t){switch(xr(ah,t),xr(lh,e),xr(Hs,sh),e=t.nodeType,e){case 9:case 11:t=(t=t.documentElement)?t.namespaceURI:Sw(null,"");break;default:e=e===8?t.parentNode:t,t=e.namespaceURI||null,e=e.tagName,t=Sw(t,e)}ir(Hs),xr(Hs,t)}o(m1,"eh");function Zc(){ir(Hs),ir(lh),ir(ah)}o(Zc,"fh");function zb(e){uf(ah.current);var t=uf(Hs.current),n=Sw(t,e.type);t!==n&&(xr(lh,e),xr(Hs,n))}o(zb,"gh");function v1(e){lh.current===e&&(ir(Hs),ir(lh))}o(v1,"hh");var Sr=Ba(0);function rg(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(rg,"ih");var Pl=null,qa=null,Ws=!1;function jb(e,t){var n=bo(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(jb,"mh");function qb(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(qb,"oh");function g1(e){if(Ws){var t=qa;if(t){var n=t;if(!qb(e,t)){if(t=jc(n.nextSibling),!t||!qb(e,t)){e.flags=e.flags&-1025|2,Ws=!1,Pl=e;return}jb(Pl,n)}Pl=e,qa=jc(t.firstChild)}else e.flags=e.flags&-1025|2,Ws=!1,Pl=e}}o(g1,"ph");function Vb(e){for(e=e.return;e!==null&&e.tag!==5&&e.tag!==3&&e.tag!==13;)e=e.return;Pl=e}o(Vb,"qh");function ng(e){if(e!==Pl)return!1;if(!Ws)return Vb(e),Ws=!0,!1;var t=e.type;if(e.tag!==5||t!=="head"&&t!=="body"&&!t1(t,e.memoizedProps))for(t=qa;t;)jb(e,t),t=jc(t.nextSibling);if(Vb(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=jc(e.nextSibling);break e}t--}else n!=="$"&&n!=="$!"&&n!=="$?"||t++}e=e.nextSibling}qa=null}}else qa=Pl?jc(e.stateNode.nextSibling):null;return!0}o(ng,"rh");function y1(){qa=Pl=null,Ws=!1}o(y1,"sh");var Jc=[];function w1(){for(var e=0;em))throw Error(we(301));m+=1,Mn=Kn=null,t.updateQueue=null,uh.current=RD,e=n(l,d)}while(ch)}if(uh.current=ag,t=Kn!==null&&Kn.next!==null,fh=0,Mn=Kn=Pr=null,ig=!1,t)throw Error(we(300));return e}o(S1,"Ch");function ff(){var e={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};return Mn===null?Pr.memoizedState=Mn=e:Mn=Mn.next=e,Mn}o(ff,"Hh");function cf(){if(Kn===null){var e=Pr.alternate;e=e!==null?e.memoizedState:null}else e=Kn.next;var t=Mn===null?Pr.memoizedState:Mn.next;if(t!==null)Mn=t,Kn=e;else{if(e===null)throw Error(we(310));Kn=e,e={memoizedState:Kn.memoizedState,baseState:Kn.baseState,baseQueue:Kn.baseQueue,queue:Kn.queue,next:null},Mn===null?Pr.memoizedState=Mn=e:Mn=Mn.next=e}return Mn}o(cf,"Ih");function Bs(e,t){return typeof t=="function"?t(e):t}o(Bs,"Jh");function ph(e){var t=cf(),n=t.queue;if(n===null)throw Error(we(311));n.lastRenderedReducer=e;var l=Kn,d=l.baseQueue,m=n.pending;if(m!==null){if(d!==null){var p=d.next;d.next=m.next,m.next=p}l.baseQueue=d=m,n.pending=null}if(d!==null){d=d.next,l=l.baseState;var x=p=m=null,_=d;do{var O=_.lane;if((fh&O)===O)x!==null&&(x=x.next={lane:0,action:_.action,eagerReducer:_.eagerReducer,eagerState:_.eagerState,next:null}),l=_.eagerReducer===e?_.eagerState:e(l,_.action);else{var D={lane:O,action:_.action,eagerReducer:_.eagerReducer,eagerState:_.eagerState,next:null};x===null?(p=x=D,m=l):x=x.next=D,Pr.lanes|=O,vh|=O}_=_.next}while(_!==null&&_!==d);x===null?m=l:x.next=p,xo(l,t.memoizedState)||(Jo=!0),t.memoizedState=l,t.baseState=m,t.baseQueue=x,n.lastRenderedState=l}return[t.memoizedState,n.dispatch]}o(ph,"Kh");function dh(e){var t=cf(),n=t.queue;if(n===null)throw Error(we(311));n.lastRenderedReducer=e;var l=n.dispatch,d=n.pending,m=t.memoizedState;if(d!==null){n.pending=null;var p=d=d.next;do m=e(m,p.action),p=p.next;while(p!==d);xo(m,t.memoizedState)||(Jo=!0),t.memoizedState=m,t.baseQueue===null&&(t.baseState=m),n.lastRenderedState=m}return[m,l]}o(dh,"Lh");function Kb(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=(fh&e)===e)&&(t._workInProgressVersionPrimary=l,Jc.push(t))),e)return n(t._source);throw Jc.push(t),Error(we(350))}o(Kb,"Mh");function Gb(e,t,n,l){var d=ci;if(d===null)throw Error(we(349));var m=t._getVersion,p=m(t._source),x=uh.current,_=x.useState(function(){return Kb(d,t,n)}),O=_[1],D=_[0];_=Mn;var Y=e.memoizedState,U=Y.refs,X=U.getSnapshot,te=Y.source;Y=Y.subscribe;var Q=Pr;return e.memoizedState={refs:U,source:t,subscribe:l},x.useEffect(function(){U.getSnapshot=n,U.setSnapshot=O;var F=m(t._source);if(!xo(p,F)){F=n(t._source),xo(D,F)||(O(F),F=Ka(Q),d.mutableReadLanes|=F&d.pendingLanes),F=d.mutableReadLanes,d.entangledLanes|=F;for(var M=d.entanglements,R=F;0n?98:n,function(){e(!0)}),af(97<\/script>",e=e.removeChild(e.firstChild)):typeof l.is=="string"?e=p.createElement(n,{is:l.is}):(e=p.createElement(n),n==="select"&&(p=e,l.multiple?p.multiple=!0:l.size&&(p.size=l.size))):e=p.createElementNS(e,n),e[Wa]=t,e[$v]=l,mE(e,t,!1,!1),t.stateNode=e,p=_w(n,l),n){case"dialog":nr("cancel",e),nr("close",e),d=l;break;case"iframe":case"object":case"embed":nr("load",e),d=l;break;case"video":case"audio":for(d=0;dU1&&(t.flags|=64,m=!0,mh(l,!1),t.lanes=33554432)}else{if(!m)if(e=rg(p),e!==null){if(t.flags|=64,m=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),mh(l,!0),l.tail===null&&l.tailMode==="hidden"&&!p.alternate&&!Ws)return t=t.lastEffect=l.lastEffect,t!==null&&(t.nextEffect=null),null}else 2*Vn()-l.renderingStartTime>U1&&n!==1073741824&&(t.flags|=64,m=!0,mh(l,!1),t.lanes=33554432);l.isBackwards?(p.sibling=t.child,t.child=p):(n=l.last,n!==null?n.sibling=p:t.child=p,l.last=p)}return l.tail!==null?(n=l.tail,l.rendering=n,l.tail=n.sibling,l.lastEffect=t.lastEffect,l.renderingStartTime=Vn(),n.sibling=null,t=Sr.current,xr(Sr,m?t&1|2:t&1),n):null;case 23:case 24:return K1(),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(ID,"Gi");function HD(e){switch(e.tag){case 1:Pi(e.type)&&jv();var t=e.flags;return t&4096?(e.flags=t&-4097|64,e):null;case 3:if(Zc(),ir(Ni),ir(qn),w1(),t=e.flags,(t&64)!=0)throw Error(we(285));return e.flags=t&-4097|64,e;case 5:return v1(e),null;case 13:return ir(Sr),t=e.flags,t&4096?(e.flags=t&-4097|64,e):null;case 19:return ir(Sr),null;case 4:return Zc(),null;case 10:return p1(e),null;case 23:case 24:return K1(),null;default:return null}}o(HD,"Li");function P1(e,t){try{var n="",l=t;do n+=yA(l),l=l.return;while(l);var d=n}catch(m){d=` -Error generating stack: `+m.message+` -`+m.stack}return{value:e,source:t,stack:d}}o(P1,"Mi");function M1(e,t){try{console.error(t.value)}catch(n){setTimeout(function(){throw n})}}o(M1,"Ni");var WD=typeof WeakMap=="function"?WeakMap:Map;function yE(e,t,n){n=za(-1,n),n.tag=3,n.payload={element:null};var l=t.value;return n.callback=function(){pg||(pg=!0,$1=l),M1(e,t)},n}o(yE,"Pi");function wE(e,t,n){n=za(-1,n),n.tag=3;var l=e.type.getDerivedStateFromError;if(typeof l=="function"){var d=t.value;n.payload=function(){return M1(e,t),l(d)}}var m=e.stateNode;return m!==null&&typeof m.componentDidCatch=="function"&&(n.callback=function(){typeof l!="function"&&(Us===null?Us=new Set([this]):Us.add(this),M1(e,t));var p=t.stack;this.componentDidCatch(t.value,{componentStack:p!==null?p:""})}),n}o(wE,"Si");var BD=typeof WeakSet=="function"?WeakSet:Set;function xE(e){var t=e.ref;if(t!==null)if(typeof t=="function")try{t(null)}catch(n){Xa(e,n)}else t.current=null}o(xE,"Vi");function UD(e,t){switch(t.tag){case 0:case 11:case 15:case 22:return;case 1:if(t.flags&256&&e!==null){var n=e.memoizedProps,l=e.memoizedState;e=t.stateNode,t=e.getSnapshotBeforeUpdate(t.elementType===t.type?n:Zo(t.type,n),l),e.__reactInternalSnapshotBeforeUpdate=t}return;case 3:t.flags&256&&r1(t.stateNode.containerInfo);return;case 5:case 6:case 4:case 17:return}throw Error(we(163))}o(UD,"Xi");function $D(e,t,n){switch(n.tag){case 0:case 11:case 15:case 22:if(t=n.updateQueue,t=t!==null?t.lastEffect:null,t!==null){e=t=t.next;do{if((e.tag&3)==3){var l=e.create;e.destroy=l()}e=e.next}while(e!==t)}if(t=n.updateQueue,t=t!==null?t.lastEffect:null,t!==null){e=t=t.next;do{var d=e;l=d.next,d=d.tag,(d&4)!=0&&(d&1)!=0&&(RE(n,e),XD(n,e)),e=l}while(e!==t)}return;case 1:e=n.stateNode,n.flags&4&&(t===null?e.componentDidMount():(l=n.elementType===n.type?t.memoizedProps:Zo(n.type,t.memoizedProps),e.componentDidUpdate(l,t.memoizedState,e.__reactInternalSnapshotBeforeUpdate))),t=n.updateQueue,t!==null&&Fb(n,t,e);return;case 3:if(t=n.updateQueue,t!==null){if(e=null,n.child!==null)switch(n.child.tag){case 5:e=n.child.stateNode;break;case 1:e=n.child.stateNode}Fb(n,t,e)}return;case 5:e=n.stateNode,t===null&&n.flags&4&&gb(n.type,n.memoizedProps)&&e.focus();return;case 6:return;case 4:return;case 12:return;case 13:n.memoizedState===null&&(n=n.alternate,n!==null&&(n=n.memoizedState,n!==null&&(n=n.dehydrated,n!==null&&D_(n))));return;case 19:case 17:case 20:case 21:case 23:case 24:return}throw Error(we(163))}o($D,"Yi");function SE(e,t){for(var n=e;;){if(n.tag===5){var l=n.stateNode;if(t)l=l.style,typeof l.setProperty=="function"?l.setProperty("display","none","important"):l.display="none";else{l=n.stateNode;var d=n.memoizedProps.style;d=d!=null&&d.hasOwnProperty("display")?d.display:null,l.style.display=g_("display",d)}}else if(n.tag===6)n.stateNode.nodeValue=t?"":n.memoizedProps;else if((n.tag!==23&&n.tag!==24||n.memoizedState===null||n===e)&&n.child!==null){n.child.return=n,n=n.child;continue}if(n===e)break;for(;n.sibling===null;){if(n.return===null||n.return===e)return;n=n.return}n.sibling.return=n.return,n=n.sibling}}o(SE,"aj");function CE(e,t){if(lf&&typeof lf.onCommitFiberUnmount=="function")try{lf.onCommitFiberUnmount(o1,t)}catch(m){}switch(t.tag){case 0:case 11:case 14:case 15:case 22:if(e=t.updateQueue,e!==null&&(e=e.lastEffect,e!==null)){var n=e=e.next;do{var l=n,d=l.destroy;if(l=l.tag,d!==void 0)if((l&4)!=0)RE(t,n);else{l=t;try{d()}catch(m){Xa(l,m)}}n=n.next}while(n!==e)}break;case 1:if(xE(t),e=t.stateNode,typeof e.componentWillUnmount=="function")try{e.props=t.memoizedProps,e.state=t.memoizedState,e.componentWillUnmount()}catch(m){Xa(t,m)}break;case 5:xE(t);break;case 4:TE(e,t)}}o(CE,"bj");function _E(e){e.alternate=null,e.child=null,e.dependencies=null,e.firstEffect=null,e.lastEffect=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.return=null,e.updateQueue=null}o(_E,"dj");function bE(e){return e.tag===5||e.tag===3||e.tag===4}o(bE,"ej");function EE(e){e:{for(var t=e.return;t!==null;){if(bE(t))break e;t=t.return}throw Error(we(160))}var n=t;switch(t=n.stateNode,n.tag){case 5:var l=!1;break;case 3:t=t.containerInfo,l=!0;break;case 4:t=t.containerInfo,l=!0;break;default:throw Error(we(161))}n.flags&16&&(Id(t,""),n.flags&=-17);e:t:for(n=e;;){for(;n.sibling===null;){if(n.return===null||bE(n.return)){n=null;break e}n=n.return}for(n.sibling.return=n.return,n=n.sibling;n.tag!==5&&n.tag!==6&&n.tag!==18;){if(n.flags&2||n.child===null||n.tag===4)continue t;n.child.return=n,n=n.child}if(!(n.flags&2)){n=n.stateNode;break e}}l?A1(e,n,t):D1(e,n,t)}o(EE,"fj");function A1(e,t,n){var l=e.tag,d=l===5||l===6;if(d)e=d?e.stateNode:e.stateNode.instance,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=Bv));else if(l!==4&&(e=e.child,e!==null))for(A1(e,t,n),e=e.sibling;e!==null;)A1(e,t,n),e=e.sibling}o(A1,"gj");function D1(e,t,n){var l=e.tag,d=l===5||l===6;if(d)e=d?e.stateNode:e.stateNode.instance,t?n.insertBefore(e,t):n.appendChild(e);else if(l!==4&&(e=e.child,e!==null))for(D1(e,t,n),e=e.sibling;e!==null;)D1(e,t,n),e=e.sibling}o(D1,"hj");function TE(e,t){for(var n=t,l=!1,d,m;;){if(!l){l=n.return;e:for(;;){if(l===null)throw Error(we(160));switch(d=l.stateNode,l.tag){case 5:m=!1;break e;case 3:d=d.containerInfo,m=!0;break e;case 4:d=d.containerInfo,m=!0;break e}l=l.return}l=!0}if(n.tag===5||n.tag===6){e:for(var p=e,x=n,_=x;;)if(CE(p,_),_.child!==null&&_.tag!==4)_.child.return=_,_=_.child;else{if(_===x)break e;for(;_.sibling===null;){if(_.return===null||_.return===x)break e;_=_.return}_.sibling.return=_.return,_=_.sibling}m?(p=d,x=n.stateNode,p.nodeType===8?p.parentNode.removeChild(x):p.removeChild(x)):d.removeChild(n.stateNode)}else if(n.tag===4){if(n.child!==null){d=n.stateNode.containerInfo,m=!0,n.child.return=n,n=n.child;continue}}else if(CE(e,n),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.tag===4&&(l=!1)}n.sibling.return=n.return,n=n.sibling}}o(TE,"cj");function R1(e,t){switch(t.tag){case 0:case 11:case 14:case 15:case 22:var n=t.updateQueue;if(n=n!==null?n.lastEffect:null,n!==null){var l=n=n.next;do(l.tag&3)==3&&(e=l.destroy,l.destroy=void 0,e!==void 0&&e()),l=l.next;while(l!==n)}return;case 1:return;case 5:if(n=t.stateNode,n!=null){l=t.memoizedProps;var d=e!==null?e.memoizedProps:l;e=t.type;var m=t.updateQueue;if(t.updateQueue=null,m!==null){for(n[$v]=l,e==="input"&&l.type==="radio"&&l.name!=null&&f_(n,l),_w(e,d),t=_w(e,l),d=0;dd&&(d=p),n&=~m}if(n=d,n=Vn()-n,n=(120>n?120:480>n?480:1080>n?1080:1920>n?1920:3e3>n?3e3:4320>n?4320:1960*jD(n/1960))-n,10{var cD=Object.create;var qc=Object.defineProperty,pD=Object.defineProperties,dD=Object.getOwnPropertyDescriptor,hD=Object.getOwnPropertyDescriptors,mD=Object.getOwnPropertyNames,Dg=Object.getOwnPropertySymbols,gD=Object.getPrototypeOf,Sw=Object.prototype.hasOwnProperty,Bb=Object.prototype.propertyIsEnumerable;var Cw=(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={}))Sw.call(t,n)&&Cw(e,n,t[n]);if(Dg)for(var n of Dg(t))Bb.call(t,n)&&Cw(e,n,t[n]);return e},Pt=(e,t)=>pD(e,hD(t)),Hb=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)Sw.call(e,l)&&t.indexOf(l)<0&&(n[l]=e[l]);if(e!=null&&Dg)for(var l of Dg(e))t.indexOf(l)<0&&Bb.call(e,l)&&(n[l]=e[l]);return n};var Ue=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),Wb=(e,t)=>{Hb(e);for(var n in t)qc(e,n,{get:t[n],enumerable:!0})},vD=(e,t,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let l of mD(t))!Sw.call(e,l)&&l!=="default"&&qc(e,l,{get:()=>t[l],enumerable:!(n=dD(t,l))||n.enumerable});return e},fe=e=>vD(Hb(qc(e!=null?cD(gD(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var Vc=(e,t,n)=>(Cw(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 bw=Ue((cH,zb)=>{"use strict";var Ub=Object.getOwnPropertySymbols,yD=Object.prototype.hasOwnProperty,wD=Object.prototype.propertyIsEnumerable;function xD(e){if(e==null)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}o(xD,"toObject");function SD(){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(SD,"shouldUseNative");zb.exports=SD()?Object.assign:function(e,t){for(var n,l=xD(e),d,h=1;h{"use strict";var Ew=bw(),Kc=60103,$b=60106;Et.Fragment=60107;Et.StrictMode=60108;Et.Profiler=60114;var jb=60109,qb=60110,Vb=60112;Et.Suspense=60113;var Kb=60115,Gb=60116;typeof Symbol=="function"&&Symbol.for&&(Co=Symbol.for,Kc=Co("react.element"),$b=Co("react.portal"),Et.Fragment=Co("react.fragment"),Et.StrictMode=Co("react.strict_mode"),Et.Profiler=Co("react.profiler"),jb=Co("react.provider"),qb=Co("react.context"),Vb=Co("react.forward_ref"),Et.Suspense=Co("react.suspense"),Kb=Co("react.memo"),Gb=Co("react.lazy"));var Co,Yb=typeof Symbol=="function"&&Symbol.iterator;function CD(e){return e===null||typeof e!="object"?null:(e=Yb&&e[Yb]||e["@@iterator"],typeof e=="function"?e:null)}o(CD,"y");function Yd(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n{"use strict";oE.exports=iE()});var cE=Ue(Rt=>{"use strict";var Yc,Xd,Fg,Pw;typeof performance=="object"&&typeof performance.now=="function"?(sE=performance,Rt.unstable_now=function(){return sE.now()}):(Ow=Date,lE=Ow.now(),Rt.unstable_now=function(){return Ow.now()-lE});var sE,Ow,lE;typeof window=="undefined"||typeof MessageChannel!="function"?(Xc=null,Mw=null,Aw=o(function(){if(Xc!==null)try{var e=Rt.unstable_now();Xc(!0,e),Xc=null}catch(t){throw setTimeout(Aw,0),t}},"w"),Yc=o(function(e){Xc!==null?setTimeout(Yc,0,e):(Xc=e,setTimeout(Aw,0))},"f"),Xd=o(function(e,t){Mw=setTimeout(e,t)},"g"),Fg=o(function(){clearTimeout(Mw)},"h"),Rt.unstable_shouldYield=function(){return!1},Pw=Rt.unstable_forceFrameRate=function(){}):(aE=window.setTimeout,uE=window.clearTimeout,typeof console!="undefined"&&(fE=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 fE!="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,Bg=-1,Dw=5,Rw=0,Rt.unstable_shouldYield=function(){return Rt.unstable_now()>=Rw},Pw=o(function(){},"k"),Rt.unstable_forceFrameRate=function(e){0>e||125>>1,d=e[l];if(d!==void 0&&0Ug(c,n))C!==void 0&&0>Ug(C,c)?(e[l]=C,e[v]=n,l=v):(e[l]=c,e[h]=n,l=h);else if(C!==void 0&&0>Ug(C,n))e[l]=C,e[v]=n,l=v;else break e}}return t}return null}o(Wg,"K");function Ug(e,t){var n=e.sortIndex-t.sortIndex;return n!==0?n:e.id-t.id}o(Ug,"I");var Us=[],Fa=[],kD=1,bo=null,Yn=3,zg=!1,df=!1,Jd=!1;function Bw(e){for(var t=os(Fa);t!==null;){if(t.callback===null)Wg(Fa);else if(t.startTime<=e)Wg(Fa),t.sortIndex=t.expirationTime,Fw(Us,t);else break;t=os(Fa)}}o(Bw,"T");function Hw(e){if(Jd=!1,Bw(e),!df)if(os(Us)!==null)df=!0,Yc(Ww);else{var t=os(Fa);t!==null&&Xd(Hw,t.startTime-e)}}o(Hw,"U");function Ww(e,t){df=!1,Jd&&(Jd=!1,Fg()),zg=!0;var n=Yn;try{for(Bw(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)&&Wg(Us),Bw(t)}else Wg(Us);bo=os(Us)}if(bo!==null)var h=!0;else{var c=os(Fa);c!==null&&Xd(Hw,c.startTime-t),h=!1}return h}finally{bo=null,Yn=n,zg=!1}}o(Ww,"V");var ND=Pw;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||zg||(df=!0,Yc(Ww))};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=ND;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,Fw(Fa,e),os(Us)===null&&e===os(Fa)&&(Jd?Fg():Jd=!0,Xd(Hw,n-l))):(e.sortIndex=d,Fw(Us,e),df||zg||(df=!0,Yc(Ww))),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 dE=Ue((mH,pE)=>{"use strict";pE.exports=cE()});var JT=Ue(Lo=>{"use strict";var $g=Oe(),hr=bw(),Tn=dE();function we(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;nt}return!1}o(MD,"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 Uw=/[\-:]([a-z])/g;function zw(e){return e[1].toUpperCase()}o(zw,"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(Uw,zw);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(Uw,zw);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(Uw,zw);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 $w(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{Jw=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?oh(e):""}o(Kg,"Pa");function AD(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=Kg(e.type,!1),e;case 11:return e=Kg(e.type.render,!1),e;case 22:return e=Kg(e.type._render,!1),e;case 1:return e=Kg(e.type,!0),e;default:return""}}o(AD,"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 jw:return"StrictMode";case nh:return"Suspense";case qg:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Vw:return(e.displayName||"Context")+".Consumer";case qw:return(e._context.displayName||"Context")+".Provider";case jg:var t=e.render;return t=t.displayName||t.name||"",e.displayName||(t!==""?"ForwardRef("+t+")":"ForwardRef");case Vg:return Zc(e.type);case Gw:return Zc(e._render);case Kw: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 xE(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}o(xE,"Ta");function DD(e){var t=xE(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(DD,"Ua");function Gg(e){e._valueTracker||(e._valueTracker=DD(e))}o(Gg,"Va");function SE(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),l="";return e&&(l=xE(e)?e.checked?"true":"false":e.value),e=l,e!==n?(t.setValue(e),!0):!1}o(SE,"Wa");function Yg(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(Yg,"Xa");function e1(e,t){var n=t.checked;return hr({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}o(e1,"Ya");function CE(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(CE,"Za");function bE(e,t){t=t.checked,t!=null&&$w(e,"checked",t,!1)}o(bE,"$a");function t1(e,t){bE(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")?r1(e,t.type,n):t.hasOwnProperty("defaultValue")&&r1(e,t.type,Ha(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}o(t1,"ab");function EE(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(EE,"cb");function r1(e,t,n){(t!=="number"||Yg(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}o(r1,"bb");function RD(e){var t="";return $g.Children.forEach(e,function(n){n!=null&&(t+=n)}),t}o(RD,"db");function n1(e,t){return e=hr({children:void 0},t),(t=RD(t.children))&&(e.children=t),e}o(n1,"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(_E,"hb");function TE(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(TE,"ib");function kE(e){var t=e.textContent;t===e._wrapperState.initialValue&&t!==""&&t!==null&&(e.value=t)}o(kE,"jb");var o1={html:"http://www.w3.org/1999/xhtml",mathml:"http://www.w3.org/1998/Math/MathML",svg:"http://www.w3.org/2000/svg"};function NE(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(NE,"lb");function s1(e,t){return e==null||e==="http://www.w3.org/1999/xhtml"?NE(t):e==="http://www.w3.org/2000/svg"&&t==="foreignObject"?"http://www.w3.org/1999/xhtml":e}o(s1,"mb");var Xg,LE=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!==o1.svg||"innerHTML"in e)e.innerHTML=t;else{for(Xg=Xg||document.createElement("div"),Xg.innerHTML=""+t.valueOf().toString()+"",t=Xg.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},ID=["Webkit","ms","Moz","O"];Object.keys(lh).forEach(function(e){ID.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),lh[t]=lh[e]})});function PE(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(PE,"sb");function OE(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var l=n.indexOf("--")===0,d=PE(n,t[n],l);n==="float"&&(n="cssFloat"),l?e.setProperty(n,d):e[n]=d}}o(OE,"tb");var FD=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 l1(e,t){if(t){if(FD[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(l1,"vb");function a1(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(a1,"wb");function u1(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}o(u1,"xb");var f1=null,ep=null,tp=null;function ME(e){if(e=_h(e)){if(typeof f1!="function")throw Error(we(280));var t=e.stateNode;t&&(t=vv(t),f1(e.stateNode,e.type,t))}}o(ME,"Bb");function AE(e){ep?tp?tp.push(e):tp=[e]:ep=e}o(AE,"Eb");function DE(){if(ep){var e=ep,t=tp;if(tp=ep=null,ME(e),t)for(e=0;el?0:1<n;n++)t.push(e);return t}o(b1,"Zc");function nv(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(nv,"$c");var $a=Math.clz32?Math.clz32:JD,QD=Math.log,ZD=Math.LN2;function JD(e){return e===0?32:31-(QD(e)/ZD|0)|0}o(JD,"ad");var eR=Tn.unstable_UserBlockingPriority,tR=Tn.unstable_runWithPriority,iv=!0;function rR(e,t,n,l){vf||p1();var d=E1,h=vf;vf=!0;try{RE(d,e,t,n,l)}finally{(vf=h)||h1()}}o(rR,"gd");function nR(e,t,n,l){tR(eR,E1.bind(null,e,t,n,l))}o(nR,"id");function E1(e,t,n,l){if(iv){var d;if((d=(t&4)==0)&&0=yh),l_=String.fromCharCode(32),a_=!1;function u_(e,t){switch(e){case"keyup":return TR.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}o(u_,"ge");function f_(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}o(f_,"he");var lp=!1;function NR(e,t){switch(e){case"compositionend":return f_(t);case"keypress":return t.which!==32?null:(a_=!0,l_);case"textInput":return e=t.data,e===l_&&a_?null:e;default:return null}}o(NR,"je");function LR(e,t){if(lp)return e==="compositionend"||!M1&&u_(e,t)?(e=t_(),ov=T1=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=g_(n)}}o(v_,"Le");function y_(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?y_(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}o(y_,"Me");function w_(){for(var e=window,t=Yg();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=Yg(e.document)}return t}o(w_,"Ne");function D1(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(D1,"Oe");var HR=Ll&&"documentMode"in document&&11>=document.documentMode,ap=null,R1=null,Ch=null,I1=!1;function x_(e,t,n){var l=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;I1||ap==null||ap!==Yg(l)||(l=ap,"selectionStart"in l&&D1(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=dv(R1,"onSelect"),0dp||(e.current=$1[dp],$1[dp]=null,dp--)}o(fr,"H");function _r(e,t){dp++,$1[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 yv(){fr(Ri),fr(Xn)}o(yv,"Gf");function R_(e,t,n){if(Xn.current!==Ka)throw Error(we(168));_r(Xn,t),_r(Ri,n)}o(R_,"Hf");function I_(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(I_,"If");function wv(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Ka,xf=Xn.current,_r(Xn,e),_r(Ri,Ri.current),!0}o(wv,"Jf");function F_(e,t,n){var l=e.stateNode;if(!l)throw Error(we(169));n?(e=I_(e,t,xf),l.__reactInternalMemoizedMergedChildContext=e,fr(Ri),fr(Xn),_r(Xn,e)):fr(Ri),_r(Ri,n)}o(F_,"Kf");var j1=null,Sf=null,zR=Tn.unstable_runWithPriority,q1=Tn.unstable_scheduleCallback,V1=Tn.unstable_cancelCallback,$R=Tn.unstable_shouldYield,B_=Tn.unstable_requestPaint,K1=Tn.unstable_now,jR=Tn.unstable_getCurrentPriorityLevel,xv=Tn.unstable_ImmediatePriority,H_=Tn.unstable_UserBlockingPriority,W_=Tn.unstable_NormalPriority,U_=Tn.unstable_LowPriority,z_=Tn.unstable_IdlePriority,G1={},qR=B_!==void 0?B_:function(){},Pl=null,Sv=null,Y1=!1,$_=K1(),Qn=1e4>$_?K1:function(){return K1()-$_};function mp(){switch(jR()){case xv:return 99;case H_:return 98;case W_:return 97;case U_:return 96;case z_:return 95;default:throw Error(we(332))}}o(mp,"eg");function j_(e){switch(e){case 99:return xv;case 98:return H_;case 97:return W_;case 96:return U_;case 95:return z_;default:throw Error(we(332))}}o(j_,"fg");function Cf(e,t){return e=j_(e),zR(e,t)}o(Cf,"gg");function Th(e,t,n){return e=j_(e),q1(e,t,n)}o(Th,"hg");function $s(){if(Sv!==null){var e=Sv;Sv=null,V1(e)}q_()}o($s,"ig");function q_(){if(!Y1&&Pl!==null){Y1=!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=Vv(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=Ix(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=Rx(I,R.mode,G),A.return=R,R=A),c(R);if(kv(I))return J(R,A,I,G);if(ih(I))return Z(R,A,I,G);if(se&&Nv(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(eT,"Sg");var Lv=eT(!0),tT=eT(!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 ex(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:s1(null,"");break;default:e=e===8?t.parentNode:t,t=e.namespaceURI||null,e=e.tagName,t=s1(t,e)}fr(js),_r(js,t)}o(ex,"eh");function yp(){fr(js),fr(Ph),fr(Oh)}o(yp,"fh");function rT(e){bf(Oh.current);var t=bf(js.current),n=s1(t,e.type);t!==n&&(_r(Ph,e),_r(js,n))}o(rT,"gh");function tx(e){Ph.current===e&&(fr(js),fr(Ph))}o(tx,"hh");var Tr=Va(0);function Pv(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(Pv,"ih");var Ol=null,Qa=null,qs=!1;function nT(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(nT,"mh");function iT(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(iT,"oh");function rx(e){if(qs){var t=Qa;if(t){var n=t;if(!iT(e,t)){if(t=fp(n.nextSibling),!t||!iT(e,t)){e.flags=e.flags&-1025|2,qs=!1,Ol=e;return}nT(Ol,n)}Ol=e,Qa=fp(t.firstChild)}else e.flags=e.flags&-1025|2,qs=!1,Ol=e}}o(rx,"ph");function oT(e){for(e=e.return;e!==null&&e.tag!==5&&e.tag!==3&&e.tag!==13;)e=e.return;Ol=e}o(oT,"qh");function Ov(e){if(e!==Ol)return!1;if(!qs)return oT(e),qs=!0,!1;var t=e.type;if(e.tag!==5||t!=="head"&&t!=="body"&&!W1(t,e.memoizedProps))for(t=Qa;t;)nT(e,t),t=fp(t.nextSibling);if(oT(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(Ov,"rh");function nx(){Qa=Ol=null,qs=!1}o(nx,"sh");var wp=[];function ix(){for(var e=0;eh))throw Error(we(301));h+=1,In=Zn=null,t.updateQueue=null,Mh.current=XR,e=n(l,d)}while(Dh)}if(Mh.current=Iv,t=Zn!==null&&Zn.next!==null,Ah=0,In=Zn=Fr=null,Mv=!1,t)throw Error(we(300));return e}o(sx,"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 sT(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(sT,"Mh");function lT(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 sT(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[gv]=l,NT(e,t,!1,!1),t.stateNode=e,c=a1(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;dTx&&(t.flags|=64,h=!0,Bh(l,!1),t.lanes=33554432)}else{if(!h)if(e=Pv(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>Tx&&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 Mx(),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(ZR,"Gi");function JR(e){switch(e.tag){case 1:Ii(e.type)&&yv();var t=e.flags;return t&4096?(e.flags=t&-4097|64,e):null;case 3:if(yp(),fr(Ri),fr(Xn),ix(),t=e.flags,(t&64)!=0)throw Error(we(285));return e.flags=t&-4097|64,e;case 5:return tx(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 Q1(e),null;case 23:case 24:return Mx(),null;default:return null}}o(JR,"Li");function gx(e,t){try{var n="",l=t;do n+=AD(l),l=l.return;while(l);var d=n}catch(h){d=` +Error generating stack: `+h.message+` +`+h.stack}return{value:e,source:t,stack:d}}o(gx,"Mi");function vx(e,t){try{console.error(t.value)}catch(n){setTimeout(function(){throw n})}}o(vx,"Ni");var eI=typeof WeakMap=="function"?WeakMap:Map;function OT(e,t,n){n=Ya(-1,n),n.tag=3,n.payload={element:null};var l=t.value;return n.callback=function(){Wv||(Wv=!0,kx=l),vx(e,t)},n}o(OT,"Pi");function MT(e,t,n){n=Ya(-1,n),n.tag=3;var l=e.type.getDerivedStateFromError;if(typeof l=="function"){var d=t.value;n.payload=function(){return vx(e,t),l(d)}}var h=e.stateNode;return h!==null&&typeof h.componentDidCatch=="function"&&(n.callback=function(){typeof l!="function"&&(Ks===null?Ks=new Set([this]):Ks.add(this),vx(e,t));var c=t.stack;this.componentDidCatch(t.value,{componentStack:c!==null?c:""})}),n}o(MT,"Si");var tI=typeof WeakSet=="function"?WeakSet:Set;function AT(e){var t=e.ref;if(t!==null)if(typeof t=="function")try{t(null)}catch(n){ru(e,n)}else t.current=null}o(AT,"Vi");function rI(e,t){switch(t.tag){case 0:case 11:case 15:case 22:return;case 1:if(t.flags&256&&e!==null){var n=e.memoizedProps,l=e.memoizedState;e=t.stateNode,t=e.getSnapshotBeforeUpdate(t.elementType===t.type?n:ss(t.type,n),l),e.__reactInternalSnapshotBeforeUpdate=t}return;case 3:t.flags&256&&U1(t.stateNode.containerInfo);return;case 5:case 6:case 4:case 17:return}throw Error(we(163))}o(rI,"Xi");function nI(e,t,n){switch(n.tag){case 0:case 11:case 15:case 22:if(t=n.updateQueue,t=t!==null?t.lastEffect:null,t!==null){e=t=t.next;do{if((e.tag&3)==3){var l=e.create;e.destroy=l()}e=e.next}while(e!==t)}if(t=n.updateQueue,t=t!==null?t.lastEffect:null,t!==null){e=t=t.next;do{var d=e;l=d.next,d=d.tag,(d&4)!=0&&(d&1)!=0&&(GT(n,e),cI(n,e)),e=l}while(e!==t)}return;case 1:e=n.stateNode,n.flags&4&&(t===null?e.componentDidMount():(l=n.elementType===n.type?t.memoizedProps:ss(n.type,t.memoizedProps),e.componentDidUpdate(l,t.memoizedState,e.__reactInternalSnapshotBeforeUpdate))),t=n.updateQueue,t!==null&&Y_(n,t,e);return;case 3:if(t=n.updateQueue,t!==null){if(e=null,n.child!==null)switch(n.child.tag){case 5:e=n.child.stateNode;break;case 1:e=n.child.stateNode}Y_(n,t,e)}return;case 5:e=n.stateNode,t===null&&n.flags&4&&P_(n.type,n.memoizedProps)&&e.focus();return;case 6:return;case 4:return;case 12:return;case 13:n.memoizedState===null&&(n=n.alternate,n!==null&&(n=n.memoizedState,n!==null&&(n=n.dehydrated,n!==null&&KE(n))));return;case 19:case 17:case 20:case 21:case 23:case 24:return}throw Error(we(163))}o(nI,"Yi");function DT(e,t){for(var n=e;;){if(n.tag===5){var l=n.stateNode;if(t)l=l.style,typeof l.setProperty=="function"?l.setProperty("display","none","important"):l.display="none";else{l=n.stateNode;var d=n.memoizedProps.style;d=d!=null&&d.hasOwnProperty("display")?d.display:null,l.style.display=PE("display",d)}}else if(n.tag===6)n.stateNode.nodeValue=t?"":n.memoizedProps;else if((n.tag!==23&&n.tag!==24||n.memoizedState===null||n===e)&&n.child!==null){n.child.return=n,n=n.child;continue}if(n===e)break;for(;n.sibling===null;){if(n.return===null||n.return===e)return;n=n.return}n.sibling.return=n.return,n=n.sibling}}o(DT,"aj");function RT(e,t){if(Sf&&typeof Sf.onCommitFiberUnmount=="function")try{Sf.onCommitFiberUnmount(j1,t)}catch(h){}switch(t.tag){case 0:case 11:case 14:case 15:case 22:if(e=t.updateQueue,e!==null&&(e=e.lastEffect,e!==null)){var n=e=e.next;do{var l=n,d=l.destroy;if(l=l.tag,d!==void 0)if((l&4)!=0)GT(t,n);else{l=t;try{d()}catch(h){ru(l,h)}}n=n.next}while(n!==e)}break;case 1:if(AT(t),e=t.stateNode,typeof e.componentWillUnmount=="function")try{e.props=t.memoizedProps,e.state=t.memoizedState,e.componentWillUnmount()}catch(h){ru(t,h)}break;case 5:AT(t);break;case 4:HT(e,t)}}o(RT,"bj");function IT(e){e.alternate=null,e.child=null,e.dependencies=null,e.firstEffect=null,e.lastEffect=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.return=null,e.updateQueue=null}o(IT,"dj");function FT(e){return e.tag===5||e.tag===3||e.tag===4}o(FT,"ej");function BT(e){e:{for(var t=e.return;t!==null;){if(FT(t))break e;t=t.return}throw Error(we(160))}var n=t;switch(t=n.stateNode,n.tag){case 5:var l=!1;break;case 3:t=t.containerInfo,l=!0;break;case 4:t=t.containerInfo,l=!0;break;default:throw Error(we(161))}n.flags&16&&(sh(t,""),n.flags&=-17);e:t:for(n=e;;){for(;n.sibling===null;){if(n.return===null||FT(n.return)){n=null;break e}n=n.return}for(n.sibling.return=n.return,n=n.sibling;n.tag!==5&&n.tag!==6&&n.tag!==18;){if(n.flags&2||n.child===null||n.tag===4)continue t;n.child.return=n,n=n.child}if(!(n.flags&2)){n=n.stateNode;break e}}l?yx(e,n,t):wx(e,n,t)}o(BT,"fj");function yx(e,t,n){var l=e.tag,d=l===5||l===6;if(d)e=d?e.stateNode:e.stateNode.instance,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=hv));else if(l!==4&&(e=e.child,e!==null))for(yx(e,t,n),e=e.sibling;e!==null;)yx(e,t,n),e=e.sibling}o(yx,"gj");function wx(e,t,n){var l=e.tag,d=l===5||l===6;if(d)e=d?e.stateNode:e.stateNode.instance,t?n.insertBefore(e,t):n.appendChild(e);else if(l!==4&&(e=e.child,e!==null))for(wx(e,t,n),e=e.sibling;e!==null;)wx(e,t,n),e=e.sibling}o(wx,"hj");function HT(e,t){for(var n=t,l=!1,d,h;;){if(!l){l=n.return;e:for(;;){if(l===null)throw Error(we(160));switch(d=l.stateNode,l.tag){case 5:h=!1;break e;case 3:d=d.containerInfo,h=!0;break e;case 4:d=d.containerInfo,h=!0;break e}l=l.return}l=!0}if(n.tag===5||n.tag===6){e:for(var c=e,v=n,C=v;;)if(RT(c,C),C.child!==null&&C.tag!==4)C.child.return=C,C=C.child;else{if(C===v)break e;for(;C.sibling===null;){if(C.return===null||C.return===v)break e;C=C.return}C.sibling.return=C.return,C=C.sibling}h?(c=d,v=n.stateNode,c.nodeType===8?c.parentNode.removeChild(v):c.removeChild(v)):d.removeChild(n.stateNode)}else if(n.tag===4){if(n.child!==null){d=n.stateNode.containerInfo,h=!0,n.child.return=n,n=n.child;continue}}else if(RT(e,n),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.tag===4&&(l=!1)}n.sibling.return=n.return,n=n.sibling}}o(HT,"cj");function xx(e,t){switch(t.tag){case 0:case 11:case 14:case 15:case 22:var n=t.updateQueue;if(n=n!==null?n.lastEffect:null,n!==null){var l=n=n.next;do(l.tag&3)==3&&(e=l.destroy,l.destroy=void 0,e!==void 0&&e()),l=l.next;while(l!==n)}return;case 1:return;case 5:if(n=t.stateNode,n!=null){l=t.memoizedProps;var d=e!==null?e.memoizedProps:l;e=t.type;var h=t.updateQueue;if(t.updateQueue=null,h!==null){for(n[gv]=l,e==="input"&&l.type==="radio"&&l.name!=null&&bE(n,l),a1(e,d),t=a1(e,l),d=0;dd&&(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*oI(n/1960))-n,10 component higher in the tree to provide a loading indicator or placeholder to display.`)}An!==5&&(An=2),_=P1(_,x),U=p;do{switch(U.tag){case 3:m=_,U.flags|=4096,t&=-t,U.lanes|=t;var ue=yE(U,m,t);Rb(U,ue);break e;case 1:m=_;var ie=U.type,de=U.stateNode;if((U.flags&64)==0&&(typeof ie.getDerivedStateFromError=="function"||de!==null&&typeof de.componentDidCatch=="function"&&(Us===null||!Us.has(de)))){U.flags|=4096,t&=-t,U.lanes|=t;var ge=wE(U,m,t);Rb(U,ge);break e}}U=U.return}while(U!==null)}DE(n)}catch(xe){t=xe,en===n&&n!==null&&(en=n=n.return);continue}break}while(1)}o(PE,"Sj");function ME(){var e=fg.current;return fg.current=ag,e===null?ag:e}o(ME,"Pj");function Sh(e,t){var n=Qe;Qe|=16;var l=ME();ci===e&&Gn===t||ip(e,t);do try{VD();break}catch(d){PE(e,d)}while(1);if(c1(),Qe=n,fg.current=l,en!==null)throw Error(we(261));return ci=null,Gn=0,An}o(Sh,"Tj");function VD(){for(;en!==null;)AE(en)}o(VD,"ak");function KD(){for(;en!==null&&!OD();)AE(en)}o(KD,"Rj");function AE(e){var t=IE(e.alternate,e,pf);e.memoizedProps=e.pendingProps,t===null?DE(e):en=t,F1.current=null}o(AE,"bk");function DE(e){var t=e;do{var n=t.alternate;if(e=t.return,(t.flags&2048)==0){if(n=ID(n,t,pf),n!==null){en=n;return}if(n=t,n.tag!==24&&n.tag!==23||n.memoizedState===null||(pf&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),1p&&(x=p,p=ue,ue=x),x=ib(R,ue),m=ib(R,p),x&&m&&(V.rangeCount!==1||V.anchorNode!==x.node||V.anchorOffset!==x.offset||V.focusNode!==m.node||V.focusOffset!==m.offset)&&(K=K.createRange(),K.setStart(x.node,x.offset),V.removeAllRanges(),ue>p?(V.addRange(K),V.extend(m.node,m.offset)):(K.setEnd(m.node,m.offset),V.addRange(K)))))),K=[],V=R;V=V.parentNode;)V.nodeType===1&&K.push({element:V,left:V.scrollLeft,top:V.scrollTop});for(typeof R.focus=="function"&&R.focus(),R=0;RVn()-B1?ip(e,0):H1|=n),_o(e,t)}o(ZD,"Yj");function JD(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=Yc()===99?1:2:(Dl===0&&(Dl=ep),t=Wc(62914560&~Dl),t===0&&(t=4194304))),n=Ki(),e=vg(e,t),e!==null&&(Lv(e,t,n),_o(e,n))}o(JD,"lj");var IE;IE=o(function(e,t,n){var l=t.lanes;if(e!==null)if(e.memoizedProps!==t.pendingProps||Ni.current)Jo=!0;else if((n&l)!=0)Jo=(e.flags&16384)!=0;else{switch(Jo=!1,t.tag){case 3:aE(t),y1();break;case 5:zb(t);break;case 1:Pi(t.type)&&qv(t);break;case 4:m1(t,t.stateNode.containerInfo);break;case 10:l=t.memoizedProps.value;var d=t.type._context;xr(Gv,d._currentValue),d._currentValue=l;break;case 13:if(t.memoizedState!==null)return(n&t.child.childLanes)!=0?uE(e,t,n):(xr(Sr,Sr.current&1),t=Ml(e,t,n),t!==null?t.sibling:null);xr(Sr,Sr.current&1);break;case 19:if(l=(n&t.childLanes)!=0,(e.flags&64)!=0){if(l)return hE(e,t,n);t.flags|=64}if(d=t.memoizedState,d!==null&&(d.rendering=null,d.tail=null,d.lastEffect=null),xr(Sr,Sr.current),l)break;return null;case 23:case 24:return t.lanes=0,T1(e,t,n)}return Ml(e,t,n)}else Jo=!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=Gc(t,qn.current),Qc(t,n),d=S1(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,Pi(l)){var m=!0;qv(t)}else m=!1;t.memoizedState=d.state!==null&&d.state!==void 0?d.state:null,d1(t);var p=l.getDerivedStateFromProps;typeof p=="function"&&Qv(t,l,p,e),d.updater=Zv,t.stateNode=d,d._reactInternals=t,h1(t,l,e,n),t=O1(null,t,l,!0,m,n)}else t.tag=0,Ai(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,m=d._init,d=m(d._payload),t.type=d,m=t.tag=tR(d),e=Zo(d,e),m){case 0:t=k1(null,t,d,e,n);break e;case 1:t=lE(null,t,d,e,n);break e;case 11:t=nE(null,t,d,e,n);break e;case 14:t=iE(null,t,d,Zo(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:Zo(l,d),k1(e,t,l,d,n);case 1:return l=t.type,d=t.pendingProps,d=t.elementType===l?d:Zo(l,d),lE(e,t,l,d,n);case 3:if(aE(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,Db(e,t),ih(t,l,null,n),l=t.memoizedState.element,l===d)y1(),t=Ml(e,t,n);else{if(d=t.stateNode,(m=d.hydrate)&&(qa=jc(t.stateNode.containerInfo.firstChild),Pl=t,m=Ws=!0),m){if(e=d.mutableSourceEagerHydrationData,e!=null)for(d=0;d{"use strict";function UE(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__=="undefined"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(UE)}catch(e){console.error(e)}}o(UE,"checkDCE");UE(),$E.exports=BE()});var jE=ur((a2,zE)=>{"use strict";var aR="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED";zE.exports=aR});var GE=ur((u2,KE)=>{"use strict";var uR=jE();function qE(){}o(qE,"emptyFunction");function VE(){}o(VE,"emptyFunctionWithReset");VE.resetWarningCache=qE;KE.exports=function(){function e(l,d,m,p,x,_){if(_!==uR){var O=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 O.name="Invariant Violation",O}}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:VE,resetWarningCache:qE};return n.PropTypes=n,n}});var XE=ur((p2,YE)=>{YE.exports=GE()();var f2,c2});var nT=ur(Ht=>{"use strict";var xn=typeof Symbol=="function"&&Symbol.for,rx=xn?Symbol.for("react.element"):60103,nx=xn?Symbol.for("react.portal"):60106,Cg=xn?Symbol.for("react.fragment"):60107,_g=xn?Symbol.for("react.strict_mode"):60108,bg=xn?Symbol.for("react.profiler"):60114,Eg=xn?Symbol.for("react.provider"):60109,Tg=xn?Symbol.for("react.context"):60110,ix=xn?Symbol.for("react.async_mode"):60111,kg=xn?Symbol.for("react.concurrent_mode"):60111,Og=xn?Symbol.for("react.forward_ref"):60112,Lg=xn?Symbol.for("react.suspense"):60113,dR=xn?Symbol.for("react.suspense_list"):60120,Ng=xn?Symbol.for("react.memo"):60115,Pg=xn?Symbol.for("react.lazy"):60116,hR=xn?Symbol.for("react.block"):60121,mR=xn?Symbol.for("react.fundamental"):60117,vR=xn?Symbol.for("react.responder"):60118,gR=xn?Symbol.for("react.scope"):60119;function Gi(e){if(typeof e=="object"&&e!==null){var t=e.$$typeof;switch(t){case rx:switch(e=e.type,e){case ix:case kg:case Cg:case bg:case _g:case Lg:return e;default:switch(e=e&&e.$$typeof,e){case Tg:case Og:case Pg:case Ng:case Eg:return e;default:return t}}case nx:return t}}}o(Gi,"z");function rT(e){return Gi(e)===kg}o(rT,"A");Ht.AsyncMode=ix;Ht.ConcurrentMode=kg;Ht.ContextConsumer=Tg;Ht.ContextProvider=Eg;Ht.Element=rx;Ht.ForwardRef=Og;Ht.Fragment=Cg;Ht.Lazy=Pg;Ht.Memo=Ng;Ht.Portal=nx;Ht.Profiler=bg;Ht.StrictMode=_g;Ht.Suspense=Lg;Ht.isAsyncMode=function(e){return rT(e)||Gi(e)===ix};Ht.isConcurrentMode=rT;Ht.isContextConsumer=function(e){return Gi(e)===Tg};Ht.isContextProvider=function(e){return Gi(e)===Eg};Ht.isElement=function(e){return typeof e=="object"&&e!==null&&e.$$typeof===rx};Ht.isForwardRef=function(e){return Gi(e)===Og};Ht.isFragment=function(e){return Gi(e)===Cg};Ht.isLazy=function(e){return Gi(e)===Pg};Ht.isMemo=function(e){return Gi(e)===Ng};Ht.isPortal=function(e){return Gi(e)===nx};Ht.isProfiler=function(e){return Gi(e)===bg};Ht.isStrictMode=function(e){return Gi(e)===_g};Ht.isSuspense=function(e){return Gi(e)===Lg};Ht.isValidElementType=function(e){return typeof e=="string"||typeof e=="function"||e===Cg||e===kg||e===bg||e===_g||e===Lg||e===dR||typeof e=="object"&&e!==null&&(e.$$typeof===Pg||e.$$typeof===Ng||e.$$typeof===Eg||e.$$typeof===Tg||e.$$typeof===Og||e.$$typeof===mR||e.$$typeof===vR||e.$$typeof===gR||e.$$typeof===hR)};Ht.typeOf=Gi});var oT=ur((k2,iT)=>{"use strict";iT.exports=nT()});var pT=ur((O2,cT)=>{"use strict";var ox=oT(),yR={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},wR={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},xR={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},sT={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},sx={};sx[ox.ForwardRef]=xR;sx[ox.Memo]=sT;function lT(e){return ox.isMemo(e)?sT:sx[e.$$typeof]||yR}o(lT,"getStatics");var SR=Object.defineProperty,CR=Object.getOwnPropertyNames,aT=Object.getOwnPropertySymbols,_R=Object.getOwnPropertyDescriptor,bR=Object.getPrototypeOf,uT=Object.prototype;function fT(e,t,n){if(typeof t!="string"){if(uT){var l=bR(t);l&&l!==uT&&fT(e,l,n)}var d=CR(t);aT&&(d=d.concat(aT(t)));for(var m=lT(e),p=lT(t),x=0;x{"use strict";var Sn=typeof Symbol=="function"&&Symbol.for,lx=Sn?Symbol.for("react.element"):60103,ax=Sn?Symbol.for("react.portal"):60106,Mg=Sn?Symbol.for("react.fragment"):60107,Ag=Sn?Symbol.for("react.strict_mode"):60108,Dg=Sn?Symbol.for("react.profiler"):60114,Rg=Sn?Symbol.for("react.provider"):60109,Fg=Sn?Symbol.for("react.context"):60110,ux=Sn?Symbol.for("react.async_mode"):60111,Ig=Sn?Symbol.for("react.concurrent_mode"):60111,Hg=Sn?Symbol.for("react.forward_ref"):60112,Wg=Sn?Symbol.for("react.suspense"):60113,ER=Sn?Symbol.for("react.suspense_list"):60120,Bg=Sn?Symbol.for("react.memo"):60115,Ug=Sn?Symbol.for("react.lazy"):60116,TR=Sn?Symbol.for("react.block"):60121,kR=Sn?Symbol.for("react.fundamental"):60117,OR=Sn?Symbol.for("react.responder"):60118,LR=Sn?Symbol.for("react.scope"):60119;function Yi(e){if(typeof e=="object"&&e!==null){var t=e.$$typeof;switch(t){case lx:switch(e=e.type,e){case ux:case Ig:case Mg:case Dg:case Ag:case Wg:return e;default:switch(e=e&&e.$$typeof,e){case Fg:case Hg:case Ug:case Bg:case Rg:return e;default:return t}}case ax:return t}}}o(Yi,"z");function dT(e){return Yi(e)===Ig}o(dT,"A");Wt.AsyncMode=ux;Wt.ConcurrentMode=Ig;Wt.ContextConsumer=Fg;Wt.ContextProvider=Rg;Wt.Element=lx;Wt.ForwardRef=Hg;Wt.Fragment=Mg;Wt.Lazy=Ug;Wt.Memo=Bg;Wt.Portal=ax;Wt.Profiler=Dg;Wt.StrictMode=Ag;Wt.Suspense=Wg;Wt.isAsyncMode=function(e){return dT(e)||Yi(e)===ux};Wt.isConcurrentMode=dT;Wt.isContextConsumer=function(e){return Yi(e)===Fg};Wt.isContextProvider=function(e){return Yi(e)===Rg};Wt.isElement=function(e){return typeof e=="object"&&e!==null&&e.$$typeof===lx};Wt.isForwardRef=function(e){return Yi(e)===Hg};Wt.isFragment=function(e){return Yi(e)===Mg};Wt.isLazy=function(e){return Yi(e)===Ug};Wt.isMemo=function(e){return Yi(e)===Bg};Wt.isPortal=function(e){return Yi(e)===ax};Wt.isProfiler=function(e){return Yi(e)===Dg};Wt.isStrictMode=function(e){return Yi(e)===Ag};Wt.isSuspense=function(e){return Yi(e)===Wg};Wt.isValidElementType=function(e){return typeof e=="string"||typeof e=="function"||e===Mg||e===Ig||e===Dg||e===Ag||e===Wg||e===ER||typeof e=="object"&&e!==null&&(e.$$typeof===Ug||e.$$typeof===Bg||e.$$typeof===Rg||e.$$typeof===Fg||e.$$typeof===Hg||e.$$typeof===kR||e.$$typeof===OR||e.$$typeof===LR||e.$$typeof===TR)};Wt.typeOf=Yi});var vT=ur((N2,mT)=>{"use strict";mT.exports=hT()});var Oh=ur((ap,kh)=>{(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",m="Invalid `variable` option passed into `_.template`",p="__lodash_hash_undefined__",x=500,_="__lodash_placeholder__",O=1,D=2,Y=4,U=1,X=2,te=1,Q=2,F=4,M=8,R=16,K=32,V=64,ue=128,ie=256,de=512,ge=30,xe="...",qe=800,et=16,Te=1,xt=2,Ue=3,Ve=1/0,Ke=9007199254740991,Ye=17976931348623157e292,Qt=0/0,ft=4294967295,Ar=ft-1,Kt=ft>>>1,Et=[["ary",ue],["bind",te],["bindKey",Q],["curry",M],["curryRight",R],["flip",de],["partial",K],["partialRight",V],["rearg",ie]],St="[object Arguments]",at="[object Array]",_r="[object AsyncFunction]",Ut="[object Boolean]",$t="[object Date]",ne="[object DOMException]",tt="[object Error]",br="[object Function]",jt="[object GeneratorFunction]",qt="[object Map]",Se="[object Number]",Er="[object Null]",nn="[object Object]",Fn="[object Promise]",ei="[object Proxy]",on="[object RegExp]",Gt="[object Set]",dr="[object String]",ct="[object Symbol]",Do="[object Undefined]",vr="[object WeakMap]",Fi="[object WeakSet]",sn="[object ArrayBuffer]",In="[object DataView]",vi="[object Float32Array]",gi="[object Float64Array]",Hn="[object Int8Array]",En="[object Int16Array]",Ks="[object Int32Array]",H="[object Uint8Array]",J="[object Uint8ClampedArray]",he="[object Uint16Array]",ke="[object Uint32Array]",Zt=/\b__p \+= '';/g,Bl=/\b(__p \+=) '' \+/g,Rt=/(__e\(.*?\)|\b__t\)) \+\n'';/g,Dr=/&(?:amp|lt|gt|quot|#39);/g,Jt=/[&<>"']/g,ti=RegExp(Dr.source),os=RegExp(Jt.source),to=/<%-([\s\S]+?)%>/g,yi=/<%([\s\S]+?)%>/g,Gs=/<%=([\s\S]+?)%>/g,ss=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,ln=/^\w*$/,Ap=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,Ys=/[\\^$.*+?()[\]{}|]/g,Pf=RegExp(Ys.source),Xs=/^\s+/,Dp=/\s/,Mf=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,uu=/\{\n\/\* \[wrapped with (.+)\] \*/,Rp=/,? & /,Ul=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,ls=/[()=,{}\[\]\/\s]/,Fp=/\\(\\)?/g,Af=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,Qs=/\w*$/,fu=/^[-+]0x[0-9a-f]+$/i,Ro=/^0b[01]+$/i,Ip=/^\[object .+?Constructor\]$/,Fo=/^0o[0-7]+$/i,$l=/^(?:0|[1-9]\d*)$/,Df=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,zt=/($^)/,Me=/['\n\r\u2028\u2029\\]/g,wi="\\ud800-\\udfff",cu="\\u0300-\\u036f",ri="\\ufe20-\\ufe2f",vt="\\u20d0-\\u20ff",ro=cu+ri+vt,Io="\\u2700-\\u27bf",zl="a-z\\xdf-\\xf6\\xf8-\\xff",ae="\\xac\\xb1\\xd7\\xf7",$e="\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf",pu="\\u2000-\\u206f",du=" \\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",as="A-Z\\xc0-\\xd6\\xd8-\\xde",Zs="\\ufe0e\\ufe0f",jl=ae+$e+pu+du,Be="['\u2019]",Hp="["+wi+"]",hu="["+jl+"]",Ho="["+ro+"]",Wn="\\d+",mu="["+Io+"]",ql="["+zl+"]",Wo="[^"+wi+jl+Wn+Io+zl+as+"]",Js="\\ud83c[\\udffb-\\udfff]",Rf="(?:"+Ho+"|"+Js+")",el="[^"+wi+"]",tl="(?:\\ud83c[\\udde6-\\uddff]){2}",us="[\\ud800-\\udbff][\\udc00-\\udfff]",no="["+as+"]",Vl="\\u200d",Ff="(?:"+ql+"|"+Wo+")",Wp="(?:"+no+"|"+Wo+")",rl="(?:"+Be+"(?:d|ll|m|re|s|t|ve))?",an="(?:"+Be+"(?:D|LL|M|RE|S|T|VE))?",vu=Rf+"?",gu="["+Zs+"]?",Kl="(?:"+Vl+"(?:"+[el,tl,us].join("|")+")"+gu+vu+")*",nl="\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",Bp="\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])",If=gu+vu+Kl,Up="(?:"+[mu,tl,us].join("|")+")"+If,$p="(?:"+[el+Ho+"?",Ho,tl,us,Hp].join("|")+")",yu=RegExp(Be,"g"),Hf=RegExp(Ho,"g"),wu=RegExp(Js+"(?="+Js+")|"+$p+If,"g"),Wf=RegExp([no+"?"+ql+"+"+rl+"(?="+[hu,no,"$"].join("|")+")",Wp+"+"+an+"(?="+[hu,no+Ff,"$"].join("|")+")",no+"?"+Ff+"+"+rl,no+"+"+an,Bp,nl,Wn,Up].join("|"),"g"),Bf=RegExp("["+Vl+wi+ro+Zs+"]"),Gl=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,Yl=["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"],N=-1,Ce={};Ce[vi]=Ce[gi]=Ce[Hn]=Ce[En]=Ce[Ks]=Ce[H]=Ce[J]=Ce[he]=Ce[ke]=!0,Ce[St]=Ce[at]=Ce[sn]=Ce[Ut]=Ce[In]=Ce[$t]=Ce[tt]=Ce[br]=Ce[qt]=Ce[Se]=Ce[nn]=Ce[on]=Ce[Gt]=Ce[dr]=Ce[vr]=!1;var gt={};gt[St]=gt[at]=gt[sn]=gt[In]=gt[Ut]=gt[$t]=gt[vi]=gt[gi]=gt[Hn]=gt[En]=gt[Ks]=gt[qt]=gt[Se]=gt[nn]=gt[on]=gt[Gt]=gt[dr]=gt[ct]=gt[H]=gt[J]=gt[he]=gt[ke]=!0,gt[tt]=gt[br]=gt[vr]=!1;var Tn={\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"},xu={"&":"&","<":"<",">":">",'"':""","'":"'"},ye={"&":"&","<":"<",">":">",""":'"',"'":"'"},kn={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},am=parseFloat,um=parseInt,Su=typeof global=="object"&&global&&global.Object===Object&&global,zp=typeof self=="object"&&self&&self.Object===Object&&self,Nt=Su||zp||Function("return this")(),Ii=typeof ap=="object"&&ap&&!ap.nodeType&&ap,_e=Ii&&typeof kh=="object"&&kh&&!kh.nodeType&&kh,Bo=_e&&_e.exports===Ii,fs=Bo&&Su.process,He=function(){try{var q=_e&&_e.require&&_e.require("util").types;return q||fs&&fs.binding&&fs.binding("util")}catch(re){}}(),Uf=He&&He.isArrayBuffer,xi=He&&He.isDate,Xl=He&&He.isMap,il=He&&He.isRegExp,cs=He&&He.isSet,Cu=He&&He.isTypedArray;function On(q,re,Z){switch(Z.length){case 0:return q.call(re);case 1:return q.call(re,Z[0]);case 2:return q.call(re,Z[0],Z[1]);case 3:return q.call(re,Z[0],Z[1],Z[2])}return q.apply(re,Z)}o(On,"apply");function jp(q,re,Z,Ne){for(var Xe=-1,_t=q==null?0:q.length;++Xe<_t;){var Tr=q[Xe];re(Ne,Tr,Z(Tr),q)}return Ne}o(jp,"arrayAggregator");function Tt(q,re){for(var Z=-1,Ne=q==null?0:q.length;++Z-1}o(ps,"arrayIncludes");function ds(q,re,Z){for(var Ne=-1,Xe=q==null?0:q.length;++Ne-1;);return Z}o(Bi,"charsStartIndex");function ta(q,re){for(var Z=q.length;Z--&&Uo(re,q[Z],0)>-1;);return Z}o(ta,"charsEndIndex");function Vf(q,re){for(var Z=q.length,Ne=0;Z--;)q[Z]===re&&++Ne;return Ne}o(Vf,"countHolders");var ku=Eu(Tn),Kp=Eu(xu);function Kf(q){return"\\"+kn[q]}o(Kf,"escapeStringChar");function Ou(q,re){return q==null?e:q[re]}o(Ou,"getValue");function ni(q){return Bf.test(q)}o(ni,"hasUnicode");function ii(q){return Gl.test(q)}o(ii,"hasUnicodeWord");function y(q){for(var re,Z=[];!(re=q.next()).done;)Z.push(re.value);return Z}o(y,"iteratorToArray");function T(q){var re=-1,Z=Array(q.size);return q.forEach(function(Ne,Xe){Z[++re]=[Xe,Ne]}),Z}o(T,"mapToArray");function B(q,re){return function(Z){return q(re(Z))}}o(B,"overArg");function W(q,re){for(var Z=-1,Ne=q.length,Xe=0,_t=[];++Z-1}o(a0,"listCacheHas");function Zf(s,f){var h=this.__data__,w=vl(h,s);return w<0?(++this.size,h.push([s,f])):h[w][1]=f,this}o(Zf,"listCacheSet"),po.prototype.clear=Jp,po.prototype.delete=vm,po.prototype.get=Ru,po.prototype.has=a0,po.prototype.set=Zf;function hr(s){var f=-1,h=s==null?0:s.length;for(this.clear();++f=f?s:f)),s}o(gl,"baseClamp");function Nn(s,f,h,w,E,L){var I,$=f&O,G=f&D,se=f&Y;if(h&&(I=E?h(s,w,E,L):h(s)),I!==e)return I;if(!mr(s))return s;var le=nt(s);if(le){if(I=u(s),!$)return Wr(s,I)}else{var fe=dn(s),Le=fe==br||fe==jt;if(La(s))return vd(s,$);if(fe==nn||fe==St||Le&&!E){if(I=G||Le?{}:a(s),!$)return G?Bm(s,c0(I,s)):w0(s,id(I,s))}else{if(!gt[fe])return E?s:{};I=c(s,fe,$)}}L||(L=new _i);var Ie=L.get(s);if(Ie)return Ie;L.set(s,I),fC(s)?s.forEach(function(je){I.add(Nn(je,f,h,je,s,L))}):aC(s)&&s.forEach(function(je,dt){I.set(dt,Nn(je,f,h,dt,s,L))});var ze=se?G?wc:Yu:G?Oi:yn,ut=le?e:ze(s);return Tt(ut||s,function(je,dt){ut&&(dt=je,je=s[dt]),Iu(I,dt,Nn(je,f,h,dt,s,L))}),I}o(Nn,"baseClone");function Sm(s){var f=yn(s);return function(h){return Cm(h,s,f)}}o(Sm,"baseConforms");function Cm(s,f,h){var w=h.length;if(s==null)return!w;for(s=ht(s);w--;){var E=h[w],L=f[E],I=s[E];if(I===e&&!(E in s)||!L(I))return!1}return!0}o(Cm,"baseConformsTo");function _m(s,f,h){if(typeof s!="function")throw new $n(d);return Mt(function(){s.apply(e,h)},f)}o(_m,"baseDelay");function ca(s,f,h,w){var E=-1,L=ps,I=!0,$=s.length,G=[],se=f.length;if(!$)return G;h&&(f=Ct(f,Rr(h))),w?(L=ds,I=!1):f.length>=n&&(L=Vr,I=!1,f=new Xr(f));e:for(;++E<$;){var le=s[E],fe=h==null?le:h(le);if(le=w||le!==0?le:0,I&&fe===fe){for(var Le=se;Le--;)if(f[Le]===fe)continue e;G.push(le)}else L(f,fe,w)||G.push(le)}return G}o(ca,"baseDifference");var si=Um(li),tc=Um(ic,!0);function rc(s,f){var h=!0;return si(s,function(w,E,L){return h=!!f(w,E,L),h}),h}o(rc,"baseEvery");function Hu(s,f,h){for(var w=-1,E=s.length;++wE?0:E+h),w=w===e||w>E?E:st(w),w<0&&(w+=E),w=h>w?0:pC(w);h0&&h($)?f>1?Qr($,f-1,h,w,E):io(E,$):w||(E[E.length]=$)}return E}o(Qr,"baseFlatten");var nc=$m(),Ir=$m(!0);function li(s,f){return s&&nc(s,f,yn)}o(li,"baseForOwn");function ic(s,f){return s&&Ir(s,f,yn)}o(ic,"baseForOwnRight");function Wu(s,f){return Hi(f,function(h){return El(s[h])})}o(Wu,"baseFunctions");function Os(s,f){f=$i(f,s);for(var h=0,w=f.length;s!=null&&hf}o(oc,"baseGt");function bm(s,f){return s!=null&&Je.call(s,f)}o(bm,"baseHas");function Em(s,f){return s!=null&&f in ht(s)}o(Em,"baseHasIn");function pa(s,f,h){return s>=Kr(f,h)&&s=120&&le.length>=120)?new Xr(I&&le):e}le=s[0];var fe=-1,Le=$[0];e:for(;++fe-1;)$!==s&&sa.call($,G,1),sa.call(s,G,1);return s}o(ud,"basePullAll");function fd(s,f){for(var h=s?f.length:0,w=h-1;h--;){var E=f[h];if(h==w||E!==L){var L=E;S(E)?sa.call(s,E,1):ju(s,E)}}return s}o(fd,"basePullAt");function fc(s,f){return s+aa(Cs()*(f-s+1))}o(fc,"baseRandom");function Dm(s,f,h,w){for(var E=-1,L=er(la((f-s)/(h||1)),0),I=Z(L);L--;)I[w?L:++E]=s,s+=h;return I}o(Dm,"baseRange");function cd(s,f){var h="";if(!s||f<1||f>Ke)return h;do f%2&&(h+=s),f=aa(f/2),f&&(s+=s);while(f);return h}o(cd,"baseRepeat");function ot(s,f){return hn(Fe(s,f,Li),s+"")}o(ot,"baseRest");function m0(s){return Jf(bc(s))}o(m0,"baseSample");function Ps(s,f){var h=bc(s);return Br(h,gl(f,0,h.length))}o(Ps,"baseSampleSize");function jo(s,f,h,w){if(!mr(s))return s;f=$i(f,s);for(var E=-1,L=f.length,I=L-1,$=s;$!=null&&++EE?0:E+f),h=h>E?E:h,h<0&&(h+=E),E=f>h?0:h-f>>>0,f>>>=0;for(var L=Z(E);++w>>1,I=s[L];I!==null&&!zi(I)&&(h?I<=f:I=n){var se=f?null:Gu(s);if(se)return Fr(se);I=!1,E=Vr,G=new Xr}else G=f?[]:$;e:for(;++w=w?s:bi(s,f,h)}o(Ms,"castSlice");var xa=Qy||function(s){return Nt.clearTimeout(s)};function vd(s,f){if(f)return s.slice();var h=s.length,w=pm?pm(h):new s.constructor(h);return s.copy(w),w}o(vd,"cloneBuffer");function hc(s){var f=new s.constructor(s.byteLength);return new cl(f).set(new cl(s)),f}o(hc,"cloneArrayBuffer");function y0(s,f){var h=f?hc(s.buffer):s.buffer;return new s.constructor(h,s.byteOffset,s.byteLength)}o(y0,"cloneDataView");function gd(s){var f=new s.constructor(s.source,Qs.exec(s));return f.lastIndex=s.lastIndex,f}o(gd,"cloneRegExp");function Fm(s){return tr?ht(tr.call(s)):{}}o(Fm,"cloneSymbol");function Im(s,f){var h=f?hc(s.buffer):s.buffer;return new s.constructor(h,s.byteOffset,s.length)}o(Im,"cloneTypedArray");function yd(s,f){if(s!==f){var h=s!==e,w=s===null,E=s===s,L=zi(s),I=f!==e,$=f===null,G=f===f,se=zi(f);if(!$&&!se&&!L&&s>f||L&&I&&G&&!$&&!se||w&&I&&G||!h&&G||!E)return 1;if(!w&&!L&&!se&&s=$)return G;var se=h[w];return G*(se=="desc"?-1:1)}}return s.index-f.index}o(Hm,"compareMultiple");function Wm(s,f,h,w){for(var E=-1,L=s.length,I=h.length,$=-1,G=f.length,se=er(L-I,0),le=Z(G+se),fe=!w;++$1?h[E-1]:e,I=E>2?h[2]:e;for(L=s.length>3&&typeof L=="function"?(E--,L):e,I&&C(h[0],h[1],I)&&(L=E<3?e:L,E=1),f=ht(f);++w-1?E[L?f[I]:I]:e}}o(xd,"createFind");function qm(s){return Ko(function(f){var h=f.length,w=h,E=Ln.prototype.thru;for(s&&f.reverse();w--;){var L=f[w];if(typeof L!="function")throw new $n(d);if(E&&!I&&Xu(L)=="wrapper")var I=new Ln([],!0)}for(w=I?w:h;++w1&&mt.reverse(),le&&G$))return!1;var se=L.get(s),le=L.get(f);if(se&&le)return se==f&&le==s;var fe=-1,Le=!0,Ie=h&X?new Xr:e;for(L.set(s,f),L.set(f,s);++fe<$;){var ze=s[fe],ut=f[fe];if(w)var je=I?w(ut,ze,fe,f,s,L):w(ze,ut,fe,s,f,L);if(je!==e){if(je)continue;Le=!1;break}if(Ie){if(!oo(f,function(dt,mt){if(!Vr(Ie,mt)&&(ze===dt||E(ze,dt,h,w,L)))return Ie.push(mt)})){Le=!1;break}}else if(!(ze===ut||E(ze,ut,h,w,L))){Le=!1;break}}return L.delete(s),L.delete(f),Le}o(Cd,"equalArrays");function Ym(s,f,h,w,E,L,I){switch(h){case In:if(s.byteLength!=f.byteLength||s.byteOffset!=f.byteOffset)return!1;s=s.buffer,f=f.buffer;case sn:return!(s.byteLength!=f.byteLength||!L(new cl(s),new cl(f)));case Ut:case $t:case Se:return Yo(+s,+f);case tt:return s.name==f.name&&s.message==f.message;case on:case dr:return s==f+"";case qt:var $=T;case Gt:var G=w&U;if($||($=Fr),s.size!=f.size&&!G)return!1;var se=I.get(s);if(se)return se==f;w|=X,I.set(s,f);var le=Cd($(s),$(f),w,E,L,I);return I.delete(s),le;case ct:if(tr)return tr.call(s)==tr.call(f)}return!1}o(Ym,"equalByTag");function Xm(s,f,h,w,E,L){var I=h&U,$=Yu(s),G=$.length,se=Yu(f),le=se.length;if(G!=le&&!I)return!1;for(var fe=G;fe--;){var Le=$[fe];if(!(I?Le in f:Je.call(f,Le)))return!1}var Ie=L.get(s),ze=L.get(f);if(Ie&&ze)return Ie==f&&ze==s;var ut=!0;L.set(s,f),L.set(f,s);for(var je=I;++fe1?"& ":"")+f[w],f=f.join(h>2?", ":" "),s.replace(Mf,`{ +Add a component higher in the tree to provide a loading indicator or placeholder to display.`)}Fn!==5&&(Fn=2),C=gx(C,v),B=c;do{switch(B.tag){case 3:h=C,B.flags|=4096,t&=-t,B.lanes|=t;var se=OT(B,h,t);G_(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=MT(B,h,t);G_(B,me);break e}}B=B.return}while(B!==null)}KT(n)}catch(xe){t=xe,sn===n&&n!==null&&(sn=n=n.return);continue}break}while(1)}o(jT,"Sj");function qT(){var e=Bv.current;return Bv.current=Iv,e===null?Iv:e}o(qT,"Pj");function jh(e,t){var n=Ze;Ze|=16;var l=qT();yi===e&&Jn===t||Ep(e,t);do try{lI();break}catch(d){jT(e,d)}while(1);if(X1(),Ze=n,Bv.current=l,sn!==null)throw Error(we(261));return yi=null,Jn=0,Fn}o(jh,"Tj");function lI(){for(;sn!==null;)VT(sn)}o(lI,"ak");function aI(){for(;sn!==null&&!$R();)VT(sn)}o(aI,"Rj");function VT(e){var t=XT(e.alternate,e,Tf);e.memoizedProps=e.pendingProps,t===null?KT(e):sn=t,Sx.current=null}o(VT,"bk");function KT(e){var t=e;do{var n=t.alternate;if(e=t.return,(t.flags&2048)==0){if(n=ZR(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=v_(I,se),h=v_(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()-_x?Ep(e,0):bx|=n),ko(e,t)}o(dI,"Yj");function hI(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=jv(e,t),e!==null&&(nv(e,t,n),ko(e,n))}o(hI,"lj");var XT;XT=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:ST(t),nx();break;case 5:rT(t);break;case 1:Ii(t.type)&&wv(t);break;case 4:ex(t,t.stateNode.containerInfo);break;case 10:l=t.memoizedProps.value;var d=t.type._context;_r(Cv,d._currentValue),d._currentValue=l;break;case 13:if(t.memoizedState!==null)return(n&t.child.childLanes)!=0?CT(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 kT(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,cx(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=sx(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;wv(t)}else h=!1;t.memoizedState=d.state!==null&&d.state!==void 0?d.state:null,Z1(t);var c=l.getDerivedStateFromProps;typeof c=="function"&&_v(t,l,c,e),d.updater=Tv,t.stateNode=d,d._reactInternals=t,J1(t,l,e,n),t=dx(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=gI(d),e=ss(d,e),h){case 0:t=px(null,t,d,e,n);break e;case 1:t=xT(null,t,d,e,n);break e;case 11:t=gT(null,t,d,e,n);break e;case 14:t=vT(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),px(e,t,l,d,n);case 1:return l=t.type,d=t.pendingProps,d=t.elementType===l?d:ss(l,d),xT(e,t,l,d,n);case 3:if(ST(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,K_(e,t),kh(t,l,null,n),l=t.memoizedState.element,l===d)nx(),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 ek(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__=="undefined"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(ek)}catch(e){console.error(e)}}o(ek,"checkDCE");ek(),tk.exports=JT()});var nk=Ue((yH,rk)=>{"use strict";var bI="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED";rk.exports=bI});var lk=Ue((wH,sk)=>{"use strict";var EI=nk();function ik(){}o(ik,"emptyFunction");function ok(){}o(ok,"emptyFunctionWithReset");ok.resetWarningCache=ik;sk.exports=function(){function e(l,d,h,c,v,C){if(C!==EI){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:ok,resetWarningCache:ik};return n.PropTypes=n,n}});var uk=Ue((CH,ak)=>{ak.exports=lk()();var xH,SH});var gk=Ue(Ht=>{"use strict";var kn=typeof Symbol=="function"&&Symbol.for,Ux=kn?Symbol.for("react.element"):60103,zx=kn?Symbol.for("react.portal"):60106,Xv=kn?Symbol.for("react.fragment"):60107,Qv=kn?Symbol.for("react.strict_mode"):60108,Zv=kn?Symbol.for("react.profiler"):60114,Jv=kn?Symbol.for("react.provider"):60109,e0=kn?Symbol.for("react.context"):60110,$x=kn?Symbol.for("react.async_mode"):60111,t0=kn?Symbol.for("react.concurrent_mode"):60111,r0=kn?Symbol.for("react.forward_ref"):60112,n0=kn?Symbol.for("react.suspense"):60113,NI=kn?Symbol.for("react.suspense_list"):60120,i0=kn?Symbol.for("react.memo"):60115,o0=kn?Symbol.for("react.lazy"):60116,LI=kn?Symbol.for("react.block"):60121,PI=kn?Symbol.for("react.fundamental"):60117,OI=kn?Symbol.for("react.responder"):60118,MI=kn?Symbol.for("react.scope"):60119;function eo(e){if(typeof e=="object"&&e!==null){var t=e.$$typeof;switch(t){case Ux:switch(e=e.type,e){case $x:case t0:case Xv:case Zv:case Qv:case n0:return e;default:switch(e=e&&e.$$typeof,e){case e0:case r0:case o0:case i0:case Jv:return e;default:return t}}case zx:return t}}}o(eo,"z");function mk(e){return eo(e)===t0}o(mk,"A");Ht.AsyncMode=$x;Ht.ConcurrentMode=t0;Ht.ContextConsumer=e0;Ht.ContextProvider=Jv;Ht.Element=Ux;Ht.ForwardRef=r0;Ht.Fragment=Xv;Ht.Lazy=o0;Ht.Memo=i0;Ht.Portal=zx;Ht.Profiler=Zv;Ht.StrictMode=Qv;Ht.Suspense=n0;Ht.isAsyncMode=function(e){return mk(e)||eo(e)===$x};Ht.isConcurrentMode=mk;Ht.isContextConsumer=function(e){return eo(e)===e0};Ht.isContextProvider=function(e){return eo(e)===Jv};Ht.isElement=function(e){return typeof e=="object"&&e!==null&&e.$$typeof===Ux};Ht.isForwardRef=function(e){return eo(e)===r0};Ht.isFragment=function(e){return eo(e)===Xv};Ht.isLazy=function(e){return eo(e)===o0};Ht.isMemo=function(e){return eo(e)===i0};Ht.isPortal=function(e){return eo(e)===zx};Ht.isProfiler=function(e){return eo(e)===Zv};Ht.isStrictMode=function(e){return eo(e)===Qv};Ht.isSuspense=function(e){return eo(e)===n0};Ht.isValidElementType=function(e){return typeof e=="string"||typeof e=="function"||e===Xv||e===t0||e===Zv||e===Qv||e===n0||e===NI||typeof e=="object"&&e!==null&&(e.$$typeof===o0||e.$$typeof===i0||e.$$typeof===Jv||e.$$typeof===e0||e.$$typeof===r0||e.$$typeof===PI||e.$$typeof===OI||e.$$typeof===MI||e.$$typeof===LI)};Ht.typeOf=eo});var yk=Ue((FH,vk)=>{"use strict";vk.exports=gk()});var _k=Ue((BH,Ek)=>{"use strict";var jx=yk(),AI={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},DI={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},RI={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},wk={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},qx={};qx[jx.ForwardRef]=RI;qx[jx.Memo]=wk;function xk(e){return jx.isMemo(e)?wk:qx[e.$$typeof]||AI}o(xk,"getStatics");var II=Object.defineProperty,FI=Object.getOwnPropertyNames,Sk=Object.getOwnPropertySymbols,BI=Object.getOwnPropertyDescriptor,HI=Object.getPrototypeOf,Ck=Object.prototype;function bk(e,t,n){if(typeof t!="string"){if(Ck){var l=HI(t);l&&l!==Ck&&bk(e,l,n)}var d=FI(t);Sk&&(d=d.concat(Sk(t)));for(var h=xk(e),c=xk(t),v=0;v{"use strict";var Nn=typeof Symbol=="function"&&Symbol.for,Vx=Nn?Symbol.for("react.element"):60103,Kx=Nn?Symbol.for("react.portal"):60106,s0=Nn?Symbol.for("react.fragment"):60107,l0=Nn?Symbol.for("react.strict_mode"):60108,a0=Nn?Symbol.for("react.profiler"):60114,u0=Nn?Symbol.for("react.provider"):60109,f0=Nn?Symbol.for("react.context"):60110,Gx=Nn?Symbol.for("react.async_mode"):60111,c0=Nn?Symbol.for("react.concurrent_mode"):60111,p0=Nn?Symbol.for("react.forward_ref"):60112,d0=Nn?Symbol.for("react.suspense"):60113,WI=Nn?Symbol.for("react.suspense_list"):60120,h0=Nn?Symbol.for("react.memo"):60115,m0=Nn?Symbol.for("react.lazy"):60116,UI=Nn?Symbol.for("react.block"):60121,zI=Nn?Symbol.for("react.fundamental"):60117,$I=Nn?Symbol.for("react.responder"):60118,jI=Nn?Symbol.for("react.scope"):60119;function to(e){if(typeof e=="object"&&e!==null){var t=e.$$typeof;switch(t){case Vx:switch(e=e.type,e){case Gx:case c0:case s0:case a0:case l0:case d0:return e;default:switch(e=e&&e.$$typeof,e){case f0:case p0:case m0:case h0:case u0:return e;default:return t}}case Kx:return t}}}o(to,"z");function Tk(e){return to(e)===c0}o(Tk,"A");Wt.AsyncMode=Gx;Wt.ConcurrentMode=c0;Wt.ContextConsumer=f0;Wt.ContextProvider=u0;Wt.Element=Vx;Wt.ForwardRef=p0;Wt.Fragment=s0;Wt.Lazy=m0;Wt.Memo=h0;Wt.Portal=Kx;Wt.Profiler=a0;Wt.StrictMode=l0;Wt.Suspense=d0;Wt.isAsyncMode=function(e){return Tk(e)||to(e)===Gx};Wt.isConcurrentMode=Tk;Wt.isContextConsumer=function(e){return to(e)===f0};Wt.isContextProvider=function(e){return to(e)===u0};Wt.isElement=function(e){return typeof e=="object"&&e!==null&&e.$$typeof===Vx};Wt.isForwardRef=function(e){return to(e)===p0};Wt.isFragment=function(e){return to(e)===s0};Wt.isLazy=function(e){return to(e)===m0};Wt.isMemo=function(e){return to(e)===h0};Wt.isPortal=function(e){return to(e)===Kx};Wt.isProfiler=function(e){return to(e)===a0};Wt.isStrictMode=function(e){return to(e)===l0};Wt.isSuspense=function(e){return to(e)===d0};Wt.isValidElementType=function(e){return typeof e=="string"||typeof e=="function"||e===s0||e===c0||e===a0||e===l0||e===d0||e===WI||typeof e=="object"&&e!==null&&(e.$$typeof===m0||e.$$typeof===h0||e.$$typeof===u0||e.$$typeof===f0||e.$$typeof===p0||e.$$typeof===zI||e.$$typeof===$I||e.$$typeof===jI||e.$$typeof===UI)};Wt.typeOf=to});var Lk=Ue((WH,Nk)=>{"use strict";Nk.exports=kk()});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,Zt=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]",Jt="[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[Jt]=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[Jt]=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(Ky,"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=jm,vo.prototype.get=Yu,vo.prototype.has=Ky,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?hg(s,Xy(F,s)):iw(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),bb(s)?s.forEach(function(je){F.add(Dn(je,f,m,je,s,L))}):Sb(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 Ym(s){var f=_n(s);return function(m){return Xm(m,s,f)}}o(Ym,"baseConforms");function Xm(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(Xm,"baseConformsTo");function Qm(s,f,m){if(typeof s!="function")throw new Kn(d);return At(function(){s.apply(e,m)},f)}o(Qm,"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:_b(x);m0&&m(z)?f>1?rn(z,f-1,m,x,_):$i(_,z):x||(_[_.length]=z)}return _}o(rn,"baseFlatten");var bc=gg(),$r=gg(!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 Zm(s,f){return s!=null&&et.call(s,f)}o(Zm,"baseHas");function Jm(s,f){return s!=null&&f in mt(s)}o(Jm,"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 ag(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(ag,"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 ew(s){return wc(jc(s))}o(ew,"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 tw(s,f){var m;return di(s,function(x,_,L){return m=f(x,_,L),!m}),!!m}o(tw,"baseSome");function es(s,f,m){var x=0,_=s==null?x:s.length;if(typeof f=="number"&&f===f&&_<=Zt){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=Iy||function(s){return qt.clearTimeout(s)};function Hd(s,f){if(f)return s.slice();var m=s.length,x=Wm?Wm(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 nw(s,f){var m=f?Dc(s.buffer):s.buffer;return new s.constructor(m,s.byteOffset,s.byteLength)}o(nw,"cloneDataView");function Wd(s){var f=new s.constructor(s.source,nl.exec(s));return f.lastIndex=s.lastIndex,f}o(Wd,"cloneRegExp");function fg(s){return lr?mt(lr.call(s)):{}}o(fg,"cloneSymbol");function cg(s,f){var m=f?Dc(s.buffer):s.buffer;return new s.constructor(m,s.byteOffset,s.length)}o(cg,"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(pg,"compareMultiple");function dg(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 mg(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(mg,"createBaseEach");function gg(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(gg,"createBaseFor");function vg(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(vg,"createBind");function yg(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(yg,"createCaseFirst");function Na(s){return function(f){return Au(Db(Ab(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 wg(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,`{ /* [wrapped with `+f+`] */ -`)}o(v,"insertWrapDetails");function g(s){return nt(s)||Qu(s)||!!(pl&&s&&s[pl])}o(g,"isFlattenable");function S(s,f){var h=typeof s;return f=f??Ke,!!f&&(h=="number"||h!="symbol"&&$l.test(s))&&s>-1&&s%1==0&&s0){if(++f>=qe)return arguments[0]}else f=0;return s.apply(e,arguments)}}o(ar,"shortOut");function Br(s,f){var h=-1,w=s.length,E=w-1;for(f=f===e?w:f;++h1?s[f-1]:e;return h=typeof h=="function"?(s.pop(),h):e,XS(s,h)});function QS(s){var f=k(s);return f.__chain__=!0,f}o(QS,"chain");function WL(s,f){return f(s),s}o(WL,"tap");function Zm(s,f){return f(s)}o(Zm,"thru");var BL=Ko(function(s){var f=s.length,h=f?s[0]:0,w=this.__wrapped__,E=o(function(L){return od(L,s)},"interceptor");return f>1||this.__actions__.length||!(w instanceof pt)||!S(h)?this.thru(E):(w=w.slice(h,+h+(f?1:0)),w.__actions__.push({func:Zm,args:[E],thisArg:e}),new Ln(w,this.__chain__).thru(function(L){return f&&!L.length&&L.push(e),L}))});function UL(){return QS(this)}o(UL,"wrapperChain");function $L(){return new Ln(this.value(),this.__chain__)}o($L,"wrapperCommit");function zL(){this.__values__===e&&(this.__values__=cC(this.value()));var s=this.__index__>=this.__values__.length,f=s?e:this.__values__[this.__index__++];return{done:s,value:f}}o(zL,"wrapperNext");function jL(){return this}o(jL,"wrapperToIterator");function qL(s){for(var f,h=this;h instanceof Qf;){var w=vn(h);w.__index__=0,w.__values__=e,f?E.__wrapped__=w:f=w;var E=w;h=h.__wrapped__}return E.__wrapped__=s,f}o(qL,"wrapperPlant");function VL(){var s=this.__wrapped__;if(s instanceof pt){var f=s;return this.__actions__.length&&(f=new pt(this)),f=f.reverse(),f.__actions__.push({func:Zm,args:[S0],thisArg:e}),new Ln(f,this.__chain__)}return this.thru(S0)}o(VL,"wrapperReverse");function KL(){return Rm(this.__wrapped__,this.__actions__)}o(KL,"wrapperValue");var GL=vc(function(s,f,h){Je.call(s,h)?++s[h]:ho(s,h,1)});function YL(s,f,h){var w=nt(s)?Ql:rc;return h&&C(s,f,h)&&(f=e),w(s,We(f,3))}o(YL,"every");function XL(s,f){var h=nt(s)?Hi:ld;return h(s,We(f,3))}o(XL,"filter");var QL=xd(Cc),ZL=xd(VS);function JL(s,f){return Qr(Jm(s,f),1)}o(JL,"flatMap");function eN(s,f){return Qr(Jm(s,f),Ve)}o(eN,"flatMapDeep");function tN(s,f,h){return h=h===e?1:st(h),Qr(Jm(s,f),h)}o(tN,"flatMapDepth");function ZS(s,f){var h=nt(s)?Tt:si;return h(s,We(f,3))}o(ZS,"forEach");function JS(s,f){var h=nt(s)?$f:tc;return h(s,We(f,3))}o(JS,"forEachRight");var rN=vc(function(s,f,h){Je.call(s,h)?s[h].push(f):ho(s,h,[f])});function nN(s,f,h,w){s=ki(s)?s:bc(s),h=h&&!w?st(h):0;var E=s.length;return h<0&&(h=er(E+h,0)),iv(s)?h<=E&&s.indexOf(f,h)>-1:!!E&&Uo(s,f,h)>-1}o(nN,"includes");var iN=ot(function(s,f,h){var w=-1,E=typeof f=="function",L=ki(s)?Z(s.length):[];return si(s,function(I){L[++w]=E?On(f,I,h):da(I,f,h)}),L}),oN=vc(function(s,f,h){ho(s,h,f)});function Jm(s,f){var h=nt(s)?Ct:ya;return h(s,We(f,3))}o(Jm,"map");function sN(s,f,h,w){return s==null?[]:(nt(f)||(f=f==null?[]:[f]),h=w?e:h,nt(h)||(h=h==null?[]:[h]),cn(s,f,h))}o(sN,"orderBy");var lN=vc(function(s,f,h){s[h?0:1].push(f)},function(){return[[],[]]});function aN(s,f,h){var w=nt(s)?_u:Tu,E=arguments.length<3;return w(s,We(f,4),h,E,si)}o(aN,"reduce");function uN(s,f,h){var w=nt(s)?zf:Tu,E=arguments.length<3;return w(s,We(f,4),h,E,tc)}o(uN,"reduceRight");function fN(s,f){var h=nt(s)?Hi:ld;return h(s,rv(We(f,3)))}o(fN,"reject");function cN(s){var f=nt(s)?Jf:m0;return f(s)}o(cN,"sample");function pN(s,f,h){(h?C(s,f,h):f===e)?f=1:f=st(f);var w=nt(s)?ks:Ps;return w(s,f)}o(pN,"sampleSize");function dN(s){var f=nt(s)?xm:qo;return f(s)}o(dN,"shuffle");function hN(s){if(s==null)return 0;if(ki(s))return iv(s)?Si(s):s.length;var f=dn(s);return f==qt||f==Gt?s.size:ac(s).length}o(hN,"size");function mN(s,f,h){var w=nt(s)?oo:v0;return h&&C(s,f,h)&&(f=e),w(s,We(f,3))}o(mN,"some");var vN=ot(function(s,f){if(s==null)return[];var h=f.length;return h>1&&C(s,f[0],f[1])?f=[]:h>2&&C(f[0],f[1],f[2])&&(f=[f[0]]),cn(s,Qr(f,1),[])}),ev=Zy||function(){return Nt.Date.now()};function gN(s,f){if(typeof f!="function")throw new $n(d);return s=st(s),function(){if(--s<1)return f.apply(this,arguments)}}o(gN,"after");function eC(s,f,h){return f=h?e:f,f=s&&f==null?s.length:f,Ti(s,ue,e,e,e,e,f)}o(eC,"ary");function tC(s,f){var h;if(typeof f!="function")throw new $n(d);return s=st(s),function(){return--s>0&&(h=f.apply(this,arguments)),s<=1&&(f=e),h}}o(tC,"before");var _0=ot(function(s,f,h){var w=te;if(h.length){var E=W(h,Ea(_0));w|=K}return Ti(s,w,f,h,E)}),rC=ot(function(s,f,h){var w=te|Q;if(h.length){var E=W(h,Ea(rC));w|=K}return Ti(f,w,s,h,E)});function nC(s,f,h){f=h?e:f;var w=Ti(s,M,e,e,e,e,e,f);return w.placeholder=nC.placeholder,w}o(nC,"curry");function iC(s,f,h){f=h?e:f;var w=Ti(s,R,e,e,e,e,e,f);return w.placeholder=iC.placeholder,w}o(iC,"curryRight");function oC(s,f,h){var w,E,L,I,$,G,se=0,le=!1,fe=!1,Le=!0;if(typeof s!="function")throw new $n(d);f=go(f)||0,mr(h)&&(le=!!h.leading,fe="maxWait"in h,L=fe?er(go(h.maxWait)||0,f):L,Le="trailing"in h?!!h.trailing:Le);function Ie(Nr){var Xo=w,kl=E;return w=E=e,se=Nr,I=s.apply(kl,Xo),I}o(Ie,"invokeFunc");function ze(Nr){return se=Nr,$=Mt(dt,f),le?Ie(Nr):I}o(ze,"leadingEdge");function ut(Nr){var Xo=Nr-G,kl=Nr-se,bC=f-Xo;return fe?Kr(bC,L-kl):bC}o(ut,"remainingWait");function je(Nr){var Xo=Nr-G,kl=Nr-se;return G===e||Xo>=f||Xo<0||fe&&kl>=L}o(je,"shouldInvoke");function dt(){var Nr=ev();if(je(Nr))return mt(Nr);$=Mt(dt,ut(Nr))}o(dt,"timerExpired");function mt(Nr){return $=e,Le&&w?Ie(Nr):(w=E=e,I)}o(mt,"trailingEdge");function ji(){$!==e&&xa($),se=0,w=G=E=$=e}o(ji,"cancel");function ui(){return $===e?I:mt(ev())}o(ui,"flush");function qi(){var Nr=ev(),Xo=je(Nr);if(w=arguments,E=this,G=Nr,Xo){if($===e)return ze(G);if(fe)return xa($),$=Mt(dt,f),Ie(G)}return $===e&&($=Mt(dt,f)),I}return o(qi,"debounced"),qi.cancel=ji,qi.flush=ui,qi}o(oC,"debounce");var yN=ot(function(s,f){return _m(s,1,f)}),wN=ot(function(s,f,h){return _m(s,go(f)||0,h)});function xN(s){return Ti(s,de)}o(xN,"flip");function tv(s,f){if(typeof s!="function"||f!=null&&typeof f!="function")throw new $n(d);var h=o(function(){var w=arguments,E=f?f.apply(this,w):w[0],L=h.cache;if(L.has(E))return L.get(E);var I=s.apply(this,w);return h.cache=L.set(E,I)||L,I},"memoized");return h.cache=new(tv.Cache||hr),h}o(tv,"memoize"),tv.Cache=hr;function rv(s){if(typeof s!="function")throw new $n(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(rv,"negate");function SN(s){return tC(2,s)}o(SN,"once");var CN=g0(function(s,f){f=f.length==1&&nt(f[0])?Ct(f[0],Rr(We())):Ct(Qr(f,1),Rr(We()));var h=f.length;return ot(function(w){for(var E=-1,L=Kr(w.length,h);++E=f}),Qu=ha(function(){return arguments}())?ha:function(s){return wr(s)&&Je.call(s,"callee")&&!Gf.call(s,"callee")},nt=Z.isArray,IN=Uf?Rr(Uf):p0;function ki(s){return s!=null&&nv(s.length)&&!El(s)}o(ki,"isArrayLike");function Lr(s){return wr(s)&&ki(s)}o(Lr,"isArrayLikeObject");function HN(s){return s===!0||s===!1||wr(s)&&Hr(s)==Ut}o(HN,"isBoolean");var La=Pu||R0,WN=xi?Rr(xi):ma;function BN(s){return wr(s)&&s.nodeType===1&&!Ed(s)}o(BN,"isElement");function UN(s){if(s==null)return!0;if(ki(s)&&(nt(s)||typeof s=="string"||typeof s.splice=="function"||La(s)||_c(s)||Qu(s)))return!s.length;var f=dn(s);if(f==qt||f==Gt)return!s.size;if(ee(s))return!ac(s).length;for(var h in s)if(Je.call(s,h))return!1;return!0}o(UN,"isEmpty");function $N(s,f){return va(s,f)}o($N,"isEqual");function zN(s,f,h){h=typeof h=="function"?h:e;var w=h?h(s,f):e;return w===e?va(s,f,e,h):!!w}o(zN,"isEqualWith");function E0(s){if(!wr(s))return!1;var f=Hr(s);return f==tt||f==ne||typeof s.message=="string"&&typeof s.name=="string"&&!Ed(s)}o(E0,"isError");function jN(s){return typeof s=="number"&&dm(s)}o(jN,"isFinite");function El(s){if(!mr(s))return!1;var f=Hr(s);return f==br||f==jt||f==_r||f==ei}o(El,"isFunction");function lC(s){return typeof s=="number"&&s==st(s)}o(lC,"isInteger");function nv(s){return typeof s=="number"&&s>-1&&s%1==0&&s<=Ke}o(nv,"isLength");function mr(s){var f=typeof s;return s!=null&&(f=="object"||f=="function")}o(mr,"isObject");function wr(s){return s!=null&&typeof s=="object"}o(wr,"isObjectLike");var aC=Xl?Rr(Xl):km;function qN(s,f){return s===f||wl(s,f,Ta(f))}o(qN,"isMatch");function VN(s,f,h){return h=typeof h=="function"?h:e,wl(s,f,Ta(f),h)}o(VN,"isMatchWith");function KN(s){return uC(s)&&s!=+s}o(KN,"isNaN");function GN(s){if(z(s))throw new Xe(l);return ga(s)}o(GN,"isNative");function YN(s){return s===null}o(YN,"isNull");function XN(s){return s==null}o(XN,"isNil");function uC(s){return typeof s=="number"||wr(s)&&Hr(s)==Se}o(uC,"isNumber");function Ed(s){if(!wr(s)||Hr(s)!=nn)return!1;var f=oa(s);if(f===null)return!0;var h=Je.call(f,"constructor")&&f.constructor;return typeof h=="function"&&h instanceof h&&uo.call(h)==Xy}o(Ed,"isPlainObject");var T0=il?Rr(il):Bu;function QN(s){return lC(s)&&s>=-Ke&&s<=Ke}o(QN,"isSafeInteger");var fC=cs?Rr(cs):Uu;function iv(s){return typeof s=="string"||!nt(s)&&wr(s)&&Hr(s)==dr}o(iv,"isString");function zi(s){return typeof s=="symbol"||wr(s)&&Hr(s)==ct}o(zi,"isSymbol");var _c=Cu?Rr(Cu):Om;function ZN(s){return s===e}o(ZN,"isUndefined");function JN(s){return wr(s)&&dn(s)==vr}o(JN,"isWeakMap");function eP(s){return wr(s)&&Hr(s)==Fi}o(eP,"isWeakSet");var tP=Pt(Ns),rP=Pt(function(s,f){return s<=f});function cC(s){if(!s)return[];if(ki(s))return iv(s)?gr(s):Wr(s);if(ws&&s[ws])return y(s[ws]());var f=dn(s),h=f==qt?T:f==Gt?Fr:bc;return h(s)}o(cC,"toArray");function Tl(s){if(!s)return s===0?s:0;if(s=go(s),s===Ve||s===-Ve){var f=s<0?-1:1;return f*Ye}return s===s?s:0}o(Tl,"toFinite");function st(s){var f=Tl(s),h=f%1;return f===f?h?f-h:f:0}o(st,"toInteger");function pC(s){return s?gl(st(s),0,ft):0}o(pC,"toLength");function go(s){if(typeof s=="number")return s;if(zi(s))return Qt;if(mr(s)){var f=typeof s.valueOf=="function"?s.valueOf():s;s=mr(f)?f+"":f}if(typeof s!="string")return s===0?s:+s;s=sl(s);var h=Ro.test(s);return h||Fo.test(s)?um(s.slice(2),h?2:8):fu.test(s)?Qt:+s}o(go,"toNumber");function dC(s){return zn(s,Oi(s))}o(dC,"toPlainObject");function nP(s){return s?gl(st(s),-Ke,Ke):s===0?s:0}o(nP,"toSafeInteger");function At(s){return s==null?"":pn(s)}o(At,"toString");var iP=Sa(function(s,f){if(ee(f)||ki(f)){zn(f,yn(f),s);return}for(var h in f)Je.call(f,h)&&Iu(s,h,f[h])}),hC=Sa(function(s,f){zn(f,Oi(f),s)}),ov=Sa(function(s,f,h,w){zn(f,Oi(f),s,w)}),oP=Sa(function(s,f,h,w){zn(f,yn(f),s,w)}),sP=Ko(od);function lP(s,f){var h=co(s);return f==null?h:id(h,f)}o(lP,"create");var aP=ot(function(s,f){s=ht(s);var h=-1,w=f.length,E=w>2?f[2]:e;for(E&&C(f[0],f[1],E)&&(w=1);++h1),L}),zn(s,wc(s),h),w&&(h=Nn(h,O|D|Y,Gm));for(var E=f.length;E--;)ju(h,f[E]);return h});function TP(s,f){return vC(s,rv(We(f)))}o(TP,"omitBy");var kP=Ko(function(s,f){return s==null?{}:Mm(s,f)});function vC(s,f){if(s==null)return{};var h=Ct(wc(s),function(w){return[w]});return f=We(f),Am(s,h,function(w,E){return f(w,E[0])})}o(vC,"pickBy");function OP(s,f,h){f=$i(f,s);var w=-1,E=f.length;for(E||(E=1,s=e);++wf){var w=s;s=f,f=w}if(h||s%1||f%1){var E=Cs();return Kr(s+E*(f-s+am("1e-"+((E+"").length-1))),f)}return fc(s,f)}o(HP,"random");var WP=Ca(function(s,f,h){return f=f.toLowerCase(),s+(h?wC(f):f)});function wC(s){return L0(At(s).toLowerCase())}o(wC,"capitalize");function xC(s){return s=At(s),s&&s.replace(Df,ku).replace(Hf,"")}o(xC,"deburr");function BP(s,f,h){s=At(s),f=pn(f);var w=s.length;h=h===e?w:gl(st(h),0,w);var E=h;return h-=f.length,h>=0&&s.slice(h,E)==f}o(BP,"endsWith");function UP(s){return s=At(s),s&&os.test(s)?s.replace(Jt,Kp):s}o(UP,"escape");function $P(s){return s=At(s),s&&Pf.test(s)?s.replace(Ys,"\\$&"):s}o($P,"escapeRegExp");var zP=Ca(function(s,f,h){return s+(h?"-":"")+f.toLowerCase()}),jP=Ca(function(s,f,h){return s+(h?" ":"")+f.toLowerCase()}),qP=jm("toLowerCase");function VP(s,f,h){s=At(s),f=st(f);var w=f?Si(s):0;if(!f||w>=f)return s;var E=(f-w)/2;return gc(aa(E),h)+s+gc(la(E),h)}o(VP,"pad");function KP(s,f,h){s=At(s),f=st(f);var w=f?Si(s):0;return f&&w>>0,h?(s=At(s),s&&(typeof f=="string"||f!=null&&!T0(f))&&(f=pn(f),!f&&ni(s))?Ms(gr(s),0,h):s.split(f,h)):[]}o(JP,"split");var eM=Ca(function(s,f,h){return s+(h?" ":"")+L0(f)});function tM(s,f,h){return s=At(s),h=h==null?0:gl(st(h),0,s.length),f=pn(f),s.slice(h,h+f.length)==f}o(tM,"startsWith");function rM(s,f,h){var w=k.templateSettings;h&&C(s,f,h)&&(f=e),s=At(s),f=ov({},f,w,yc);var E=ov({},f.imports,w.imports,yc),L=yn(E),I=ll(E,L),$,G,se=0,le=f.interpolate||zt,fe="__p += '",Le=vs((f.escape||zt).source+"|"+le.source+"|"+(le===Gs?Af:zt).source+"|"+(f.evaluate||zt).source+"|$","g"),Ie="//# sourceURL="+(Je.call(f,"sourceURL")?(f.sourceURL+"").replace(/\s/g," "):"lodash.templateSources["+ ++N+"]")+` -`;s.replace(Le,function(je,dt,mt,ji,ui,qi){return mt||(mt=ji),fe+=s.slice(se,qi).replace(Me,Kf),dt&&($=!0,fe+=`' + -__e(`+dt+`) + -'`),ui&&(G=!0,fe+=`'; -`+ui+`; -__p += '`),mt&&(fe+=`' + -((__t = (`+mt+`)) == null ? '' : __t) + -'`),se=qi+je.length,je}),fe+=`'; -`;var ze=Je.call(f,"variable")&&f.variable;if(!ze)fe=`with (obj) { -`+fe+` +`)}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,ub(s,m)});function fb(s){var f=N(s);return f.__chain__=!0,f}o(fb,"chain");function eM(s,f){return f(s),s}o(eM,"tap");function Tg(s,f){return f(s)}o(Tg,"thru");var tM=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:Tg,args:[_],thisArg:e}),new An(x,this.__chain__).thru(function(L){return f&&!L.length&&L.push(e),L}))});function rM(){return fb(this)}o(rM,"wrapperChain");function nM(){return new An(this.value(),this.__chain__)}o(nM,"wrapperCommit");function iM(){this.__values__===e&&(this.__values__=Eb(this.value()));var s=this.__index__>=this.__values__.length,f=s?e:this.__values__[this.__index__++];return{done:s,value:f}}o(iM,"wrapperNext");function oM(){return this}o(oM,"wrapperToIterator");function sM(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(sM,"wrapperPlant");function lM(){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:Tg,args:[sw],thisArg:e}),new An(f,this.__chain__)}return this.thru(sw)}o(lM,"wrapperReverse");function aM(){return ug(this.__wrapped__,this.__actions__)}o(aM,"wrapperValue");var uM=Ic(function(s,f,m){et.call(s,m)?++s[m]:yo(s,m,1)});function fM(s,f,m){var x=ot(s)?Mu:Cc;return m&&b(s,f,m)&&(f=e),x(s,Be(f,3))}o(fM,"every");function cM(s,f){var m=ot(s)?Vt:Pd;return m(s,Be(f,3))}o(cM,"filter");var pM=$d(zc),dM=$d(ob);function hM(s,f){return rn(kg(s,f),1)}o(hM,"flatMap");function mM(s,f){return rn(kg(s,f),Ke)}o(mM,"flatMapDeep");function gM(s,f,m){return m=m===e?1:lt(m),rn(kg(s,f),m)}o(gM,"flatMapDepth");function cb(s,f){var m=ot(s)?jn:di;return m(s,Be(f,3))}o(cb,"forEach");function pb(s,f){var m=ot(s)?dd:Sc;return m(s,Be(f,3))}o(pb,"forEachRight");var vM=Ic(function(s,f,m){et.call(s,m)?s[m].push(f):yo(s,m,[f])});function yM(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)),Mg(s)?m<=_&&s.indexOf(f,m)>-1:!!_&&Go(s,f,m)>-1}o(yM,"includes");var wM=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}),xM=Ic(function(s,f,m){yo(s,m,f)});function kg(s,f){var m=ot(s)?yt:Ea;return m(s,Be(f,3))}o(kg,"map");function SM(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(SM,"orderBy");var CM=Ic(function(s,f,m){s[m?0:1].push(f)},function(){return[[],[]]});function bM(s,f,m){var x=ot(s)?Au:Fu,_=arguments.length<3;return x(s,Be(f,4),m,_,di)}o(bM,"reduce");function EM(s,f,m){var x=ot(s)?hd:Fu,_=arguments.length<3;return x(s,Be(f,4),m,_,Sc)}o(EM,"reduceRight");function _M(s,f){var m=ot(s)?Vt:Pd;return m(s,Pg(Be(f,3)))}o(_M,"reject");function TM(s){var f=ot(s)?wc:ew;return f(s)}o(TM,"sample");function kM(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(kM,"sampleSize");function NM(s){var f=ot(s)?Gm:Jo;return f(s)}o(NM,"shuffle");function LM(s){if(s==null)return 0;if(Mi(s))return Mg(s)?wr(s):s.length;var f=xn(s);return f==Yt||f==Jt?s.size:Nc(s).length}o(LM,"size");function PM(s,f,m){var x=ot(s)?Vo:tw;return m&&b(s,f,m)&&(f=e),x(s,Be(f,3))}o(PM,"some");var OM=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),[])}),Ng=Fy||function(){return qt.Date.now()};function MM(s,f){if(typeof f!="function")throw new Kn(d);return s=lt(s),function(){if(--s<1)return f.apply(this,arguments)}}o(MM,"after");function db(s,f,m){return f=m?e:f,f=s&&f==null?s.length:f,Oi(s,se,e,e,e,e,f)}o(db,"ary");function hb(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(hb,"before");var aw=st(function(s,f,m){var x=J;if(m.length){var _=Vi(m,Oa(aw));x|=G}return Oi(s,x,f,m,_)}),mb=st(function(s,f,m){var x=J|Z;if(m.length){var _=Vi(m,Oa(mb));x|=G}return Oi(f,x,s,m,_)});function gb(s,f,m){f=m?e:f;var x=Oi(s,A,e,e,e,e,e,f);return x.placeholder=gb.placeholder,x}o(gb,"curry");function vb(s,f,m){f=m?e:f;var x=Oi(s,I,e,e,e,e,e,f);return x.placeholder=vb.placeholder,x}o(vb,"curryRight");function yb(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,Fb=f-is;return ce?Zr(Fb,L-kl):Fb}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=Ng();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(Ng())}o(gi,"flush");function Qi(){var Ir=Ng(),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(yb,"debounce");var AM=st(function(s,f){return Qm(s,1,f)}),DM=st(function(s,f,m){return Qm(s,So(f)||0,m)});function RM(s){return Oi(s,pe)}o(RM,"flip");function Lg(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(Lg.Cache||xr),m}o(Lg,"memoize"),Lg.Cache=xr;function Pg(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(Pg,"negate");function IM(s){return hb(2,s)}o(IM,"once");var FM=rw(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,ZM=ta?zr(ta):Qy;function Mi(s){return s!=null&&Og(s.length)&&!_l(s)}o(Mi,"isArrayLike");function Rr(s){return Er(s)&&Mi(s)}o(Rr,"isArrayLikeObject");function JM(s){return s===!0||s===!1||Er(s)&&jr(s)==zt}o(JM,"isBoolean");var Ra=qu||xw,eA=Ou?zr(Ou):Sa;function tA(s){return Er(s)&&s.nodeType===1&&!Gd(s)}o(tA,"isElement");function rA(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==Jt)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(rA,"isEmpty");function nA(s,f){return Ca(s,f)}o(nA,"isEqual");function iA(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(iA,"isEqualWith");function fw(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(fw,"isError");function oA(s){return typeof s=="number"&&Um(s)}o(oA,"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 xb(s){return typeof s=="number"&&s==lt(s)}o(xb,"isInteger");function Og(s){return typeof s=="number"&&s>-1&&s%1==0&&s<=Ge}o(Og,"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 Sb=nt?zr(nt):tg;function sA(s,f){return s===f||wl(s,f,Ma(f))}o(sA,"isMatch");function lA(s,f,m){return m=typeof m=="function"?m:e,wl(s,f,Ma(f),m)}o(lA,"isMatchWith");function aA(s){return Cb(s)&&s!=+s}o(aA,"isNaN");function uA(s){if($(s))throw new Qe(l);return ba(s)}o(uA,"isNative");function fA(s){return s===null}o(fA,"isNull");function cA(s){return s==null}o(cA,"isNil");function Cb(s){return typeof s=="number"||Er(s)&&jr(s)==Se}o(Cb,"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)==Ry}o(Gd,"isPlainObject");var cw=uc?zr(uc):ef;function pA(s){return xb(s)&&s>=-Ge&&s<=Ge}o(pA,"isSafeInteger");var bb=fi?zr(fi):tf;function Mg(s){return typeof s=="string"||!ot(s)&&Er(s)&&jr(s)==gr}o(Mg,"isString");function Yi(s){return typeof s=="symbol"||Er(s)&&jr(s)==pt}o(Yi,"isSymbol");var $c=ll?zr(ll):rg;function dA(s){return s===e}o(dA,"isUndefined");function hA(s){return Er(s)&&xn(s)==Cr}o(hA,"isWeakMap");function mA(s){return Er(s)&&jr(s)==Ui}o(mA,"isWeakSet");var gA=Mt(Is),vA=Mt(function(s,f){return s<=f});function Eb(s){if(!s)return[];if(Mi(s))return Mg(s)?Kt(s):qr(s);if(_s&&s[_s])return pc(s[_s]());var f=xn(s),m=f==Yt?aa:f==Jt?w:jc;return m(s)}o(Eb,"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 _b(s){return s?vl(lt(s),0,ct):0}o(_b,"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 Tb(s){return Gn(s,Ai(s))}o(Tb,"toPlainObject");function yA(s){return s?vl(lt(s),-Ge,Ge):s===0?s:0}o(yA,"toSafeInteger");function Dt(s){return s==null?"":wn(s)}o(Dt,"toString");var wA=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])}),kb=ka(function(s,f){Gn(f,Ai(f),s)}),Ag=ka(function(s,f,m,x){Gn(f,Ai(f),s,x)}),xA=ka(function(s,f,m,x){Gn(f,_n(f),s,x)}),SA=ts(Nd);function CA(s,f){var m=go(s);return f==null?m:kd(m,f)}o(CA,"create");var bA=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,Cg));for(var _=f.length;_--;)of(m,f[_]);return m});function UA(s,f){return Lb(s,Pg(Be(f)))}o(UA,"omitBy");var zA=ts(function(s,f){return s==null?{}:sg(s,f)});function Lb(s,f){if(s==null)return{};var m=yt(Hc(s),function(x){return[x]});return f=Be(f),lg(s,m,function(x,_){return f(x,_[0])})}o(Lb,"pickBy");function $A(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($A,"result");function jA(s,f,m){return s==null?s:Zo(s,f,m)}o(jA,"set");function qA(s,f,m,x){return x=typeof x=="function"?x:e,s==null?s:Zo(s,f,m,x)}o(qA,"setWith");var Pb=Pi(_n),Ob=Pi(Ai);function VA(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(VA,"transform");function KA(s,f){return s==null?!0:of(s,f)}o(KA,"unset");function GA(s,f,m){return s==null?s:Mc(s,f,Ac(m))}o(GA,"update");function YA(s,f,m,x){return x=typeof x=="function"?x:e,s==null?s:Mc(s,f,Ac(m),x)}o(YA,"updateWith");function jc(s){return s==null?[]:sa(s,_n(s))}o(jc,"values");function XA(s){return s==null?[]:sa(s,Ai(s))}o(XA,"valuesIn");function QA(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(QA,"clamp");function ZA(s,f,m){return f=Tl(f),m===e?(m=f,f=0):m=Tl(m),s=So(s),ya(s,f,m)}o(ZA,"inRange");function JA(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(JA,"random");var e2=Na(function(s,f,m){return f=f.toLowerCase(),s+(m?Mb(f):f)});function Mb(s){return hw(Dt(s).toLowerCase())}o(Mb,"capitalize");function Ab(s){return s=Dt(s),s&&s.replace(ec,Wu).replace(sc,"")}o(Ab,"deburr");function t2(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(t2,"endsWith");function r2(s){return s=Dt(s),s&&ds.test(s)?s.replace(or,gd):s}o(r2,"escape");function n2(s){return s=Dt(s),s&&Qf.test(s)?s.replace(tl,"\\$&"):s}o(n2,"escapeRegExp");var i2=Na(function(s,f,m){return s+(m?"-":"")+f.toLowerCase()}),o2=Na(function(s,f,m){return s+(m?" ":"")+f.toLowerCase()}),s2=yg("toLowerCase");function l2(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(l2,"pad");function a2(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&&!cw(f))&&(f=wn(f),!f&&qn(s))?Bs(Kt(s),0,m):s.split(f,m)):[]}o(h2,"split");var m2=Na(function(s,f,m){return s+(m?" ":"")+hw(f)});function g2(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(g2,"startsWith");function v2(s,f,m){var x=N.templateSettings;m&&b(s,f,m)&&(f=e),s=Dt(s),f=Ag({},f,x,Bc);var _=Ag({},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+=`' + +__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+` } -`;else if(ls.test(ze))throw new Xe(m);fe=(G?fe.replace(Zt,""):fe).replace(Bl,"$1").replace(Rt,"$1;"),fe="function("+(ze||"obj")+`) { -`+(ze?"":`obj || (obj = {}); -`)+"var __t, __p = ''"+($?", __e = _.escape":"")+(G?`, __j = Array.prototype.join; +`;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; function print() { __p += __j.call(arguments, '') } `:`; -`)+fe+`return __p -}`;var ut=CC(function(){return _t(L,Ie+"return "+fe).apply(e,I)});if(ut.source=fe,E0(ut))throw ut;return ut}o(rM,"template");function nM(s){return At(s).toLowerCase()}o(nM,"toLower");function iM(s){return At(s).toUpperCase()}o(iM,"toUpper");function oM(s,f,h){if(s=At(s),s&&(h||f===e))return sl(s);if(!s||!(f=pn(f)))return s;var w=gr(s),E=gr(f),L=Bi(w,E),I=ta(w,E)+1;return Ms(w,L,I).join("")}o(oM,"trim");function sM(s,f,h){if(s=At(s),s&&(h||f===e))return s.slice(0,al(s)+1);if(!s||!(f=pn(f)))return s;var w=gr(s),E=ta(w,gr(f))+1;return Ms(w,0,E).join("")}o(sM,"trimEnd");function lM(s,f,h){if(s=At(s),s&&(h||f===e))return s.replace(Xs,"");if(!s||!(f=pn(f)))return s;var w=gr(s),E=Bi(w,gr(f));return Ms(w,E).join("")}o(lM,"trimStart");function aM(s,f){var h=ge,w=xe;if(mr(f)){var E="separator"in f?f.separator:E;h="length"in f?st(f.length):h,w="omission"in f?pn(f.omission):w}s=At(s);var L=s.length;if(ni(s)){var I=gr(s);L=I.length}if(h>=L)return s;var $=h-Si(w);if($<1)return w;var G=I?Ms(I,0,$).join(""):s.slice(0,$);if(E===e)return G+w;if(I&&($+=G.length-$),T0(E)){if(s.slice($).search(E)){var se,le=G;for(E.global||(E=vs(E.source,At(Qs.exec(E))+"g")),E.lastIndex=0;se=E.exec(le);)var fe=se.index;G=G.slice(0,fe===e?$:fe)}}else if(s.indexOf(pn(E),$)!=$){var Le=G.lastIndexOf(E);Le>-1&&(G=G.slice(0,Le))}return G+w}o(aM,"truncate");function uM(s){return s=At(s),s&&ti.test(s)?s.replace(Dr,ul):s}o(uM,"unescape");var fM=Ca(function(s,f,h){return s+(h?" ":"")+f.toUpperCase()}),L0=jm("toUpperCase");function SC(s,f,h){return s=At(s),f=h?e:f,f===e?ii(s)?fn(s):jf(s):s.match(f)||[]}o(SC,"words");var CC=ot(function(s,f){try{return On(s,e,f)}catch(h){return E0(h)?h:new Xe(h)}}),cM=Ko(function(s,f){return Tt(f,function(h){h=Lt(h),ho(s,h,_0(s[h],s))}),s});function pM(s){var f=s==null?0:s.length,h=We();return s=f?Ct(s,function(w){if(typeof w[1]!="function")throw new $n(d);return[h(w[0]),w[1]]}):[],ot(function(w){for(var E=-1;++EKe)return[];var h=ft,w=Kr(s,ft);f=We(f),s-=ft;for(var E=so(w,f);++h0||f<0)?new pt(h):(s<0?h=h.takeRight(-s):s&&(h=h.drop(s)),f!==e&&(f=st(f),h=f<0?h.dropRight(-f):h.take(f-s)),h)},pt.prototype.takeRightWhile=function(s){return this.reverse().takeWhile(s).reverse()},pt.prototype.toArray=function(){return this.take(ft)},li(pt.prototype,function(s,f){var h=/^(?:filter|find|map|reject)|While$/.test(f),w=/^(?:head|last)$/.test(f),E=k[w?"take"+(f=="last"?"Right":""):f],L=w||/^find/.test(f);!E||(k.prototype[f]=function(){var I=this.__wrapped__,$=w?[1]:arguments,G=I instanceof pt,se=$[0],le=G||nt(I),fe=o(function(dt){var mt=E.apply(k,io([dt],$));return w&&Le?mt[0]:mt},"interceptor");le&&h&&typeof se=="function"&&se.length!=1&&(G=le=!1);var Le=this.__chain__,Ie=!!this.__actions__.length,ze=L&&!Le,ut=G&&!Ie;if(!L&&le){I=ut?I:new pt(this);var je=s.apply(I,$);return je.__actions__.push({func:Zm,args:[fe],thisArg:e}),new Ln(je,Le)}return ze&&ut?s.apply(this,$):(je=this.thru(fe),ze?w?je.value()[0]:je.value():je)})}),Tt(["pop","push","shift","sort","splice","unshift"],function(s){var f=ia[s],h=/^(?:push|sort|unshift)$/.test(s)?"tap":"thru",w=/^(?:pop|shift)$/.test(s);k.prototype[s]=function(){var E=arguments;if(w&&!this.__chain__){var L=this.value();return f.apply(nt(L)?L:[],E)}return this[h](function(I){return f.apply(nt(I)?I:[],E)})}}),li(pt.prototype,function(s,f){var h=k[f];if(h){var w=h.name+"";Je.call(fa,w)||(fa[w]=[]),fa[w].push({name:f,func:h})}}),fa[qu(e,Q).name]=[{name:"wrapper",func:e}],pt.prototype.clone=o0,pt.prototype.reverse=s0,pt.prototype.value=Yp,k.prototype.at=BL,k.prototype.chain=UL,k.prototype.commit=$L,k.prototype.next=zL,k.prototype.plant=qL,k.prototype.reverse=VL,k.prototype.toJSON=k.prototype.valueOf=k.prototype.value=KL,k.prototype.first=k.prototype.head,ws&&(k.prototype[ws]=jL),k},"runInContext"),lo=Ci();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(Nt._=lo,define(function(){return lo})):_e?((_e.exports=lo)._=lo,Ii._=lo):Nt._=lo}).call(ap)});var PT=ur((yx,wx)=>{(function(e,t){typeof yx=="object"&&typeof wx!="undefined"?wx.exports=t():typeof define=="function"&&define.amd?define(t):e.stable=t()})(yx,function(){"use strict";var e=o(function(l,d){return t(l.slice(),d)},"stable");e.inplace=function(l,d){var m=t(l,d);return m!==l&&n(m,null,l.length,l),l};function t(l,d){typeof d!="function"&&(d=o(function(O,D){return String(O).localeCompare(D)},"comp"));var m=l.length;if(m<=1)return l;for(var p=new Array(m),x=1;xx&&(Y=x),U>x&&(U=x),X=D,te=Y;;)if(X{(function(){"use strict";var e={}.hasOwnProperty;function t(){for(var n=[],l=0;l{(function(e,t){typeof Xx=="object"&&typeof Qx!="undefined"?Qx.exports=t():typeof define=="function"&&define.amd?define(t):(e=e||self,e.CodeMirror=t())})(Xx,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),m=/Edge\/(\d+)/.exec(e),p=l||d||m,x=p&&(l?document.documentMode||6:+(m||d)[1]),_=!m&&/WebKit\//.test(e),O=_&&/Qt\/\d+\.\d+/.test(e),D=!m&&/Chrome\//.test(e),Y=/Opera\//.test(e),U=/Apple Computer/.test(navigator.vendor),X=/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(e),te=/PhantomJS/.test(e),Q=U&&(/Mobile\/\w+/.test(e)||navigator.maxTouchPoints>2),F=/Android/.test(e),M=Q||F||/webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(e),R=Q||/Mac/.test(t),K=/\bCrOS\b/.test(e),V=/win/i.test(t),ue=Y&&e.match(/Version\/(\d*\.\d*)/);ue&&(ue=Number(ue[1])),ue&&ue>=15&&(Y=!1,_=!0);var ie=R&&(O||Y&&(ue==null||ue<12.11)),de=n||p&&x>=9;function ge(r){return new RegExp("(^|\\s)"+r+"(?:$|\\s)\\s*")}o(ge,"classTest");var xe=o(function(r,i){var u=r.className,a=ge(i).exec(u);if(a){var c=u.slice(a.index+a[0].length);r.className=u.slice(0,a.index)+(c?a[1]+c:"")}},"rmClass");function qe(r){for(var i=r.childNodes.length;i>0;--i)r.removeChild(r.firstChild);return r}o(qe,"removeChildren");function et(r,i){return qe(r).appendChild(i)}o(et,"removeChildrenAndAdd");function Te(r,i,u,a){var c=document.createElement(r);if(u&&(c.className=u),a&&(c.style.cssText=a),typeof i=="string")c.appendChild(document.createTextNode(i));else if(i)for(var v=0;v=i)return g+(i-v);g+=S-v,g+=u-g%u,v=S+1}}o(Et,"countColumn");var St=o(function(){this.id=null,this.f=null,this.time=0,this.handler=Ar(this.onTimeout,this)},"Delayed");St.prototype.onTimeout=function(r){r.id=0,r.time<=+new Date?r.f():setTimeout(r.handler,r.time-+new Date)},St.prototype.set=function(r,i){this.f=i;var u=+new Date+r;(!this.id||u=i)return a+Math.min(g,i-c);if(c+=v-a,c+=u-c%u,a=v+1,c>=i)return a}}o(br,"findColumn");var jt=[""];function qt(r){for(;jt.length<=r;)jt.push(Se(jt)+" ");return jt[r]}o(qt,"spaceStr");function Se(r){return r[r.length-1]}o(Se,"lst");function Er(r,i){for(var u=[],a=0;a"\x80"&&(r.toUpperCase()!=r.toLowerCase()||on.test(r))}o(Gt,"isWordCharBasic");function dr(r,i){return i?i.source.indexOf("\\w")>-1&&Gt(r)?!0:i.test(r):Gt(r)}o(dr,"isWordChar");function ct(r){for(var i in r)if(r.hasOwnProperty(i)&&r[i])return!1;return!0}o(ct,"isEmpty");var Do=/[\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 vr(r){return r.charCodeAt(0)>=768&&Do.test(r)}o(vr,"isExtendingChar");function Fi(r,i,u){for(;(u<0?i>0:iu?-1:1;;){if(i==u)return i;var c=(i+u)/2,v=a<0?Math.ceil(c):Math.floor(c);if(v==i)return r(v)?i:u;r(v)?u=v:i=v+a}}o(sn,"findFirst");function In(r,i,u,a){if(!r)return a(i,u,"ltr",0);for(var c=!1,v=0;vi||i==u&&g.to==i)&&(a(Math.max(g.from,i),Math.min(g.to,u),g.level==1?"rtl":"ltr",v),c=!0)}c||a(i,u,"ltr")}o(In,"iterateBidiSections");var vi=null;function gi(r,i,u){var a;vi=null;for(var c=0;ci)return c;v.to==i&&(v.from!=v.to&&u=="before"?a=c:vi=c),v.from==i&&(v.from!=v.to&&u!="before"?a=c:vi=c)}return a??vi}o(gi,"getBidiPartAt");var Hn=function(){var r="bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN",i="nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111";function u(b){return b<=247?r.charAt(b):1424<=b&&b<=1524?"R":1536<=b&&b<=1785?i.charAt(b-1536):1774<=b&&b<=2220?"r":8192<=b&&b<=8203?"w":b==8204?"b":"L"}o(u,"charType");var a=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,c=/[stwN]/,v=/[LRr]/,g=/[Lb1n]/,S=/[1n]/;function C(b,P,A){this.level=b,this.from=P,this.to=A}return o(C,"BidiSpan"),function(b,P){var A=P=="ltr"?"L":"R";if(b.length==0||P=="ltr"&&!a.test(b))return!1;for(var j=b.length,z=[],ee=0;ee-1&&(a[i]=c.slice(0,v).concat(c.slice(v+1)))}}}o(he,"off");function ke(r,i){var u=J(r,i);if(!!u.length)for(var a=Array.prototype.slice.call(arguments,2),c=0;c0}o(Rt,"hasHandler");function Dr(r){r.prototype.on=function(i,u){H(this,i,u)},r.prototype.off=function(i,u){he(this,i,u)}}o(Dr,"eventMixin");function Jt(r){r.preventDefault?r.preventDefault():r.returnValue=!1}o(Jt,"e_preventDefault");function ti(r){r.stopPropagation?r.stopPropagation():r.cancelBubble=!0}o(ti,"e_stopPropagation");function os(r){return r.defaultPrevented!=null?r.defaultPrevented:r.returnValue==!1}o(os,"e_defaultPrevented");function to(r){Jt(r),ti(r)}o(to,"e_stop");function yi(r){return r.target||r.srcElement}o(yi,"e_target");function Gs(r){var i=r.which;return i==null&&(r.button&1?i=1:r.button&2?i=3:r.button&4&&(i=2)),R&&r.ctrlKey&&i==1&&(i=3),i}o(Gs,"e_button");var ss=function(){if(p&&x<9)return!1;var r=Te("div");return"draggable"in r||"dragDrop"in r}(),ln;function Ap(r){if(ln==null){var i=Te("span","\u200B");et(r,Te("span",[i,document.createTextNode("x")])),r.firstChild.offsetHeight!=0&&(ln=i.offsetWidth<=1&&i.offsetHeight>2&&!(p&&x<8))}var u=ln?Te("span","\u200B"):Te("span","\xA0",null,"display: inline-block; width: 1px; margin-right: -1px");return u.setAttribute("cm-text",""),u}o(Ap,"zeroWidthElement");var Ys;function Pf(r){if(Ys!=null)return Ys;var i=et(r,document.createTextNode("A\u062EA")),u=Ue(i,0,1).getBoundingClientRect(),a=Ue(i,1,2).getBoundingClientRect();return qe(r),!u||u.left==u.right?!1:Ys=a.right-u.right<3}o(Pf,"hasBadBidiRects");var Xs=` +`)+ce+`return __p +}`;var ft=Rb(function(){return bt(L,Fe+"return "+ce).apply(e,F)});if(ft.source=ce,fw(ft))throw ft;return ft}o(v2,"template");function y2(s){return Dt(s).toLowerCase()}o(y2,"toLower");function w2(s){return Dt(s).toUpperCase()}o(w2,"toUpper");function x2(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(x2,"trim");function S2(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(S2,"trimEnd");function C2(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(C2,"trimStart");function b2(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),cw(_)){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(b2,"truncate");function E2(s){return s=Dt(s),s&&li.test(s)?s.replace(Wr,ci):s}o(E2,"unescape");var _2=Na(function(s,f,m){return s+(m?" ":"")+f.toUpperCase()}),hw=yg("toUpperCase");function Db(s,f,m){return s=Dt(s),f=m?e:f,f===e?Ti(s)?Vn(s):Du(s):s.match(f)||[]}o(Db,"words");var Rb=st(function(s,f){try{return Ur(s,e,f)}catch(m){return fw(m)?m:new Qe(m)}}),T2=ts(function(s,f){return jn(f,function(m){m=Lt(m),yo(s,m,aw(s[m],s))}),s});function k2(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:Tg,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=jy,dt.prototype.reverse=qy,dt.prototype.value=yd,N.prototype.at=tM,N.prototype.chain=rM,N.prototype.commit=nM,N.prototype.next=iM,N.prototype.plant=sM,N.prototype.reverse=lM,N.prototype.toJSON=N.prototype.valueOf=N.prototype.value=aM,N.prototype.first=N.prototype.head,_s&&(N.prototype[_s]=oM),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 jk=Ue((iS,oS)=>{(function(e,t){typeof iS=="object"&&typeof oS!="undefined"?oS.exports=t():typeof define=="function"&&define.amd?define(t):e.stable=t()})(iS,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 IS=="object"&&typeof FS!="undefined"?FS.exports=t():typeof define=="function"&&define.amd?define(t):(e=e||self,e.CodeMirror=t())})(IS,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(Jt,"isWordCharBasic");function gr(r,i){return i?i.source.indexOf("\\w")>-1&&Jt(r)?!0:i.test(r):Jt(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=` -b`.split(/\n/).length!=3?function(r){for(var i=0,u=[],a=r.length;i<=a;){var c=r.indexOf(` -`,i);c==-1&&(c=r.length);var v=r.slice(i,r.charAt(c-1)=="\r"?c-1:c),g=v.indexOf("\r");g!=-1?(u.push(v.slice(0,g)),i+=g+1):(u.push(v),i=c+1)}return u}:function(r){return r.split(/\r\n?|\n/)},Dp=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},Mf=function(){var r=Te("div");return"oncopy"in r?!0:(r.setAttribute("oncopy","return;"),typeof r.oncopy=="function")}(),uu=null;function Rp(r){if(uu!=null)return uu;var i=et(r,Te("span","x")),u=i.getBoundingClientRect(),a=Ue(i,0,1).getBoundingClientRect();return uu=Math.abs(u.left-a.left)>1}o(Rp,"hasBadZoomedRects");var Ul={},ls={};function Fp(r,i){arguments.length>2&&(i.dependencies=Array.prototype.slice.call(arguments,2)),Ul[r]=i}o(Fp,"defineMode");function Af(r,i){ls[r]=i}o(Af,"defineMIME");function Qs(r){if(typeof r=="string"&&ls.hasOwnProperty(r))r=ls[r];else if(r&&typeof r.name=="string"&&ls.hasOwnProperty(r.name)){var i=ls[r.name];typeof i=="string"&&(i={name:i}),r=ei(i,r),r.name=i.name}else{if(typeof r=="string"&&/^[\w\-]+\/[\w\-]+\+xml$/.test(r))return Qs("application/xml");if(typeof r=="string"&&/^[\w\-]+\/[\w\-]+\+json$/.test(r))return Qs("application/json")}return typeof r=="string"?{name:r}:r||{name:"null"}}o(Qs,"resolveMode");function fu(r,i){i=Qs(i);var u=Ul[i.name];if(!u)return fu(r,"text/plain");var a=u(r,i);if(Ro.hasOwnProperty(i.name)){var c=Ro[i.name];for(var v in c)!c.hasOwnProperty(v)||(a.hasOwnProperty(v)&&(a["_"+v]=a[v]),a[v]=c[v])}if(a.name=i.name,i.helperType&&(a.helperType=i.helperType),i.modeProps)for(var g in i.modeProps)a[g]=i.modeProps[g];return a}o(fu,"getMode");var Ro={};function Ip(r,i){var u=Ro.hasOwnProperty(r)?Ro[r]:Ro[r]={};Kt(i,u)}o(Ip,"extendMode");function Fo(r,i){if(i===!0)return i;if(r.copyState)return r.copyState(i);var u={};for(var a in i){var c=i[a];c instanceof Array&&(c=c.concat([])),u[a]=c}return u}o(Fo,"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 Df(r,i,u){return r.startState?r.startState(i,u):!0}o(Df,"startState");var zt=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");zt.prototype.eol=function(){return this.pos>=this.string.length},zt.prototype.sol=function(){return this.pos==this.lineStart},zt.prototype.peek=function(){return this.string.charAt(this.pos)||void 0},zt.prototype.next=function(){if(this.posi},zt.prototype.eatSpace=function(){for(var r=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>r},zt.prototype.skipToEnd=function(){this.pos=this.string.length},zt.prototype.skipTo=function(r){var i=this.string.indexOf(r,this.pos);if(i>-1)return this.pos=i,!0},zt.prototype.backUp=function(r){this.pos-=r},zt.prototype.column=function(){return this.lastColumnPos0?null:(v&&i!==!1&&(this.pos+=v[0].length),v)}},zt.prototype.current=function(){return this.string.slice(this.start,this.pos)},zt.prototype.hideFirstChars=function(r,i){this.lineStart+=r;try{return i()}finally{this.lineStart-=r}},zt.prototype.lookAhead=function(r){var i=this.lineOracle;return i&&i.lookAhead(r)},zt.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 c=u.children[a],v=c.chunkSize();if(i=r.first&&iu?ae(u,Me(r,u).text.length):Hp(i,Me(r,i.line).text.length)}o(Be,"clipPos");function Hp(r,i){var u=r.ch;return u==null||u>i?ae(r.line,i):u<0?ae(r.line,0):r}o(Hp,"clipToLen");function hu(r,i){for(var u=[],a=0;athis.maxLookAhead&&(this.maxLookAhead=r),i},Wn.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}},Wn.prototype.nextLine=function(){this.line++,this.maxLookAhead>0&&this.maxLookAhead--},Wn.fromSaved=function(r,i,u){return i instanceof Ho?new Wn(r,Fo(r.mode,i.state),u,i.lookAhead):new Wn(r,Fo(r.mode,i),u)},Wn.prototype.save=function(r){var i=r!==!1?Fo(this.doc.mode,this.state):this.state;return this.maxLookAhead>0?new Ho(i,this.maxLookAhead):i};function mu(r,i,u,a){var c=[r.state.modeGen],v={};Vl(r,i.text,r.doc.mode,u,function(b,P){return c.push(b,P)},v,a);for(var g=u.state,S=o(function(b){u.baseTokens=c;var P=r.state.overlays[b],A=1,j=0;u.state=!0,Vl(r,i.text,P.mode,u,function(z,ee){for(var oe=A;jz&&c.splice(A,1,z,c[A+1],ce),A+=2,j=Math.min(z,ce)}if(!!ee)if(P.opaque)c.splice(oe,A-oe,z,"overlay "+ee),A=oe+2;else for(;oer.options.maxHighlightLength&&Fo(r.doc.mode,a.state),v=mu(r,i,a);c&&(a.state=c),i.stateAfter=a.save(!c),i.styles=v.styles,v.classes?i.styleClasses=v.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(ql,"getLineStyles");function Wo(r,i,u){var a=r.doc,c=r.display;if(!a.mode.startState)return new Wn(a,!0,i);var v=Ff(r,i,u),g=v>a.first&&Me(a,v-1).stateAfter,S=g?Wn.fromSaved(a,g,v):new Wn(a,Df(a.mode),v);return a.iter(v,i,function(C){Js(r,C.text,S);var b=S.line;C.stateAfter=b==i-1||b%5==0||b>=c.viewFrom&&bi.start)return v}throw new Error("Mode "+r.name+" failed to advance stream.")}o(el,"readToken");var tl=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 us(r,i,u,a){var c=r.doc,v=c.mode,g;i=Be(c,i);var S=Me(c,i.line),C=Wo(r,i.line,u),b=new zt(S.text,r.options.tabSize,C),P;for(a&&(P=[]);(a||b.posr.options.maxHighlightLength?(S=!1,g&&Js(r,i,a,P.pos),P.pos=i.length,A=null):A=no(el(u,P,a.state,j),v),j){var z=j[0].name;z&&(A="m-"+(A?z+" "+A:z))}if(!S||b!=A){for(;Cg;--S){if(S<=v.first)return v.first;var C=Me(v,S-1),b=C.stateAfter;if(b&&(!u||S+(b instanceof Ho?b.lookAhead:0)<=v.modeFrontier))return S;var P=Et(C.text,null,r.options.tabSize);(c==null||a>P)&&(c=S-1,a=P)}return c}o(Ff,"findStartLine");function Wp(r,i){if(r.modeFrontier=Math.min(r.modeFrontier,i),!(r.highlightFrontieru;a--){var c=Me(r,a).stateAfter;if(c&&(!(c instanceof Ho)||a+c.lookAhead=i:v.to>i);(a||(a=[])).push(new Kl(g,v.from,C?null:v.to))}}return a}o(Up,"markedSpansBefore");function $p(r,i,u){var a;if(r)for(var c=0;c=i:v.to>i);if(S||v.from==i&&g.type=="bookmark"&&(!u||v.marker.insertLeft)){var C=v.from==null||(g.inclusiveLeft?v.from<=i:v.from0&&S)for(var Oe=0;Oe0)){var P=[C,1],A=$e(b.from,S.from),j=$e(b.to,S.to);(A<0||!g.inclusiveLeft&&!A)&&P.push({from:b.from,to:S.from}),(j>0||!g.inclusiveRight&&!j)&&P.push({from:S.to,to:b.to}),c.splice.apply(c,P),C+=P.length-3}}return c}o(wu,"removeReadOnlyRanges");function Wf(r){var i=r.markedSpans;if(!!i){for(var u=0;ui)&&(!a||N(a,v.marker)<0)&&(a=v.marker)}return a}o(xu,"collapsedSpanAround");function ye(r,i,u,a,c){var v=Me(r,i),g=an&&v.markedSpans;if(g)for(var S=0;S=0&&A<=0||P<=0&&A>=0)&&(P<=0&&(C.marker.inclusiveRight&&c.inclusiveLeft?$e(b.to,u)>=0:$e(b.to,u)>0)||P>=0&&(C.marker.inclusiveRight&&c.inclusiveLeft?$e(b.from,a)<=0:$e(b.from,a)<0)))return!0}}}o(ye,"conflictingCollapsedRange");function kn(r){for(var i;i=gt(r);)r=i.find(-1,!0).line;return r}o(kn,"visualLine");function am(r){for(var i;i=Tn(r);)r=i.find(1,!0).line;return r}o(am,"visualLineEnd");function um(r){for(var i,u;i=Tn(r);)r=i.find(1,!0).line,(u||(u=[])).push(r);return u}o(um,"visualLineContinued");function Su(r,i){var u=Me(r,i),a=kn(u);return u==a?i:vt(a)}o(Su,"visualLineNo");function zp(r,i){if(i>r.lastLine())return i;var u=Me(r,i),a;if(!Nt(r,u))return i;for(;a=Tn(u);)u=a.find(1,!0).line;return vt(u)+1}o(zp,"visualLineEndNo");function Nt(r,i){var u=an&&i.markedSpans;if(u){for(var a=void 0,c=0;ci.maxLineLength&&(i.maxLineLength=c,i.maxLine=a)})}o(fs,"findMaxLine");var He=o(function(r,i,u){this.text=r,Bf(this,i),this.height=u?u(this):1},"Line");He.prototype.lineNo=function(){return vt(this)},Dr(He);function Uf(r,i,u,a){r.text=i,r.stateAfter&&(r.stateAfter=null),r.styles&&(r.styles=null),r.order!=null&&(r.order=null),Wf(r),Bf(r,u);var c=a?a(r):1;c!=r.height&&ri(r,c)}o(Uf,"updateLine");function xi(r){r.parent=null,Wf(r)}o(xi,"cleanUpLine");var Xl={},il={};function cs(r,i){if(!r||/^\s*$/.test(r))return null;var u=i.addModeClass?il:Xl;return u[r]||(u[r]=r.replace(/\S+/g,"cm-$&"))}o(cs,"interpretTokenStyle");function Cu(r,i){var u=xt("span",null,null,_?"padding-right: .1px":null),a={pre:xt("pre",[u],"CodeMirror-line"),content:u,col:0,pos:0,cm:r,trailingSpace:!1,splitSpaces:r.getOption("lineWrapping")};i.measure={};for(var c=0;c<=(i.rest?i.rest.length:0);c++){var v=c?i.rest[c-1]:i.line,g=void 0;a.pos=0,a.addToken=jp,Pf(r.display.measure)&&(g=En(v,r.doc.direction))&&(a.addToken=$f(a.addToken,g)),a.map=[];var S=i!=r.display.externalMeasured&&vt(v);Hi(v,a,ql(r,v,S)),v.styleClasses&&(v.styleClasses.bgClass&&(a.bgClass=Qt(v.styleClasses.bgClass,a.bgClass||"")),v.styleClasses.textClass&&(a.textClass=Qt(v.styleClasses.textClass,a.textClass||""))),a.map.length==0&&a.map.push(0,0,a.content.appendChild(Ap(r.display.measure))),c==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(_){var C=a.content.lastChild;(/\bcm-tab\b/.test(C.className)||C.querySelector&&C.querySelector(".cm-tab"))&&(a.content.className="cm-tab-wrap-hack")}return ke(r,"renderLine",r,i.line,a.pre),a.pre.className&&(a.textClass=Qt(a.pre.className,a.textClass||"")),a}o(Cu,"buildLineContent");function On(r){var i=Te("span","\u2022","cm-invalidchar");return i.title="\\u"+r.charCodeAt(0).toString(16),i.setAttribute("aria-label",i.title),i}o(On,"defaultSpecialCharPlaceholder");function jp(r,i,u,a,c,v,g){if(!!i){var S=r.splitSpaces?Tt(i,r.trailingSpace):i,C=r.cm.state.specialChars,b=!1,P;if(!C.test(i))r.col+=i.length,P=document.createTextNode(S),r.map.push(r.pos,r.pos+i.length,P),p&&x<9&&(b=!0),r.pos+=i.length;else{P=document.createDocumentFragment();for(var A=0;;){C.lastIndex=A;var j=C.exec(i),z=j?j.index-A:i.length-A;if(z){var ee=document.createTextNode(S.slice(A,A+z));p&&x<9?P.appendChild(Te("span",[ee])):P.appendChild(ee),r.map.push(r.pos,r.pos+z,ee),r.col+=z,r.pos+=z}if(!j)break;A+=z+1;var oe=void 0;if(j[0]==" "){var ce=r.cm.options.tabSize,me=ce-r.col%ce;oe=P.appendChild(Te("span",qt(me),"cm-tab")),oe.setAttribute("role","presentation"),oe.setAttribute("cm-text"," "),r.col+=me}else j[0]=="\r"||j[0]==` -`?(oe=P.appendChild(Te("span",j[0]=="\r"?"\u240D":"\u2424","cm-invalidchar")),oe.setAttribute("cm-text",j[0]),r.col+=1):(oe=r.cm.options.specialCharPlaceholder(j[0]),oe.setAttribute("cm-text",j[0]),p&&x<9?P.appendChild(Te("span",[oe])):P.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||c||b||v||g){var be=u||"";a&&(be+=a),c&&(be+=c);var ve=Te("span",[P],be,v);if(g)for(var Oe in g)g.hasOwnProperty(Oe)&&Oe!="style"&&Oe!="class"&&ve.setAttribute(Oe,g[Oe]);return r.content.appendChild(ve)}r.content.appendChild(P)}}o(jp,"buildToken");function Tt(r,i){if(r.length>1&&!/ /.test(r))return r;for(var u=i,a="",c=0;cb&&A.from<=b));j++);if(A.to>=P)return r(u,a,c,v,g,S,C);r(u,a.slice(0,A.to-b),c,v,null,S,C),v=null,a=a.slice(A.to-b),b=A.to}}}o($f,"buildTokenBadBidi");function Ql(r,i,u,a){var c=!a&&u.widgetNode;c&&r.map.push(r.pos,r.pos+i,c),!a&&r.cm.display.input.needsContentAttribute&&(c||(c=r.content.appendChild(document.createElement("span"))),c.setAttribute("cm-marker",u.id)),c&&(r.cm.display.input.setUneditable(c),r.content.appendChild(c)),r.pos+=i,r.trailingSpace=!1}o(Ql,"buildCollapsedSpan");function Hi(r,i,u){var a=r.markedSpans,c=r.text,v=0;if(!a){for(var g=1;gC||rt.collapsed&&Re.to==C&&Re.from==C)){if(Re.to!=null&&Re.to!=C&&z>Re.to&&(z=Re.to,oe=""),rt.className&&(ee+=" "+rt.className),rt.css&&(j=(j?j+";":"")+rt.css),rt.startStyle&&Re.from==C&&(ce+=" "+rt.startStyle),rt.endStyle&&Re.to==z&&(Oe||(Oe=[])).push(rt.endStyle,Re.to),rt.title&&((be||(be={})).title=rt.title),rt.attributes)for(var kt in rt.attributes)(be||(be={}))[kt]=rt.attributes[kt];rt.collapsed&&(!me||N(me.marker,rt)<0)&&(me=Re)}else Re.from>C&&z>Re.from&&(z=Re.from)}if(Oe)for(var yr=0;yr=S)break;for(var hn=Math.min(S,z);;){if(P){var mn=C+P.length;if(!me){var ar=mn>hn?P.slice(0,hn-C):P;i.addToken(i,ar,A?A+ee:ee,ce,C+ar.length==z?oe:"",j,be)}if(mn>=hn){P=P.slice(hn-C),C=hn;break}C=mn,ce=""}P=c.slice(v,v=u[b++]),A=cs(u[b++],i.cm.options)}}}o(Hi,"insertLineContent");function ps(r,i,u){this.line=i,this.rest=um(i),this.size=this.rest?vt(Se(this.rest))-u+1:1,this.node=this.text=null,this.hidden=Nt(r,i)}o(ps,"LineView");function ds(r,i,u){for(var a=[],c,v=i;v2&&v.push((C.bottom+b.top)/2-u.top)}}v.push(u.bottom-u.top)}}o(Vf,"ensureLineHeights");function ku(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[c],cache:r.measure.caches[c],before:!0}}o(ku,"mapFromLineView");function Kp(r,i){i=kn(i);var u=vt(i),a=r.display.externalMeasured=new ps(r.doc,i,u);a.lineN=u;var c=a.built=Cu(r,a);return a.text=c.pre,et(r.display.lineMeasure,c.pre),a}o(Kp,"updateExternalMeasurement");function Kf(r,i,u,a){return ii(r,ni(r,i),u,a)}o(Kf,"measureChar");function Ou(r,i){if(i>=r.display.viewFrom&&i=u.lineN&&ii)&&(v=C-S,c=v-1,i>=C&&(g="right")),c!=null){if(a=r[b+2],S==C&&u==(a.insertLeft?"left":"right")&&(g=u),u=="left"&&c==0)for(;b&&r[b-2]==r[b-3]&&r[b-1].insertLeft;)a=r[(b-=3)+2],g="left";if(u=="right"&&c==C-S)for(;b=0&&(u=r[c]).left==u.right;c--);return u}o(B,"getUsefulRect");function W(r,i,u,a){var c=T(i.map,u,a),v=c.node,g=c.start,S=c.end,C=c.collapse,b;if(v.nodeType==3){for(var P=0;P<4;P++){for(;g&&vr(i.line.text.charAt(c.coverStart+g));)--g;for(;c.coverStart+S0&&(C=a="right");var A;r.options.lineWrapping&&(A=v.getClientRects()).length>1?b=A[a=="right"?A.length-1:0]:b=v.getBoundingClientRect()}if(p&&x<9&&!g&&(!b||!b.left&&!b.right)){var j=v.parentNode.getClientRects()[0];j?b={left:j.left,right:j.left+na(r.display),top:j.top,bottom:j.bottom}:b=y}for(var z=b.top-i.rect.top,ee=b.bottom-i.rect.top,oe=(z+ee)/2,ce=i.view.measure.heights,me=0;me=a.text.length?(C=a.text.length,b="before"):C<=0&&(C=0,b="after"),!S)return g(b=="before"?C-1:C,b=="before");function P(ee,oe,ce){var me=S[oe],be=me.level==1;return g(ce?ee-1:ee,be!=ce)}o(P,"getBidi");var A=gi(S,C,b),j=vi,z=P(C,A,b=="before");return j!=null&&(z.other=P(C,j,b!="before")),z}o(fn,"cursorCoords");function Ci(r,i){var u=0;i=Be(r.doc,i),r.options.lineWrapping||(u=na(r.display)*i.ch);var a=Me(r.doc,i.line),c=_e(a)+sl(r.display);return{left:u,right:u,top:c,bottom:c+a.height}}o(Ci,"estimateCoords");function lo(r,i,u,a,c){var v=ae(r,i,u);return v.xRel=c,a&&(v.outside=a),v}o(lo,"PosWithInfo");function q(r,i,u){var a=r.doc;if(u+=r.display.viewOffset,u<0)return lo(a.first,0,null,-1,-1);var c=ro(a,u),v=a.first+a.size-1;if(c>v)return lo(a.first+a.size-1,Me(a,v).text.length,null,1,1);i<0&&(i=0);for(var g=Me(a,c);;){var S=Xe(r,g,c,i,u),C=xu(g,S.ch+(S.xRel>0||S.outside>0?1:0));if(!C)return S;var b=C.find(1);if(b.line==c)return b;g=Me(a,c=b.line)}}o(q,"coordsChar");function re(r,i,u,a){a-=al(i);var c=i.text.length,v=sn(function(g){return ii(r,u,g-1).bottom<=a},c,0);return c=sn(function(g){return ii(r,u,g).top>a},v,c),{begin:v,end:c}}o(re,"wrappedLineExtent");function Z(r,i,u,a){u||(u=ni(r,i));var c=ul(r,i,ii(r,u,a),"line").top;return re(r,i,u,c)}o(Z,"wrappedLineExtentChar");function Ne(r,i,u,a){return r.bottom<=u?!1:r.top>u?!0:(a?r.left:r.right)>i}o(Ne,"boxIsAfter");function Xe(r,i,u,a,c){c-=_e(i);var v=ni(r,i),g=al(i),S=0,C=i.text.length,b=!0,P=En(i,r.doc.direction);if(P){var A=(r.options.lineWrapping?Tr:_t)(r,i,u,v,P,a,c);b=A.level!=1,S=b?A.from:A.to-1,C=b?A.to:A.from-1}var j=null,z=null,ee=sn(function(Fe){var Re=ii(r,v,Fe);return Re.top+=g,Re.bottom+=g,Ne(Re,a,c,!1)?(Re.top<=c&&Re.left<=a&&(j=Fe,z=Re),!0):!1},S,C),oe,ce,me=!1;if(z){var be=a-z.left=Oe.bottom?1:0}return ee=Fi(i.text,ee,1),lo(u,ee,ce,me,a-oe)}o(Xe,"coordsCharInner");function _t(r,i,u,a,c,v,g){var S=sn(function(A){var j=c[A],z=j.level!=1;return Ne(fn(r,ae(u,z?j.to:j.from,z?"before":"after"),"line",i,a),v,g,!0)},0,c.length-1),C=c[S];if(S>0){var b=C.level!=1,P=fn(r,ae(u,b?C.from:C.to,b?"after":"before"),"line",i,a);Ne(P,v,g,!0)&&P.top>g&&(C=c[S-1])}return C}o(_t,"coordsBidiPart");function Tr(r,i,u,a,c,v,g){var S=re(r,i,a,g),C=S.begin,b=S.end;/\s/.test(i.text.charAt(b-1))&&b--;for(var P=null,A=null,j=0;j=b||z.to<=C)){var ee=z.level!=1,oe=ii(r,a,ee?Math.min(b,z.to)-1:Math.max(C,z.from)).right,ce=oece)&&(P=z,A=ce)}}return P||(P=c[c.length-1]),P.fromb&&(P={from:P.from,to:b,level:P.level}),P}o(Tr,"coordsBidiPartWrapped");var ht;function vs(r){if(r.cachedTextHeight!=null)return r.cachedTextHeight;if(ht==null){ht=Te("pre",null,"CodeMirror-line-like");for(var i=0;i<49;++i)ht.appendChild(document.createTextNode("x")),ht.appendChild(Te("br"));ht.appendChild(document.createTextNode("x"))}et(r.measure,ht);var u=ht.offsetHeight/50;return u>3&&(r.cachedTextHeight=u),qe(r.measure),u||1}o(vs,"textHeight");function na(r){if(r.cachedCharWidth!=null)return r.cachedCharWidth;var i=Te("span","xxxxxxxxxx"),u=Te("pre",[i],"CodeMirror-line-like");et(r.measure,u);var a=i.getBoundingClientRect(),c=(a.right-a.left)/10;return c>2&&(r.cachedCharWidth=c),c||10}o(na,"charWidth");function $n(r){for(var i=r.display,u={},a={},c=i.gutters.clientLeft,v=i.gutters.firstChild,g=0;v;v=v.nextSibling,++g){var S=r.display.gutterSpecs[g].className;u[S]=v.offsetLeft+v.clientLeft+c,a[S]=v.clientWidth}return{fixedPos:ia(i),gutterTotalWidth:i.gutters.offsetWidth,gutterLeft:u,gutterWidth:a,wrapperWidth:i.wrapper.clientWidth}}o($n,"getDimensions");function ia(r){return r.scroller.getBoundingClientRect().left-r.sizer.getBoundingClientRect().left}o(ia,"compensateForHScroll");function fm(r){var i=vs(r.display),u=r.options.lineWrapping,a=u&&Math.max(5,r.display.scroller.clientWidth/na(r.display)-3);return function(c){if(Nt(r.doc,c))return 0;var v=0;if(c.widgets)for(var g=0;g0&&(b=Me(r.doc,C.line).text).length==C.ch){var P=Et(b,b.length,r.options.tabSize)-b.length;C=ae(C.line,Math.max(0,Math.round((v-ll(r.display).left)/na(r.display))-P))}return C}o(ao,"posFromMouse");function uo(r,i){if(i>=r.display.viewTo||(i-=r.display.viewFrom,i<0))return null;for(var u=r.display.view,a=0;ai)&&(c.updateLineNumbers=i),r.curOp.viewChanged=!0,i>=c.viewTo)an&&Su(r.doc,i)c.viewFrom?$o(r):(c.viewFrom+=a,c.viewTo+=a);else if(i<=c.viewFrom&&u>=c.viewTo)$o(r);else if(i<=c.viewFrom){var v=fl(r,u,u+a,1);v?(c.view=c.view.slice(v.index),c.viewFrom=v.lineN,c.viewTo+=a):$o(r)}else if(u>=c.viewTo){var g=fl(r,i,i,-1);g?(c.view=c.view.slice(0,g.index),c.viewTo=g.lineN):$o(r)}else{var S=fl(r,i,i,-1),C=fl(r,u,u+a,1);S&&C?(c.view=c.view.slice(0,S.index).concat(ds(r,S.lineN,C.lineN)).concat(c.view.slice(C.index)),c.viewTo+=a):$o(r)}var b=c.externalMeasured;b&&(u=c.lineN&&i=a.viewTo)){var v=a.view[uo(r,i)];if(v.node!=null){var g=v.changes||(v.changes=[]);at(g,u)==-1&&g.push(u)}}}o(ys,"regLineChange");function $o(r){r.display.viewFrom=r.display.viewTo=r.doc.first,r.display.view=[],r.display.viewOffset=0}o($o,"resetView");function fl(r,i,u,a){var c=uo(r,i),v,g=r.display.view;if(!an||u==r.doc.first+r.doc.size)return{index:c,lineN:u};for(var S=r.display.viewFrom,C=0;C0){if(c==g.length-1)return null;v=S+g[c].size-i,c++}else v=S-i;i+=v,u+=v}for(;Su(r.doc,u)!=u;){if(c==(a<0?0:g.length-1))return null;u+=a*g[c-(a<0?1:0)].size,c+=a}return{index:c,lineN:u}}o(fl,"viewCuttingPoint");function Xy(r,i,u){var a=r.display,c=a.view;c.length==0||i>=a.viewTo||u<=a.viewFrom?(a.view=ds(r,i,u),a.viewFrom=i):(a.viewFrom>i?a.view=ds(r,i,a.viewFrom).concat(a.view):a.viewFromu&&(a.view=a.view.slice(0,uo(r,u)))),a.viewTo=u}o(Xy,"adjustView");function cm(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(oa,"restartBlink");function Gp(r){r.hasFocus()||(r.display.input.focus(),r.state.focused||sa(r))}o(Gp,"ensureFocus");function Gf(r){r.state.delayingBlurEvent=!0,setTimeout(function(){r.state.delayingBlurEvent&&(r.state.delayingBlurEvent=!1,r.state.focused&&pl(r))},100)}o(Gf,"delayBlurEvent");function sa(r,i){r.state.delayingBlurEvent&&!r.state.draggingText&&(r.state.delayingBlurEvent=!1),r.options.readOnly!="nocursor"&&(r.state.focused||(ke(r,"focus",r,i),r.state.focused=!0,Ye(r.display.wrapper,"CodeMirror-focused"),!r.curOp&&r.display.selForContextMenu!=r.doc.sel&&(r.display.input.reset(),_&&setTimeout(function(){return r.display.input.reset(!0)},20)),r.display.input.receivedFocus()),oa(r))}o(sa,"onFocus");function pl(r,i){r.state.delayingBlurEvent||(r.state.focused&&(ke(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 ws(r){for(var i=r.display,u=i.lineDiv.offsetTop,a=0;a.005||P<-.005)&&(ri(c.line,g),xs(c.line),c.rest))for(var A=0;Ar.display.sizerWidth){var j=Math.ceil(S/na(r.display));j>r.display.maxLineLength&&(r.display.maxLineLength=j,r.display.maxLine=c.line,r.display.maxLineChanged=!0)}}}}o(ws,"updateHeightsInViewport");function xs(r){if(r.widgets)for(var i=0;i=g&&(v=ro(i,_e(Me(i,C))-r.wrapper.clientHeight),g=C)}return{from:v,to:Math.max(g,v+1)}}o(dl,"visibleLines");function Qy(r,i){if(!Zt(r,"scrollCursorIntoView")){var u=r.display,a=u.sizer.getBoundingClientRect(),c=null;if(i.top+a.top<0?c=!0:i.bottom+a.top>(window.innerHeight||document.documentElement.clientHeight)&&(c=!1),c!=null&&!te){var v=Te("div","\u200B",null,`position: absolute; - top: `+(i.top-u.viewOffset-sl(r.display))+`px; - height: `+(i.bottom-i.top+Vr(r)+u.barHeight)+`px; - left: `+i.left+"px; width: "+Math.max(2,i.right-i.left)+"px;");r.display.lineSpace.appendChild(v),v.scrollIntoView(c),r.display.lineSpace.removeChild(v)}}}o(Qy,"maybeScrollWindow");function Zy(r,i,u,a){a==null&&(a=0);var c;!r.options.lineWrapping&&i==u&&(u=i.sticky=="before"?ae(i.line,i.ch+1,"before"):i,i=i.ch?ae(i.line,i.sticky=="before"?i.ch-1:i.ch,"after"):i);for(var v=0;v<5;v++){var g=!1,S=fn(r,i),C=!u||u==i?S:fn(r,u);c={left:Math.min(S.left,C.left),top:Math.min(S.top,C.top)-a,right:Math.max(S.left,C.left),bottom:Math.max(S.bottom,C.bottom)+a};var b=la(r,c),P=r.doc.scrollTop,A=r.doc.scrollLeft;if(b.scrollTop!=null&&(er(r,b.scrollTop),Math.abs(r.doc.scrollTop-P)>1&&(g=!0)),b.scrollLeft!=null&&(hl(r,b.scrollLeft),Math.abs(r.doc.scrollLeft-A)>1&&(g=!0)),!g)break}return c}o(Zy,"scrollPosIntoView");function Jy(r,i){var u=la(r,i);u.scrollTop!=null&&er(r,u.scrollTop),u.scrollLeft!=null&&hl(r,u.scrollLeft)}o(Jy,"scrollIntoView");function la(r,i){var u=r.display,a=vs(r.display);i.top<0&&(i.top=0);var c=r.curOp&&r.curOp.scrollTop!=null?r.curOp.scrollTop:u.scroller.scrollTop,v=ta(r),g={};i.bottom-i.top>v&&(i.bottom=i.top+v);var S=r.doc.height+Rr(u),C=i.topS-a;if(i.topc+v){var P=Math.min(i.top,(b?S:i.bottom)-v);P!=c&&(g.scrollTop=P)}var A=r.options.fixedGutter?0:u.gutters.offsetWidth,j=r.curOp&&r.curOp.scrollLeft!=null?r.curOp.scrollLeft:u.scroller.scrollLeft-A,z=Bi(r)-u.gutters.offsetWidth,ee=i.right-i.left>z;return ee&&(i.right=i.left+z),i.left<10?g.scrollLeft=0:i.leftz+j-3&&(g.scrollLeft=i.right+(ee?0:10)-z),g}o(la,"calculateScrollPos");function aa(r,i){i!=null&&(Yf(r),r.curOp.scrollTop=(r.curOp.scrollTop==null?r.doc.scrollTop:r.curOp.scrollTop)+i)}o(aa,"addToScrollTop");function Ss(r){Yf(r);var i=r.getCursor();r.curOp.scrollToPos={from:i,to:i,margin:r.options.cursorScrollMargin}}o(Ss,"ensureCursorVisible");function Pu(r,i,u){(i!=null||u!=null)&&Yf(r),i!=null&&(r.curOp.scrollLeft=i),u!=null&&(r.curOp.scrollTop=u)}o(Pu,"scrollToCoords");function dm(r,i){Yf(r),r.curOp.scrollToPos=i}o(dm,"scrollToRange");function Yf(r){var i=r.curOp.scrollToPos;if(i){r.curOp.scrollToPos=null;var u=Ci(r,i.from),a=Ci(r,i.to);hm(r,u,a,i.margin)}}o(Yf,"resolveScrollToPos");function hm(r,i,u,a){var c=la(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});Pu(r,c.scrollLeft,c.scrollTop)}o(hm,"scrollToCoordsRange");function er(r,i){Math.abs(r.doc.scrollTop-i)<2||(n||Xp(r,{top:i}),Kr(r,i,!0),n&&Xp(r),co(r,100))}o(er,"updateScrollTop");function Kr(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(Kr,"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,mm(r),r.display.scroller.scrollLeft!=i&&(r.display.scroller.scrollLeft=i),r.display.scrollbars.setScrollLeft(i))}o(hl,"setScrollLeft");function Mu(r){var i=r.display,u=i.gutters.offsetWidth,a=Math.round(r.doc.height+Rr(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+Vr(r)+i.barHeight,nativeBarWidth:i.nativeBarWidth,gutterWidth:u}}o(Mu,"measureForScrollbars");var Cs=o(function(r,i,u){this.cm=u;var a=this.vert=Te("div",[Te("div",null,null,"min-width: 1px")],"CodeMirror-vscrollbar"),c=this.horiz=Te("div",[Te("div",null,null,"height: 100%; min-height: 1px")],"CodeMirror-hscrollbar");a.tabIndex=c.tabIndex=-1,r(a),r(c),H(a,"scroll",function(){a.clientHeight&&i(a.scrollTop,"vertical")}),H(c,"scroll",function(){c.clientWidth&&i(c.scrollLeft,"horizontal")}),this.checkedZeroWidth=!1,p&&x<8&&(this.horiz.style.minHeight=this.vert.style.minWidth="18px")},"NativeScrollbars");Cs.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 c=r.viewHeight-(i?a:0);this.vert.firstChild.style.height=Math.max(0,r.scrollHeight-r.clientHeight+c)+"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 v=r.viewWidth-r.barLeft-(u?a:0);this.horiz.firstChild.style.width=Math.max(0,r.scrollWidth-r.clientWidth+v)+"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}},Cs.prototype.setScrollLeft=function(r){this.horiz.scrollLeft!=r&&(this.horiz.scrollLeft=r),this.disableHoriz&&this.enableZeroWidthBar(this.horiz,this.disableHoriz,"horiz")},Cs.prototype.setScrollTop=function(r){this.vert.scrollTop!=r&&(this.vert.scrollTop=r),this.disableVert&&this.enableZeroWidthBar(this.vert,this.disableVert,"vert")},Cs.prototype.zeroWidthHack=function(){var r=R&&!X?"12px":"18px";this.horiz.style.height=this.vert.style.width=r,this.horiz.style.pointerEvents=this.vert.style.pointerEvents="none",this.disableHoriz=new St,this.disableVert=new St},Cs.prototype.enableZeroWidthBar=function(r,i,u){r.style.pointerEvents="auto";function a(){var c=r.getBoundingClientRect(),v=u=="vert"?document.elementFromPoint(c.right-1,(c.top+c.bottom)/2):document.elementFromPoint((c.right+c.left)/2,c.bottom-1);v!=r?r.style.pointerEvents="none":i.set(1e3,a)}o(a,"maybeDisable"),i.set(1e3,a)},Cs.prototype.clear=function(){var r=this.horiz.parentNode;r.removeChild(this.horiz),r.removeChild(this.vert)};var Au=o(function(){},"NullScrollbars");Au.prototype.update=function(){return{bottom:0,right:0}},Au.prototype.setScrollLeft=function(){},Au.prototype.setScrollTop=function(){},Au.prototype.clear=function(){};function _s(r,i){i||(i=Mu(r));var u=r.display.barWidth,a=r.display.barHeight;ua(r,i);for(var c=0;c<4&&u!=r.display.barWidth||a!=r.display.barHeight;c++)u!=r.display.barWidth&&r.options.lineWrapping&&ws(r),ua(r,Mu(r)),u=r.display.barWidth,a=r.display.barHeight}o(_s,"updateScrollbars");function ua(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(ua,"updateScrollbarsInner");var Xf={native:Cs,null:Au};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 Xf[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):er(r,i)},r),r.display.scrollbars.addClass&&Ye(r.display.wrapper,r.display.scrollbars.addClass)}o(ml,"initScrollbars");var Du=0;function Ui(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:++Du,markArrays:null},io(r.curOp)}o(Ui,"startOperation");function fo(r){var i=r.curOp;i&&zf(i,function(u){for(var a=0;a=u.viewTo)||u.maxLineChanged&&i.options.lineWrapping,r.update=r.mustUpdate&&new Ln(i,r.mustUpdate&&{top:r.scrollTop,ensure:r.scrollToPos},r.forceUpdate)}o(e0,"endOperation_R1");function t0(r){r.updatedDisplay=r.mustUpdate&&Yp(r.cm,r.update)}o(t0,"endOperation_W1");function r0(r){var i=r.cm,u=i.display;r.updatedDisplay&&ws(i),r.barMeasure=Mu(i),u.maxLineChanged&&!i.options.lineWrapping&&(r.adjustWidthTo=Kf(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+Vr(i)+i.display.barWidth),r.maxScrollLeft=Math.max(0,u.sizer.offsetLeft+r.adjustWidthTo-Bi(i))),(r.updatedDisplay||r.selectionChanged)&&(r.preparedSelection=u.input.prepareSelection())}o(r0,"endOperation_R2");function n0(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=Wo(r,i.highlightFrontier),c=[];i.iter(a.line,Math.min(i.first+i.size,r.display.viewTo+500),function(v){if(a.line>=r.display.viewFrom){var g=v.styles,S=v.text.length>r.options.maxHighlightLength?Fo(i.mode,a.state):null,C=mu(r,v,a,!0);S&&(a.state=S),v.styles=C.styles;var b=v.styleClasses,P=C.classes;P?v.styleClasses=P:b&&(v.styleClasses=null);for(var A=!g||g.length!=v.styles.length||b!=P&&(!b||!P||b.bgClass!=P.bgClass||b.textClass!=P.textClass),j=0;!A&&ju)return co(r,r.options.workDelay),!0}),i.highlightFrontier=a.line,i.modeFrontier=Math.max(i.modeFrontier,a.line),c.length&&Gr(r,function(){for(var v=0;v=u.viewFrom&&i.visible.to<=u.viewTo&&(u.updateLineNumbers==null||u.updateLineNumbers>=u.viewTo)&&u.renderedView==u.view&&cm(r)==0)return!1;po(r)&&($o(r),i.dims=$n(r));var c=a.first+a.size,v=Math.max(i.visible.from-r.options.viewportMargin,a.first),g=Math.min(c,i.visible.to+r.options.viewportMargin);u.viewFromg&&u.viewTo-g<20&&(g=Math.min(c,u.viewTo)),an&&(v=Su(r.doc,v),g=zp(r.doc,g));var S=v!=u.viewFrom||g!=u.viewTo||u.lastWrapHeight!=i.wrapperHeight||u.lastWrapWidth!=i.wrapperWidth;Xy(r,v,g),u.viewOffset=_e(Me(r.doc,u.viewFrom)),r.display.mover.style.top=u.viewOffset+"px";var C=cm(r);if(!S&&C==0&&!i.force&&u.renderedView==u.view&&(u.updateLineNumbers==null||u.updateLineNumbers>=u.viewTo))return!1;var b=o0(r);return C>4&&(u.lineDiv.style.display="none"),l0(r,u.updateLineNumbers,i.dims),C>4&&(u.lineDiv.style.display=""),u.renderedView=u.view,s0(b),qe(u.cursorDiv),qe(u.selectionDiv),u.gutters.style.height=u.sizer.style.minHeight=0,S&&(u.lastWrapHeight=i.wrapperHeight,u.lastWrapWidth=i.wrapperWidth,co(r,400)),u.updateLineNumbers=null,!0}o(Yp,"updateDisplayIfNeeded");function bs(r,i){for(var u=i.viewport,a=!0;;a=!1){if(!a||!r.options.lineWrapping||i.oldDisplayWidth==Bi(r)){if(u&&u.top!=null&&(u={top:Math.min(r.doc.height+Rr(r.display)-ta(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(!Yp(r,i))break;ws(r);var c=Mu(r);Lu(r),_s(r,c),Zp(r,c),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(bs,"postUpdateDisplay");function Xp(r,i){var u=new Ln(r,i);if(Yp(r,u)){ws(r),bs(r,u);var a=Mu(r);Lu(r),_s(r,a),Zp(r,a),u.finish()}}o(Xp,"updateDisplaySimple");function l0(r,i,u){var a=r.display,c=r.options.lineNumbers,v=a.lineDiv,g=v.firstChild;function S(ee){var oe=ee.nextSibling;return _&&R&&r.display.currentWheelTarget==ee?ee.style.display="none":ee.parentNode.removeChild(ee),oe}o(S,"rm");for(var C=a.view,b=a.viewFrom,P=0;P-1&&(z=!1),jf(r,A,b,u)),z&&(qe(A.lineNumber),A.lineNumber.appendChild(document.createTextNode(zl(r.options,b)))),g=A.node.nextSibling}b+=A.size}for(;g;)g=S(g)}o(l0,"patchDisplay");function Qp(r){var i=r.gutters.offsetWidth;r.sizer.style.marginLeft=i+"px",sr(r,"gutterChanged",r)}o(Qp,"updateGutterSpace");function Zp(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+Vr(r)+"px"}o(Zp,"setDocumentHeight");function mm(r){var i=r.display,u=i.view;if(!(!i.alignWidgets&&(!i.gutters.firstChild||!r.options.fixedGutter))){for(var a=ia(i)-i.scroller.scrollLeft+r.doc.scrollLeft,c=i.gutters.offsetWidth,v=a+"px",g=0;gg.clientWidth,C=g.scrollHeight>g.clientHeight;if(!!(a&&S||c&&C)){if(c&&R&&_){e:for(var b=i.target,P=v.view;b!=g;b=b.parentNode)for(var A=0;A=0&&$e(r,a.to())<=0)return u}return-1};var yt=o(function(r,i){this.anchor=r,this.head=i},"Range");yt.prototype.from=function(){return Zs(this.anchor,this.head)},yt.prototype.to=function(){return as(this.anchor,this.head)},yt.prototype.empty=function(){return this.head.line==this.anchor.line&&this.head.ch==this.anchor.ch};function Xr(r,i,u){var a=r&&r.options.selectionsMayTouch,c=i[u];i.sort(function(j,z){return $e(j.from(),z.from())}),u=at(i,c);for(var v=1;v0:C>=0){var b=Zs(S.from(),g.from()),P=as(S.to(),g.to()),A=S.empty()?g.from()==g.head:S.from()==S.head;v<=u&&--u,i.splice(--v,2,new yt(A?P:b,A?b:P))}}return new oi(i,u)}o(Xr,"normalizeSelection");function Es(r,i){return new oi([new yt(r,i||r)],0)}o(Es,"simpleSelection");function Ts(r){return r.text?ae(r.from.line+r.text.length-1,Se(r.text).length+(r.text.length==1?r.from.ch:0)):r.to}o(Ts,"changeEnd");function _i(r,i){if($e(r,i.from)<0)return r;if($e(r,i.to)<=0)return Ts(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+=Ts(i).ch-i.to.ch),ae(u,a)}o(_i,"adjustForChange");function ed(r,i){for(var u=[],a=0;a1&&r.remove(S.line+1,ee-1),r.insert(S.line+1,me)}sr(r,"change",r,i)}o(Jf,"updateDoc");function ks(r,i,u){function a(c,v,g){if(c.linked)for(var S=0;S1&&!r.done[r.done.length-2].ranges)return r.done.pop(),Se(r.done)}o(c0,"lastChangeEvent");function ho(r,i,u,a){var c=r.history;c.undone.length=0;var v=+new Date,g,S;if((c.lastOp==a||c.lastOrigin==i.origin&&i.origin&&(i.origin.charAt(0)=="+"&&c.lastModTime>v-(r.cm?r.cm.options.historyEventDelay:500)||i.origin.charAt(0)=="*"))&&(g=c0(c,c.lastOp==a)))S=Se(g.changes),$e(i.from,i.to)==0&&$e(i.from,S.to)==0?S.to=Ts(i):g.changes.push(nd(r,i));else{var C=Se(c.done);for((!C||!C.ranges)&&Nn(r.sel,c.done),g={changes:[nd(r,i)],generation:c.generation},c.done.push(g);c.done.length>c.undoDepth;)c.done.shift(),c.done[0].ranges||c.done.shift()}c.done.push(u),c.generation=++c.maxGeneration,c.lastModTime=c.lastSelTime=v,c.lastOp=c.lastSelOp=a,c.lastOrigin=c.lastSelOrigin=i.origin,S||ke(r,"historyAdded")}o(ho,"addChangeToHistory");function od(r,i,u,a){var c=i.charAt(0);return c=="*"||c=="+"&&u.ranges.length==a.ranges.length&&u.somethingSelected()==a.somethingSelected()&&new Date-r.history.lastSelTime<=(r.cm?r.cm.options.historyEventDelay:500)}o(od,"selectionEventCanBeMerged");function gl(r,i,u,a){var c=r.history,v=a&&a.origin;u==c.lastSelOp||v&&c.lastSelOrigin==v&&(c.lastModTime==c.lastSelTime&&c.lastOrigin==v||od(r,v,Se(c.done),i))?c.done[c.done.length-1]=i:Nn(i,c.done),c.lastSelTime=+new Date,c.lastSelOrigin=v,c.lastSelOp=u,a&&a.clearRedo!==!1&&id(c.undone)}o(gl,"addSelectionToHistory");function Nn(r,i){var u=Se(i);u&&u.ranges&&u.equals(r)||i.push(r)}o(Nn,"pushSelectionToHistory");function Sm(r,i,u,a){var c=i["spans_"+r.id],v=0;r.iter(Math.max(r.first,u),Math.min(r.first+r.size,a),function(g){g.markedSpans&&((c||(c=i["spans_"+r.id]={}))[v]=g.markedSpans),++v})}o(Sm,"attachLocalSpans");function Cm(r){if(!r)return null;for(var i,u=0;u-1&&(Se(S)[A]=b[A],delete b[A])}}return a}o(si,"copyHistoryArray");function tc(r,i,u,a){if(a){var c=r.anchor;if(u){var v=$e(i,c)<0;v!=$e(u,c)<0?(c=i,i=u):v!=$e(i,u)<0&&(i=u)}return new yt(c,i)}else return new yt(u||i,i)}o(tc,"extendRange");function rc(r,i,u,a,c){c==null&&(c=r.cm&&(r.cm.display.shift||r.extend)),Ir(r,new oi([tc(r.sel.primary(),i,u,c)],0),a)}o(rc,"extendSelection");function Hu(r,i,u){for(var a=[],c=r.cm&&(r.cm.display.shift||r.extend),v=0;v=i.ch:S.to>i.ch))){if(c&&(ke(C,"beforeCursorEnter"),C.explicitlyCleared))if(v.markedSpans){--g;continue}else break;if(!C.atomic)continue;if(u){var A=C.find(a<0?1:-1),j=void 0;if((a<0?P:b)&&(A=oc(r,A,-a,A&&A.line==i.line?v:null)),A&&A.line==i.line&&(j=$e(A,u))&&(a<0?j<0:j>0))return yl(r,A,i,a,c)}var z=C.find(a<0?-1:1);return(a<0?b:P)&&(z=oc(r,z,a,z.line==i.line?v:null)),z?yl(r,z,i,a,c):null}}return i}o(yl,"skipAtomicInner");function Hr(r,i,u,a,c){var v=a||1,g=yl(r,i,u,v,c)||!c&&yl(r,i,u,v,!0)||yl(r,i,u,-v,c)||!c&&yl(r,i,u,-v,!0);return g||(r.cantEdit=!0,ae(r.first,0))}o(Hr,"skipAtomic");function oc(r,i,u,a){return u<0&&i.ch==0?i.line>r.first?Be(r,ae(i.line-1)):null:u>0&&i.ch==(a||Me(r,i.line)).text.length?i.line=0;--c)sc(r,{from:a[c].from,to:a[c].to,text:c?[""]:i.text,origin:i.origin});else sc(r,i)}}o(pa,"makeChange");function sc(r,i){if(!(i.text.length==1&&i.text[0]==""&&$e(i.from,i.to)==0)){var u=ed(r,i);ho(r,i,u,r.cm?r.cm.curOp.id:NaN),ha(r,i,u,yu(r,i));var a=[];ks(r,function(c,v){!v&&at(a,c.history)==-1&&(km(c.history,i),a.push(c.history)),ha(c,i,null,yu(c,i))})}}o(sc,"makeChangeInner");function lc(r,i,u){var a=r.cm&&r.cm.state.suppressEdits;if(!(a&&!u)){for(var c=r.history,v,g=r.sel,S=i=="undo"?c.done:c.undone,C=i=="undo"?c.undone:c.done,b=0;b=0;--z){var ee=j(z);if(ee)return ee.v}}}}o(lc,"makeChangeFromHistory");function da(r,i){if(i!=0&&(r.first+=i,r.sel=new oi(Er(r.sel.ranges,function(c){return new yt(ae(c.anchor.line+i,c.anchor.ch),ae(c.head.line+i,c.head.ch))}),r.sel.primIndex),r.cm)){Je(r.cm,r.first,r.first-i,i);for(var u=r.cm.display,a=u.viewFrom;ar.lastLine())){if(i.from.linev&&(i={from:i.from,to:ae(v,Me(r,v).text.length),text:[i.text[0]],origin:i.origin}),i.removed=wi(r,i.from,i.to),u||(u=ed(r,i)),r.cm?p0(r.cm,i,a):Jf(r,i,a),li(r,u,$t),r.cantEdit&&Hr(r,ae(r.firstLine(),0))&&(r.cantEdit=!1)}}o(ha,"makeChangeSingleDoc");function p0(r,i,u){var a=r.doc,c=r.display,v=i.from,g=i.to,S=!1,C=v.line;r.options.lineWrapping||(C=vt(kn(Me(a,v.line))),a.iter(C,g.line+1,function(z){if(z==c.maxLine)return S=!0,!0})),a.sel.contains(i.from,i.to)>-1&&Bl(r),Jf(a,i,u,fm(r)),r.options.lineWrapping||(a.iter(C,v.line+i.text.length,function(z){var ee=Bo(z);ee>c.maxLineLength&&(c.maxLine=z,c.maxLineLength=ee,c.maxLineChanged=!0,S=!1)}),S&&(r.curOp.updateMaxLine=!0)),Wp(a,v.line),co(r,400);var b=i.text.length-(g.line-v.line)-1;i.full?Je(r):v.line==g.line&&i.text.length==1&&!rd(r.doc,i)?ys(r,v.line,"text"):Je(r,v.line,g.line+1,b);var P=Rt(r,"changes"),A=Rt(r,"change");if(A||P){var j={from:v,to:g,text:i.text,removed:i.removed,origin:i.origin};A&&sr(r,"change",r,j),P&&(r.curOp.changeObjs||(r.curOp.changeObjs=[])).push(j)}r.display.selForContextMenu=null}o(p0,"makeChangeSingleDocInEditor");function ma(r,i,u,a,c){var v;a||(a=u),$e(a,u)<0&&(v=[a,u],u=v[0],a=v[1]),typeof i=="string"&&(i=r.splitLines(i)),pa(r,{from:u,to:a,text:i,origin:c})}o(ma,"replaceRange");function va(r,i,u,a){u1||!(this.children[0]instanceof ga))){var S=[];this.collapse(S),this.children=[new ga(S)],this.children[0].parent=this}},collapse:function(r){for(var i=0;i50){for(var g=c.lines.length%25+25,S=g;S10);r.parent.maybeSpill()}},iterN:function(r,i,u){for(var a=0;ar.display.maxLineLength&&(r.display.maxLine=b,r.display.maxLineLength=P,r.display.maxLineChanged=!0)}a!=null&&r&&this.collapsed&&Je(r,a,c+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,r&&Wu(r.doc)),r&&sr(r,"markerCleared",r,this,a,c),i&&fo(r),this.parent&&this.parent.clear()}},Ls.prototype.find=function(r,i){r==null&&this.type=="bookmark"&&(r=1);for(var u,a,c=0;c0||g==0&&v.clearWhenEmpty!==!1)return v;if(v.replacedWith&&(v.collapsed=!0,v.widgetNode=xt("span",[v.replacedWith],"CodeMirror-widget"),a.handleMouseEvents||v.widgetNode.setAttribute("cm-ignore-events","true"),a.insertLeft&&(v.widgetNode.insertLeft=!0)),v.collapsed){if(ye(r,i.line,i,u,v)||i.line!=u.line&&ye(r,u.line,i,u,v))throw new Error("Inserting collapsed marker partially overlapping an existing one");gu()}v.addToHistory&&ho(r,{from:i,to:u,origin:"markText"},r.sel,NaN);var S=i.line,C=r.cm,b;if(r.iter(S,u.line+1,function(A){C&&v.collapsed&&!C.options.lineWrapping&&kn(A)==C.display.maxLine&&(b=!0),v.collapsed&&S!=i.line&&ri(A,0),If(A,new Kl(v,S==i.line?i.ch:null,S==u.line?u.ch:null),r.cm&&r.cm.curOp),++S}),v.collapsed&&r.iter(i.line,u.line+1,function(A){Nt(r,A)&&ri(A,0)}),v.clearOnEnter&&H(v,"beforeCursorEnter",function(){return v.clear()}),v.readOnly&&(vu(),(r.history.done.length||r.history.undone.length)&&r.clearHistory()),v.collapsed&&(v.id=++ac,v.atomic=!0),C){if(b&&(C.curOp.updateMaxLine=!0),v.collapsed)Je(C,i.line,u.line+1);else if(v.className||v.startStyle||v.endStyle||v.css||v.attributes||v.title)for(var P=i.line;P<=u.line;P++)ys(C,P,"text");v.atomic&&Wu(C.doc),sr(C,"markerAdded",C,v)}return v}o(Ns,"markText");var ya=o(function(r,i){this.markers=r,this.primary=i;for(var u=0;u=0;C--)pa(this,a[C]);S?nc(this,S):this.cm&&Ss(this.cm)}),undo:k(function(){lc(this,"undo")}),redo:k(function(){lc(this,"redo")}),undoSelection:k(function(){lc(this,"undo",!0)}),redoSelection:k(function(){lc(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(c.marker.parent||c.marker)}return i},findMarks:function(r,i,u){r=Be(this,r),i=Be(this,i);var a=[],c=r.line;return this.iter(r.line,i.line+1,function(v){var g=v.markedSpans;if(g)for(var S=0;S=C.to||C.from==null&&c!=r.line||C.from!=null&&c==i.line&&C.from>=i.ch)&&(!u||u(C.marker))&&a.push(C.marker.parent||C.marker)}++c}),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-=v,++u}),Be(this,ae(u,i))},indexFromPos:function(r){r=Be(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 P=r.dataTransfer.getData("Text");if(P){var A;if(i.state.draggingText&&!i.state.draggingText.copy&&(A=i.listSelections()),li(i.doc,Es(u,u)),A)for(var j=0;j=0;S--)ma(r.doc,"",a[S].from,a[S].to,"+delete");Ss(r)})}o(ai,"deleteNearSelection");function ju(r,i,u){var a=Fi(r.text,i+u,u);return a<0||a>r.text.length?null:a}o(ju,"moveCharLogically");function pc(r,i,u){var a=ju(r,i.ch,u);return a==null?null:new ae(i.line,a,u<0?"after":"before")}o(pc,"moveLogically");function wa(r,i,u,a,c){if(r){i.doc.direction=="rtl"&&(c=-c);var v=En(u,i.doc.direction);if(v){var g=c<0?Se(v):v[0],S=c<0==(g.level==1),C=S?"after":"before",b;if(g.level>0||i.doc.direction=="rtl"){var P=ni(i,u);b=c<0?u.text.length-1:0;var A=ii(i,P,b).top;b=sn(function(j){return ii(i,P,j).top==A},c<0==(g.level==1)?g.from:g.to-1,b),C=="before"&&(b=ju(u,b,1))}else b=c<0?g.to:g.from;return new ae(a,b,C)}}return new ae(a,c<0?u.text.length:0,c<0?"before":"after")}o(wa,"endOfLine");function Rm(r,i,u,a){var c=En(i,r.doc.direction);if(!c)return pc(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 v=gi(c,u.ch,u.sticky),g=c[v];if(r.doc.direction=="ltr"&&g.level%2==0&&(a>0?g.to>u.ch:g.from=g.from&&j>=P.begin)){var z=A?"before":"after";return new ae(u.line,j,z)}}var ee=o(function(me,be,ve){for(var Oe=o(function(kt,yr){return yr?new ae(u.line,S(kt,1),"before"):new ae(u.line,kt,"after")},"getRes");me>=0&&me0==(Fe.level!=1),rt=Re?ve.begin:S(ve.end,-1);if(Fe.from<=rt&&rt0?P.end:S(P.begin,-1);return ce!=null&&!(a>0&&ce==i.text.length)&&(oe=ee(a>0?0:c.length-1,a,b(ce)),oe)?oe:null}o(Rm,"moveVisually");var xl={selectAll:bm,singleSelection:function(r){return r.setSelection(r.getCursor("anchor"),r.getCursor("head"),$t)},killLine:function(r){return ai(r,function(i){if(i.empty()){var u=Me(r.doc,i.head.line).text.length;return i.head.ch==u&&i.head.line0)c=new ae(c.line,c.ch+1),r.replaceRange(v.charAt(c.ch-1)+v.charAt(c.ch-2),ae(c.line,c.ch-2),c,"+transpose");else if(c.line>r.doc.first){var g=Me(r.doc,c.line-1).text;g&&(c=new ae(c.line,1),r.replaceRange(v.charAt(0)+r.doc.lineSeparator()+g.charAt(g.length-1),ae(c.line-1,g.length-1),c,"+transpose"))}}u.push(new yt(c,c))}r.setSelections(u)})},newlineAndIndent:function(r){return Gr(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&&$e(i,this.pos)==0&&u==this.button};var Wr,zn;function w0(r,i){var u=+new Date;return zn&&zn.compare(u,r,i)?(Wr=zn=null,"triple"):Wr&&Wr.compare(u,r,i)?(zn=new mc(u,r,i),Wr=null,"double"):(Wr=new mc(u,r,i),zn=null,"single")}o(w0,"clickRepeat");function Bm(r){var i=this,u=i.display;if(!(Zt(i,r)||u.activeTouch&&u.input.supportsTouch())){if(u.input.ensurePolled(),u.shift=r.shiftKey,Wi(u,r)){_||(u.scroller.draggable=!1,setTimeout(function(){return u.scroller.draggable=!0},100));return}if(!wd(i,r)){var a=ao(i,r),c=Gs(r),v=a?w0(a,c):"single";window.focus(),c==1&&i.state.selectingText&&i.state.selectingText(r),!(a&&vc(i,c,a,v,r))&&(c==1?a?Um(i,a,v,r):yi(r)==u.scroller&&Jt(r):c==2?(a&&rc(i.doc,a),setTimeout(function(){return u.input.focus()},20)):c==3&&(de?i.display.input.onContextMenu(r):Gf(i)))}}}o(Bm,"onMouseDown");function vc(r,i,u,a,c){var v="Click";return a=="double"?v="Double"+v:a=="triple"&&(v="Triple"+v),v=(i==1?"Left":i==2?"Middle":"Right")+v,xa(r,pd(v,c),c,function(g){if(typeof g=="string"&&(g=xl[g]),!g)return!1;var S=!1;try{r.isReadOnly()&&(r.state.suppressEdits=!0),S=g(r,u)!=Ut}finally{r.state.suppressEdits=!1}return S})}o(vc,"handleMappedButton");function Sa(r,i,u){var a=r.getOption("configureMouse"),c=a?a(r,i,u):{};if(c.unit==null){var v=K?u.shiftKey&&u.metaKey:u.altKey;c.unit=v?"rectangle":i=="single"?"char":i=="double"?"word":"line"}return(c.extend==null||r.doc.extend)&&(c.extend=r.doc.extend||u.shiftKey),c.addNew==null&&(c.addNew=R?u.metaKey:u.ctrlKey),c.moveOnDrag==null&&(c.moveOnDrag=!(R?u.altKey:u.ctrlKey)),c}o(Sa,"configureMouse");function Um(r,i,u,a){p?setTimeout(Ar(Gp,r),0):r.curOp.focus=Ke();var c=Sa(r,u,a),v=r.doc.sel,g;r.options.dragDrop&&ss&&!r.isReadOnly()&&u=="single"&&(g=v.contains(i))>-1&&($e((g=v.ranges[g]).from(),i)<0||i.xRel>0)&&($e(g.to(),i)>0||i.xRel<0)?$m(r,a,i,c):jm(r,a,i,c)}o(Um,"leftButtonDown");function $m(r,i,u,a){var c=r.display,v=!1,g=tr(r,function(b){_&&(c.scroller.draggable=!1),r.state.draggingText=!1,r.state.delayingBlurEvent&&(r.hasFocus()?r.state.delayingBlurEvent=!1:Gf(r)),he(c.wrapper.ownerDocument,"mouseup",g),he(c.wrapper.ownerDocument,"mousemove",S),he(c.scroller,"dragstart",C),he(c.scroller,"drop",g),v||(Jt(b),a.addNew||rc(r.doc,u,null,null,a.extend),_&&!U||p&&x==9?setTimeout(function(){c.wrapper.ownerDocument.body.focus({preventScroll:!0}),c.input.focus()},20):c.input.focus())}),S=o(function(b){v=v||Math.abs(i.clientX-b.clientX)+Math.abs(i.clientY-b.clientY)>=10},"mouseMove"),C=o(function(){return v=!0},"dragStart");_&&(c.scroller.draggable=!0),r.state.draggingText=g,g.copy=!a.moveOnDrag,H(c.wrapper.ownerDocument,"mouseup",g),H(c.wrapper.ownerDocument,"mousemove",S),H(c.scroller,"dragstart",C),H(c.scroller,"drop",g),r.state.delayingBlurEvent=!0,setTimeout(function(){return c.input.focus()},20),c.scroller.dragDrop&&c.scroller.dragDrop()}o($m,"leftButtonStartDrag");function zm(r,i,u){if(u=="char")return new yt(i,i);if(u=="word")return r.findWordAt(i);if(u=="line")return new yt(ae(i.line,0),Be(r.doc,ae(i.line+1,0)));var a=u(r,i);return new yt(a.from,a.to)}o(zm,"rangeForUnit");function jm(r,i,u,a){p&&Gf(r);var c=r.display,v=r.doc;Jt(i);var g,S,C=v.sel,b=C.ranges;if(a.addNew&&!a.extend?(S=v.sel.contains(u),S>-1?g=b[S]:g=new yt(u,u)):(g=v.sel.primary(),S=v.sel.primIndex),a.unit=="rectangle")a.addNew||(g=new yt(u,u)),u=ao(r,i,!0,!0),S=-1;else{var P=zm(r,u,a.unit);a.extend?g=tc(g,P.anchor,P.head,a.extend):g=P}a.addNew?S==-1?(S=b.length,Ir(v,Xr(r,b.concat([g]),S),{scroll:!1,origin:"*mouse"})):b.length>1&&b[S].empty()&&a.unit=="char"&&!a.extend?(Ir(v,Xr(r,b.slice(0,S).concat(b.slice(S+1)),0),{scroll:!1,origin:"*mouse"}),C=v.sel):sd(v,S,g,ne):(S=0,Ir(v,new oi([g],0),ne),C=v.sel);var A=u;function j(ve){if($e(A,ve)!=0)if(A=ve,a.unit=="rectangle"){for(var Oe=[],Fe=r.options.tabSize,Re=Et(Me(v,u.line).text,u.ch,Fe),rt=Et(Me(v,ve.line).text,ve.ch,Fe),kt=Math.min(Re,rt),yr=Math.max(Re,rt),Mt=Math.min(u.line,ve.line),hn=Math.min(r.lastLine(),Math.max(u.line,ve.line));Mt<=hn;Mt++){var mn=Me(v,Mt).text,ar=br(mn,kt,Fe);kt==yr?Oe.push(new yt(ae(Mt,ar),ae(Mt,ar))):mn.length>ar&&Oe.push(new yt(ae(Mt,ar),ae(Mt,br(mn,yr,Fe))))}Oe.length||Oe.push(new yt(u,u)),Ir(v,Xr(r,C.ranges.slice(0,S).concat(Oe),S),{origin:"*mouse",scroll:!1}),r.scrollIntoView(ve)}else{var Br=g,kr=zm(r,ve,a.unit),Lt=Br.anchor,Ft;$e(kr.anchor,Lt)>0?(Ft=kr.head,Lt=Zs(Br.from(),kr.anchor)):(Ft=kr.anchor,Lt=as(Br.to(),kr.head));var rr=C.ranges.slice(0);rr[S]=Ca(r,new yt(Be(v,Lt),Ft)),Ir(v,Xr(r,rr,S),ne)}}o(j,"extendTo");var z=c.wrapper.getBoundingClientRect(),ee=0;function oe(ve){var Oe=++ee,Fe=ao(r,ve,!0,a.unit=="rectangle");if(!!Fe)if($e(Fe,A)!=0){r.curOp.focus=Ke(),j(Fe);var Re=dl(c,v);(Fe.line>=Re.to||Fe.linez.bottom?20:0;rt&&setTimeout(tr(r,function(){ee==Oe&&(c.scroller.scrollTop+=rt,oe(ve))}),50)}}o(oe,"extend");function ce(ve){r.state.selectingText=!1,ee=1/0,ve&&(Jt(ve),c.input.focus()),he(c.wrapper.ownerDocument,"mousemove",me),he(c.wrapper.ownerDocument,"mouseup",be),v.history.lastSelOrigin=null}o(ce,"done");var me=tr(r,function(ve){ve.buttons===0||!Gs(ve)?ce(ve):oe(ve)}),be=tr(r,ce);r.state.selectingText=be,H(c.wrapper.ownerDocument,"mousemove",me),H(c.wrapper.ownerDocument,"mouseup",be)}o(jm,"leftButtonSelect");function Ca(r,i){var u=i.anchor,a=i.head,c=Me(r.doc,u.line);if($e(u,a)==0&&u.sticky==a.sticky)return i;var v=En(c);if(!v)return i;var g=gi(v,u.ch,u.sticky),S=v[g];if(S.from!=u.ch&&S.to!=u.ch)return i;var C=g+(S.from==u.ch==(S.level!=1)?0:1);if(C==0||C==v.length)return i;var b;if(a.line!=u.line)b=(a.line-u.line)*(r.doc.direction=="ltr"?1:-1)>0;else{var P=gi(v,a.ch,a.sticky),A=P-g||(a.ch-u.ch)*(S.level==1?-1:1);P==C-1||P==C?b=A<0:b=A>0}var j=v[C+(b?-1:0)],z=b==(j.level==1),ee=z?j.from:j.to,oe=z?"after":"before";return u.ch==ee&&u.sticky==oe?i:new yt(new ae(u.line,ee,oe),a)}o(Ca,"bidiSimplify");function _a(r,i,u,a){var c,v;if(i.touches)c=i.touches[0].clientX,v=i.touches[0].clientY;else try{c=i.clientX,v=i.clientY}catch(j){return!1}if(c>=Math.floor(r.display.gutters.getBoundingClientRect().right))return!1;a&&Jt(i);var g=r.display,S=g.lineDiv.getBoundingClientRect();if(v>S.bottom||!Rt(r,u))return os(i);v-=S.top-g.viewOffset;for(var C=0;C=c){var P=ro(r.doc,v),A=r.display.gutterSpecs[C];return ke(r,u,r,P,A.className,i),os(i)}}}o(_a,"gutterEvent");function wd(r,i){return _a(r,i,"gutterClick",!0)}o(wd,"clickInGutter");function xd(r,i){Wi(r.display,i)||qm(r,i)||Zt(r,i,"contextmenu")||de||r.display.input.onContextMenu(i)}o(xd,"onContextMenu");function qm(r,i){return Rt(r,"gutterContextMenu")?_a(r,i,"gutterContextMenu",!1):!1}o(qm,"contextMenuInGutter");function qu(r){r.display.wrapper.className=r.display.wrapper.className.replace(/\s*cm-s-\S+/g,"")+r.options.theme.replace(/(^|\s)\s*/g," cm-s-"),Un(r)}o(qu,"themeChanged");var Sl={toString:function(){return"CodeMirror.Init"}},Vu={},ba={};function gc(r){var i=r.optionHandlers;function u(a,c,v,g){r.defaults[a]=c,v&&(i[a]=g?function(S,C,b){b!=Sl&&v(S,C,b)}:v)}o(u,"option"),r.defineOption=u,r.Init=Sl,u("value","",function(a,c){return a.setValue(c)},!0),u("mode",null,function(a,c){a.doc.modeOption=c,td(a)},!0),u("indentUnit",2,td,!0),u("indentWithTabs",!1),u("smartIndent",!0),u("tabSize",4,function(a){Fu(a),Un(a),Je(a)},!0),u("lineSeparator",null,function(a,c){if(a.doc.lineSep=c,!!c){var v=[],g=a.doc.first;a.doc.iter(function(C){for(var b=0;;){var P=C.text.indexOf(c,b);if(P==-1)break;b=P+c.length,v.push(ae(g,P))}g++});for(var S=v.length-1;S>=0;S--)ma(a.doc,c,v[S],ae(v[S].line,v[S].ch+c.length))}}),u("specialChars",/[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\ufeff\ufff9-\ufffc]/g,function(a,c,v){a.state.specialChars=new RegExp(c.source+(c.test(" ")?"":"| "),"g"),v!=Sl&&a.refresh()}),u("specialCharPlaceholder",On,function(a){return a.refresh()},!0),u("electricChars",!0),u("inputStyle",M?"contenteditable":"textarea",function(){throw new Error("inputStyle can not (yet) be changed in a running editor")},!0),u("spellcheck",!1,function(a,c){return a.getInputField().spellcheck=c},!0),u("autocorrect",!1,function(a,c){return a.getInputField().autocorrect=c},!0),u("autocapitalize",!1,function(a,c){return a.getInputField().autocapitalize=c},!0),u("rtlMoveVisually",!V),u("wholeLineUpdateBefore",!0),u("theme","default",function(a){qu(a),Ru(a)},!0),u("keyMap","default",function(a,c,v){var g=pn(c),S=v!=Sl&&pn(v);S&&S.detach&&S.detach(a,g),g.attach&&g.attach(a,S||null)}),u("extraKeys",null),u("configureMouse",null),u("lineWrapping",!1,Vm,!0),u("gutters",[],function(a,c){a.display.gutterSpecs=Jp(c,a.options.lineNumbers),Ru(a)},!0),u("fixedGutter",!0,function(a,c){a.display.gutters.style.left=c?ia(a.display)+"px":"0",a.refresh()},!0),u("coverGutterNextToScrollbar",!1,function(a){return _s(a)},!0),u("scrollbarStyle","native",function(a){ml(a),_s(a),a.display.scrollbars.setScrollTop(a.doc.scrollTop),a.display.scrollbars.setScrollLeft(a.doc.scrollLeft)},!0),u("lineNumbers",!1,function(a,c){a.display.gutterSpecs=Jp(a.options.gutters,c),Ru(a)},!0),u("firstLineNumber",1,Ru,!0),u("lineNumberFormatter",function(a){return a},Ru,!0),u("showCursorWhenSelecting",!1,Lu,!0),u("resetSelectionOnContextMenu",!0),u("lineWiseCopyCut",!0),u("pasteLinesPerSelection",!0),u("selectionsMayTouch",!1),u("readOnly",!1,function(a,c){c=="nocursor"&&(pl(a),a.display.input.blur()),a.display.input.readOnlyChanged(c)}),u("screenReaderLabel",null,function(a,c){c=c===""?null:c,a.display.input.screenReaderLabelChanged(c)}),u("disableInput",!1,function(a,c){c||a.display.input.reset()},!0),u("dragDrop",!0,x0),u("allowDropFileTypes",null),u("cursorBlinkRate",530),u("cursorScrollMargin",0),u("cursorHeight",1,Lu,!0),u("singleCursorHeightPerLine",!0,Lu,!0),u("workTime",100),u("workDelay",100),u("flattenSpans",!0,Fu,!0),u("addModeClass",!1,Fu,!0),u("pollInterval",100),u("undoDepth",200,function(a,c){return a.doc.history.undoDepth=c}),u("historyEventDelay",1250),u("viewportMargin",10,function(a){return a.refresh()},!0),u("maxHighlightLength",1e4,Fu,!0),u("moveInputWithCursor",!0,function(a,c){c||a.display.input.resetPosition()}),u("tabindex",null,function(a,c){return a.display.input.getField().tabIndex=c||""}),u("autofocus",null),u("direction","ltr",function(a,c){return a.doc.setDirection(c)},!0),u("phrases",null)}o(gc,"defineOptions");function x0(r,i,u){var a=u&&u!=Sl;if(!i!=!a){var c=r.display.dragFunctions,v=i?H:he;v(r.display.scroller,"dragstart",c.start),v(r.display.scroller,"dragenter",c.enter),v(r.display.scroller,"dragover",c.over),v(r.display.scroller,"dragleave",c.leave),v(r.display.scroller,"drop",c.drop)}}o(x0,"dragDropChanged");function Vm(r){r.options.lineWrapping?(Ye(r.display.wrapper,"CodeMirror-wrap"),r.display.sizer.style.minWidth="",r.display.sizerWidth=null):(xe(r.display.wrapper,"CodeMirror-wrap"),fs(r)),gs(r),Je(r),Un(r),setTimeout(function(){return _s(r)},100)}o(Vm,"wrappingChanged");function Pt(r,i){var u=this;if(!(this instanceof Pt))return new Pt(r,i);this.options=i=i?Kt(i):{},Kt(Vu,i,!1);var a=i.value;typeof a=="string"?a=new cn(a,i.mode,null,i.lineSeparator,i.direction):i.mode&&(a.modeOption=i.mode),this.doc=a;var c=new Pt.inputStyles[i.inputStyle](this),v=this.display=new a0(r,a,c,i);v.wrapper.CodeMirror=this,qu(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 St,keySeq:null,specialChars:null},i.autofocus&&!M&&v.input.focus(),p&&x<11&&setTimeout(function(){return u.display.input.reset(!0)},20),Km(this),cd(),Ui(this),this.curOp.forceUpdate=!0,xm(this,a),i.autofocus&&!M||this.hasFocus()?setTimeout(function(){u.hasFocus()&&!u.state.focused&&sa(u)},20):pl(this);for(var g in ba)ba.hasOwnProperty(g)&&ba[g](this,i[g],Sl);po(this),i.finishInit&&i.finishInit(this);for(var S=0;S20*20}o(g,"farAway"),H(i.scroller,"touchstart",function(C){if(!Zt(r,C)&&!v(C)&&!wd(r,C)){i.input.ensurePolled(),clearTimeout(u);var b=+new Date;i.activeTouch={start:b,moved:!1,prev:b-a.end<=300?a:null},C.touches.length==1&&(i.activeTouch.left=C.touches[0].pageX,i.activeTouch.top=C.touches[0].pageY)}}),H(i.scroller,"touchmove",function(){i.activeTouch&&(i.activeTouch.moved=!0)}),H(i.scroller,"touchend",function(C){var b=i.activeTouch;if(b&&!Wi(i,C)&&b.left!=null&&!b.moved&&new Date-b.start<300){var P=r.coordsChar(i.activeTouch,"page"),A;!b.prev||g(b,b.prev)?A=new yt(P,P):!b.prev.prev||g(b,b.prev.prev)?A=r.findWordAt(P):A=new yt(ae(P.line,0),Be(r.doc,ae(P.line+1,0))),r.setSelection(A.anchor,A.head),r.focus(),Jt(C)}c()}),H(i.scroller,"touchcancel",c),H(i.scroller,"scroll",function(){i.scroller.clientHeight&&(er(r,i.scroller.scrollTop),hl(r,i.scroller.scrollLeft,!0),ke(r,"scroll",r))}),H(i.scroller,"mousewheel",function(C){return ym(r,C)}),H(i.scroller,"DOMMouseScroll",function(C){return ym(r,C)}),H(i.wrapper,"scroll",function(){return i.wrapper.scrollTop=i.wrapper.scrollLeft=0}),i.dragFunctions={enter:function(C){Zt(r,C)||to(C)},over:function(C){Zt(r,C)||(ud(r,C),to(C))},start:function(C){return h0(r,C)},drop:tr(r,Am),leave:function(C){Zt(r,C)||fd(r)}};var S=i.input.getField();H(S,"keyup",function(C){return yd.call(r,C)}),H(S,"keydown",tr(r,Fm)),H(S,"keypress",tr(r,Hm)),H(S,"focus",function(C){return sa(r,C)}),H(S,"blur",function(C){return pl(r,C)})}o(Km,"registerEventHandlers");var Ku=[];Pt.defineInitHook=function(r){return Ku.push(r)};function Gu(r,i,u,a){var c=r.doc,v;u==null&&(u="add"),u=="smart"&&(c.mode.indent?v=Wo(r,i).state:u="prev");var g=r.options.tabSize,S=Me(c,i),C=Et(S.text,null,g);S.stateAfter&&(S.stateAfter=null);var b=S.text.match(/^\s*/)[0],P;if(!a&&!/\S/.test(S.text))P=0,u="not";else if(u=="smart"&&(P=c.mode.indent(v,S.text.slice(b.length),S.text),P==Ut||P>150)){if(!a)return;u="prev"}u=="prev"?i>c.first?P=Et(Me(c,i-1).text,null,g):P=0:u=="add"?P=C+r.options.indentUnit:u=="subtract"?P=C-r.options.indentUnit:typeof u=="number"&&(P=C+u),P=Math.max(0,P);var A="",j=0;if(r.options.indentWithTabs)for(var z=Math.floor(P/g);z;--z)j+=g,A+=" ";if(jg,C=Xs(i),b=null;if(S&&a.ranges.length>1)if(Ei&&Ei.text.join(` -`)==i){if(a.ranges.length%Ei.text.length==0){b=[];for(var P=0;P=0;j--){var z=a.ranges[j],ee=z.from(),oe=z.to();z.empty()&&(u&&u>0?ee=ae(ee.line,ee.ch-u):r.state.overwrite&&!S?oe=ae(oe.line,Math.min(Me(v,oe.line).text.length,oe.ch+Se(C).length)):S&&Ei&&Ei.lineWise&&Ei.text.join(` -`)==C.join(` -`)&&(ee=oe=ae(ee.line,0)));var ce={from:ee,to:oe,text:b?b[j%b.length]:C,origin:c||(S?"paste":r.state.cutIncoming>g?"cut":"+input")};pa(r.doc,ce),sr(r,"inputRead",r,ce)}i&&!S&&Gm(r,i),Ss(r),r.curOp.updateInput<2&&(r.curOp.updateInput=A),r.curOp.typing=!0,r.state.pasteIncoming=r.state.cutIncoming=-1}o(yc,"applyTextInput");function Sd(r,i){var u=r.clipboardData&&r.clipboardData.getData("Text");if(u)return r.preventDefault(),!i.isReadOnly()&&!i.options.disableInput&&Gr(i,function(){return yc(i,u,0,null,"paste")}),!0}o(Sd,"handlePaste");function Gm(r,i){if(!(!r.options.electricChars||!r.options.smartIndent))for(var u=r.doc.sel,a=u.ranges.length-1;a>=0;a--){var c=u.ranges[a];if(!(c.head.ch>100||a&&u.ranges[a-1].head.line==c.head.line)){var v=r.getModeAt(c.head),g=!1;if(v.electricChars){for(var S=0;S-1){g=Gu(r,c.head.line,"smart");break}}else v.electricInput&&v.electricInput.test(Me(r.doc,c.head.line).text.slice(0,c.head.ch))&&(g=Gu(r,c.head.line,"smart"));g&&sr(r,"electricInput",r,c.head.line)}}}o(Gm,"triggerElectric");function Cd(r){for(var i=[],u=[],a=0;av&&(Gu(this,S.head.line,a,!0),v=S.head.line,g==this.doc.sel.primIndex&&Ss(this));else{var C=S.from(),b=S.to(),P=Math.max(v,C.line);v=Math.min(this.lastLine(),b.line-(b.ch?0:1))+1;for(var A=P;A0&&sd(this.doc,g,new yt(C,j[g].to()),$t)}}}),getTokenAt:function(a,c){return us(this,a,c)},getLineTokens:function(a,c){return us(this,ae(a),c,!0)},getTokenTypeAt:function(a){a=Be(this.doc,a);var c=ql(this,Me(this.doc,a.line)),v=0,g=(c.length-1)/2,S=a.ch,C;if(S==0)C=c[2];else for(;;){var b=v+g>>1;if((b?c[b*2-1]:0)>=S)g=b;else if(c[b*2+1]C&&(a=C,g=!0),S=Me(this.doc,a)}else S=a;return ul(this,S,{top:0,left:0},c||"page",v||g).top+(g?this.doc.height-_e(S):0)},defaultTextHeight:function(){return vs(this.display)},defaultCharWidth:function(){return na(this.display)},getViewport:function(){return{from:this.display.viewFrom,to:this.display.viewTo}},addWidget:function(a,c,v,g,S){var C=this.display;a=fn(this,Be(this.doc,a));var b=a.bottom,P=a.left;if(c.style.position="absolute",c.setAttribute("cm-ignore-events","true"),this.display.input.setUneditable(c),C.sizer.appendChild(c),g=="over")b=a.top;else if(g=="above"||g=="near"){var A=Math.max(C.wrapper.clientHeight,this.doc.height),j=Math.max(C.sizer.clientWidth,C.lineSpace.clientWidth);(g=="above"||a.bottom+c.offsetHeight>A)&&a.top>c.offsetHeight?b=a.top-c.offsetHeight:a.bottom+c.offsetHeight<=A&&(b=a.bottom),P+c.offsetWidth>j&&(P=j-c.offsetWidth)}c.style.top=b+"px",c.style.left=c.style.right="",S=="right"?(P=C.sizer.clientWidth-c.offsetWidth,c.style.right="0px"):(S=="left"?P=0:S=="middle"&&(P=(C.sizer.clientWidth-c.offsetWidth)/2),c.style.left=P+"px"),v&&Jy(this,{left:P,top:b,right:P+c.offsetWidth,bottom:b+c.offsetHeight})},triggerOnKeyDown:Yr(Fm),triggerOnKeyPress:Yr(Hm),triggerOnKeyUp:yd,triggerOnMouseDown:Yr(Bm),execCommand:function(a){if(xl.hasOwnProperty(a))return xl[a].call(null,this)},triggerElectric:Yr(function(a){Gm(this,a)}),findPosH:function(a,c,v,g){var S=1;c<0&&(S=-1,c=-c);for(var C=Be(this.doc,a),b=0;b0&&P(v.charAt(g-1));)--g;for(;S.5||this.options.lineWrapping)&&gs(this),ke(this,"refresh",this)}),swapDoc:Yr(function(a){var c=this.doc;return c.cm=null,this.state.selectingText&&this.state.selectingText(),xm(this,a),Un(this),this.display.input.reset(),Pu(this,a.scrollLeft,a.scrollTop),this.curOp.forceScroll=!0,sr(this,"swapDoc",this,c),c}),phrase:function(a){var c=this.options.phrases;return c&&Object.prototype.hasOwnProperty.call(c,a)?c[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}},Dr(r),r.registerHelper=function(a,c,v){u.hasOwnProperty(a)||(u[a]=r[a]={_global:[]}),u[a][c]=v},r.registerGlobalHelper=function(a,c,v,g){r.registerHelper(a,c,g),u[a]._global.push({pred:v,val:g})}}o(Ko,"addEditorMethods");function Yu(r,i,u,a,c){var v=i,g=u,S=Me(r,i.line),C=c&&r.direction=="rtl"?-u:u;function b(){var be=i.line+C;return be=r.first+r.size?!1:(i=new ae(be,i.ch,i.sticky),S=Me(r,be))}o(b,"findNextLine");function P(be){var ve;if(a=="codepoint"){var Oe=S.text.charCodeAt(i.ch+(u>0?0:-1));if(isNaN(Oe))ve=null;else{var Fe=u>0?Oe>=55296&&Oe<56320:Oe>=56320&&Oe<57343;ve=new ae(i.line,Math.max(0,Math.min(S.text.length,i.ch+u*(Fe?2:1))),-u)}}else c?ve=Rm(r.cm,S,i,u):ve=pc(S,i,u);if(ve==null)if(!be&&b())i=wa(c,r.cm,S,i.line,C);else return!1;else i=ve;return!0}if(o(P,"moveOnce"),a=="char"||a=="codepoint")P();else if(a=="column")P(!0);else if(a=="word"||a=="group")for(var A=null,j=a=="group",z=r.cm&&r.cm.getHelper(i,"wordChars"),ee=!0;!(u<0&&!P(!ee));ee=!1){var oe=S.text.charAt(i.ch)||` -`,ce=dr(oe,z)?"w":j&&oe==` -`?"n":!j||/\s/.test(oe)?null:"p";if(j&&!ee&&!ce&&(ce="s"),A&&A!=ce){u<0&&(u=1,P(),i.sticky="after");break}if(ce&&(A=ce),u>0&&!P(!ee))break}var me=Hr(r,i,v,g,!0);return pu(v,me)&&(me.hitSide=!0),me}o(Yu,"findPosH");function wc(r,i,u,a){var c=r.doc,v=i.left,g;if(a=="page"){var S=Math.min(r.display.wrapper.clientHeight,window.innerHeight||document.documentElement.clientHeight),C=Math.max(S-.5*vs(r.display),3);g=(u>0?i.bottom:i.top)+u*C}else a=="line"&&(g=u>0?i.bottom+3:i.top-3);for(var b;b=q(r,v,g),!!b.outside;){if(u<0?g<=0:g>=c.height){b.hitSide=!0;break}g+=u*5}return b}o(wc,"findPosV");var wt=o(function(r){this.cm=r,this.lastAnchorNode=this.lastAnchorOffset=this.lastFocusNode=this.lastFocusOffset=null,this.polling=new St,this.composing=null,this.gracePeriod=!1,this.readDOMTimeout=null},"ContentEditableInput");wt.prototype.init=function(r){var i=this,u=this,a=u.cm,c=u.div=r.lineDiv;c.contentEditable=!0,Ym(c,a.options.spellcheck,a.options.autocorrect,a.options.autocapitalize);function v(S){for(var C=S.target;C;C=C.parentNode){if(C==c)return!0;if(/\bCodeMirror-(?:line)?widget\b/.test(C.className))break}return!1}o(v,"belongsToInput"),H(c,"paste",function(S){!v(S)||Zt(a,S)||Sd(S,a)||x<=11&&setTimeout(tr(a,function(){return i.updateFromDOM()}),20)}),H(c,"compositionstart",function(S){i.composing={data:S.data,done:!1}}),H(c,"compositionupdate",function(S){i.composing||(i.composing={data:S.data,done:!1})}),H(c,"compositionend",function(S){i.composing&&(S.data!=i.composing.data&&i.readFromDOMSoon(),i.composing.done=!0)}),H(c,"touchstart",function(){return u.forceCompositionEnd()}),H(c,"input",function(){i.composing||i.readFromDOMSoon()});function g(S){if(!(!v(S)||Zt(a,S))){if(a.somethingSelected())Ti({lineWise:!1,text:a.getSelections()}),S.type=="cut"&&a.replaceSelection("",null,"cut");else if(a.options.lineWiseCopyCut){var C=Cd(a);Ti({lineWise:!0,text:C.text}),S.type=="cut"&&a.operation(function(){a.setSelections(C.ranges,0,$t),a.replaceSelection("",null,"cut")})}else return;if(S.clipboardData){S.clipboardData.clearData();var b=Ei.text.join(` -`);if(S.clipboardData.setData("Text",b),S.clipboardData.getData("Text")==b){S.preventDefault();return}}var P=Xm(),A=P.firstChild;a.display.lineSpace.insertBefore(P,a.display.lineSpace.firstChild),A.value=Ei.text.join(` -`);var j=Ke();ft(A),setTimeout(function(){a.display.lineSpace.removeChild(P),j.focus(),j==c&&u.showPrimarySelection()},50)}}o(g,"onCopyCut"),H(c,"copy",g),H(c,"cut",g)},wt.prototype.screenReaderLabelChanged=function(r){r?this.div.setAttribute("aria-label",r):this.div.removeAttribute("aria-label")},wt.prototype.prepareSelection=function(){var r=Nu(this.cm,!1);return r.focus=Ke()==this.div,r},wt.prototype.showSelection=function(r,i){!r||!this.cm.display.view.length||((r.focus||i)&&this.showPrimarySelection(),this.showMultipleSelections(r))},wt.prototype.getSelection=function(){return this.cm.display.wrapper.ownerDocument.getSelection()},wt.prototype.showPrimarySelection=function(){var r=this.getSelection(),i=this.cm,u=i.doc.sel.primary(),a=u.from(),c=u.to();if(i.display.viewTo==i.display.viewFrom||a.line>=i.display.viewTo||c.line=i.display.viewFrom&&Xu(i,a)||{node:S[0].measure.map[2],offset:0},b=c.liner.firstLine()&&(a=ae(a.line-1,Me(r.doc,a.line-1).length)),c.ch==Me(r.doc,c.line).text.length&&c.linei.viewTo-1)return!1;var v,g,S;a.line==i.viewFrom||(v=uo(r,a.line))==0?(g=vt(i.view[0].line),S=i.view[0].node):(g=vt(i.view[v].line),S=i.view[v-1].node.nextSibling);var C=uo(r,c.line),b,P;if(C==i.view.length-1?(b=i.viewTo-1,P=i.lineDiv.lastChild):(b=vt(i.view[C+1].line)-1,P=i.view[C+1].node.previousSibling),!S)return!1;for(var A=r.doc.splitLines(xc(r,S,P,g,b)),j=wi(r.doc,ae(g,0),ae(b,Me(r.doc,b).text.length));A.length>1&&j.length>1;)if(Se(A)==Se(j))A.pop(),j.pop(),b--;else if(A[0]==j[0])A.shift(),j.shift(),g++;else break;for(var z=0,ee=0,oe=A[0],ce=j[0],me=Math.min(oe.length,ce.length);za.ch&&be.charCodeAt(be.length-ee-1)==ve.charCodeAt(ve.length-ee-1);)z--,ee++;A[A.length-1]=be.slice(0,be.length-ee).replace(/^\u200b+/,""),A[0]=A[0].slice(z).replace(/\u200b+$/,"");var Fe=ae(g,z),Re=ae(b,j.length?Se(j).length-ee:0);if(A.length>1||A[0]||$e(Fe,Re))return ma(r.doc,A,Fe,Re,"+input"),!0},wt.prototype.ensurePolled=function(){this.forceCompositionEnd()},wt.prototype.reset=function(){this.forceCompositionEnd()},wt.prototype.forceCompositionEnd=function(){!this.composing||(clearTimeout(this.readDOMTimeout),this.composing=null,this.updateFromDOM(),this.div.blur(),this.div.focus())},wt.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))},wt.prototype.updateFromDOM=function(){var r=this;(this.cm.isReadOnly()||!this.pollContent())&&Gr(this.cm,function(){return Je(r.cm)})},wt.prototype.setUneditable=function(r){r.contentEditable="false"},wt.prototype.onKeyPress=function(r){r.charCode==0||this.composing||(r.preventDefault(),this.cm.isReadOnly()||tr(this.cm,yc)(this.cm,String.fromCharCode(r.charCode==null?r.keyCode:r.charCode),0))},wt.prototype.readOnlyChanged=function(r){this.div.contentEditable=String(r!="nocursor")},wt.prototype.onContextMenu=function(){},wt.prototype.resetPosition=function(){},wt.prototype.needsContentAttribute=!0;function Xu(r,i){var u=Ou(r,i.line);if(!u||u.hidden)return null;var a=Me(r.doc,i.line),c=ku(u,a,i.line),v=En(a,r.doc.direction),g="left";if(v){var S=gi(v,i.ch);g=S%2?"right":"left"}var C=T(c.map,i.ch,g);return C.offset=C.collapse=="right"?C.end:C.start,C}o(Xu,"posToDOM");function Ea(r){for(var i=r;i;i=i.parentNode)if(/CodeMirror-gutter-wrapper/.test(i.className))return!0;return!1}o(Ea,"isInGutter");function We(r,i){return i&&(r.bad=!0),r}o(We,"badPos");function xc(r,i,u,a,c){var v="",g=!1,S=r.doc.lineSeparator(),C=!1;function b(z){return function(ee){return ee.id==z}}o(b,"recognizeMarker");function P(){g&&(v+=S,C&&(v+=S),g=C=!1)}o(P,"close");function A(z){z&&(P(),v+=z)}o(A,"addText");function j(z){if(z.nodeType==1){var ee=z.getAttribute("cm-text");if(ee){A(ee);return}var oe=z.getAttribute("cm-marker"),ce;if(oe){var me=r.findMarks(ae(a,0),ae(c+1,0),b(+oe));me.length&&(ce=me[0].find(0))&&A(wi(r.doc,ce.from,ce.to).join(S));return}if(z.getAttribute("contenteditable")=="false")return;var be=/^(pre|div|p|li|table|br)$/i.test(z.nodeName);if(!/^br$/i.test(z.nodeName)&&z.textContent.length==0)return;be&&P();for(var ve=0;ve=9&&i.hasSelection&&(i.hasSelection=null),u.poll()}),H(c,"paste",function(g){Zt(a,g)||Sd(g,a)||(a.state.pasteIncoming=+new Date,u.fastPoll())});function v(g){if(!Zt(a,g)){if(a.somethingSelected())Ti({lineWise:!1,text:a.getSelections()});else if(a.options.lineWiseCopyCut){var S=Cd(a);Ti({lineWise:!0,text:S.text}),g.type=="cut"?a.setSelections(S.ranges,null,$t):(u.prevInput="",c.value=S.text.join(` -`),ft(c))}else return;g.type=="cut"&&(a.state.cutIncoming=+new Date)}}o(v,"prepareCopyCut"),H(c,"cut",v),H(c,"copy",v),H(r.scroller,"paste",function(g){if(!(Wi(r,g)||Zt(a,g))){if(!c.dispatchEvent){a.state.pasteIncoming=+new Date,u.focus();return}var S=new Event("paste");S.clipboardData=g.clipboardData,c.dispatchEvent(S)}}),H(r.lineSpace,"selectstart",function(g){Wi(r,g)||Jt(g)}),H(c,"compositionstart",function(){var g=a.getCursor("from");u.composing&&u.composing.range.clear(),u.composing={start:g,range:a.markText(g,a.getCursor("to"),{className:"CodeMirror-composing"})}}),H(c,"compositionend",function(){u.composing&&(u.poll(),u.composing.range.clear(),u.composing=null)})},lr.prototype.createField=function(r){this.wrapper=Xm(),this.textarea=this.wrapper.firstChild},lr.prototype.screenReaderLabelChanged=function(r){r?this.textarea.setAttribute("aria-label",r):this.textarea.removeAttribute("aria-label")},lr.prototype.prepareSelection=function(){var r=this.cm,i=r.display,u=r.doc,a=Nu(r);if(r.options.moveInputWithCursor){var c=fn(r,u.sel.primary().head,"div"),v=i.wrapper.getBoundingClientRect(),g=i.lineDiv.getBoundingClientRect();a.teTop=Math.max(0,Math.min(i.wrapper.clientHeight-10,c.top+g.top-v.top)),a.teLeft=Math.max(0,Math.min(i.wrapper.clientWidth-10,c.left+g.left-v.left))}return a},lr.prototype.showSelection=function(r){var i=this.cm,u=i.display;et(u.cursorDiv,r.cursors),et(u.selectionDiv,r.selection),r.teTop!=null&&(this.wrapper.style.top=r.teTop+"px",this.wrapper.style.left=r.teLeft+"px")},lr.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&&ft(this.textarea),p&&x>=9&&(this.hasSelection=u)}else r||(this.prevInput=this.textarea.value="",p&&x>=9&&(this.hasSelection=null))}},lr.prototype.getField=function(){return this.textarea},lr.prototype.supportsTouch=function(){return!1},lr.prototype.focus=function(){if(this.cm.options.readOnly!="nocursor"&&(!M||Ke()!=this.textarea))try{this.textarea.focus()}catch(r){}},lr.prototype.blur=function(){this.textarea.blur()},lr.prototype.resetPosition=function(){this.wrapper.style.top=this.wrapper.style.left=0},lr.prototype.receivedFocus=function(){this.slowPoll()},lr.prototype.slowPoll=function(){var r=this;this.pollingFast||this.polling.set(this.cm.options.pollInterval,function(){r.poll(),r.cm.state.focused&&r.slowPoll()})},lr.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)},lr.prototype.poll=function(){var r=this,i=this.cm,u=this.textarea,a=this.prevInput;if(this.contextMenuPending||!i.state.focused||Dp(u)&&!a&&!this.composing||i.isReadOnly()||i.options.disableInput||i.state.keySeq)return!1;var c=u.value;if(c==a&&!i.somethingSelected())return!1;if(p&&x>=9&&this.hasSelection===c||R&&/[\uf700-\uf7ff]/.test(c))return i.display.input.reset(),!1;if(i.doc.sel==i.display.selForContextMenu){var v=c.charCodeAt(0);if(v==8203&&!a&&(a="\u200B"),v==8666)return this.reset(),this.cm.execCommand("undo")}for(var g=0,S=Math.min(a.length,c.length);g1e3||c.indexOf(` -`)>-1?u.value=r.prevInput="":r.prevInput=c,r.composing&&(r.composing.range.clear(),r.composing.range=i.markText(r.composing.start,i.getCursor("to"),{className:"CodeMirror-composing"}))}),!0},lr.prototype.ensurePolled=function(){this.pollingFast&&this.poll()&&(this.pollingFast=!1)},lr.prototype.onKeyPress=function(){p&&x>=9&&(this.hasSelection=null),this.fastPoll()},lr.prototype.onContextMenu=function(r){var i=this,u=i.cm,a=u.display,c=i.textarea;i.contextMenuPending&&i.contextMenuPending();var v=ao(u,r),g=a.scroller.scrollTop;if(!v||Y)return;var S=u.options.resetSelectionOnContextMenu;S&&u.doc.sel.contains(v)==-1&&tr(u,Ir)(u.doc,Es(v),$t);var C=c.style.cssText,b=i.wrapper.style.cssText,P=i.wrapper.offsetParent.getBoundingClientRect();i.wrapper.style.cssText="position: static",c.style.cssText=`position: absolute; width: 30px; height: 30px; - top: `+(r.clientY-P.top-5)+"px; left: "+(r.clientX-P.left-5)+`px; - z-index: 1000; background: `+(p?"rgba(255, 255, 255, .05)":"transparent")+`; - outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);`;var A;_&&(A=window.scrollY),a.input.focus(),_&&window.scrollTo(null,A),a.input.reset(),u.somethingSelected()||(c.value=i.prevInput=" "),i.contextMenuPending=z,a.selForContextMenu=u.doc.sel,clearTimeout(a.detectingSelectAll);function j(){if(c.selectionStart!=null){var oe=u.somethingSelected(),ce="\u200B"+(oe?c.value:"");c.value="\u21DA",c.value=ce,i.prevInput=oe?"":"\u200B",c.selectionStart=1,c.selectionEnd=ce.length,a.selForContextMenu=u.doc.sel}}o(j,"prepareSelectAllHack");function z(){if(i.contextMenuPending==z&&(i.contextMenuPending=!1,i.wrapper.style.cssText=b,c.style.cssText=C,p&&x<9&&a.scrollbars.setScrollTop(a.scroller.scrollTop=g),c.selectionStart!=null)){(!p||p&&x<9)&&j();var oe=0,ce=o(function(){a.selForContextMenu==u.doc.sel&&c.selectionStart==0&&c.selectionEnd>0&&i.prevInput=="\u200B"?tr(u,bm)(u):oe++<10?a.detectingSelectAll=setTimeout(ce,500):(a.selForContextMenu=null,a.input.reset())},"poll");a.detectingSelectAll=setTimeout(ce,200)}}if(o(z,"rehide"),p&&x>=9&&j(),de){to(r);var ee=o(function(){he(window,"mouseup",ee),setTimeout(z,20)},"mouseup");H(window,"mouseup",ee)}else setTimeout(z,50)},lr.prototype.readOnlyChanged=function(r){r||this.reset(),this.textarea.disabled=r=="nocursor",this.textarea.readOnly=!!r},lr.prototype.setUneditable=function(){},lr.prototype.needsContentAttribute=!1;function _d(r,i){if(i=i?Kt(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=Ke();i.autofocus=u==r||r.getAttribute("autofocus")!=null&&u==document.body}function a(){r.value=S.getValue()}o(a,"save");var c;if(r.form&&(H(r.form,"submit",a),!i.leaveSubmitMethodAlone)){var v=r.form;c=v.submit;try{var g=v.submit=function(){a(),v.submit=c,v.submit(),v.submit=g}}catch(C){}}i.finishInit=function(C){C.save=a,C.getTextArea=function(){return r},C.toTextArea=function(){C.toTextArea=isNaN,a(),r.parentNode.removeChild(C.getWrapperElement()),r.style.display="",r.form&&(he(r.form,"submit",a),!i.leaveSubmitMethodAlone&&typeof r.form.submit=="function"&&(r.form.submit=c))}},r.style.display="none";var S=Pt(function(C){return r.parentNode.insertBefore(C,r.nextSibling)},i);return S}o(_d,"fromTextArea");function Qm(r){r.off=he,r.on=H,r.wheelEventPixels=u0,r.Doc=cn,r.splitLines=Xs,r.countColumn=Et,r.findColumn=br,r.isWordChar=Gt,r.Pass=Ut,r.signal=ke,r.Line=He,r.changeEnd=Ts,r.scrollbarModel=Xf,r.Pos=ae,r.cmpPos=$e,r.modes=Ul,r.mimeModes=ls,r.resolveMode=Qs,r.getMode=fu,r.modeExtensions=Ro,r.extendMode=Ip,r.copyState=Fo,r.startState=Df,r.innerMode=$l,r.commands=xl,r.keyMap=qo,r.keyName=dd,r.isModifierKey=cc,r.lookupKey=Vo,r.normalizeKeyMap=v0,r.StringStream=zt,r.SharedTextMarker=ya,r.TextMarker=Ls,r.LineWidget=Uu,r.e_preventDefault=Jt,r.e_stopPropagation=ti,r.e_stop=to,r.addClass=Ye,r.contains=Ve,r.rmClass=xe,r.keyNames=Ps}o(Qm,"addLegacyProps"),gc(Pt),Ko(Pt);var dn="iter insert remove copy getEditor constructor".split(" ");for(var Sc in cn.prototype)cn.prototype.hasOwnProperty(Sc)&&at(dn,Sc)<0&&(Pt.prototype[Sc]=function(r){return function(){return r.apply(this.doc,arguments)}}(cn.prototype[Sc]));return Dr(cn),Pt.inputStyles={textarea:lr,contenteditable:wt},Pt.defineMode=function(r){!Pt.defaults.mode&&r!="null"&&(Pt.defaults.mode=r),Fp.apply(this,arguments)},Pt.defineMIME=Af,Pt.defineMode("null",function(){return{token:function(r){return r.skipToEnd()}}}),Pt.defineMIME("text/plain","null"),Pt.defineExtension=function(r,i){Pt.prototype[r]=i},Pt.defineDocExtension=function(r,i){cn.prototype[r]=i},Pt.fromTextArea=_d,Qm(Pt),Pt.version="5.62.3",Pt})});var eO=ur((X4,Jk)=>{var cI=typeof Element!="undefined",pI=typeof Map=="function",dI=typeof Set=="function",hI=typeof ArrayBuffer=="function"&&!!ArrayBuffer.isView;function Ay(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(!Ay(e[l],t[l]))return!1;return!0}var m;if(pI&&e instanceof Map&&t instanceof Map){if(e.size!==t.size)return!1;for(m=e.entries();!(l=m.next()).done;)if(!t.has(l.value[0]))return!1;for(m=e.entries();!(l=m.next()).done;)if(!Ay(l.value[1],t.get(l.value[0])))return!1;return!0}if(dI&&e instanceof Set&&t instanceof Set){if(e.size!==t.size)return!1;for(m=e.entries();!(l=m.next()).done;)if(!t.has(l.value[0]))return!1;return!0}if(hI&&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(cI&&e instanceof Element)return!1;for(l=n;l--!=0;)if(!((d[l]==="_owner"||d[l]==="__v"||d[l]==="__o")&&e.$$typeof)&&!Ay(e[d[l]],t[d[l]]))return!1;return!0}return e!==e&&t!==t}o(Ay,"equal");Jk.exports=o(function(t,n){try{return Ay(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 dO=ur((d$,pO)=>{"use strict";var TI="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED";pO.exports=TI});var gO=ur((h$,vO)=>{"use strict";var kI=dO();function hO(){}o(hO,"emptyFunction");function mO(){}o(mO,"emptyFunctionWithReset");mO.resetWarningCache=hO;vO.exports=function(){function e(l,d,m,p,x,_){if(_!==kI){var O=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 O.name="Invariant Violation",O}}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:mO,resetWarningCache:hO};return n.PropTypes=n,n}});var Jh=ur((g$,yO)=>{yO.exports=gO()();var m$,v$});var _S=ur((y$,wO)=>{wO.exports=o(function(t,n,l,d){var m=l?l.call(d,t,n):void 0;if(m!==void 0)return!!m;if(t===n)return!0;if(typeof t!="object"||!t||typeof n!="object"||!n)return!1;var p=Object.keys(t),x=Object.keys(n);if(p.length!==x.length)return!1;for(var _=Object.prototype.hasOwnProperty.bind(n),O=0;O=0)&&(n[d]=e[d]);return n}o(Ja,"_objectWithoutPropertiesLoose");var fx=pe(pT()),or=pe(De()),gT=pe(vT());var NR=[],PR=[null,null];function MR(e,t){var n=e[1];return[t.payload,n+1]}o(MR,"storeStateUpdatesReducer");function yT(e,t,n){hf(function(){return e.apply(void 0,t)},n)}o(yT,"useIsomorphicLayoutEffectWithArgs");function AR(e,t,n,l,d,m,p){e.current=l,t.current=d,n.current=!1,m.current&&(m.current=null,p())}o(AR,"captureWrapperProps");function DR(e,t,n,l,d,m,p,x,_,O){if(!!e){var D=!1,Y=null,U=o(function(){if(!D){var Q=t.getState(),F,M;try{F=l(Q,d.current)}catch(R){M=R,Y=R}M||(Y=null),F===m.current?p.current||_():(m.current=F,x.current=F,p.current=!0,O({type:"STORE_UPDATED",payload:{error:M}}))}},"checkForUpdates");n.onStateChange=U,n.trySubscribe(),U();var X=o(function(){if(D=!0,n.tryUnsubscribe(),n.onStateChange=null,Y)throw Y},"unsubscribeWrapper");return X}}o(DR,"subscribeUpdates");var RR=o(function(){return[null,0]},"initStateUpdates");function $g(e,t){t===void 0&&(t={});var n=t,l=n.getDisplayName,d=l===void 0?function(ie){return"ConnectAdvanced("+ie+")"}:l,m=n.methodName,p=m===void 0?"connectAdvanced":m,x=n.renderCountProp,_=x===void 0?void 0:x,O=n.shouldHandleStateChanges,D=O===void 0?!0:O,Y=n.storeKey,U=Y===void 0?"store":Y,X=n.withRef,te=X===void 0?!1:X,Q=n.forwardRef,F=Q===void 0?!1:Q,M=n.context,R=M===void 0?Yn:M,K=Ja(n,["getDisplayName","methodName","renderCountProp","shouldHandleStateChanges","storeKey","withRef","forwardRef","context"]);if(!1)var V;var ue=R;return o(function(de){var ge=de.displayName||de.name||"Component",xe=d(ge),qe=To({},K,{getDisplayName:d,methodName:p,renderCountProp:_,shouldHandleStateChanges:D,storeKey:U,displayName:xe,wrappedComponentName:ge,WrappedComponent:de}),et=K.pure;function Te(Ye){return e(Ye.dispatch,qe)}o(Te,"createChildSelector");var xt=et?or.useMemo:function(Ye){return Ye()};function Ue(Ye){var Qt=(0,or.useMemo)(function(){var vr=Ye.reactReduxForwardedRef,Fi=Ja(Ye,["reactReduxForwardedRef"]);return[Ye.context,vr,Fi]},[Ye]),ft=Qt[0],Ar=Qt[1],Kt=Qt[2],Et=(0,or.useMemo)(function(){return ft&&ft.Consumer&&(0,gT.isContextConsumer)(or.default.createElement(ft.Consumer,null))?ft:ue},[ft,ue]),St=(0,or.useContext)(Et),at=Boolean(Ye.store)&&Boolean(Ye.store.getState)&&Boolean(Ye.store.dispatch),_r=Boolean(St)&&Boolean(St.store),Ut=at?Ye.store:St.store,$t=(0,or.useMemo)(function(){return Te(Ut)},[Ut]),ne=(0,or.useMemo)(function(){if(!D)return PR;var vr=new sp(Ut,at?null:St.subscription),Fi=vr.notifyNestedSubs.bind(vr);return[vr,Fi]},[Ut,at,St]),tt=ne[0],br=ne[1],jt=(0,or.useMemo)(function(){return at?St:To({},St,{subscription:tt})},[at,St,tt]),qt=(0,or.useReducer)(MR,NR,RR),Se=qt[0],Er=Se[0],nn=qt[1];if(Er&&Er.error)throw Er.error;var Fn=(0,or.useRef)(),ei=(0,or.useRef)(Kt),on=(0,or.useRef)(),Gt=(0,or.useRef)(!1),dr=xt(function(){return on.current&&Kt===ei.current?on.current:$t(Ut.getState(),Kt)},[Ut,Er,Kt]);yT(AR,[ei,Fn,Gt,Kt,dr,on,br]),yT(DR,[D,Ut,tt,$t,ei,Fn,Gt,on,br,nn],[Ut,tt,$t]);var ct=(0,or.useMemo)(function(){return or.default.createElement(de,To({},dr,{ref:Ar}))},[Ar,de,dr]),Do=(0,or.useMemo)(function(){return D?or.default.createElement(Et.Provider,{value:jt},ct):ct},[Et,ct,jt]);return Do}o(Ue,"ConnectFunction");var Ve=et?or.default.memo(Ue):Ue;if(Ve.WrappedComponent=de,Ve.displayName=Ue.displayName=xe,F){var Ke=or.default.forwardRef(o(function(Qt,ft){return or.default.createElement(Ve,To({},Qt,{reactReduxForwardedRef:ft}))},"forwardConnectRef"));return Ke.displayName=xe,Ke.WrappedComponent=de,(0,fx.default)(Ke,de)}return(0,fx.default)(Ve,de)},"wrapWithConnect")}o($g,"connectAdvanced");function wT(e,t){return e===t?e!==0||t!==0||1/e==1/t:e!==e&&t!==t}o(wT,"is");function lp(e,t){if(wT(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(m,p){throw new Error("Invalid value of type "+typeof e+" for "+n+" argument when connecting component "+p.wrappedComponentName+".")}}o(dx,"match");function GR(e,t){return e===t}o(GR,"strictEqual");function YR(e){var t=e===void 0?{}:e,n=t.connectHOC,l=n===void 0?$g:n,d=t.mapStateToPropsFactories,m=d===void 0?CT:d,p=t.mapDispatchToPropsFactories,x=p===void 0?ST:p,_=t.mergePropsFactories,O=_===void 0?_T:_,D=t.selectorFactory,Y=D===void 0?px:D;return o(function(X,te,Q,F){F===void 0&&(F={});var M=F,R=M.pure,K=R===void 0?!0:R,V=M.areStatesEqual,ue=V===void 0?GR:V,ie=M.areOwnPropsEqual,de=ie===void 0?lp:ie,ge=M.areStatePropsEqual,xe=ge===void 0?lp:ge,qe=M.areMergedPropsEqual,et=qe===void 0?lp:qe,Te=Ja(M,["pure","areStatesEqual","areOwnPropsEqual","areStatePropsEqual","areMergedPropsEqual"]),xt=dx(X,m,"mapStateToProps"),Ue=dx(te,x,"mapDispatchToProps"),Ve=dx(Q,O,"mergeProps");return l(Y,To({methodName:"connect",getDisplayName:o(function(Ye){return"Connect("+Ye+")"},"getDisplayName"),shouldHandleStateChanges:Boolean(X),initMapStateToProps:xt,initMapDispatchToProps:Ue,initMergeProps:Ve,pure:K,areStatesEqual:ue,areOwnPropsEqual:de,areStatePropsEqual:xe,areMergedPropsEqual:et},Te))},"connect")}o(YR,"createConnect");var Di=YR();var ET=pe(De());var bT=pe(De());function jg(){var e=(0,bT.useContext)(Yn);return e}o(jg,"useReduxContext");function qg(e){e===void 0&&(e=Yn);var t=e===Yn?jg:function(){return(0,ET.useContext)(e)};return o(function(){var l=t(),d=l.store;return d},"useStore")}o(qg,"createStoreHook");var hx=qg();function TT(e){e===void 0&&(e=Yn);var t=e===Yn?hx:qg(e);return o(function(){var l=t();return l.dispatch},"useDispatch")}o(TT,"createDispatchHook");var $s=TT();var Xi=pe(De());var XR=o(function(t,n){return t===n},"refEquality");function QR(e,t,n,l){var d=(0,Xi.useReducer)(function(te){return te+1},0),m=d[1],p=(0,Xi.useMemo)(function(){return new sp(n,l)},[n,l]),x=(0,Xi.useRef)(),_=(0,Xi.useRef)(),O=(0,Xi.useRef)(),D=(0,Xi.useRef)(),Y=n.getState(),U;try{if(e!==_.current||Y!==O.current||x.current){var X=e(Y);D.current===void 0||!t(X,D.current)?U=X:U=D.current}else U=D.current}catch(te){throw x.current&&(te.message+=` +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]={};Zt(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 Bm(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 Ry(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(Ry,"adjustView");function Hm(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 Iy(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(Iy,"maybeScrollWindow");function Fy(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(Fy,"scrollPosIntoView");function By(r,i){var u=da(r,i);u.scrollTop!=null&&sr(r,u.scrollTop),u.scrollLeft!=null&&hl(r,u.scrollLeft)}o(By,"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 Um(r,i){mc(r),r.curOp.scrollToPos=i}o(Um,"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);zm(r,u,a,i.margin)}}o(mc,"resolveScrollToPos");function zm(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(zm,"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,$m(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(Hy,"endOperation_R1");function Wy(r){r.updatedDisplay=r.mustUpdate&&yd(r.cm,r.update)}o(Wy,"endOperation_W1");function Uy(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(Uy,"endOperation_R2");function zy(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&&Hm(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;Ry(r,g,y),u.viewOffset=hn(Me(r.doc,u.viewFrom)),r.display.mover.style.top=u.viewOffset+"px";var b=Hm(r);if(!S&&b==0&&!i.force&&u.renderedView==u.view&&(u.updateLineNumbers==null||u.updateLineNumbers>=u.viewTo))return!1;var E=jy(r);return b>4&&(u.lineDiv.style.display="none"),Vy(r,u.updateLineNumbers,i.dims),b>4&&(u.lineDiv.style.display=""),u.renderedView=u.view,qy(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 Vy(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(Vy,"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 $m(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(Xy,"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=Xy(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 Ym(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(Ym,"attachLocalSpans");function Xm(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&&(tg(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?Qy(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 Qy(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,Bm(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(Qy,"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 ug(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(ug,"moveVisually");var xl={selectAll:Zm,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 iw(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(iw,"clickRepeat");function hg(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?iw(a,p):"single";window.focus(),p==1&&i.state.selectingText&&i.state.selectingText(r),!(a&&Ic(i,p,a,g,r))&&(p==1?a?mg(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(hg,"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 mg(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)?gg(r,a,i,p):yg(r,a,i,p)}o(mg,"leftButtonDown");function gg(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(gg,"leftButtonStartDrag");function vg(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(vg,"rangeForUnit");function yg(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=vg(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=vg(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(yg,"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)||wg(r,i)||ir(r,i,"contextmenu")||pe||r.display.input.onContextMenu(i)}o($d,"onContextMenu");function wg(r,i){return Ft(r,"gutterContextMenu")?La(r,i,"gutterContextMenu",!1):!1}o(wg,"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,xg,!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,ow),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 ow(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(ow,"dragDropChanged");function xg(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(xg,"wrappingChanged");function Mt(r,i){var u=this;if(!(this instanceof Mt))return new Mt(r,i);this.options=i=i?Zt(i):{},Zt(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 Ky(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),Sg(this),Dd(),Ki(this),this.curOp.forceUpdate=!0,Gm(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 Vm(r,b)}),H(i.scroller,"DOMMouseScroll",function(b){return Vm(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 Jy(r,b)},drop:lr(r,lg),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,fg)),H(S,"keypress",lr(r,pg)),H(S,"focus",function(b){return pa(r,b)}),H(S,"blur",function(b){return pl(r,b)})}o(Sg,"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&&Cg(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 Cg(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(Cg,"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&&By(this,{left:M,top:E,right:M+p.offsetWidth,bottom:E+p.offsetHeight})},triggerOnKeyDown:en(fg),triggerOnKeyPress:en(pg),triggerOnKeyUp:Ud,triggerOnMouseDown:en(hg),execCommand:function(a){if(xl.hasOwnProperty(a))return xl[a].call(null,this)},triggerElectric:en(function(a){Cg(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(),Gm(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=ug(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,bg(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=Eg(),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=Eg(),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,Zm)(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?Zt(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 _g(r){r.off=he,r.on=H,r.wheelEventPixels=Gy,r.Doc=yn,r.splitLines=rl,r.countColumn=_t,r.findColumn=Pr,r.isWordChar=Jt,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=tw,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(_g,"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,_g(Mt),Mt.version="5.62.3",Mt})});var pL=Ue((sz,cL)=>{var k3=typeof Element!="undefined",N3=typeof Map=="function",L3=typeof Set=="function",P3=typeof ArrayBuffer=="function"&&!!ArrayBuffer.isView;function sy(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(!sy(e[l],t[l]))return!1;return!0}var h;if(N3&&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(!sy(l.value[1],t.get(l.value[0])))return!1;return!0}if(L3&&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(P3&&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(k3&&e instanceof Element)return!1;for(l=n;l--!=0;)if(!((d[l]==="_owner"||d[l]==="__v"||d[l]==="__o")&&e.$$typeof)&&!sy(e[d[l]],t[d[l]]))return!1;return!0}return e!==e&&t!==t}o(sy,"equal");cL.exports=o(function(t,n){try{return sy(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 _L=Ue((T9,EL)=>{"use strict";var z3="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED";EL.exports=z3});var LL=Ue((k9,NL)=>{"use strict";var $3=_L();function TL(){}o(TL,"emptyFunction");function kL(){}o(kL,"emptyFunctionWithReset");kL.resetWarningCache=TL;NL.exports=function(){function e(l,d,h,c,v,C){if(C!==$3){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:kL,resetWarningCache:TL};return n.PropTypes=n,n}});var Cm=Ue((P9,PL)=>{PL.exports=LL()();var N9,L9});var cC=Ue((O9,OL)=>{OL.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{WL.exports=function(){return typeof Promise=="function"&&Promise.prototype&&Promise.prototype.then}});var du=Ue(qf=>{var dC,q3=[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 q3[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.');dC=t},"setToSJISFunction");qf.isKanjiModeEnabled=function(){return typeof dC!="undefined"};qf.toSJIS=o(function(t){return dC(t)},"toSJIS")});var gy=Ue(Do=>{Do.L={bit:1};Do.M={bit:0};Do.Q={bit:3};Do.H={bit:2};function V3(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(V3,"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 V3(t)}catch(l){return n}},"from")});var jL=Ue((Q9,$L)=>{function zL(){this.buffer=[],this.length=0}o(zL,"BitBuffer");zL.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++}};$L.exports=zL});var VL=Ue((Z9,qL)=>{function Tm(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(Tm,"BitMatrix");Tm.prototype.set=function(e,t,n,l){let d=e*this.size+t;this.data[d]=n,l&&(this.reservedBit[d]=!0)};Tm.prototype.get=function(e,t){return this.data[e*this.size+t]};Tm.prototype.xor=function(e,t,n){this.data[e*this.size+t]^=n};Tm.prototype.isReserved=function(e,t){return this.reservedBit[e*this.size+t]};qL.exports=Tm});var KL=Ue(vy=>{var K3=du().getSymbolSize;vy.getRowColCoords=o(function(t){if(t===1)return[];let n=Math.floor(t/7)+2,l=K3(t),d=l===145?26:Math.ceil((l-13)/(2*n-2))*2,h=[l-7];for(let c=1;c{var G3=du().getSymbolSize,GL=7;YL.getPositions=o(function(t){let n=G3(t);return[[0,0],[n-GL,0],[0,n-GL]]},"getPositions")});var QL=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=gy(),yy=[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],wy=[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];hC.getBlocksCount=o(function(t,n){switch(n){case hu.L:return yy[(t-1)*4+0];case hu.M:return yy[(t-1)*4+1];case hu.Q:return yy[(t-1)*4+2];case hu.H:return yy[(t-1)*4+3];default:return}},"getBlocksCount");hC.getTotalCodewordsCount=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}},"getTotalCodewordsCount")});var ZL=Ue(Sy=>{var km=new Uint8Array(512),xy=new Uint8Array(256);o(function(){let t=1;for(let n=0;n<255;n++)km[n]=t,xy[t]=n,t<<=1,t&256&&(t^=285);for(let n=255;n<512;n++)km[n]=km[n-255]},"initTables")();Sy.log=o(function(t){if(t<1)throw new Error("log("+t+")");return xy[t]},"log");Sy.exp=o(function(t){return km[t]},"exp");Sy.mul=o(function(t,n){return t===0||n===0?0:km[xy[t]+xy[n]]},"mul")});var JL=Ue(Nm=>{var gC=ZL();Nm.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 eP=JL();function vC(e){this.genPoly=void 0,this.degree=e,this.degree&&this.initialize(this.degree)}o(vC,"ReedSolomonEncoder");vC.prototype.initialize=o(function(t){this.degree=t,this.genPoly=eP.generateECPolynomial(this.degree)},"initialize");vC.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=eP.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");tP.exports=vC});var yC=Ue(nP=>{nP.isValid=o(function(t){return!isNaN(t)&&t>=1&&t<=40},"isValid")});var wC=Ue(Bl=>{var iP="[0-9]+",X3="[A-Z $%*+\\-./:]+",Lm="(?:[u3000-u303F]|[u3040-u309F]|[u30A0-u30FF]|[uFF00-uFFEF]|[u4E00-u9FAF]|[u2605-u2606]|[u2190-u2195]|u203B|[u2010u2015u2018u2019u2025u2026u201Cu201Du2225u2260]|[u0391-u0451]|[u00A7u00A8u00B1u00B4u00D7u00F7])+";Lm=Lm.replace(/u/g,"\\u");var Q3="(?:(?![A-Z0-9 $%*+\\-./:]|"+Lm+`)(?:.|[\r +]))+`;Bl.KANJI=new RegExp(Lm,"g");Bl.BYTE_KANJI=new RegExp("[^A-Z0-9 $%*+\\-./:]+","g");Bl.BYTE=new RegExp(Q3,"g");Bl.NUMERIC=new RegExp(iP,"g");Bl.ALPHANUMERIC=new RegExp(X3,"g");var Z3=new RegExp("^"+Lm+"$"),J3=new RegExp("^"+iP+"$"),eB=new RegExp("^[A-Z0-9 $%*+\\-./:]+$");Bl.testKanji=o(function(t){return Z3.test(t)},"testKanji");Bl.testNumeric=o(function(t){return J3.test(t)},"testNumeric");Bl.testAlphanumeric=o(function(t){return eB.test(t)},"testAlphanumeric")});var mu=Ue(Xr=>{var tB=yC(),xC=wC();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(!tB.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 xC.testNumeric(t)?Xr.NUMERIC:xC.testAlphanumeric(t)?Xr.ALPHANUMERIC:xC.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 rB(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(rB,"fromString");Xr.from=o(function(t,n){if(Xr.isValid(t))return t;try{return rB(t)}catch(l){return n}},"from")});var uP=Ue(Kf=>{var Cy=du(),nB=mC(),oP=gy(),gu=mu(),SC=yC(),sP=1<<12|1<<11|1<<10|1<<9|1<<8|1<<5|1<<2|1<<0,lP=Cy.getBCHDigit(sP);function iB(e,t,n){for(let l=1;l<=40;l++)if(t<=Kf.getCapacity(l,n,e))return l}o(iB,"getBestVersionForDataLength");function aP(e,t){return gu.getCharCountIndicator(e,t)+4}o(aP,"getReservedBitsCount");function oB(e,t){let n=0;return e.forEach(function(l){n+=aP(l.mode,t)+l.getBitsLength()}),n}o(oB,"getTotalBitsFromDataArray");function sB(e,t){for(let n=1;n<=40;n++)if(oB(e,n)<=Kf.getCapacity(n,t,gu.MIXED))return n}o(sB,"getBestVersionForMixedData");Kf.from=o(function(t,n){return SC.isValid(t)?parseInt(t,10):n},"from");Kf.getCapacity=o(function(t,n,l){if(!SC.isValid(t))throw new Error("Invalid QR Code version");typeof l=="undefined"&&(l=gu.BYTE);let d=Cy.getSymbolTotalCodewords(t),h=nB.getTotalCodewordsCount(t,n),c=(d-h)*8;if(l===gu.MIXED)return c;let v=c-aP(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=oP.from(n,oP.M);if(Array.isArray(t)){if(t.length>1)return sB(t,d);if(t.length===0)return 1;l=t[0]}else l=t;return iB(l.mode,l.getLength(),d)},"getBestVersionForData");Kf.getEncodedBits=o(function(t){if(!SC.isValid(t)||t<7)throw new Error("Invalid QR Code version");let n=t<<12;for(;Cy.getBCHDigit(n)-lP>=0;)n^=sP<{var CC=du(),fP=1<<10|1<<8|1<<5|1<<4|1<<2|1<<1|1<<0,lB=1<<14|1<<12|1<<10|1<<4|1<<1,cP=CC.getBCHDigit(fP);pP.getEncodedBits=o(function(t,n){let l=t.bit<<3|n,d=l<<10;for(;CC.getBCHDigit(d)-cP>=0;)d^=fP<{var aB=mu();function Kp(e){this.mode=aB.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");hP.exports=Kp});var vP=Ue((p$,gP)=>{var uB=mu(),bC=["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=uB.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=bC.indexOf(this.data[n])*45;l+=bC.indexOf(this.data[n+1]),t.put(l,11)}this.data.length%2&&t.put(bC.indexOf(this.data[n]),6)},"write");gP.exports=Gp});var wP=Ue((d$,yP)=>{"use strict";yP.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 SP=Ue((h$,xP)=>{var fB=wP(),cB=mu();function Yp(e){this.mode=cB.BYTE,typeof e=="string"&&(e=fB(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 pB=mu(),dB=du();function Xp(e){this.mode=pB.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)}};CP.exports=Xp});var EP=Ue((g$,EC)=>{"use strict";var Pm={single_source_shortest_paths:function(e,t,n){var l={},d={};d[t]=0;var h=Pm.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=Pm.single_source_shortest_paths(e,t,n);return Pm.extract_shortest_path_from_predecessor_list(l,n)},PriorityQueue:{make:function(e){var t=Pm.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 EC!="undefined"&&(EC.exports=Pm)});var MP=Ue(Qp=>{var It=mu(),_P=mP(),TP=vP(),kP=SP(),NP=bP(),Om=wC(),by=du(),hB=EP();function LP(e){return unescape(encodeURIComponent(e)).length}o(LP,"getStringByteLength");function Mm(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(Mm,"getSegments");function PP(e){let t=Mm(Om.NUMERIC,It.NUMERIC,e),n=Mm(Om.ALPHANUMERIC,It.ALPHANUMERIC,e),l,d;return by.isKanjiModeEnabled()?(l=Mm(Om.BYTE,It.BYTE,e),d=Mm(Om.KANJI,It.KANJI,e)):(l=Mm(Om.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(PP,"getSegmentsFromString");function _C(e,t){switch(t){case It.NUMERIC:return _P.getBitsLength(e);case It.ALPHANUMERIC:return TP.getBitsLength(e);case It.KANJI:return NP.getBitsLength(e);case It.BYTE:return kP.getBitsLength(e)}}o(_C,"getSegmentBitsLength");function mB(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(mB,"mergeSegments");function gB(e){let t=[];for(let n=0;n{var Ey=du(),TC=gy(),yB=jL(),wB=VL(),xB=KL(),SB=XL(),kC=QL(),NC=mC(),CB=rP(),_y=uP(),bB=dP(),EB=mu(),LC=MP();function _B(e,t){let n=e.size,l=SB.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(_B,"setupFinderPattern");function TB(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(NB,"setupVersionInfo");function PC(e,t,n){let l=e.size,d=bB.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(PC,"setupFormatInfo");function LB(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(LB,"setupData");function PB(e,t,n){let l=new yB;n.forEach(function(C){l.put(C.mode.bit,4),l.put(C.getLength(),EB.getCharCountIndicator(C.mode,e)),C.write(l)});let d=Ey.getSymbolTotalCodewords(e),h=NC.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;C=7&&NB(C,t),LB(C,c),isNaN(l)&&(l=kC.getBestMask(C,PC.bind(null,C,n))),kC.applyMask(l,C),PC(C,n,l),{modules:C,version:t,errorCorrectionLevel:n,maskPattern:l,segments:d}}o(MB,"createSymbol");AP.create=o(function(t,n){if(typeof t=="undefined"||t==="")throw new Error("No input text");let l=TC.M,d,h;return typeof n!="undefined"&&(l=TC.from(n.errorCorrectionLevel,TC.M),d=_y.from(n.version),h=kC.from(n.maskPattern),n.toSJISFunc&&Ey.setToSJISFunction(n.toSJISFunc)),MB(t,d,l,h)},"create")});var OC=Ue(Gf=>{function RP(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(RP,"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:RP(t.color.dark||"#000000ff"),light:RP(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 MC=OC();function AB(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(AB,"clearCanvas");function DB(){try{return document.createElement("canvas")}catch(e){throw new Error("You need to specify a canvas element")}}o(DB,"getCanvasElement");Ty.render=o(function(t,n,l){let d=l,h=n;typeof d=="undefined"&&(!n||!n.getContext)&&(d=n,n=void 0),n||(h=DB()),d=MC.getOptions(d);let c=MC.getImageWidth(t.modules.size,d),v=h.getContext("2d"),C=v.createImageData(c,c);return MC.qrToImageData(C.data,t,d),AB(v,h,c),v.putImageData(C,0,0),h},"render");Ty.renderToDataURL=o(function(t,n,l){let d=l;typeof d=="undefined"&&(!n||!n.getContext)&&(d=n,n=void 0),d||(d={});let h=Ty.render(t,n,d),c=d.type||"image/png",v=d.rendererOpts||{};return h.toDataURL(c,v.quality)},"renderToDataURL")});var HP=Ue(BP=>{var RB=OC();function FP(e,t){let n=e.a/255,l=t+'="'+e.hex+'"';return n<1?l+" "+t+'-opacity="'+n.toFixed(2).slice(1)+'"':l}o(FP,"getColorAttrib");function AC(e,t,n){let l=e+t;return typeof n!="undefined"&&(l+=" "+n),l}o(AC,"svgCmd");function IB(e,t,n){let l="",d=0,h=!1,c=0;for(let v=0;v0&&C>0&&e[v-1]||(l+=h?AC("M",C+n,.5+k+n):AC("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 UP=Ue(Am=>{var FB=UL(),DC=DP(),WP=IP(),BB=HP();function RC(e,t,n,l,d){let h=[].slice.call(arguments,1),c=h.length,v=typeof h[c-1]=="function";if(!v&&!FB())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=DC.create(n,l);C(e(O,t,l))}catch(O){k(O)}})}try{let C=DC.create(n,l);d(null,e(C,t,l))}catch(C){d(C)}}o(RC,"renderCanvas");Am.create=DC.create;Am.toCanvas=RC.bind(null,WP.render);Am.toDataURL=RC.bind(null,WP.renderToDataURL);Am.toString=RC.bind(null,function(e,t,n){return BB.render(e,n)})});var ib=fe(Oe()),uO=fe(iu());var Gh=fe(Oe()),NH=fe(uk());var fk=fe(Oe()),ei=fk.default.createContext(null);function _I(e){e()}o(_I,"defaultNoopBatch");var ck=_I,pk=o(function(t){return ck=t},"setBatch"),dk=o(function(){return ck},"getBatch");var hk={notify:o(function(){},"notify")};function TI(){var e=dk(),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(TI,"createListenerCollection");var Tp=function(){function e(n,l){this.store=n,this.parentSub=l,this.unsubscribe=null,this.listeners=hk,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=TI())},"trySubscribe"),t.tryUnsubscribe=o(function(){this.unsubscribe&&(this.unsubscribe(),this.unsubscribe=null,this.listeners.clear(),this.listeners=hk)},"tryUnsubscribe"),e}();var Yv=fe(Oe()),Nf=typeof window!="undefined"&&typeof window.document!="undefined"&&typeof window.document.createElement!="undefined"?Yv.useLayoutEffect:Yv.useEffect;function kI(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(kI,"Provider");var Wx=kI;function Po(){return Po=Object.assign||function(e){for(var t=1;t=0)&&(n[d]=e[d]);return n}o(ou,"_objectWithoutPropertiesLoose");var Yx=fe(_k()),cr=fe(Oe()),Pk=fe(Lk());var qI=[],VI=[null,null];function KI(e,t){var n=e[1];return[t.payload,n+1]}o(KI,"storeStateUpdatesReducer");function Ok(e,t,n){Nf(function(){return e.apply(void 0,t)},n)}o(Ok,"useIsomorphicLayoutEffectWithArgs");function GI(e,t,n,l,d,h,c){e.current=l,t.current=d,n.current=!1,h.current&&(h.current=null,c())}o(GI,"captureWrapperProps");function YI(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(YI,"subscribeUpdates");var XI=o(function(){return[null,0]},"initStateUpdates");function g0(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],Zt=nr[2],_t=(0,cr.useMemo)(function(){return ct&&ct.Consumer&&(0,Pk.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 VI;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)(KI,qI,XI),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)(Zt),cn=(0,cr.useRef)(),Jt=(0,cr.useRef)(!1),gr=St(function(){return cn.current&&Zt===si.current?cn.current:$t(zt.getState(),Zt)},[zt,Or,Zt]);Ok(GI,[si,Un,Jt,Zt,gr,cn,Pr]),Ok(YI,[O,zt,rt,$t,si,Un,Jt,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,Yx.default)(Ge,pe)}return(0,Yx.default)(Ke,pe)},"wrapWithConnect")}o(g0,"connectAdvanced");function Mk(e,t){return e===t?e!==0||t!==0||1/e==1/t:e!==e&&t!==t}o(Mk,"is");function kp(e,t){if(Mk(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(Zx,"match");function uF(e,t){return e===t}o(uF,"strictEqual");function fF(e){var t=e===void 0?{}:e,n=t.connectHOC,l=n===void 0?g0:n,d=t.mapStateToPropsFactories,h=d===void 0?Rk:d,c=t.mapDispatchToPropsFactories,v=c===void 0?Dk:c,C=t.mergePropsFactories,k=C===void 0?Ik:C,O=t.selectorFactory,j=O===void 0?Qx: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?uF: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=Zx(X,h,"mapStateToProps"),We=Zx(J,v,"mapDispatchToProps"),Ke=Zx(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(fF,"createConnect");var Hi=fF();var Bk=fe(Oe());var Fk=fe(Oe());function y0(){var e=(0,Fk.useContext)(ei);return e}o(y0,"useReduxContext");function w0(e){e===void 0&&(e=ei);var t=e===ei?y0:function(){return(0,Bk.useContext)(e)};return o(function(){var l=t(),d=l.store;return d},"useStore")}o(w0,"createStoreHook");var Jx=w0();function Hk(e){e===void 0&&(e=ei);var t=e===ei?Jx:w0(e);return o(function(){var l=t();return l.dispatch},"useDispatch")}o(Hk,"createDispatchHook");var Gs=Hk();var ro=fe(Oe());var cF=o(function(t,n){return t===n},"refEquality");function pF(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+=` The error may be correlated with this previous error: -`+x.current.stack+` +`+v.current.stack+` -`),te}return hf(function(){_.current=e,O.current=Y,D.current=U,x.current=void 0}),hf(function(){function te(){try{var Q=n.getState(),F=_.current(Q);if(t(F,D.current))return;D.current=F,O.current=Q}catch(M){x.current=M}m()}return o(te,"checkForUpdates"),p.onStateChange=te,p.trySubscribe(),te(),function(){return p.tryUnsubscribe()}},[n,p]),U}o(QR,"useSelectorWithStoreAndSubscription");function kT(e){e===void 0&&(e=Yn);var t=e===Yn?jg:function(){return(0,Xi.useContext)(e)};return o(function(l,d){d===void 0&&(d=XR);var m=t(),p=m.store,x=m.subscription,_=QR(l,d,p,x);return(0,Xi.useDebugValue)(_),_},"useSelector")}o(kT,"createSelectorHook");var mx=kT();var vx=pe(Za());JE(vx.unstable_batchedUpdates);var Rn=pe(De());var OT="UI_FLOWVIEW_SET_TAB",LT="SET_CONTENT_VIEW_FOR",ZR={tab:"request",contentViewFor:{}};function gx(e=ZR,t){switch(t.type){case LT:return It(Pe({},e),{contentViewFor:It(Pe({},e.contentViewFor),{[t.messageId]:t.contentView})});case OT:return It(Pe({},e),{tab:t.tab?t.tab:"request"});default:return e}}o(gx,"reducer");function mf(e){return{type:OT,tab:e}}o(mf,"selectTab");function Vg(e,t){return{type:LT,messageId:e,contentView:t}}o(Vg,"setContentViewFor");var NT=pe(Oh()),JR=pe(De());window._=NT.default;window.React=JR;var Kg=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"),Gg=o(function(e){for(var t=e,n=["ms","s","min","h"],l=[1e3,60,60],d=0;Math.abs(t)>=l[d]&&dOt(e,Pe({method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)},n));Ot.post=(e,t,n={})=>Ot(e,Pe({method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)},n));function vf(e,...t){return Na(this,null,function*(){return yield(yield Ot(`/commands/${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({arguments:t})})).json()})}o(vf,"runCommand");var Nh={};kC(Nh,{ADD:()=>_x,RECEIVE:()=>Tx,REMOVE:()=>Ex,SET_FILTER:()=>Sx,SET_SORT:()=>Cx,UPDATE:()=>bx,add:()=>rF,defaultState:()=>Yg,receive:()=>oF,reduce:()=>up,remove:()=>iF,setFilter:()=>kx,setSort:()=>MT,update:()=>nF});var xx=pe(PT()),Sx="LIST_SET_FILTER",Cx="LIST_SET_SORT",_x="LIST_ADD",bx="LIST_UPDATE",Ex="LIST_REMOVE",Tx="LIST_RECEIVE",Yg={byId:{},list:[],listIndex:{},view:[],viewIndex:{}};function up(e=Yg,t){let{byId:n,list:l,listIndex:d,view:m,viewIndex:p}=e;switch(t.type){case Sx:m=(0,xx.default)(l.filter(t.filter),t.sort),p={},m.forEach((O,D)=>{p[O.id]=D});break;case Cx:m=(0,xx.default)([...m],t.sort),p={},m.forEach((O,D)=>{p[O.id]=D});break;case _x:if(t.item.id in n)break;n=It(Pe({},n),{[t.item.id]:t.item}),d=It(Pe({},d),{[t.item.id]:l.length}),l=[...l,t.item],t.filter(t.item)&&({view:m,viewIndex:p}=AT(e,t.item,t.sort));break;case bx:n=It(Pe({},n),{[t.item.id]:t.item}),l=[...l],l[d[t.item.id]]=t.item;let x=t.item.id in p,_=t.filter(t.item);_&&!x?{view:m,viewIndex:p}=AT(e,t.item,t.sort):!_&&x?{data:m,dataIndex:p}=Ox(m,p,t.item.id):_&&x&&({view:m,viewIndex:p}=sF(e,t.item,t.sort));break;case Ex:if(!(t.id in n))break;n=Pe({},n),delete n[t.id],{data:l,dataIndex:d}=Ox(l,d,t.id),t.id in p&&({data:m,dataIndex:p}=Ox(m,p,t.id));break;case Tx:l=t.list,d={},n={},l.forEach((O,D)=>{n[O.id]=O,d[O.id]=D}),m=l.filter(t.filter).sort(t.sort),p={},m.forEach((O,D)=>{p[O.id]=D});break}return{byId:n,list:l,listIndex:d,view:m,viewIndex:p}}o(up,"reduce");function kx(e=Xg,t=Lh){return{type:Sx,filter:e,sort:t}}o(kx,"setFilter");function MT(e=Lh){return{type:Cx,sort:e}}o(MT,"setSort");function rF(e,t=Xg,n=Lh){return{type:_x,item:e,filter:t,sort:n}}o(rF,"add");function nF(e,t=Xg,n=Lh){return{type:bx,item:e,filter:t,sort:n}}o(nF,"update");function iF(e){return{type:Ex,id:e}}o(iF,"remove");function oF(e,t=Xg,n=Lh){return{type:Tx,list:e,filter:t,sort:n}}o(oF,"receive");function AT(e,t,n){let l=lF(e.view,t,n),d=[...e.view],m=Pe({},e.viewIndex);d.splice(l,0,t);for(let p=d.length-1;p>=l;p--)m[d[p].id]=p;return{view:d,viewIndex:m}}o(AT,"sortedInsert");function Ox(e,t,n){let l=t[n],d=[...e],m=Pe({},t);delete m[n],d.splice(l,1);for(let p=d.length-1;p>=l;p--)m[d[p].id]=p;return{data:d,dataIndex:m}}o(Ox,"removeData");function sF(e,t,n){let l=[...e.view],d=Pe({},e.viewIndex),m=d[t.id];for(l[m]=t;m+10;)l[m]=l[m+1],l[m+1]=t,d[t.id]=m+1,d[l[m].id]=m,++m;for(;m>0&&n(l[m],l[m-1])<0;)l[m]=l[m-1],l[m-1]=t,d[t.id]=m-1,d[l[m].id]=m,--m;return{view:l,viewIndex:d}}o(sF,"sortedUpdate");function lF(e,t,n){let l=0,d=e.length;for(;l>>1;n(t,e[m])>=0?l=m+1:d=m}return l}o(lF,"sortedIndex");function Xg(){return!0}o(Xg,"defaultFilter");function Lh(e,t){return 0}o(Lh,"defaultSort");var DT={http:80,https:443},Ur=class{static getContentType(t){var n=Ur.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 m;for(let p=0;p{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":return e.messages_meta.contentLength||0;case"dns":return(n=(t=e.response)==null?void 0:t.size)!=null?n:0}},"getTotalSize"),Qg=o(e=>e.type==="http"&&!e.websocket,"canReplay");var gf=function(){"use strict";function e(l,d){function m(){this.constructor=l}o(m,"ctor"),m.prototype=d.prototype,l.prototype=new m}o(e,"peg$subclass");function t(l,d,m,p){this.message=l,this.expected=d,this.found=m,this.location=p,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]:{},m=this,p={},x={start:fs},_=fs,O={type:"other",description:"filter expression"},D=o(function(y){return y},"peg$c1"),Y={type:"other",description:"whitespace"},U=/^[ \t\n\r]/,X={type:"class",value:"[ \\t\\n\\r]",description:"[ \\t\\n\\r]"},te={type:"other",description:"control character"},Q=/^[|&!()~"]/,F={type:"class",value:'[|&!()~"]',description:'[|&!()~"]'},M={type:"other",description:"optional whitespace"},R="|",K={type:"literal",value:"|",description:'"|"'},V=o(function(y,T){return ds(y,T)},"peg$c11"),ue="&",ie={type:"literal",value:"&",description:'"&"'},de=o(function(y,T){return Ct(y,T)},"peg$c14"),ge="!",xe={type:"literal",value:"!",description:'"!"'},qe=o(function(y){return io(y)},"peg$c17"),et="(",Te={type:"literal",value:"(",description:'"("'},xt=")",Ue={type:"literal",value:")",description:'")"'},Ve=o(function(y){return _u(y)},"peg$c22"),Ke="~all",Ye={type:"literal",value:"~all",description:'"~all"'},Qt=o(function(){return zf},"peg$c25"),ft="~a",Ar={type:"literal",value:"~a",description:'"~a"'},Kt=o(function(){return sr},"peg$c28"),Et="~b",St={type:"literal",value:"~b",description:'"~b"'},at=o(function(y){return qp(y)},"peg$c31"),_r="~bq",Ut={type:"literal",value:"~bq",description:'"~bq"'},$t=o(function(y){return jf(y)},"peg$c34"),ne="~bs",tt={type:"literal",value:"~bs",description:'"~bs"'},br=o(function(y){return hs(y)},"peg$c37"),jt="~c",qt={type:"literal",value:"~c",description:'"~c"'},Se=o(function(y){return Zl(y)},"peg$c40"),Er="~comment",nn={type:"literal",value:"~comment",description:'"~comment"'},Fn=o(function(y){return Uo(y)},"peg$c43"),ei="~d",on={type:"literal",value:"~d",description:'"~d"'},Gt=o(function(y){return Vp(y)},"peg$c46"),dr="~dns",ct={type:"literal",value:"~dns",description:'"~dns"'},Do=o(function(){return ol},"peg$c49"),vr="~dst",Fi={type:"literal",value:"~dst",description:'"~dst"'},sn=o(function(y){return bu(y)},"peg$c52"),In="~e",vi={type:"literal",value:"~e",description:'"~e"'},gi=o(function(){return Jl},"peg$c55"),Hn="~h",En={type:"literal",value:"~h",description:'"~h"'},Ks=o(function(y){return Eu(y)},"peg$c58"),H="~hq",J={type:"literal",value:"~hq",description:'"~hq"'},he=o(function(y){return Tu(y)},"peg$c61"),ke="~hs",Zt={type:"literal",value:"~hs",description:'"~hs"'},Bl=o(function(y){return qf(y)},"peg$c64"),Rt="~http",Dr={type:"literal",value:"~http",description:'"~http"'},Jt=o(function(){return ea},"peg$c67"),ti="~marked",os={type:"literal",value:"~marked",description:'"~marked"'},to=o(function(){return so},"peg$c70"),yi="~marker",Gs={type:"literal",value:"~marker",description:'"~marker"'},ss=o(function(y){return Wi(y)},"peg$c73"),ln="~m",Ap={type:"literal",value:"~m",description:'"~m"'},Ys=o(function(y){return sl(y)},"peg$c76"),Pf="~q",Xs={type:"literal",value:"~q",description:'"~q"'},Dp=o(function(){return Rr},"peg$c79"),Mf="~replayq",uu={type:"literal",value:"~replayq",description:'"~replayq"'},Rp=o(function(){return ll},"peg$c82"),Ul="~replays",ls={type:"literal",value:"~replays",description:'"~replays"'},Fp=o(function(){return Vr},"peg$c85"),Af="~replay",Qs={type:"literal",value:"~replay",description:'"~replay"'},fu=o(function(){return Bi},"peg$c88"),Ro="~src",Ip={type:"literal",value:"~src",description:'"~src"'},Fo=o(function(y){return ta(y)},"peg$c91"),$l="~s",Df={type:"literal",value:"~s",description:'"~s"'},zt=o(function(){return Vf},"peg$c94"),Me="~tcp",wi={type:"literal",value:"~tcp",description:'"~tcp"'},cu=o(function(){return ku},"peg$c97"),ri="~tq",vt={type:"literal",value:"~tq",description:'"~tq"'},ro=o(function(y){return Kp(y)},"peg$c100"),Io="~ts",zl={type:"literal",value:"~ts",description:'"~ts"'},ae=o(function(y){return Kf(y)},"peg$c103"),$e="~t",pu={type:"literal",value:"~t",description:'"~t"'},du=o(function(y){return Ou(y)},"peg$c106"),as="~u",Zs={type:"literal",value:"~u",description:'"~u"'},jl=o(function(y){return ni(y)},"peg$c109"),Be="~websocket",Hp={type:"literal",value:"~websocket",description:'"~websocket"'},hu=o(function(){return ii},"peg$c112"),Ho={type:"other",description:"integer"},Wn=/^['"]/,mu={type:"class",value:`['"]`,description:`['"]`},ql=/^[0-9]/,Wo={type:"class",value:"[0-9]",description:"[0-9]"},Js=o(function(y){return parseInt(y.join(""),10)},"peg$c118"),Rf={type:"other",description:"string"},el='"',tl={type:"literal",value:'"',description:'"\\""'},us=o(function(y){return y.join("")},"peg$c122"),no="'",Vl={type:"literal",value:"'",description:`"'"`},Ff=/^["\\]/,Wp={type:"class",value:'["\\\\]',description:'["\\\\]'},rl={type:"any",description:"any character"},an=o(function(y){return y},"peg$c128"),vu="\\",gu={type:"literal",value:"\\",description:'"\\\\"'},Kl=/^['\\]/,nl={type:"class",value:"['\\\\]",description:"['\\\\]"},Bp=/^['"\\]/,If={type:"class",value:`['"\\\\]`,description:`['"\\\\]`},Up="n",$p={type:"literal",value:"n",description:'"n"'},yu=o(function(){return` -`},"peg$c137"),Hf="r",wu={type:"literal",value:"r",description:'"r"'},Wf=o(function(){return"\r"},"peg$c140"),Bf="t",Gl={type:"literal",value:"t",description:'"t"'},Yl=o(function(){return" "},"peg$c143"),N=0,Ce=0,gt=[{line:1,column:1,seenCR:!1}],Tn=0,xu=[],ye=0,kn;if("startRule"in d){if(!(d.startRule in x))throw new Error(`Can't start parsing from rule "`+d.startRule+'".');_=x[d.startRule]}function am(){return l.substring(Ce,N)}o(am,"text");function um(){return Ii(Ce,N)}o(um,"location");function Su(y){throw Bo(null,[{type:"other",description:y}],l.substring(Ce,N),Ii(Ce,N))}o(Su,"expected");function zp(y){throw Bo(y,null,l.substring(Ce,N),Ii(Ce,N))}o(zp,"error");function Nt(y){var T=gt[y],B,W;if(T)return T;for(B=y-1;!gt[B];)B--;for(T=gt[B],T={line:T.line,column:T.column,seenCR:T.seenCR};BTn&&(Tn=N,xu=[]),xu.push(y))}o(_e,"peg$fail");function Bo(y,T,B,W){function Fr(Bn){var Un=1;for(Bn.sort(function(Si,gr){return Si.descriptiongr.description?1:0});Un1?gr.slice(0,-1).join(", ")+" or "+gr[Bn.length-1]:gr[0],ul=Un?'"'+Si(Un)+'"':"end of input","Expected "+al+" but "+ul+" found."}return o(un,"buildMessage"),T!==null&&Fr(T),new t(y!==null?y:un(T,B),T,B,W)}o(Bo,"peg$buildException");function fs(){var y,T,B,W;return ye++,y=N,T=xi(),T!==p?(B=Xl(),B!==p?(W=xi(),W!==p?(Ce=y,T=D(B),y=T):(N=y,y=p)):(N=y,y=p)):(N=y,y=p),ye--,y===p&&(T=p,ye===0&&_e(O)),y}o(fs,"peg$parsestart");function He(){var y,T;return ye++,U.test(l.charAt(N))?(y=l.charAt(N),N++):(y=p,ye===0&&_e(X)),ye--,y===p&&(T=p,ye===0&&_e(Y)),y}o(He,"peg$parsews");function Uf(){var y,T;return ye++,Q.test(l.charAt(N))?(y=l.charAt(N),N++):(y=p,ye===0&&_e(F)),ye--,y===p&&(T=p,ye===0&&_e(te)),y}o(Uf,"peg$parsecc");function xi(){var y,T;for(ye++,y=[],T=He();T!==p;)y.push(T),T=He();return ye--,y===p&&(T=p,ye===0&&_e(M)),y}o(xi,"peg$parse__");function Xl(){var y,T,B,W,Fr,un;return y=N,T=il(),T!==p?(B=xi(),B!==p?(l.charCodeAt(N)===124?(W=R,N++):(W=p,ye===0&&_e(K)),W!==p?(Fr=xi(),Fr!==p?(un=Xl(),un!==p?(Ce=y,T=V(T,un),y=T):(N=y,y=p)):(N=y,y=p)):(N=y,y=p)):(N=y,y=p)):(N=y,y=p),y===p&&(y=il()),y}o(Xl,"peg$parseOrExpr");function il(){var y,T,B,W,Fr,un;if(y=N,T=cs(),T!==p?(B=xi(),B!==p?(l.charCodeAt(N)===38?(W=ue,N++):(W=p,ye===0&&_e(ie)),W!==p?(Fr=xi(),Fr!==p?(un=il(),un!==p?(Ce=y,T=de(T,un),y=T):(N=y,y=p)):(N=y,y=p)):(N=y,y=p)):(N=y,y=p)):(N=y,y=p),y===p){if(y=N,T=cs(),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=il(),W!==p?(Ce=y,T=de(T,W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;y===p&&(y=cs())}return y}o(il,"peg$parseAndExpr");function cs(){var y,T,B,W;return y=N,l.charCodeAt(N)===33?(T=ge,N++):(T=p,ye===0&&_e(xe)),T!==p?(B=xi(),B!==p?(W=cs(),W!==p?(Ce=y,T=qe(W),y=T):(N=y,y=p)):(N=y,y=p)):(N=y,y=p),y===p&&(y=Cu()),y}o(cs,"peg$parseNotExpr");function Cu(){var y,T,B,W,Fr,un;return y=N,l.charCodeAt(N)===40?(T=et,N++):(T=p,ye===0&&_e(Te)),T!==p?(B=xi(),B!==p?(W=Xl(),W!==p?(Fr=xi(),Fr!==p?(l.charCodeAt(N)===41?(un=xt,N++):(un=p,ye===0&&_e(Ue)),un!==p?(Ce=y,T=Ve(W),y=T):(N=y,y=p)):(N=y,y=p)):(N=y,y=p)):(N=y,y=p)):(N=y,y=p),y===p&&(y=On()),y}o(Cu,"peg$parseBindingExpr");function On(){var y,T,B,W;if(y=N,l.substr(N,4)===Ke?(T=Ke,N+=4):(T=p,ye===0&&_e(Ye)),T!==p&&(Ce=y,T=Qt()),y=T,y===p&&(y=N,l.substr(N,2)===ft?(T=ft,N+=2):(T=p,ye===0&&_e(Ar)),T!==p&&(Ce=y,T=Kt()),y=T,y===p)){if(y=N,l.substr(N,2)===Et?(T=Et,N+=2):(T=p,ye===0&&_e(St)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=at(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,3)===_r?(T=_r,N+=3):(T=p,ye===0&&_e(Ut)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=$t(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,3)===ne?(T=ne,N+=3):(T=p,ye===0&&_e(tt)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=br(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,2)===jt?(T=jt,N+=2):(T=p,ye===0&&_e(qt)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=jp(),W!==p?(Ce=y,T=Se(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,8)===Er?(T=Er,N+=8):(T=p,ye===0&&_e(nn)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=Fn(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,2)===ei?(T=ei,N+=2):(T=p,ye===0&&_e(on)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=Gt(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p&&(y=N,l.substr(N,4)===dr?(T=dr,N+=4):(T=p,ye===0&&_e(ct)),T!==p&&(Ce=y,T=Do()),y=T,y===p)){if(y=N,l.substr(N,4)===vr?(T=vr,N+=4):(T=p,ye===0&&_e(Fi)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=sn(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p&&(y=N,l.substr(N,2)===In?(T=In,N+=2):(T=p,ye===0&&_e(vi)),T!==p&&(Ce=y,T=gi()),y=T,y===p)){if(y=N,l.substr(N,2)===Hn?(T=Hn,N+=2):(T=p,ye===0&&_e(En)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=Ks(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,3)===H?(T=H,N+=3):(T=p,ye===0&&_e(J)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=he(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,3)===ke?(T=ke,N+=3):(T=p,ye===0&&_e(Zt)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=Bl(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p&&(y=N,l.substr(N,5)===Rt?(T=Rt,N+=5):(T=p,ye===0&&_e(Dr)),T!==p&&(Ce=y,T=Jt()),y=T,y===p&&(y=N,l.substr(N,7)===ti?(T=ti,N+=7):(T=p,ye===0&&_e(os)),T!==p&&(Ce=y,T=to()),y=T,y===p))){if(y=N,l.substr(N,7)===yi?(T=yi,N+=7):(T=p,ye===0&&_e(Gs)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=ss(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,2)===ln?(T=ln,N+=2):(T=p,ye===0&&_e(Ap)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=Ys(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p&&(y=N,l.substr(N,2)===Pf?(T=Pf,N+=2):(T=p,ye===0&&_e(Xs)),T!==p&&(Ce=y,T=Dp()),y=T,y===p&&(y=N,l.substr(N,8)===Mf?(T=Mf,N+=8):(T=p,ye===0&&_e(uu)),T!==p&&(Ce=y,T=Rp()),y=T,y===p&&(y=N,l.substr(N,8)===Ul?(T=Ul,N+=8):(T=p,ye===0&&_e(ls)),T!==p&&(Ce=y,T=Fp()),y=T,y===p&&(y=N,l.substr(N,7)===Af?(T=Af,N+=7):(T=p,ye===0&&_e(Qs)),T!==p&&(Ce=y,T=fu()),y=T,y===p))))){if(y=N,l.substr(N,4)===Ro?(T=Ro,N+=4):(T=p,ye===0&&_e(Ip)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=Fo(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p&&(y=N,l.substr(N,2)===$l?(T=$l,N+=2):(T=p,ye===0&&_e(Df)),T!==p&&(Ce=y,T=zt()),y=T,y===p&&(y=N,l.substr(N,4)===Me?(T=Me,N+=4):(T=p,ye===0&&_e(wi)),T!==p&&(Ce=y,T=cu()),y=T,y===p))){if(y=N,l.substr(N,3)===ri?(T=ri,N+=3):(T=p,ye===0&&_e(vt)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=ro(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,3)===Io?(T=Io,N+=3):(T=p,ye===0&&_e(zl)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=ae(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,2)===$e?(T=$e,N+=2):(T=p,ye===0&&_e(pu)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=du(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,2)===as?(T=as,N+=2):(T=p,ye===0&&_e(Zs)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=jl(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;y===p&&(y=N,l.substr(N,10)===Be?(T=Be,N+=10):(T=p,ye===0&&_e(Hp)),T!==p&&(Ce=y,T=hu()),y=T,y===p&&(y=N,T=Tt(),T!==p&&(Ce=y,T=jl(T)),y=T))}}}}}}}}}}}}}}}}}return y}o(On,"peg$parseExpr");function jp(){var y,T,B,W;if(ye++,y=N,Wn.test(l.charAt(N))?(T=l.charAt(N),N++):(T=p,ye===0&&_e(mu)),T===p&&(T=null),T!==p){if(B=[],ql.test(l.charAt(N))?(W=l.charAt(N),N++):(W=p,ye===0&&_e(Wo)),W!==p)for(;W!==p;)B.push(W),ql.test(l.charAt(N))?(W=l.charAt(N),N++):(W=p,ye===0&&_e(Wo));else B=p;B!==p?(Wn.test(l.charAt(N))?(W=l.charAt(N),N++):(W=p,ye===0&&_e(mu)),W===p&&(W=null),W!==p?(Ce=y,T=Js(B),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;return ye--,y===p&&(T=p,ye===0&&_e(Ho)),y}o(jp,"peg$parseIntegerLiteral");function Tt(){var y,T,B,W;if(ye++,y=N,l.charCodeAt(N)===34?(T=el,N++):(T=p,ye===0&&_e(tl)),T!==p){for(B=[],W=$f();W!==p;)B.push(W),W=$f();B!==p?(l.charCodeAt(N)===34?(W=el,N++):(W=p,ye===0&&_e(tl)),W!==p?(Ce=y,T=us(B),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.charCodeAt(N)===39?(T=no,N++):(T=p,ye===0&&_e(Vl)),T!==p){for(B=[],W=Ql();W!==p;)B.push(W),W=Ql();B!==p?(l.charCodeAt(N)===39?(W=no,N++):(W=p,ye===0&&_e(Vl)),W!==p?(Ce=y,T=us(B),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p)if(y=N,T=N,ye++,B=Uf(),ye--,B===p?T=void 0:(N=T,T=p),T!==p){if(B=[],W=Hi(),W!==p)for(;W!==p;)B.push(W),W=Hi();else B=p;B!==p?(Ce=y,T=us(B),y=T):(N=y,y=p)}else N=y,y=p}return ye--,y===p&&(T=p,ye===0&&_e(Rf)),y}o(Tt,"peg$parseStringLiteral");function $f(){var y,T,B;return y=N,T=N,ye++,Ff.test(l.charAt(N))?(B=l.charAt(N),N++):(B=p,ye===0&&_e(Wp)),ye--,B===p?T=void 0:(N=T,T=p),T!==p?(l.length>N?(B=l.charAt(N),N++):(B=p,ye===0&&_e(rl)),B!==p?(Ce=y,T=an(B),y=T):(N=y,y=p)):(N=y,y=p),y===p&&(y=N,l.charCodeAt(N)===92?(T=vu,N++):(T=p,ye===0&&_e(gu)),T!==p?(B=ps(),B!==p?(Ce=y,T=an(B),y=T):(N=y,y=p)):(N=y,y=p)),y}o($f,"peg$parseDoubleStringChar");function Ql(){var y,T,B;return y=N,T=N,ye++,Kl.test(l.charAt(N))?(B=l.charAt(N),N++):(B=p,ye===0&&_e(nl)),ye--,B===p?T=void 0:(N=T,T=p),T!==p?(l.length>N?(B=l.charAt(N),N++):(B=p,ye===0&&_e(rl)),B!==p?(Ce=y,T=an(B),y=T):(N=y,y=p)):(N=y,y=p),y===p&&(y=N,l.charCodeAt(N)===92?(T=vu,N++):(T=p,ye===0&&_e(gu)),T!==p?(B=ps(),B!==p?(Ce=y,T=an(B),y=T):(N=y,y=p)):(N=y,y=p)),y}o(Ql,"peg$parseSingleStringChar");function Hi(){var y,T,B;return y=N,T=N,ye++,B=He(),ye--,B===p?T=void 0:(N=T,T=p),T!==p?(l.length>N?(B=l.charAt(N),N++):(B=p,ye===0&&_e(rl)),B!==p?(Ce=y,T=an(B),y=T):(N=y,y=p)):(N=y,y=p),y}o(Hi,"peg$parseUnquotedStringChar");function ps(){var y,T;return Bp.test(l.charAt(N))?(y=l.charAt(N),N++):(y=p,ye===0&&_e(If)),y===p&&(y=N,l.charCodeAt(N)===110?(T=Up,N++):(T=p,ye===0&&_e($p)),T!==p&&(Ce=y,T=yu()),y=T,y===p&&(y=N,l.charCodeAt(N)===114?(T=Hf,N++):(T=p,ye===0&&_e(wu)),T!==p&&(Ce=y,T=Wf()),y=T,y===p&&(y=N,l.charCodeAt(N)===116?(T=Bf,N++):(T=p,ye===0&&_e(Gl)),T!==p&&(Ce=y,T=Yl()),y=T))),y}o(ps,"peg$parseEscapeSequence");function ds(y,T){function B(){return y.apply(this,arguments)||T.apply(this,arguments)}return o(B,"orFilter"),B.desc=y.desc+" or "+T.desc,B}o(ds,"or");function Ct(y,T){function B(){return y.apply(this,arguments)&&T.apply(this,arguments)}return o(B,"andFilter"),B.desc=y.desc+" and "+T.desc,B}o(Ct,"and");function io(y){function T(){return!y.apply(this,arguments)}return o(T,"notFilter"),T.desc="not "+y.desc,T}o(io,"not");function _u(y){function T(){return y.apply(this,arguments)}return o(T,"bindingFilter"),T.desc="("+y.desc+")",T}o(_u,"binding");function zf(y){return!0}o(zf,"allFilter"),zf.desc="all flows";var oo=[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 sr(y){if(y.response){for(var T=zs.getContentType(y.response),B=oo.length;B--;)if(oo[B].test(T))return!0}return!1}o(sr,"assetFilter"),sr.desc="is asset";function qp(y){y=new RegExp(y,"i");function T(B){return!0}return o(T,"bodyFilter"),T.desc="body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10",T}o(qp,"body");function jf(y){y=new RegExp(y,"i");function T(B){return!0}return o(T,"requestBodyFilter"),T.desc="body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10",T}o(jf,"requestBody");function hs(y){y=new RegExp(y,"i");function T(B){return!0}return o(T,"responseBodyFilter"),T.desc="body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10",T}o(hs,"responseBody");function Zl(y){function T(B){return B.response&&B.response.status_code===y}return o(T,"responseCodeFilter"),T.desc="resp. code is "+y,T}o(Zl,"responseCode");function Uo(y){y=new RegExp(y,"i");function T(B){return y.test(B.comment)}return o(T,"commentFilter"),T.desc="comment matches "+y,T}o(Uo,"comment");function Vp(y){y=new RegExp(y,"i");function T(B){return B.request&&(y.test(B.request.host)||y.test(B.request.pretty_host))}return o(T,"domainFilter"),T.desc="domain matches "+y,T}o(Vp,"domain");function ol(y){return y.type==="dns"}o(ol,"dnsFilter"),ol.desc="is a DNS Flow";function bu(y){y=new RegExp(y,"i");function T(B){return!!B.server_conn.address&&y.test(B.server_conn.address[0]+":"+B.server_conn.address[1])}return o(T,"destinationFilter"),T.desc="destination address matches "+y,T}o(bu,"destination");function Jl(y){return!!y.error}o(Jl,"errorFilter"),Jl.desc="has error";function Eu(y){y=new RegExp(y,"i");function T(B){return B.request&&ko.match_header(B.request,y)||B.response&&zs.match_header(B.response,y)}return o(T,"headerFilter"),T.desc="header matches "+y,T}o(Eu,"header");function Tu(y){y=new RegExp(y,"i");function T(B){return B.request&&ko.match_header(B.request,y)}return o(T,"requestHeaderFilter"),T.desc="req. header matches "+y,T}o(Tu,"requestHeader");function qf(y){y=new RegExp(y,"i");function T(B){return B.response&&zs.match_header(B.response,y)}return o(T,"responseHeaderFilter"),T.desc="resp. header matches "+y,T}o(qf,"responseHeader");function ea(y){return y.type==="http"}o(ea,"httpFilter"),ea.desc="is an HTTP Flow";function so(y){return y.marked}o(so,"markedFilter"),so.desc="is marked";function Wi(y){y=new RegExp(y,"i");function T(B){return y.test(B.marked)}return o(T,"markerFilter"),T.desc="marker matches "+y,T}o(Wi,"marker");function sl(y){y=new RegExp(y,"i");function T(B){return B.request&&y.test(B.request.method)}return o(T,"methodFilter"),T.desc="method matches "+y,T}o(sl,"method");function Rr(y){return y.request&&!y.response}o(Rr,"noResponseFilter"),Rr.desc="has no response";function ll(y){return y.is_replay==="request"}o(ll,"clientReplayFilter"),ll.desc="request has been replayed";function Vr(y){return y.is_replay==="response"}o(Vr,"serverReplayFilter"),Vr.desc="response has been replayed";function Bi(y){return!!y.is_replay}o(Bi,"replayFilter"),Bi.desc="flow has been replayed";function ta(y){y=new RegExp(y,"i");function T(B){return!!B.client_conn.peername&&y.test(B.client_conn.peername[0]+":"+B.client_conn.peername[1])}return o(T,"sourceFilter"),T.desc="source address matches "+y,T}o(ta,"source");function Vf(y){return!!y.response}o(Vf,"responseFilter"),Vf.desc="has response";function ku(y){return y.type==="tcp"}o(ku,"tcpFilter"),ku.desc="is a TCP Flow";function Kp(y){y=new RegExp(y,"i");function T(B){return B.request&&y.test(ko.getContentType(B.request))}return o(T,"requestContentTypeFilter"),T.desc="req. content type matches "+y,T}o(Kp,"requestContentType");function Kf(y){y=new RegExp(y,"i");function T(B){return B.response&&y.test(zs.getContentType(B.response))}return o(T,"responseContentTypeFilter"),T.desc="resp. content type matches "+y,T}o(Kf,"responseContentType");function Ou(y){y=new RegExp(y,"i");function T(B){return B.request&&y.test(ko.getContentType(B.request))||B.response&&y.test(zs.getContentType(B.response))}return o(T,"contentTypeFilter"),T.desc="content type matches "+y,T}o(Ou,"contentType");function ni(y){y=new RegExp(y,"i");function T(B){var W;if(B.type==="dns"){let Fr=(W=B.request)==null?void 0:W.questions[0];return Fr&&y.test(Fr.name)}return B.request&&y.test(ko.pretty_url(B.request))}return o(T,"urlFilter"),T.desc="url matches "+y,T}o(ni,"url");function ii(y){return!!y.websocket}if(o(ii,"websocketFilter"),ii.desc="is a Websocket Flow",kn=_(),kn!==p&&N===l.length)return kn;throw kn!==p&&NAx,icon:()=>ty,method:()=>Mh,path:()=>ry,quickactions:()=>fp,size:()=>ny,status:()=>Ah,time:()=>iy,timestamp:()=>oy,tls:()=>ey});var cr=pe(De());var Jg=pe(Xn());var ey=o(({flow:e})=>cr.default.createElement("td",{className:(0,Jg.default)("col-tls",e.client_conn.tls_established?"col-tls-https":"col-tls-http")}),"tls");ey.headerName="";ey.sortKey=e=>e.type==="http"&&e.request.scheme;var ty=o(({flow:e})=>cr.default.createElement("td",{className:"col-icon"},cr.default.createElement("div",{className:(0,Jg.default)("resource-icon",RT(e))})),"icon");ty.headerName="";ty.sortKey=e=>RT(e);var RT=o(e=>{if(e.type==="tcp"||e.type==="dns")return`resource-icon-${e.type}`;if(e.websocket)return"resource-icon-websocket";if(!e.response)return"resource-icon-plain";var t=zs.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"),FT=o(e=>{var t,n,l,d;switch(e.type){case"http":return ko.pretty_url(e.request);case"tcp":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(m=>`${m.name} ${m.type}`).join(", ")} = ${((d=(l=e.response)==null?void 0:l.answers.map(m=>m.data).join(", "))!=null?d:"...")||"?"}`}},"mainPath"),ry=o(({flow:e})=>{let t;return e.error&&(e.error.msg==="Connection killed."?t=cr.default.createElement("i",{className:"fa fa-fw fa-times pull-right"}):t=cr.default.createElement("i",{className:"fa fa-fw fa-exclamation pull-right"})),cr.default.createElement("td",{className:"col-path"},e.is_replay==="request"&&cr.default.createElement("i",{className:"fa fa-fw fa-repeat pull-right"}),e.intercepted&&cr.default.createElement("i",{className:"fa fa-fw fa-pause pull-right"}),t,cr.default.createElement("span",{className:"marker pull-right"},e.marked),FT(e))},"path");ry.headerName="Path";ry.sortKey=e=>FT(e);var Mh=o(({flow:e})=>cr.default.createElement("td",{className:"col-method"},Mh.sortKey(e)),"method");Mh.headerName="Method";Mh.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 Ah=o(({flow:e})=>{let t="darkred";return e.type!=="http"&&e.type!="dns"||!e.response?cr.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"),cr.default.createElement("td",{className:"col-status",style:{color:t}},Ah.sortKey(e)))},"status");Ah.headerName="Status";Ah.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 ny=o(({flow:e})=>cr.default.createElement("td",{className:"col-size"},Kg(Mx(e))),"size");ny.headerName="Size";ny.sortKey=e=>Mx(e);var iy=o(({flow:e})=>{let t=Ph(e),n=Px(e);return cr.default.createElement("td",{className:"col-time"},t&&n?Gg(1e3*(n-t)):"...")},"time");iy.headerName="Time";iy.sortKey=e=>{let t=Ph(e),n=Px(e);return t&&n&&n-t};var oy=o(({flow:e})=>{let t=Ph(e);return cr.default.createElement("td",{className:"col-timestamp"},t?Qi(t):"...")},"timestamp");oy.headerName="Start time";oy.sortKey=e=>Ph(e);var fp=o(({flow:e})=>{let t=$s(),[n,l]=(0,cr.useState)(!1),d=null;return e.intercepted?d=cr.default.createElement("a",{href:"#",className:"quickaction",onClick:()=>t(cp(e))},cr.default.createElement("i",{className:"fa fa-fw fa-play text-success"})):Qg(e)&&(d=cr.default.createElement("a",{href:"#",className:"quickaction",onClick:()=>t(pp(e))},cr.default.createElement("i",{className:"fa fa-fw fa-repeat text-primary"}))),cr.default.createElement("td",{className:(0,Jg.default)("col-quickactions",{hover:n}),onClick:()=>0},cr.default.createElement("div",null,d))},"quickactions");fp.headerName="";fp.sortKey=e=>0;var Ax={icon:ty,method:Mh,path:ry,quickactions:fp,size:ny,status:Ah,time:iy,timestamp:oy,tls:ey};var fF="FLOWS_ADD",cF="FLOWS_UPDATE",IT="FLOWS_REMOVE",pF="FLOWS_RECEIVE",HT="FLOWS_SELECT",WT="FLOWS_SET_FILTER",BT="FLOWS_SET_SORT",UT="FLOWS_SET_HIGHLIGHT",dF="FLOWS_REQUEST_ACTION",hF=Pe({highlight:void 0,filter:void 0,sort:{column:void 0,desc:!1},selected:[]},Yg);function Dx(e=hF,t){switch(t.type){case fF:case cF:case IT:case pF:let n=Nh[t.cmd](t.data,$T(e.filter),Rx(e.sort)),l=e.selected;if(t.type===IT&&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],m;d===e.view.length-1?m=e.view[d-1]:m=e.view[d+1],l.push(m.id)}}return Pe(It(Pe({},e),{selected:l}),up(e,n));case WT:return Pe(It(Pe({},e),{filter:t.filter}),up(e,kx($T(t.filter),Rx(e.sort))));case UT:return It(Pe({},e),{highlight:t.highlight});case BT:return Pe(It(Pe({},e),{sort:t.sort}),up(e,MT(Rx(t.sort))));case HT:return It(Pe({},e),{selected:t.flowIds});default:return e}}o(Dx,"reducer");function $T(e){if(!!e)return gf.parse(e)}o($T,"makeFilter");function Rx({column:e,desc:t}){if(!e)return(l,d)=>0;let n=Ax[e].sortKey;return(l,d)=>{let m=n(l),p=n(d);return m>p?t?-1:1:mOt(`/flows/${e.id}/resume`,{method:"POST"})}o(cp,"resume");function ay(){return e=>Ot("/flows/resume",{method:"POST"})}o(ay,"resumeAll");function uy(e){return t=>Ot(`/flows/${e.id}/kill`,{method:"POST"})}o(uy,"kill");function jT(){return e=>Ot("/flows/kill",{method:"POST"})}o(jT,"killAll");function fy(e){return t=>Ot(`/flows/${e.id}`,{method:"DELETE"})}o(fy,"remove");function cy(e){return t=>Ot(`/flows/${e.id}/duplicate`,{method:"POST"})}o(cy,"duplicate");function pp(e){return t=>Ot(`/flows/${e.id}/replay`,{method:"POST"})}o(pp,"replay");function py(e){return t=>Ot(`/flows/${e.id}/revert`,{method:"POST"})}o(py,"revert");function Ri(e,t){return n=>Ot.put(`/flows/${e.id}`,t)}o(Ri,"update");function qT(e,t,n){let l=new FormData;return t=new window.Blob([t],{type:"plain/text"}),l.append("file",t),d=>Ot(`/flows/${e.id}/${n}/content.data`,{method:"POST",body:l})}o(qT,"uploadContent");function dy(){return e=>Ot("/clear",{method:"POST"})}o(dy,"clear");function VT(){return window.location.href="/flows/dump",{type:dF}}o(VT,"download");function KT(e){let t=new FormData;return t.append("file",e),n=>Ot("/flows/dump",{method:"POST",body:t})}o(KT,"upload");function wf(e){return{type:HT,flowIds:e?[e]:[]}}o(wf,"select");var hy="UI_HIDE_MODAL",GT="UI_SET_ACTIVE_MODAL",mF={activeModal:void 0};function Fx(e=mF,t){switch(t.type){case GT:return It(Pe({},e),{activeModal:t.activeModal});case hy:return It(Pe({},e),{activeModal:void 0});default:return e}}o(Fx,"reducer");function YT(e){return{type:GT,activeModal:e}}o(YT,"setActiveModal");function my(){return{type:hy}}o(my,"hideModal");var Xh=pe(De());var Bt=pe(De());var hp=pe(De());var Rh=pe(De()),XT=pe(Xn()),QT=(()=>{let e=document.createElement("div");return e.setAttribute("contenteditable","PLAINTEXT-ONLY"),e.contentEditable==="plaintext-only"?"plaintext-only":"true"})(),dp=!1,js=class extends Rh.Component{constructor(){super(...arguments);this.input=Rh.default.createRef();this.isEditing=o(()=>{var t;return((t=this.input.current)==null?void 0:t.contentEditable)===QT},"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=QT,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=>{dp&&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());dp&&console.warn("mouseUp",this.suppress_events,n,l),n&&l&&this.startEditing(),this.suppress_events=!1},"onMouseUp");this.onClick=o(t=>{dp&&console.debug("onClick",this.suppress_events)},"onClick");this.onFocus=o(t=>{if(dp&&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=>{dp&&console.debug("onBlur",this.props.content,this.suppress_events),!this.suppress_events&&this.finishEditing()},"onBlur");this.onKeyDown=o(t=>{var n,l;switch(dp&&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,XT.default)("inline-input",this.props.className);return Rh.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(js,"ValueEditor");var ZT=pe(Xn());function xf(e){let[t,n]=(0,hp.useState)(e.isValid(e.content)),l=(0,hp.useRef)(null),d=o(p=>{var x;e.isValid(p)?e.onEditDone(p):(x=l.current)==null||x.resetValue()},"onEditDone"),m=(0,ZT.default)(e.className,t?"has-success":"has-warning");return hp.default.createElement(js,It(Pe({},e),{className:m,onInput:p=>n(e.isValid(p)),onEditDone:d,ref:l}))}o(xf,"ValidateEditor");function Ix(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}o(Ix,"_defineProperty");function JT(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(JT,"ownKeys");function vy(e){for(var t=1;tn[l.level])));case rk:case yF:return Pe(Pe({},e),up(e,Nh[t.cmd](t.data,l=>e.filters[l.level])));default:return e}}o(Ux,"reduce");function ok(e){return{type:ik,filter:e}}o(ok,"toggleFilter");function mp(){return{type:nk}}o(mp,"toggleVisibility");function sk(e,t="web"){let n={id:Math.random().toString(),message:e,level:t};return{type:rk,cmd:"add",data:n}}o(sk,"add");var lk="UI_OPTION_UPDATE_START",ak="UI_OPTION_UPDATE_SUCCESS",uk="UI_OPTION_UPDATE_ERROR",xF={};function $x(e=xF,t){switch(t.type){case lk:return It(Pe({},e),{[t.option]:{isUpdating:!0,value:t.value,error:!1}});case ak:return It(Pe({},e),{[t.option]:void 0});case uk:let n=e[t.option].value;return typeof n=="boolean"&&(n=!n),It(Pe({},e),{[t.option]:{value:n,isUpdating:!1,error:t.error}});case hy:return{};default:return e}}o($x,"reducer");function fk(e,t){return{type:lk,option:e,value:t}}o(fk,"startUpdate");function ck(e){return{type:ak,option:e}}o(ck,"updateSuccess");function pk(e,t){return{type:uk,option:e,error:t}}o(pk,"updateError");var dk=yy({flow:gx,modal:Fx,optionsEditor:$x});var Zn;(function(m){m.INIT="CONNECTION_INIT",m.FETCHING="CONNECTION_FETCHING",m.ESTABLISHED="CONNECTION_ESTABLISHED",m.ERROR="CONNECTION_ERROR",m.OFFLINE="CONNECTION_OFFLINE"})(Zn||(Zn={}));var SF={state:Zn.INIT,message:void 0};function zx(e=SF,t){switch(t.type){case Zn.ESTABLISHED:case Zn.FETCHING:case Zn.ERROR:case Zn.OFFLINE:return{state:t.type,message:t.message};default:return e}}o(zx,"reducer");function hk(){return{type:Zn.FETCHING}}o(hk,"startFetching");function mk(){return{type:Zn.ESTABLISHED}}o(mk,"connectionEstablished");function vk(e){return{type:Zn.ERROR,message:e}}o(vk,"connectionError");var gk={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",connection_strategy:"eager",console_focus_follow:!1,content_view_lines_cutoff:512,dns_listen_host:"",dns_listen_port:53,dns_mode:"regular",dns_server:!1,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:8080,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,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",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 jx="OPTIONS_RECEIVE",qx="OPTIONS_UPDATE";function Vx(e=gk,t){switch(t.type){case jx:let n={};for(let[d,{value:m}]of Object.entries(t.data))n[d]=m;return n;case qx:let l=Pe({},e);for(let[d,{value:m}]of Object.entries(t.data))l[d]=m;return l;default:return e}}o(Vx,"reducer");function CF(e,t,n){return Na(this,null,function*(){try{let l=yield Ot.put("/options",{[e]:t});if(l.status===200)n(ck(e));else throw yield l.text()}catch(l){n(pk(e,l))}})}o(CF,"pureSendUpdate");var _F=CF;function vp(e,t){return n=>{n(fk(e,t)),_F(e,t,n)}}o(vp,"update");function yk(){return e=>Ot("/options/save",{method:"POST"})}o(yk,"save");var wk="COMMANDBAR_TOGGLE_VISIBILITY",bF={visible:!1};function Kx(e=bF,t){switch(t.type){case wk:return It(Pe({},e),{visible:!e.visible});default:return e}}o(Kx,"reducer");function wy(){return{type:wk}}o(wy,"toggleVisibility");function xk(e){return function(t){var n=t.dispatch,l=t.getState;return function(d){return function(m){return typeof m=="function"?m(n,l,e):d(m)}}}}o(xk,"createThunkMiddleware");var Sk=xk();Sk.withExtraArgument=xk;var Ck=Sk;var EF=window.MITMWEB_CONF||{static:!1,version:"1.2.3",contentViews:["Auto","Raw"]};function Gx(e=EF,t){return e}o(Gx,"reducer");var TF={},kF=o((e=TF,t)=>{switch(t.type){case jx:return t.data;case qx:return Pe(Pe({},e),t.data);default:return e}},"reducer"),_k=kF;var OF=window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__||Bx,LF=yy({commandBar:Kx,eventLog:Ux,flows:Dx,connection:zx,ui:dk,options:Vx,options_meta:_k,conf:Gx}),NF=o(e=>Wx(LF,e,OF(tk(Ck))),"createAppStore"),gp=NF(void 0),Vt=o(()=>$s(),"useAppDispatch"),it=mx;var Zi=pe(De());var bk=pe(Oh()),Ek=pe(Xn()),Yx=class extends Zi.Component{constructor(){super(...arguments);this.container=Zi.default.createRef();this.nameInput=Zi.default.createRef();this.valueInput=Zi.default.createRef();this.render=o(()=>{let[t,n]=this.props.item;return Zi.default.createElement("div",{ref:this.container,className:"kv-row",onClick:this.onClick,onKeyDownCapture:this.onKeyDown},Zi.default.createElement(js,{ref:this.nameInput,className:"kv-key",content:t,onEditStart:this.props.onEditStart,onEditDone:l=>this.props.onEditDone([l,n])}),":\xA0",Zi.default.createElement(js,{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(Yx,"Row");var yp=class extends Zi.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)=>Zi.default.createElement(Yx,{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 Zi.default.createElement("div",{className:(0,Ek.default)("kv-editor",this.props.className),onMouseDown:this.onMouseDown},t,Zi.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,bk.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(yp,"KeyValueListEditor");var Xt=pe(De());var Fh=pe(De());var xy=80;function Sy(e,t){let[n,l]=(0,Fh.useState)(),[d,m]=(0,Fh.useState)();return(0,Fh.useEffect)(()=>{d&&d.abort();let p=new AbortController;return Ot(e,{signal:p.signal}).then(x=>{if(!x.ok)throw`${x.status} ${x.statusText}`.trim();return x.text()}).then(x=>{l(x)}).catch(x=>{p.signal.aborted||l(`Error getting content: ${x}.`)}),m(p),()=>{p.signal.aborted||p.abort()}},[e,t]),n}o(Sy,"useContent");var Ih=pe(De()),Cy=Ih.default.memo(o(function({icon:t,text:n,className:l,title:d,onOpenFile:m,onClick:p}){let x;return Ih.default.createElement("a",{href:"#",onClick:_=>{x.click(),p&&p(_)},className:l,title:d},Ih.default.createElement("i",{className:"fa fa-fw "+t}),n,Ih.default.createElement("input",{ref:_=>x=_,className:"hidden",type:"file",onChange:_=>{_.preventDefault(),_.target.files&&_.target.files.length>0&&m(_.target.files[0]),x.value=""}}))},"FileChooser"));var wp=pe(De()),Tk=pe(Xn());function Cr({onClick:e,children:t,icon:n,disabled:l,className:d,title:m}){return wp.createElement("button",{className:(0,Tk.default)(d,"btn btn-default"),onClick:l?void 0:e,disabled:l,title:m},n&&wp.createElement(wp.Fragment,null,wp.createElement("i",{className:"fa "+n}),"\xA0"),t)}o(Cr,"Button");var Wh=pe(De()),Mk=pe(De());var Hh=pe(De()),Ok=pe(Xn()),Lk=pe(kk()),Nk=pe(Oh());function Pk(e){return e&&e.replace(/\r\n|\r/g,` -`)}o(Pk,"normalizeLineEndings");var xp=class extends Hh.Component{constructor(t){super(t);this.state={isFocused:!1}}getCodeMirrorInstance(){return this.props.codeMirrorInstance||Lk.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&&Pk(this.codeMirror.getValue())!==Pk(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);Nk.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,Ok.default)("ReactCodeMirror",this.state.isFocused?"ReactCodeMirror--focused":null,this.props.className);return Hh.createElement("div",{className:t},Hh.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(xp,"CodeMirror"),xp.defaultProps={preserveScrollPosition:!1};var Bh=class extends Mk.Component{constructor(){super(...arguments);this.editor=Wh.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 Wh.createElement("div",{className:"codeeditor",onKeyDown:n=>n.stopPropagation()},Wh.createElement(xp,{ref:this.editor,value:this.props.initialContent,onChange:()=>0,options:t}))},"render")}};o(Bh,"CodeEditor");var Sf=pe(De()),PF=Sf.default.memo(o(function({lines:t,maxLines:n,showMore:l}){return t.length===0?null:Sf.default.createElement("pre",null,t.map((d,m)=>m===n?Sf.default.createElement("button",{key:"showmore",onClick:l,className:"btn btn-xs btn-info"},Sf.default.createElement("i",{className:"fa fa-angle-double-down","aria-hidden":"true"})," Show more"):Sf.default.createElement("div",{key:m},d.map(([p,x],_)=>Sf.default.createElement("span",{key:_,className:p},x)))))},"LineRenderer")),_y=PF;var Of=pe(De());var di=pe(De());var by=pe(De());var Zx=o(function(t){return t.reduce(function(n,l){var d=l[0],m=l[1];return n[d]=m,n},{})},"fromEntries"),Jx=typeof window!="undefined"&&window.document&&window.document.createElement?by.useLayoutEffect:by.useEffect;var iu=pe(De());var $r="top",Cn="bottom",tn="right",rn="left",Ey="auto",eu=[$r,Cn,tn,rn],Rl="start",Ty="end",Ak="clippingParents",ky="viewport",Sp="popper",Dk="reference",eS=eu.reduce(function(e,t){return e.concat([t+"-"+Rl,t+"-"+Ty])},[]),Oy=[].concat(eu,[Ey]).reduce(function(e,t){return e.concat([t,t+"-"+Rl,t+"-"+Ty])},[]),MF="beforeRead",AF="read",DF="afterRead",RF="beforeMain",FF="main",IF="afterMain",HF="beforeWrite",WF="write",BF="afterWrite",Rk=[MF,AF,DF,RF,FF,IF,HF,WF,BF];function _n(e){return e?(e.nodeName||"").toLowerCase():null}o(_n,"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}o(Mr,"getWindow");function Fl(e){var t=Mr(e).Element;return e instanceof t||e instanceof Element}o(Fl,"isElement");function zr(e){var t=Mr(e).HTMLElement;return e instanceof t||e instanceof HTMLElement}o(zr,"isHTMLElement");function Ly(e){if(typeof ShadowRoot=="undefined")return!1;var t=Mr(e).ShadowRoot;return e instanceof t||e instanceof ShadowRoot}o(Ly,"isShadowRoot");function UF(e){var t=e.state;Object.keys(t.elements).forEach(function(n){var l=t.styles[n]||{},d=t.attributes[n]||{},m=t.elements[n];!zr(m)||!_n(m)||(Object.assign(m.style,l),Object.keys(d).forEach(function(p){var x=d[p];x===!1?m.removeAttribute(p):m.setAttribute(p,x===!0?"":x)}))})}o(UF,"applyStyles");function $F(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],m=t.attributes[l]||{},p=Object.keys(t.styles.hasOwnProperty(l)?t.styles[l]:n[l]),x=p.reduce(function(_,O){return _[O]="",_},{});!zr(d)||!_n(d)||(Object.assign(d.style,x),Object.keys(m).forEach(function(_){d.removeAttribute(_)}))})}}o($F,"effect");var Fk={name:"applyStyles",enabled:!0,phase:"write",fn:UF,effect:$F,requires:["computeStyles"]};function bn(e){return e.split("-")[0]}o(bn,"getBasePlacement");var tu=Math.round;function Oo(e,t){t===void 0&&(t=!1);var n=e.getBoundingClientRect(),l=1,d=1;return zr(e)&&t&&(l=n.width/e.offsetWidth||1,d=n.height/e.offsetHeight||1),{width:tu(n.width/l),height:tu(n.height/d),top:tu(n.top/d),right:tu(n.right/l),bottom:tu(n.bottom/d),left:tu(n.left/l),x:tu(n.left/l),y:tu(n.top/d)}}o(Oo,"getBoundingClientRect");function Cf(e){var t=Oo(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(Cf,"getLayoutRect");function Uh(e,t){var n=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(n&&Ly(n)){var l=t;do{if(l&&e.isSameNode(l))return!0;l=l.parentNode||l.host}while(l)}return!1}o(Uh,"contains");function pi(e){return Mr(e).getComputedStyle(e)}o(pi,"getComputedStyle");function tS(e){return["table","td","th"].indexOf(_n(e))>=0}o(tS,"isTableElement");function Dn(e){return((Fl(e)?e.ownerDocument:e.document)||window.document).documentElement}o(Dn,"getDocumentElement");function Il(e){return _n(e)==="html"?e:e.assignedSlot||e.parentNode||(Ly(e)?e.host:null)||Dn(e)}o(Il,"getParentNode");function Ik(e){return!zr(e)||pi(e).position==="fixed"?null:e.offsetParent}o(Ik,"getTrueOffsetParent");function zF(e){var t=navigator.userAgent.toLowerCase().indexOf("firefox")!==-1,n=navigator.userAgent.indexOf("Trident")!==-1;if(n&&zr(e)){var l=pi(e);if(l.position==="fixed")return null}for(var d=Il(e);zr(d)&&["html","body"].indexOf(_n(d))<0;){var m=pi(d);if(m.transform!=="none"||m.perspective!=="none"||m.contain==="paint"||["transform","perspective"].indexOf(m.willChange)!==-1||t&&m.willChange==="filter"||t&&m.filter&&m.filter!=="none")return d;d=d.parentNode}return null}o(zF,"getContainingBlock");function es(e){for(var t=Mr(e),n=Ik(e);n&&tS(n)&&pi(n).position==="static";)n=Ik(n);return n&&(_n(n)==="html"||_n(n)==="body"&&pi(n).position==="static")?t:n||zF(e)||t}o(es,"getOffsetParent");function _f(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}o(_f,"getMainAxisFromPlacement");var Lo=Math.max,ru=Math.min,$h=Math.round;function bf(e,t,n){return Lo(e,ru(t,n))}o(bf,"within");function zh(){return{top:0,right:0,bottom:0,left:0}}o(zh,"getFreshSideObject");function jh(e){return Object.assign({},zh(),e)}o(jh,"mergePaddingObject");function qh(e,t){return t.reduce(function(n,l){return n[l]=e,n},{})}o(qh,"expandToHashMap");var jF=o(function(t,n){return t=typeof t=="function"?t(Object.assign({},n.rects,{placement:n.placement})):t,jh(typeof t!="number"?t:qh(t,eu))},"toPaddingObject");function qF(e){var t,n=e.state,l=e.name,d=e.options,m=n.elements.arrow,p=n.modifiersData.popperOffsets,x=bn(n.placement),_=_f(x),O=[rn,tn].indexOf(x)>=0,D=O?"height":"width";if(!(!m||!p)){var Y=jF(d.padding,n),U=Cf(m),X=_==="y"?$r:rn,te=_==="y"?Cn:tn,Q=n.rects.reference[D]+n.rects.reference[_]-p[_]-n.rects.popper[D],F=p[_]-n.rects.reference[_],M=es(m),R=M?_==="y"?M.clientHeight||0:M.clientWidth||0:0,K=Q/2-F/2,V=Y[X],ue=R-U[D]-Y[te],ie=R/2-U[D]/2+K,de=bf(V,ie,ue),ge=_;n.modifiersData[l]=(t={},t[ge]=de,t.centerOffset=de-ie,t)}}o(qF,"arrow");function VF(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)||!Uh(t.elements.popper,d)||(t.elements.arrow=d))}o(VF,"effect");var Hk={name:"arrow",enabled:!0,phase:"main",fn:qF,effect:VF,requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};var KF={top:"auto",right:"auto",bottom:"auto",left:"auto"};function GF(e){var t=e.x,n=e.y,l=window,d=l.devicePixelRatio||1;return{x:$h($h(t*d)/d)||0,y:$h($h(n*d)/d)||0}}o(GF,"roundOffsetsByDPR");function Wk(e){var t,n=e.popper,l=e.popperRect,d=e.placement,m=e.offsets,p=e.position,x=e.gpuAcceleration,_=e.adaptive,O=e.roundOffsets,D=O===!0?GF(m):typeof O=="function"?O(m):m,Y=D.x,U=Y===void 0?0:Y,X=D.y,te=X===void 0?0:X,Q=m.hasOwnProperty("x"),F=m.hasOwnProperty("y"),M=rn,R=$r,K=window;if(_){var V=es(n),ue="clientHeight",ie="clientWidth";V===Mr(n)&&(V=Dn(n),pi(V).position!=="static"&&(ue="scrollHeight",ie="scrollWidth")),V=V,d===$r&&(R=Cn,te-=V[ue]-l.height,te*=x?1:-1),d===rn&&(M=tn,U-=V[ie]-l.width,U*=x?1:-1)}var de=Object.assign({position:p},_&&KF);if(x){var ge;return Object.assign({},de,(ge={},ge[R]=F?"0":"",ge[M]=Q?"0":"",ge.transform=(K.devicePixelRatio||1)<2?"translate("+U+"px, "+te+"px)":"translate3d("+U+"px, "+te+"px, 0)",ge))}return Object.assign({},de,(t={},t[R]=F?te+"px":"",t[M]=Q?U+"px":"",t.transform="",t))}o(Wk,"mapToStyles");function YF(e){var t=e.state,n=e.options,l=n.gpuAcceleration,d=l===void 0?!0:l,m=n.adaptive,p=m===void 0?!0:m,x=n.roundOffsets,_=x===void 0?!0:x;if(!1)var O;var D={placement:bn(t.placement),popper:t.elements.popper,popperRect:t.rects.popper,gpuAcceleration:d};t.modifiersData.popperOffsets!=null&&(t.styles.popper=Object.assign({},t.styles.popper,Wk(Object.assign({},D,{offsets:t.modifiersData.popperOffsets,position:t.options.strategy,adaptive:p,roundOffsets:_})))),t.modifiersData.arrow!=null&&(t.styles.arrow=Object.assign({},t.styles.arrow,Wk(Object.assign({},D,{offsets:t.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:_})))),t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-placement":t.placement})}o(YF,"computeStyles");var Bk={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:YF,data:{}};var Ny={passive:!0};function XF(e){var t=e.state,n=e.instance,l=e.options,d=l.scroll,m=d===void 0?!0:d,p=l.resize,x=p===void 0?!0:p,_=Mr(t.elements.popper),O=[].concat(t.scrollParents.reference,t.scrollParents.popper);return m&&O.forEach(function(D){D.addEventListener("scroll",n.update,Ny)}),x&&_.addEventListener("resize",n.update,Ny),function(){m&&O.forEach(function(D){D.removeEventListener("scroll",n.update,Ny)}),x&&_.removeEventListener("resize",n.update,Ny)}}o(XF,"effect");var Uk={name:"eventListeners",enabled:!0,phase:"write",fn:o(function(){},"fn"),effect:XF,data:{}};var QF={left:"right",right:"left",bottom:"top",top:"bottom"};function Cp(e){return e.replace(/left|right|bottom|top/g,function(t){return QF[t]})}o(Cp,"getOppositePlacement");var ZF={start:"end",end:"start"};function Py(e){return e.replace(/start|end/g,function(t){return ZF[t]})}o(Py,"getOppositeVariationPlacement");function Ef(e){var t=Mr(e),n=t.pageXOffset,l=t.pageYOffset;return{scrollLeft:n,scrollTop:l}}o(Ef,"getWindowScroll");function Tf(e){return Oo(Dn(e)).left+Ef(e).scrollLeft}o(Tf,"getWindowScrollBarX");function rS(e){var t=Mr(e),n=Dn(e),l=t.visualViewport,d=n.clientWidth,m=n.clientHeight,p=0,x=0;return l&&(d=l.width,m=l.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(p=l.offsetLeft,x=l.offsetTop)),{width:d,height:m,x:p+Tf(e),y:x}}o(rS,"getViewportRect");function nS(e){var t,n=Dn(e),l=Ef(e),d=(t=e.ownerDocument)==null?void 0:t.body,m=Lo(n.scrollWidth,n.clientWidth,d?d.scrollWidth:0,d?d.clientWidth:0),p=Lo(n.scrollHeight,n.clientHeight,d?d.scrollHeight:0,d?d.clientHeight:0),x=-l.scrollLeft+Tf(e),_=-l.scrollTop;return pi(d||n).direction==="rtl"&&(x+=Lo(n.clientWidth,d?d.clientWidth:0)-m),{width:m,height:p,x,y:_}}o(nS,"getDocumentRect");function kf(e){var t=pi(e),n=t.overflow,l=t.overflowX,d=t.overflowY;return/auto|scroll|overlay|hidden/.test(n+d+l)}o(kf,"isScrollParent");function My(e){return["html","body","#document"].indexOf(_n(e))>=0?e.ownerDocument.body:zr(e)&&kf(e)?e:My(Il(e))}o(My,"getScrollParent");function nu(e,t){var n;t===void 0&&(t=[]);var l=My(e),d=l===((n=e.ownerDocument)==null?void 0:n.body),m=Mr(l),p=d?[m].concat(m.visualViewport||[],kf(l)?l:[]):l,x=t.concat(p);return d?x:x.concat(nu(Il(p)))}o(nu,"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 JF(e){var t=Oo(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(JF,"getInnerBoundingClientRect");function $k(e,t){return t===ky?_p(rS(e)):zr(t)?JF(t):_p(nS(Dn(e)))}o($k,"getClientRectFromMixedType");function eI(e){var t=nu(Il(e)),n=["absolute","fixed"].indexOf(pi(e).position)>=0,l=n&&zr(e)?es(e):e;return Fl(l)?t.filter(function(d){return Fl(d)&&Uh(d,l)&&_n(d)!=="body"}):[]}o(eI,"getClippingParents");function iS(e,t,n){var l=t==="clippingParents"?eI(e):[].concat(t),d=[].concat(l,[n]),m=d[0],p=d.reduce(function(x,_){var O=$k(e,_);return x.top=Lo(O.top,x.top),x.right=ru(O.right,x.right),x.bottom=ru(O.bottom,x.bottom),x.left=Lo(O.left,x.left),x},$k(e,m));return p.width=p.right-p.left,p.height=p.bottom-p.top,p.x=p.left,p.y=p.top,p}o(iS,"getClippingRect");function qs(e){return e.split("-")[1]}o(qs,"getVariation");function Vh(e){var t=e.reference,n=e.element,l=e.placement,d=l?bn(l):null,m=l?qs(l):null,p=t.x+t.width/2-n.width/2,x=t.y+t.height/2-n.height/2,_;switch(d){case $r:_={x:p,y:t.y-n.height};break;case Cn:_={x:p,y:t.y+t.height};break;case tn:_={x:t.x+t.width,y:x};break;case rn:_={x:t.x-n.width,y:x};break;default:_={x:t.x,y:t.y}}var O=d?_f(d):null;if(O!=null){var D=O==="y"?"height":"width";switch(m){case Rl:_[O]=_[O]-(t[D]/2-n[D]/2);break;case Ty:_[O]=_[O]+(t[D]/2-n[D]/2);break;default:}}return _}o(Vh,"computeOffsets");function ts(e,t){t===void 0&&(t={});var n=t,l=n.placement,d=l===void 0?e.placement:l,m=n.boundary,p=m===void 0?Ak:m,x=n.rootBoundary,_=x===void 0?ky:x,O=n.elementContext,D=O===void 0?Sp:O,Y=n.altBoundary,U=Y===void 0?!1:Y,X=n.padding,te=X===void 0?0:X,Q=jh(typeof te!="number"?te:qh(te,eu)),F=D===Sp?Dk:Sp,M=e.elements.reference,R=e.rects.popper,K=e.elements[U?F:D],V=iS(Fl(K)?K:K.contextElement||Dn(e.elements.popper),p,_),ue=Oo(M),ie=Vh({reference:ue,element:R,strategy:"absolute",placement:d}),de=_p(Object.assign({},R,ie)),ge=D===Sp?de:ue,xe={top:V.top-ge.top+Q.top,bottom:ge.bottom-V.bottom+Q.bottom,left:V.left-ge.left+Q.left,right:ge.right-V.right+Q.right},qe=e.modifiersData.offset;if(D===Sp&&qe){var et=qe[d];Object.keys(xe).forEach(function(Te){var xt=[tn,Cn].indexOf(Te)>=0?1:-1,Ue=[$r,Cn].indexOf(Te)>=0?"y":"x";xe[Te]+=et[Ue]*xt})}return xe}o(ts,"detectOverflow");function oS(e,t){t===void 0&&(t={});var n=t,l=n.placement,d=n.boundary,m=n.rootBoundary,p=n.padding,x=n.flipVariations,_=n.allowedAutoPlacements,O=_===void 0?Oy:_,D=qs(l),Y=D?x?eS:eS.filter(function(te){return qs(te)===D}):eu,U=Y.filter(function(te){return O.indexOf(te)>=0});U.length===0&&(U=Y);var X=U.reduce(function(te,Q){return te[Q]=ts(e,{placement:Q,boundary:d,rootBoundary:m,padding:p})[bn(Q)],te},{});return Object.keys(X).sort(function(te,Q){return X[te]-X[Q]})}o(oS,"computeAutoPlacement");function tI(e){if(bn(e)===Ey)return[];var t=Cp(e);return[Py(e),t,Py(t)]}o(tI,"getExpandedFallbackPlacements");function rI(e){var t=e.state,n=e.options,l=e.name;if(!t.modifiersData[l]._skip){for(var d=n.mainAxis,m=d===void 0?!0:d,p=n.altAxis,x=p===void 0?!0:p,_=n.fallbackPlacements,O=n.padding,D=n.boundary,Y=n.rootBoundary,U=n.altBoundary,X=n.flipVariations,te=X===void 0?!0:X,Q=n.allowedAutoPlacements,F=t.options.placement,M=bn(F),R=M===F,K=_||(R||!te?[Cp(F)]:tI(F)),V=[F].concat(K).reduce(function(at,_r){return at.concat(bn(_r)===Ey?oS(t,{placement:_r,boundary:D,rootBoundary:Y,padding:O,flipVariations:te,allowedAutoPlacements:Q}):_r)},[]),ue=t.rects.reference,ie=t.rects.popper,de=new Map,ge=!0,xe=V[0],qe=0;qe=0,Ve=Ue?"width":"height",Ke=ts(t,{placement:et,boundary:D,rootBoundary:Y,altBoundary:U,padding:O}),Ye=Ue?xt?tn:rn:xt?Cn:$r;ue[Ve]>ie[Ve]&&(Ye=Cp(Ye));var Qt=Cp(Ye),ft=[];if(m&&ft.push(Ke[Te]<=0),x&&ft.push(Ke[Ye]<=0,Ke[Qt]<=0),ft.every(function(at){return at})){xe=et,ge=!1;break}de.set(et,ft)}if(ge)for(var Ar=te?3:1,Kt=o(function(_r){var Ut=V.find(function($t){var ne=de.get($t);if(ne)return ne.slice(0,_r).every(function(tt){return tt})});if(Ut)return xe=Ut,"break"},"_loop"),Et=Ar;Et>0;Et--){var St=Kt(Et);if(St==="break")break}t.placement!==xe&&(t.modifiersData[l]._skip=!0,t.placement=xe,t.reset=!0)}}o(rI,"flip");var zk={name:"flip",enabled:!0,phase:"main",fn:rI,requiresIfExists:["offset"],data:{_skip:!1}};function jk(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(jk,"getSideOffsets");function qk(e){return[$r,tn,Cn,rn].some(function(t){return e[t]>=0})}o(qk,"isAnySideFullyClipped");function nI(e){var t=e.state,n=e.name,l=t.rects.reference,d=t.rects.popper,m=t.modifiersData.preventOverflow,p=ts(t,{elementContext:"reference"}),x=ts(t,{altBoundary:!0}),_=jk(p,l),O=jk(x,d,m),D=qk(_),Y=qk(O);t.modifiersData[n]={referenceClippingOffsets:_,popperEscapeOffsets:O,isReferenceHidden:D,hasPopperEscaped:Y},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":D,"data-popper-escaped":Y})}o(nI,"hide");var Vk={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:nI};function iI(e,t,n){var l=bn(e),d=[rn,$r].indexOf(l)>=0?-1:1,m=typeof n=="function"?n(Object.assign({},t,{placement:e})):n,p=m[0],x=m[1];return p=p||0,x=(x||0)*d,[rn,tn].indexOf(l)>=0?{x,y:p}:{x:p,y:x}}o(iI,"distanceAndSkiddingToXY");function oI(e){var t=e.state,n=e.options,l=e.name,d=n.offset,m=d===void 0?[0,0]:d,p=Oy.reduce(function(D,Y){return D[Y]=iI(Y,t.rects,m),D},{}),x=p[t.placement],_=x.x,O=x.y;t.modifiersData.popperOffsets!=null&&(t.modifiersData.popperOffsets.x+=_,t.modifiersData.popperOffsets.y+=O),t.modifiersData[l]=p}o(oI,"offset");var Kk={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:oI};function sI(e){var t=e.state,n=e.name;t.modifiersData[n]=Vh({reference:t.rects.reference,element:t.rects.popper,strategy:"absolute",placement:t.placement})}o(sI,"popperOffsets");var Gk={name:"popperOffsets",enabled:!0,phase:"read",fn:sI,data:{}};function sS(e){return e==="x"?"y":"x"}o(sS,"getAltAxis");function lI(e){var t=e.state,n=e.options,l=e.name,d=n.mainAxis,m=d===void 0?!0:d,p=n.altAxis,x=p===void 0?!1:p,_=n.boundary,O=n.rootBoundary,D=n.altBoundary,Y=n.padding,U=n.tether,X=U===void 0?!0:U,te=n.tetherOffset,Q=te===void 0?0:te,F=ts(t,{boundary:_,rootBoundary:O,padding:Y,altBoundary:D}),M=bn(t.placement),R=qs(t.placement),K=!R,V=_f(M),ue=sS(V),ie=t.modifiersData.popperOffsets,de=t.rects.reference,ge=t.rects.popper,xe=typeof Q=="function"?Q(Object.assign({},t.rects,{placement:t.placement})):Q,qe={x:0,y:0};if(!!ie){if(m||x){var et=V==="y"?$r:rn,Te=V==="y"?Cn:tn,xt=V==="y"?"height":"width",Ue=ie[V],Ve=ie[V]+F[et],Ke=ie[V]-F[Te],Ye=X?-ge[xt]/2:0,Qt=R===Rl?de[xt]:ge[xt],ft=R===Rl?-ge[xt]:-de[xt],Ar=t.elements.arrow,Kt=X&&Ar?Cf(Ar):{width:0,height:0},Et=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:zh(),St=Et[et],at=Et[Te],_r=bf(0,de[xt],Kt[xt]),Ut=K?de[xt]/2-Ye-_r-St-xe:Qt-_r-St-xe,$t=K?-de[xt]/2+Ye+_r+at+xe:ft+_r+at+xe,ne=t.elements.arrow&&es(t.elements.arrow),tt=ne?V==="y"?ne.clientTop||0:ne.clientLeft||0:0,br=t.modifiersData.offset?t.modifiersData.offset[t.placement][V]:0,jt=ie[V]+Ut-br-tt,qt=ie[V]+$t-br;if(m){var Se=bf(X?ru(Ve,jt):Ve,Ue,X?Lo(Ke,qt):Ke);ie[V]=Se,qe[V]=Se-Ue}if(x){var Er=V==="x"?$r:rn,nn=V==="x"?Cn:tn,Fn=ie[ue],ei=Fn+F[Er],on=Fn-F[nn],Gt=bf(X?ru(ei,jt):ei,Fn,X?Lo(on,qt):on);ie[ue]=Gt,qe[ue]=Gt-Fn}}t.modifiersData[l]=qe}}o(lI,"preventOverflow");var Yk={name:"preventOverflow",enabled:!0,phase:"main",fn:lI,requiresIfExists:["offset"]};function lS(e){return{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}}o(lS,"getHTMLElementScroll");function aS(e){return e===Mr(e)||!zr(e)?Ef(e):lS(e)}o(aS,"getNodeScroll");function aI(e){var t=e.getBoundingClientRect(),n=t.width/e.offsetWidth||1,l=t.height/e.offsetHeight||1;return n!==1||l!==1}o(aI,"isElementScaled");function uS(e,t,n){n===void 0&&(n=!1);var l=zr(t),d=zr(t)&&aI(t),m=Dn(t),p=Oo(e,d),x={scrollLeft:0,scrollTop:0},_={x:0,y:0};return(l||!l&&!n)&&((_n(t)!=="body"||kf(m))&&(x=aS(t)),zr(t)?(_=Oo(t,!0),_.x+=t.clientLeft,_.y+=t.clientTop):m&&(_.x=Tf(m))),{x:p.left+x.scrollLeft-_.x,y:p.top+x.scrollTop-_.y,width:p.width,height:p.height}}o(uS,"getCompositeRect");function uI(e){var t=new Map,n=new Set,l=[];e.forEach(function(m){t.set(m.name,m)});function d(m){n.add(m.name);var p=[].concat(m.requires||[],m.requiresIfExists||[]);p.forEach(function(x){if(!n.has(x)){var _=t.get(x);_&&d(_)}}),l.push(m)}return o(d,"sort"),e.forEach(function(m){n.has(m.name)||d(m)}),l}o(uI,"order");function fS(e){var t=uI(e);return Rk.reduce(function(n,l){return n.concat(t.filter(function(d){return d.phase===l}))},[])}o(fS,"orderModifiers");function cS(e){var t;return function(){return t||(t=new Promise(function(n){Promise.resolve().then(function(){t=void 0,n(e())})})),t}}o(cS,"debounce");function pS(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(pS,"mergeByName");var Xk={placement:"bottom",modifiers:[],strategy:"absolute"};function Qk(){for(var e=arguments.length,t=new Array(e),n=0;ndi.default.createElement("li",{role:"separator",className:"divider"}),"Divider");function hi(l){var d=l,{onClick:e,children:t}=d,n=Ds(d,["onClick","children"]);return di.default.createElement("li",null,di.default.createElement("a",Pe({href:"#",onClick:o(p=>{p.preventDefault(),e()},"click")},n),t))}o(hi,"MenuItem");var ou=di.default.memo(o(function(x){var _=x,{text:t,children:n,options:l,className:d,onOpen:m}=_,p=Ds(_,["text","children","options","className","onOpen"]);let[O,D]=(0,di.useState)(null),[Y,U]=(0,di.useState)(!1),[X,te]=(0,di.useState)(null),{styles:Q,attributes:F}=hS(O,X,Pe({},l)),M=o(K=>{U(K),m&&m(K)},"setOpen");(0,di.useEffect)(()=>{!X||document.addEventListener("click",K=>{X.contains(K.target)?document.addEventListener("click",()=>M(!1),{once:!0}):(K.preventDefault(),K.stopPropagation(),M(!1))},{once:!0,capture:!0})},[X]);let R;return Y?R=di.default.createElement("ul",Pe({className:"dropdown-menu show",ref:te,style:Q.popper},F.popper),n):R=null,di.default.createElement(di.default.Fragment,null,di.default.createElement("a",Pe({href:"#",ref:D,className:(0,rO.default)(d,{open:Y}),onClick:K=>{K.preventDefault(),M(!0)}},p),t),R)},"Dropdown"));function Kh({value:e,onChange:t}){let n=it(d=>d.conf.contentViews||[]),l=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(ou,{text:l,className:"btn btn-default btn-xs",options:{placement:"top-start"}},n.map(d=>Of.default.createElement(hi,{key:d,onClick:()=>t(d)},d.toLowerCase().replace("_"," "))))}o(Kh,"ViewSelector");function mS({flow:e,message:t}){let n=Vt(),l=e.request===t?"request":"response",d=it(te=>te.ui.flow.contentViewFor[e.id+l]||"Auto"),m=(0,Xt.useRef)(null),[p,x]=(0,Xt.useState)(xy),_=(0,Xt.useCallback)(()=>x(Math.max(1024,p*2)),[p]),[O,D]=(0,Xt.useState)(!1),Y;O?Y=Ur.getContentURL(e,t):Y=Ur.getContentURL(e,t,d,p+1);let U=Sy(Y,t.contentHash),X=(0,Xt.useMemo)(()=>{if(U&&!O)try{return JSON.parse(U)}catch(te){return{description:"Network Error",lines:[[["error",`${U}`]]]}}else return},[U]);if(O)return Xt.default.createElement("div",{className:"contentview",key:"edit"},Xt.default.createElement("div",{className:"controls"},Xt.default.createElement("h5",null,"[Editing]"),Xt.default.createElement(Cr,{onClick:o(()=>Na(this,null,function*(){var F;let Q=(F=m.current)==null?void 0:F.getContent();yield n(Ri(e,{[l]:{content:Q}})),D(!1)}),"save"),icon:"fa-check text-success",className:"btn-xs"},"Done"),"\xA0",Xt.default.createElement(Cr,{onClick:()=>D(!1),icon:"fa-times text-danger",className:"btn-xs"},"Cancel")),Xt.default.createElement(Bh,{ref:m,initialContent:U||""}));{let te=X?X.description:"Loading...";return Xt.default.createElement("div",{className:"contentview",key:"view"},Xt.default.createElement("div",{className:"controls"},Xt.default.createElement("h5",null,te),Xt.default.createElement(Cr,{onClick:()=>D(!0),icon:"fa-edit",className:"btn-xs"},"Edit"),"\xA0",Xt.default.createElement(Cy,{icon:"fa-upload",text:"Replace",title:"Upload a file to replace the content.",onOpenFile:Q=>n(qT(e,Q,l)),className:"btn btn-default btn-xs"}),"\xA0",Xt.default.createElement(Kh,{value:d,onChange:Q=>n(Vg(e.id+l,Q))})),vS.matches(t)&&Xt.default.createElement(vS,{flow:e,message:t}),Xt.default.createElement(_y,{lines:(X==null?void 0:X.lines)||[],maxLines:p,showMore:_}))}}o(mS,"HttpMessage");var vI=/^image\/(png|jpe?g|gif|webp|vnc.microsoft.icon|x-icon)$/i;vS.matches=e=>vI.test(Ur.getContentType(e)||"");function vS({flow:e,message:t}){return Xt.default.createElement("div",{className:"flowview-image"},Xt.default.createElement("img",{src:Ur.getContentURL(e,t),alt:"preview",className:"img-thumbnail"}))}o(vS,"ViewImage");function gI({flow:e}){let t=Vt();return Bt.createElement("div",{className:"first-line request-line"},Bt.createElement("div",null,Bt.createElement(xf,{content:e.request.method,onEditDone:n=>t(Ri(e,{request:{method:n}})),isValid:n=>n.length>0}),"\xA0",Bt.createElement(xf,{content:ko.pretty_url(e.request),onEditDone:n=>t(Ri(e,{request:Pe({path:""},Lx(n))})),isValid:n=>{var l;return!!((l=Lx(n))==null?void 0:l.host)}}),"\xA0",Bt.createElement(xf,{content:e.request.http_version,onEditDone:n=>t(Ri(e,{request:{http_version:n}})),isValid:Nx})))}o(gI,"RequestLine");function yI({flow:e}){let t=Vt();return Bt.createElement("div",{className:"first-line response-line"},Bt.createElement(xf,{content:e.response.http_version,onEditDone:n=>t(Ri(e,{response:{http_version:n}})),isValid:Nx}),"\xA0",Bt.createElement(xf,{content:e.response.status_code+"",onEditDone:n=>t(Ri(e,{response:{code:parseInt(n)}})),isValid:n=>/^\d+$/.test(n)}),e.response.http_version!=="HTTP/2.0"&&Bt.createElement(Bt.Fragment,null,"\xA0",Bt.createElement(js,{content:e.response.reason,onEditDone:n=>t(Ri(e,{response:{msg:n}}))})))}o(yI,"ResponseLine");function wI({flow:e,message:t}){let n=Vt(),l=e.request===t?"request":"response";return Bt.createElement(yp,{className:"headers",data:t.headers,onChange:d=>n(Ri(e,{[l]:{headers:d}}))})}o(wI,"Headers");function xI({flow:e,message:t}){let n=Vt(),l=e.request===t?"request":"response";return!Ur.get_first_header(t,/^trailer$/i)?null:Bt.createElement(Bt.Fragment,null,Bt.createElement("hr",null),Bt.createElement("h5",null,"HTTP Trailers"),Bt.createElement(yp,{className:"trailers",data:t.trailers,onChange:m=>n(Ri(e,{[l]:{trailers:m}}))}))}o(xI,"Trailers");var iO=Bt.memo(o(function({flow:t,message:n}){let l=t.request===n?"request":"response",d=t.request===n?gI:yI;return Bt.createElement("section",{className:l},Bt.createElement(d,{flow:t}),Bt.createElement(wI,{flow:t,message:n}),Bt.createElement("hr",null),Bt.createElement(mS,{key:t.id+l,flow:t,message:n}),Bt.createElement(xI,{flow:t,message:n}))},"Message"));function gS(){let e=it(t=>t.flows.byId[t.flows.selected[0]]);return Bt.createElement(iO,{flow:e,message:e.request})}o(gS,"Request");gS.displayName="Request";function yS(){let e=it(t=>t.flows.byId[t.flows.selected[0]]);return Bt.createElement(iO,{flow:e,message:e.response})}o(yS,"Response");yS.displayName="Response";var Ge=pe(De());var SI=o(({message:e})=>Ge.createElement("div",null,e.query?e.op_code:e.response_code,"\xA0",e.truncation?"(Truncated)":""),"Summary"),CI=o(({message:e})=>Ge.createElement(Ge.Fragment,null,Ge.createElement("h5",null,e.recursion_desired?"Recursive ":"","Question"),Ge.createElement("table",null,Ge.createElement("thead",null,Ge.createElement("tr",null,Ge.createElement("th",null,"Name"),Ge.createElement("th",null,"Type"),Ge.createElement("th",null,"Class"))),Ge.createElement("tbody",null,e.questions.map(t=>Ge.createElement("tr",{key:t.name},Ge.createElement("td",null,t.name),Ge.createElement("td",null,t.type),Ge.createElement("td",null,t.class)))))),"Questions"),wS=o(({name:e,values:t})=>Ge.createElement(Ge.Fragment,null,Ge.createElement("h5",null,e),t.length>0?Ge.createElement("table",null,Ge.createElement("thead",null,Ge.createElement("tr",null,Ge.createElement("th",null,"Name"),Ge.createElement("th",null,"Type"),Ge.createElement("th",null,"Class"),Ge.createElement("th",null,"TTL"),Ge.createElement("th",null,"Data"))),Ge.createElement("tbody",null,t.map(n=>Ge.createElement("tr",{key:n.name},Ge.createElement("td",null,n.name),Ge.createElement("td",null,n.type),Ge.createElement("td",null,n.class),Ge.createElement("td",null,n.ttl),Ge.createElement("td",null,n.data))))):"\u2014"),"ResourceRecords"),oO=o(({type:e,message:t})=>Ge.createElement("section",{className:"dns-"+e},Ge.createElement("div",{className:`first-line ${e}-line`},Ge.createElement(SI,{message:t})),Ge.createElement(CI,{message:t}),Ge.createElement("hr",null),Ge.createElement(wS,{name:`${t.authoritative_answer?"Authoritative ":""}${t.recursion_available?"Recursive ":""}Answer`,values:t.answers}),Ge.createElement("hr",null),Ge.createElement(wS,{name:"Authority",values:t.authorities}),Ge.createElement("hr",null),Ge.createElement(wS,{name:"Additional",values:t.additionals})),"Message");function xS(){let e=it(t=>t.flows.byId[t.flows.selected[0]]);return Ge.createElement(oO,{type:"request",message:e.request})}o(xS,"Request");xS.displayName="Request";function SS(){let e=it(t=>t.flows.byId[t.flows.selected[0]]);return Ge.createElement(oO,{type:"response",message:e.response})}o(SS,"Response");SS.displayName="Response";var Ee=pe(De());function sO({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(sO,"ConnectionInfo");function lO(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(lO,"attrList");function _I({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,Qi(t.notbefore,{milliseconds:!1}))),Ee.createElement("tr",null,Ee.createElement("td",null,"Valid to"),Ee.createElement("td",null,Qi(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,lO(t.subject))),Ee.createElement("tr",null,Ee.createElement("td",null,"Issuer"),Ee.createElement("td",null,lO(t.issuer))),Ee.createElement("tr",null,Ee.createElement("td",null,"Serial"),Ee.createElement("td",null,t.serial))))):Ee.createElement(Ee.Fragment,null)}o(_I,"CertificateInfo");function Dy({flow:e}){var t;return Ee.createElement("section",{className:"detail"},Ee.createElement("h4",null,"Client Connection"),Ee.createElement(sO,{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(sO,{conn:e.server_conn})),Ee.createElement(_I,{flow:e}))}o(Dy,"Connection");Dy.displayName="Connection";var Gh=pe(De());function Ry({flow:e}){return Gh.createElement("section",{className:"error"},Gh.createElement("div",{className:"alert alert-warning"},e.error.msg,Gh.createElement("div",null,Gh.createElement("small",null,Qi(e.error.timestamp)))))}o(Ry,"Error");Ry.displayName="Error";var rs=pe(De());function bI({t:e,deltaTo:t,title:n}){return e?rs.createElement("tr",null,rs.createElement("td",null,n,":"),rs.createElement("td",null,Qi(e),t&&rs.createElement("span",{className:"text-muted"},"(",Gg(1e3*(e-t)),")"))):rs.createElement("tr",null)}o(bI,"TimeStamp");function Fy({flow:e}){var l,d,m,p,x,_;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:(m=e.server_conn)==null?void 0:m.timestamp_tls_setup,deltaTo:t},{title:"Server conn. closed",t:(p=e.server_conn)==null?void 0:p.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:(x=e.response)==null?void 0:x.timestamp_start,deltaTo:t},{title:"Response complete",t:(_=e.response)==null?void 0:_.timestamp_end,deltaTo:t}),rs.createElement("section",{className:"timing"},rs.createElement("h4",null,"Timing"),rs.createElement("table",{className:"timing-table"},rs.createElement("tbody",null,n.filter(O=>!!O.t).sort((O,D)=>O.t-D.t).map(O=>rs.createElement(bI,Pe({key:O.title},O))))))}o(Fy,"Timing");Fy.displayName="Timing";var su=pe(De());var Vs=pe(De()),bp=pe(De());function Yh({flow:e,messages_meta:t}){let n=Vt(),l=it(O=>O.ui.flow.contentViewFor[e.id+"messages"]||"Auto"),[d,m]=(0,bp.useState)(xy),p=(0,bp.useCallback)(()=>m(Math.max(1024,d*2)),[d]),x=Sy(Ur.getContentURL(e,"messages",l,d+1),e.id+t.count),_=(0,bp.useMemo)(()=>x&&JSON.parse(x),[x])||[];return Vs.createElement("div",{className:"contentview"},Vs.createElement("div",{className:"controls"},Vs.createElement("h5",null,t.count," Messages"),Vs.createElement(Kh,{value:l,onChange:O=>n(Vg(e.id+"messages",O))})),_.map((O,D)=>{let Y=`fa fa-fw fa-arrow-${O.from_client?"right text-primary":"left text-danger"}`,U=Vs.createElement("div",{key:D},Vs.createElement("small",null,Vs.createElement("i",{className:Y}),Vs.createElement("span",{className:"pull-right"},O.timestamp&&Qi(O.timestamp))),Vs.createElement(_y,{lines:O.lines,maxLines:d,showMore:p}));return d-=O.lines.length,U}))}o(Yh,"Messages");function Iy({flow:e}){return su.createElement("section",{className:"websocket"},su.createElement("h4",null,"WebSocket"),su.createElement(Yh,{flow:e,messages_meta:e.websocket.messages_meta}),su.createElement(EI,{websocket:e.websocket}))}o(Iy,"WebSocket");Iy.displayName="WebSocket";function EI({websocket:e}){if(!e.timestamp_end)return null;let t=e.close_reason?`(${e.close_reason})`:"";return su.createElement("div",null,su.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,".",su.createElement("small",{className:"pull-right"},Qi(e.timestamp_end)))}o(EI,"CloseSummary");var aO=pe(Xn());var Hy=pe(De());function Wy({flow:e}){return Hy.createElement("section",{className:"tcp"},Hy.createElement("h4",null,"TCP Data"),Hy.createElement(Yh,{flow:e,messages_meta:e.messages_meta}))}o(Wy,"TcpMessages");Wy.displayName="TCP Messages";var uO={request:gS,response:yS,error:Ry,connection:Dy,timing:Fy,websocket:Iy,messages:Wy,dnsrequest:xS,dnsresponse:SS};function By(e){let t;switch(e.type){case"http":t=["request","response","websocket"].filter(n=>e[n]);break;case"tcp":t=["messages"];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(By,"tabsForFlow");function CS(){let e=Vt(),t=it(m=>m.flows.byId[m.flows.selected[0]]),n=By(t),l=it(m=>m.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=uO[l];return Xh.createElement("div",{className:"flow-detail"},Xh.createElement("nav",{className:"nav-tabs nav-tabs-sm"},n.map(m=>Xh.createElement("a",{key:m,href:"#",className:(0,aO.default)({active:l===m}),onClick:p=>{p.preventDefault(),e(mf(m))}},uO[m].displayName))),Xh.createElement(d,{flow:t}))}o(CS,"FlowView");function fO(e){if(e.ctrlKey||e.metaKey)return()=>{};let t=e.key;return e.preventDefault(),(n,l)=>{let d=l().flows,m=d.byId[l().flows.selected[0]];switch(t){case"k":case"ArrowUp":n(yf(d,-1));break;case"j":case"ArrowDown":n(yf(d,1));break;case" ":case"PageDown":n(yf(d,10));break;case"PageUp":n(yf(d,-10));break;case"End":n(yf(d,1e10));break;case"Home":n(yf(d,-1e10));break;case"Escape":l().ui.modal.activeModal?n(my()):n(wf(void 0));break;case"ArrowLeft":{if(!m)break;let p=By(m),x=l().ui.flow.tab,_=p[(Math.max(0,p.indexOf(x))-1+p.length)%p.length];n(mf(_));break}case"Tab":case"ArrowRight":{if(!m)break;let p=By(m),x=l().ui.flow.tab,_=p[(Math.max(0,p.indexOf(x))+1)%p.length];n(mf(_));break}case"d":{if(!m)return;n(fy(m));break}case"n":{vf("view.flows.create","get","https://example.com/");break}case"D":{if(!m)return;n(cy(m));break}case"a":{m&&m.intercepted&&n(cp(m));break}case"A":{n(ay());break}case"r":{m&&n(pp(m));break}case"v":{m&&m.modified&&n(py(m));break}case"x":{m&&m.intercepted&&n(uy(m));break}case"X":{n(jT());break}case"z":{n(dy());break}default:return}}}o(fO,"onKeyDown");var nm=pe(De());var Qh=pe(De()),Zh=pe(Za()),cO=pe(Xn()),Ep=class extends Qh.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(){Zh.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=Zh.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,Zh.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=Zh.default.findDOMNode(this);n.previousElementSibling.style.flex="",n.nextElementSibling.style.flex="",t||this.setState({applied:!1}),this.onResize()}componentWillUnmount(){this.reset(!0)}render(){return Qh.default.createElement("div",{className:(0,cO.default)("splitter",this.props.axis==="x"?"splitter-x":"splitter-y")},Qh.default.createElement("div",{onMouseDown:this.onMouseDown,draggable:"true"}))}};o(Ep,"Splitter"),Ep.defaultProps={axis:"x"};var ns=pe(De()),tm=pe(Jh()),$y=pe(Za());var TO=pe(_S());var bS=pe(Za()),xO=Symbol("shouldStick"),SO=o(e=>e.scrollTop+e.clientHeight===e.scrollHeight,"isAtBottom"),Uy=o(e=>{var t;return Object.assign((o(t=class extends e{UNSAFE_componentWillUpdate(){let l=bS.default.findDOMNode(this);this[xO]=l.scrollTop&&SO(l),super.UNSAFE_componentWillUpdate&&super.UNSAFE_componentWillUpdate(),super.componentWillUpdate&&super.componentWillUpdate()}componentDidUpdate(){let l=bS.default.findDOMNode(this);this[xO]&&!SO(l)&&(l.scrollTop=l.scrollHeight),super.componentDidUpdate&&super.componentDidUpdate()}},"AutoScrollWrapper"),t.displayName=e.name,t),e)},"default");function Tp(e=void 0){if(!e)return{start:0,end:0,paddingTop:0,paddingBottom:0};let{itemCount:t,rowHeight:n,viewportTop:l,viewportHeight:d,itemHeights:m}=e,p=l+d,x=0,_=0,O=0,D=0;if(m)for(let Y=0,U=0;Yx.flows.sort.desc),l=it(x=>x.flows.sort.column),d=it(x=>x.options.web_columns),m=n?"sort-desc":"sort-asc",p=d.map(x=>Dh[x]).filter(x=>x).concat(fp);return em.createElement("tr",null,p.map(x=>em.createElement("th",{className:(0,CO.default)(`col-${x.name}`,l===x.name&&m),key:x.name,onClick:()=>t(zT(x.name===l&&n?void 0:x.name,x.name!==l?!1:!n))},x.headerName)))},"FlowTableHead"));var kp=pe(De()),bO=pe(Xn());var EO=kp.default.memo(o(function({flow:t,selected:n,highlighted:l}){let d=Vt(),m=it(O=>O.options.web_columns),p=(0,bO.default)({selected:n,highlighted:l,intercepted:t.intercepted,"has-request":t.type==="http"&&t.request,"has-response":t.type==="http"&&t.response}),x=(0,kp.useCallback)(O=>{let D=O.target;for(;D.parentNode;){if(D.classList.contains("col-quickactions"))return;D=D.parentNode}d(wf(t.id))},[t]),_=m.map(O=>Dh[O]).filter(O=>O).concat(fp);return kp.default.createElement("tr",{className:p,onClick:x},_.map(O=>kp.default.createElement(O,{key:O.name,flow:t})))},"FlowRow"));var rm=class extends ns.Component{constructor(t,n){super(t,n);this.state={vScroll:Tp()},this.onViewportUpdate=this.onViewportUpdate.bind(this)}UNSAFE_componentWillMount(){window.addEventListener("resize",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=$y.default.findDOMNode(this),m=$y.default.findDOMNode(this.refs.head),p=m?m.offsetHeight:0,x=n.indexOf(l)*t+p,_=x+t,O=d.scrollTop,D=d.offsetHeight;x-pO+D&&(d.scrollTop=_-D)}UNSAFE_componentWillReceiveProps(t){t.selected&&t.selected!==this.props.selected&&(this.shouldScrollIntoView=!0)}onViewportUpdate(){let t=$y.default.findDOMNode(this),n=t.scrollTop||0,l=Tp({viewportTop:n,viewportHeight:t.offsetHeight||0,itemCount:this.props.flows.length,rowHeight:this.props.rowHeight});(this.state.viewportTop!==n||!(0,TO.default)(this.state.vScroll,l))&&this.setState({vScroll:l,viewportTop:n})}render(){let{vScroll:t,viewportTop:n}=this.state,{flows:l,selected:d,highlight:m}=this.props,p=m?gf.parse(m):()=>!1;return ns.createElement("div",{className:"flow-table",onScroll:this.onViewportUpdate},ns.createElement("table",null,ns.createElement("thead",{ref:"head",style:{transform:`translateY(${n}px)`}},ns.createElement(_O,null)),ns.createElement("tbody",null,ns.createElement("tr",{style:{height:t.paddingTop}}),l.slice(t.start,t.end).map(x=>ns.createElement(EO,{key:x.id,flow:x,selected:x===d,highlighted:p(x)})),ns.createElement("tr",{style:{height:t.paddingBottom}}))))}};o(rm,"FlowTable"),Tc(rm,"propTypes",{flows:tm.default.array.isRequired,rowHeight:tm.default.number,highlight:tm.default.string,selected:tm.default.object}),Tc(rm,"defaultProps",{rowHeight:32});var OI=Uy(rm),kO=Di(e=>({flows:e.flows.view,highlight:e.flows.highlight,selected:e.flows.byId[e.flows.selected[0]]}))(OI);function ES(){let e=it(t=>!!t.flows.byId[t.flows.selected[0]]);return nm.createElement("div",{className:"main-view"},nm.createElement(kO,null),e&&nm.createElement(Ep,{key:"splitter"}),e&&nm.createElement(CS,{key:"flowDetails"}))}o(ES,"MainView");var Po=pe(De()),DO=pe(Xn());var Jn=pe(De());var is=pe(De()),zy=pe(Za()),OO=pe(Xn());var Ji=pe(De());var eo=class extends Ji.Component{constructor(t,n){super(t,n);this.state={doc:eo.doc}}componentDidMount(){eo.xhr||(eo.xhr=Ot("/filter-help").then(t=>t.json()),eo.xhr.catch(()=>{eo.xhr=null})),this.state.doc||eo.xhr.then(t=>{eo.doc=t,this.setState({doc:t})})}render(){let{doc:t}=this.state;return t?Ji.default.createElement("table",{className:"table table-condensed"},Ji.default.createElement("tbody",null,t.commands.map(n=>Ji.default.createElement("tr",{key:n[1],onClick:l=>this.props.selectHandler(n[0].split(" ")[0]+" ")},Ji.default.createElement("td",null,n[0].replace(" ","\xA0")),Ji.default.createElement("td",null,n[1]))),Ji.default.createElement("tr",{key:"docs-link"},Ji.default.createElement("td",{colSpan:2},Ji.default.createElement("a",{href:"https://mitmproxy.org/docs/latest/concepts-filters/",target:"_blank"},Ji.default.createElement("i",{className:"fa fa-external-link"}),"\xA0 mitmproxy docs"))))):Ji.default.createElement("i",{className:"fa fa-spinner fa-spin"})}};o(eo,"FilterDocs");var Lf=class extends is.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&&gf.parse(t),!0}catch(n){return!1}}getDesc(){if(!this.state.value)return is.default.createElement(eo,{selectHandler:this.selectFilter});try{return gf.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}),zy.default.findDOMNode(this.refs.input).focus()}blur(){zy.default.findDOMNode(this.refs.input).blur()}select(){zy.default.findDOMNode(this.refs.input).select()}render(){let{type:t,color:n,placeholder:l}=this.props,{value:d,focus:m,mousefocus:p}=this.state;return is.default.createElement("div",{className:(0,OO.default)("filter-input input-group",{"has-error":!this.isValid(d)})},is.default.createElement("span",{className:"input-group-addon"},is.default.createElement("i",{className:"fa fa-fw fa-"+t,style:{color:n}})),is.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}),(m||p)&&is.default.createElement("div",{className:"popover bottom",onMouseEnter:this.onMouseEnter,onMouseLeave:this.onMouseLeave},is.default.createElement("div",{className:"arrow"}),is.default.createElement("div",{className:"popover-content"},this.getDesc())))}};o(Lf,"FilterInput");Op.title="Start";function Op(){return Jn.createElement("div",{className:"main-menu"},Jn.createElement("div",{className:"menu-group"},Jn.createElement("div",{className:"menu-content"},Jn.createElement(NI,null),Jn.createElement(PI,null)),Jn.createElement("div",{className:"menu-legend"},"Find")),Jn.createElement("div",{className:"menu-group"},Jn.createElement("div",{className:"menu-content"},Jn.createElement(LI,null),Jn.createElement(MI,null)),Jn.createElement("div",{className:"menu-legend"},"Intercept")))}o(Op,"StartMenu");function LI(){let e=Vt(),t=it(n=>n.options.intercept);return Jn.createElement(Lf,{value:t||"",placeholder:"Intercept",type:"pause",color:"hsl(208, 56%, 53%)",onChange:n=>e(vp("intercept",n))})}o(LI,"InterceptInput");function NI(){let e=Vt(),t=it(n=>n.flows.filter);return Jn.createElement(Lf,{value:t||"",placeholder:"Search",type:"search",color:"black",onChange:n=>e(sy(n))})}o(NI,"FlowFilterInput");function PI(){let e=Vt(),t=it(n=>n.flows.highlight);return Jn.createElement(Lf,{value:t||"",placeholder:"Highlight",type:"tag",color:"hsl(48, 100%, 50%)",onChange:n=>e(ly(n))})}o(PI,"HighlightInput");function MI(){let e=Vt();return Jn.createElement(Cr,{className:"btn-sm",title:"[a]ccept all",icon:"fa-forward text-success",onClick:()=>e(ay())},"Resume All")}o(MI,"ResumeAll");var jr=pe(De());var Nf=pe(De());function TS({value:e,onChange:t,children:n}){return Nf.createElement("div",{className:"menu-entry"},Nf.createElement("label",null,Nf.createElement("input",{type:"checkbox",checked:e,onChange:t}),n))}o(TS,"MenuToggle");function jy({name:e,children:t}){let n=Vt(),l=it(d=>d.options[e]);return Nf.createElement(TS,{value:!!l,onChange:()=>n(vp(e,!l))},t)}o(jy,"OptionsToggle");function LO(){let e=$s(),t=it(n=>n.eventLog.visible);return Nf.createElement(TS,{value:t,onChange:()=>e(mp())},"Display Event Log")}o(LO,"EventlogToggle");function NO(){let e=$s(),t=it(n=>n.commandBar.visible);return Nf.createElement(TS,{value:t,onChange:()=>e(wy())},"Display Command Bar")}o(NO,"CommandBarToggle");var kS=pe(De());function OS({children:e,resource:t}){let n=`https://docs.mitmproxy.org/stable/${t}`;return kS.createElement("a",{target:"_blank",href:n},e||kS.createElement("i",{className:"fa fa-question-circle"}))}o(OS,"DocsLink");var qy=pe(De());function No({children:e}){return window.MITMWEB_CONF&&window.MITMWEB_CONF.static?null:qy.createElement(qy.Fragment,null,e)}o(No,"HideInStatic");Vy.title="Options";function Vy(){let e=Vt(),t=o(()=>YT("OptionModal"),"openOptions");return jr.createElement("div",null,jr.createElement(No,null,jr.createElement("div",{className:"menu-group"},jr.createElement("div",{className:"menu-content"},jr.createElement(Cr,{title:"Open Options",icon:"fa-cogs text-primary",onClick:()=>e(t())},"Edit Options ",jr.createElement("sup",null,"alpha"))),jr.createElement("div",{className:"menu-legend"},"Options Editor")),jr.createElement("div",{className:"menu-group"},jr.createElement("div",{className:"menu-content"},jr.createElement(jy,{name:"anticache"},"Strip cache headers ",jr.createElement(OS,{resource:"overview-features/#anticache"})),jr.createElement(jy,{name:"showhost"},"Use host header for display"),jr.createElement(jy,{name:"ssl_insecure"},"Don't verify server certificates")),jr.createElement("div",{className:"menu-legend"},"Quick Options"))),jr.createElement("div",{className:"menu-group"},jr.createElement("div",{className:"menu-content"},jr.createElement(LO,null),jr.createElement(NO,null)),jr.createElement("div",{className:"menu-legend"},"View Options")))}o(Vy,"OptionMenu");var mi=pe(De());var PO=mi.memo(o(function(){let t=$s();return mi.createElement(ou,{className:"pull-left special",text:"File",options:{placement:"bottom-start"}},mi.createElement("li",null,mi.createElement(Cy,{icon:"fa-folder-open",text:"\xA0Open...",onClick:n=>n.stopPropagation(),onOpenFile:n=>{t(KT(n)),document.body.click()}})),mi.createElement(hi,{onClick:()=>t(VT())},mi.createElement("i",{className:"fa fa-fw fa-floppy-o"}),"\xA0Save..."),mi.createElement(hi,{onClick:()=>confirm("Delete all flows?")&&t(dy())},mi.createElement("i",{className:"fa fa-fw fa-trash"}),"\xA0Clear All"),mi.createElement(No,null,mi.createElement(nO,null),mi.createElement("li",null,mi.createElement("a",{href:"http://mitm.it/",target:"_blank"},mi.createElement("i",{className:"fa fa-fw fa-external-link"}),"\xA0Install Certificates..."))))},"FileMenu"));var lt=pe(De());function MO(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(MO,"copyToClipboard");var Lp=o((e,t)=>Na(void 0,null,function*(){let n=yield vf("export",t,`@${e.id}`);n.value?yield MO(n.value):n.error?alert(n.error):console.error(n)}),"copy");Np.title="Flow";function Np(){let e=Vt(),t=it(n=>n.flows.byId[n.flows.selected[0]]);return t?lt.createElement("div",{className:"flow-menu"},lt.createElement(No,null,lt.createElement("div",{className:"menu-group"},lt.createElement("div",{className:"menu-content"},lt.createElement(Cr,{title:"[r]eplay flow",icon:"fa-repeat text-primary",onClick:()=>e(pp(t)),disabled:!Qg(t)},"Replay"),lt.createElement(Cr,{title:"[D]uplicate flow",icon:"fa-copy text-info",onClick:()=>e(cy(t))},"Duplicate"),lt.createElement(Cr,{disabled:!t||!t.modified,title:"revert changes to flow [V]",icon:"fa-history text-warning",onClick:()=>e(py(t))},"Revert"),lt.createElement(Cr,{title:"[d]elete flow",icon:"fa-trash text-danger",onClick:()=>e(fy(t))},"Delete"),lt.createElement(FI,{flow:t})),lt.createElement("div",{className:"menu-legend"},"Flow Modification"))),lt.createElement("div",{className:"menu-group"},lt.createElement("div",{className:"menu-content"},lt.createElement(AI,{flow:t}),lt.createElement(DI,{flow:t})),lt.createElement("div",{className:"menu-legend"},"Export")),lt.createElement(No,null,lt.createElement("div",{className:"menu-group"},lt.createElement("div",{className:"menu-content"},lt.createElement(Cr,{disabled:!t||!t.intercepted,title:"[a]ccept intercepted flow",icon:"fa-play text-success",onClick:()=>e(cp(t))},"Resume"),lt.createElement(Cr,{disabled:!t||!t.intercepted,title:"kill intercepted flow [x]",icon:"fa-times text-danger",onClick:()=>e(uy(t))},"Abort")),lt.createElement("div",{className:"menu-legend"},"Interception")))):lt.createElement("div",null)}o(Np,"FlowMenu");var Ky=o(e=>{let t=window.open(e,"_blank","noopener,noreferrer");t&&(t.opener=null)},"openInNewTab");function AI({flow:e}){var t;if(e.type!=="http")return lt.createElement(Cr,{icon:"fa-download",onClick:()=>0,disabled:!0},"Download");if(e.request.contentLength&&!((t=e.response)==null?void 0:t.contentLength))return lt.createElement(Cr,{icon:"fa-download",onClick:()=>Ky(Ur.getContentURL(e,e.request))},"Download");if(e.response){let n=e.response;if(!e.request.contentLength&&e.response.contentLength)return lt.createElement(Cr,{icon:"fa-download",onClick:()=>Ky(Ur.getContentURL(e,n))},"Download");if(e.request.contentLength&&e.response.contentLength)return lt.createElement(ou,{text:lt.createElement(Cr,{icon:"fa-download",onClick:()=>1},"Download\u25BE"),options:{placement:"bottom-start"}},lt.createElement(hi,{onClick:()=>Ky(Ur.getContentURL(e,e.request))},"Download request"),lt.createElement(hi,{onClick:()=>Ky(Ur.getContentURL(e,n))},"Download response"))}return null}o(AI,"DownloadButton");function DI({flow:e}){return lt.createElement(ou,{className:"",text:lt.createElement(Cr,{title:"Export flow.",icon:"fa-clone",onClick:()=>1,disabled:e.type!=="http"},"Export\u25BE"),options:{placement:"bottom-start"}},lt.createElement(hi,{onClick:()=>Lp(e,"raw_request")},"Copy raw request"),lt.createElement(hi,{onClick:()=>Lp(e,"raw_response")},"Copy raw response"),lt.createElement(hi,{onClick:()=>Lp(e,"raw")},"Copy raw request and response"),lt.createElement(hi,{onClick:()=>Lp(e,"curl")},"Copy as cURL"),lt.createElement(hi,{onClick:()=>Lp(e,"httpie")},"Copy as HTTPie"))}o(DI,"ExportButton");var RI={":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 FI({flow:e}){let t=Vt();return lt.createElement(ou,{className:"",text:lt.createElement(Cr,{title:"mark flow",icon:"fa-paint-brush text-success",onClick:()=>1},"Mark\u25BE"),options:{placement:"bottom-start"}},lt.createElement(hi,{onClick:()=>t(Ri(e,{marked:""}))},"\u26AA (no marker)"),Object.entries(RI).map(([n,l])=>lt.createElement(hi,{key:n,onClick:()=>t(Ri(e,{marked:n}))},l," ",n.replace(/[:_]/g," "))))}o(FI,"MarkButton");var lu=pe(De());var AO=lu.memo(o(function(){let t=it(l=>l.connection.state),n=it(l=>l.connection.message);switch(t){case Zn.INIT:return lu.createElement("span",{className:"connection-indicator init"},"connecting\u2026");case Zn.FETCHING:return lu.createElement("span",{className:"connection-indicator fetching"},"fetching data\u2026");case Zn.ESTABLISHED:return lu.createElement("span",{className:"connection-indicator established"},"connected");case Zn.ERROR:return lu.createElement("span",{className:"connection-indicator error",title:n},"connection lost");case Zn.OFFLINE:return lu.createElement("span",{className:"connection-indicator offline"},"offline");default:let l=t;throw"unknown connection state"}},"ConnectionIndicator"));function LS(){let e=it(x=>x.flows.selected.filter(_=>_ in x.flows.byId)),[t,n]=(0,Po.useState)(()=>Op),[l,d]=(0,Po.useState)(!1),m=[Op,Vy];e.length>0?(l||(n(()=>Np),d(!0)),m.push(Np)):(l&&d(!1),t===Np&&n(()=>Op));function p(x,_){_.preventDefault(),n(()=>x)}return o(p,"handleClick"),Po.default.createElement("header",null,Po.default.createElement("nav",{className:"nav-tabs nav-tabs-lg"},Po.default.createElement(PO,null),m.map(x=>Po.default.createElement("a",{key:x.title,href:"#",className:(0,DO.default)({active:x===t}),onClick:_=>p(x,_)},x.title)),Po.default.createElement(No,null,Po.default.createElement(AO,null))),Po.default.createElement("div",null,Po.default.createElement(t,null)))}o(LS,"Header");var Ze=pe(De()),RO=pe(Xn());var Gy=function(){"use strict";function e(l,d){function m(){this.constructor=l}o(m,"ctor"),m.prototype=d.prototype,l.prototype=new m}o(e,"peg$subclass");function t(l,d,m,p){this.message=l,this.expected=d,this.found=m,this.location=p,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]:{},m=this,p={},x={Expr:vr},_=vr,O=o(function(H,J){return[H,...J]},"peg$c0"),D=o(function(H){return[H]},"peg$c1"),Y=o(function(){return""},"peg$c2"),U={type:"other",description:"string"},X='"',te={type:"literal",value:'"',description:'"\\""'},Q=o(function(H){return H.join("")},"peg$c6"),F="'",M={type:"literal",value:"'",description:`"'"`},R=/^["\\]/,K={type:"class",value:'["\\\\]',description:'["\\\\]'},V={type:"any",description:"any character"},ue=o(function(H){return H},"peg$c12"),ie="\\",de={type:"literal",value:"\\",description:'"\\\\"'},ge=/^['\\]/,xe={type:"class",value:"['\\\\]",description:"['\\\\]"},qe=/^['"\\]/,et={type:"class",value:`['"\\\\]`,description:`['"\\\\]`},Te="n",xt={type:"literal",value:"n",description:'"n"'},Ue=o(function(){return` -`},"peg$c21"),Ve="r",Ke={type:"literal",value:"r",description:'"r"'},Ye=o(function(){return"\r"},"peg$c24"),Qt="t",ft={type:"literal",value:"t",description:'"t"'},Ar=o(function(){return" "},"peg$c27"),Kt={type:"other",description:"whitespace"},Et=/^[ \t\n\r]/,St={type:"class",value:"[ \\t\\n\\r]",description:"[ \\t\\n\\r]"},at={type:"other",description:"control character"},_r=/^[|&!()~"]/,Ut={type:"class",value:'[|&!()~"]',description:'[|&!()~"]'},$t={type:"other",description:"optional whitespace"},ne=0,tt=0,br=[{line:1,column:1,seenCR:!1}],jt=0,qt=[],Se=0,Er;if("startRule"in d){if(!(d.startRule in x))throw new Error(`Can't start parsing from rule "`+d.startRule+'".');_=x[d.startRule]}function nn(){return l.substring(tt,ne)}o(nn,"text");function Fn(){return dr(tt,ne)}o(Fn,"location");function ei(H){throw Do(null,[{type:"other",description:H}],l.substring(tt,ne),dr(tt,ne))}o(ei,"expected");function on(H){throw Do(H,null,l.substring(tt,ne),dr(tt,ne))}o(on,"error");function Gt(H){var J=br[H],he,ke;if(J)return J;for(he=H-1;!br[he];)he--;for(J=br[he],J={line:J.line,column:J.column,seenCR:J.seenCR};hejt&&(jt=ne,qt=[]),qt.push(H))}o(ct,"peg$fail");function Do(H,J,he,ke){function Zt(Rt){var Dr=1;for(Rt.sort(function(Jt,ti){return Jt.descriptionti.description?1:0});Dr1?ti.slice(0,-1).join(", ")+" or "+ti[Rt.length-1]:ti[0],to=Dr?'"'+Jt(Dr)+'"':"end of input","Expected "+os+" but "+to+" found."}return o(Bl,"buildMessage"),J!==null&&Zt(J),new t(H!==null?H:Bl(J,he),J,he,ke)}o(Do,"peg$buildException");function vr(){var H,J,he,ke;if(H=ne,J=Fi(),J!==p){if(he=[],ke=Hn(),ke!==p)for(;ke!==p;)he.push(ke),ke=Hn();else he=p;he!==p?(ke=vr(),ke!==p?(tt=H,J=O(J,ke),H=J):(ne=H,H=p)):(ne=H,H=p)}else ne=H,H=p;if(H===p&&(H=ne,J=Fi(),J!==p&&(tt=H,J=D(J)),H=J,H===p)){for(H=ne,J=[],he=Hn();he!==p;)J.push(he),he=Hn();J!==p&&(tt=H,J=Y()),H=J}return H}o(vr,"peg$parseExpr");function Fi(){var H,J,he,ke;if(Se++,H=ne,l.charCodeAt(ne)===34?(J=X,ne++):(J=p,Se===0&&ct(te)),J!==p){for(he=[],ke=sn();ke!==p;)he.push(ke),ke=sn();he!==p?(l.charCodeAt(ne)===34?(ke=X,ne++):(ke=p,Se===0&&ct(te)),ke!==p?(tt=H,J=Q(he),H=J):(ne=H,H=p)):(ne=H,H=p)}else ne=H,H=p;if(H===p){if(H=ne,l.charCodeAt(ne)===39?(J=F,ne++):(J=p,Se===0&&ct(M)),J!==p){for(he=[],ke=In();ke!==p;)he.push(ke),ke=In();he!==p?(l.charCodeAt(ne)===39?(ke=F,ne++):(ke=p,Se===0&&ct(M)),ke!==p?(tt=H,J=Q(he),H=J):(ne=H,H=p)):(ne=H,H=p)}else ne=H,H=p;if(H===p){if(H=ne,J=ne,Se++,he=En(),Se--,he===p?J=void 0:(ne=J,J=p),J!==p){if(he=[],ke=vi(),ke!==p)for(;ke!==p;)he.push(ke),ke=vi();else he=p;he!==p?(tt=H,J=Q(he),H=J):(ne=H,H=p)}else ne=H,H=p;if(H===p){if(H=ne,l.charCodeAt(ne)===34?(J=X,ne++):(J=p,Se===0&&ct(te)),J!==p){for(he=[],ke=sn();ke!==p;)he.push(ke),ke=sn();he!==p?(tt=H,J=Q(he),H=J):(ne=H,H=p)}else ne=H,H=p;if(H===p)if(H=ne,l.charCodeAt(ne)===39?(J=F,ne++):(J=p,Se===0&&ct(M)),J!==p){for(he=[],ke=In();ke!==p;)he.push(ke),ke=In();he!==p?(tt=H,J=Q(he),H=J):(ne=H,H=p)}else ne=H,H=p}}}return Se--,H===p&&(J=p,Se===0&&ct(U)),H}o(Fi,"peg$parseStringLiteral");function sn(){var H,J,he;return H=ne,J=ne,Se++,R.test(l.charAt(ne))?(he=l.charAt(ne),ne++):(he=p,Se===0&&ct(K)),Se--,he===p?J=void 0:(ne=J,J=p),J!==p?(l.length>ne?(he=l.charAt(ne),ne++):(he=p,Se===0&&ct(V)),he!==p?(tt=H,J=ue(he),H=J):(ne=H,H=p)):(ne=H,H=p),H===p&&(H=ne,l.charCodeAt(ne)===92?(J=ie,ne++):(J=p,Se===0&&ct(de)),J!==p?(he=gi(),he!==p?(tt=H,J=ue(he),H=J):(ne=H,H=p)):(ne=H,H=p)),H}o(sn,"peg$parseDoubleStringChar");function In(){var H,J,he;return H=ne,J=ne,Se++,ge.test(l.charAt(ne))?(he=l.charAt(ne),ne++):(he=p,Se===0&&ct(xe)),Se--,he===p?J=void 0:(ne=J,J=p),J!==p?(l.length>ne?(he=l.charAt(ne),ne++):(he=p,Se===0&&ct(V)),he!==p?(tt=H,J=ue(he),H=J):(ne=H,H=p)):(ne=H,H=p),H===p&&(H=ne,l.charCodeAt(ne)===92?(J=ie,ne++):(J=p,Se===0&&ct(de)),J!==p?(he=gi(),he!==p?(tt=H,J=ue(he),H=J):(ne=H,H=p)):(ne=H,H=p)),H}o(In,"peg$parseSingleStringChar");function vi(){var H,J,he;return H=ne,J=ne,Se++,he=Hn(),Se--,he===p?J=void 0:(ne=J,J=p),J!==p?(l.length>ne?(he=l.charAt(ne),ne++):(he=p,Se===0&&ct(V)),he!==p?(tt=H,J=ue(he),H=J):(ne=H,H=p)):(ne=H,H=p),H}o(vi,"peg$parseUnquotedStringChar");function gi(){var H,J;return qe.test(l.charAt(ne))?(H=l.charAt(ne),ne++):(H=p,Se===0&&ct(et)),H===p&&(H=ne,l.charCodeAt(ne)===110?(J=Te,ne++):(J=p,Se===0&&ct(xt)),J!==p&&(tt=H,J=Ue()),H=J,H===p&&(H=ne,l.charCodeAt(ne)===114?(J=Ve,ne++):(J=p,Se===0&&ct(Ke)),J!==p&&(tt=H,J=Ye()),H=J,H===p&&(H=ne,l.charCodeAt(ne)===116?(J=Qt,ne++):(J=p,Se===0&&ct(ft)),J!==p&&(tt=H,J=Ar()),H=J))),H}o(gi,"peg$parseEscapeSequence");function Hn(){var H,J;return Se++,Et.test(l.charAt(ne))?(H=l.charAt(ne),ne++):(H=p,Se===0&&ct(St)),Se--,H===p&&(J=p,Se===0&&ct(Kt)),H}o(Hn,"peg$parsews");function En(){var H,J;return Se++,_r.test(l.charAt(ne))?(H=l.charAt(ne),ne++):(H=p,Se===0&&ct(Ut)),Se--,H===p&&(J=p,Se===0&&ct(at)),H}o(En,"peg$parsecc");function Ks(){var H,J;for(Se++,H=[],J=Hn();J!==p;)H.push(J),J=Hn();return Se--,H===p&&(J=p,Se===0&&ct($t)),H}if(o(Ks,"peg$parse__"),Er=_(),Er!==p&&ne===l.length)return Er;throw Er!==p&&ne{t&&t.current.addEventListener("DOMNodeInserted",n=>{let l=n.currentTarget;l.scroll({top:l.scrollHeight,behavior:"auto"})})},[]),Ze.default.createElement("div",{className:"command-result",ref:t},e.map((n,l)=>Ze.default.createElement("div",{key:l},Ze.default.createElement("div",null,Ze.default.createElement("strong",null,"$ ",n.command)),n.result)))}o(II,"Results");function HI({nextArgs:e,currentArg:t,help:n,description:l,availableCommands:d}){let m=[];for(let p=0;p0&&Ze.default.createElement("div",null,Ze.default.createElement("strong",null,"Argument suggestion:")," ",m),(n==null?void 0:n.includes("->"))&&Ze.default.createElement("div",null,Ze.default.createElement("strong",null,"Signature help: "),n),l&&Ze.default.createElement("div",null,"# ",l),Ze.default.createElement("div",null,Ze.default.createElement("strong",null,"Available Commands: "),Ze.default.createElement("p",{className:"available-commands"},JSON.stringify(d)))))}o(HI,"CommandHelp");function PS(){let[e,t]=(0,Ze.useState)(""),[n,l]=(0,Ze.useState)(""),[d,m]=(0,Ze.useState)(0),[p,x]=(0,Ze.useState)([]),[_,O]=(0,Ze.useState)([]),[D,Y]=(0,Ze.useState)({}),[U,X]=(0,Ze.useState)([]),[te,Q]=(0,Ze.useState)(0),[F,M]=(0,Ze.useState)(""),[R,K]=(0,Ze.useState)(""),[V,ue]=(0,Ze.useState)([]),[ie,de]=(0,Ze.useState)([]),[ge,xe]=(0,Ze.useState)(void 0);(0,Ze.useEffect)(()=>{Ot("/commands",{method:"GET"}).then(Ue=>Ue.json()).then(Ue=>{Y(Ue),x(NS(Ue)),O(Object.keys(Ue))}).catch(Ue=>console.error(Ue))},[]),(0,Ze.useEffect)(()=>{vf("commands.history.get").then(Ue=>{de(Ue.value)}).catch(Ue=>console.error(Ue))},[]);let qe=o((Ue,Ve)=>{var ft,Ar,Kt;let Ke=Gy.parse(Ve),Ye=Gy.parse(Ue);M((ft=D[Ke[0]])==null?void 0:ft.signature_help),K(((Ar=D[Ke[0]])==null?void 0:Ar.help)||""),x(NS(D,Ye[0])),O(NS(D,Ke[0]));let Qt=(Kt=D[Ke[0]])==null?void 0:Kt.parameters.map(Et=>Et.name);Qt&&(X([Ke[0],...Qt]),Q(Ke.length-1))},"parseCommand"),et=o(Ue=>{t(Ue.target.value),l(Ue.target.value),m(0)},"onChange"),Te=o(Ue=>{if(Ue.key==="Enter"){let[Ve,...Ke]=Gy.parse(e);de([...ie,e]),vf("commands.history.add",e).catch(()=>0),Ot.post(`/commands/${Ve}`,{arguments:Ke}).then(Ye=>Ye.json()).then(Ye=>{xe(void 0),X([]),ue([...V,{command:e,result:JSON.stringify(Ye.value||Ye.error)}])}).catch(Ye=>{xe(void 0),X([]),ue([...V,{command:e,result:Ye.toString()}])}),M(""),K(""),t(""),l(""),m(0),x(_)}if(Ue.key==="ArrowUp"){let Ve;ge===void 0?Ve=ie.length-1:Ve=Math.max(0,ge-1),t(ie[Ve]),l(ie[Ve]),xe(Ve)}if(Ue.key==="ArrowDown"){if(ge===void 0)return;if(ge==ie.length-1)t(""),l(""),xe(void 0);else{let Ve=ge+1;t(ie[Ve]),l(ie[Ve]),xe(Ve)}}Ue.key==="Tab"&&(t(p[d]),m((d+1)%p.length),Ue.preventDefault()),Ue.stopPropagation()},"onKeyDown"),xt=o(Ue=>{if(!e){O(Object.keys(D));return}qe(n,e),Ue.stopPropagation()},"onKeyUp");return Ze.default.createElement("div",{className:"command"},Ze.default.createElement("div",{className:"command-title"},"Command Result"),Ze.default.createElement(II,{results:V}),Ze.default.createElement(HI,{nextArgs:U,currentArg:te,help:F,description:R,availableCommands:_}),Ze.default.createElement("div",{className:(0,RO.default)("command-input input-group")},Ze.default.createElement("span",{className:"input-group-addon"},Ze.default.createElement("i",{className:"fa fa-fw fa-terminal"})),Ze.default.createElement("input",{type:"text",placeholder:"Enter command",className:"form-control",value:e||"",onChange:et,onKeyDown:Te,onKeyUp:xt})))}o(PS,"CommandBar");var Wl=pe(De()),Pp=pe(Jh());var MS=pe(De());function AS({checked:e,onToggle:t,text:n}){return MS.default.createElement("div",{className:"btn btn-toggle "+(e?"btn-primary":"btn-default"),onClick:t},MS.default.createElement("i",{className:"fa fa-fw "+(e?"fa-check-square-o":"fa-square-o")}),"\xA0",n)}o(AS,"ToggleButton");var Hl=pe(De()),DS=pe(Jh()),FO=pe(Za()),IO=pe(_S());var im=class extends Hl.Component{constructor(t){super(t);this.heights={},this.state={vScroll:Tp()},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=FO.default.findDOMNode(this),n=Tp({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,IO.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(WI,{event:l}),l.message)),Hl.default.createElement("div",{style:{height:t.paddingBottom}}))}};o(im,"EventLogList"),im.propTypes={events:DS.default.array.isRequired,rowHeight:DS.default.number},im.defaultProps={rowHeight:18};function WI({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(WI,"LogIcon");var HO=Uy(im);var om=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:m}=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(p=>Wl.default.createElement(AS,{key:p,text:p,checked:n[p],onToggle:()=>d(p)})),Wl.default.createElement("i",{onClick:m,className:"fa fa-close"}))),Wl.default.createElement(HO,{events:l}))}};o(om,"PureEventLog"),Tc(om,"propTypes",{filters:Pp.default.object.isRequired,events:Pp.default.array.isRequired,toggleFilter:Pp.default.func.isRequired,close:Pp.default.func.isRequired,defaultHeight:Pp.default.number}),Tc(om,"defaultProps",{defaultHeight:200});var WO=Di(e=>({filters:e.eventLog.filters,events:e.eventLog.view}),{close:mp,toggleFilter:ok})(om);var qr=pe(De());function RS(){let e=it(R=>R.conf.version),{mode:t,intercept:n,showhost:l,upstream_cert:d,rawtcp:m,dns_server:p,http2:x,websocket:_,anticache:O,anticomp:D,stickyauth:Y,stickycookie:U,stream_large_bodies:X,listen_host:te,listen_port:Q,server:F,ssl_insecure:M}=it(R=>R.options);return qr.createElement("footer",null,t&&t!=="regular"&&qr.createElement("span",{className:"label label-success"},t," mode"),n&&qr.createElement("span",{className:"label label-success"},"Intercept: ",n),M&&qr.createElement("span",{className:"label label-danger"},"ssl_insecure"),l&&qr.createElement("span",{className:"label label-success"},"showhost"),!d&&qr.createElement("span",{className:"label label-success"},"no-upstream-cert"),!m&&qr.createElement("span",{className:"label label-success"},"no-raw-tcp"),p&&qr.createElement("span",{className:"label label-success"},"dns-server"),!x&&qr.createElement("span",{className:"label label-success"},"no-http2"),!_&&qr.createElement("span",{className:"label label-success"},"no-websocket"),O&&qr.createElement("span",{className:"label label-success"},"anticache"),D&&qr.createElement("span",{className:"label label-success"},"anticomp"),Y&&qr.createElement("span",{className:"label label-success"},"stickyauth: ",Y),U&&qr.createElement("span",{className:"label label-success"},"stickycookie: ",U),X&&qr.createElement("span",{className:"label label-success"},"stream: ",Kg(X)),qr.createElement("div",{className:"pull-right"},qr.createElement(No,null,F&&qr.createElement("span",{className:"label label-primary",title:"HTTP Proxy Server Address"},te||"*",":",Q)),qr.createElement("span",{className:"label label-default",title:"Mitmproxy Version"},"mitmproxy ",e)))}o(RS,"Footer");var US=pe(De());var BS=pe(De());var Mp=pe(De());function FS({children:e}){return Mp.createElement("div",null,Mp.createElement("div",{className:"modal-backdrop fade in"}),Mp.createElement("div",{className:"modal modal-visible",id:"optionsModal",tabIndex:-1,role:"dialog","aria-labelledby":"options"},Mp.createElement("div",{className:"modal-dialog modal-lg",role:"document"},Mp.createElement("div",{className:"modal-content"},e))))}o(FS,"ModalLayout");var pr=pe(De());var Mo=pe(De()),Ao=pe(Jh());var BO=pe(Xn()),BI=o(e=>{e.key!=="Escape"&&e.stopPropagation()},"stopPropagation");IS.propTypes={value:Ao.default.bool.isRequired,onChange:Ao.default.func.isRequired};function IS(l){var d=l,{value:e,onChange:t}=d,n=Ds(d,["value","onChange"]);return Mo.default.createElement("div",{className:"checkbox"},Mo.default.createElement("label",null,Mo.default.createElement("input",Pe({type:"checkbox",checked:e,onChange:m=>t(m.target.checked)},n)),"Enable"))}o(IS,"BooleanOption");HS.propTypes={value:Ao.default.string,onChange:Ao.default.func.isRequired};function HS(l){var d=l,{value:e,onChange:t}=d,n=Ds(d,["value","onChange"]);return Mo.default.createElement("input",Pe({type:"text",value:e||"",onChange:m=>t(m.target.value)},n))}o(HS,"StringOption");function UI(e){return function(l){var d=l,{onChange:t}=d,n=Ds(d,["onChange"]);return Mo.default.createElement(e,Pe({onChange:m=>t(m||null)},n))}}o(UI,"Optional");UO.propTypes={value:Ao.default.number.isRequired,onChange:Ao.default.func.isRequired};function UO(l){var d=l,{value:e,onChange:t}=d,n=Ds(d,["value","onChange"]);return Mo.default.createElement("input",Pe({type:"number",value:e,onChange:m=>t(parseInt(m.target.value))},n))}o(UO,"NumberOption");$O.propTypes={value:Ao.default.string.isRequired,onChange:Ao.default.func.isRequired};function $O(d){var m=d,{value:e,onChange:t,choices:n}=m,l=Ds(m,["value","onChange","choices"]);return Mo.default.createElement("select",Pe({onChange:p=>t(p.target.value),value:e},l),n.map(p=>Mo.default.createElement("option",{key:p,value:p},p)))}o($O,"ChoicesOption");zO.propTypes={value:Ao.default.arrayOf(Ao.default.string).isRequired,onChange:Ao.default.func.isRequired};function zO(l){var d=l,{value:e,onChange:t}=d,n=Ds(d,["value","onChange"]);let m=Math.max(e.length,1);return Mo.default.createElement("textarea",Pe({rows:m,value:e.join(` -`),onChange:p=>t(p.target.value.split(` -`))},n))}o(zO,"StringSequenceOption");var $I={bool:IS,str:HS,int:UO,"optional str":UI(HS),"sequence of str":zO};function zI({choices:e,type:t,value:n,onChange:l,name:d,error:m}){let p,x={};if(e)p=$O,x.choices=e;else if(p=$I[t],!p)throw`unknown option type ${t}`;return p!==IS&&(x.className="form-control"),Mo.default.createElement("div",{className:(0,BO.default)({"has-error":m})},Mo.default.createElement(p,Pe({name:d,value:n,onChange:l,onKeyDown:BI},x)))}o(zI,"PureOption");var jO=Di((e,{name:t})=>Pe(Pe({},e.options_meta[t]),e.ui.optionsEditor[t]),(e,{name:t})=>({onChange:n=>e(vp(t,n))}))(zI);var Yy=pe(Oh());function jI({help:e}){return pr.default.createElement("div",{className:"help-block small"},e)}o(jI,"PureOptionHelp");var qI=Di((e,{name:t})=>({help:e.options_meta[t].help}))(jI);function VI({error:e}){return e?pr.default.createElement("div",{className:"small text-danger"},e):null}o(VI,"PureOptionError");var KI=Di((e,{name:t})=>({error:e.ui.optionsEditor[t]&&e.ui.optionsEditor[t].error}))(VI);function GI({value:e,defaultVal:t}){if(e===t)return null;if(typeof t=="boolean")t=t?"true":"false";else if(Array.isArray(t)){if(Yy.default.isEmpty(Yy.default.compact(e))&&Yy.default.isEmpty(t))return null;t="[ ]"}else t===""?t='""':t===null&&(t="null");return pr.default.createElement("div",{className:"small"},"Default: ",pr.default.createElement("strong",null," ",t," ")," ")}o(GI,"PureOptionDefault");var YI=Di((e,{name:t})=>({value:e.options[t],defaultVal:e.options_meta[t].default}))(GI),WS=class extends pr.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 pr.default.createElement("div",null,pr.default.createElement("div",{className:"modal-header"},pr.default.createElement("button",{type:"button",className:"close","data-dismiss":"modal",onClick:()=>{t()}},pr.default.createElement("i",{className:"fa fa-fw fa-times"})),pr.default.createElement("div",{className:"modal-title"},pr.default.createElement("h4",null,l))),pr.default.createElement("div",{className:"modal-body"},pr.default.createElement("div",{className:"form-horizontal"},n.map(d=>pr.default.createElement("div",{key:d,className:"form-group"},pr.default.createElement("div",{className:"col-xs-6"},pr.default.createElement("label",{htmlFor:d},d),pr.default.createElement(qI,{name:d})),pr.default.createElement("div",{className:"col-xs-6"},pr.default.createElement(jO,{name:d}),pr.default.createElement(KI,{name:d}),pr.default.createElement(YI,{name:d})))))),pr.default.createElement("div",{className:"modal-footer"}))}};o(WS,"PureOptionModal");var qO=Di(e=>({options:Object.keys(e.options_meta).sort()}),{hideModal:my,save:yk})(WS);function XI(){return BS.createElement(FS,null,BS.createElement(qO,null))}o(XI,"OptionModal");var VO=[XI];function $S(){let e=it(n=>n.ui.modal.activeModal),t=VO.find(n=>n.name===e);return e&&t!==void 0?US.createElement(t,null):US.createElement("div",null)}o($S,"PureModal");var zS=class extends Rn.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),Rn.default.createElement("div",{className:"container"},Rn.default.createElement("h1",null,"mitmproxy has crashed."),Rn.default.createElement("pre",null,this.state.error.stack,Rn.default.createElement("br",null),Rn.default.createElement("br",null),"Component Stack:",(l=this.state.errorInfo)==null?void 0:l.componentStack),Rn.default.createElement("p",null,"Please lodge a bug report at ",Rn.default.createElement("a",{href:"https://github.com/mitmproxy/mitmproxy/issues"},"https://github.com/mitmproxy/mitmproxy/issues"),"."))):Rn.default.createElement("div",{id:"container",tabIndex:0},Rn.default.createElement(LS,null),Rn.default.createElement(ES,null),n&&Rn.default.createElement(PS,{key:"commandbar"}),t&&Rn.default.createElement(WO,{key:"eventlog"}),Rn.default.createElement(RS,null),Rn.default.createElement($S,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(zS,"ProxyAppMain");var KO=Di(e=>({showEventLog:e.eventLog.visible,showCommandBar:e.commandBar.visible}),{onKeyDown:fO})(zS);var au={SEARCH:"s",HIGHLIGHT:"h",SHOW_EVENTLOG:"e",SHOW_COMMANDBAR:"c"};function QI(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,m]=l.slice(1);e.dispatch(wf(d)),e.dispatch(mf(m))}n&&n.split("&").forEach(d=>{let[m,p]=d.split("=",2);switch(p=decodeURIComponent(p),m){case au.SEARCH:e.dispatch(sy(p));break;case au.HIGHLIGHT:e.dispatch(ly(p));break;case au.SHOW_EVENTLOG:e.getState().eventLog.visible||e.dispatch(mp());break;case au.SHOW_COMMANDBAR:e.getState().commandBar.visible||e.dispatch(wy());break;default:console.error(`unimplemented query arg: ${d}`)}})}o(QI,"updateStoreFromUrl");function ZI(e){let t=e.getState(),n={[au.SEARCH]:t.flows.filter,[au.HIGHLIGHT]:t.flows.highlight,[au.SHOW_EVENTLOG]:t.eventLog.visible,[au.SHOW_COMMANDBAR]:t.commandBar.visible},l=Object.keys(n).filter(p=>n[p]).map(p=>`${p}=${encodeURIComponent(n[p])}`).join("&"),d;t.flows.selected.length>0?d=`/flows/${t.flows.selected[0]}/${t.ui.flow.tab}`:d="/flows",l&&(d+="?"+l);let m=window.location.pathname;m==="blank"&&(m="/"),window.location.hash.substr(1)!==d&&history.replaceState(void 0,"",`${m}#${d}`)}o(ZI,"updateUrlFromStore");function jS(e){QI(e),e.subscribe(()=>ZI(e))}o(jS,"initialize");var JI="reset",sm=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("flows"),this.fetchData("events"),this.fetchData("options"),this.store.dispatch(hk())}fetchData(t){let n=[];this.activeFetches[t]=n,Ot(`./${t}`).then(l=>l.json()).then(l=>{this.activeFetches[t]===n&&this.receive(t,l)})}onMessage(t){if(t.cmd===JI)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(Pe({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(m=>this.onMessage(m)),Object.keys(this.activeFetches).length===0&&this.store.dispatch(mk())}onClose(t){this.store.dispatch(vk(`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(sm,"WebsocketBackend");var lm=class{constructor(t){this.store=t,this.onOpen()}onOpen(){this.fetchData("flows"),this.fetchData("options")}fetchData(t){Ot(`./${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(lm,"StaticBackend");jS(gp);window.MITMWEB_STATIC?window.backend=new lm(gp):window.backend=new sm(gp);window.addEventListener("error",e=>{gp.dispatch(sk(`${e.message} -${e.error.stack}`))});document.addEventListener("DOMContentLoaded",()=>{(0,GO.render)(qS.createElement(tx,{store:gp},qS.createElement(KO,null)),document.getElementById("mitmproxy"))});})(); +`),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(pF,"useSelectorWithStoreAndSubscription");function Wk(e){e===void 0&&(e=ei);var t=e===ei?y0:function(){return(0,ro.useContext)(e)};return o(function(l,d){d===void 0&&(d=cF);var h=t(),c=h.store,v=h.subscription,C=pF(l,d,c,v);return(0,ro.useDebugValue)(C),C},"useSelector")}o(Wk,"createSelectorHook");var eS=Wk();var tS=fe(iu());pk(tS.unstable_batchedUpdates);var Wn=fe(Oe());var Uk="UI_FLOWVIEW_SET_TAB",zk="SET_CONTENT_VIEW_FOR",dF={tab:"request",contentViewFor:{}};function rS(e=dF,t){switch(t.type){case zk:return Pt(ke({},e),{contentViewFor:Pt(ke({},e.contentViewFor),{[t.messageId]:t.contentView})});case Uk:return Pt(ke({},e),{tab:t.tab?t.tab:"request"});default:return e}}o(rS,"reducer");function Lf(e){return{type:Uk,tab:e}}o(Lf,"selectTab");function x0(e,t){return{type:zk,messageId:e,contentView:t}}o(x0,"setContentViewFor");var $k=fe(Qh()),hF=fe(Oe());window._=$k.default;window.React=hF;var S0=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"),C0=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={};Wb(Jh,{ADD:()=>uS,RECEIVE:()=>pS,REMOVE:()=>cS,SET_FILTER:()=>lS,SET_SORT:()=>aS,UPDATE:()=>fS,add:()=>vF,defaultState:()=>b0,receive:()=>xF,reduce:()=>Lp,remove:()=>wF,setFilter:()=>dS,setSort:()=>qk,update:()=>yF});var sS=fe(jk()),lS="LIST_SET_FILTER",aS="LIST_SET_SORT",uS="LIST_ADD",fS="LIST_UPDATE",cS="LIST_REMOVE",pS="LIST_RECEIVE",b0={byId:{},list:[],listIndex:{},view:[],viewIndex:{}};function Lp(e=b0,t){let{byId:n,list:l,listIndex:d,view:h,viewIndex:c}=e;switch(t.type){case lS:h=(0,sS.default)(l.filter(t.filter),t.sort),c={},h.forEach((k,O)=>{c[k.id]=O});break;case aS:h=(0,sS.default)([...h],t.sort),c={},h.forEach((k,O)=>{c[k.id]=O});break;case uS: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}=Vk(e,t.item,t.sort));break;case fS: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}=Vk(e,t.item,t.sort):!C&&v?{data:h,dataIndex:c}=hS(h,c,t.item.id):C&&v&&({view:h,viewIndex:c}=SF(e,t.item,t.sort));break;case cS:if(!(t.id in n))break;n=ke({},n),delete n[t.id],{data:l,dataIndex:d}=hS(l,d,t.id),t.id in c&&({data:h,dataIndex:c}=hS(h,c,t.id));break;case pS: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 dS(e=E0,t=Zh){return{type:lS,filter:e,sort:t}}o(dS,"setFilter");function qk(e=Zh){return{type:aS,sort:e}}o(qk,"setSort");function vF(e,t=E0,n=Zh){return{type:uS,item:e,filter:t,sort:n}}o(vF,"add");function yF(e,t=E0,n=Zh){return{type:fS,item:e,filter:t,sort:n}}o(yF,"update");function wF(e){return{type:cS,id:e}}o(wF,"remove");function xF(e,t=E0,n=Zh){return{type:pS,list:e,filter:t,sort:n}}o(xF,"receive");function Vk(e,t,n){let l=CF(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(Vk,"sortedInsert");function hS(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(hS,"removeData");function SF(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(SF,"sortedUpdate");function CF(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(CF,"sortedIndex");function E0(){return!0}o(E0,"defaultFilter");function Zh(e,t){return 0}o(Zh,"defaultSort");var Kk={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"),_0=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"'},Zt=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"'},Jt=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=Zt()),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=Jt(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/mitmproxy/issues/3609",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/mitmproxy/issues/3609",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/mitmproxy/issues/3609",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&&PwS,icon:()=>L0,method:()=>tm,path:()=>P0,quickactions:()=>Pp,size:()=>O0,status:()=>nm,time:()=>M0,timestamp:()=>A0,tls:()=>N0,version:()=>rm});var Xt=fe(Oe());var k0=fe(ti());var N0=o(({flow:e})=>Xt.default.createElement("td",{className:(0,k0.default)("col-tls",e.client_conn.tls_established?"col-tls-https":"col-tls-http")}),"tls");N0.headerName="";N0.sortKey=e=>e.type==="http"&&e.request.scheme;var L0=o(({flow:e})=>Xt.default.createElement("td",{className:"col-icon"},Xt.default.createElement("div",{className:(0,k0.default)("resource-icon",Gk(e))})),"icon");L0.headerName="";L0.sortKey=e=>Gk(e);var Gk=o(e=>{if(e.type!=="http")return e.client_conn.tls_version==="QUIC"?"resource-icon-quic":`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"),Yk=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"),P0=o(({flow:e})=>{let t;return e.error&&(e.error.msg==="Connection killed."?t=Xt.default.createElement("i",{className:"fa fa-fw fa-times pull-right"}):t=Xt.default.createElement("i",{className:"fa fa-fw fa-exclamation pull-right"})),Xt.default.createElement("td",{className:"col-path"},e.is_replay==="request"&&Xt.default.createElement("i",{className:"fa fa-fw fa-repeat pull-right"}),e.intercepted&&Xt.default.createElement("i",{className:"fa fa-fw fa-pause pull-right"}),t,Xt.default.createElement("span",{className:"marker pull-right"},e.marked),Yk(e))},"path");P0.headerName="Path";P0.sortKey=e=>Yk(e);var tm=o(({flow:e})=>Xt.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})=>Xt.default.createElement("td",{className:"col-http-version"},rm.sortKey(e)),"version");rm.headerName="Version";rm.sortKey=e=>{switch(e.type){case"http":return e.request.http_version;default:return""}};var nm=o(({flow:e})=>{let t="darkred";return e.type!=="http"&&e.type!="dns"||!e.response?Xt.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"),Xt.default.createElement("td",{className:"col-status",style:{color:t}},nm.sortKey(e)))},"status");nm.headerName="Status";nm.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 O0=o(({flow:e})=>Xt.default.createElement("td",{className:"col-size"},S0(yS(e))),"size");O0.headerName="Size";O0.sortKey=e=>yS(e);var M0=o(({flow:e})=>{let t=em(e),n=vS(e);return Xt.default.createElement("td",{className:"col-time"},t&&n?C0(1e3*(n-t)):"...")},"time");M0.headerName="Time";M0.sortKey=e=>{let t=em(e),n=vS(e);return t&&n&&n-t};var A0=o(({flow:e})=>{let t=em(e);return Xt.default.createElement("td",{className:"col-timestamp"},t?no(t):"...")},"timestamp");A0.headerName="Start time";A0.sortKey=e=>em(e);var Pp=o(({flow:e})=>{let t=Gs(),[n,l]=(0,Xt.useState)(!1),d=null;return e.intercepted?d=Xt.default.createElement("a",{href:"#",className:"quickaction",onClick:()=>t(Op(e))},Xt.default.createElement("i",{className:"fa fa-fw fa-play text-success"})):_0(e)&&(d=Xt.default.createElement("a",{href:"#",className:"quickaction",onClick:()=>t(Mp(e))},Xt.default.createElement("i",{className:"fa fa-fw fa-repeat text-primary"}))),Xt.default.createElement("td",{className:(0,k0.default)("col-quickactions",{hover:n}),onClick:()=>0},d?Xt.default.createElement("div",null,d):Xt.default.createElement(Xt.default.Fragment,null))},"quickactions");Pp.headerName="";Pp.sortKey=e=>0;var wS={icon:L0,method:tm,version:rm,path:P0,quickactions:Pp,size:O0,status:nm,time:M0,timestamp:A0,tls:N0};var _F="FLOWS_ADD",TF="FLOWS_UPDATE",Xk="FLOWS_REMOVE",kF="FLOWS_RECEIVE",Qk="FLOWS_SELECT",Zk="FLOWS_SET_FILTER",Jk="FLOWS_SET_SORT",eN="FLOWS_SET_HIGHLIGHT",NF=ke({highlight:void 0,filter:void 0,sort:{column:void 0,desc:!1},selected:[]},b0);function xS(e=NF,t){switch(t.type){case _F:case TF:case Xk:case kF:let n=Jh[t.cmd](t.data,tN(e.filter),SS(e.sort)),l=e.selected;if(t.type===Xk&&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 Zk:return ke(Pt(ke({},e),{filter:t.filter}),Lp(e,dS(tN(t.filter),SS(e.sort))));case eN:return Pt(ke({},e),{highlight:t.highlight});case Jk:return ke(Pt(ke({},e),{sort:t.sort}),Lp(e,qk(SS(t.sort))));case Qk:return Pt(ke({},e),{selected:t.flowIds});default:return e}}o(xS,"reducer");function tN(e){if(!!e)return Of.parse(e)}o(tN,"makeFilter");function SS({column:e,desc:t}){if(!e)return(l,d)=>0;let n=wS[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 I0(){return e=>kt("/flows/resume",{method:"POST"})}o(I0,"resumeAll");function F0(e){return t=>kt(`/flows/${e.id}/kill`,{method:"POST"})}o(F0,"kill");function nN(){return e=>kt("/flows/kill",{method:"POST"})}o(nN,"killAll");function B0(e){return t=>kt(`/flows/${e.id}`,{method:"DELETE"})}o(B0,"remove");function H0(e){return t=>kt(`/flows/${e.id}/duplicate`,{method:"POST"})}o(H0,"duplicate");function Mp(e){return t=>kt(`/flows/${e.id}/replay`,{method:"POST"})}o(Mp,"replay");function W0(e){return t=>kt(`/flows/${e.id}/revert`,{method:"POST"})}o(W0,"revert");function Wi(e,t){return n=>kt.put(`/flows/${e.id}`,t)}o(Wi,"update");function iN(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(iN,"uploadContent");function U0(){return e=>kt("/clear",{method:"POST"})}o(U0,"clear");function oN(e){let t=new FormData;return t.append("file",e),n=>kt("/flows/dump",{method:"POST",body:t})}o(oN,"upload");function Af(e){return{type:Qk,flowIds:e?[e]:[]}}o(Af,"select");var z0="UI_HIDE_MODAL",sN="UI_SET_ACTIVE_MODAL",LF={activeModal:void 0};function CS(e=LF,t){switch(t.type){case sN:return Pt(ke({},e),{activeModal:t.activeModal});case z0:return Pt(ke({},e),{activeModal:void 0});default:return e}}o(CS,"reducer");function lN(e){return{type:sN,activeModal:e}}o(lN,"setActiveModal");function $0(){return{type:z0}}o($0,"hideModal");var wm=fe(Oe());var Ut=fe(Oe());var Dp=fe(Oe());var om=fe(Oe()),aN=fe(ti()),uN=(()=>{let e=document.createElement("div");return e.setAttribute("contenteditable","PLAINTEXT-ONLY"),e.contentEditable==="plaintext-only"?"plaintext-only":"true"})(),Ap=!1,Xs=class extends om.Component{constructor(){super(...arguments);this.input=om.default.createRef();this.isEditing=o(()=>{var t;return((t=this.input.current)==null?void 0:t.contentEditable)===uN},"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=uN,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,aN.default)("inline-input",this.props.className);return om.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 fN=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,fN.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 bS(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}o(bS,"_defineProperty");function cN(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(cN,"ownKeys");function j0(e){for(var t=1;tn[l.level])));case hN:case MF:return ke(ke({},e),Lp(e,Jh[t.cmd](t.data,l=>e.filters[l.level])));default:return e}}o(kS,"reduce");function vN(e){return{type:gN,filter:e}}o(vN,"toggleFilter");function Rp(){return{type:mN}}o(Rp,"toggleVisibility");function yN(e,t="web"){let n={id:Math.random().toString(),message:e,level:t};return{type:hN,cmd:"add",data:n}}o(yN,"add");var wN="UI_OPTION_UPDATE_START",xN="UI_OPTION_UPDATE_SUCCESS",SN="UI_OPTION_UPDATE_ERROR",DF={};function NS(e=DF,t){switch(t.type){case wN:return Pt(ke({},e),{[t.option]:{isUpdating:!0,value:t.value,error:!1}});case xN:return Pt(ke({},e),{[t.option]:void 0});case SN: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 z0:return{};default:return e}}o(NS,"reducer");function CN(e,t){return{type:wN,option:e,value:t}}o(CN,"startUpdate");function bN(e){return{type:xN,option:e}}o(bN,"updateSuccess");function EN(e,t){return{type:SN,option:e,error:t}}o(EN,"updateError");var _N=V0({flow:rS,modal:CS,optionsEditor:NS});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 RF={state:ni.INIT,message:void 0};function LS(e=RF,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(LS,"reducer");function TN(){return{type:ni.FETCHING}}o(TN,"startFetching");function kN(){return{type:ni.ESTABLISHED}}o(kN,"connectionEstablished");function NN(e){return{type:ni.ERROR,message:e}}o(NN,"connectionError");var LN={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,hardump:"",http2:!0,http2_ping_keepalive:58,http3:!0,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",proxy_debug:!1,proxyauth:void 0,rawtcp:!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_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:[],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_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 PS="OPTIONS_RECEIVE",OS="OPTIONS_UPDATE";function MS(e=LN,t){switch(t.type){case PS:let n={};for(let[d,{value:h}]of Object.entries(t.data))n[d]=h;return n;case OS:let l=ke({},e);for(let[d,{value:h}]of Object.entries(t.data))l[d]=h;return l;default:return e}}o(MS,"reducer");function IF(e,t,n){return Ia(this,null,function*(){try{let l=yield kt.put("/options",{[e]:t});if(l.status===200)n(bN(e));else throw yield l.text()}catch(l){n(EN(e,l))}})}o(IF,"pureSendUpdate");var FF=IF;function Ip(e,t){return n=>{n(CN(e,t)),FF(e,t,n)}}o(Ip,"update");function PN(){return e=>kt("/options/save",{method:"POST"})}o(PN,"save");var ON="COMMANDBAR_TOGGLE_VISIBILITY",BF={visible:!1};function AS(e=BF,t){switch(t.type){case ON:return Pt(ke({},e),{visible:!e.visible});default:return e}}o(AS,"reducer");function K0(){return{type:ON}}o(K0,"toggleVisibility");function MN(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(MN,"createThunkMiddleware");var AN=MN();AN.withExtraArgument=MN;var DN=AN;var HF="STATE_RECEIVE",WF="STATE_UPDATE",UF={available:!1,version:"",contentViews:[],servers:[]};function DS(e=UF,t){switch(t.type){case HF:case WF:return ke(Pt(ke({},e),{available:!0}),t.data);default:return e}}o(DS,"reducer");var zF={},$F=o((e=zF,t)=>{switch(t.type){case PS:return t.data;case OS:return ke(ke({},e),t.data);default:return e}},"reducer"),RN=$F;var jF=window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__||TS,qF=V0({commandBar:AS,eventLog:kS,flows:xS,connection:LS,ui:_N,options:MS,options_meta:RN,backendState:DS}),VF=o(e=>_S(qF,e,jF(dN(DN))),"createAppStore"),Fp=VF(void 0),Qt=o(()=>Gs(),"useAppDispatch"),qe=eS;var io=fe(Oe());var IN=fe(Qh()),FN=fe(ti()),RS=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(RS,"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(RS,{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,FN.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,IN.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 sm=fe(Oe());function G0(e,t){let[n,l]=(0,sm.useState)(),[d,h]=(0,sm.useState)();return(0,sm.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(G0,"useContent");var lm=fe(Oe()),Y0=lm.default.memo(o(function({icon:t,text:n,className:l,title:d,onOpenFile:h,onClick:c}){let v;return lm.default.createElement("a",{href:"#",onClick:C=>{v.click(),c&&c(C)},className:l,title:d},lm.default.createElement("i",{className:"fa fa-fw "+t}),n,lm.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()),BN=fe(ti());function kr({onClick:e,children:t,icon:n,disabled:l,className:d,title:h}){return Hp.createElement("button",{className:(0,BN.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 um=fe(Oe()),jN=fe(Oe());var am=fe(Oe()),WN=fe(ti()),UN=fe(HN()),zN=fe(Qh());function $N(e){return e&&e.replace(/\r\n|\r/g,` +`)}o($N,"normalizeLineEndings");var Wp=class extends am.Component{constructor(t){super(t);this.state={isFocused:!1}}getCodeMirrorInstance(){return this.props.codeMirrorInstance||UN.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&&$N(this.codeMirror.getValue())!==$N(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);zN.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,WN.default)("ReactCodeMirror",this.state.isFocused?"ReactCodeMirror--focused":null,this.props.className);return am.createElement("div",{className:t},am.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 fm=class extends jN.Component{constructor(){super(...arguments);this.editor=um.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 um.createElement("div",{className:"codeeditor",onKeyDown:n=>n.stopPropagation()},um.createElement(Wp,{ref:this.editor,value:this.props.initialContent,onChange:()=>0,options:t}))},"render")}};o(fm,"CodeEditor");var Rf=fe(Oe()),KF=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")),X0=KF;var zf=fe(Oe());var xi=fe(Oe());var Q0=fe(Oe());var BS=o(function(t){return t.reduce(function(n,l){var d=l[0],h=l[1];return n[d]=h,n},{})},"fromEntries"),HS=typeof window!="undefined"&&window.document&&window.document.createElement?Q0.useLayoutEffect:Q0.useEffect;var fu=fe(Oe());var Gr="top",Ln="bottom",ln="right",an="left",Z0="auto",su=[Gr,Ln,ln,an],Rl="start",J0="end",qN="clippingParents",ey="viewport",Up="popper",VN="reference",WS=su.reduce(function(e,t){return e.concat([t+"-"+Rl,t+"-"+J0])},[]),ty=[].concat(su,[Z0]).reduce(function(e,t){return e.concat([t,t+"-"+Rl,t+"-"+J0])},[]),GF="beforeRead",YF="read",XF="afterRead",QF="beforeMain",ZF="main",JF="afterMain",e3="beforeWrite",t3="write",r3="afterWrite",KN=[GF,YF,XF,QF,ZF,JF,e3,t3,r3];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 ry(e){if(typeof ShadowRoot=="undefined")return!1;var t=Br(e).ShadowRoot;return e instanceof t||e instanceof ShadowRoot}o(ry,"isShadowRoot");function n3(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(n3,"applyStyles");function i3(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(i3,"effect");var GN={name:"applyStyles",enabled:!0,phase:"write",fn:n3,effect:i3,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 cm(e,t){var n=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(n&&ry(n)){var l=t;do{if(l&&e.isSameNode(l))return!0;l=l.parentNode||l.host}while(l)}return!1}o(cm,"contains");function wi(e){return Br(e).getComputedStyle(e)}o(wi,"getComputedStyle");function US(e){return["table","td","th"].indexOf(Pn(e))>=0}o(US,"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||(ry(e)?e.host:null)||Bn(e)}o(Fl,"getParentNode");function YN(e){return!Yr(e)||wi(e).position==="fixed"?null:e.offsetParent}o(YN,"getTrueOffsetParent");function o3(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(o3,"getContainingBlock");function as(e){for(var t=Br(e),n=YN(e);n&&US(n)&&wi(n).position==="static";)n=YN(n);return n&&(Pn(n)==="html"||Pn(n)==="body"&&wi(n).position==="static")?t:n||o3(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,pm=Math.round;function Bf(e,t,n){return Ao(e,au(t,n))}o(Bf,"within");function dm(){return{top:0,right:0,bottom:0,left:0}}o(dm,"getFreshSideObject");function hm(e){return Object.assign({},dm(),e)}o(hm,"mergePaddingObject");function mm(e,t){return t.reduce(function(n,l){return n[l]=e,n},{})}o(mm,"expandToHashMap");var s3=o(function(t,n){return t=typeof t=="function"?t(Object.assign({},n.rects,{placement:n.placement})):t,hm(typeof t!="number"?t:mm(t,su))},"toPaddingObject");function l3(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=s3(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(l3,"arrow");function a3(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)||!cm(t.elements.popper,d)||(t.elements.arrow=d))}o(a3,"effect");var XN={name:"arrow",enabled:!0,phase:"main",fn:l3,effect:a3,requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};var u3={top:"auto",right:"auto",bottom:"auto",left:"auto"};function f3(e){var t=e.x,n=e.y,l=window,d=l.devicePixelRatio||1;return{x:pm(pm(t*d)/d)||0,y:pm(pm(n*d)/d)||0}}o(f3,"roundOffsetsByDPR");function QN(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?f3(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&&u3);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(QN,"mapToStyles");function c3(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,QN(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,QN(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(c3,"computeStyles");var ZN={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:c3,data:{}};var ny={passive:!0};function p3(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,ny)}),v&&C.addEventListener("resize",n.update,ny),function(){h&&k.forEach(function(O){O.removeEventListener("scroll",n.update,ny)}),v&&C.removeEventListener("resize",n.update,ny)}}o(p3,"effect");var JN={name:"eventListeners",enabled:!0,phase:"write",fn:o(function(){},"fn"),effect:p3,data:{}};var d3={left:"right",right:"left",bottom:"top",top:"bottom"};function zp(e){return e.replace(/left|right|bottom|top/g,function(t){return d3[t]})}o(zp,"getOppositePlacement");var h3={start:"end",end:"start"};function iy(e){return e.replace(/start|end/g,function(t){return h3[t]})}o(iy,"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 zS(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(zS,"getViewportRect");function $S(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($S,"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 oy(e){return["html","body","#document"].indexOf(Pn(e))>=0?e.ownerDocument.body:Yr(e)&&Uf(e)?e:oy(Fl(e))}o(oy,"getScrollParent");function uu(e,t){var n;t===void 0&&(t=[]);var l=oy(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 m3(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(m3,"getInnerBoundingClientRect");function eL(e,t){return t===ey?$p(zS(e)):Yr(t)?m3(t):$p($S(Bn(e)))}o(eL,"getClientRectFromMixedType");function g3(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)&&cm(d,l)&&Pn(d)!=="body"}):[]}o(g3,"getClippingParents");function jS(e,t,n){var l=t==="clippingParents"?g3(e):[].concat(t),d=[].concat(l,[n]),h=d[0],c=d.reduce(function(v,C){var k=eL(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},eL(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(jS,"getClippingRect");function Qs(e){return e.split("-")[1]}o(Qs,"getVariation");function gm(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 J0:C[k]=C[k]+(t[O]/2-n[O]/2);break;default:}}return C}o(gm,"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?qN:h,v=n.rootBoundary,C=v===void 0?ey: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=hm(typeof J!="number"?J:mm(J,su)),R=O===Up?VN:Up,A=e.elements.reference,I=e.rects.popper,G=e.elements[B?R:O],K=jS(Il(G)?G:G.contextElement||Bn(e.elements.popper),c,C),se=Mo(A),ne=gm({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 qS(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?ty:C,O=Qs(l),j=O?v?WS:WS.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(qS,"computeAutoPlacement");function v3(e){if(On(e)===Z0)return[];var t=zp(e);return[iy(e),t,iy(t)]}o(v3,"getExpandedFallbackPlacements");function y3(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)]:v3(R)),K=[R].concat(G).reduce(function(ut,Lr){return ut.concat(On(Lr)===Z0?qS(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,Zt=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=Zt(_t);if(Ct==="break")break}t.placement!==xe&&(t.modifiersData[l]._skip=!0,t.placement=xe,t.reset=!0)}}o(y3,"flip");var tL={name:"flip",enabled:!0,phase:"main",fn:y3,requiresIfExists:["offset"],data:{_skip:!1}};function rL(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(rL,"getSideOffsets");function nL(e){return[Gr,ln,Ln,an].some(function(t){return e[t]>=0})}o(nL,"isAnySideFullyClipped");function w3(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=rL(c,l),k=rL(v,d,h),O=nL(C),j=nL(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(w3,"hide");var iL={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:w3};function x3(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(x3,"distanceAndSkiddingToXY");function S3(e){var t=e.state,n=e.options,l=e.name,d=n.offset,h=d===void 0?[0,0]:d,c=ty.reduce(function(O,j){return O[j]=x3(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(S3,"offset");var oL={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:S3};function C3(e){var t=e.state,n=e.name;t.modifiersData[n]=gm({reference:t.rects.reference,element:t.rects.popper,strategy:"absolute",placement:t.placement})}o(C3,"popperOffsets");var sL={name:"popperOffsets",enabled:!0,phase:"read",fn:C3,data:{}};function VS(e){return e==="x"?"y":"x"}o(VS,"getAltAxis");function b3(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=VS(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,Zt=X&&Hr?If(Hr):{width:0,height:0},_t=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:dm(),Ct=_t[tt],ut=_t[_e],Lr=Bf(0,pe[St],Zt[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],Jt=Bf(X?au(si,Gt):si,Un,X?Ao(cn,Yt):cn);ne[se]=Jt,Ve[se]=Jt-Un}}t.modifiersData[l]=Ve}}o(b3,"preventOverflow");var lL={name:"preventOverflow",enabled:!0,phase:"main",fn:b3,requiresIfExists:["offset"]};function KS(e){return{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}}o(KS,"getHTMLElementScroll");function GS(e){return e===Br(e)||!Yr(e)?Hf(e):KS(e)}o(GS,"getNodeScroll");function E3(e){var t=e.getBoundingClientRect(),n=t.width/e.offsetWidth||1,l=t.height/e.offsetHeight||1;return n!==1||l!==1}o(E3,"isElementScaled");function YS(e,t,n){n===void 0&&(n=!1);var l=Yr(t),d=Yr(t)&&E3(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=GS(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(YS,"getCompositeRect");function _3(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(_3,"order");function XS(e){var t=_3(e);return KN.reduce(function(n,l){return n.concat(t.filter(function(d){return d.phase===l}))},[])}o(XS,"orderModifiers");function QS(e){var t;return function(){return t||(t=new Promise(function(n){Promise.resolve().then(function(){t=void 0,n(e())})})),t}}o(QS,"debounce");function ZS(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(ZS,"mergeByName");var aL={placement:"bottom",modifiers:[],strategy:"absolute"};function uL(){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}=eC(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,hL.default)(d,{open:j}),onClick:G=>{G.preventDefault(),A(!0)}},c),t),I)},"Dropdown"));function vm({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(vm,"ViewSelector");function tC({flow:e,message:t}){let n=Qt(),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=G0(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(fm,{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(Y0,{icon:"fa-upload",text:"Replace",title:"Upload a file to replace the content.",onOpenFile:Z=>n(iN(e,Z,l)),className:"btn btn-default btn-xs"}),"\xA0",tr.default.createElement(vm,{value:d,onChange:Z=>n(x0(e.id+l,Z))})),rC.matches(t)&&tr.default.createElement(rC,{flow:e,message:t}),tr.default.createElement(X0,{lines:(X==null?void 0:X.lines)||[],maxLines:c,showMore:C}))}}o(tC,"HttpMessage");var M3=/^image\/(png|jpe?g|gif|webp|vnc.microsoft.icon|x-icon)$/i;rC.matches=e=>M3.test(Kr.getContentType(e)||"");function rC({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(rC,"ViewImage");function A3({flow:e}){let t=Qt();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:""},mS(n))})),isValid:n=>{var l;return!!((l=mS(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:gS})))}o(A3,"RequestLine");function D3({flow:e}){let t=Qt();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:gS}),"\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(D3,"ResponseLine");function R3({flow:e,message:t}){let n=Qt(),l=e.request===t?"request":"response";return Ut.createElement(Bp,{className:"headers",data:t.headers,onChange:d=>n(Wi(e,{[l]:{headers:d}}))})}o(R3,"Headers");function I3({flow:e,message:t}){let n=Qt(),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(I3,"Trailers");var gL=Ut.memo(o(function({flow:t,message:n}){let l=t.request===n?"request":"response",d=t.request===n?A3:D3;return Ut.createElement("section",{className:l},Ut.createElement(d,{flow:t}),Ut.createElement(R3,{flow:t,message:n}),Ut.createElement("hr",null),Ut.createElement(tC,{key:t.id+l,flow:t,message:n}),Ut.createElement(I3,{flow:t,message:n}))},"Message"));function nC(){let e=qe(t=>t.flows.byId[t.flows.selected[0]]);return Ut.createElement(gL,{flow:e,message:e.request})}o(nC,"Request");nC.displayName="Request";function iC(){let e=qe(t=>t.flows.byId[t.flows.selected[0]]);return Ut.createElement(gL,{flow:e,message:e.response})}o(iC,"Response");iC.displayName="Response";var Ye=fe(Oe());var F3=o(({message:e})=>Ye.createElement("div",null,e.query?e.op_code:e.response_code,"\xA0",e.truncation?"(Truncated)":""),"Summary"),B3=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"),oC=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"),vL=o(({type:e,message:t})=>Ye.createElement("section",{className:"dns-"+e},Ye.createElement("div",{className:`first-line ${e}-line`},Ye.createElement(F3,{message:t})),Ye.createElement(B3,{message:t}),Ye.createElement("hr",null),Ye.createElement(oC,{name:`${t.authoritative_answer?"Authoritative ":""}${t.recursion_available?"Recursive ":""}Answer`,values:t.answers}),Ye.createElement("hr",null),Ye.createElement(oC,{name:"Authority",values:t.authorities}),Ye.createElement("hr",null),Ye.createElement(oC,{name:"Additional",values:t.additionals})),"Message");function sC(){let e=qe(t=>t.flows.byId[t.flows.selected[0]]);return Ye.createElement(vL,{type:"request",message:e.request})}o(sC,"Request");sC.displayName="Request";function lC(){let e=qe(t=>t.flows.byId[t.flows.selected[0]]);return Ye.createElement(vL,{type:"response",message:e.response})}o(lC,"Response");lC.displayName="Response";var Ee=fe(Oe());function yL({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(yL,"ConnectionInfo");function wL(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(wL,"attrList");function H3({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,wL(t.subject))),Ee.createElement("tr",null,Ee.createElement("td",null,"Issuer"),Ee.createElement("td",null,wL(t.issuer))),Ee.createElement("tr",null,Ee.createElement("td",null,"Serial"),Ee.createElement("td",null,t.serial))))):Ee.createElement(Ee.Fragment,null)}o(H3,"CertificateInfo");function ly({flow:e}){var t;return Ee.createElement("section",{className:"detail"},Ee.createElement("h4",null,"Client Connection"),Ee.createElement(yL,{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(yL,{conn:e.server_conn})),Ee.createElement(H3,{flow:e}))}o(ly,"Connection");ly.displayName="Connection";var ym=fe(Oe());function ay({flow:e}){return ym.createElement("section",{className:"error"},ym.createElement("div",{className:"alert alert-warning"},e.error.msg,ym.createElement("div",null,ym.createElement("small",null,no(e.error.timestamp)))))}o(ay,"Error");ay.displayName="Error";var fs=fe(Oe());function W3({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"},"(",C0(1e3*(e-t)),")"))):fs.createElement("tr",null)}o(W3,"TimeStamp");function uy({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(W3,ke({key:k.title},k))))))}o(uy,"Timing");uy.displayName="Timing";var pu=fe(Oe());var Zs=fe(Oe()),jp=fe(Oe());function $f({flow:e,messages_meta:t}){let n=Qt(),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=G0(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(vm,{value:l,onChange:k=>n(x0(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(X0,{lines:k.lines,maxLines:d,showMore:c}));return d-=k.lines.length,B}))}o($f,"Messages");function fy({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(U3,{websocket:e.websocket}))}o(fy,"WebSocket");fy.displayName="WebSocket";function U3({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(U3,"CloseSummary");var xL=fe(ti());var aC=fe(Oe());function cy({flow:e}){return aC.createElement("section",{className:"tcp"},aC.createElement($f,{flow:e,messages_meta:e.messages_meta}))}o(cy,"TcpMessages");cy.displayName="Stream Data";var uC=fe(Oe());function py({flow:e}){return uC.createElement("section",{className:"udp"},uC.createElement($f,{flow:e,messages_meta:e.messages_meta}))}o(py,"UdpMessages");py.displayName="Datagrams";var SL={request:nC,response:iC,error:ay,connection:ly,timing:uy,websocket:fy,tcpmessages:cy,udpmessages:py,dnsrequest:sC,dnsresponse:lC};function dy(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(dy,"tabsForFlow");function fC(){let e=Qt(),t=qe(h=>h.flows.byId[h.flows.selected[0]]),n=dy(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=SL[l];return wm.createElement("div",{className:"flow-detail"},wm.createElement("nav",{className:"nav-tabs nav-tabs-sm"},n.map(h=>wm.createElement("a",{key:h,href:"#",className:(0,xL.default)({active:l===h}),onClick:c=>{c.preventDefault(),e(Lf(h))}},SL[h].displayName))),wm.createElement(d,{flow:t}))}o(fC,"FlowView");function CL(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($0()):n(Af(void 0));break;case"ArrowLeft":{if(!h)break;let c=dy(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=dy(h),v=l().ui.flow.tab,C=c[(Math.max(0,c.indexOf(v))+1)%c.length];n(Lf(C));break}case"Delete":case"d":{if(!h)return;n(B0(h));break}case"n":{Pf("view.flows.create","get","https://example.com/");break}case"D":{if(!h)return;n(H0(h));break}case"a":{h&&h.intercepted&&n(Op(h));break}case"A":{n(I0());break}case"r":{h&&n(Mp(h));break}case"v":{h&&h.modified&&n(W0(h));break}case"x":{h&&h.intercepted&&n(F0(h));break}case"X":{n(nN());break}case"z":{n(U0());break}default:return}}}o(CL,"onKeyDown");var Zp=fe(Oe());var xm=fe(Oe()),Sm=fe(iu()),bL=fe(ti()),qp=class extends xm.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(){Sm.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=Sm.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,Sm.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=Sm.default.findDOMNode(this);n.previousElementSibling&&(n.previousElementSibling.style.flex=""),n.nextElementSibling&&(n.nextElementSibling.style.flex=""),t||this.setState({applied:!1}),this.onResize()}componentWillUnmount(){this.reset(!0)}render(){return xm.default.createElement("div",{className:(0,bL.default)("splitter",this.props.axis==="x"?"splitter-x":"splitter-y")},xm.default.createElement("div",{onMouseDown:this.onMouseDown,draggable:"true"}))}};o(qp,"Splitter"),qp.defaultProps={axis:"x"};var cs=fe(Oe()),Em=fe(Cm()),my=fe(iu());var BL=fe(cC());var pC=fe(iu()),ML=Symbol("shouldStick"),AL=o(e=>Math.round(e.scrollTop)+e.clientHeight===e.scrollHeight,"isAtBottom"),hy=o(e=>{var t;return Object.assign((o(t=class extends e{UNSAFE_componentWillUpdate(){let l=pC.default.findDOMNode(this);this[ML]=l.scrollTop&&AL(l),super.UNSAFE_componentWillUpdate&&super.UNSAFE_componentWillUpdate(),super.componentWillUpdate&&super.componentWillUpdate()}componentDidUpdate(){let l=pC.default.findDOMNode(this);this[ML]&&!AL(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=>im[v]).filter(v=>v).concat(Pp);return bm.createElement("tr",null,c.map(v=>bm.createElement("th",{className:(0,DL.default)(`col-${v.name}`,l===v.name&&h),key:v.name,onClick:()=>t(rN(v.name===l&&n?void 0:v.name,v.name!==l?!1:!n))},v.headerName)))},"FlowTableHead"));var Vp=fe(Oe()),IL=fe(ti());var FL=Vp.default.memo(o(function({flow:t,selected:n,highlighted:l}){let d=Qt(),h=qe(k=>k.options.web_columns),c=(0,IL.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=>im[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 _m=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=my.default.findDOMNode(this),h=my.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=my.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,BL.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(RL,null)),cs.createElement("tbody",null,cs.createElement("tr",{style:{height:t.paddingTop}}),l.slice(t.start,t.end).map(v=>cs.createElement(FL,{key:v.id,flow:v,selected:v===d,highlighted:c(v)})),cs.createElement("tr",{style:{height:t.paddingBottom}}))))}};o(_m,"FlowTable"),Vc(_m,"propTypes",{flows:Em.default.array.isRequired,rowHeight:Em.default.number,highlight:Em.default.string,selected:Em.default.object}),Vc(_m,"defaultProps",{rowHeight:32});var j3=hy(_m),HL=Hi(e=>({flows:e.flows.view,highlight:e.flows.highlight,selected:e.flows.byId[e.flows.selected[0]]}))(j3);var Nr=fe(Oe()),ky=fe(Oe());var zP=fe(UP());function IC(){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(HB,ke({},n))))))}o(IC,"CaptureSetup");function HB({description:e,listen_addrs:t,last_exception:n,is_running:l,full_spec:d,wireguard_conf:h}){let c=(0,ky.useRef)(null);(0,ky.useEffect)(()=>{h&&c.current&&zP.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=nS(["*",t[0][1]]):v=t.map(nS).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(HB,"ServerDescription");function FC(){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(HL,null):Zp.createElement(IC,null),e&&Zp.createElement(qp,{key:"splitter"}),e&&Zp.createElement(fC,{key:"flowDetails"}))}o(FC,"MainView");var Io=fe(Oe()),YP=fe(ti());var oi=fe(Oe());var ps=fe(Oe()),Ny=fe(iu()),$P=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}),Ny.default.findDOMNode(this.refs.input).focus()}blur(){Ny.default.findDOMNode(this.refs.input).blur()}select(){Ny.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,$P.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(UB,null),oi.createElement(zB,null)),oi.createElement("div",{className:"menu-legend"},"Find")),oi.createElement("div",{className:"menu-group"},oi.createElement("div",{className:"menu-content"},oi.createElement(WB,null),oi.createElement($B,null)),oi.createElement("div",{className:"menu-legend"},"Intercept")))}o(Jp,"StartMenu");function WB(){let e=Qt(),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(WB,"InterceptInput");function UB(){let e=Qt(),t=qe(n=>n.flows.filter);return oi.createElement(Yf,{value:t||"",placeholder:"Search",type:"search",color:"black",onChange:n=>e(D0(n))})}o(UB,"FlowFilterInput");function zB(){let e=Qt(),t=qe(n=>n.flows.highlight);return oi.createElement(Yf,{value:t||"",placeholder:"Highlight",type:"tag",color:"hsl(48, 100%, 50%)",onChange:n=>e(R0(n))})}o(zB,"HighlightInput");function $B(){let e=Qt();return oi.createElement(kr,{className:"btn-sm",title:"[a]ccept all",icon:"fa-forward text-success",onClick:()=>e(I0())},"Resume All")}o($B,"ResumeAll");var Qr=fe(Oe());var Xf=fe(Oe());function BC({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(BC,"MenuToggle");function Ly({name:e,children:t}){let n=Qt(),l=qe(d=>d.options[e]);return Xf.createElement(BC,{value:!!l,onChange:()=>n(Ip(e,!l))},t)}o(Ly,"OptionsToggle");function jP(){let e=Gs(),t=qe(n=>n.eventLog.visible);return Xf.createElement(BC,{value:t,onChange:()=>e(Rp())},"Display Event Log")}o(jP,"EventlogToggle");function qP(){let e=Gs(),t=qe(n=>n.commandBar.visible);return Xf.createElement(BC,{value:t,onChange:()=>e(K0())},"Display Command Bar")}o(qP,"CommandBarToggle");var HC=fe(Oe());function WC({children:e,resource:t}){let n=`https://docs.mitmproxy.org/stable/${t}`;return HC.createElement("a",{target:"_blank",href:n},e||HC.createElement("i",{className:"fa fa-question-circle"}))}o(WC,"DocsLink");var Py=fe(Oe());function Ro({children:e}){return window.MITMWEB_STATIC?null:Py.createElement(Py.Fragment,null,e)}o(Ro,"HideInStatic");Oy.title="Options";function Oy(){let e=Qt(),t=o(()=>lN("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(Ly,{name:"anticache"},"Strip cache headers"," ",Qr.createElement(WC,{resource:"overview-features/#anticache"})),Qr.createElement(Ly,{name:"showhost"},"Use host header for display"),Qr.createElement(Ly,{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(jP,null),Qr.createElement(qP,null)),Qr.createElement("div",{className:"menu-legend"},"View Options")))}o(Oy,"OptionMenu");var Hn=fe(Oe());var VP=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(Y0,{icon:"fa-folder-open",text:"\xA0Open...",onClick:l=>l.stopPropagation(),onOpenFile:l=>{t(oN(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(U0())},Hn.createElement("i",{className:"fa fa-fw fa-trash"}),"\xA0Clear All"),Hn.createElement(Ro,null,Hn.createElement(mL,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 KP(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(KP,"copyToClipboard");var ed=o((e,t)=>Ia(void 0,null,function*(){let n=yield Pf("export",t,`@${e.id}`);n.value?yield KP(n.value):n.error?alert(n.error):console.error(n)}),"copy");td.title="Flow";function td(){let e=Qt(),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:!_0(t)},"Replay"),at.createElement(kr,{title:"[D]uplicate flow",icon:"fa-copy text-info",onClick:()=>e(H0(t))},"Duplicate"),at.createElement(kr,{disabled:!t||!t.modified,title:"revert changes to flow [V]",icon:"fa-history text-warning",onClick:()=>e(W0(t))},"Revert"),at.createElement(kr,{title:"[d]elete flow",icon:"fa-trash text-danger",onClick:()=>e(B0(t))},"Delete"),at.createElement(KB,{flow:t})),at.createElement("div",{className:"menu-legend"},"Flow Modification"))),at.createElement("div",{className:"menu-group"},at.createElement("div",{className:"menu-content"},at.createElement(jB,{flow:t}),at.createElement(qB,{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(F0(t))},"Abort")),at.createElement("div",{className:"menu-legend"},"Interception")))):at.createElement("div",null)}o(td,"FlowMenu");var My=o(e=>{let t=window.open(e,"_blank","noopener,noreferrer");t&&(t.opener=null)},"openInNewTab");function jB({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:()=>My(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:()=>My(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:()=>My(Kr.getContentURL(e,e.request))},"Download request"),at.createElement(ii,{onClick:()=>My(Kr.getContentURL(e,n))},"Download response"))}return null}o(jB,"DownloadButton");function qB({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(qB,"ExportButton");var VB={":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 KB({flow:e}){let t=Qt();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(VB).map(([n,l])=>at.createElement(ii,{key:n,onClick:()=>t(Wi(e,{marked:n}))},l," ",n.replace(/[:_]/g," "))))}o(KB,"MarkButton");var vu=fe(Oe());var GP=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 UC(){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,Oy];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(VP,null),h.map(v=>Io.default.createElement("a",{key:v.title,href:"#",className:(0,YP.default)({active:v===t}),onClick:C=>c(v,C)},v.title)),Io.default.createElement(Ro,null,Io.default.createElement(GP,null))),Io.default.createElement("div",null,Io.default.createElement(t,null)))}o(UC,"Header");var Je=fe(Oe()),XP=fe(ti());var Ay=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"),Zt={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 Jt(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(Zt)),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(GB,"Results");function YB({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(YB,"CommandHelp");function $C(){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(zC(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,Zt;let Ge=Ay.parse(Ke),Xe=Ay.parse(We);A((ct=O[Ge[0]])==null?void 0:ct.signature_help),G(((Hr=O[Ge[0]])==null?void 0:Hr.help)||""),v(zC(O,Xe[0])),k(zC(O,Ge[0]));let nr=(Zt=O[Ge[0]])==null?void 0:Zt.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]=Ay.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(GB,{results:K}),Je.default.createElement(YB,{nextArgs:B,currentArg:J,help:R,description:I,availableCommands:C}),Je.default.createElement("div",{className:(0,XP.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($C,"CommandBar");var Wl=fe(Oe()),rd=fe(Cm());var jC=fe(Oe());function qC({checked:e,onToggle:t,text:n}){return jC.default.createElement("div",{className:"btn btn-toggle "+(e?"btn-primary":"btn-default"),onClick:t},jC.default.createElement("i",{className:"fa fa-fw "+(e?"fa-check-square-o":"fa-square-o")}),"\xA0",n)}o(qC,"ToggleButton");var Hl=fe(Oe()),VC=fe(Cm()),QP=fe(iu()),ZP=fe(cC());var Dm=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=QP.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,ZP.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(XB,{event:l}),l.message)),Hl.default.createElement("div",{style:{height:t.paddingBottom}}))}};o(Dm,"EventLogList"),Dm.propTypes={events:VC.default.array.isRequired,rowHeight:VC.default.number},Dm.defaultProps={rowHeight:18};function XB({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(XB,"LogIcon");var JP=hy(Dm);var Rm=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(qC,{key:c,text:c,checked:n[c],onToggle:()=>d(c)})),Wl.default.createElement("i",{onClick:h,className:"fa fa-close"}))),Wl.default.createElement(JP,{events:l}))}};o(Rm,"PureEventLog"),Vc(Rm,"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(Rm,"defaultProps",{defaultHeight:200});var eO=Hi(e=>({filters:e.eventLog.filters,events:e.eventLog.view}),{close:Rp,toggleFilter:vN})(Rm);var un=fe(Oe());function KC(){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: ",S0(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(KC,"Footer");var eb=fe(Oe());var JC=fe(Oe());var nd=fe(Oe());function GC({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(GC,"ModalLayout");var mr=fe(Oe());var Fo=fe(Oe()),Bo=fe(Cm());var tO=fe(ti()),QB=o(e=>{e.key!=="Escape"&&e.stopPropagation()},"stopPropagation");YC.propTypes={value:Bo.default.bool.isRequired,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("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(YC,"BooleanOption");XC.propTypes={value:Bo.default.string,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:"text",value:e||"",onChange:h=>t(h.target.value)},n))}o(XC,"StringOption");function rO(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(rO,"Optional");QC.propTypes={value:Bo.default.number.isRequired,onChange:Bo.default.func.isRequired};function QC(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(QC,"NumberOption");nO.propTypes={value:Bo.default.string.isRequired,onChange:Bo.default.func.isRequired};function nO(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(nO,"ChoicesOption");iO.propTypes={value:Bo.default.arrayOf(Bo.default.string).isRequired,onChange:Bo.default.func.isRequired};function iO(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(iO,"StringSequenceOption");var ZB={bool:YC,str:XC,int:QC,"optional str":rO(XC),"optional int":rO(QC),"sequence of str":iO};function JB({choices:e,type:t,value:n,onChange:l,name:d,error:h}){let c,v={};if(e)c=nO,v.choices=e;else if(c=ZB[t],!c)throw`unknown option type ${t}`;return c!==YC&&(v.className="form-control"),Fo.default.createElement("div",{className:(0,tO.default)({"has-error":h})},Fo.default.createElement(c,ke({name:d,value:n,onChange:l,onKeyDown:QB},v)))}o(JB,"PureOption");var oO=Hi((e,{name:t})=>ke(ke({},e.options_meta[t]),e.ui.optionsEditor[t]),(e,{name:t})=>({onChange:n=>e(Ip(t,n))}))(JB);var Dy=fe(Qh());function eH({help:e}){return mr.default.createElement("div",{className:"help-block small"},e)}o(eH,"PureOptionHelp");var tH=Hi((e,{name:t})=>({help:e.options_meta[t].help}))(eH);function rH({error:e}){return e?mr.default.createElement("div",{className:"small text-danger"},e):null}o(rH,"PureOptionError");var nH=Hi((e,{name:t})=>({error:e.ui.optionsEditor[t]&&e.ui.optionsEditor[t].error}))(rH);function iH({value:e,defaultVal:t}){if(e===t)return null;if(typeof t=="boolean")t=t?"true":"false";else if(Array.isArray(t)){if(Dy.default.isEmpty(Dy.default.compact(e))&&Dy.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(iH,"PureOptionDefault");var oH=Hi((e,{name:t})=>({value:e.options[t],defaultVal:e.options_meta[t].default}))(iH),ZC=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(tH,{name:d})),mr.default.createElement("div",{className:"col-xs-6"},mr.default.createElement(oO,{name:d}),mr.default.createElement(nH,{name:d}),mr.default.createElement(oH,{name:d})))))),mr.default.createElement("div",{className:"modal-footer"}))}};o(ZC,"PureOptionModal");var sO=Hi(e=>({options:Object.keys(e.options_meta).sort()}),{hideModal:$0,save:PN})(ZC);function sH(){return JC.createElement(GC,null,JC.createElement(sO,null))}o(sH,"OptionModal");var lO=[sH];function tb(){let e=qe(n=>n.ui.modal.activeModal),t=lO.find(n=>n.name===e);return e&&t!==void 0?eb.createElement(t,null):eb.createElement("div",null)}o(tb,"PureModal");var rb=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(UC,null),Wn.default.createElement(FC,null),n&&Wn.default.createElement($C,{key:"commandbar"}),t&&Wn.default.createElement(eO,{key:"eventlog"}),Wn.default.createElement(KC,null),Wn.default.createElement(tb,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(rb,"ProxyAppMain");var aO=Hi(e=>({showEventLog:e.eventLog.visible,showCommandBar:e.commandBar.visible}),{onKeyDown:CL})(rb);var yu={SEARCH:"s",HIGHLIGHT:"h",SHOW_EVENTLOG:"e",SHOW_COMMANDBAR:"c"};function lH(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(D0(c));break;case yu.HIGHLIGHT:e.dispatch(R0(c));break;case yu.SHOW_EVENTLOG:e.getState().eventLog.visible||e.dispatch(Rp());break;case yu.SHOW_COMMANDBAR:e.getState().commandBar.visible||e.dispatch(K0());break;default:console.error(`unimplemented query arg: ${d}`)}})}o(lH,"updateStoreFromUrl");function aH(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(aH,"updateUrlFromStore");function nb(e){lH(e),e.subscribe(()=>aH(e))}o(nb,"initialize");var uH="reset",Im=class{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(TN())}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===uH)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(kN())}onClose(t){this.store.dispatch(NN(`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(Im,"WebsocketBackend");var Fm=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(Fm,"StaticBackend");nb(Fp);window.MITMWEB_STATIC?window.backend=new Fm(Fp):window.backend=new Im(Fp);window.addEventListener("error",e=>{Fp.dispatch(yN(`${e.message} +${e.error.stack}`))});document.addEventListener("DOMContentLoaded",()=>{(0,uO.render)(ib.createElement(Wx,{store:Fp},ib.createElement(aO,null)),document.getElementById("mitmproxy"))});})(); /* object-assign (c) Sindre Sorhus 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/images/resourceUdpIcon.png b/mitmproxy/tools/web/static/images/resourceUdpIcon.png new file mode 100644 index 0000000000..1d6ba6f290 Binary files /dev/null and b/mitmproxy/tools/web/static/images/resourceUdpIcon.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 2d7eb8817f..3f9b5dbc62 100644 --- a/mitmproxy/tools/web/static_viewer.py +++ b/mitmproxy/tools/web/static_viewer.py @@ -1,4 +1,5 @@ import json +import logging import os.path import pathlib import shutil @@ -6,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 @@ -69,11 +72,8 @@ def save_flows_content(path: pathlib.Path, flows: Iterable[flow.Flow]) -> None: else: description, lines = "No content.", [] if time.time() - t > 0.1: - ctx.log( - "Slow content view: {} took {}s".format( - description.strip(), round(time.time() - t, 1) - ), - "info", + logging.info( + f"Slow content view: {description.strip()} took {round(time.time() - t, 1)}s", ) with open( str(message_path / "content" / "Auto.json"), "w" diff --git a/mitmproxy/tools/web/templates/index.html b/mitmproxy/tools/web/templates/index.html index 69a88546e8..b7c6a0beef 100644 --- a/mitmproxy/tools/web/templates/index.html +++ b/mitmproxy/tools/web/templates/index.html @@ -1,16 +1,15 @@ - - - mitmproxy - - - - - - - - -
    - + + + mitmproxy + + + + + + + +
    + diff --git a/mitmproxy/tools/web/webaddons.py b/mitmproxy/tools/web/webaddons.py index 265cc83f28..6d5970988a 100644 --- a/mitmproxy/tools/web/webaddons.py +++ b/mitmproxy/tools/web/webaddons.py @@ -1,3 +1,4 @@ +import logging import webbrowser from collections.abc import Sequence @@ -22,7 +23,7 @@ def running(self): web_url = f"http://{ctx.options.web_host}:{ctx.options.web_port}/" success = open_browser(web_url) if not success: - ctx.log.info( + logging.info( f"No web browser found. Please open a browser and point it to {web_url}", ) @@ -42,7 +43,8 @@ def open_browser(url: str) -> bool: "windows-default", "macosx", "wslview %s", - "x-www-browser %s", + "gio", + "x-www-browser", "gnome-open %s", "xdg-open", "google-chrome", diff --git a/mitmproxy/types.py b/mitmproxy/types.py index 868dd42a21..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 @@ -71,7 +74,7 @@ def parse(self, manager: "CommandManager", typ: Any, s: str) -> Any: """ Parse a string, given the specific type instance (to allow rich type annotations like Choice) and a string. - Raises exceptions.TypeError if the value is invalid. + Raises ValueError if the value is invalid. """ raise NotImplementedError @@ -95,7 +98,7 @@ def parse(self, manager: "CommandManager", t: type, s: str) -> bool: elif s == "false": return False else: - raise exceptions.TypeError("Booleans are 'true' or 'false', got %s" % s) + raise ValueError("Booleans are 'true' or 'false', got %s" % s) def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: return val in [True, False] @@ -128,10 +131,7 @@ def completion(self, manager: "CommandManager", t: type, s: str) -> Sequence[str return [] def parse(self, manager: "CommandManager", t: type, s: str) -> str: - try: - return self.escape_sequences.sub(self._unescape, s) - except ValueError as e: - raise exceptions.TypeError(f"Invalid str: {e}") from e + return self.escape_sequences.sub(self._unescape, s) def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: return isinstance(val, str) @@ -145,10 +145,7 @@ def completion(self, manager: "CommandManager", t: type, s: str) -> Sequence[str return [] def parse(self, manager: "CommandManager", t: type, s: str) -> bytes: - try: - return strutils.escaped_str_to_bytes(s) - except ValueError as e: - raise exceptions.TypeError(str(e)) + return strutils.escaped_str_to_bytes(s) def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: return isinstance(val, bytes) @@ -176,10 +173,7 @@ def completion(self, manager: "CommandManager", t: type, s: str) -> Sequence[str return [] def parse(self, manager: "CommandManager", t: type, s: str) -> int: - try: - return int(s) - except ValueError as e: - raise exceptions.TypeError(str(e)) from e + return int(s) def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: return isinstance(val, int) @@ -229,7 +223,7 @@ def completion(self, manager: "CommandManager", t: type, s: str) -> Sequence[str def parse(self, manager: "CommandManager", t: type, s: str) -> str: if s not in manager.commands: - raise exceptions.TypeError("Unknown command: %s" % s) + raise ValueError("Unknown command: %s" % s) return s def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: @@ -372,9 +366,9 @@ def parse(self, manager: "CommandManager", t: type, s: str) -> flow.Flow: try: flows = manager.call_strings("view.flows.resolve", [s]) except exceptions.CommandError as e: - raise exceptions.TypeError(str(e)) from e + raise ValueError(str(e)) from e if len(flows) != 1: - raise exceptions.TypeError( + raise ValueError( "Command requires one flow, specification matched %s." % len(flows) ) return flows[0] @@ -391,7 +385,7 @@ def parse(self, manager: "CommandManager", t: type, s: str) -> Sequence[flow.Flo try: return manager.call_strings("view.flows.resolve", [s]) except exceptions.CommandError as e: - raise exceptions.TypeError(str(e)) from e + raise ValueError(str(e)) from e def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: try: @@ -410,12 +404,12 @@ class _DataType(_BaseType): def completion( self, manager: "CommandManager", t: type, s: str ) -> Sequence[str]: # pragma: no cover - raise exceptions.TypeError("data cannot be passed as argument") + raise ValueError("data cannot be passed as argument") def parse( self, manager: "CommandManager", t: type, s: str ) -> Any: # pragma: no cover - raise exceptions.TypeError("data cannot be passed as argument") + raise ValueError("data cannot be passed as argument") def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: # FIXME: validate that all rows have equal length, and all columns have equal types @@ -439,7 +433,7 @@ def completion(self, manager: "CommandManager", t: Choice, s: str) -> Sequence[s def parse(self, manager: "CommandManager", t: Choice, s: str) -> str: opts = manager.execute(t.options_command) if s not in opts: - raise exceptions.TypeError("Invalid choice.") + raise ValueError("Invalid choice.") return s def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: @@ -462,7 +456,7 @@ def completion(self, manager: "CommandManager", t: Choice, s: str) -> Sequence[s def parse(self, manager: "CommandManager", t: Choice, s: str) -> str: if s not in ALL_MARKERS: - raise exceptions.TypeError("Invalid choice.") + raise ValueError("Invalid choice.") if s == "true": return ":default:" elif s == "false": @@ -479,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 new file mode 100644 index 0000000000..2d716e9108 --- /dev/null +++ b/mitmproxy/udp.py @@ -0,0 +1,72 @@ +import time + +from mitmproxy import connection +from mitmproxy import flow +from mitmproxy.coretypes import serializable + + +class UDPMessage(serializable.Serializable): + """ + An individual UDP datagram. + """ + + def __init__(self, from_client, content, timestamp=None): + self.from_client = from_client + self.content = content + self.timestamp = timestamp or time.time() + + @classmethod + def from_state(cls, state): + return cls(*state) + + def get_state(self): + return self.from_client, self.content, self.timestamp + + def set_state(self, state): + self.from_client, self.content, self.timestamp = state + + def __repr__(self): + return "{direction} {content}".format( + direction="->" if self.from_client else "<-", content=repr(self.content) + ) + + +class UDPFlow(flow.Flow): + """ + A UDPFlow is a representation of a UDP session. + """ + + messages: list[UDPMessage] + """ + The messages transmitted over this connection. + + The latest message can be accessed as `flow.messages[-1]` in event hooks. + """ + + def __init__( + self, + client_conn: connection.Client, + server_conn: connection.Server, + live: bool = False, + ): + super().__init__(client_conn, server_conn, live) + self.messages = [] + + 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"" + + +__all__ = [ + "UDPFlow", + "UDPMessage", +] 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..95c01edd00 100644 --- a/mitmproxy/utils/asyncio_utils.py +++ b/mitmproxy/utils/asyncio_utils.py @@ -1,7 +1,6 @@ import asyncio import time from collections.abc import Coroutine -from typing import Optional from mitmproxy.utils import human @@ -10,7 +9,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,7 +23,7 @@ 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 @@ -36,7 +35,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() 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 new file mode 100644 index 0000000000..815e513d91 --- /dev/null +++ b/mitmproxy/utils/magisk.py @@ -0,0 +1,112 @@ +import hashlib +import os +from zipfile import ZipFile + +from cryptography import x509 +from cryptography.hazmat.primitives import serialization + +from mitmproxy import certs +from mitmproxy import ctx +from mitmproxy.options import CONF_BASENAME + +# The following 3 variables are for including in the magisk module as text file +MODULE_PROP_TEXT = """id=mitmproxycert +name=MITMProxy cert +version=v1 +versionCode=1 +author=mitmproxy +description=Adds the mitmproxy certificate to the system store +template=3""" + +CONFIG_SH_TEXT = """ +MODID=mitmproxycert +AUTOMOUNT=true +PROPFILE=false +POSTFSDATA=false +LATESTARTSERVICE=false + +print_modname() { + ui_print "*******************************" + ui_print " MITMProxy cert installer " + ui_print "*******************************" +} + +REPLACE=" +" + +set_permissions() { + set_perm_recursive $MODPATH 0 0 0755 0644 +} +""" + +UPDATE_BINARY_TEXT = """ +#!/sbin/sh + +################# +# Initialization +################# + +umask 022 + +# echo before loading util_functions +ui_print() { echo "$1"; } + +require_new_magisk() { + ui_print "*******************************" + ui_print " Please install Magisk v20.4+! " + ui_print "*******************************" + exit 1 +} + +OUTFD=$2 +ZIPFILE=$3 + +mount /data 2>/dev/null +[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk +. /data/adb/magisk/util_functions.sh +[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk + +install_module +exit 0 +""" + + +def get_ca_from_files() -> x509.Certificate: + # Borrowed from tlsconfig + certstore_path = os.path.expanduser(ctx.options.confdir) + 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, + ) + return certstore.default_ca._cert + + +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 + return hex(sho)[2:] + + +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() + der_cert = ca.public_bytes(serialization.Encoding.DER) + with ZipFile(path, "w") as zipp: + # Main cert file, name is always the old subject hash with a '.0' added + zipp.writestr(f"system/etc/security/cacerts/{subject_hash_old(ca)}.0", der_cert) + zipp.writestr("module.prop", MODULE_PROP_TEXT) + 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/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 new file mode 100644 index 0000000000..f11ee56726 --- /dev/null +++ b/mitmproxy/utils/signals.py @@ -0,0 +1,136 @@ +""" +This module provides signals, which are a simple dispatching system that allows any number of interested parties +to subscribe to events ("signals"). + +This is similar to the Blinker library (https://pypi.org/project/blinker/), with the following changes: + - provides only a small subset of Blinker's functionality + - supports type hints + - supports async receivers. +""" +from __future__ import annotations + +import asyncio +import inspect +import weakref +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") + + +def make_weak_ref(obj: Any) -> weakref.ReferenceType: + """ + Like weakref.ref(), but using weakref.WeakMethod for bound methods. + """ + if hasattr(obj, "__self__"): + return cast(weakref.ref, weakref.WeakMethod(obj)) + else: + return weakref.ref(obj) + + +# 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) -> None: + self.receivers: list[weakref.ref[Callable]] = [] + + def connect(self, receiver: Callable) -> None: + """ + Register a signal receiver. + + The signal will only hold a weak reference to the receiver function. + """ + receiver = make_weak_ref(receiver) + self.receivers.append(receiver) + + def disconnect(self, receiver: Callable) -> None: + self.receivers = [r for r in self.receivers if r() != receiver] + + def notify(self, *args, **kwargs): + cleanup = False + for ref in self.receivers: + r = ref() + if r is not None: + yield r(*args, **kwargs) + else: + cleanup = True + if cleanup: + self.receivers = [r for r in self.receivers if r() is not None] + + +class _SyncSignal(Generic[P], _SignalMixin): + def connect(self, receiver: Callable[P, None]) -> None: + assert not asyncio.iscoroutinefunction(receiver) + super().connect(receiver) + + def disconnect(self, receiver: Callable[P, None]) -> None: + super().disconnect(receiver) + + def send(self, *args: P.args, **kwargs: P.kwargs) -> None: + for ret in super().notify(*args, **kwargs): + assert ret is None or not inspect.isawaitable(ret) + + +class _AsyncSignal(Generic[P], _SignalMixin): + def connect(self, receiver: Callable[P, Awaitable[None] | None]) -> None: + super().connect(receiver) + + 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) + ] + ) + + +# noinspection PyPep8Naming +def SyncSignal(receiver_spec: Callable[P, None]) -> _SyncSignal[P]: + """ + Create a synchronous signal with the given function signature for receivers. + + Example: + + s = SyncSignal(lambda event: None) # all receivers must accept a single "event" argument. + def receiver(event): + print(event) + + s.connect(receiver) + s.send("foo") # prints foo + s.send(event="bar") # prints bar + + def receiver2(): + ... + + s.connect(receiver2) # mypy complains about receiver2 not having the right signature + + s2 = SyncSignal(lambda: None) # this signal has no arguments + s2.send() + """ + return cast(_SyncSignal[P], _SyncSignal()) + + +# noinspection PyPep8Naming +def AsyncSignal(receiver_spec: Callable[P, Awaitable[None] | None]) -> _AsyncSignal[P]: + """ + Create an signal that supports both regular and async receivers: + + Example: + + s = AsyncSignal(lambda event: None) + async def receiver(event): + print(event) + s.connect(receiver) + await s.send("foo") # prints foo + """ + return cast(_AsyncSignal[P], _AsyncSignal()) 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..9b58c2101b 100644 --- a/mitmproxy/utils/strutils.py +++ b/mitmproxy/utils/strutils.py @@ -1,8 +1,8 @@ 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 @@ -13,13 +13,11 @@ 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): @@ -36,11 +34,11 @@ 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 +58,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 +235,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 49ce3b90fe..a898a5b387 100644 --- a/mitmproxy/utils/typecheck.py +++ b/mitmproxy/utils/typecheck.py @@ -3,7 +3,7 @@ try: from types import UnionType -except ImportError: +except ImportError: # pragma: no cover UnionType = object() # type: ignore Type = typing.Union[ @@ -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..8927a3318b 100644 --- a/mitmproxy/utils/vt_codes.py +++ b/mitmproxy/utils/vt_codes.py @@ -6,8 +6,12 @@ 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 +53,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 a047d86f61..5e84f10e8e 100644 --- a/mitmproxy/version.py +++ b/mitmproxy/version.py @@ -2,12 +2,12 @@ import subprocess import sys -VERSION = "8.1.1" +VERSION = "11.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 = 17 +FLOW_FORMAT_VERSION = 20 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..7424e12696 100644 --- a/mitmproxy/websocket.py +++ b/mitmproxy/websocket.py @@ -7,13 +7,13 @@ """ 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 +52,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: @@ -144,45 +144,29 @@ 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 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..19b952e3dd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,232 @@ +[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 :: 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_mitmproxy>=0.9.21,<0.10", + "asgiref>=3.2.10,<3.8", + "Brotli>=1.0,<1.2", + "certifi>=2019.9.11", # no semver here - this should always be on the last release! + "cryptography>=38.0,<41.1", + "flask>=1.1.1,<3.1", + "h11>=0.11,<0.15", + "h2>=4.1,<5", + "hyperframe>=6.0,<7", + "kaitaistruct>=0.10,<0.11", + "ldap3>=2.8,<2.10", + "mitmproxy_rs>=0.4,<0.5", + "msgpack>=1.0.0, <1.1.0", + "passlib>=1.6.5, <1.8", + "protobuf>=3.14,<5", + "pydivert>=2.0.3,<2.2; sys_platform == 'win32'", + "pyOpenSSL>=22.1,<23.4", + "pyparsing>=2.4.2,<3.2", + "pyperclip>=1.6.0,<1.9", + "ruamel.yaml>=0.16,<0.19", + "sortedcontainers>=2.3,<2.5", + "tornado>=6.2,<7", + "typing-extensions>=4.3,<5; python_version<'3.11'", + "urwid-mitmproxy>=2.1.1,<2.2", + "wsproto>=1.0,<1.3", + "publicsuffix2>=2.20190812,<3", + "zstandard>=0.11,<0.23", +] + +[project.optional-dependencies] +dev = [ + "click>=7.0,<8.2", + "hypothesis>=5.8,<7", + "pdoc>=4.0.0", + "pyinstaller==6.2.0", + "pytest-asyncio>=0.17,<0.22", + "pytest-cov>=2.7.1,<4.2", + "pytest-timeout>=1.3.3,<2.3", + "pytest-xdist>=2.1.0,<3.6", + "pytest>=6.1.0,<8", + "requests>=2.9.1,<3", + "tox>=3.5,<5", + "wheel>=0.36.2,<0.43", + "build>=0.10.0", +] + +[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" + +[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" +testpaths = "test" +addopts = "--capture=no --color=yes" +filterwarnings = [ + "ignore::DeprecationWarning:tornado.*:", +] + +[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] +select = ["E", "F", "I"] +extend-exclude = ["mitmproxy/contrib/"] +ignore = ["F541", "E501"] + + +[tool.ruff.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"] + +[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 \ + --full-cov=mitmproxy/ \ + {posargs} + +[testenv:lint] +deps = + ruff>=0.1.3,<0.2 +commands = + ruff . + +[testenv:filename_matching] +deps = +commands = + python ./test/filename_matching.py + +[testenv:mypy] +deps = + mypy==1.6.1 + types-certifi==2021.10.8.3 + types-Flask==1.1.6 + types-Werkzeug==1.0.9 + types-requests==2.31.0.10 + types-cryptography==3.3.23.2 + types-pyOpenSSL==23.3.0.0 + -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/release/README.md b/release/README.md index b14737122a..103a204564 100644 --- a/release/README.md +++ b/release/README.md @@ -1,49 +1,49 @@ # Release Checklist - -These steps assume you are on the correct branch and have a git remote called `origin` that points to the `mitmproxy/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 (`npm start prod`). -- 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. Make sure that `CHANGELOG.md` is up-to-date with all entries in the "Unreleased" section. +2. Invoke the [release workflow](https://github.com/mitmproxy/mitmproxy/actions/workflows/release.yml) from the GitHub UI. +3. The spawned workflow runs will require manual confirmation on GitHub which you need to approve twice: + https://github.com/mitmproxy/mitmproxy/actions +4. Once everything has been deployed, update the website. +5. Verify that the front-page download links for all platforms are working. ### GitHub Releases -- Create release notice on GitHub - [here](https://github.com/mitmproxy/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/mitmproxy/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/mitmproxy/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/mitmproxy/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/mitmproxy/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 diff --git a/release/__init__.py b/release/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/release/build-and-deploy-docker.py b/release/build-and-deploy-docker.py new file mode 100644 index 0000000000..3cf92881dd --- /dev/null +++ b/release/build-and-deploy-docker.py @@ -0,0 +1,102 @@ +#!/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 + +# Security: No third-party dependencies here! + +root = Path(__file__).absolute().parent.parent + +ref = os.environ["GITHUB_REF"] +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/", "") +else: + raise AssertionError("Failed to parse $GITHUB_REF") + +(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", + "-v", + f"{root / 'release'}:/release", + "localtesting", + "mitmdump", + "-s", + "/release/selftest.py", + ], + capture_output=True, +) +print(r.stdout.decode()) +assert "Self-test successful" in r.stdout.decode() +assert r.returncode == 0 + +# 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("mitmproxy/mitmproxy:dev") +elif branch == "citest": + _buildx("mitmproxy/mitmproxy:citest") +elif tag: + _buildx(f"mitmproxy/mitmproxy:{tag}") + _buildx("mitmproxy/mitmproxy:latest") +else: + raise AssertionError diff --git a/release/build.py b/release/build.py new file mode 100755 index 0000000000..9d6bdf6dad --- /dev/null +++ b/release/build.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import hashlib +import os +import platform +import re +import shutil +import subprocess +import tarfile +import urllib.request +import warnings +import zipfile +from datetime import datetime +from pathlib import Path + +import click +import cryptography.fernet + +here = Path(__file__).absolute().parent + +TEMP_DIR = here / "build" +DIST_DIR = here / "dist" + + +@click.group(chain=True) +@click.option("--dirty", is_flag=True) +def cli(dirty): + if dirty: + print("Keeping temporary files.") + else: + print("Cleaning up temporary files...") + if TEMP_DIR.exists(): + shutil.rmtree(TEMP_DIR) + if DIST_DIR.exists(): + shutil.rmtree(DIST_DIR) + + TEMP_DIR.mkdir() + DIST_DIR.mkdir() + + +@cli.command() +def wheel(): + """Build the wheel for PyPI.""" + print("Building wheel...") + subprocess.check_call( + [ + "python", + "-m", + "build", + "--outdir", + DIST_DIR, + ] + ) + if os.environ.get("GITHUB_REF", "").startswith("refs/tags/"): + ver = version() # assert for tags that the version matches the tag. + else: + ver = "*" + (whl,) = DIST_DIR.glob(f"mitmproxy-{ver}-py3-none-any.whl") + print(f"Found wheel package: {whl}") + subprocess.check_call(["tox", "-e", "wheeltest", "--", whl]) + + +class ZipFile2(zipfile.ZipFile): + # ZipFile and tarfile have slightly different APIs. Let's fix that. + def add(self, name: str, arcname: str) -> None: + return self.write(name, arcname) + + def __enter__(self) -> ZipFile2: + return self + + @property + def name(self) -> str: + assert self.filename + return self.filename + + +def archive(path: Path) -> tarfile.TarFile | ZipFile2: + if platform.system() == "Windows": + return ZipFile2(path.with_name(f"{path.name}.zip"), "w") + else: + return tarfile.open(path.with_name(f"{path.name}.tar.gz"), "w:gz") + + +def version() -> str: + return os.environ.get("GITHUB_REF_NAME", "").replace("/", "-") or os.environ.get( + "BUILD_VERSION", "dev" + ) + + +def operating_system() -> str: + match (platform.system(), platform.machine()): + case ("Windows", _): + return "windows" + case ("Linux", _): + return "linux" + case ("Darwin", "x86_64"): + return "macos-x86_64" + case ("Darwin", "arm64"): + return "macos-arm64" + warnings.warn("Unexpected platform.") + return f"{platform.system()}-{platform.machine()}" + + +def _pyinstaller(specfile: str) -> None: + print(f"Invoking PyInstaller with {specfile}...") + subprocess.check_call( + [ + "pyinstaller", + "--clean", + "--workpath", + TEMP_DIR / "pyinstaller/temp", + "--distpath", + TEMP_DIR / "pyinstaller/out", + specfile, + ], + cwd=here / "specs", + ) + + +@cli.command() +def standalone_binaries(): + """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/out" / tool + if platform.system() == "Windows": + executable = executable.with_suffix(".exe") + + f.add(str(executable), str(executable.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/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") + 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(): + """Windows: Build the MSIX installer for the Windows Store.""" + _ensure_pyinstaller_onedir() + + shutil.copytree( + TEMP_DIR / "pyinstaller/out/onedir", + TEMP_DIR / "msix", + dirs_exist_ok=True, + ) + shutil.copytree(here / "windows-installer", TEMP_DIR / "msix", dirs_exist_ok=True) + + 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", ".") + ) + manifest.write_text(manifest.read_text().replace("1.2.3", app_version)) + + makeappx_exe = ( + Path(os.environ["ProgramFiles(x86)"]) + / "Windows Kits/10/App Certification Kit/makeappx.exe" + ) + subprocess.check_call( + [ + makeappx_exe, + "pack", + "/d", + TEMP_DIR / "msix", + "/p", + DIST_DIR / f"mitmproxy-{version()}-installer.msix", + ], + ) + assert (DIST_DIR / f"mitmproxy-{version()}-installer.msix").exists() + + +@cli.command() +def installbuilder_installer(): + """Windows: Build the InstallBuilder installer.""" + _ensure_pyinstaller_onedir() + + 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\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: + outfile.write(f.decrypt(infile.read())) + + if not IB_CLI.exists(): + if not IB_SETUP.exists(): + 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 + if round(100 * done / total) != round(100 * (done - blocksize) / total): + print(f"Downloading... {round(100 * done / total)}%") + + tmp = IB_SETUP.with_suffix(".tmp") + urllib.request.urlretrieve( + url, + tmp, + reporthook=report, + ) + tmp.rename(IB_SETUP) + + ib_setup_hash = hashlib.sha256() + with IB_SETUP.open("rb") as fp: + while True: + data = fp.read(65_536) + if not data: + 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()}" + ) + + print("Install InstallBuilder...") + subprocess.run( + [IB_SETUP, "--mode", "unattended", "--unattendedmodeui", "none"], check=True + ) + assert IB_CLI.is_file() + + print("Run InstallBuilder...") + subprocess.check_call( + [ + IB_CLI, + "build", + str(IB_DIR / "mitmproxy.xml"), + "windows-x64", + "--license", + str(IB_LICENSE), + "--setvars", + f"project.version={version()}", + "--verbose", + ], + cwd=IB_DIR, + ) + installer = DIST_DIR / f"mitmproxy-{version()}-windows-x64-installer.exe" + assert installer.exists() + + print("Run installer...") + subprocess.run( + [installer, "--mode", "unattended", "--unattendedmodeui", "none"], check=True + ) + _test_binaries(Path(r"C:\Program Files\mitmproxy\bin")) + + +if __name__ == "__main__": + cli() diff --git a/release/cibuild.py b/release/cibuild.py deleted file mode 100755 index a1c6ac5e6f..0000000000 --- a/release/cibuild.py +++ /dev/null @@ -1,633 +0,0 @@ -#!/usr/bin/env python3 - -import contextlib -import hashlib -import os -import platform -import re -import shutil -import subprocess -import sys -import tarfile -import urllib.request -import zipfile -from dataclasses import dataclass -from pathlib import Path -from typing import Optional, Union - -import click -import cryptography.fernet -import parver - - -@contextlib.contextmanager -def chdir(path: Path): # pragma: no cover - old_dir = os.getcwd() - os.chdir(path) - yield - os.chdir(old_dir) - - -class BuildError(Exception): - pass - - -def bool_from_env(envvar: str) -> bool: - val = os.environ.get(envvar, "") - if not val or val.lower() in ("0", "false"): - return False - else: - return True - - -class ZipFile2(zipfile.ZipFile): - # ZipFile and tarfile have slightly different APIs. Let's fix that. - def add(self, name: str, arcname: str) -> None: - return self.write(name, arcname) - - def __enter__(self) -> "ZipFile2": - return self - - -@dataclass(frozen=True, repr=False) -class BuildEnviron: - PLATFORM_TAGS = { - "Darwin": "osx", - "Windows": "windows", - "Linux": "linux", - } - - system: str - root_dir: Path - branch: Optional[str] = None - tag: Optional[str] = None - is_pull_request: bool = True - should_build_wheel: bool = False - should_build_docker: bool = False - should_build_pyinstaller: bool = False - should_build_wininstaller: bool = False - has_aws_creds: bool = False - has_twine_creds: bool = False - docker_username: Optional[str] = None - docker_password: Optional[str] = None - build_key: Optional[str] = None - - @classmethod - def from_env(cls) -> "BuildEnviron": - branch = None - tag = None - - if ref := os.environ.get("GITHUB_REF", ""): - if ref.startswith("refs/heads/"): - branch = ref.replace("refs/heads/", "") - if ref.startswith("refs/pull/"): - branch = "pr-" + ref.split("/")[2] - if ref.startswith("refs/tags/"): - tag = ref.replace("refs/tags/", "") - - is_pull_request = ( - os.environ.get("GITHUB_EVENT_NAME", "pull_request") == "pull_request" - ) - - return cls( - system=platform.system(), - root_dir=Path(__file__).parent.parent, - branch=branch, - tag=tag, - is_pull_request=is_pull_request, - should_build_wheel=bool_from_env("CI_BUILD_WHEEL"), - should_build_pyinstaller=bool_from_env("CI_BUILD_PYINSTALLER"), - should_build_wininstaller=bool_from_env("CI_BUILD_WININSTALLER"), - should_build_docker=bool_from_env("CI_BUILD_DOCKER"), - has_aws_creds=bool_from_env("AWS_ACCESS_KEY_ID"), - has_twine_creds=bool_from_env("TWINE_USERNAME") - and bool_from_env("TWINE_PASSWORD"), - docker_username=os.environ.get("DOCKER_USERNAME", None), - docker_password=os.environ.get("DOCKER_PASSWORD", None), - build_key=os.environ.get("CI_BUILD_KEY", None), - ) - - def archive(self, path: Path) -> Union[tarfile.TarFile, ZipFile2]: - if self.system == "Windows": - return ZipFile2(path, "w") - else: - return tarfile.open(path, "w:gz") - - @property - def archive_path(self) -> Path: - if self.system == "Windows": - ext = "zip" - else: - ext = "tar.gz" - return self.dist_dir / f"mitmproxy-{self.version}-{self.platform_tag}.{ext}" - - @property - def build_dir(self) -> Path: - return self.release_dir / "build" - - @property - def dist_dir(self) -> Path: - return self.release_dir / "dist" - - @property - def docker_tag(self) -> str: - if self.branch == "main": - t = "dev" - else: - t = self.version - return f"mitmproxy/mitmproxy:{t}" - - def dump_info(self, fp=sys.stdout) -> None: - lst = [ - "version", - "tag", - "branch", - "platform_tag", - "root_dir", - "release_dir", - "build_dir", - "dist_dir", - "upload_dir", - "should_build_wheel", - "should_build_pyinstaller", - "should_build_wininstaller", - "should_build_docker", - "should_upload_aws", - "should_upload_docker", - "should_upload_pypi", - ] - for attr in lst: - print(f"cibuild.{attr}={getattr(self, attr)}", file=fp) - - def check_version(self) -> None: - """ - Check that version numbers match our conventions. - Raises a ValueError if there is a mismatch. - """ - contents = (self.root_dir / "mitmproxy" / "version.py").read_text("utf8") - match = re.search(r'^VERSION = "(.+?)"', contents, re.M) - assert match - version = match.group(1) - - if self.is_prod_release: - # For production releases, we require strict version equality - if self.version != version: - raise ValueError( - f"Tag is {self.tag}, but mitmproxy/version.py is {version}." - ) - elif not self.is_maintenance_branch: - # Commits on maintenance branches don't need the dev suffix. This - # allows us to incorporate and test commits between tagged releases. - # For snapshots, we only ensure that mitmproxy/version.py contains a - # dev release. - version_info = parver.Version.parse(version) - if not version_info.is_devrelease: - raise ValueError( - f"Non-production releases must have dev suffix: {version}" - ) - - @property - def is_maintenance_branch(self) -> bool: - """ - Is this an untagged commit on a maintenance branch? - """ - if not self.tag and self.branch and re.match(r"v\d+\.x", self.branch): - return True - return False - - @property - def has_docker_creds(self) -> bool: - return bool(self.docker_username and self.docker_password) - - @property - def is_prod_release(self) -> bool: - if not self.tag or not self.tag.startswith("v"): - return False - try: - v = parver.Version.parse(self.version, strict=True) - except (parver.ParseError, BuildError): - return False - return not v.is_prerelease - - @property - def platform_tag(self) -> str: - if self.system in self.PLATFORM_TAGS: - return self.PLATFORM_TAGS[self.system] - raise BuildError(f"Unsupported platform: {self.system}") - - @property - def release_dir(self) -> Path: - return self.root_dir / "release" - - @property - def should_upload_docker(self) -> bool: - return all( - [ - (self.is_prod_release or self.branch in ["main", "dockertest"]), - self.should_build_docker, - self.has_docker_creds, - ] - ) - - @property - def should_upload_aws(self) -> bool: - return all( - [ - self.has_aws_creds, - ( - self.should_build_wheel - or self.should_build_pyinstaller - or self.should_build_wininstaller - ), - ] - ) - - @property - def should_upload_pypi(self) -> bool: - return all( - [ - self.is_prod_release, - self.should_build_wheel, - self.has_twine_creds, - ] - ) - - @property - def upload_dir(self) -> str: - if self.tag: - return self.version - else: - return f"branches/{self.version}" - - @property - def version(self) -> str: - if self.tag: - if self.tag.startswith("v"): - try: - parver.Version.parse(self.tag[1:], strict=True) - except parver.ParseError: - return self.tag - return self.tag[1:] - return self.tag - elif self.branch: - return self.branch - else: - raise BuildError( - "We're on neither a tag nor a branch - could not establish version" - ) - - -def build_wheel(be: BuildEnviron) -> None: # pragma: no cover - click.echo("Building wheel...") - subprocess.check_call( - [ - "python", - "setup.py", - "-q", - "bdist_wheel", - "--dist-dir", - be.dist_dir, - ] - ) - (whl,) = be.dist_dir.glob("mitmproxy-*-py3-none-any.whl") - click.echo(f"Found wheel package: {whl}") - subprocess.check_call(["tox", "-e", "wheeltest", "--", whl]) - - -DOCKER_PLATFORMS = "linux/amd64,linux/arm64" - - -def build_docker_image(be: BuildEnviron) -> None: # pragma: no cover - click.echo("Building Docker images...") - - (whl,) = be.dist_dir.glob("mitmproxy-*-py3-none-any.whl") - docker_build_dir = be.release_dir / "docker" - shutil.copy(whl, docker_build_dir / whl.name) - - subprocess.check_call( - [ - "docker", - "buildx", - "build", - "--tag", - be.docker_tag, - "--platform", - DOCKER_PLATFORMS, - "--build-arg", - f"MITMPROXY_WHEEL={whl.name}", - ".", - ], - cwd=docker_build_dir, - ) - # smoke-test the newly built docker image - - # build again without --platform but with --load to make the tag available, - # see https://github.com/docker/buildx/issues/59#issuecomment-616050491 - subprocess.check_call( - [ - "docker", - "buildx", - "build", - "--tag", - be.docker_tag, - "--load", - "--build-arg", - f"MITMPROXY_WHEEL={whl.name}", - ".", - ], - cwd=docker_build_dir, - ) - r = subprocess.run( - [ - "docker", - "run", - "--rm", - be.docker_tag, - "mitmdump", - "--version", - ], - check=True, - capture_output=True, - ) - print(r.stdout.decode()) - assert "Mitmproxy: " in r.stdout.decode() - - -def build_pyinstaller(be: BuildEnviron) -> None: # pragma: no cover - click.echo("Building pyinstaller package...") - - PYINSTALLER_SPEC = be.release_dir / "specs" - PYINSTALLER_TEMP = be.build_dir / "pyinstaller" - PYINSTALLER_DIST = be.build_dir / "binaries" / be.platform_tag - - if PYINSTALLER_TEMP.exists(): - shutil.rmtree(PYINSTALLER_TEMP) - if PYINSTALLER_DIST.exists(): - shutil.rmtree(PYINSTALLER_DIST) - - if be.platform_tag == "windows": - with chdir(PYINSTALLER_SPEC): - click.echo("Building PyInstaller binaries in directory mode...") - subprocess.check_call( - [ - "pyinstaller", - "--clean", - "--workpath", - PYINSTALLER_TEMP, - "--distpath", - PYINSTALLER_DIST, - "./windows-dir.spec", - ] - ) - for tool in ["mitmproxy", "mitmdump", "mitmweb"]: - click.echo(f"> {tool} --version") - executable = (PYINSTALLER_DIST / "onedir" / tool).with_suffix(".exe") - click.echo(subprocess.check_output([executable, "--version"]).decode()) - - with be.archive(be.archive_path) as archive: - for tool in ["mitmproxy", "mitmdump", "mitmweb"]: - # We can't have a folder and a file with the same name. - if tool == "mitmproxy": - tool = "mitmproxy_main" - # Make sure that we are in the spec folder. - with chdir(PYINSTALLER_SPEC): - click.echo(f"Building PyInstaller {tool} binary...") - excludes = [] - if tool != "mitmweb": - excludes.append("mitmproxy.tools.web") - if tool != "mitmproxy_main": - excludes.append("mitmproxy.tools.console") - - subprocess.check_call( - [ # type: ignore - "pyinstaller", - "--clean", - "--workpath", - PYINSTALLER_TEMP, - "--distpath", - PYINSTALLER_DIST, - "--onefile", - "--console", - "--icon", - "icon.ico", - ] - + [x for e in excludes for x in ["--exclude-module", e]] - + [tool] - ) - # Delete the spec file - we're good without. - os.remove(f"{tool}.spec") - - executable = PYINSTALLER_DIST / tool - if be.platform_tag == "windows": - executable = executable.with_suffix(".exe") - - # Remove _main suffix from mitmproxy executable - if "_main" in executable.name: - executable = executable.rename( - executable.with_name(executable.name.replace("_main", "")) - ) - - # Test if it works at all O:-) - click.echo(f"> {executable} --version") - click.echo(subprocess.check_output([executable, "--version"]).decode()) - - archive.add(str(executable), str(executable.name)) - click.echo(f"Packed {be.archive_path.name}.") - - -def build_wininstaller(be: BuildEnviron) -> None: # pragma: no cover - click.echo("Building wininstaller package...") - - IB_VERSION = "21.6.0" - IB_SETUP_SHA256 = "2bc9f9945cb727ad176aa31fa2fa5a8c57a975bad879c169b93e312af9d05814" - IB_DIR = be.release_dir / "installbuilder" - IB_SETUP = IB_DIR / "setup" / f"{IB_VERSION}-installer.exe" - IB_CLI = Path( - fr"C:\Program Files\VMware InstallBuilder Enterprise {IB_VERSION}\bin\builder-cli.exe" - ) - IB_LICENSE = IB_DIR / "license.xml" - - if not IB_LICENSE.exists() and not be.build_key: - click.echo("Cannot build windows installer without secret key.") - return - - if not IB_CLI.exists(): - if not IB_SETUP.exists(): - click.echo("Downloading InstallBuilder...") - - def report(block, blocksize, total): - done = block * blocksize - if round(100 * done / total) != round(100 * (done - blocksize) / total): - click.secho(f"Downloading... {round(100 * done / total)}%") - - tmp = IB_SETUP.with_suffix(".tmp") - urllib.request.urlretrieve( - f"https://clients.bitrock.com/installbuilder/installbuilder-enterprise-{IB_VERSION}-windows-x64-installer.exe", - tmp, - reporthook=report, - ) - tmp.rename(IB_SETUP) - - ib_setup_hash = hashlib.sha256() - with IB_SETUP.open("rb") as fp: - while True: - data = fp.read(65_536) - if not data: - break - ib_setup_hash.update(data) - if ib_setup_hash.hexdigest() != IB_SETUP_SHA256: # pragma: no cover - raise RuntimeError("InstallBuilder hashes don't match.") - - click.echo("Install InstallBuilder...") - subprocess.run( - [IB_SETUP, "--mode", "unattended", "--unattendedmodeui", "none"], check=True - ) - assert IB_CLI.is_file() - - if not IB_LICENSE.exists(): - assert be.build_key - click.echo("Decrypt InstallBuilder license...") - f = cryptography.fernet.Fernet(be.build_key.encode()) - with open(IB_LICENSE.with_suffix(".xml.enc"), "rb") as infile, open( - IB_LICENSE, "wb" - ) as outfile: - outfile.write(f.decrypt(infile.read())) - - click.echo("Run InstallBuilder...") - subprocess.run( - [ - IB_CLI, - "build", - str(IB_DIR / "mitmproxy.xml"), - "windows-x64", - "--license", - str(IB_LICENSE), - "--setvars", - f"project.version={be.version}", - "--verbose", - ], - check=True, - ) - assert (be.dist_dir / f"mitmproxy-{be.version}-windows-x64-installer.exe").exists() - - -@click.group(chain=True) -def cli(): # pragma: no cover - """ - mitmproxy build tool - """ - - -@cli.command("build") -def build(): # pragma: no cover - """ - Build a binary distribution - """ - be = BuildEnviron.from_env() - be.dump_info() - - be.check_version() - os.makedirs(be.dist_dir, exist_ok=True) - - if be.should_build_wheel: - build_wheel(be) - if be.should_build_docker: - build_docker_image(be) - if be.should_build_pyinstaller: - build_pyinstaller(be) - if be.should_build_wininstaller: - build_wininstaller(be) - - -@cli.command("upload") -def upload(): # pragma: no cover - """ - Upload build artifacts - - Uploads the wheels package to PyPi. - Uploads the Pyinstaller and wheels packages to the snapshot server. - Pushes the Docker image to Docker Hub. - """ - be = BuildEnviron.from_env() - be.dump_info() - - if be.is_pull_request: - click.echo("Refusing to upload artifacts from a pull request!") - return - - if be.should_upload_aws: - num_files = len([name for name in be.dist_dir.iterdir() if name.is_file()]) - click.echo(f"Uploading {num_files} files to AWS dir {be.upload_dir}...") - subprocess.check_call( - [ - "aws", - "s3", - "cp", - "--acl", - "public-read", - f"{be.dist_dir}/", - f"s3://snapshots.mitmproxy.org/{be.upload_dir}/", - "--recursive", - ] - ) - - if be.should_upload_pypi: - (whl,) = be.dist_dir.glob("mitmproxy-*-py3-none-any.whl") - click.echo(f"Uploading {whl} to PyPi...") - subprocess.check_call(["twine", "upload", whl]) - - if be.should_upload_docker: - click.echo(f"Uploading Docker image to tag={be.docker_tag}...") - subprocess.check_call( - [ - "docker", - "login", - "-u", - be.docker_username, - "-p", - be.docker_password, - ] - ) - - (whl,) = be.dist_dir.glob("mitmproxy-*-py3-none-any.whl") - docker_build_dir = be.release_dir / "docker" - shutil.copy(whl, docker_build_dir / whl.name) - # buildx is a bit weird in that we need to reinvoke build, but oh well. - subprocess.check_call( - [ - "docker", - "buildx", - "build", - "--tag", - be.docker_tag, - "--push", - "--platform", - DOCKER_PLATFORMS, - "--build-arg", - f"MITMPROXY_WHEEL={whl.name}", - ".", - ], - cwd=docker_build_dir, - ) - - if be.is_prod_release: - subprocess.check_call( - [ - "docker", - "buildx", - "build", - "--tag", - "mitmproxy/mitmproxy:latest", - "--push", - "--platform", - DOCKER_PLATFORMS, - "--build-arg", - f"MITMPROXY_WHEEL={whl.name}", - ".", - ], - cwd=docker_build_dir, - ) - - -if __name__ == "__main__": # pragma: no cover - cli() diff --git a/release/deploy-microsoft-store.py b/release/deploy-microsoft-store.py new file mode 100755 index 0000000000..ca99e9f002 --- /dev/null +++ b/release/deploy-microsoft-store.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +""" +This script submits a single MSIX installer to the Microsoft Store. + +The client_secret will expire after 24 months and needs to be recreated (see docstring below). + +References: + - https://docs.microsoft.com/en-us/windows/uwp/monetize/manage-app-submissions + - 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 +import sys +import tempfile +import urllib.parse +from zipfile import ZipFile + +# Security: No third-party dependencies here! + +assert ( + os.environ["GITHUB_REF"].startswith("refs/tags/") + or os.environ["GITHUB_REF"] == "refs/heads/citest" +) + +app_id = os.environ["MSFT_APP_ID"] +""" +The public application ID / product ID of the app. +For https://www.microsoft.com/store/productId/9NWNDLQMNZD7, the app id is 9NWNDLQMNZD7. +""" +app_flight = os.environ.get("MSFT_APP_FLIGHT", "") +""" +The application flight we want to target. This is useful to deploy ci test builds to a subset of users. +""" +tenant_id = os.environ["MSFT_TENANT_ID"] +""" +The tenant ID for the Azure AD application. +https://partner.microsoft.com/en-us/dashboard/account/v3/usermanagement +""" +client_id = os.environ["MSFT_CLIENT_ID"] +""" +The client ID for the Azure AD application. +https://partner.microsoft.com/en-us/dashboard/account/v3/usermanagement +""" +client_secret = os.environ["MSFT_CLIENT_SECRET"] +""" +The client secret. Expires every 24 months and needs to be recreated at +https://partner.microsoft.com/en-us/dashboard/account/v3/usermanagement +or at https://portal.azure.com/ -> App registrations -> Certificates & Secrets -> Client secrets. +""" + + +try: + _, msi_file = sys.argv +except ValueError: + print(f"Usage: {sys.argv[0]} installer.msix") + sys.exit(1) + +if app_flight: + app_id = f"{app_id}/flights/{app_flight}" + pending_submission = "pendingFlightSubmission" + packages = "flightPackages" +else: + pending_submission = "pendingApplicationSubmission" + packages = "applicationPackages" + +print("Obtaining auth token...") +auth = http.client.HTTPSConnection("login.microsoftonline.com") +auth.request( + "POST", + f"/{tenant_id}/oauth2/token", + body=urllib.parse.urlencode( + { + "grant_type": "client_credentials", + "client_id": client_id, + "client_secret": client_secret, + "resource": "https://manage.devcenter.microsoft.com", + } + ), + headers={"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"}, +) +token = json.loads(auth.getresponse().read())["access_token"] +auth.close() +headers = { + "Authorization": f"Bearer {token}", + "Content-type": "application/json", + "User-Agent": "Python/mitmproxy", +} + + +def request(method: str, path: str, body: str = "") -> bytes: + print(f"{method} {path}") + conn.request(method, path, body, headers=headers) + resp = conn.getresponse() + data = resp.read() + print(f"{resp.status} {resp.reason}") + # noinspection PyUnreachableCode + if False: + assert "CI" not in os.environ + # This contains sensitive data such as the fileUploadUrl, so don't print it in production. + print(data.decode(errors="ignore")) + assert 200 <= resp.status < 300 + return data + + +print("Getting app info...") +conn = http.client.HTTPSConnection("manage.devcenter.microsoft.com") +# print(request("GET", f"/v1.0/my/applications/{app_id}/listflights")) +app_info = json.loads(request("GET", f"/v1.0/my/applications/{app_id}")) + +if pending_submission in app_info: + print("Deleting pending submission...") + request( + "DELETE", + f"/v1.0/my/applications/{app_id}/submissions/{app_info[pending_submission]['id']}", + ) + +print("Creating new submission...") +submission = json.loads(request("POST", f"/v1.0/my/applications/{app_id}/submissions")) + +print("Updating submission...") +# Mark all existing packages for deletion. +for package in submission[packages]: + package["fileStatus"] = "PendingDelete" +submission[packages].append( + { + "fileName": f"installer.msix", + "fileStatus": "PendingUpload", + "minimumDirectXVersion": "None", + "minimumSystemRam": "None", + } +) +request( + "PUT", + f"/v1.0/my/applications/{app_id}/submissions/{submission['id']}", + json.dumps(submission), +) +conn.close() + +print(f"Zipping {msi_file}...") +with tempfile.TemporaryFile() as zipfile: + with ZipFile(zipfile, "w") as f: + f.write(msi_file, f"installer.msix") + zip_size = zipfile.tell() + zipfile.seek(0) + + print("Uploading zip file...") + host, _, path = submission["fileUploadUrl"].removeprefix("https://").partition("/") + upload = http.client.HTTPSConnection(host) + upload.request( + "PUT", + "/" + path, + zipfile, + { + "x-ms-blob-type": "BlockBlob", + "x-ms-version": "2019-12-12", + "Content-Length": str(zip_size), + }, + ) +resp = upload.getresponse() +resp.read() +print(resp.status, resp.reason) +assert 200 <= resp.status < 300 +upload.close() + +print("Publishing submission...") +# previous connection has timed out during upload. +conn = http.client.HTTPSConnection("manage.devcenter.microsoft.com") +request("POST", f"/v1.0/my/applications/{app_id}/submissions/{submission['id']}/commit") +# We could wait until it's published here, but CI is billed by the minute. +# resp = request("GET", f"/v1.0/my/applications/{app_id}/submissions/{submission['id']}/status") +conn.close() diff --git a/release/deploy.py b/release/deploy.py index 3c4d4aaaac..9e652a1480 100755 --- a/release/deploy.py +++ b/release/deploy.py @@ -1,16 +1,16 @@ #!/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/"): @@ -20,30 +20,57 @@ # 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}" + # 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", - "cp", - "--acl", - "public-read", - f"./release/dist/", - f"s3://snapshots.mitmproxy.org/{upload_dir}/", - "--recursive", + "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 + # Upload docs + def upload_docs(path: str, src: Path = root / "docs/public"): subprocess.check_call(["aws", "configure", "set", "preview.cloudfront", "true"]) subprocess.check_call( [ @@ -53,8 +80,8 @@ "--delete", "--acl", "public-read", - "docs/public", - "s3://docs.mitmproxy.org/dev", + src, + f"s3://docs.mitmproxy.org{path}", ] ) subprocess.check_call( @@ -65,6 +92,14 @@ "--distribution-id", "E1TH3USJHFQZ5Q", "--paths", - "/dev/*", + 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 f202e373a8..82f45b651c 100644 --- a/release/docker/DockerHub-README.md +++ b/release/docker/DockerHub-README.md @@ -4,14 +4,16 @@ Containerized version of [mitmproxy](https://mitmproxy.org/): an interactive, SS ## Usage +To launch the terminal user interface of mitmproxy: + ```sh -$ docker run --rm -it [-v ~/.mitmproxy:/home/mitmproxy/.mitmproxy] -p 8080:8080 mitmproxy/mitmproxy -[terminal user interface of mitmproxy is launched...] +$ 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. +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 a HTTP proxy on `localhost:8080`: +Once started, mitmproxy listens as an HTTP proxy on `localhost:8080`: ```sh $ http_proxy=http://localhost:8080/ curl http://example.com/ @@ -45,7 +47,8 @@ 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 diff --git a/release/docker/Dockerfile b/release/docker/Dockerfile index 0ebf71ec07..f0255c8cf4 100644 --- a/release/docker/Dockerfile +++ b/release/docker/Dockerfile @@ -1,14 +1,14 @@ -FROM python:3.10-bullseye as wheelbuilder +FROM python:3.11-bullseye as wheelbuilder ARG MITMPROXY_WHEEL COPY $MITMPROXY_WHEEL /wheels/ RUN pip install wheel && pip wheel --wheel-dir /wheels /wheels/${MITMPROXY_WHEEL} -FROM python:3.10-slim-bullseye +FROM python:3.11-slim-bullseye 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/* COPY --from=wheelbuilder /wheels /wheels diff --git a/release/docker/docker-entrypoint.sh b/release/docker/docker-entrypoint.sh index 3aaefe72fd..8a38c93ec1 100755 --- a/release/docker/docker-entrypoint.sh +++ b/release/docker/docker-entrypoint.sh @@ -7,10 +7,18 @@ 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 + 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/github-release-notes.txt b/release/github-release-notes.txt new file mode 100644 index 0000000000..49c12179af --- /dev/null +++ b/release/github-release-notes.txt @@ -0,0 +1,3 @@ +Changes: See [CHANGELOG.md](https://github.com/mitmproxy/mitmproxy/blob/main/CHANGELOG.md). + +You can find the latest release packages at https://mitmproxy.org/downloads/. 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 new file mode 100755 index 0000000000..f8ddbb8253 --- /dev/null +++ b/release/release.py @@ -0,0 +1,210 @@ +#!/usr/bin/env -S python3 -u +import datetime +import http.client +import json +import os +import re +import subprocess +import sys +import time +from pathlib import Path + +# Security: No third-party dependencies here! + +root = Path(__file__).absolute().parent.parent + + +def get(url: str) -> http.client.HTTPResponse: + assert url.startswith("https://") + host, path = re.split(r"(?=/)", url.removeprefix("https://"), maxsplit=1) + conn = http.client.HTTPSConnection(host) + conn.request("GET", path, headers={"User-Agent": "mitmproxy/release-bot"}) + resp = conn.getresponse() + print(f"HTTP {resp.status} {resp.reason}") + return resp + + +def get_json(url: str) -> dict: + resp = get(url) + body = resp.read() + try: + return json.loads(body) + except Exception as e: + raise RuntimeError(f"{resp.status=} {body=}") from e + + +if __name__ == "__main__": + version = sys.argv[1] + assert re.match(r"^\d+\.\d+\.\d+$", version) + major_version = int(version.split(".")[0]) + + skip_branch_status_check = sys.argv[2] == "true" + + # changing this is useful for testing on a fork. + repo = os.environ.get("GITHUB_REPOSITORY", "mitmproxy/mitmproxy") + print(f"{version=} {skip_branch_status_check=} {repo=}") + + branch = subprocess.run( + ["git", "branch", "--show-current"], + cwd=root, + check=True, + capture_output=True, + text=True, + ).stdout.strip() + + print("➡️ Working dir clean?") + assert not subprocess.run(["git", "status", "--porcelain"]).stdout + + if skip_branch_status_check: + 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" + ) + + print("➡️ Updating CHANGELOG.md...") + changelog = root / "CHANGELOG.md" + date = datetime.date.today().strftime("%d %B %Y") + 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) + 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 + ) + + print("➡️ Updating version...") + version_py = root / "mitmproxy" / "version.py" + ver = version_py.read_text("utf8") + ver, ok = re.subn(r'(?<=VERSION = ")[^"]+', version, ver) + assert ok == 1 + 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) + release_sha = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=root, + check=True, + capture_output=True, + text=True, + ).stdout.strip() + + if branch == "main": + print("➡️ Bump version...") + next_dev_version = f"{major_version + 1}.0.0.dev" + ver, ok = re.subn(r'(?<=VERSION = ")[^"]+', next_dev_version, ver) + assert ok == 1 + 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, + ) + + print("➡️ Pushing...") + subprocess.run( + ["git", "push", "--atomic", "origin", branch, version], 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, + ) + + print("➡️ Dispatching release workflow...") + subprocess.run( + ["gh", "workflow", "run", "main.yml", "--ref", version], 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"] + + all_done = True + if not workflows: + all_done = False # we expect to have at least one workflow. + for workflow in workflows: + if workflow["status"] != "completed": + all_done = False + if workflow["status"] == "waiting": + print(f"⚠️ CI is waiting for approval: {workflow['html_url']}") + + if all_done: + for workflow in workflows: + if workflow["conclusion"] != "success": + print(f"⚠️ {workflow['display_title']} workflow run failed.") + break + else: + time.sleep(30) # relatively strict rate limits here. + + print("➡️ Checking GitHub Releases...") + resp = get(f"https://api.github.com/repos/{repo}/releases/tags/{version}") + assert resp.status == 200 + + print("➡️ Checking PyPI...") + pypi_data = get_json("https://pypi.org/pypi/mitmproxy/json") + assert version in pypi_data["releases"] + + print("➡️ Checking docs archive...") + resp = get(f"https://docs.mitmproxy.org/archive/v{major_version}/") + assert resp.status == 200 + + print(f"➡️ Checking Docker ({version} tag)...") + resp = get( + f"https://hub.docker.com/v2/repositories/mitmproxy/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/mitmproxy/mitmproxy/tags/latest" + ) + docker_last_updated = datetime.datetime.fromisoformat( + 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) + + print("") + print("✅ All done. 🥳") + print("") diff --git a/release/selftest.py b/release/selftest.py new file mode 100644 index 0000000000..b601772f18 --- /dev/null +++ b/release/selftest.py @@ -0,0 +1,44 @@ +""" +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" + 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/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/mitmproxy_main b/release/specs/mitmproxy_main deleted file mode 100644 index 59160ff793..0000000000 --- a/release/specs/mitmproxy_main +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env python -from mitmproxy.tools.main import mitmproxy -mitmproxy() diff --git a/release/specs/windows-dir.spec b/release/specs/onedir.spec similarity index 55% rename from release/specs/windows-dir.spec rename to release/specs/onedir.spec index da6041f89d..290259c438 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 @@ -6,6 +7,11 @@ from PyInstaller.building.build_main import Analysis here = Path(r".") tools = ["mitmproxy", "mitmdump", "mitmweb"] +if platform.system() == "Darwin": + icon = "icon.icns" +else: + icon = "icon.ico" + analysis = Analysis( tools, excludes=["tcl", "tk", "tkinter"], @@ -25,10 +31,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, @@ -37,3 +45,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 new file mode 100644 index 0000000000..768b2ade0d --- /dev/null +++ b/release/specs/standalone.spec @@ -0,0 +1,26 @@ +# -*- mode: python ; coding: utf-8 -*- + +for tool in ["mitmproxy", "mitmdump", "mitmweb"]: + excludes = [] + if tool != "mitmweb": + excludes.append("mitmproxy.tools.web") + if tool != "mitmproxy": + excludes.append("mitmproxy.tools.console") + + a = Analysis( + [tool], + excludes=excludes, + ) + pyz = PYZ(a.pure, a.zipped_data) + + EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name=tool, + console=True, + icon='icon.ico', + ) diff --git a/release/windows-installer/AppxManifest.xml b/release/windows-installer/AppxManifest.xml new file mode 100644 index 0000000000..014c26262a --- /dev/null +++ b/release/windows-installer/AppxManifest.xml @@ -0,0 +1,85 @@ + + + + + + mitmproxy + mitmproxy.org + mitmproxy is a free and open source interactive HTTPS proxy. + Assets\logo.150x150.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/release/windows-store-experiment/Assets/logo.150x150.png b/release/windows-installer/Assets/logo.150x150.png similarity index 100% rename from release/windows-store-experiment/Assets/logo.150x150.png rename to release/windows-installer/Assets/logo.150x150.png diff --git a/release/windows-store-experiment/Assets/logo.44x44.png b/release/windows-installer/Assets/logo.44x44.png similarity index 100% rename from release/windows-store-experiment/Assets/logo.44x44.png rename to release/windows-installer/Assets/logo.44x44.png diff --git a/release/windows-store-experiment/Assets/logo.50x50.png b/release/windows-installer/Assets/logo.50x50.png similarity index 100% rename from release/windows-store-experiment/Assets/logo.50x50.png rename to release/windows-installer/Assets/logo.50x50.png diff --git a/release/windows-store-experiment/AppxManifest.xml b/release/windows-store-experiment/AppxManifest.xml deleted file mode 100644 index 5e687f0068..0000000000 --- a/release/windows-store-experiment/AppxManifest.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - Mitmproxy - Maximilian Hils - Assets\logo.44x44.png - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/release/windows-store-experiment/README.md b/release/windows-store-experiment/README.md deleted file mode 100644 index 3118aa23e9..0000000000 --- a/release/windows-store-experiment/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Mitmproxy on the Windows Store - -@mhils experimented with bringing mitmproxy to the Window Store using the Desktop Bridge. This would replace our current InstallBuilder setup and allow for clean installs and - more importantly - automatic updates. - -## Advantages - -- Automatic updates -- Clean installs -- Very simple setup on our end -- Possibility to roll out experimental releases to a subset of users - -## Disadvantages - -- No support for mitmproxy. That only runs under WSL. Making WSL nicer is a complementary effort. -- "Your developer account doesn’t have permission to submit apps converted with the Desktop Bridge at this time." (requested) -- New releases need to be submitted manually (Submission API is in preview). - -## Notes - -We do not want to force anyone to use this, we would of course keep our portable binaries (and, of course, WSL). diff --git a/setup.cfg b/setup.cfg index 660b9a2641..8cd2a7ab80 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,48 +1,3 @@ -[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 - -[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/ @@ -65,9 +20,8 @@ exclude = mitmproxy/net/http/cookies.py mitmproxy/net/http/message.py mitmproxy/net/http/multipart.py - mitmproxy/net/tcp.py mitmproxy/net/tls.py - mitmproxy/net/udp.py + mitmproxy/net/udp_wireguard.py mitmproxy/options.py mitmproxy/proxy/config.py mitmproxy/proxy/server.py diff --git a/setup.py b/setup.py deleted file mode 100644 index 54365ff853..0000000000 --- a/setup.py +++ /dev/null @@ -1,120 +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 :: 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", - ], - "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", - "blinker>=1.4, <1.5", - "Brotli>=1.0,<1.1", - "certifi>=2019.9.11", # no semver here - this should always be on the last release! - "cryptography>=36,<38", - "flask>=1.1.1,<2.2", - "h11>=0.11,<0.14", - "h2>=4.1,<5", - "hyperframe>=6.0,<7", - "kaitaistruct>=0.7,<0.10", - "ldap3>=2.8,<2.10", - "msgpack>=1.0.0, <1.1.0", - "passlib>=1.6.5, <1.8", - "protobuf>=3.14,<5", - "pyOpenSSL>=21.0,<22.1", - "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.2", - "publicsuffix2>=2.20190812,<3", - "zstandard>=0.11,<0.19", - ], - 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.1", - "pytest-asyncio>=0.17.0,<0.19", - "pytest-cov>=2.7.1,<3.1", - "pytest-timeout>=1.3.3,<2.2", - "pytest-xdist>=2.1.0,<3", - "pytest>=6.1.0,<8", - "requests>=2.9.1,<3", - "tox>=3.5,<4", - "wheel>=0.36.2,<0.38", - ], - }, -) diff --git a/test/bench/benchmark.py b/test/bench/benchmark.py index 9f733dcd21..b0d6902744 100644 --- a/test/bench/benchmark.py +++ b/test/bench/benchmark.py @@ -1,5 +1,7 @@ import asyncio import cProfile +import logging + from mitmproxy import ctx @@ -22,7 +24,7 @@ def response(self, f): self.resps += 1 async def procs(self): - ctx.log.error("starting benchmark") + logging.error("starting benchmark") backend = await asyncio.create_subprocess_exec("devd", "-q", "-p", "10001", ".") traf = await asyncio.create_subprocess_exec( "wrk", @@ -34,8 +36,8 @@ async def procs(self): stdout, _ = await traf.communicate() with open(ctx.options.benchmark_save_path + ".bench", mode="wb") as f: f.write(stdout) - ctx.log.error(f"Proxy saw {self.reqs} requests, {self.resps} responses") - ctx.log.error(stdout.decode("ascii")) + logging.error(f"Proxy saw {self.reqs} requests, {self.resps} responses") + logging.error(stdout.decode("ascii")) backend.kill() ctx.master.shutdown() @@ -54,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 9df569b1a2..63d2164110 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,10 +1,13 @@ +from __future__ import annotations + +import asyncio import os import socket -from mitmproxy.utils import data - 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") @@ -28,3 +31,28 @@ @pytest.fixture() def tdata(): return data.Data(__name__) + + +class AsyncLogCaptureFixture: + def __init__(self, caplog: pytest.LogCaptureFixture): + self.caplog = caplog + + def set_level(self, level: int | str, logger: str | None = None) -> None: + self.caplog.set_level(level, logger) + + async def await_log(self, text, timeout=2): + await asyncio.sleep(0) + for i in range(int(timeout / 0.01)): + if text in self.caplog.text: + return True + else: + await asyncio.sleep(0.01) + raise AssertionError(f"Did not find {text!r} in log:\n{self.caplog.text}") + + def clear(self) -> None: + self.caplog.clear() + + +@pytest.fixture +def caplog_async(caplog): + return AsyncLogCaptureFixture(caplog) 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 index 3d1b8b6787..36ac7f263d 100644 --- a/test/full_coverage_plugin.py +++ b/test/full_coverage_plugin.py @@ -1,8 +1,9 @@ -import os import configparser -import pytest +import os import sys +import pytest + here = os.path.abspath(os.path.dirname(__file__)) @@ -98,7 +99,7 @@ def pytest_runtestloop(session): (s, cov.report(s, ignore_errors=True, file=null)) for s in files ] coverage_values[name] = (overall, singles) - except: + except Exception: pass if any(v < 100 for v, _ in coverage_values.values()): diff --git a/test/helper_tools/dumperview.py b/test/helper_tools/dumperview.py index fd0fbc9b0c..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): @@ -39,6 +40,13 @@ def tcp(level): show(level, [f1]) +@cli.command() +@click.option("--level", default=1, help="Detail level") +def udp(level): + f1 = tflow.tudpflow() + show(level, [f1]) + + @cli.command() @click.option("--level", default=1, help="Detail level") def large(level): 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..c9528f4913 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: 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 7c989c3852..72ba8e1048 100755 --- a/test/individual_coverage.py +++ b/test/individual_coverage.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 - -import io +import configparser import contextlib -import os -import sys import glob -import multiprocessing -import configparser +import io import itertools +import multiprocessing +import os +import sys + import pytest @@ -48,9 +48,9 @@ def run_tests(src, test, fail): e = 0 else: cov = [ - l - for l in stdout.getvalue().split("\n") - if (src in l) or ("was never imported" in l) + line + for line in stdout.getvalue().split("\n") + if (src in line) or ("was never imported" in line) ] if len(cov) == 1: print("FAIL:", cov[0]) @@ -87,6 +87,8 @@ def main(): 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): 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 d282b33368..0926f091e1 100644 --- a/test/mitmproxy/addons/test_asgiapp.py +++ b/test/mitmproxy/addons/test_asgiapp.py @@ -44,7 +44,7 @@ async def noresponseapp(scope, receive, send): return -async def test_asgi_full(): +async def test_asgi_full(caplog): ps = Proxyserver() addons = [ asgiapp.WSGIApp(tapp, "testapp", 80), @@ -54,9 +54,8 @@ async def test_asgi_full(): with taddons.context(ps, *addons) as tctx: tctx.master.addons.add(next_layer.NextLayer()) tctx.configure(ps, listen_host="127.0.0.1", listen_port=0) - await ps.running() - await tctx.master.await_log("Proxy server listening", level="info") - proxy_addr = ps.tcp_server.sockets[0].getsockname()[:2] + assert await ps.setup_servers() + proxy_addr = ("127.0.0.1", ps.listen_addrs()[0][1]) reader, writer = await asyncio.open_connection(*proxy_addr) req = f"GET http://testapp:80/ HTTP/1.1\r\n\r\n" @@ -65,6 +64,8 @@ async def test_asgi_full(): assert header.startswith(b"HTTP/1.1 200 OK") body = await reader.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" @@ -73,6 +74,8 @@ async def test_asgi_full(): assert header.startswith(b"HTTP/1.1 200 OK") body = await reader.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!" @@ -81,6 +84,8 @@ async def test_asgi_full(): assert header.startswith(b"HTTP/1.1 200 OK") body = await reader.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" @@ -89,6 +94,9 @@ async def test_asgi_full(): assert header.startswith(b"HTTP/1.1 500") body = await reader.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" @@ -97,3 +105,9 @@ async def test_asgi_full(): assert header.startswith(b"HTTP/1.1 500") body = await reader.readuntil(b"ASGI Error") assert body == b"ASGI Error" + writer.close() + await writer.wait_closed() + assert "no response sent" in caplog.text + + 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..3fad8f5b8c 100644 --- a/test/mitmproxy/addons/test_block.py +++ b/test/mitmproxy/addons/test_block.py @@ -56,6 +56,6 @@ 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 diff --git a/test/mitmproxy/addons/test_browser.py b/test/mitmproxy/addons/test_browser.py index 0beb2071fc..e5b9c15570 100644 --- a/test/mitmproxy/addons/test_browser.py +++ b/test/mitmproxy/addons/test_browser.py @@ -4,29 +4,31 @@ from mitmproxy.test import taddons -async def test_browser(): - with mock.patch("subprocess.Popen") as po, mock.patch("shutil.which") as which: +def test_browser(caplog): + caplog.set_level("INFO") + with mock.patch("subprocess.Popen") as po, mock.patch( + "shutil.which" + ) as which, taddons.context(): which.return_value = "chrome" b = browser.Browser() - with taddons.context() as tctx: - b.start() - assert po.called + b.start() + assert po.called - b.start() - await tctx.master.await_log("Starting additional browser") - assert len(b.browser) == 2 - b.done() - assert not b.browser + b.start() + assert "Starting additional browser" in caplog.text + assert len(b.browser) == 2 + b.done() + assert not b.browser -async def test_no_browser(): +async def test_no_browser(caplog): + caplog.set_level("INFO") with mock.patch("shutil.which") as which: which.return_value = False b = browser.Browser() - with taddons.context() as tctx: - b.start() - await tctx.master.await_log("platform is not supported") + b.start() + assert "platform is not supported" in caplog.text async def test_get_browser_cmd_executable(): diff --git a/test/mitmproxy/addons/test_clientplayback.py b/test/mitmproxy/addons/test_clientplayback.py index 2b93d7aff7..f9f889e42f 100644 --- a/test/mitmproxy/addons/test_clientplayback.py +++ b/test/mitmproxy/addons/test_clientplayback.py @@ -1,18 +1,23 @@ 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: + server = await asyncio.start_server(handle_conn, "127.0.0.1", 0, **server_args) await server.start_serving() try: yield server.sockets[0].getsockname() @@ -20,9 +25,9 @@ async def tcp_server(handle_conn) -> Address: server.close() -@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): +async def test_playback(tdata, mode, concurrency): handler_ok = asyncio.Event() async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): @@ -36,7 +41,11 @@ 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() @@ -44,19 +53,42 @@ async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): 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" if mode == "upstream": - tctx.options.mode = f"upstream:http://{addr[0]}:{addr[1]}" + tctx.options.mode = [f"upstream:http://{addr[0]}:{addr[1]}"] flow.request.authority = f"{addr[0]}:{addr[1]}" 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) @@ -86,7 +118,7 @@ async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): flow = tflow.tflow(live=False) flow.request.scheme = b"https" flow.request.content = b"data" - tctx.options.mode = f"upstream:http://{addr[0]}:{addr[1]}" + tctx.options.mode = [f"upstream:http://{addr[0]}:{addr[1]}"] cp.start_replay([flow]) assert cp.count() == 1 await asyncio.wait_for(cp.queue.join(), 5) @@ -99,16 +131,16 @@ async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): ) -async def test_playback_crash(monkeypatch): +async def test_playback_crash(monkeypatch, caplog_async): async def raise_err(): raise ValueError("oops") monkeypatch.setattr(ReplayHandler, "replay", raise_err) cp = ClientPlayback() - with taddons.context(cp) as tctx: + with taddons.context(cp): cp.running() cp.start_replay([tflow.tflow(live=False)]) - await tctx.master.await_log("Client replay has crashed!", level="error") + await caplog_async.await_log("Client replay has crashed!") assert cp.count() == 0 cp.done() @@ -131,21 +163,21 @@ def test_check(): f.request.raw_content = None assert "missing content" in cp.check(f) - f = tflow.ttcpflow() - f.live = False - assert "Can only replay HTTP" in cp.check(f) + for f in (tflow.ttcpflow(), tflow.tudpflow()): + f.live = False + assert "Can only replay HTTP" in cp.check(f) -async def test_start_stop(tdata): +async def test_start_stop(tdata, caplog_async): cp = ClientPlayback() - with taddons.context(cp) as tctx: + with taddons.context(cp): cp.start_replay([tflow.tflow(live=False)]) assert cp.count() == 1 ws_flow = tflow.twebsocketflow() ws_flow.live = False cp.start_replay([ws_flow]) - await tctx.master.await_log("Can't replay WebSocket flows.", level="warn") + await caplog_async.await_log("Can't replay WebSocket flows.") assert cp.count() == 1 cp.stop_replay() diff --git a/test/mitmproxy/addons/test_command_history.py b/test/mitmproxy/addons/test_command_history.py index 8a8a8a5234..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 @@ -24,7 +24,7 @@ def test_load_and_save(self, tmpdir): with open(history_file) as f: assert f.read() == "cmd3\ncmd4\n" - async def test_done_writing_failed(self): + async def test_done_writing_failed(self, caplog): ch = command_history.CommandHistory() ch.VACUUM_SIZE = 1 with taddons.context(ch) as tctx: @@ -33,7 +33,7 @@ async def test_done_writing_failed(self): ch.history.append("cmd3") tctx.options.confdir = "/non/existent/path/foobar1234/" ch.done() - await tctx.master.await_log(f"Failed writing to {ch.history_file}") + assert "Failed writing to" in caplog.text def test_add_command(self): ch = command_history.CommandHistory() @@ -45,12 +45,12 @@ def test_add_command(self): ch.add_command("") assert ch.history == ["cmd1", "cmd2"] - async def test_add_command_failed(self): + async def test_add_command_failed(self, caplog): ch = command_history.CommandHistory() with taddons.context(ch) as tctx: tctx.options.confdir = "/non/existent/path/foobar1234/" ch.add_command("cmd1") - await tctx.master.await_log(f"Failed writing to {ch.history_file}") + assert "Failed writing to" in caplog.text def test_get_next_and_prev(self, tmpdir): ch = command_history.CommandHistory() @@ -152,7 +152,7 @@ def test_clear(self, tmpdir): ch.clear_history() - async def test_clear_failed(self, monkeypatch): + async def test_clear_failed(self, monkeypatch, caplog): ch = command_history.CommandHistory() with taddons.context(ch) as tctx: @@ -163,7 +163,7 @@ async def test_clear_failed(self, monkeypatch): with patch.object(Path, "unlink") as mock_unlink: mock_unlink.side_effect = IOError() ch.clear_history() - await tctx.master.await_log(f"Failed deleting {ch.history_file}") + assert "Failed deleting" in caplog.text def test_filter(self, tmpdir): ch = command_history.CommandHistory() 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 707d775f29..be2cb1b139 100644 --- a/test/mitmproxy/addons/test_core.py +++ b/test/mitmproxy/addons/test_core.py @@ -1,18 +1,17 @@ -from unittest import mock +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(): sa = core.Core() with taddons.context(loadcore=False) as tctx: - assert tctx.master.options.server - tctx.command(sa.set, "server", "false") - assert not tctx.master.options.server + assert tctx.master.options.upstream_cert + tctx.command(sa.set, "upstream_cert", "false") + assert not tctx.master.options.upstream_cert with pytest.raises(exceptions.CommandError): tctx.command(sa.set, "nonexistent") @@ -167,25 +166,6 @@ def test_validation_simple(): tctx.configure( sa, add_upstream_certs_to_client_chain=True, upstream_cert=False ) - with pytest.raises(exceptions.OptionsError, match="Invalid mode"): - tctx.configure(sa, mode="Flibble") - - -@mock.patch("mitmproxy.platform.original_addr", None) -def test_validation_no_transparent(): - sa = core.Core() - with taddons.context() as tctx: - with pytest.raises(Exception, match="Transparent mode not supported"): - tctx.configure(sa, mode="transparent") - - -@mock.patch("mitmproxy.platform.original_addr") -def test_validation_modes(m): - sa = core.Core() - with taddons.context() as tctx: - tctx.configure(sa, mode="reverse:http://localhost") - with pytest.raises(Exception, match="Invalid server specification"): - tctx.configure(sa, mode="reverse:") def test_client_certs(tdata): diff --git a/test/mitmproxy/addons/test_cut.py b/test/mitmproxy/addons/test_cut.py index b17cf8155e..d2a30abb45 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): @@ -72,7 +74,7 @@ def qr(f): return fp.read() -async def test_cut_clip(): +async def test_cut_clip(caplog): v = view.View() c = cut.Cut() with taddons.context() as tctx: @@ -97,7 +99,7 @@ async def test_cut_clip(): ) pc.side_effect = pyperclip.PyperclipException(log_message) tctx.command(c.clip, "@all", "request.method") - await tctx.master.await_log(log_message, level="error") + assert log_message in caplog.text def test_cut_save(tmpdir): @@ -130,7 +132,7 @@ def test_cut_save(tmpdir): (FileNotFoundError, "No such file or directory"), ], ) -async def test_cut_save_open(exception, log_message, tmpdir): +async def test_cut_save_open(exception, log_message, tmpdir, caplog): f = str(tmpdir.join("path")) v = view.View() c = cut.Cut() @@ -141,7 +143,7 @@ async def test_cut_save_open(exception, log_message, tmpdir): with mock.patch("mitmproxy.addons.cut.open") as m: m.side_effect = exception(log_message) tctx.command(c.save, "@all", "request.method", f) - await tctx.master.await_log(log_message, level="error") + assert log_message in caplog.text def test_cut(): @@ -171,8 +173,9 @@ def test_cut(): assert c.cut(tflows, ["response.reason"]) == [[""]] assert c.cut(tflows, ["response.header[key]"]) == [[""]] - c = cut.Cut() - with taddons.context(): - tflows = [tflow.ttcpflow()] - assert c.cut(tflows, ["request.method"]) == [[""]] - assert c.cut(tflows, ["response.status"]) == [[""]] + for f in (tflow.ttcpflow(), tflow.tudpflow()): + c = cut.Cut() + with taddons.context(): + tflows = [f] + assert c.cut(tflows, ["request.method"]) == [[""]] + assert c.cut(tflows, ["response.status"]) == [[""]] 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 a4b959eb9c..a72d5ed257 100644 --- a/test/mitmproxy/addons/test_dns_resolver.py +++ b/test/mitmproxy/addons/test_dns_resolver.py @@ -1,14 +1,18 @@ import asyncio import ipaddress import socket -from typing import Callable +from collections.abc import Callable import pytest from mitmproxy import dns -from mitmproxy.addons import dns_resolver, proxyserver +from mitmproxy.addons import dns_resolver +from mitmproxy.addons import proxyserver from mitmproxy.connection import Address -from mitmproxy.test import taddons, tflow, tutils +from mitmproxy.proxy.mode_specs import ProxyMode +from mitmproxy.test import taddons +from mitmproxy.test import tflow +from mitmproxy.test import tutils async def test_simple(monkeypatch): @@ -17,13 +21,13 @@ async def test_simple(monkeypatch): ) dr = dns_resolver.DnsResolver() - with taddons.context(dr, proxyserver.Proxyserver()) as tctx: + with taddons.context(dr, proxyserver.Proxyserver()): f = tflow.tdnsflow() await dr.dns_request(f) assert f.response - tctx.options.dns_mode = "reverse:8.8.8.8" 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 diff --git a/test/mitmproxy/addons/test_dumper.py b/test/mitmproxy/addons/test_dumper.py index e1041226ba..c7f2e85d4e 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 @@ -173,7 +174,8 @@ def test_echo_request_line(): sio.truncate(0) -async def test_contentview(): +async def test_contentview(caplog): + caplog.set_level("DEBUG") with mock.patch("mitmproxy.contentviews.auto.ViewAuto.__call__") as va: va.side_effect = ValueError("") sio = io.StringIO() @@ -181,7 +183,7 @@ async def test_contentview(): with taddons.context(d) as tctx: tctx.configure(d, flow_detail=4) d.response(tflow.tflow()) - await tctx.master.await_log("content viewer failed") + assert "content viewer failed" in caplog.text def test_tcp(): @@ -199,6 +201,21 @@ def test_tcp(): assert "Error in TCP" in sio.getvalue() +def test_udp(): + sio = io.StringIO() + d = dumper.Dumper(sio) + with taddons.context(d) as ctx: + ctx.configure(d, flow_detail=3, showhost=True) + f = tflow.tudpflow() + d.udp_message(f) + assert "it's me" in sio.getvalue() + sio.truncate(0) + + f = tflow.tudpflow(client_conn=True, err=True) + d.udp_error(f) + assert "Error in UDP" in sio.getvalue() + + def test_dns(): sio = io.StringIO() d = dumper.Dumper(sio) @@ -210,6 +227,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() @@ -263,6 +286,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 = "QUIC" + # 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 = "QUIC" + # 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 17d3f12e0b..b96a6dbadc 100644 --- a/test/mitmproxy/addons/test_errorcheck.py +++ b/test/mitmproxy/addons/test_errorcheck.py @@ -1,29 +1,33 @@ -import asyncio +import logging import pytest -from mitmproxy import log from mitmproxy.addons.errorcheck import ErrorCheck +from mitmproxy.tools import main -@pytest.mark.parametrize("do_log", [True, False]) -def test_errorcheck(capsys, do_log): - async def run(): - # suppress error that task exception was not retrieved. - asyncio.get_running_loop().set_exception_handler(lambda *_: 0) - e = ErrorCheck(do_log) - e.add_log(log.LogEntry("fatal", "error")) - await e.running() - await asyncio.sleep(0) - +def test_errorcheck(tdata, capsys): + """Integration test: Make sure that we catch errors on startup an exit.""" with pytest.raises(SystemExit): - asyncio.run(run()) - - if do_log: - assert capsys.readouterr().err == "Error on startup: fatal\n" + main.mitmproxy( + [ + "-s", + tdata.path("mitmproxy/data/addonscripts/load_error.py"), + ] + ) + assert "Error logged during startup" in capsys.readouterr().err async def test_no_error(): e = ErrorCheck() - await e.running() - await asyncio.sleep(0) + 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" in capsys.readouterr().err diff --git a/test/mitmproxy/addons/test_eventstore.py b/test/mitmproxy/addons/test_eventstore.py index 3fbd2b90b0..32e030ab76 100644 --- a/test/mitmproxy/addons/test_eventstore.py +++ b/test/mitmproxy/addons/test_eventstore.py @@ -1,47 +1,62 @@ -from unittest import mock -from mitmproxy import log +import asyncio +import logging + from mitmproxy.addons import eventstore -def test_simple(): +async def test_simple(): store = eventstore.EventStore() assert not store.data - sig_add = mock.Mock(spec=lambda: 42) - sig_refresh = mock.Mock(spec=lambda: 42) + sig_add_called = False + sig_refresh_called = False + + def sig_add(entry): + nonlocal sig_add_called + sig_add_called = True + + def sig_refresh(): + nonlocal sig_refresh_called + sig_refresh_called = True + store.sig_add.connect(sig_add) store.sig_refresh.connect(sig_refresh) - assert not sig_add.called - assert not sig_refresh.called + assert not sig_add_called + assert not sig_refresh_called # test .log() - store.add_log(log.LogEntry("test", "info")) + logging.error("test") + await asyncio.sleep(0) assert store.data - assert sig_add.called - assert not sig_refresh.called + assert sig_add_called + assert not sig_refresh_called # test .clear() - sig_add.reset_mock() + sig_add_called = False store.clear() assert not store.data - assert not sig_add.called - assert sig_refresh.called + assert not sig_add_called + assert sig_refresh_called + store.done() -def test_max_size(): +async def test_max_size(): store = eventstore.EventStore(3) assert store.size == 3 - store.add_log(log.LogEntry("foo", "info")) - store.add_log(log.LogEntry("bar", "info")) - store.add_log(log.LogEntry("baz", "info")) + logging.warning("foo") + logging.warning("bar") + logging.warning("baz") + await asyncio.sleep(0) assert len(store.data) == 3 - assert ["foo", "bar", "baz"] == [x.msg for x in store.data] + assert "baz" in store.data[-1].msg # overflow - store.add_log(log.LogEntry("boo", "info")) + logging.warning("boo") + await asyncio.sleep(0) assert len(store.data) == 3 - assert ["bar", "baz", "boo"] == [x.msg for x in store.data] + assert "boo" in store.data[-1].msg + store.done() diff --git a/test/mitmproxy/addons/test_export.py b/test/mitmproxy/addons/test_export.py index f8a24bf54f..f3dcb8d760 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 @@ -53,6 +53,11 @@ def tcp_flow(): return tflow.ttcpflow() +@pytest.fixture +def udp_flow(): + return tflow.tudpflow() + + @pytest.fixture(scope="module") def export_curl(): e = export.Export() @@ -88,6 +93,10 @@ def test_tcp(self, export_curl, tcp_flow): with pytest.raises(exceptions.CommandError): export_curl(tcp_flow) + def test_udp(self, export_curl, udp_flow): + with pytest.raises(exceptions.CommandError): + export_curl(udp_flow) + def test_escape_single_quotes_in_body(self, export_curl): request = tflow.tflow( req=tutils.treq(method=b"POST", headers=(), content=b"'&#") @@ -153,6 +162,10 @@ def test_tcp(self, tcp_flow): with pytest.raises(exceptions.CommandError): export.httpie_command(tcp_flow) + def test_udp(self, udp_flow): + with pytest.raises(exceptions.CommandError): + export.httpie_command(udp_flow) + def test_escape_single_quotes_in_body(self): request = tflow.tflow( req=tutils.treq(method=b"POST", headers=(), content=b"'&#") @@ -197,6 +210,13 @@ def test_tcp(self, tcp_flow): ): export.raw(tcp_flow) + def test_udp(self, udp_flow): + with pytest.raises( + exceptions.CommandError, + match="Can't export flow with no request or response", + ): + export.raw(udp_flow) + class TestRawRequest: def test_get(self, get_request): @@ -212,6 +232,10 @@ def test_tcp(self, tcp_flow): with pytest.raises(exceptions.CommandError): export.raw_request(tcp_flow) + def test_udp(self, udp_flow): + with pytest.raises(exceptions.CommandError): + export.raw_request(udp_flow) + class TestRawResponse: def test_get(self, get_response): @@ -226,6 +250,10 @@ def test_tcp(self, tcp_flow): with pytest.raises(exceptions.CommandError): export.raw_response(tcp_flow) + def test_udp(self, udp_flow): + with pytest.raises(exceptions.CommandError): + export.raw_response(udp_flow) + def qr(f): with open(f, "rb") as fp: @@ -267,17 +295,16 @@ def test_export(tmp_path) -> None: (FileNotFoundError, "No such file or directory"), ], ) -async def test_export_open(exception, log_message, tmpdir): +async def test_export_open(exception, log_message, tmpdir, caplog): f = str(tmpdir.join("path")) e = export.Export() - with taddons.context() as tctx: - with mock.patch("mitmproxy.addons.export.open") as m: - m.side_effect = exception(log_message) - e.file("raw_request", tflow.tflow(resp=True), f) - await tctx.master.await_log(log_message, level="error") + with mock.patch("mitmproxy.addons.export.open") as m: + m.side_effect = exception(log_message) + e.file("raw_request", tflow.tflow(resp=True), f) + assert log_message in caplog.text -async def test_clip(tmpdir): +async def test_clip(tmpdir, caplog): e = export.Export() with taddons.context() as tctx: tctx.configure(e) @@ -307,4 +334,4 @@ async def test_clip(tmpdir): ) pc.side_effect = pyperclip.PyperclipException(log_message) e.clip("raw_request", tflow.tflow(resp=True)) - await tctx.master.await_log(log_message, level="error") + assert log_message in caplog.text diff --git a/test/mitmproxy/addons/test_intercept.py b/test/mitmproxy/addons/test_intercept.py index 60008ddf9a..0e45a28920 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 @@ -75,3 +75,17 @@ async def test_tcp(): f = tflow.ttcpflow() await tctx.cycle(r, f) assert not f.intercepted + + +async def test_udp(): + r = intercept.Intercept() + with taddons.context(r) as tctx: + tctx.configure(r, intercept="~udp") + f = tflow.tudpflow() + await tctx.cycle(r, f) + assert f.intercepted + + tctx.configure(r, intercept_active=False) + f = tflow.tudpflow() + 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 fd0de25140..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( @@ -169,7 +171,8 @@ def test_simple(self, tmpdir): ml.request(f) assert f.response.content == b"foofoobar" - async def test_nonexistent_files(self, tmpdir, monkeypatch): + async def test_nonexistent_files(self, tmpdir, monkeypatch, caplog): + caplog.set_level("INFO") ml = MapLocal() with taddons.context(ml) as tctx: @@ -178,7 +181,7 @@ async def test_nonexistent_files(self, tmpdir, monkeypatch): f.request.url = b"https://example.org/css/nonexistent" ml.request(f) assert f.response.status_code == 404 - await tctx.master.await_log("None of the local file candidates exist") + assert "None of the local file candidates exist" in caplog.text tmpfile = tmpdir.join("foo.jpg") tmpfile.write("foo") @@ -188,7 +191,7 @@ async def test_nonexistent_files(self, tmpdir, monkeypatch): f = tflow.tflow() f.request.url = b"https://example.org/images/foo.jpg" ml.request(f) - await tctx.master.await_log("could not read file") + assert "Could not read" in caplog.text def test_is_killed(self, tmpdir): ml = MapLocal() 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 05388e1d5c..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: @@ -83,7 +91,7 @@ def test_simple(self, tmpdir): mb.request(f) assert f.request.content == b"bar" - async def test_nonexistent(self, tmpdir): + async def test_nonexistent(self, tmpdir, caplog): mb = modifybody.ModifyBody() with taddons.context(mb) as tctx: with pytest.raises(Exception, match="Invalid file path"): @@ -96,4 +104,4 @@ async def test_nonexistent(self, tmpdir): f = tflow.tflow() f.request.content = b"foo" mb.request(f) - await tctx.master.await_log("could not read") + assert "Could not read" in caplog.text diff --git a/test/mitmproxy/addons/test_modifyheaders.py b/test/mitmproxy/addons/test_modifyheaders.py index 6a078488a6..83a46256fc 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 @@ -127,7 +128,7 @@ def test_simple(self, tmpdir): mh.request(f) assert f.request.headers["one"] == "two" - async def test_nonexistent(self, tmpdir): + async def test_nonexistent(self, tmpdir, caplog): mh = ModifyHeaders() with taddons.context(mh) as tctx: with pytest.raises( @@ -142,4 +143,4 @@ async def test_nonexistent(self, tmpdir): f = tflow.tflow() f.request.content = b"foo" mh.request(f) - await tctx.master.await_log("could not read") + 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 534fbd6fd6..b91a942237 100644 --- a/test/mitmproxy/addons/test_next_layer.py +++ b/test/mitmproxy/addons/test_next_layer.py @@ -1,22 +1,36 @@ +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 NeedsMoreData from mitmproxy.addons.next_layer import NextLayer +from mitmproxy.addons.next_layer import stack_match +from mitmproxy.connection import Client +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, layers +from mitmproxy.proxy.layers.http import HttpStream +from mitmproxy.proxy.mode_specs import ProxyMode from mitmproxy.test import taddons - -@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 "01000061" # handshake header @@ -34,6 +48,50 @@ def tctx(): "170018" ) +dtls_client_hello_with_extensions = bytes.fromhex( + "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" +) + +dns_query = bytes.fromhex("002a01000001000000000000076578616d706c6503636f6d0000010001") + +http_query = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" + class TestNextLayer: def test_configure(self): @@ -44,108 +102,574 @@ def test_configure(self): nl, allow_hosts=["example.org"], ignore_hosts=["example.com"] ) - def test_ignore_connection(self): + @pytest.mark.parametrize( + "mode, ignore, allow, transport_protocol, server_address, data_client, result", + [ + pytest.param( + [], + [], + ["example.com"], + "tcp", + "example.org", + http_query, + False, + id="extract host from http request", + ), + pytest.param( + ["wireguard"], + ["example.com"], + [], + "udp", + "10.0.0.53", + dns_query, + False, + id="special handling for wireguard mode", + ), + # ignore + pytest.param( + [], [], [], "example.com", "tcp", b"", False, id="nothing ignored" + ), + pytest.param( + [], ["example.com"], [], "tcp", "example.com", b"", True, id="address" + ), + pytest.param( + [], ["1.2.3.4"], [], "tcp", "example.com", b"", True, id="ip address" + ), + 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", + None, + client_hello_with_extensions, + True, + id="sni", + ), + pytest.param( + [], + ["example.com"], + [], + "tcp", + None, + client_hello_with_extensions[:-5], + NeedsMoreData, + id="incomplete client hello", + ), + pytest.param( + [], + ["example.com"], + [], + "tcp", + None, + 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", + None, + dtls_client_hello_with_extensions, + True, + id="dtls sni", + ), + pytest.param( + [], + ["example.com"], + [], + "udp", + None, + dtls_client_hello_with_extensions[:-5], + NeedsMoreData, + id="incomplete dtls client hello", + ), + pytest.param( + [], + ["example.com"], + [], + "udp", + None, + dtls_client_hello_with_extensions[:9] + b"\x00" * 200, + False, + id="invalid dtls client hello", + ), + pytest.param( + [], + ["example.com"], + [], + "udp", + None, + 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", + "decoy", + client_hello_with_extensions, + False, + id="allow: sni mismatch", + ), + ], + ) + def test_ignore_connection( + self, + mode: list[str], + ignore: list[str], + allow: list[str], + transport_protocol: TransportProtocol, + server_address: str, + data_client: bytes, + result: bool | type[NeedsMoreData], + ): nl = NextLayer() with taddons.context(nl) as tctx: - assert not nl.ignore_connection(("example.com", 443), b"") - - 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 + if ignore: + tctx.configure(nl, ignore_hosts=ignore) + if allow: + tctx.configure(nl, allow_hosts=allow) + if mode: + tctx.options.mode = mode + ctx = Context( + Client(peername=("192.168.0.42", 51234), sockname=("0.0.0.0", 8080)), + tctx.options, ) + ctx.client.transport_protocol = transport_protocol + if server_address: + ctx.server.address = (server_address, 443) + ctx.server.peername = ("1.2.3.4", 443) + if "wireguard" in tctx.options.mode: + ctx.server.peername = ("10.0.0.53", 53) + ctx.server.address = ("10.0.0.53", 53) + ctx.client.proxy_mode = ProxyMode.parse("wireguard") + if result is NeedsMoreData: + with pytest.raises(NeedsMoreData): + nl._ignore_connection(ctx, data_client) + else: + assert nl._ignore_connection(ctx, data_client) is result - def test_make_top_layer(self): + def test_next_layer(self, monkeypatch, caplog): + caplog.set_level(logging.INFO) nl = NextLayer() - ctx = MagicMock() + with taddons.context(nl) as tctx: - tctx.configure(nl, mode="regular") - assert isinstance(nl.make_top_layer(ctx), layers.modes.HttpProxy) + 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"]) - tctx.configure(nl, mode="transparent") - assert isinstance(nl.make_top_layer(ctx), layers.modes.TransparentProxy) + m.layer = preexisting = object() + nl.next_layer(m) + assert m.layer is preexisting - tctx.configure(nl, mode="reverse:http://example.com") - assert isinstance(nl.make_top_layer(ctx), layers.modes.ReverseProxy) + m.layer = None + monkeypatch.setattr(m, "data_client", lambda: http_query) + nl.next_layer(m) + assert m.layer - tctx.configure(nl, mode="socks5") - assert isinstance(nl.make_top_layer(ctx), layers.modes.Socks5Proxy) + 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 - def test_next_layer(self): - nl = NextLayer() - ctx = MagicMock() - ctx.client.alpn = None - ctx.server.address = ("example.com", 443) - with taddons.context(nl) as tctx: - ctx.layers = [] - assert isinstance(nl._next_layer(ctx, b"", b""), layers.modes.HttpProxy) - assert nl._next_layer(ctx, b"", b"") is None +@dataclass +class TConf: + before: list[type[Layer]] + after: list[type[Layer]] + proxy_mode: str = "regular" + transport_protocol: TransportProtocol = "tcp" + data_client: bytes = b"" + data_server: bytes = b"" + ignore_hosts: Sequence[str] = () + tcp_hosts: Sequence[str] = () + udp_hosts: Sequence[str] = () + ignore_conn: bool = False - 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) +explicit_proxy_configs = [ + pytest.param( + TConf( + before=[modes.HttpProxy], + after=[modes.HttpProxy, HttpLayer], + ), + id=f"explicit proxy: regular http", + ), + 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", + ), +] - ctx.layers = [] - assert isinstance(nl._next_layer(ctx, b"", b""), layers.modes.HttpProxy) - assert isinstance( - nl._next_layer(ctx, client_hello_no_extensions, b""), - layers.ClientTLSLayer, - ) +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 - ctx.layers = [] - assert isinstance(nl._next_layer(ctx, b"", b""), layers.modes.HttpProxy) - 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 - - ctx.layers = [] - tctx.configure(nl, mode="upstream:http://localhost:8081") - assert isinstance(nl._next_layer(ctx, b"", b""), layers.modes.HttpProxy) - 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.upstream + 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}", + ), + ] + ) + +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( + TConf( + before=[modes.ReverseProxy], + after=[modes.ReverseProxy, ServerQuicLayer, ClientQuicLayer, HttpLayer], + proxy_mode="reverse:http3://example.com", + ), + id="reverse proxy: http3", + ), + pytest.param( + TConf( + before=[modes.ReverseProxy], + after=[ + modes.ReverseProxy, + ServerQuicLayer, + ClientQuicLayer, + RawQuicLayer, + ], + proxy_mode="reverse:quic://example.com", + ), + id="reverse proxy: quic", + ), + ] +) + +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( + TConf( + before=[modes.TransparentProxy], + after=[modes.TransparentProxy, ServerQuicLayer, ClientQuicLayer], + data_client=quic_client_hello, + transport_protocol="udp", + ), + id="transparent proxy: quic", + ), + 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], + data_client=b"GET / HTTP/1.1\r\n", + ), + id="transparent proxy: http", + ), + pytest.param( + dataclasses.replace( + http, + tcp_hosts=["example.com"], + after=[modes.TransparentProxy, TCPLayer], + ), + id="transparent proxy: tcp_hosts", + ), + pytest.param( + dataclasses.replace( + http, + ignore_hosts=["example.com"], + after=[modes.TransparentProxy, TCPLayer], + ignore_conn=True, + ), + id="transparent proxy: ignore_hosts", + ), + pytest.param( + dns := TConf( + before=[modes.TransparentProxy], + after=[modes.TransparentProxy, DNSLayer], + transport_protocol="udp", + data_client=dns_query, + ), + id="transparent proxy: dns", + ), + pytest.param( + TConf( + before=[modes.TransparentProxy], + after=[modes.TransparentProxy, UDPLayer], + transport_protocol="udp", + data_client=b"\xFF", + ), + id="transparent proxy: raw udp", + ), + pytest.param( + dataclasses.replace( + dns, + udp_hosts=["example.com"], + after=[modes.TransparentProxy, UDPLayer], + ), + id="transparent proxy: udp_hosts", + ), +] - 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) +@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)), + tctx.options, + ) + ctx.server.address = ("example.com", 42) + # these aren't properly set up, but this does not matter here. + ctx.client.transport_protocol = test_conf.transport_protocol + 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) - l = MagicMock() - l.layer = None - nl.next_layer(l) - assert isinstance(l.layer, layers.modes.HttpProxy) + last_layer = ctx.layers[-1] + if isinstance(last_layer, (UDPLayer, TCPLayer)): + assert bool(last_layer.flow) ^ test_conf.ignore_conn diff --git a/test/mitmproxy/addons/test_onboarding.py b/test/mitmproxy/addons/test_onboarding.py index dc90e4073c..adee52762a 100644 --- a/test/mitmproxy/addons/test_onboarding.py +++ b/test/mitmproxy/addons/test_onboarding.py @@ -14,14 +14,14 @@ class TestApp: def addons(self): return [onboarding.Onboarding()] - async def test_basic(self, client): + def test_basic(self, client): ob = onboarding.Onboarding() with taddons.context(ob) as tctx: tctx.configure(ob) assert client.get("/").status_code == 200 - @pytest.mark.parametrize("ext", ["pem", "p12", "cer"]) - async def test_cert(self, client, ext, tdata): + @pytest.mark.parametrize("ext", ["pem", "p12", "cer", "magisk"]) + def test_cert(self, client, ext, tdata): ob = onboarding.Onboarding() with taddons.context(ob) as tctx: tctx.configure(ob, confdir=tdata.path("mitmproxy/data/confdir")) @@ -29,12 +29,15 @@ async def test_cert(self, client, ext, tdata): assert resp.status_code == 200 assert resp.data - @pytest.mark.parametrize("ext", ["pem", "p12", "cer"]) - async def test_head(self, client, ext, tdata): + @pytest.mark.parametrize("ext", ["pem", "p12", "cer", "magisk"]) + def test_head(self, client, ext, tdata): ob = onboarding.Onboarding() with taddons.context(ob) as tctx: tctx.configure(ob, confdir=tdata.path("mitmproxy/data/confdir")) resp = client.head(f"http://{tctx.options.onboarding_host}/cert/{ext}") assert resp.status_code == 200 assert "Content-Length" in resp.headers + assert "Content-Type" in resp.headers + assert "Content-Disposition" in resp.headers + assert "attachment" in resp.headers["Content-Disposition"] assert not resp.data diff --git a/test/mitmproxy/addons/test_proxyauth.py b/test/mitmproxy/addons/test_proxyauth.py index a27ccd4671..743bc635bf 100644 --- a/test/mitmproxy/addons/test_proxyauth.py +++ b/test/mitmproxy/addons/test_proxyauth.py @@ -7,101 +7,87 @@ from mitmproxy import exceptions from mitmproxy.addons import proxyauth from mitmproxy.proxy.layers import modes +from mitmproxy.proxy.mode_specs import ProxyMode from mitmproxy.test import taddons from mitmproxy.test import tflow -class TestMkauth: - def test_mkauth_scheme(self): - assert ( - proxyauth.mkauth("username", "password") - == "basic dXNlcm5hbWU6cGFzc3dvcmQ=\n" - ) - - @pytest.mark.parametrize( - "scheme, expected", - [ - ("", " dXNlcm5hbWU6cGFzc3dvcmQ=\n"), - ("basic", "basic dXNlcm5hbWU6cGFzc3dvcmQ=\n"), - ("foobar", "foobar dXNlcm5hbWU6cGFzc3dvcmQ=\n"), - ], - ) - def test_mkauth(self, scheme, expected): - assert proxyauth.mkauth("username", "password", scheme) == expected - - -class TestParseHttpBasicAuth: - @pytest.mark.parametrize( - "input", - [ - "", - "foo bar", - "basic abc", - "basic " + binascii.b2a_base64(b"foo").decode("ascii"), - ], - ) - def test_parse_http_basic_auth_error(self, input): - with pytest.raises(ValueError): - proxyauth.parse_http_basic_auth(input) - - def test_parse_http_basic_auth(self): - input = proxyauth.mkauth("test", "test") - assert proxyauth.parse_http_basic_auth(input) == ("basic", "test", "test") +@pytest.mark.parametrize( + "scheme, expected", + [ + ("", " dXNlcm5hbWU6cGFzc3dvcmQ=\n"), + ("basic", "basic dXNlcm5hbWU6cGFzc3dvcmQ=\n"), + ("foobar", "foobar dXNlcm5hbWU6cGFzc3dvcmQ=\n"), + ], +) +def test_mkauth(scheme, expected): + assert proxyauth.mkauth("username", "password", scheme) == expected -class TestProxyAuth: - @pytest.mark.parametrize( - "mode, expected", - [ - ("", False), - ("foobar", False), - ("regular", True), - ("upstream:", True), - ("upstream:foobar", True), - ], - ) - def test_is_http_proxy(self, mode, expected): - up = proxyauth.ProxyAuth() - with taddons.context(up, loadcore=False) as ctx: - ctx.options.mode = mode - assert up.is_http_proxy is expected - - @pytest.mark.parametrize( - "is_http_proxy, expected", - [ - (True, "Proxy-Authorization"), - (False, "Authorization"), - ], - ) - def test_which_auth_header(self, is_http_proxy, expected): - up = proxyauth.ProxyAuth() - with mock.patch( - "mitmproxy.addons.proxyauth.ProxyAuth.is_http_proxy", new=is_http_proxy - ): - assert up.http_auth_header == expected - - @pytest.mark.parametrize( - "is_http_proxy, expected_status_code, expected_header", - [ - (True, 407, "Proxy-Authenticate"), - (False, 401, "WWW-Authenticate"), - ], - ) - def test_auth_required_response( - self, is_http_proxy, expected_status_code, expected_header - ): - up = proxyauth.ProxyAuth() - with mock.patch( - "mitmproxy.addons.proxyauth.ProxyAuth.is_http_proxy", new=is_http_proxy - ): - resp = up.make_auth_required_response() - assert resp.status_code == expected_status_code - assert expected_header in resp.headers.keys() +def test_parse_http_basic_auth(): + input = proxyauth.mkauth("test", "test") + assert proxyauth.parse_http_basic_auth(input) == ("basic", "test", "test") + + +@pytest.mark.parametrize( + "input", + [ + "", + "foo bar", + "basic abc", + "basic " + binascii.b2a_base64(b"foo").decode("ascii"), + ], +) +def test_parse_http_basic_auth_error(input): + with pytest.raises(ValueError): + proxyauth.parse_http_basic_auth(input) + + +@pytest.mark.parametrize( + "mode, expected", + [ + ("regular", True), + ("upstream:proxy", True), + ("reverse:example.com", False), + ], +) +def test_is_http_proxy(mode, expected): + f = tflow.tflow() + f.client_conn.proxy_mode = ProxyMode.parse(mode) + assert proxyauth.is_http_proxy(f) == expected + + +@pytest.mark.parametrize( + "is_http_proxy, expected", + [ + (True, "Proxy-Authorization"), + (False, "Authorization"), + ], +) +def test_http_auth_header(is_http_proxy, expected): + assert proxyauth.http_auth_header(is_http_proxy) == expected + +@pytest.mark.parametrize( + "is_http_proxy, expected_status_code, expected_header", + [ + (True, 407, "Proxy-Authenticate"), + (False, 401, "WWW-Authenticate"), + ], +) +def test_make_auth_required_response( + is_http_proxy, expected_status_code, expected_header +): + resp = proxyauth.make_auth_required_response(is_http_proxy) + assert resp.status_code == expected_status_code + assert expected_header in resp.headers.keys() + + +class TestProxyAuth: def test_socks5(self): pa = proxyauth.ProxyAuth() with taddons.context(pa, loadcore=False) as ctx: - ctx.configure(pa, proxyauth="foo:bar", mode="regular") + ctx.configure(pa, proxyauth="foo:bar") data = modes.Socks5AuthData(tflow.tclient_conn(), "foo", "baz") pa.socks5_auth(data) assert not data.valid @@ -112,9 +98,10 @@ def test_socks5(self): def test_authenticate(self): up = proxyauth.ProxyAuth() with taddons.context(up, loadcore=False) as ctx: - ctx.configure(up, proxyauth="any", mode="regular") + ctx.configure(up, proxyauth="any") f = tflow.tflow() + f.client_conn.proxy_mode = ProxyMode.parse("regular") assert not f.response up.authenticate_http(f) assert f.response.status_code == 407 @@ -126,12 +113,13 @@ def test_authenticate(self): assert not f.request.headers.get("Proxy-Authorization") f = tflow.tflow() - ctx.configure(up, mode="reverse") + f.client_conn.proxy_mode = ProxyMode.parse("reverse:https://example.com") assert not f.response up.authenticate_http(f) assert f.response.status_code == 401 f = tflow.tflow() + f.client_conn.proxy_mode = ProxyMode.parse("reverse:https://example.com") f.request.headers["Authorization"] = proxyauth.mkauth("test", "test") up.authenticate_http(f) assert not f.response @@ -177,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" ): @@ -210,16 +212,10 @@ def test_configure(self, monkeypatch, tdata): assert pa.validator("test", "test") assert not pa.validator("test", "foo") - with pytest.raises( - exceptions.OptionsError, - match="Proxy Authentication not supported in transparent mode.", - ): - ctx.configure(pa, proxyauth="any", mode="transparent") - def test_handlers(self): up = proxyauth.ProxyAuth() with taddons.context(up) as ctx: - ctx.configure(up, proxyauth="any", mode="regular") + ctx.configure(up, proxyauth="any") f = tflow.tflow() assert not f.response @@ -249,6 +245,7 @@ def test_handlers(self): [ "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 9abeb983ec..341bfbe091 100644 --- a/test/mitmproxy/addons/test_proxyserver.py +++ b/test/mitmproxy/addons/test_proxyserver.py @@ -1,29 +1,52 @@ +from __future__ import annotations + import asyncio -from contextlib import asynccontextmanager import socket +import ssl +from collections.abc import AsyncGenerator +from collections.abc import Callable +from contextlib import asynccontextmanager +from dataclasses import dataclass +from typing import Any +from typing import ClassVar +from typing import TypeVar +from unittest.mock import Mock import pytest - -from mitmproxy import dns, exceptions +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 + +import mitmproxy.platform +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.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.modes.HttpProxy(ctx), - lambda ctx: layers.HttpLayer(ctx, HTTPMode.regular), - lambda ctx: layers.TCPLayer(ctx), - ] def request(self, f): self.flows.append(f) @@ -31,9 +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: @@ -45,7 +65,9 @@ async def tcp_server(handle_conn) -> Address: server.close() -async def test_start_stop(): +async def test_start_stop(caplog_async): + caplog_async.set_level("INFO") + async def server_handler( reader: asyncio.StreamReader, writer: asyncio.StreamWriter ): @@ -55,17 +77,19 @@ async def server_handler( 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.tcp_server - await ps.running() - await tctx.master.await_log("Proxy server listening", level="info") - assert ps.tcp_server + assert not ps.servers + assert await ps.setup_servers() + ps.running() + await caplog_async.await_log("HTTP(S) proxy listening at") + assert ps.servers - proxy_addr = ps.tcp_server.sockets[0].getsockname()[:2] + proxy_addr = ps.listen_addrs()[0] reader, writer = await asyncio.open_connection(*proxy_addr) req = f"GET http://{addr[0]}:{addr[1]}/hello HTTP/1.1\r\n\r\n" writer.write(req.encode()) @@ -73,17 +97,23 @@ async def server_handler( await reader.readuntil(b"\r\n\r\n") == b"HTTP/1.1 204 No Content\r\n\r\n" ) - assert repr(ps) == "ProxyServer(running, 1 active conns)" + assert repr(ps) == "Proxyserver(1 active conns)" + await ( + ps.setup_servers() + ) # assert this can always be called without side effects tctx.configure(ps, server=False) - await tctx.master.await_log("Stopping Proxy server", level="info") - assert not ps.tcp_server + await caplog_async.await_log("stopped") + if ps.servers.is_updating: + async with ps.servers._lock: + pass # wait until start/stop is finished. + assert not ps.servers assert state.flows 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] + conn_handler = list(ps.connections.values())[0] client_handler = conn_handler.transports[conn_handler.client].handler writer.close() await writer.wait_closed() @@ -94,7 +124,7 @@ async def server_handler( for _ in range(5): # Get all other scheduled coroutines to run. await asyncio.sleep(0) - assert repr(ps) == "ProxyServer(stopped, 0 active conns)" + assert repr(ps) == "Proxyserver(0 active conns)" async def test_inject() -> None: @@ -105,14 +135,15 @@ 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) - await ps.running() - await tctx.master.await_log("Proxy server listening", level="info") - proxy_addr = ps.tcp_server.sockets[0].getsockname()[:2] + assert await ps.setup_servers() + ps.running() + proxy_addr = ps.servers["regular"].listen_addrs[0] reader, writer = await asyncio.open_connection(*proxy_addr) req = f"CONNECT {addr[0]}:{addr[1]} HTTP/1.1\r\n\r\n" @@ -130,38 +161,35 @@ async def server_handler( assert await reader.read(1) == b"c" -async def test_inject_fail() -> None: +async def test_inject_fail(caplog) -> None: ps = Proxyserver() - with taddons.context(ps) as tctx: - ps.inject_websocket(tflow.tflow(), True, b"test") - await tctx.master.await_log( - "Cannot inject WebSocket messages into non-WebSocket flows.", level="warn" - ) - ps.inject_tcp(tflow.tflow(), True, b"test") - await tctx.master.await_log( - "Cannot inject TCP messages into non-TCP flows.", level="warn" - ) + ps.inject_websocket(tflow.tflow(), True, b"test") + assert "Cannot inject WebSocket messages into non-WebSocket flows." in caplog.text + 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") - await tctx.master.await_log("Flow is not from a live connection.", level="warn") - ps.inject_websocket(tflow.ttcpflow(), True, b"test") - await tctx.master.await_log("Flow is not from a live connection.", level="warn") + 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") + assert "Cannot inject WebSocket messages into non-WebSocket flows" in caplog.text -async def test_warn_no_nextlayer(): +async def test_warn_no_nextlayer(caplog): """ Test that we log an error if the proxy server is started without NextLayer addon. That is a mean trap to fall into when writing end-to-end tests. """ ps = Proxyserver() with taddons.context(ps) as tctx: - tctx.configure(ps, listen_host="127.0.0.1", listen_port=0) - await ps.running() - await tctx.master.await_log("Proxy server listening at", level="info") - assert tctx.master.has_log( - "Warning: Running proxyserver without nextlayer addon!", level="warn" - ) - await ps.shutdown_server() + tctx.configure(ps, listen_host="127.0.0.1", listen_port=0, server=False) + assert await ps.setup_servers() + ps.running() + assert "Warning: Running proxyserver without nextlayer addon!" in caplog.text async def test_self_connect(): @@ -170,68 +198,68 @@ async def test_self_connect(): server.address = ("localhost", 8080) ps = Proxyserver() with taddons.context(ps) as tctx: - # not calling .running() here to avoid unnecessary socket - ps.options = tctx.options + tctx.configure(ps, listen_host="127.0.0.1", listen_port=0) + assert await ps.setup_servers() + ps.running() + assert ps.servers + server.address = ("localhost", ps.servers["regular"].listen_addrs[0][1]) ps.server_connect(server_hooks.ServerConnectionHookData(server, client)) assert "Request destination unknown" in server.error + tctx.configure(ps, server=False) + assert await ps.setup_servers() def test_options(): ps = Proxyserver() with taddons.context(ps) as tctx: - with pytest.raises(exceptions.OptionsError): - tctx.configure(ps, body_size_limit="invalid") - tctx.configure(ps, body_size_limit="1m") - with pytest.raises(exceptions.OptionsError): tctx.configure(ps, stream_large_bodies="invalid") tctx.configure(ps, stream_large_bodies="1m") - with pytest.raises(exceptions.OptionsError): - tctx.configure(ps, dns_mode="invalid") - tctx.configure(ps, dns_mode="regular") with pytest.raises(exceptions.OptionsError): - tctx.configure(ps, dns_mode="reverse") - tctx.configure(ps, dns_mode="reverse:8.8.8.8") - assert ps.dns_reverse_addr == ("8.8.8.8", 53) - - with pytest.raises(exceptions.OptionsError): - tctx.configure(ps, dns_mode="reverse:invalid:53") - tctx.configure(ps, dns_mode="reverse:8.8.8.8:53") - assert ps.dns_reverse_addr == ("8.8.8.8", 53) + tctx.configure(ps, body_size_limit="invalid") + tctx.configure(ps, body_size_limit="1m") with pytest.raises(exceptions.OptionsError): tctx.configure(ps, connect_addr="invalid") tctx.configure(ps, connect_addr="1.2.3.4") - assert ps.connect_addr == ("1.2.3.4", 0) + assert ps._connect_addr == ("1.2.3.4", 0) + + with pytest.raises(exceptions.OptionsError): + 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) -async def test_startup_err(monkeypatch) -> None: +async def test_startup_err(monkeypatch, caplog) -> None: async def _raise(*_): raise OSError("cannot bind") monkeypatch.setattr(asyncio, "start_server", _raise) ps = Proxyserver() - with taddons.context(ps) as tctx: - await ps.running() - await tctx.master.await_log("cannot bind", level="error") + with taddons.context(ps): + assert not await ps.setup_servers() + assert "cannot bind" in caplog.text + +async def test_shutdown_err(caplog_async) -> None: + caplog_async.set_level("INFO") -async def test_shutdown_err() -> None: - def _raise(*_): + async def _raise(*_): raise OSError("cannot close") ps = Proxyserver() with taddons.context(ps) as tctx: tctx.configure(ps, listen_host="127.0.0.1", listen_port=0) - await ps.running() - assert ps.running_servers - for server in ps.running_servers: - setattr(server, "close", _raise) - await ps.shutdown_server() - await tctx.master.await_log("cannot close", level="error") - assert ps.running_servers + assert await ps.setup_servers() + ps.running() + assert ps.servers + for server in ps.servers: + setattr(server, "stop", _raise) + tctx.configure(ps, server=False) + await caplog_async.await_log("cannot close") class DummyResolver: @@ -246,37 +274,587 @@ async def getaddrinfo(self, host: str, port: int, *, family: int): raise e -async def test_dns() -> None: +async def test_dns(caplog_async) -> None: + caplog_async.set_level("INFO") ps = Proxyserver() with taddons.context(ps, DummyResolver()) as tctx: tctx.configure( ps, - server=False, - dns_server=True, - dns_listen_host="127.0.0.1", - dns_listen_port=0, - dns_mode="regular", + mode=["dns@127.0.0.1:0"], ) - await ps.running() - await tctx.master.await_log("DNS server listening at", level="info") - assert ps.dns_server - dns_addr = ps.dns_server.sockets[0].getsockname()[:2] + assert await ps.setup_servers() + ps.running() + 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) - w.write(b"\x00") - await tctx.master.await_log("Invalid DNS datagram received", level="info") req = tdnsreq() w.write(req.packed) resp = dns.Message.unpack(await r.read(udp.MAX_DATAGRAM_SIZE)) assert req.id == resp.id and "8.8.8.8" in str(resp) - assert len(ps._connections) == 1 + assert len(ps.connections) == 1 w.write(req.packed) resp = dns.Message.unpack(await r.read(udp.MAX_DATAGRAM_SIZE)) assert req.id == resp.id and "8.8.8.8" in str(resp) - assert len(ps._connections) == 1 + 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)) assert req.id == resp.id and "8.8.8.8" in str(resp) - assert len(ps._connections) == 2 - await ps.shutdown_server() - await tctx.master.await_log("Stopping DNS server", level="info") + assert len(ps.connections) == 1 + dns_layer = ps.connections[(w.get_extra_info("sockname"), dns_addr)].layer + assert isinstance(dns_layer, layers.DNSLayer) + assert len(dns_layer.flows) == 2 + + w.write(b"\x00") + await caplog_async.await_log("sent an invalid message") + tctx.configure(ps, server=False) + await caplog_async.await_log("stopped") + + +def test_validation_no_transparent(monkeypatch): + monkeypatch.setattr(mitmproxy.platform, "original_addr", None) + ps = Proxyserver() + with taddons.context(ps) as tctx: + with pytest.raises(Exception, match="Transparent mode not supported"): + tctx.configure(ps, mode=["transparent"]) + + +def test_transparent_init(monkeypatch): + init = Mock() + monkeypatch.setattr(mitmproxy.platform, "original_addr", lambda: 1) + monkeypatch.setattr(mitmproxy.platform, "init_transparent_mode", init) + ps = Proxyserver() + with taddons.context(ps) as tctx: + tctx.configure(ps, mode=["transparent"], server=False) + assert init.called + + +@asynccontextmanager +async def udp_server(handle_conn) -> Address: + server = await udp.start_server(handle_conn, "127.0.0.1", 0) + try: + yield server.sockets[0].getsockname() + finally: + server.close() + + +async def test_udp(caplog_async) -> None: + caplog_async.set_level("INFO") + + def server_handler( + transport: asyncio.DatagramTransport, + data: bytes, + remote_addr: Address, + _: Address, + ): + assert data == b"\x16" + transport.sendto(b"\x01", remote_addr) + + ps = Proxyserver() + nl = NextLayer() + + with taddons.context(ps, nl) as tctx: + async with udp_server(server_handler) 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 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) + assert repr(ps) == "Proxyserver(1 active conns)" + assert len(ps.connections) == 1 + tctx.configure(ps, server=False) + await caplog_async.await_log("stopped") + + +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] = 5 + + 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("connection_strategy", ["lazy", "eager"]) +@pytest.mark.parametrize("scheme", ["http3", "quic"]) +async def test_reverse_http3_and_quic_stream( + caplog_async, scheme: str, connection_strategy: 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 + tctx.options.connection_strategy = connection_strategy + 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") + + +@pytest.mark.parametrize("connection_strategy", ["lazy", "eager"]) +async def test_reverse_quic_datagram(caplog_async, connection_strategy: 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 + tctx.options.connection_strategy = connection_strategy + 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") + + +@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 = udp.open_connection + + def open_connection_path( + host: str, port: int, *args, **kwargs + ) -> udp.UdpClient: + 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(udp, "open_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") diff --git a/test/mitmproxy/addons/test_readfile.py b/test/mitmproxy/addons/test_readfile.py index 20e7721afc..78513d586c 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 @@ -47,7 +47,7 @@ def test_configure(self): tctx.configure(rf, readfile_filter="~~") tctx.configure(rf, readfile_filter="") - async def test_read(self, tmpdir, data, corrupt_data): + async def test_read(self, tmpdir, data, corrupt_data, caplog_async): rf = readfile.ReadFile() with taddons.context(rf) as tctx: assert not rf.reading() @@ -65,25 +65,24 @@ async def test_read(self, tmpdir, data, corrupt_data): tf.write(corrupt_data.getvalue()) tctx.configure(rf, rfile=str(tf)) rf.running() - await tctx.master.await_log("corrupted") + await caplog_async.await_log("corrupted") - async def test_corrupt(self, corrupt_data): + async def test_corrupt(self, corrupt_data, caplog_async): rf = readfile.ReadFile() - with taddons.context(rf) as tctx: + with taddons.context(rf): with pytest.raises(exceptions.FlowReadException): await rf.load_flows(io.BytesIO(b"qibble")) - tctx.master.clear() + caplog_async.clear() with pytest.raises(exceptions.FlowReadException): await rf.load_flows(corrupt_data) - await tctx.master.await_log("file corrupted") + await caplog_async.await_log("file corrupted") - async def test_nonexistent_file(self): + async def test_nonexistent_file(self, caplog): rf = readfile.ReadFile() - with taddons.context(rf) as tctx: - with pytest.raises(exceptions.FlowReadException): - await rf.load_flows_from_path("nonexistent") - await tctx.master.await_log("nonexistent") + with pytest.raises(exceptions.FlowReadException): + await rf.load_flows_from_path("nonexistent") + assert "nonexistent" in caplog.text class TestReadFileStdin: diff --git a/test/mitmproxy/addons/test_save.py b/test/mitmproxy/addons/test_save.py index 48af437deb..b14f5ccace 100644 --- a/test/mitmproxy/addons/test_save.py +++ b/test/mitmproxy/addons/test_save.py @@ -47,6 +47,24 @@ def test_tcp(tmp_path): assert len(rd(p)) == 2 +def test_udp(tmp_path): + sa = save.Save() + with taddons.context(sa) as tctx: + p = str(tmp_path / "foo") + tctx.configure(sa, save_stream_file=p) + + tt = tflow.tudpflow() + sa.udp_start(tt) + sa.udp_end(tt) + + tt = tflow.tudpflow() + sa.udp_start(tt) + sa.udp_error(tt) + + tctx.configure(sa, save_stream_file=None) + assert len(rd(p)) == 2 + + def test_dns(tmp_path): sa = save.Save() with taddons.context(sa) as tctx: diff --git a/test/mitmproxy/addons/test_savehar.py b/test/mitmproxy/addons/test_savehar.py new file mode 100644 index 0000000000..c725ece55c --- /dev/null +++ b/test/mitmproxy/addons/test_savehar.py @@ -0,0 +1,237 @@ +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"] is None + + +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 + + +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 071911628b..add0801fbe 100644 --- a/test/mitmproxy/addons/test_script.py +++ b/test/mitmproxy/addons/test_script.py @@ -14,23 +14,22 @@ from mitmproxy.test import tflow from mitmproxy.tools import main - # We want this to be speedy for testing script.ReloadInterval = 0.1 -async def test_load_script(tdata): - with taddons.context() as tctx: - ns = script.load_script( - tdata.path("mitmproxy/data/addonscripts/recorder/recorder.py") - ) - assert ns.addons +def test_load_script(tmp_path, tdata, caplog): + ns = script.load_script( + tdata.path("mitmproxy/data/addonscripts/recorder/recorder.py") + ) + assert ns.addons - script.load_script("nonexistent") - await tctx.master.await_log("No such file or directory") + script.load_script("nonexistent") + assert "No such file or directory" in caplog.text - script.load_script(tdata.path("mitmproxy/data/addonscripts/recorder/error.py")) - await tctx.master.await_log("invalid syntax") + (tmp_path / "error.py").write_text("this is invalid syntax") + script.load_script(str(tmp_path / "error.py")) + assert "invalid syntax" in caplog.text def test_load_fullname(tdata): @@ -64,14 +63,15 @@ def test_quotes_around_filename(self, tdata): s = script.Script(f'"{path}"', False) assert '"' not in s.fullpath - async def test_simple(self, tdata): + async def test_simple(self, tdata, caplog_async): + caplog_async.set_level("DEBUG") sc = script.Script( tdata.path("mitmproxy/data/addonscripts/recorder/recorder.py"), True, ) with taddons.context(sc) as tctx: tctx.configure(sc) - await tctx.master.await_log("recorder configure") + await caplog_async.await_log("recorder configure") rec = tctx.master.addons.get("recorder") assert rec.call_log[0][0:2] == ("recorder", "load") @@ -81,62 +81,79 @@ async def test_simple(self, tdata): tctx.master.addons.trigger(HttpRequestHook(f)) assert rec.call_log[0][1] == "request" + sc.done() - async def test_reload(self, tmpdir): + async def test_reload(self, tmp_path, caplog_async): + caplog_async.set_level("INFO") with taddons.context() as tctx: - f = tmpdir.join("foo.py") - f.ensure(file=True) - f.write("\n") + f = tmp_path / "foo.py" + f.write_text("\n") sc = script.Script(str(f), True) tctx.configure(sc) - await tctx.master.await_log("Loading") + await caplog_async.await_log("Loading") + caplog_async.clear() - tctx.master.clear() for i in range(20): - f.write("\n") - if tctx.master.has_log("Loading"): + # Some filesystems only have second-level granularity, + # so just writing once again is not good enough. + f.write_text("\n") + if "Loading" in caplog_async.caplog.text: break await asyncio.sleep(0.1) else: raise AssertionError("No reload seen") + sc.done() - async def test_exception(self, tdata): + async def test_exception(self, tdata, caplog_async): + caplog_async.set_level("INFO") with taddons.context() as tctx: sc = script.Script( tdata.path("mitmproxy/data/addonscripts/error.py"), True, ) tctx.master.addons.add(sc) - await tctx.master.await_log("error load") + await caplog_async.await_log("error load") tctx.configure(sc) f = tflow.tflow(resp=True) tctx.master.addons.trigger(HttpRequestHook(f)) - await tctx.master.await_log("ValueError: Error!") - await tctx.master.await_log("error.py") + await caplog_async.await_log("ValueError: Error!") + await caplog_async.await_log("error.py") + sc.done() - async def test_optionexceptions(self, tdata): - 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 tctx.master.await_log("Options Error") + assert "Options Error" in caplog.text - async def test_addon(self, tdata): + async def test_addon(self, tdata, caplog_async): + caplog_async.set_level("INFO") with taddons.context() as tctx: sc = script.Script(tdata.path("mitmproxy/data/addonscripts/addon.py"), True) tctx.master.addons.add(sc) - await tctx.master.await_log("addon running") + await caplog_async.await_log("addon running") assert sc.ns.event_log == [ "scriptload", "addonload", "scriptconfigure", "addonconfigure", ] + sc.done() class TestCutTraceback: @@ -158,13 +175,16 @@ def test_simple(self): class TestScriptLoader: - async def test_script_run(self, tdata): + async def test_script_run(self, tdata, caplog_async): + caplog_async.set_level("DEBUG") rp = tdata.path("mitmproxy/data/addonscripts/recorder/recorder.py") sc = script.ScriptLoader() - with taddons.context(sc) as tctx: + with taddons.context(sc): sc.script_run([tflow.tflow(resp=True)], rp) - await tctx.master.await_log("recorder response") - debug = [i.msg for i in tctx.master.logs if i.level == "debug"] + await caplog_async.await_log("recorder response") + debug = [ + i.msg for i in caplog_async.caplog.records if i.levelname == "DEBUG" + ] assert debug == [ "recorder configure", "recorder running", @@ -174,11 +194,10 @@ async def test_script_run(self, tdata): "recorder response", ] - async def test_script_run_nonexistent(self): + async def test_script_run_nonexistent(self, caplog): sc = script.ScriptLoader() - with taddons.context(sc) as tctx: - sc.script_run([tflow.tflow(resp=True)], "/") - await tctx.master.await_log("No such script") + sc.script_run([tflow.tflow(resp=True)], "/") + assert "No such script" in caplog.text async def test_simple(self, tdata): sc = script.ScriptLoader() @@ -201,7 +220,8 @@ def test_dupes(self): with pytest.raises(exceptions.OptionsError): tctx.configure(sc, scripts=["one", "one"]) - async def test_script_deletion(self, tdata): + async def test_script_deletion(self, tdata, caplog_async): + caplog_async.set_level("INFO") tdir = tdata.path("mitmproxy/data/addonscripts/") with open(tdir + "/dummy.py", "w") as f: f.write("\n") @@ -212,28 +232,17 @@ async def test_script_deletion(self, tdata): tctx.configure( sl, scripts=[tdata.path("mitmproxy/data/addonscripts/dummy.py")] ) - await tctx.master.await_log("Loading") + await caplog_async.await_log("Loading") os.remove(tdata.path("mitmproxy/data/addonscripts/dummy.py")) - await tctx.master.await_log("Removing") + await caplog_async.await_log("Removing") await asyncio.sleep(0.1) assert not tctx.options.scripts assert not sl.addons - async def test_script_error_handler(self): - path = "/sample/path/example.py" - exc = SyntaxError - msg = "Error raised" - tb = True - with taddons.context() as tctx: - script.script_error_handler(path, exc, msg, tb) - await tctx.master.await_log("/sample/path/example.py") - await tctx.master.await_log("Error raised") - await tctx.master.await_log("lineno") - await tctx.master.await_log("NoneType") - - async def test_order(self, tdata): + async def test_order(self, tdata, caplog_async): + caplog_async.set_level("DEBUG") rec = tdata.path("mitmproxy/data/addonscripts/recorder") sc = script.ScriptLoader() sc.is_running = True @@ -246,8 +255,10 @@ async def test_order(self, tdata): "%s/c.py" % rec, ], ) - await tctx.master.await_log("configure") - debug = [i.msg for i in tctx.master.logs if i.level == "debug"] + await caplog_async.await_log("configure") + debug = [ + i.msg for i in caplog_async.caplog.records if i.levelname == "DEBUG" + ] assert debug == [ "a load", "a configure", @@ -260,7 +271,7 @@ async def test_order(self, tdata): "c running", ] - tctx.master.clear() + caplog_async.clear() tctx.configure( sc, scripts=[ @@ -270,15 +281,17 @@ async def test_order(self, tdata): ], ) - await tctx.master.await_log("b configure") - debug = [i.msg for i in tctx.master.logs if i.level == "debug"] + await caplog_async.await_log("b configure") + debug = [ + i.msg for i in caplog_async.caplog.records if i.levelname == "DEBUG" + ] assert debug == [ "c configure", "a configure", "b configure", ] - tctx.master.clear() + caplog_async.clear() tctx.configure( sc, scripts=[ @@ -286,8 +299,10 @@ async def test_order(self, tdata): "%s/a.py" % rec, ], ) - await tctx.master.await_log("e configure") - debug = [i.msg for i in tctx.master.logs if i.level == "debug"] + await caplog_async.await_log("e configure") + debug = [ + i.msg for i in caplog_async.caplog.records if i.levelname == "DEBUG" + ] assert debug == [ "c done", "b done", @@ -312,12 +327,13 @@ def test_order(tdata, capsys): tdata.path("mitmproxy/data/addonscripts/shutdown.py"), ] ) + time = r"\[[\d:.]+\] " assert re.match( - r"Loading script.+recorder.py\n" - r"\('recorder', 'load', .+\n" - r"\('recorder', 'configure', .+\n" - r"Loading script.+shutdown.py\n" - r"\('recorder', 'running', .+\n" - r"\('recorder', 'done', .+\n$", + rf"{time}Loading script.+recorder.py\n" + rf"{time}\('recorder', 'load', .+\n" + rf"{time}\('recorder', 'configure', .+\n" + rf"{time}Loading script.+shutdown.py\n" + rf"{time}\('recorder', 'running', .+\n" + rf"{time}\('recorder', 'done', .+\n$", capsys.readouterr().out, ) diff --git a/test/mitmproxy/addons/test_server_side_events.py b/test/mitmproxy/addons/test_server_side_events.py index 548afeabf5..641199ab50 100644 --- a/test/mitmproxy/addons/test_server_side_events.py +++ b/test/mitmproxy/addons/test_server_side_events.py @@ -1,14 +1,10 @@ from mitmproxy.addons.server_side_events import ServerSideEvents -from mitmproxy.test import taddons from mitmproxy.test.tflow import tflow -async def test_simple(): +async def test_simple(caplog): s = ServerSideEvents() - with taddons.context() as tctx: - f = tflow(resp=True) - f.response.headers["content-type"] = "text/event-stream" - s.response(f) - await tctx.master.await_log( - "mitmproxy currently does not support server side events." - ) + f = tflow(resp=True) + f.response.headers["content-type"] = "text/event-stream" + s.response(f) + assert "mitmproxy currently does not support server side events" in caplog.text diff --git a/test/mitmproxy/addons/test_serverplayback.py b/test/mitmproxy/addons/test_serverplayback.py index 77d4f28367..aa3a2178de 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" @@ -343,6 +364,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..906087c0d2 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 diff --git a/test/mitmproxy/addons/test_termlog.py b/test/mitmproxy/addons/test_termlog.py index e6ea67eff7..0ebf3601a0 100644 --- a/test/mitmproxy/addons/test_termlog.py +++ b/test/mitmproxy/addons/test_termlog.py @@ -1,30 +1,72 @@ +import asyncio +import builtins import io +import logging + +import pytest -from mitmproxy import log from mitmproxy.addons import termlog from mitmproxy.test import taddons +from mitmproxy.utils import vt_codes + + +@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 def test_output(capsys): + logging.getLogger().setLevel(logging.DEBUG) t = termlog.TermLog() with taddons.context(t) as tctx: tctx.options.termlog_verbosity = "info" tctx.configure(t) - t.add_log(log.LogEntry("one", "info")) - t.add_log(log.LogEntry("two", "debug")) - t.add_log(log.LogEntry("three", "warn")) - t.add_log(log.LogEntry("four", "error")) + logging.info("one") + logging.debug("two") + logging.warning("three") + logging.error("four") out, err = capsys.readouterr() - assert out.strip().splitlines() == ["one", "three"] - assert err.strip().splitlines() == ["four"] + assert "one" in out + assert "two" not in out + assert "three" in out + assert "four" in out + t.done() -def test_styling(monkeypatch) -> None: +async def test_styling(monkeypatch) -> None: + monkeypatch.setattr(vt_codes, "ensure_supported", lambda _: True) + f = io.StringIO() t = termlog.TermLog(out=f) - t.out_has_vt_codes = True with taddons.context(t) as tctx: tctx.configure(t) - t.add_log(log.LogEntry("hello world", "info")) + logging.warning("hello") + + assert "\x1b[33mhello\x1b[0m" in f.getvalue() + t.done() + + +async def test_cannot_print(monkeypatch) -> None: + def _raise(*args, **kwargs): + raise OSError + + monkeypatch.setattr(builtins, "print", _raise) + + t = termlog.TermLog() + with taddons.context(t) as tctx: + tctx.configure(t) + with pytest.raises(SystemExit) as exc_info: + logging.info("Should not log this, but raise instead") + + assert exc_info.value.args[0] == 1 - assert f.getvalue() == "\x1b[22mhello world\x1b[0m\n" + t.done() diff --git a/test/mitmproxy/addons/test_tlsconfig.py b/test/mitmproxy/addons/test_tlsconfig.py index a4bb9697ab..64144e12e5 100644 --- a/test/mitmproxy/addons/test_tlsconfig.py +++ b/test/mitmproxy/addons/test_tlsconfig.py @@ -1,16 +1,22 @@ 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.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_quic from test.mitmproxy.proxy.layers import test_tls @@ -60,6 +66,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 +84,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, @@ -92,10 +112,7 @@ def test_get_cert(self, tdata): 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. @@ -114,12 +131,17 @@ def test_get_cert(self, tdata): assert entry.cert.altnames == [ "example.mitmproxy.org", "server-address.example", + "127.0.0.1", ] # And now we also incorporate SNI. ctx.client.sni = "sni.example" entry = ta.get_cert(ctx) - assert entry.cert.altnames == ["example.mitmproxy.org", "sni.example"] + assert entry.cert.altnames == [ + "example.mitmproxy.org", + "sni.example", + "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())] @@ -132,18 +154,15 @@ 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 +181,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 +202,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 +218,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 +282,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 +302,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 +349,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) @@ -319,10 +411,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 +428,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, @@ -360,11 +446,9 @@ def test_client_cert_file(self, tdata, client_certs): assert self.do_handshake(tssl_client, tssl_server) assert tssl_server.obj.getpeercert() - async def test_ca_expired(self, monkeypatch): + async def test_ca_expired(self, monkeypatch, caplog): monkeypatch.setattr(certs.Cert, "has_expired", lambda self: True) ta = tlsconfig.TlsConfig() - with taddons.context(ta) as tctx: + with taddons.context(ta): ta.configure(["confdir"]) - await tctx.master.await_log( - "The mitmproxy certificate authority has expired", "warn" - ) + assert "The mitmproxy certificate authority has expired" in caplog.text diff --git a/test/mitmproxy/addons/test_upstream_auth.py b/test/mitmproxy/addons/test_upstream_auth.py index 67ec559277..883dabc2f3 100644 --- a/test/mitmproxy/addons/test_upstream_auth.py +++ b/test/mitmproxy/addons/test_upstream_auth.py @@ -1,10 +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(): @@ -41,10 +43,10 @@ def test_simple(): assert "proxy-authorization" not in f.request.headers assert "authorization" not in f.request.headers - tctx.configure(up, mode="upstream:127.0.0.1") + f.client_conn.proxy_mode = ProxyMode.parse("upstream:127.0.0.1") up.requestheaders(f) assert "proxy-authorization" in f.request.headers - tctx.configure(up, mode="reverse:127.0.0.1") + f.client_conn.proxy_mode = ProxyMode.parse("reverse:127.0.0.1") up.requestheaders(f) assert "authorization" in f.request.headers diff --git a/test/mitmproxy/addons/test_view.py b/test/mitmproxy/addons/test_view.py index be0a10022f..7e76a83eea 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): @@ -74,15 +74,13 @@ def test_order_generators_dns(): assert sz.generate(tf) == 0 -def test_order_generators_tcp(): +def order_generators_proto(tf, name): v = view.View() - tf = tflow.ttcpflow() - rs = view.OrderRequestStart(v) assert rs.generate(tf) == 946681200 rm = view.OrderRequestMethod(v) - assert rm.generate(tf) == "TCP" + assert rm.generate(tf) == name ru = view.OrderRequestURL(v) assert ru.generate(tf) == "address:22" @@ -91,6 +89,14 @@ def test_order_generators_tcp(): assert sz.generate(tf) == sum(len(m.content) for m in tf.messages) +def test_order_generators_tcp(): + order_generators_proto(tflow.ttcpflow(), "TCP") + + +def test_order_generators_udp(): + order_generators_proto(tflow.tudpflow(), "UDP") + + def test_simple(): v = view.View() f = tft(start=1) @@ -158,6 +164,21 @@ def test_simple_tcp(): assert list(v) == [f] +def test_simple_udp(): + v = view.View() + f = tflow.tudpflow() + assert v.store_count() == 0 + v.udp_start(f) + assert list(v) == [f] + + # These all just call update + v.udp_start(f) + v.udp_message(f) + v.udp_error(f) + v.udp_end(f) + assert list(v) == [f] + + def test_simple_dns(): v = view.View() f = tflow.tdnsflow(resp=True, err=True) @@ -228,24 +249,22 @@ def test_orders(): assert v.order_options() -async def test_load(tmpdir): +async def test_load(tmpdir, caplog): path = str(tmpdir.join("path")) v = view.View() - with taddons.context() as tctx: - tctx.master.addons.add(v) - tdump(path, [tflow.tflow(resp=True), tflow.tflow(resp=True)]) - v.load_file(path) - assert len(v) == 2 - v.load_file(path) - assert len(v) == 4 - try: - v.load_file("nonexistent_file_path") - except OSError: - assert False - with open(path, "wb") as f: - f.write(b"invalidflows") - v.load_file(path) - await tctx.master.await_log("Invalid data format.") + tdump(path, [tflow.tflow(resp=True), tflow.tflow(resp=True)]) + v.load_file(path) + assert len(v) == 2 + v.load_file(path) + assert len(v) == 4 + try: + v.load_file("nonexistent_file_path") + except OSError: + assert False + with open(path, "wb") as f: + f.write(b"invalidflows") + v.load_file(path) + assert "Invalid data format." in caplog.text def test_resolve(): @@ -284,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, "~") @@ -329,6 +348,12 @@ def test_movement(): v.focus_prev() assert v.focus.index == 0 + v.clear() + v.focus_next() + assert v.focus.index is None + v.focus_prev() + assert v.focus.index is None + def test_duplicate(): v = view.View() 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_auto.py b/test/mitmproxy/contentviews/test_auto.py index 459d839f0a..6911eddf2b 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(): @@ -47,7 +47,7 @@ def test_view_auto(): assert f[0] == "Unknown Image" f = v(b"\xFF" * 30) - assert f[0] == "Hex" + 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..89beda814c 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(): diff --git a/test/mitmproxy/contentviews/test_grpc.py b/test/mitmproxy/contentviews/test_grpc.py index 83de5000d9..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/" @@ -796,4 +796,5 @@ def test_render_priority(): assert v.render_priority(b"data", content_type="application/x-protobuffer") assert v.render_priority(b"data", content_type="application/grpc-proto") assert v.render_priority(b"data", content_type="application/grpc") + assert v.render_priority(b"data", content_type="application/prpc") assert not v.render_priority(b"data", content_type="text/plain") diff --git a/test/mitmproxy/contentviews/test_hex.py b/test/mitmproxy/contentviews/test_hex.py index 90db4bd7c1..1e7f9ff9c2 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 new file mode 100644 index 0000000000..0ffc9c1115 --- /dev/null +++ b/test/mitmproxy/contentviews/test_http3.py @@ -0,0 +1,58 @@ +import pytest + +from . import full_eval +from mitmproxy.contentviews import http3 +from mitmproxy.tcp import TCPMessage +from mitmproxy.test import tflow + + +@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.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", + ], +) +def test_view_http3_unidirectional(data): + v = full_eval(http3.ViewHttp3()) + 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]) + + +def test_render_priority(): + v = http3.ViewHttp3() + assert not v.render_priority(b"random stuff") 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 43565b2302..9711a64659 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(): @@ -17,6 +17,67 @@ def test_parse_json(): 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", "}")], + ] + assert list(json.format_json({"num": 4})) == [ + [("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", "}")], + ] + 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", "}")], + ] + 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", "}")], + ] def test_view_json(): diff --git a/test/mitmproxy/contentviews/test_mqtt.py b/test/mitmproxy/contentviews/test_mqtt.py new file mode 100644 index 0000000000..87cc09d40e --- /dev/null +++ b/test/mitmproxy/contentviews/test_mqtt.py @@ -0,0 +1,70 @@ +import pytest + +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"\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'", + id="SUBSCRIBE", + ), + pytest.param( + b"""\x32\x9a\x01\x00\x2dxxxx/yy/zzzzzz/56:6F:5E:6A:01:05/messages/out\x00\x04""" + b"""{"body":{"parameters":null},"header":{"from":"56:6F:5E:6A:01:05","messageId":"connected","type":"event"}}""", + """[PUBLISH] '{"body":{"parameters":null},"header":{"from":"56:6F:5E:6A:01:05",""" + """"messageId":"connected","type":"event"}}' to topic 'xxxx/yy/zzzzzz/56:6F:5E:6A:01:05/messages/out'""", + id="PUBLISH", + ), + pytest.param( + b"""\x10\xba\x01\x00\x04MQTT\x04\x06\x00\x1e\x00\x1156:6F:5E:6A:01:05\x00-""" + b"""xxxx/yy/zzzzzz/56:6F:5E:6A:01:05/messages/out""" + b"""\x00l{"body":{"parameters":null},"header":{"from":"56:6F:5E:6A:01:05","messageId":"disconnected","type":"event"}}""", + [ + "[CONNECT]", + "", + "Client Id: 56:6F:5E:6A:01:05", + "Will Topic: xxxx/yy/zzzzzz/56:6F:5E:6A:01:05/messages/out", + """Will Message: {"body":{"parameters":null},"header":{"from":"56:6F:5E:6A:01:05",""" + """"messageId":"disconnected","type":"event"}}""", + "User Name: None", + "Password: None", + ], + id="CONNECT", + ), + ], +) +def test_view_mqtt(data, expected_text): + """testing helper for single line messages""" + v = full_eval(mqtt.ViewMQTT()) + content_type, output = v(data) + assert content_type == "MQTT" + if isinstance(expected_text, list): + assert output == [[("text", text)] for text in expected_text] + else: + assert output == [[("text", expected_text)]] + + +@pytest.mark.parametrize("data", [b"\xC0\xFF\xFF\xFF\xFF"]) +def test_mqtt_malformed(data): + v = full_eval(mqtt.ViewMQTT()) + with pytest.raises(Exception): + v(data) + + +def test_render_priority(): + # missing: good MQTT heuristics. + assert mqtt.ViewMQTT().render_priority(b"") == 0 diff --git a/test/mitmproxy/contentviews/test_msgpack.py b/test/mitmproxy/contentviews/test_msgpack.py index 458dac810c..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): @@ -19,8 +18,111 @@ def test_parse_msgpack(): def test_format_msgpack(): assert list( - msgpack.format_msgpack({"data": ["str", 42, True, False, None, {}, []]}) - ) + 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": [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(1.2)) == [[("Token_Literal_Number", "1.2")]] + + assert list(msgpack.format_msgpack(True)) == [[("Token_Keyword_Constant", "True")]] + + 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..a748231d64 100644 --- a/test/mitmproxy/contentviews/test_multipart.py +++ b/test/mitmproxy/contentviews/test_multipart.py @@ -1,5 +1,5 @@ -from mitmproxy.contentviews import multipart from . import full_eval +from mitmproxy.contentviews import multipart def test_view_multipart(): 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..f5020c2793 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..e6005c0c8a 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(): diff --git a/test/mitmproxy/contentviews/test_wbxml.py b/test/mitmproxy/contentviews/test_wbxml.py index e37f0da21f..11f2886bf8 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/" 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/contentviews/test_xml_html_data/test-formatted.html b/test/mitmproxy/contentviews/test_xml_html_data/test-formatted.html index 0eb6000482..015043feaa 100644 --- a/test/mitmproxy/contentviews/test_xml_html_data/test-formatted.html +++ b/test/mitmproxy/contentviews/test_xml_html_data/test-formatted.html @@ -40,5 +40,6 @@

    esse cillum dolore

    eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

    + diff --git a/test/mitmproxy/contentviews/test_xml_html_data/test.html b/test/mitmproxy/contentviews/test_xml_html_data/test.html index e74ac31467..645bcaf944 100644 --- a/test/mitmproxy/contentviews/test_xml_html_data/test.html +++ b/test/mitmproxy/contentviews/test_xml_html_data/test.html @@ -9,6 +9,6 @@

    Ut enim ad minim

    veniam, quis nostrud

    exercitation

    ullamco laboris

    nisi ut aliquip ex ea

    commodo consequat.

    Duis aute irure

    dolor in reprehenderit

    in voluptate velit

    esse cillum dolore

    eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

    - + - \ No newline at end of file + diff --git a/test/mitmproxy/coretypes/test_basethread.py b/test/mitmproxy/coretypes/test_basethread.py deleted file mode 100644 index 59e28bb0e0..0000000000 --- a/test/mitmproxy/coretypes/test_basethread.py +++ /dev/null @@ -1,7 +0,0 @@ -import re -from mitmproxy.coretypes import basethread - - -def test_basethread(): - t = basethread.BaseThread("foobar") - assert re.match(r"foobar - age: \d+s", t._threadinfo()) 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 0acb97e00b..43debfe9bd 100644 --- a/test/mitmproxy/data/addonscripts/addon.py +++ b/test/mitmproxy/data/addonscripts/addon.py @@ -1,4 +1,4 @@ -from mitmproxy import ctx +import logging event_log = [] @@ -9,7 +9,7 @@ def event_log(self): return event_log def load(self, opts): - ctx.log.info("addon running") + logging.info("addon running") event_log.append("addonload") def configure(self, updated): @@ -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/error.py b/test/mitmproxy/data/addonscripts/error.py index 8dcd404d8b..aaa86084b5 100644 --- a/test/mitmproxy/data/addonscripts/error.py +++ b/test/mitmproxy/data/addonscripts/error.py @@ -1,8 +1,8 @@ -from mitmproxy import ctx +import logging def load(loader): - ctx.log.info("error load") + logging.info("error load") def request(flow): 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/addonscripts/recorder/error.py b/test/mitmproxy/data/addonscripts/recorder/error.py deleted file mode 100644 index 2e7e648a45..0000000000 --- a/test/mitmproxy/data/addonscripts/recorder/error.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -This file is intended to have syntax errors for test purposes -""" - -impotr recorder # Intended Syntax Error - -addons = [recorder.Recorder("e")] diff --git a/test/mitmproxy/data/addonscripts/recorder/recorder.py b/test/mitmproxy/data/addonscripts/recorder/recorder.py index 3af4e5303e..2a01b8cfdf 100644 --- a/test/mitmproxy/data/addonscripts/recorder/recorder.py +++ b/test/mitmproxy/data/addonscripts/recorder/recorder.py @@ -1,4 +1,5 @@ -from mitmproxy import ctx +import logging + from mitmproxy import hooks @@ -9,14 +10,13 @@ def __init__(self, name="recorder"): self.name = name def __getattr__(self, attr): - if attr in hooks.all_hooks: + if attr in hooks.all_hooks and attr != "add_log": def prox(*args, **kwargs): lg = (self.name, attr, args, kwargs) - if attr != "add_log": - ctx.log.info(str(lg)) - self.call_log.append(lg) - ctx.log.debug(f"{self.name} {attr}") + logging.info(str(lg)) + self.call_log.append(lg) + logging.debug(f"{self.name} {attr}") return prox raise AttributeError diff --git a/test/mitmproxy/data/addonscripts/stream_modify.py b/test/mitmproxy/data/addonscripts/stream_modify.py index 7941320542..5a87897ef8 100644 --- a/test/mitmproxy/data/addonscripts/stream_modify.py +++ b/test/mitmproxy/data/addonscripts/stream_modify.py @@ -1,4 +1,4 @@ -from mitmproxy import ctx +import logging def modify(chunks): @@ -7,7 +7,7 @@ def modify(chunks): def running(): - ctx.log.info("stream_modify running") + logging.info("stream_modify running") def responseheaders(flow): diff --git a/test/mitmproxy/data/confdir/mitmproxy-magisk-module.zip b/test/mitmproxy/data/confdir/mitmproxy-magisk-module.zip new file mode 100644 index 0000000000..6a140d0b86 Binary files /dev/null and b/test/mitmproxy/data/confdir/mitmproxy-magisk-module.zip differ 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/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/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..f41ee9575e --- /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": null, + "ssl": null, + "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" + } + ] + } +} \ No newline at end of file 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..5b4c48fc6f --- /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": null, + "ssl": null, + "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": null, + "ssl": null, + "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": null, + "ssl": null, + "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": null, + "ssl": null, + "send": 0.0, + "receive": 0, + "wait": 0 + } + } + ] + } +} \ No newline at end of file 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..d38a849a9a --- /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": 1680136769.482 + }, + "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": 1680136769.482, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "text/html; charset=utf-8" + ], + [ + "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": 1680136769.482 + } + } +] \ 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/QEFCQ0RFRkdISUpLTE1O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", + "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..d7fdc4035d --- /dev/null +++ b/test/mitmproxy/data/har_files/chrome.json @@ -0,0 +1,584 @@ +[ + { + "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": 1680134468.9160001 + }, + "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": 1680134468.9160001, + "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" + ], + [ + "content-length", + "23866" + ] + ], + "contentLength": 23866, + "contentHash": "7fd5f643a86976f5711df86ae2d5f9f8137a47c705dee31ccc550215564a5364", + "timestamp_start": 1680134454.25, + "timestamp_end": 1680134468.9160001 + } + }, + { + "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": 1689251556.795 + }, + "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": 1689251556.795, + "pretty_host": "www.google.com" + }, + "response": { + "http_version": "HTTP/1.1", + "status_code": 0, + "reason": "", + "headers": [ + [ + "content-length", + "0" + ] + ], + "contentLength": 0, + "contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "timestamp_start": 1689251552.676, + "timestamp_end": 1689251556.795 + } + }, + { + "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": 1690289984.3869998 + }, + "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": 1690289984.3869998, + "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": 1690289984.3869998 + } + } +] \ 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/yla8P8pWvL/KVrx/yla8f8pWvL/KVrw/xElY/8RWJH/DUyA/4OCgf//////9/f3/4GBgf9aWlr/XFxc/1xcXP9cXFz/XFxc/1xcXP9cXFz/XFxc/1xcXP9cXFz/XFxc/1xcXP9cXFz/Wlpa/3p6ev/z8/P//////46Ojv8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/19fX////////f39/1paWv8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/x8fH//c3Nz//////8LAv/8WMkn/F3fH/wklRP8aOZr/KVrw/ypc9f8qXPX/KVrw/xo4mv8JJUT/F3fH/xcySf/CwcD//////9zc3P8eHh7/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP81NTX/8PDw//////+Ghob/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP9CQkL/9vb2//////9+fn7/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8DAwP/np6e///////4+Pj/X2Fi/wxJff8WcLv/CSVE/xElYv8aOJr/Gjia/xElYv8JJUT/F3C7/wxJfP9hYmP/+Pj4//////+dnZ3/AwMD/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/RERE//f39///////dXV1/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/JCQk/+Pj4///////qamp/wMDA/8AAAD/AAAA/wAAAP8AAAD/AAAA/wcHB/88PDz/YF9f/0BAQP8JCQn/AAAA/wAAAP8AAAD/AAAA/z8/P//s7Oz//////93c3P9ARUn/DEl9/xd3x/8RV5H/DT1n/w09Z/8RWJH/F3fH/wxJfP9ARUn/3d3c///////r6+v/PT09/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/11dXf/+/v7//v7+/11dXf8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wwMDP/BwcH//////9XV1f8YGBj/AAAA/wAAAP8AAAD/AAAA/woKCv+EhIT/qqqr/4WKj/+nqKj/jY2M/w4ODv8AAAD/AAAA/wAAAP8DAwP/hYWF//39/f//////3dzc/2BhYv8VMEf/DUyA/xBfov8QX6L/DUyB/xUxSP9gYWL/3dzc///////9/f3/g4OD/wMDA/8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP+AgID///////X19f9AQED/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/jIyM///////19fX/RUVF/wAAAP8AAAD/AAAA/wAAAP9TU1P/o6Sm/x5Re/8Tbrv/GlKA/5ueoP9fX1//AAAA/wAAAP8AAAD/AAAA/xAQEP+dnZ3//f39///////39/f/wsG//4KBgP9iY2T/YmNk/4OBgf/CwcD/9/f3///////8/Pz/np6e/xAQEP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8EBAT/rKys///////h4eH/IiIi/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/0xMTf/4+Pj//////4uLi/8AAAD/AAAA/wAAAP8AAAD/hIOC/2Nyfv8WftX/H5j+/xeD3v9Xanr/j46N/wAAAP8AAAD/AAAA/wAAAP8AAAD/EBAR/4WFhv/u7u7////////////////////////////////////////////t7e3/hYWF/xAQEP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8VFRX/eXl5/4iIiP+Hh4f/h4eH/4eHh/+Hh4f/h4eH/4eHh/+Ghob/jo6O/+bm5v//////vr6+/woKCv8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8YGBj/0dHR///////T09P/Ghoa/wAAAP8AAAD/AAAA/3Nycv98hIr/EWat/xuR9P8Ra7f/b3qD/39+fv8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8EBAT/Pz8//5+fn//d3d3/9vb2//39/f/9/f3/9vb2/93d3f+fn5//Pj4+/wQEBP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/dnZ2//39/f///////////////////////////////////////////////////////////4eHh/8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/4iIiP///////Pz8/2JiYv8AAAD/AAAA/wAAAP8mJib/srKx/2Vzf/8yV3b/XW17/8nJyP85OTn/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8EBAT/Hh4e/0FBQf9WVlb/VlZW/0FBQf8eHh7/BAQE/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/NjY2/+Hh4f//////+fn5//Dw8P/w8PD/8PDw//Dw8P/w8PD/8PDw//Dw8P/w8PD/8PDw/+np6f9GRkb/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP80NDT/6enp///////CwsL/EhIS/wAAAP8AAAD/AAAA/zIyMv+SkpH/yMbE//n49//6+vr/eXl5/wMDA/8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/HBwc/76+vv//////+/v7/4ODg/8xMTH/MzMz/zMzM/8zMzP/MzMz/zMzM/8zMzP/MzMz/zQ0NP8wMDD/CgoK/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/BAQE/5ycnP//////+/v7/2ZmZv8AAAD/AAAA/wAAAP8AAAD/AQEB/0NDQ//l5eX///////Hx8f9oaGj/AwMD/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/FxcX/6mpqf///////////6+vr/8RERH/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP83Nzf/5+fo///////U1NT/JCQk/wAAAP8AAAD/AAAA/wAAAP8AAAD/YWFh//Hx8f//////8PDw/3V1df8KCgr/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/JCQk/7Gxsf/+/v7//////8vLy/8nJyf/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AgIC/4iIiP///////////5mZmf8ICAj/AAAA/wAAAP8AAAD/AAAA/wQEBP9wcHD/8vLy///////4+Pj/n5+f/yYmJv8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8GBgb/Tk5O/87Ozv///////////9DQ0P80NDT/AAAA/wAAAP8AAAD/AAAA/wICAv8GBgb/AQEB/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8dHR3/ysrK///////29vb/ZmZm/wAAAP8AAAD/AAAA/wAAAP8AAAD/BAQE/2VlZf/o6Oj////////////Z2dn/c3Nz/x4eHv8BAQH/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wcHB/84ODj/np6e//Hx8f///////////8HBwf8wMDD/AAAA/wAAAP8AAAD/AAAA/y8vL/+Mi4v/pKKg/4iHhv8pKSn/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/0tLS//s7Oz//////+bm5v9HR0f/AAAA/wAAAP8AAAD/AAAA/wAAAP8BAQH/Q0ND/8bGxv/9/f3///////39/f/Z2dn/kJCQ/01NTf8jIyP/DQ0N/wUFBf8CAgL/AgIC/wcHB/8UFBT/MDAw/2VlZf+urq7/7Ozs////////////9PT0/5iYmP8cHBz/AAAA/wAAAP8AAAD/AAAA/ykpKf+ysrH/aneB/z1cdf9xfIX/r66u/yEhIf8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8CAgL/d3d3//n5+f//////2dnZ/zs7O/8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8ZGRn/gICA/+Hh4f/////////////////4+Pj/4uLi/8fHx/+xsbH/p6en/6mpqf+4uLj/0dHR/+zs7P/9/f3////////////6+vr/w8PD/1NTU/8HBwf/AAAA/wAAAP8AAAD/AAAA/wAAAP98e3v/dn+H/xFmrv8aju//EWGl/4KIjv9vbm7/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wsLC/+UlJT//f39///////W1tb/QEBA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wICAv8pKSn/f39//87Ozv/29vb//////////////////////////////////////////////////v7+/+vr6/+1tbX/XV1d/xMTE/8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/j46N/1Zpef8XheD/H5j+/xZ/1/9jcX7/hIOC/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/EhIS/6CgoP/+/v7//////+Dg4P9VVVX/AwMD/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wEBAf8VFRX/RkZG/319ff+qqqr/yMjI/9jY2P/f39//3d3d/9PT0/++vr7/mpqa/2hoaP8wMDD/CQkJ/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/CAgI/5ybm/+YnJ//FlOG/xR0xf8ZUoH/naCi/1hYWP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8TExP/nJyc//v7+///////7+/v/4CAgP8TExP/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/BAQE/w4ODv8YGBj/HR0d/xwcHP8VFRX/CQkJ/wEBAf8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/HR0d/5aWlv/39/f/9/f3/6mqq/95gYf/pKWm/46Ojv8NDQ3/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/w4ODv+Ghob/9PT0///////8/Pz/vLy8/0NDQ/8FBQX/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8JCQn/U1NT/8vLy//+/v7//////+vr6/+Hh4f/a2pq/0lJSP8LCwv/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP4AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/BgYG/19fX//e3t7////////////u7u7/mZmZ/zQ0NP8FBQX/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wgICP9BQUH/qqqq//T09P///////////8/Pz/9KSkr/AgIC/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP4AAADyAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/Ly8v/6mpqf/39/f////////////p6en/o6Oj/05OTv8WFhb/AQEB/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8DAwP/HBwc/1lZWv+wsLD/8PDw////////////8fHx/5eXl/8hISH/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAADyAAAA0wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8LCwv/WVlZ/8bGxv/6+vr////////////39/f/0tLS/5eXl/9dXV3/MjIy/xgYGP8LCwv/BAQE/wEBAf8AAAD/AQEB/wQEBP8MDAz/HBwc/zk5Of9mZmb/oKCg/9nZ2f/6+vr////////////39/f/uLi4/0lJSf8GBgb/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA0gAAAJQAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8UFBT/YmJi/76+vv/09PT//////////////////Pz8/+7u7v/Y2Nj/wsLC/6+vr/+jo6P/n5+f/6SkpP+xsbH/xcXF/9zc3P/x8fH//v7+/////////////////+/v7/+ysrL/U1NT/w4ODv8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAJQAAABBAAAA6wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8NDg7/Q0ND/46Ojv/Nzc3/8fHx//7+/v////////////////////////////////////////////////////////////39/f/t7e3/xcXF/4ODg/85OTn/CQkJ/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAOsAAABBAAAABwAAAJ8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8BAQH/ExMT/zk5Of9oaGj/k5OT/7W1tf/MzMz/2tra/+Li4v/j4+P/4eHh/9jY2P/Jycn/sbGx/42Njf9gYGD/MjIy/w4ODv8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAACeAAAABwAAAAAAAAAtAAAA2QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8GBgb/EBAQ/xoaGv8gICD/IiIi/x8fH/8YGBj/Dg4O/wUFBf8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAADZAAAALAAAAAAAAAAAAAAAAAAAAE8AAADnAAAA/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/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAADnAAAATwAAAAAAAAAAAAAAAAAAAAAAAAABAAAATwAAANoAAAD/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/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..849cb79edc --- /dev/null +++ b/test/mitmproxy/data/har_files/firefox.json @@ -0,0 +1,2149 @@ +[ + { + "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": 1680134362.303 + }, + "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": 1680134362.303, + "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==" + ], + [ + "content-type", + "text/plain; charset=utf-8" + ], + [ + "content-length", + "23866" + ] + ], + "contentLength": 23866, + "contentHash": "7fd5f643a86976f5711df86ae2d5f9f8137a47c705dee31ccc550215564a5364", + "timestamp_start": 1680134339.303, + "timestamp_end": 1680134362.303 + } + }, + { + "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": [ + [ + "content-length", + "0" + ] + ], + "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": [ + [ + "content-length", + "0" + ] + ], + "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": [ + [ + "content-length", + "0" + ] + ], + "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": [ + [ + "content-length", + "0" + ] + ], + "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": [ + [ + "content-length", + "0" + ] + ], + "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==" + ], + [ + "content-length", + "3969" + ] + ], + "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==" + ], + [ + "content-length", + "3346" + ] + ], + "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==" + ], + [ + "content-length", + "794" + ] + ], + "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": 1680134345.527 + }, + "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": 1680134345.527, + "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==" + ], + [ + "content-length", + "9689" + ] + ], + "contentLength": 9689, + "contentHash": "a4dcfad01ab92fbd09cad3477fb26184fbb26f164d1302ee79489519b280e22a", + "timestamp_start": 1680134339.527, + "timestamp_end": 1680134345.527 + } + }, + { + "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": 1680134346.528 + }, + "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": 1680134346.528, + "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==" + ], + [ + "content-length", + "9689" + ] + ], + "contentLength": 9689, + "contentHash": "a4dcfad01ab92fbd09cad3477fb26184fbb26f164d1302ee79489519b280e22a", + "timestamp_start": 1680134339.528, + "timestamp_end": 1680134346.528 + } + }, + { + "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": 1680134586.543 + }, + "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": 1680134586.543, + "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": 1680134586.543 + } + }, + { + "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": 1680134346.639 + }, + "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": 1680134346.639, + "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==" + ], + [ + "content-length", + "6986" + ] + ], + "contentLength": 6986, + "contentHash": "ebb5ca702c6b7f09fe1c10e8992602bad67989e25151f0cb6928ea51299bf4e8", + "timestamp_start": 1680134339.639, + "timestamp_end": 1680134346.639 + } + }, + { + "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/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..d2f39b93f6 --- /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": 1680151229.383 + }, + "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": 1680151229.383, + "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": 1680151229.383 + } + } +] \ 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..0529a2438e --- /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": 1689428415.889 + }, + "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": 1689428415.889, + "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": 1689428415.889 + } + } +] \ 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": "iVBORw0KGgoAAAANSUhEUgAAAKAAAACgCAMAAAC8EZcfAAAC/VBMVEUAAACRy/CM2vLK4/M8d9xGmOKjzOpGrOdFf9q73PFHouVnlNucz+1Cuusytus+eNwvdNdCfN1Xjdys0e9Dfdo6ddtUhdlnx+pjveqAquJJvOpOu+dbwulkktx/teWkv+d3zOuKzuyO2PCJqeTl6+2QrePV4/NTw+xlxukYq+4Xp+0XY+AZtu8XZuEXauIZse0XoesXsO8XnuoYre4Ys+8YbuP4+fwXpOwWZd8SW9sXm+oWlukVeeP6/P4Wh+YYfOUXduT09/sXgub9/v4UY94RV9kVYN8Uad8SXtzX5Pfw9foWmeoYkugWkOcPU9cVgOMWceIOFB3T4fa9zu/s8/sPT9bJ2PQNS9QYcuQTdOEmNUDe6vcXjOgMERfl7/kiL0ARGCPa6PgfKzzC0fEpOUXO3vbN2vQUHCcao+wlM0Xh7PkTbt8xQlfp8PkRZdzF1PMVjOVgfI5zkaMtPVEcJzcaIzNAVGxvjJ65yu13lqex4/RTa4EiduI8TmYXICyAnq/o8/spN0vd5vZ8mqtKYXbS3vOEorI4SmHX4fVbc4lFWnIsPkm1xusIDBH1+/1UcoOqvuc1RlxqhZhAve88feBOZnzv9/6wwedmgJMamOeIprVZeoiqvOGcr9iPq7ujuOSMp7jE3PYiq+1pi5lPa3c2SVVihJMxRE6lt9ySrr08j+QkMjtCWmYcKC+Oockwqu6cst/A0t1HYW4hLjWr0PGYqc+Wsb+LpNTC0OiUqtavw+sdatyatcI+VF87luUPSs/I2eKAnc8mb96iye82s+9Bq+q3zNY7T1vR7Pgtouy6yuXJ1uyRrOCrws50k8WEmcEpmecptexSk+IPIUPL5vc+oee80/IpfeOhu8cwdtxvgaaUvOoZMFuf2/O0w9xlxu4xheZujLx6p+YjP3F/kK97zu8mjeV3iK4xTX99telhnOVZiNumtM5heqfb8PlSv+yLmrfC5vVdreaZp8CO0O8tYs9mdptDW4g2feAeWNFGVGcfgt1ulN1UaZY6atR3pdr6AAAAKXRSTlMAbFsUztY91v0n1bFO6PHR9tbQXubKvofRZs60oo7OlHWgYYRhvrXStriTLScAACalSURBVHjavJPBbqMwEIa7QYqEBFxTpTmkubRUkUYWaG8cVnmrnHgCX5DyADwLh1wt9Wr5xDUoUh5gh3EdU9aBZpv0xzVD7X/m87h9GJWH8qfBNAgmk9Vq8n21OQJM6PuY+eFb8oLZ4iUMo6gSqPUthfmqKArDl6fZyv8vOH+5CAWsf0KimM8C77rWLZ+iKzuWXouVfnJE85n/dbxZSPYUH8qU6slNkZolWuwanFgdA00WV7xOv3a3z1EntyYd7FKfpGdwOdwJUvEYjPMFIR0+dWrtjknjhvX4vmgxdrsL0ffCGsCEOrCxW5cMfYdzbT54z9M5uOx3kps4Wl7+h56E1ok/kDIAUShVn05Syu0thflOda2aqi2mHxoYiIvXvBSM9gCSYQSVkvtdlm3uqYwftnUBDBjTgIDRo3eBr10FEu5UxxzZeH7EoyrVNE1xQ2E6vJhabg/8NxbZn4SpTG8nYSDaU2DvqM31Icvybd0IuIcYiZBEgffEs50suoiv/xJOQwbMGNWe82NdaQvrzECbesWcsi56O3eDnkA0Ms9y6qK2MDHr83lzey4heXZUpS2ifVa2FgGPyeZwSS8Wcpe9NwDmd9GkB7jApQSoZLHPcgX2NORwNccoGRbu1vPQAQCK92xXn6tB6PX+ABOzpvLNtmIJG1BioSiI4zgx7/hDLlIcFwWsrDmXiEFK4KnL54cJAKMUasdlqT/Yx8QoubNdyIFMAyLsz5S28aYEFQZI1GEjy0R/MfGrA/hcgrZDkfMTxaOK8enivb21E87tIHUxcWjagXwQNwden6t3LtmLzPHEPpM9PugRmQ+CwtE+Qxpsal/NH96YuFydAf8yWj4hSARRGC+CIqguHfpLf455ioKExIN4svQi3gRji8CkBTtVRrAFgSdvFQt1aE/dtkCoLoFgYQVisHYT8WDgxXDz1KnvvXmz07pSfbuOuzOj8/N789566IxsIG+SbXlyw61R/C7FR5wsrVoIbZwxSSmIid5pZ9nX15GFu/bTdLww7PQ1WfLTCUINltY6Sy9uoq5/WBkXAtPKT+id7dKl5ogn4wjwVC7/Cw7LM5bWubPnzvE7JF1xSrUn/ip/2YkYdCIf0x3zbOD9BYtOXiZmnELTukgvNCJDGYv3VsokxIHd6iF3QO69AeD/8llG01JwBuzixcsxue7lyyAVzmS8GdRExMgfZH9oIPXAO6WBp/kVXyYhUx5OD4Oel/JSqbpSZBxgXNFIyXUzquuidrIOCaBHOoMjRWfSQsGAjkqEyRlQrLLzlAkm94EI+gr14/IjfVIah4t1dxUMqtDSGqxa60U4VkO+qL8hfKsHnTercSr1a5f6uKLzAOfxfgndV8fpx+sH3rbFFZKwunAqk3FH4SRwKpWKzXJsur5EsrqLMc+QaCeyhoCkMYV+kp9LHnoH6THnC8MUNVC4tirNgJKsoLuMlYvFUbheNWy7VmuwahBTCmPODtbhqJjJYEcSpCRNKimzMraa7qBNuE9mM3gcUK6S7lFGZKDSeB4QW5XEdA5EcMIHQihYj0uYrWyMis8WNmWQn31Sl14qNKf1lEH+hzE9IcoJg+cSnRt+cwBnWRbT2SBjJNElHNSyWuEow4xR8ZGkMRJAaJntS8ceAB7XBE86vpolgOa5xZJKfJHwXOCVwpUNOoVXA14EdyES3wpuEBa1jZC4qBbZBJzAKiXKksMy7medukyjHvksTvxUU46p2oEOeIFdtQqFAuPZFFAhS0qoc8GHIm9G46I2YANwjs0mULvxIBacD9lVWsoUceEU1REOomM8zoxSc9xtAI/4gEeRNcZl/1QcstJ9zyYaRPr5sgyt6IGEs2RN3AR4UAChH/lJSgOK4B3Y0gwoicvuFRfVhlUoA8+qwj3gbaeLKDsacbkoItCIQixdDGCEIn+N6jt37D0gw4TNMoAqL2KZW4JgH/DKZdiHtDV4Bi0fl7aStiJMRKBBKIhQXS0GQFn5Q7ZVF4KTKIN1JQReXXgetfTrNvAuc3SbC6taaDMfbb4N9wzUhdwTynHaopUcDWoTw6ZOFpIQ8k6vC0s/twID95zecdDHGGmdnzIzACVr43yMV2p+79YK5evXyb6Gjq5xT6HZ7c8v3wyHs9lsqDQb9l7dKVSUiZV1k0w0ZZEZufD4DHDWdwa+AJ7YcfATdW0CYn6kqC4Drzl60Whff0R8khwb7nUab58PZ73nT7+0q8htLtuXKnb10duXvWHvWdmhdOp+L5WIUJecCJIAETnfWfrMlD5LgBihA8VHGed5EZ75KyV848CCfeCrVjf5YN2TR896vXef27WHH3/evfX4/s0HN65cvXrlwc1f927/fF2rPALku2tWJRe8ZxM1YRwQBEvHl67DO3YyIPSbMfv7abMK4wCuVyZeee2f4QUmQ5rArlpdmkLThLXUVqCLzKYF3QCJwMo6kVlmSMYAgRQH8ptRQCgGyBoyNmgohA3EQYgV0405Ftwulowbv89zzuFQ4cJHNhcweT9+n/M8B94BSF+kBKVONJd8Yrf4/S8fBYqFryRteomXEbgVi/XFjZFXLW8+r7YUoCwolwV/RFmqv6w6iNiDHYnEaK63a91/jk5i65GQlWgsE14035MhAYjwJPAefic/63V6anrhe+Au9niEr/a4D63Fk6c9JZGyb6qJ46oeHh8fPF9UdB1VdH5wfPiCpSBUcOnyQdC5k0j2Gf+Z8zuozeryE0qZ23+Bor5WwMU79B9rX6Zsr+FlibG4y8b95fHVvOzCRCJqjZR+CgRsg+ev9/b29Ny/Pzs7OyRrtuf6eLUlFKq+ueP1JJLt1OVzcpxVjB/ckd1+0ciW04EfElDz+OoQPrfRY7MVB+V8ACh9GfFEzON8/s0F0o2fL7pOOJKNjY1d7dY1Nts76AKxYad2KZmYQIYQqhBZyEBAHxwB33/rbYnWCf4kgXp64TMY1u318HmKjem3x9ms4FTMYz/4oiBkqR4flDrGEeoG6kdZN27AWHQhFHp4M+ruSP6Z8h/tG0nEkwUwSwHfAVD2E0BxGChBFR/nx765LqPNRw0mn2owpsPbF9sJvOqxFFhkeOir1jGstPRnWaU/3ugeKnKFCrb/CkYTfx8a0GY6iGeoGMgCAoqTKYEfaCAKQNap5cy+1ITb5vPZ1IDQNmNfMDFdEmmw6PTAg0/pShl3WxeM3bPD6PPrqDv2nsEvhK1SyNFoIIqAqNOBIj4+f+Ytt0f4EGAtA6m92e1JT6AJZ+8CDwbGIo33s8KVU5XRBxl/7O7Fedj+5UXHe34p/Pj/AiED8MwJ30t7sa+QgG7agBwgfIGVUe/zK6GQaxjd7YWP47vKPBUdVKjKSnyg6M/lIM7if+rhE/v3aLJDCJmIJ58K5G38NT5JXz+zuMg8dfmSbyAXPt1gAcyKr0QDLRbEB5+eDZGe4hEOVVdXU3MNVVNXR8rbLWPDEO67Jw4N8hzieQxkAoBkEkD6BKqVgSgAScf5ifkwmOeNto1Cn4cb7FUrJhoL3n0TOt2n01O6ts7+ycnJ/s62GiKWt3QPhywPH9u3zAY5yyC2yt4RkP/NQEgVUHR2cRE66XP4wTOv2T2+wkI+gdxgvoOXEvadL0Mhi/D1ah+NBvsUrw68/pnNZdTm5kxn2zXEWNbUXY1zuOJcYyETW08CzwDIE5QOhE7lR8DDrqAvb0MGKIHwOZe/kr6iE77b0sc88oG3sBBZWFhY3pzs5E7f7naFXNu/PEoZ5CxDeKdVWB6chYUrHfgx1yqA4MHnd/iRn/ml25a3wQHKHcg+7w5Wrgs+uV7Yp8YDPO2rgY94kchd/FpYRog1aHT5G0vo4W58z6yFd1pZwEA6hBqITypg5uqi4HF/c8zmP7qKNxCgje6QEj6C8MXszzGIFvJxgOnzAR8B1XB0zkgflRBeI2EPLpXX9XMQyi4v4mgpoE6QfBqYCaDw+YXvoz2jLy9vQwQob+FAIhj5jPpbDaC4PthXlXYAy1SAnQgQvmPCMAtLB9HklfnDIyGiYSCdwY8VkFJVQPAISDzlwwn0wOc73mFvIhq8D5+LfHy/8f7TC6aJgSpB3WFRkeXN/nAbZqWym5ocXTPnkJCBKAFklExQAzMlEO110HpBfjJAn0cu6dpak3X6F28VfDiABNQB3tBA7nA6UAupyZxhWQ8meX/eDCFvbEQDgARyEZBzY6CY3dVV8jngI+BhV1deBXe4XgKt0aT3APtZ+mSHEWCVvN84QQmsAZBbzEIN7BfClmpE2DdgNiMOh5+BKAJmamAmbz0A14lHQD9vF84v/09jYUUFz7CakdykJ1iNAcEB1CMypoCcYPqOvhaeSY8Qu4Z3NoRvKMKtj0SG/kXehxrYKoAoAYQOxw9AAx0/1OHh70sjyakK6rDags6Rad1gNSK6w/oMaiD3WAsjAM5M9rPw9iVEuHRIQgBXAUQxEIXcjgEnGtcz+R3z+jp+Sw3sbc1PTGxsBO9GVmIYERxBAtbW5iUDyy4V4Lju8ClAlAB2TqoIycfAGQDDuPdmC1zbya29Z6kUnnqPX2+3fnIEpAS562gugGK5pLDbD5+9wLfyJiv+MVmd+3kAGt28ZUqSfc5v4XPRikk7gqrFpQJYfixB3HQqQ/DgA3BGNLmlumD7aTBeO5/CQVzFpCBEAHHcuNKBPL0ENL+0ZuGNSoYpm99bRVYQoJETdOYl3c8tIfalASFMAyJC5YMwTEIQqXAjKyAirOxFj9sLrdldhwAaHCxMB8qaaF5HfBjeVMo8YBLvCuTPHdn7QQbiPSUCtDfgBErfEXCI1rTusQDWcXr0Kxzun5zZhBG1ST6UmJO6Kotrdz/uzc6YN+es8ix/cu4BwqI6AYSPgRv0xiyLIszg+isalAl69t10xx0FKIFpPVaLsLIG0XUiJhZ2Tk7OzCicAoav1ZVfsmw/jQcyMprXzDhc/D32IwZm4uPdI+A5AP20/XJSqWfZzrgvgJd6WQASdKfPKIGjI87vCvgOSQMOKeB33OOmJggJGO7kWSAgqP2TqCOeANZU9lgwx0EEUnyY4mXj8DMQ1aqB5wSQ1ktOaqP51q+3cp36tVBgxI7XlQC6k9H4FYxIOlBMCQ5hFYD8c5IEtgEIoYwQwmNEBazrBnCkvjHrbONaihe2w6GAnCDrJJDXX85cdnx09Ht3wAqf6SyVKenkBEt8+/awS3dYAFWENwFkYUtLC/WYE+wkoBL2Q0jF+aldXXoJh9CIB2VtpbANKcNHzQByMRA8rJcJ65y4fnP2TNNTUza7O4CfOwQw67EEdqw4q2SHxxVQ9VgKAYQQTS4vq6shFmLiOdFCyZPXXXmRZfd1rhePMQ7g6RKI0CSQeEjVP1ELIN++E4HY1KjRHqjHm1FvI/f4SYBb7E60269wgOTTQBI2AHiTIlRCMcZtiI9LNVkaqfUCWHafpsSekZXR+EzceQxESSAHKIA5dP3+5onGYj/QW/wSvFO2ijEO1hOweD8Ype9T04G6xyzUTS4joighDCthv/Lh55OyMcv27k4wG5tjTwAN/zSvYx+iBNBBAR4B8/+w98VieQQMYEvb+a9nDuJGAkb37a8sBBQdZiAfQrmrCaiEmBN1m6j7hIVkVD4ECGAVgH1xK56zlc9CBjpOAJ1zHGD+QO1ULGYjoNFkaqz3oqwHUdoz9vak83EBtZhHhIC6x2jyVSUEUAlBJKMWqgojP/JVlpVe2N59XljirTVNHAeyEECHKMO8Aq45E7GpXADr603WbI8d5Qwv8SKcXimpEksGPAbKCFWT5SSTkIkkLFMhQiiGJt1X1nIJeyYvF4/ZyP+IhV1WAjoQXTrQTB3OX3MDaKQEcwH05aICBESPR6ZwzxVQgtxfBnKE/FrmcoMQAkhdlkJF1AeRSvsA/Ozh7sqSDX+x0XWRI8zpqgUQxQn6hc8wXzIHHwHrEwkJtHqz82yo3IOlegCNIx2BywIIGpcSfgHh5csNDdRkJpamCSVR7BvFIx+A5U2fAngLDyr2AQghgHMittOAF9dykwro9WZXFKI8K1HkSED3CSB6LI/hN5eHIJSjzBlqYqUOkUrz8NWmIgJWFG74NjRQt9jPPgaSD0AjWqwSNH1fgbI9iStgQ4imGMAi1H+FQ0Oqy9RmuQ/5JOofQmHjIh58qJYrOIPtv1aAKIBmAA3EOg148SKfwXoF/OFXlO1xMFgM4FSH+2qBAlLpJotRpi5DSETdZhWiSpHC07ymppai7acjfXhQxcTF/HwFxGJRQPAc/xJud69t1WEcwL0Rdin6d3ipWKimna1teqE1J0ZnNGl6loz0JUHS1iNLiwmGdluYpxcuNzkEQTE4DMeXkDcVWi+UNBpwstKuFJ2FlrWFQrF1xYrf5zm/019Coz4ro81g57Pv83s7OdlzF78b+9QCXhhbWlpKEXBhaHIop6M8+4EgASsV76kEtgpBnKAMpfCqDLGN+M4WcNCBRzr43gpv7BRNXCj9HQG7GQiTnSB8KAJ2M/DPscrSUtRaqCffNXModd8bCqLH+VJy/Qx4QwrDYr1ePCdsJ3IJHPPYd82/sbNXwYXS9zoCoRNA+ABcC+KBlkZAr9s91jBR6d+SBFzQC8nmiAVEZO0RslBkiKnSThRGKtgkDvXWW1dHN3bypQYuc18CQRJAtJfvMm3gwLKGvTjrxUKNx5kLDapswRlCjxe0JWd+jlZqAqIE0C+Xw8V5CFtClEQYZQkd+659NPX7zmYBl0k/aANC9sajHYC1ezjNVJwAYo9TSqhGMx9kYHTJFfXzVvLl5RuxmBASkUMUQtFmEKURSChlQQYb1bWr700D2MR1tJ/HMY0htIFvMBDVDnwwaTQqUZwH8Tg1t4SN2dwvhwjoChWizlkMQgDDsdhEDECRIcrOEEQSSiK3mpHMPOIvoYPvg5M7AP60tFS69+c4RehgIBcDoXvuIgPhI+CfPpeZywSSk0NDYxVKMHca8jDQZeiBI+s48/qNiQkIQSTgbRby8XqChCCyUSC52CigZwXfe9/c2Tne/KlUMu/HOUEJ5ATZ1wp8qvaVu57OhMZw0Eo1qHLrQY+Hp3G64ixOM9B/Y2KRhByh//Zt/20Iw7zroc2CKI1SKdKUvvc+vrOzv7nUaGgXWoAX/z3B8drPbp8nmoLPreSomoaHgLj19JRc5Qk+sY4CuDgRI6GV4CgyDIcxEGMUoiQyUjKFk397/33yfRHb2NnKNnL6V7U4T5JuAaTqkOD4U8u1u7ANocbSVVT6VI0KoMtQrR7PfXb5k8V5IQy/Dh+K2kzCGEK0ibayxSgLvuvXT7BOG3k97dmNMFAkCJUNJJ+cJOPDy30XJm/19gKZUqjy+1HVAi4403pgb8paaG7AQMLE5XA47Gcit5mFCJGGojC2tlso4WQfgKO/H9fz2or+jAS6v4WvI5ATjEQe+OizYb5UiGo7r0ajAhgygqn36An/lanLE/Oz8/M8UVg4ykVtTgjiPEoi27vNBd/MCYbgaSob9NQiFhDCD28RULSYeAz0Adjz2vDwcnygL7Kr4p0tbyCQTAYOtxVFJWAQQGdWSRaneSm8HaOrL0KYsIFfkhBEzBUIQRRGlDBKJPvw2knizs76iqI/FqmNrw5f6oGPgVQMvCiBDocjHo+v/rm8XKv98aDRuJdLfb55cJTTCOgBEMJozpW6zvvxdHhidmZ2ljK8bCc4RUIZIhPZ2DFI+GZPTrDRFe/tLteW4/FfV1dXgRBARNcGfNceg/FIZLCrq//FF195tVHc/ylXTStiltDNZ0bBKKRPdFwZTSxCSOMwkRCjcGoKRHxHwgQRudOLgtgWJPHwwsn3d34/Vi90DfZFIgOr45Rgt0zwPBDCOAEH+wn4ym40hzU7rSlngzDp0V2uay+/SU32x+ZnIJyH0M5wCmWHyClySaMkkg9FHd731LoGI30DBOwm4C8E5Ag7AIcBhBAJQvh3KNcw9Sr3OAQhEnSm04Fy4k0STvsnIJyhDMUwZKBMESGSUbaakSycgY9+/mZ647jwXT+AkfHxVWuOMJCLgcDhywL2nAFJ+PCvqlNvmC09dgEYyoYCWzjT2MNQdPlyu9BOMUFEYZRC4lngGOaw8vMgfJEBBsLXGfjVOeCFe5ri0RoyQkwTEqq6M3VthN+nnmIhz2U5DLlYiOpEROEfxSvl91Mbx78F/0CA8HUGsu6iw/GVG0DqcTw+wMDdqoL1xWNWsAvJCNFkV0YL1GeoySzkcWgLGTg9PW23uVOKxIOPhuY8BZjXu7oiAtjdCfhSJ2DXhbsKVTBbKaHJGqg0CvnmLpj1JDdjLcLZNiGAkggjhHIwAsU6FIAfU4DRXQIOAPirDbz57UutQBABvCWBA0hQL6vwoceVUoWFHKE1Tz7MBr17YRZiHGLBlsJRKZQpCqIIkXj4iYBhBJjVXhABMtBhAx34JVqM/GSCwxZwzalGVZWEZglCnYUh3k5A1DJOb3FKCP0xDCYA2zLkgtAmyhBJx98DijWwqd7vH7SAlwBEEVC2WAJvtQIjD5LRaFRV0hk1U2pCiNXwrpWhC0QWFvwkxFweTSAMivC8kIgyRFH8PYAfj24c72furg3SFPkvIHjtwEhfpDEZ8niimm6aqtFsFljIU5mHYdKF1dB7YHV5ZG40THmcbSkSCKEkthV12I8GmwhQzOHhS79CIIGixbBJIC8zPEmqvQshAHOVUiFTgrBkYC5rQuh0Jp1pzes95Ayxp0z76YqtEcoMxVBkpCgGLuIdj9NC9S6tMeeAOBecJdgKvNQjgLXo87cWgh5FN0pHxUxpCUKaKiJEFmpawLe5SJsehTgFIoqBwjdHJZFcwkktTtAA1JVdK8B24LMEdLQA8apIsEcAU88/7YZQyxml/aJWKTRpIJq6RWRhUlW8vvLMiBUiRqKfkoFQAq9cmbOVkklE9mGF0auNh61APioIIBPbW9zTQ+fBVQIup56HMIkMq2ZlfUvLVgqFQqVi5EDULGIyEFUCk6mjaUsI4hSM9nbCPiogr0gkCRnIvpyu//FiB+APBOQ6BxzGaWaVE+SnncmFkJrOms31vXoexFIFfRYbHx1fParTlzyIvWwT5yyBSJCBI7YSxSlawDD7cjkdHT7X4v8GDsdX47VITeUPuPdOOlMIMYcQ62mTbpIrhnmWYsoZUl0+3+b1KyQURBT7RIQjVDaR/4xjxvxo6jjKadruw0EJFMuM8xwQg1AAh2kMxmuDjWchpEexY85UVMnopaPDMuY0iohkxOYXcgVVj9fnFSGykSRc0ieFDMQYwBnQyJgVExuBtmuvMgBCQMTzCXa3AyO15bVeSpBqaIxT1AtHW+Uo/lYqEzHyORZrpZryelfeDjORygYxrxNw9M7GaTOTNUuGokFYXftfIHztwCch/OtpBuKB+82hsaTLg02luL6159GypoEUESMbVeyHasrprL8d5vksjLKkUAxCbB/b+XTeKBSVTJoivP9/QGo7gDcBhE8s1JHlHHgEpM+SDflAVLWqsbV9WI9q2bxhCGIVOWqaigG5+fbE3MvS2MpElmcBJq6vF+sZo9gsKvlclSLUa3IWUzm628YgxmQn4GBXTe99+iYDJ90gTiadwaiiZZuIccWjZXI5E422jGneBkP1g+tTiBFGWS350RxPHG8bmob4mhUtWzDooKkoaxLIJYA9FlB0uB3YB2B///1JTtCN/xfiG7rZ6/Zh1YFRbx5t7ZVDUS2jZ3NEhLFKMxsH8M1tfJB/5OVW41l/R2MzR4XNlXrewIplKHvbzYpOQ2ShE7DbTvAJR7eDX20FQsg3nhcWOEEAvckkGYfYqCqZwvrRwWYK8ztdzehnVc1kNLV++Nasf3rE5llrz2hi/up2sR4qZ/OmUSwZmejhUbNgVHG7uOC2gLj0r5cs4A/Yi7s5uMcfeWK122J/d/NnCXymj4X9D//CI1n3pG8s6XTikDWGD2f0ujFlQh5VyRa2t7cON1ecJNbQZkuay2br5frm11vr1z7g+9+rp0fbhb16eWWlns3nc6aRz6jlw/XtUjGfUaIhb+/QWh8BnwKwh8OKB7DtUjkY2COAT7cABxhI98Z/m75eN4BJ54IrhTt3vK1J5LFkCkgtWzzY2jo4/LG8kkp52Jmx+p7N1OtlqyBTlTq9DD7N+/LewfpW0TCzacWzgFE0uXwGjPfYQAvlaAH+09r9hDQZh3EAX6xksiA1EzxYh/5cxN3eWNALicvSQ4wyWwwGY7RDQ7122EEGgx1keBC9KOFNPARBQvUehHc3M4IdJmsQMQJxiwiimCDS93ne37Pf1ltW1Dff7fV12/vxeX6/1232VgXA+61AJbzrmx8fB4h9Sfrr/LmHEygkb8MzshlS7u6C+fHd8+wYF/cO6klSlfmlpTlM9sSrZHLr45cPb3efrK0tLxEPHTFHMqFQCMBAOP3+Pvd459Pse6U62wqsCTAgPXZevd87fBkbn4rn8NsmAEFKJCYZiXaTMksbE3NrTz7vfvv2lqQf373b4to5wdrTd6+Jhi+/oEKCO5PNTOX5bIrSFQIG8CxlBz4CPowqYOSs56QA9y7X8Gw1ooAklBLevbv5dTs6PgWKABP0ijSZmQKSahmLosD4mvMiYXll48mbD7u7u2+bQYXfbKwtY6pzKWeSWcw5ORulcjMUCt5I74TDApwdE5Xf0/FegGaVfQCmAKSJTEIh7tdf4dc6uSwVywHil4zY2WQuNu4w4XwIaO6B+jaQhJM7HKzQs90snqjFuPwYzAQ063i2YO2kA4axE44Q5f3iAwW8etHT2VDAqrkXIZ8Ag9PTrcJ79zYPq3ceooxJqhMCH71DjMGGGcqTR37yTNC8B5Zmlg49x3VO5eGix2J8D9OcsW4G7bR1I5BiIIjr+VJaqc57On24InfNrADILTZS3GOEhUKE0bednY1lspMJBVzCBJinyYBR/w5OjCsqTuupiDr4LJ8nWyYDbjQ2wcDh+k3btrBDBxiBcX3kmfKlz3kuFAmIFM1nYYQrmEoBGCSgLiITqY7bCdSRjACuLW+sraw5WaFsrKyACmk2k8GJ23za7AQyRcdSHsQI3kFhILV49sCyrWk6zqDFaeweFNRKgJc8F2rYQsJG/naEgZG0Yagmh6jLqohi3Ly3+bU+F52K5l4lHs3hacPGxtoGAhsBOVhh9vIyBgDmBYYrD0J+rzuJYUhA7nDOZ6EI0yHszkilFLA6sodLcu1c8nirESfpfD7tACOGYQRoJnMNEU1s1nHz8GAuh597k0DOL7PwyRN8cEQIIBFfilADc86YzR0MXr9+RQFThgArIzWFWrjg8ZzFJko4bq6HIwqYSjEQcYQIC2UwMnL/sL49GXuYS+LYglGIErLwDV4B0vWKAJcYyD4AyZeJYqguztf3b8F3hY8yAKLFAECyaq5fY9P9YifOcRdgyayFjbCBVSogSigzBUZptCJqJJQH2/MzmTEMyq2l5TUwJQCSTwGlftkcxub4VGK7vr9591YLMEBj0OBeLpppVcETOIX8pK0+qZqVcBjd5RYzMEBFbBOKkaOIrPQdVB/PJQDAj42trTlisYyLBxvTYEPBJ19u1w+hu+v4bpJPAWnP4UhxJB5ROUv/mItPVXBh+MEOAQ0NJOG0ELXRXcdNugD062H9YBvPkxPJsXhGglOMAYsn77zcPvB93acb3yMe+wQYaAKNvZFCRJm2vBD2GJxw+rZZNCTcY+6yFFHGohAFSUwdETN3/6sEKrhIxoGOeeybJh+qgUTYsXq5GHH6iKMMMuDIafIUrCbQSAUoAiTi6CgRNfJoJYnEylda1+bTQMNJMR/HZGFgsYOAHeWwwSlPxNapxAzkMHEoiJASQIpDdCE11B2hAUc65okPDW4HPjP3+Bpl83so3m7HFLEKZsFISQkFyV1mIAKiNiKCFKeOw6ELXpe06pSPpzA+pIATD8sy6Po9nAH1ubE+tbgQFh35hIgiauMoEyVMxB/ldEHbabAJD8GgGZUp3CxgyayAwummKcI9NlT2zMl0SirY2uWh4JAIQYTRhZRyHhWm6eqJj6ewEKvmg7RwBjxOvH3S1/JqvmpISOgeiSJUzRahMLVTaeWSIjjpLsLAIQ1ciOVrhkpXh0elw5ZtxfF8MeXYxCdAEraX0VVJhXVH40SH8oW4fMgQgCrlLBoswRSR9BmS6uVoMYD8FIhFooia2Y6UK1aJi6ObizAPafqemaW0zNGuTg08WeZNVOTCSLwYsFJuIOkEODg46JTRmdiMhPH3GcXEUDjpb7sv3qAdk8ZCAXX8WlLIDx9wj11ARLVEAflSF1I5pe0CpjVpK0WAg/xYAsRhZiFLPkmvtxXo7cUmYoG+N5x/VD4KiAqCCB0DB0dHsd4UaqlgEfpMh+5GCwM5tAOrGrtcUj4w7H5PWzq6AsyjFOMj0aqtcXgAutBECgMpXEnu9Z+G7xZExIdvP1AsmcMVW3gBWzVYZ8DWU2OhMHz5QbVhNYE4ktLjKOKQFFECJDP/zKZ1AgzaxWezZrIW0Onp/BHoxTDUqa2aZrRSK1s3GMgtlgRlILrCvQaUqbywSX1oXvMheLF81ZnLZqxiY1+SXhwCXcIeGqkSq1gax2vqsUK1VlxolG3btgL6SNPSaH3cCTaLSRccHmx6lW7uHPosC49YbviK1UppEadB3N4rB9RgJEQXfO509qCJgebthsrVGTqbBMzh2dineBy/atd50JIkgovVH4K/kObeQjddxX3oZO+pRX5vJj9W8VkYRdweELTP3WWbx5maFbiyFvAtFh6t4p2U2cV83jTzbRlebMv4xOJES2YpvCJb9E3xluhYdrVUqFRrPpvHDeOcAun+uoQD5SGZDlgo9H1Z6Ea50Vj4j2k0yhg2FtOcNHdq9ZzUIpew30ckrl57aPDpI7UrgypqJcQLLn8a173VgzLQ9nd6jkpnn6Xv1sLDooFMYpWDkwUqoFw4l0/duFVIu8Bi9R7z/C7ne3HPI6J8CDMH/y3tD9vl9/7W1+nxnsekOjoCdPNCf8hyqt7e7rK/w+OF8Pfp6O/pors7lF8Df7brPy4dL80RNGj3DvzN/27g7Riol60gHVp5QQPoesiN+Euk/iL76KGxF7vu7xfdnxsvnPP39XaVbSuIvv33DDHNtrt83f5znfrI97dKb8fJ/nMX/f6+vr6e7jPHjx8/dQY5LmmuNTeeOnXq+Knu7m6sUHhbdzc+5BN8tfegp6/P7794vv/c6d8Nuu/BT1/xkr225gAAAABJRU5ErkJggg==", + "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..f1a9b01898 --- /dev/null +++ b/test/mitmproxy/data/har_files/safari.json @@ -0,0 +1,2611 @@ +[ + { + "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": 1680135323.7605977 + }, + "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": 1680135323.7605977, + "pretty_host": "mitmproxy.org" + }, + "response": { + "http_version": "HTTP/2", + "status_code": 200, + "reason": "OK", + "headers": [ + [ + "Content-Type", + "text/html; charset=utf-8" + ], + [ + "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" + ], + [ + "content-length", + "4946" + ] + ], + "contentLength": 4946, + "contentHash": "b643b55c326524222b66cf8d6676c9b640e6417d23aaaa12c8a4ac58216d6586", + "timestamp_start": 1680135212.418, + "timestamp_end": 1680135323.7605977 + } + }, + { + "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.606069 + }, + "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.606069, + "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" + ], + [ + "content-length", + "36819" + ] + ], + "contentLength": 36819, + "contentHash": "62c365a16e642d4b512700140d3e99371aed31b74edc736f9927b6375b1230c0", + "timestamp_start": 1680135212.542, + "timestamp_end": 1680135212.606069 + } + }, + { + "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.625603 + }, + "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.625603, + "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.625603 + } + }, + { + "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.604729 + }, + "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.604729, + "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.604729 + } + }, + { + "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.609786 + }, + "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.609786, + "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.609786 + } + }, + { + "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.856044 + }, + "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.856044, + "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.856044 + } + }, + { + "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.8695679 + }, + "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.8695679, + "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" + ], + [ + "content-length", + "5167" + ] + ], + "contentLength": 5167, + "contentHash": "8a74d8c765558a54c3fb4eeb2e24367cfca6a889f0d56b7fc179eb722e5f8ebf", + "timestamp_start": 1680135212.557, + "timestamp_end": 1680135212.8695679 + } + }, + { + "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.631427 + }, + "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.631427, + "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" + ], + [ + "content-length", + "3346" + ] + ], + "contentLength": 3346, + "contentHash": "e6c21f88023539515a971172a00e500b7e4444fbf9506e47ceee126ace246808", + "timestamp_start": 1680135212.559, + "timestamp_end": 1680135212.631427 + } + }, + { + "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.620405 + }, + "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.620405, + "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" + ], + [ + "content-length", + "794" + ] + ], + "contentLength": 794, + "contentHash": "43cb84ef784bafbab5472abc7c396d95fc4468973d6501c83709e40963b2a953", + "timestamp_start": 1680135212.559, + "timestamp_end": 1680135212.620405 + } + }, + { + "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": 1680135247.3946638 + }, + "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": 1680135247.3946638, + "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" + ], + [ + "content-length", + "3346" + ] + ], + "contentLength": 3346, + "contentHash": "1f3ac146af1c45c1a2b4e6c694ecb234382d77b4256524d5ffc365fc8d6130b0", + "timestamp_start": 1680135212.56, + "timestamp_end": 1680135247.3946638 + } + }, + { + "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": 1680135247.3946638 + }, + "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": 1680135247.3946638, + "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" + ], + [ + "content-length", + "3346" + ] + ], + "contentLength": 3346, + "contentHash": "1f3ac146af1c45c1a2b4e6c694ecb234382d77b4256524d5ffc365fc8d6130b0", + "timestamp_start": 1680135212.56, + "timestamp_end": 1680135247.3946638 + } + }, + { + "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": 1680135252.528817 + }, + "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": 1680135252.528817, + "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" + ], + [ + "content-length", + "3346" + ] + ], + "contentLength": 3346, + "contentHash": "1f3ac146af1c45c1a2b4e6c694ecb234382d77b4256524d5ffc365fc8d6130b0", + "timestamp_start": 1680135212.56, + "timestamp_end": 1680135252.528817 + } + }, + { + "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": 1680135252.528817 + }, + "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": 1680135252.528817, + "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" + ], + [ + "content-length", + "3346" + ] + ], + "contentLength": 3346, + "contentHash": "1f3ac146af1c45c1a2b4e6c694ecb234382d77b4256524d5ffc365fc8d6130b0", + "timestamp_start": 1680135212.56, + "timestamp_end": 1680135252.528817 + } + }, + { + "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.644795 + }, + "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.644795, + "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.644795 + } + }, + { + "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.609024 + }, + "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.609024, + "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.609024 + } + }, + { + "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.622383 + }, + "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.622383, + "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.622383 + } + }, + { + "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.729846 + }, + "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.729846, + "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" + ], + [ + "content-length", + "3969" + ] + ], + "contentLength": 3969, + "contentHash": "3c4880b4b424071aa5e5c5f652b934179099ee8786ea67520b2fadbc4305e5a8", + "timestamp_start": 1680135212.583, + "timestamp_end": 1680135212.729846 + } + }, + { + "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": 1680135389.811421 + }, + "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": 1680135389.811421, + "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": 1680135389.811421 + } + }, + { + "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": 1680135222.496895 + }, + "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": 1680135222.496895, + "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" + ], + [ + "content-length", + "1421" + ] + ], + "contentLength": 1421, + "contentHash": "81de6d4a4bdb984627d61de60369ec4f0ce182170fbe6d9a980b15574d5f6c50", + "timestamp_start": 1680135212.6, + "timestamp_end": 1680135222.496895 + } + } +] \ 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..35b11d6191 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,6 +11,7 @@ ["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], ], ) 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 caf37fe5ab..a0207130cc 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 = {} @@ -87,7 +87,8 @@ def test_roundtrip_format_unicode(self): self.assertEqual((v, b""), tnetstring.pop(tnetstring.dumps(v))) def test_roundtrip_big_integer(self): - i1 = math.factorial(30000) + # Recent Python versions do not like ints above 4300 digits, https://github.com/python/cpython/issues/95778 + i1 = math.factorial(1557) s = tnetstring.dumps(i1) i2 = tnetstring.loads(s) self.assertEqual(i1, i2) diff --git a/test/mitmproxy/net/dns/test_domain_names.py b/test/mitmproxy/net/dns/test_domain_names.py index 72e6e5391b..1d0f54d6cc 100644 --- a/test/mitmproxy/net/dns/test_domain_names.py +++ b/test/mitmproxy/net/dns/test_domain_names.py @@ -1,5 +1,6 @@ import re import struct + import pytest from mitmproxy.net.dns import domain_names @@ -8,21 +9,21 @@ 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) + ) == ( + "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(): @@ -61,9 +62,7 @@ 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" 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..a9148e7abc 100644 --- a/test/mitmproxy/net/http/http1/test_read.py +++ b/test/mitmproxy/net/http/http1/test_read.py @@ -1,18 +1,17 @@ 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.net.http.http1.read import validate_headers +from mitmproxy.test.tutils import treq +from mitmproxy.test.tutils import tresp def test_get_header_tokens(): 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..473b930f84 100644 --- a/test/mitmproxy/net/http/test_headers.py +++ b/test/mitmproxy/net/http/test_headers.py @@ -1,6 +1,7 @@ import collections -from mitmproxy.net.http.headers import parse_content_type, assemble_content_type +from mitmproxy.net.http.headers import assemble_content_type +from mitmproxy.net.http.headers import parse_content_type def test_parse_content_type(): 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/test_encoding.py b/test/mitmproxy/net/test_encoding.py index 9d155961b8..640d318aef 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 diff --git a/test/mitmproxy/net/test_local_ip.py b/test/mitmproxy/net/test_local_ip.py new file mode 100644 index 0000000000..f8b983a969 --- /dev/null +++ b/test/mitmproxy/net/test_local_ip.py @@ -0,0 +1,17 @@ +from mitmproxy.net import local_ip + + +def test_get_local_ip(): + # should never error, but may return None depending on the host OS configuration. + local_ip.get_local_ip() + local_ip.get_local_ip("0.0.0.0") + local_ip.get_local_ip("127.0.0.1") + local_ip.get_local_ip("invalid!") + + +def test_get_local_ip6(): + # should never error, but may return None depending on the host OS configuration. + local_ip.get_local_ip6() + local_ip.get_local_ip6("::") + local_ip.get_local_ip6("::1") + local_ip.get_local_ip("invalid!") diff --git a/test/mitmproxy/net/test_server_spec.py b/test/mitmproxy/net/test_server_spec.py index ba527a20ee..1fe5590182 100644 --- a/test/mitmproxy/net/test_server_spec.py +++ b/test/mitmproxy/net/test_server_spec.py @@ -4,40 +4,34 @@ @pytest.mark.parametrize( - "spec,out", + "spec,default_scheme,out", [ - ("example.com", ("https", ("example.com", 443))), - ("http://example.com", ("http", ("example.com", 80))), - ("smtp.example.com:25", ("http", ("smtp.example.com", 25))), - ("http://127.0.0.1", ("http", ("127.0.0.1", 80))), - ("http://[::1]", ("http", ("::1", 80))), - ("http://[::1]/", ("http", ("::1", 80))), - ("https://[::1]/", ("https", ("::1", 443))), - ("http://[::1]:8080", ("http", ("::1", 8080))), + ("example.com", "https", ("https", ("example.com", 443))), + ("http://example.com", "https", ("http", ("example.com", 80))), + ("smtp.example.com:25", "tcp", ("tcp", ("smtp.example.com", 25))), + ("http://127.0.0.1", "https", ("http", ("127.0.0.1", 80))), + ("http://[::1]", "https", ("http", ("::1", 80))), + ("http://[::1]/", "https", ("http", ("::1", 80))), + ("https://[::1]/", "https", ("https", ("::1", 443))), + ("http://[::1]:8080", "https", ("http", ("::1", 8080))), ], ) -def test_parse(spec, out): - assert server_spec.parse(spec) == out +def test_parse(spec, default_scheme, out): + assert server_spec.parse(spec, default_scheme) == out def test_parse_err(): with pytest.raises(ValueError, match="Invalid server specification"): - server_spec.parse(":") + server_spec.parse(":", "https") with pytest.raises(ValueError, match="Invalid server scheme"): - server_spec.parse("ftp://example.com") + server_spec.parse("ftp://example.com", "https") with pytest.raises(ValueError, match="Invalid hostname"): - server_spec.parse("$$$") + server_spec.parse("$$$", "https") with pytest.raises(ValueError, match="Invalid port"): - server_spec.parse("example.com:999999") + server_spec.parse("example.com:999999", "https") - -def test_parse_with_mode(): - assert server_spec.parse_with_mode("m:example.com") == ( - "m", - ("https", ("example.com", 443)), - ) - with pytest.raises(ValueError): - server_spec.parse_with_mode("moo") + with pytest.raises(ValueError, match="Port specification missing"): + server_spec.parse("example.com", "tcp") diff --git a/test/mitmproxy/net/test_tls.py b/test/mitmproxy/net/test_tls.py index 67ade461e4..58e97b3dee 100644 --- a/test/mitmproxy/net/test_tls.py +++ b/test/mitmproxy/net/test_tls.py @@ -1,6 +1,8 @@ from pathlib import Path +from OpenSSL import crypto from OpenSSL import SSL + from mitmproxy import certs from mitmproxy.net import tls @@ -23,20 +25,23 @@ def test_sslkeylogfile(tdata, monkeypatch): entry = store.get_cert("example.com", [], None) cctx = tls.create_proxy_server_context( + method=tls.Method.TLS_CLIENT_METHOD, 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, - cert=entry.cert, - key=entry.privatekey, + ecdh_curve=None, chain_file=entry.chain_file, alpn_select_callback=None, request_client_cert=False, @@ -47,16 +52,18 @@ def test_sslkeylogfile(tdata, monkeypatch): server = SSL.Connection(sctx) server.set_accept_state() + server.use_certificate(entry.cert.to_pyopenssl()) + server.use_privatekey(crypto.PKey.from_cryptography_key(entry.privatekey)) + client = SSL.Connection(cctx) client.set_connect_state() read, write = client, server while True: try: - print(read) 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 @@ -66,10 +73,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 index 1db5a60997..b8f5250943 100644 --- a/test/mitmproxy/net/test_udp.py +++ b/test/mitmproxy/net/test_udp.py @@ -1,10 +1,84 @@ +import asyncio + import pytest -from mitmproxy.net.udp import MAX_DATAGRAM_SIZE, DatagramReader +from mitmproxy.connection import Address +from mitmproxy.net.udp import DatagramReader +from mitmproxy.net.udp import DatagramWriter +from mitmproxy.net.udp import MAX_DATAGRAM_SIZE +from mitmproxy.net.udp import open_connection +from mitmproxy.net.udp import start_server + + +async def test_client_server(): + server_reader = DatagramReader() + server_writer: DatagramWriter | None = 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) - TermLog().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, ) 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..9b1e2676df 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 @@ -9,19 +8,17 @@ 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 6c82a58143..273179bdf3 100644 --- a/test/mitmproxy/proxy/layers/http/test_http.py +++ b/test/mitmproxy/proxy/layers/http/test_http.py @@ -1,25 +1,34 @@ import gc +from logging import WARNING import pytest -from mitmproxy.connection import ConnectionState, Server -from mitmproxy.http import HTTPFlow, Response -from mitmproxy.net.server_spec import ServerSpec +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.tcp import TCPFlow, TCPMessage -from test.mitmproxy.proxy.tutils import ( - BytesMatching, - Placeholder, - Playbook, - reply, - reply_next_layer, -) +from mitmproxy.proxy.mode_specs import ProxyMode +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): @@ -669,6 +678,7 @@ def test_server_unreachable(tctx, 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. + # See also: test_connect_unauthorized playbook << http.HttpErrorHook(flow) playbook >> reply() playbook << SendData( @@ -729,7 +739,7 @@ def test_upstream_proxy(tctx, redirect, domain, scheme): server = Placeholder(Server) server2 = Placeholder(Server) flow = Placeholder(HTTPFlow) - tctx.options.mode = "upstream:http://proxy:8080" + tctx.client.proxy_mode = ProxyMode.parse("upstream:http://proxy:8080") playbook = Playbook(http.HttpLayer(tctx, HTTPMode.upstream), hooks=False) if scheme == "http": @@ -742,7 +752,8 @@ 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), ) ) @@ -782,7 +793,7 @@ def test_upstream_proxy(tctx, redirect, domain, scheme): flow().request.host = domain + b".test" flow().request.host_header = domain elif redirect == "change-proxy": - flow().server_conn.via = ServerSpec("http", address=("other-proxy", 1234)) + flow().server_conn.via = ("http", ("other-proxy", 1234)) playbook >> reply() if redirect: @@ -797,7 +808,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( @@ -806,7 +818,9 @@ 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\n\r\n" % domain + ) playbook >> DataReceived( server2, b"HTTP/1.1 200 Connection established\r\n\r\n" ) @@ -828,11 +842,9 @@ 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.address == ("other-proxy", 1234) - ) + assert server2().address == flow().server_conn.via[1] == ("other-proxy", 1234) else: - assert server2().address == flow().server_conn.via.address == ("proxy", 8080) + assert server2().address == flow().server_conn.via[1] == ("proxy", 8080) playbook >> ConnectionClosed(tctx.client) playbook << CloseConnection(tctx.client) @@ -848,10 +860,10 @@ def test_http_proxy_tcp(tctx, mode, close_first): tctx.options.connection_strategy = "lazy" if mode == "upstream": - tctx.options.mode = "upstream:http://proxy:8080" + tctx.client.proxy_mode = ProxyMode.parse("upstream:http://proxy:8080") toplayer = http.HttpLayer(tctx, HTTPMode.upstream) else: - tctx.options.mode = "regular" + tctx.client.proxy_mode = ProxyMode.parse("regular") toplayer = http.HttpLayer(tctx, HTTPMode.regular) playbook = Playbook(toplayer, hooks=False) @@ -956,8 +968,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" + ) ) @@ -1348,7 +1364,7 @@ def test_upgrade(tctx, proto): playbook << Log( "Sent HTTP 101 response, but no protocol is enabled to upgrade to.", - "warn", + WARNING, ) << CloseConnection(tctx.client) ) @@ -1417,6 +1433,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 @@ -1594,6 +1633,42 @@ 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\n\r\n") + << http.HttpConnectHook(flow) + >> reply(side_effect=require_auth) + # This isn't ideal - we should probably have a custom CONNECT error hook here. + # See also: test_server_unreachable + << http.HttpResponseHook(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"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()) @@ -1656,7 +1731,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 6e8297af49..40364a7085 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" @@ -325,8 +335,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, @@ -373,9 +382,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 +419,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 +434,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/" @@ -1032,7 +1031,7 @@ def test_informational_response(self, tctx, code, log_msg): >> DataReceived( tctx.server, frame_factory.build_headers_frame(resp).serialize() ) - << Log(f"Swallowing HTTP/2 informational response: {log_msg}", "info") + << Log(f"Swallowing HTTP/2 informational response: {log_msg}") ) @@ -1196,3 +1195,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..6e6eb88955 --- /dev/null +++ b/test/mitmproxy/proxy/layers/http/test_http3.py @@ -0,0 +1,1132 @@ +import collections.abc +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 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 MultiPlaybook(tutils.Playbook): + """Playbook that allows multiple events and commands to be registered at once.""" + + def __lshift__(self, c): + if isinstance(c, collections.abc.Iterable): + for c_i in c: + super().__lshift__(c_i) + else: + super().__lshift__(c) + return self + + def __rshift__(self, e): + if isinstance(e, collections.abc.Iterable): + for e_i in e: + super().__rshift__(e_i) + else: + super().__rshift__(e) + return self + + +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] = {} + self.max_push_id: int | None = None + + 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_max_push_id(self) -> quic.SendQuicStreamData: + def cb(data: bytes) -> None: + buf = Buffer(data=data) + assert buf.pull_uint_var() == FrameType.MAX_PUSH_ID + buf = Buffer(data=buf.pull_bytes(buf.pull_uint_var())) + self.max_push_id = buf.pull_uint_var() + assert buf.eof() + + return quic.SendQuicStreamData( + connection=self.conn, + stream_id=self.peer_stream_id[StreamType.CONTROL], + data=CallbackPlaceholder(cb), + 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: int, stream_id: int = 0) -> quic.ResetQuicStream: + return quic.ResetQuicStream( + connection=self.conn, + stream_id=stream_id, + error_code=error_code, + ) + + def receive_reset( + self, error_code: int, stream_id: int = 0 + ) -> quic.QuicStreamReset: + return quic.QuicStreamReset( + connection=self.conn, + stream_id=stream_id, + error_code=error_code, + ) + + def send_init(self) -> Iterable[quic.SendQuicStreamData]: + yield self.send_stream_type(StreamType.CONTROL) + yield self.send_settings() + if not self.is_client: + yield self.send_max_push_id() + 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_client(tctx: context.Context) -> tuple[tutils.Playbook, FrameFactory]: + tctx.client.alpn = b"h3" + tctx.client.transport_protocol = "udp" + tctx.server.transport_protocol = "udp" + + playbook = MultiPlaybook(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_client(tctx) + + +def test_fail_without_header(tctx: context.Context): + playbook = MultiPlaybook(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) + ) + assert cff.is_done + + +def test_invalid_header(tctx: context.Context): + playbook, cff = start_h3_client(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, + 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_client(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_client(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_client(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_client(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_client(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, + frame_type=None, + reason_phrase="peer closed connection", + ) + + if stream: + playbook << commands.CloseConnection(server) + playbook << http.HttpErrorHook(flow) + playbook >> tutils.reply() + + if how == "RST+disconnect": + playbook >> quic.QuicConnectionClosed( + tctx.client, + error_code=ErrorCode.H3_NO_ERROR, + frame_type=None, + reason_phrase="peer closed connection", + ) + assert playbook + assert ( + "stream reset" 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, + frame_type=None, + reason_phrase="peer closed connection", + ) + + playbook << commands.CloseConnection(server) + playbook << http.HttpErrorHook(flow) + playbook >> tutils.reply() + assert playbook + + if how == "RST+disconnect": + playbook >> quic.QuicConnectionClosed( + tctx.client, + error_code=ErrorCode.H3_REQUEST_CANCELLED, + frame_type=None, + reason_phrase="peer closed connection", + ) + assert playbook + + if "RST" in how: + assert "stream reset" 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_client(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=quic.QuicErrorCode.PROTOCOL_VIOLATION, + frame_type=None, + reason_phrase=err, + ) + >> quic.QuicConnectionClosed( + tctx.client, + error_code=quic.QuicErrorCode.PROTOCOL_VIOLATION, + 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_client(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) + << http.HttpErrorHook(flow) + >> tutils.reply() + >> 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_client(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) + >> tutils.reply(to=response) + << cff.send_reset(ErrorCode.H3_INTERNAL_ERROR) + ) + 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_client(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_client(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_client(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) + >> 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 + + +class TestClient: + def test_no_data_on_closed_stream(self, tctx: context.Context): + frame_factory = FrameFactory(tctx.server, is_client=False) + playbook = MultiPlaybook(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.receive_data(b"foo") + << quic.StopQuicStream(tctx.server, 0, ErrorCode.H3_REQUEST_CANCELLED) + ) # 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 = MultiPlaybook(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_client(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..599793e32d 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"), @@ -77,7 +79,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() diff --git a/test/mitmproxy/proxy/layers/test_dns.py b/test/mitmproxy/proxy/layers/test_dns.py index 6f4e2bb077..133ae20b3c 100644 --- a/test/mitmproxy/proxy/layers/test_dns.py +++ b/test/mitmproxy/proxy/layers/test_dns.py @@ -1,11 +1,18 @@ import time -from mitmproxy.proxy.commands import CloseConnection, Log, OpenConnection, SendData -from mitmproxy.proxy.events import ConnectionClosed, DataReceived -from mitmproxy.proxy.layers import dns +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.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.tutils import tdnsreq +from mitmproxy.test.tutils import tdnsresp def test_invalid_and_dummy_end(tctx): @@ -15,11 +22,8 @@ def test_invalid_and_dummy_end(tctx): << Log( "Client(client:1234, state=open) sent an invalid message: question #0: unpack encountered a label of length 99" ) + << CloseConnection(tctx.client) >> ConnectionClosed(tctx.client) - >> DataReceived(tctx.client, b"You still there?") - >> DataReceived(tctx.client, tdnsreq().packed) - >> DataReceived(tctx.client, b"Hello?") - << None ) diff --git a/test/mitmproxy/proxy/layers/test_modes.py b/test/mitmproxy/proxy/layers/test_modes.py index 6ebe4943bd..f99685c3f5 100644 --- a/test/mitmproxy/proxy/layers/test_modes.py +++ b/test/mitmproxy/proxy/layers/test_modes.py @@ -2,33 +2,45 @@ import pytest -from mitmproxy import platform +from mitmproxy import dns from mitmproxy.addons.proxyauth import ProxyAuth -from mitmproxy.connection import Client, Server -from mitmproxy.proxy.commands import ( - CloseConnection, - GetSocket, - Log, - OpenConnection, - SendData, -) +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 +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 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.test import taddons +from mitmproxy.test import tflow +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): @@ -41,18 +53,30 @@ 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.options.mode = "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.options.mode == "regular" + assert tctx2.client.proxy_mode == ProxyMode.parse("regular") del tctx - proxy1 = Playbook(modes.HttpProxy(tctx1), hooks=False) + proxy1 = Playbook(modes.HttpUpstreamProxy(tctx1), hooks=False) proxy2 = Playbook(modes.HttpProxy(tctx2), hooks=False) upstream = Placeholder(Server) @@ -89,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) ) @@ -124,7 +148,7 @@ def test_reverse_proxy(tctx, keep_host_header): - make sure that we include non-standard ports in the host header (#4280) """ server = Placeholder(Server) - tctx.options.mode = "reverse:http://localhost:8000" + tctx.client.proxy_mode = ProxyMode.parse("reverse:http://localhost:8000") tctx.options.connection_strategy = "lazy" tctx.options.keep_host_header = keep_host_header assert ( @@ -149,6 +173,75 @@ def test_reverse_proxy(tctx, keep_host_header): assert server().address == ("localhost", 8000) +def test_reverse_dns(tctx): + f = Placeholder(dns.DNSFlow) + server = Placeholder(Server) + tctx.client.proxy_mode = ProxyMode.parse("reverse:dns://8.8.8.8:53") + tctx.options.connection_strategy = "lazy" + 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) + >> reply(None) + << SendData(tctx.server, tflow.tdnsreq().packed) + ) + 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" + tctx.client.proxy_mode = ProxyMode.parse("reverse:quic://1.2.3.4:5") + 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 == ("1.2.3.4", 5) + assert quic.quic_parse_client_hello(client_hello()).sni == ( + "other" if keep_host_header else "1.2.3.4" + ) + + +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( @@ -160,12 +253,9 @@ def test_reverse_proxy_tcp_over_tls( reverse proxying. """ - if patch: - monkeypatch.setattr(tls, "ServerTLSLayer", tls.MockTLSLayer) - flow = Placeholder(TCPFlow) data = Placeholder(bytes) - tctx.options.mode = "reverse:https://localhost:8000" + tctx.client.proxy_mode = ProxyMode.parse("reverse:https://localhost:8000") tctx.options.connection_strategy = connection_strategy playbook = Playbook(modes.ReverseProxy(tctx)) if connection_strategy == "eager": @@ -187,8 +277,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) ) @@ -197,6 +287,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 @@ -217,16 +312,12 @@ def test_reverse_proxy_tcp_over_tls( @pytest.mark.parametrize("connection_strategy", ["eager", "lazy"]) -def test_transparent_tcp(tctx: Context, monkeypatch, connection_strategy): - monkeypatch.setattr(platform, "original_addr", lambda sock: ("address", 22)) - +def test_transparent_tcp(tctx: Context, connection_strategy): flow = Placeholder(TCPFlow) tctx.options.connection_strategy = connection_strategy + tctx.server.address = ("address", 22) - sock = object() playbook = Playbook(modes.TransparentProxy(tctx)) - playbook << GetSocket(tctx.client) - playbook >> reply(sock) if connection_strategy == "lazy": assert playbook else: @@ -249,23 +340,6 @@ def test_transparent_tcp(tctx: Context, monkeypatch, connection_strategy): assert tctx.server.address == ("address", 22) -def test_transparent_failure(tctx: Context, monkeypatch): - """Test that we recover from a transparent mode resolve error.""" - - def raise_err(sock): - raise RuntimeError("platform-specific error") - - monkeypatch.setattr(platform, "original_addr", raise_err) - assert ( - Playbook(modes.TransparentProxy(tctx), logs=True) - << GetSocket(tctx.client) - >> reply(object()) - << Log( - "Transparent mode failure: RuntimeError('platform-specific error')", "info" - ) - ) - - def test_reverse_eager_connect_failure(tctx: Context): """ Test @@ -273,7 +347,7 @@ def test_reverse_eager_connect_failure(tctx: Context): reverse proxying. """ - tctx.options.mode = "reverse:https://localhost:8000" + tctx.client.proxy_mode = ProxyMode.parse("reverse:https://localhost:8000") tctx.options.connection_strategy = "eager" playbook = Playbook(modes.ReverseProxy(tctx)) assert ( @@ -285,15 +359,13 @@ def test_reverse_eager_connect_failure(tctx: Context): ) -def test_transparent_eager_connect_failure(tctx: Context, monkeypatch): - """Test that we recover from a transparent mode resolve error.""" +def test_transparent_eager_connect_failure(tctx: Context): + """Test that we recover from a transparent mode connect error.""" tctx.options.connection_strategy = "eager" - monkeypatch.setattr(platform, "original_addr", lambda sock: ("address", 22)) + tctx.server.address = ("address", 22) assert ( Playbook(modes.TransparentProxy(tctx), logs=True) - << GetSocket(tctx.client) - >> reply(object()) << OpenConnection(tctx.server) >> reply("something something") << CloseConnection(tctx.client) diff --git a/test/mitmproxy/proxy/layers/test_quic.py b/test/mitmproxy/proxy/layers/test_quic.py new file mode 100644 index 0000000000..54ae91f340 --- /dev/null +++ b/test/mitmproxy/proxy/layers/test_quic.py @@ -0,0 +1,1252 @@ +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 import layers +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 udp +from mitmproxy.tcp import TCPFlow +from mitmproxy.udp import UDPFlow +from mitmproxy.udp import UDPMessage +from mitmproxy.utils import data +from test.mitmproxy.proxy import tutils + +tlsdata = data.Data(__name__) + + +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: quic.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 quic.CloseQuicConnection(event.connection, 123, None, "error") + elif isinstance(event, events.DataReceived) and event.data == b"stop-stream": + yield quic.StopQuicStream(event.connection, 24, 123) + 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(quic.QuicStreamCommand): + pass + + yield InvalidStreamCommand(event.connection, 42) + elif isinstance(event, quic.QuicConnectionClosed): + self.closed = event + elif isinstance(event, quic.QuicStreamDataReceived): + yield quic.SendQuicStreamData( + event.connection, event.stream_id, event.data, event.end_stream + ) + elif isinstance(event, quic.QuicStreamReset): + yield quic.ResetQuicStream( + 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" +) + + +def test_error_code_to_str(): + assert quic.error_code_to_str(0x6) == "FINAL_SIZE_ERROR" + assert quic.error_code_to_str(0x104) == "H3_CLOSED_CRITICAL_STREAM" + assert quic.error_code_to_str(0xDEAD) == f"unknown error (0xdead)" + + +def test_is_success_error_code(): + assert quic.is_success_error_code(0x0) + assert not quic.is_success_error_code(0x6) + assert quic.is_success_error_code(0x100) + assert not quic.is_success_error_code(0x104) + assert not quic.is_success_error_code(0xDEAD) + + +@pytest.mark.parametrize("value", ["s1 s2\n", "s1 s2"]) +def test_secrets_logger(value: str): + logger = MagicMock() + quic_logger = quic.QuicSecretsLogger(logger) + assert quic_logger.write(value) == 6 + quic_logger.flush() + logger.assert_called_once_with(None, b"s1 s2") + + +class TestParseClientHello: + def test_input(self): + assert quic.quic_parse_client_hello(client_hello).sni == "example.com" + with pytest.raises(ValueError): + quic.quic_parse_client_hello( + client_hello[:183] + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00" + ) + with pytest.raises(ValueError, match="not initial"): + quic.quic_parse_client_hello( + 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): + class InvalidClientHello(Exception): + @property + def data(self): + raise EOFError() + + monkeypatch.setattr(quic, "QuicClientHello", InvalidClientHello) + with pytest.raises(ValueError, match="Invalid ClientHello"): + quic.quic_parse_client_hello(client_hello) + + def test_connection_error(self, monkeypatch): + def raise_conn_err(self, data, addr, now): + raise quic.QuicConnectionError(0, 0, "Conn err") + + monkeypatch.setattr(QuicConnection, "receive_datagram", raise_conn_err) + with pytest.raises(ValueError, match="Conn err"): + quic.quic_parse_client_hello(client_hello) + + def test_no_return(self): + with pytest.raises(ValueError, match="No ClientHello"): + quic.quic_parse_client_hello( + client_hello[0:1200] + b"\x00" + client_hello[1200:] + ) + + +class TestQuicStreamLayer: + def test_ignored(self, tctx: context.Context): + quic_layer = quic.QuicStreamLayer(tctx, True, 1) + assert isinstance(quic_layer.child_layer, layers.TCPLayer) + assert not quic_layer.child_layer.flow + 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 = quic.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("ignore", [True, False]) + def test_error(self, tctx: context.Context, ignore: bool): + quic_layer = quic.RawQuicLayer(tctx, ignore=ignore) + 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_ignored(self, tctx: context.Context): + quic_layer = quic.RawQuicLayer(tctx, ignore=True) + assert ( + tutils.Playbook(quic_layer) + << 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") + >> quic.QuicStreamDataReceived(tctx.client, 0, b"msg3", end_stream=False) + << quic.SendQuicStreamData(tctx.server, 0, b"msg3", end_stream=False) + >> quic.QuicStreamDataReceived(tctx.client, 6, b"msg4", end_stream=False) + << quic.SendQuicStreamData(tctx.server, 2, b"msg4", end_stream=False) + >> quic.QuicStreamDataReceived(tctx.server, 9, b"msg5", end_stream=False) + << quic.SendQuicStreamData(tctx.client, 1, b"msg5", end_stream=False) + >> quic.QuicStreamDataReceived(tctx.client, 0, b"", end_stream=True) + << quic.SendQuicStreamData(tctx.server, 0, b"", end_stream=True) + >> quic.QuicStreamReset(tctx.client, 6, 142) + << quic.ResetQuicStream(tctx.server, 2, 142) + >> quic.QuicConnectionClosed(tctx.client, 42, None, "closed") + << quic.CloseQuicConnection(tctx.server, 42, None, "closed") + >> quic.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(quic.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(quic.RawQuicLayer(tctx)) + << commands.OpenConnection(tctx.server) + >> tutils.reply(None) + >> quic.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() + << quic.SendQuicStreamData(tctx.server, 2, b"msg1", end_stream=False) + >> quic.QuicStreamReset(tctx.client, 2, 42) + << quic.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(quic.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") + >> quic.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() + << quic.SendQuicStreamData(tctx.server, 2, b"msg2", end_stream=False) + >> quic.QuicConnectionClosed(tctx.client, 42, None, "bye") + << quic.CloseQuicConnection(tctx.server, 42, None, "bye") + << tcp.TcpEndHook(tcpflow) + >> tutils.reply() + >> quic.QuicConnectionClosed(tctx.server, 42, None, "bye") + << udp.UdpEndHook(udpflow) + >> tutils.reply() + ) + + def test_invalid_stream_event(self, tctx: context.Context): + playbook = tutils.Playbook(quic.RawQuicLayer(tctx)) + assert ( + tutils.Playbook(quic.RawQuicLayer(tctx)) + << commands.OpenConnection(tctx.server) + >> tutils.reply(None) + ) + with pytest.raises(AssertionError, match="Unexpected stream event"): + + class InvalidStreamEvent(quic.QuicStreamEvent): + pass + + playbook >> InvalidStreamEvent(tctx.client, 0) + assert playbook + + def test_invalid_event(self, tctx: context.Context): + playbook = tutils.Playbook(quic.RawQuicLayer(tctx)) + assert ( + tutils.Playbook(quic.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(quic.RawQuicLayer(tctx)) + << commands.OpenConnection(tctx.server) + >> tutils.reply(None) + >> quic.QuicStreamDataReceived(tctx.client, 0, b"msg1", end_stream=True) + << layer.NextLayerHook(tutils.Placeholder()) + >> tutils.reply_next_layer(lambda ctx: udp.UDPLayer(ctx, ignore=True)) + << quic.SendQuicStreamData(tctx.server, 0, b"msg1", end_stream=False) + << quic.SendQuicStreamData(tctx.server, 0, b"", end_stream=True) + << quic.StopQuicStream(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(quic.RawQuicLayer(tctx)) + << commands.OpenConnection(tctx.server) + >> tutils.reply(None) + >> quic.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") + << quic.SendQuicStreamData( + tctx.client, 0, b"open-connection failed: uhoh", end_stream=False + ) + ) + + def test_invalid_connection_command(self, tctx: context.Context): + playbook = tutils.Playbook(quic.RawQuicLayer(tctx)) + assert ( + playbook + << commands.OpenConnection(tctx.server) + >> tutils.reply(None) + >> quic.QuicStreamDataReceived(tctx.client, 0, b"msg1", end_stream=False) + << layer.NextLayerHook(tutils.Placeholder()) + >> tutils.reply_next_layer(TlsEchoLayer) + << quic.SendQuicStreamData(tctx.client, 0, b"msg1", end_stream=False) + ) + with pytest.raises( + AssertionError, match="Unexpected stream connection command" + ): + playbook >> quic.QuicStreamDataReceived( + tctx.client, 0, b"invalid-command", end_stream=False + ) + assert playbook + + +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 = quic.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"") + << quic.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.DatagramFrameReceived(b"stop-stream") + ) + 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: quic.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=tlsdata.path( + "../../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=tlsdata.path( + f"../../net/data/verificationcerts/{filename}.crt" + ), + keyfile=tlsdata.path( + f"../../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 = quic.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) -> int: + 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: quic.QuicTlsData) -> None: + config = QuicConfiguration() + config.load_cert_chain( + tlsdata.path("../../net/data/verificationcerts/trusted-leaf.crt"), + tlsdata.path("../../net/data/verificationcerts/trusted-leaf.key"), + ) + tls_start.settings = quic.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: quic.QuicTlsData) -> None: + tls_start.settings = quic.QuicTlsSettings( + ca_file=tlsdata.path("../../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 TestServerTLS: + def test_repr(self, tctx: context.Context): + assert repr(quic.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 = quic.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(quic.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 + << quic.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 + >> events.Wakeup(playbook.actual[4]) + << 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(quic.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) + << quic.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(quic.QuicTlsData) + assert ( + playbook + >> events.Wakeup(playbook.actual[9]) + << commands.Log( + "Server QUIC handshake failed. Certificate does not match hostname 'wrong.host.mitmproxy.org'", + WARNING, + ) + << tls.TlsFailedServerHook(tls_hook_data) + >> tutils.reply() + << commands.CloseConnection(tctx.server) + << commands.SendData( + tctx.client, + b"open-connection failed: Certificate does not match hostname 'wrong.host.mitmproxy.org'", + ) + ) + assert ( + tls_hook_data().conn.error + == "Certificate does not match hostname 'wrong.host.mitmproxy.org'" + ) + assert not tctx.server.tls_established + + +def make_client_tls_layer( + tctx: context.Context, no_server: bool = False, **kwargs +) -> tuple[tutils.Playbook, quic.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 quic.ServerQuicLayer(tctx, time=lambda: tssl_client.now) + ) + client_layer = quic.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 TestClientTLS: + 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(quic.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 TLS 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() + << quic.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 + >> events.Wakeup(playbook.actual[16]) + << commands.Log( + " >> Wakeup(command=RequestWakeup({'delay': 0.20000000000000004}))", + 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 + << quic.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()) + << quic.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( + "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(quic.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() + << quic.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(quic.QuicTlsData) + playbook >> events.DataReceived(tctx.client, tssl_client.read()) + assert playbook + tssl_client.now = tssl_client.now + 60 + assert ( + playbook + >> events.Wakeup(playbook.actual[7]) + << commands.Log( + "Client QUIC handshake failed. Certificate does not match hostname 'wrong.host.mitmproxy.org'", + 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." + ) + << quic.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." + ) + << quic.QuicStartClientHook(tutils.Placeholder()) + ) + + def test_version_negotiation(self, tctx: context.Context): + playbook, client_layer, tssl_client = make_client_tls_layer(tctx, version=0) + 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: No ClientHello returned. ({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) + quic.ClientQuicLayer(tctx, time=lambda: 0) + assert tctx.client.sni is None 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 0a28010f7b..9960f8b748 100644 --- a/test/mitmproxy/proxy/layers/test_tls.py +++ b/test/mitmproxy/proxy/layers/test_tls.py @@ -1,33 +1,29 @@ import ssl 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"] @@ -91,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() @@ -134,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: @@ -156,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": @@ -189,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. """ @@ -218,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. """ @@ -363,8 +355,10 @@ 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"), - "warn", + StrMatching( + "Server TLS handshake failed. Certificate verify failed: [Hh]ostname mismatch" + ), + WARNING, ) << tls.TlsFailedServerHook(tls_hook_data) >> tutils.reply() @@ -372,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 @@ -396,7 +393,7 @@ def test_remote_speaks_no_tls(self, tctx): >> events.DataReceived(tctx.server, b"HTTP/1.1 404 Not Found\r\n") << commands.Log( "Server TLS handshake failed. The remote server does not speak TLS.", - "warn", + WARNING, ) << tls.TlsFailedServerHook(tls_hook_data) >> tutils.reply() @@ -436,7 +433,7 @@ def test_unsupported_protocol(self, tctx: context.Context): << commands.Log( "Server TLS handshake failed. The remote server and mitmproxy cannot agree on a TLS version" " to use. You may need to adjust mitmproxy's tls_version_server_min option.", - "warn", + WARNING, ) << tls.TlsFailedServerHook(tls_hook_data) >> tutils.reply() @@ -499,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") @@ -621,7 +618,7 @@ def test_cannot_parse_clienthello(self, tctx: context.Context): >> events.DataReceived(tctx.client, invalid) << commands.Log( f"Client TLS handshake failed. Cannot parse ClientHello: {invalid.hex()}", - level="warn", + level=WARNING, ) << tls.TlsFailedClientHook(tls_hook_data) >> tutils.reply() @@ -634,11 +631,11 @@ def test_cannot_parse_clienthello(self, tctx: context.Context): client_layer.debug = "" assert ( playbook - >> events.DataReceived(Server(None), b"data on other stream") - << commands.Log(">> DataReceived(server, b'data on other stream')", "debug") + >> 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.", - "debug", + "[tls] Swallowing DataReceived(server, b'data on other stream') as handshake failed.", + DEBUG, ) ) @@ -670,7 +667,7 @@ def test_mitmproxy_ca_is_untrusted(self, tctx: context.Context): << commands.Log( "Client TLS handshake failed. The client does not trust the proxy's certificate " "for wrong.host.mitmproxy.org (sslv3 alert bad certificate)", - "warn", + WARNING, ) << tls.TlsFailedClientHook(tls_hook_data) >> tutils.reply() @@ -733,8 +730,7 @@ def test_immediate_disconnect(self, tctx: context.Context, close_at): << commands.Log( "Client TLS handshake failed. The client disconnected during the handshake. " "If this happens consistently for wrong.host.mitmproxy.org, this may indicate that the " - "client does not trust the proxy's certificate.", - "info", + "client does not trust the proxy's certificate." ) << tls.TlsFailedClientHook(tls_hook_data) >> tutils.reply() @@ -760,10 +756,76 @@ def test_unsupported_protocol(self, tctx: context.Context): << commands.Log( "Client TLS handshake failed. Client and mitmproxy cannot agree on a TLS version to " "use. You may need to adjust mitmproxy's tls_version_client_min option.", - "warn", + WARNING, ) << tls.TlsFailedClientHook(tls_hook_data) >> tutils.reply() << commands.CloseConnection(tctx.client) ) assert tls_hook_data().conn.error + + +def test_dtls_record_contents(): + 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])) == [] + + +def test__dtls_record_contents_err(): + with pytest.raises(ValueError, match="Expected DTLS record"): + next(tls.dtls_handshake_record_contents(b"GET /this-will-cause-error")) + + empty_record = bytes.fromhex("16fefd00000000000000000000") + with pytest.raises(ValueError, match="Record must not be empty"): + next(tls.dtls_handshake_record_contents(empty_record)) + + +dtls_client_hello_no_extensions = bytes.fromhex( + "010000360000000000000036fefd62be32f048777da890ddd213b0cb8dc3e2903f88dda1cd5f67808e1169110e840000000" + "cc02bc02fc00ac014c02cc03001000000" +) +dtls_client_hello_with_extensions = bytes.fromhex( + "16fefd00000000000000000085" # record layer + "010000790000000000000079" # hanshake layer + "fefd62bf0e0bf809df43e7669197be831919878b1a72c07a584d3c0a8ca6665878010000000cc02bc02fc00ac014c02cc0" + "3001000043000d0010000e0403050306030401050106010807ff01000100000a00080006001d00170018000b00020100001" + "7000000000010000e00000b6578616d706c652e636f6d" +) + + +def test_dtls_get_client_hello(): + 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:] + ) + 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[: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" + ) 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 new file mode 100644 index 0000000000..9b8d3b419f --- /dev/null +++ b/test/mitmproxy/proxy/layers/test_udp.py @@ -0,0 +1,130 @@ +import pytest + +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 +from mitmproxy.udp import UDPMessage + + +def test_open_connection(tctx): + """ + If there is no server connection yet, establish one, + because the server may send data first. + """ + assert Playbook(udp.UDPLayer(tctx, True)) << OpenConnection(tctx.server) + + tctx.server.timestamp_start = 1624544785 + assert Playbook(udp.UDPLayer(tctx, True)) << None + + +def test_open_connection_err(tctx): + f = Placeholder(UDPFlow) + assert ( + Playbook(udp.UDPLayer(tctx)) + << udp.UdpStartHook(f) + >> reply() + << OpenConnection(tctx.server) + >> reply("Connect call failed") + << udp.UdpErrorHook(f) + >> reply() + << CloseConnection(tctx.client) + ) + + +def test_simple(tctx): + """open connection, receive data, send it to peer""" + f = Placeholder(UDPFlow) + + assert ( + Playbook(udp.UDPLayer(tctx)) + << udp.UdpStartHook(f) + >> reply() + << OpenConnection(tctx.server) + >> reply(None) + >> DataReceived(tctx.client, b"hello!") + << udp.UdpMessageHook(f) + >> reply() + << SendData(tctx.server, b"hello!") + >> DataReceived(tctx.server, b"hi") + << udp.UdpMessageHook(f) + >> reply() + << SendData(tctx.client, b"hi") + >> ConnectionClosed(tctx.server) + << CloseConnection(tctx.client) + >> ConnectionClosed(tctx.client) + << udp.UdpEndHook(f) + >> reply() + >> DataReceived(tctx.server, b"ignored") + << None + ) + assert len(f().messages) == 2 + + +def test_receive_data_before_server_connected(tctx): + """ + assert that data received before a server connection is established + will still be forwarded. + """ + assert ( + Playbook(udp.UDPLayer(tctx), hooks=False) + << OpenConnection(tctx.server) + >> DataReceived(tctx.client, b"hello!") + >> reply(None, to=-2) + << SendData(tctx.server, b"hello!") + ) + + +@pytest.mark.parametrize("ignore", [True, False]) +def test_ignore(tctx, ignore): + """ + no flow hooks when we set ignore. + """ + + def no_flow_hooks(): + assert ( + Playbook(udp.UDPLayer(tctx, ignore=ignore), hooks=True) + << OpenConnection(tctx.server) + >> reply(None) + >> DataReceived(tctx.client, b"hello!") + << SendData(tctx.server, b"hello!") + ) + + if ignore: + no_flow_hooks() + else: + with pytest.raises(AssertionError): + no_flow_hooks() + + +def test_inject(tctx): + """inject data into an open connection.""" + f = Placeholder(UDPFlow) + + assert ( + Playbook(udp.UDPLayer(tctx)) + << udp.UdpStartHook(f) + >> UdpMessageInjected(f, UDPMessage(True, b"hello!")) + >> reply(to=-2) + << OpenConnection(tctx.server) + >> reply(None) + << udp.UdpMessageHook(f) + >> reply() + << SendData(tctx.server, b"hello!") + # and the other way... + >> UdpMessageInjected( + f, UDPMessage(False, b"I have already done the greeting for you.") + ) + << udp.UdpMessageHook(f) + >> reply() + << SendData(tctx.client, b"I have already done the greeting for you.") + << None + ) + assert len(f().messages) == 2 diff --git a/test/mitmproxy/proxy/layers/test_websocket.py b/test/mitmproxy/proxy/layers/test_websocket.py index a1d96133f4..eebca259dd 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 diff --git a/test/mitmproxy/proxy/test_commands.py b/test/mitmproxy/proxy/test_commands.py index 2ca7c23f0a..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,8 +17,8 @@ def test_dataclasses(tconn): assert repr(commands.SendData(tconn, b"foo")) assert repr(commands.OpenConnection(tconn)) assert repr(commands.CloseConnection(tconn)) - assert repr(commands.GetSocket(tconn)) - assert repr(commands.Log("hello", "info")) + assert repr(commands.CloseTcpConnection(tconn, half_close=True)) + assert repr(commands.Log("hello")) def test_start_hook(): 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 bd9ce7de3b..b646c2e027 100644 --- a/test/mitmproxy/proxy/test_layer.py +++ b/test/mitmproxy/proxy/test_layer.py @@ -1,6 +1,10 @@ +from logging import DEBUG + 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 @@ -42,29 +46,27 @@ def state_foo(self, event: events.Event) -> layer.CommandGenerator[None]: def state_bar(self, event: events.Event) -> layer.CommandGenerator[None]: assert isinstance(event, events.DataReceived) - yield commands.Log("baz", "info") + yield commands.Log("baz") tlayer = TLayer(tctx) assert ( tutils.Playbook(tlayer, hooks=True, logs=True) - << commands.Log(" >> Start({})", "debug") + << commands.Log(" >> Start({})", DEBUG) << commands.Log( - " << OpenConnection({'connection': Server({'id': '…rverid', 'address': None, " - "'state': , 'transport_protocol': 'tcp'})})", - "debug", + " << OpenConnection({'connection': Server({'id': '…rverid', 'address': None})})", + DEBUG, ) << commands.OpenConnection(tctx.server) >> events.DataReceived(tctx.client, b"foo") - << commands.Log(" >! DataReceived(client, b'foo')", "debug") + << 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)", - "debug", + " >> Reply(OpenConnection({'connection': Server({'id': '…rverid', 'address': None, " + "'state': , 'timestamp_start': 1624544785})}), None)", + DEBUG, ) - << commands.Log(" !> DataReceived(client, b'foo')", "debug") - << commands.Log("baz", "info") + << commands.Log(" !> DataReceived(client, b'foo')", DEBUG) + << commands.Log("baz") ) assert repr(tlayer) == "TLayer(state: bar)" diff --git a/test/mitmproxy/proxy/test_mode_servers.py b/test/mitmproxy/proxy/test_mode_servers.py new file mode 100644 index 0000000000..274eabe425 --- /dev/null +++ b/test/mitmproxy/proxy/test_mode_servers.py @@ -0,0 +1,414 @@ +import asyncio +import platform +from typing import cast +from unittest.mock import AsyncMock +from unittest.mock import MagicMock +from unittest.mock import Mock + +import mitmproxy_rs +import pytest + +import mitmproxy.platform +from mitmproxy.addons.proxyserver import Proxyserver +from mitmproxy.net import udp +from mitmproxy.proxy.mode_servers import DnsInstance +from mitmproxy.proxy.mode_servers import LocalRedirectorInstance +from mitmproxy.proxy.mode_servers import ServerInstance +from mitmproxy.proxy.mode_servers import WireGuardServerInstance +from mitmproxy.proxy.server import ConnectionHandler +from mitmproxy.test import taddons + + +def test_make(): + manager = Mock() + context = MagicMock() + assert ServerInstance.make("regular", manager) + + 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." + ): + WireGuardServerInstance.make("regular", manager) + + +async def test_last_exception_and_running(monkeypatch): + manager = MagicMock() + err = ValueError("something else") + + def _raise(*_): + nonlocal err + raise err + + async def _raise_async(*_): + nonlocal err + raise err + + with taddons.context(): + inst1 = ServerInstance.make("regular@127.0.0.1:0", manager) + await inst1.start() + assert inst1.last_exception is None + assert inst1.is_running + 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_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)): + await inst2.start() + assert inst2.last_exception is err + assert not inst1.is_running + + +async def test_tcp_start_stop(caplog_async): + caplog_async.set_level("INFO") + manager = MagicMock() + + with taddons.context(): + inst = ServerInstance.make("regular@127.0.0.1:0", manager) + await inst.start() + assert inst.last_exception is None + assert await caplog_async.await_log("proxy listening") + + host, port, *_ = inst.listen_addrs[0] + reader, writer = await asyncio.open_connection(host, port) + assert await caplog_async.await_log("client connect") + + writer.close() + await writer.wait_closed() + assert await caplog_async.await_log("client disconnect") + + await inst.stop() + assert await caplog_async.await_log("stopped") + + +@pytest.mark.parametrize("failure", [True, False]) +async def test_transparent(failure, monkeypatch, caplog_async): + caplog_async.set_level("INFO") + manager = MagicMock() + + if failure: + monkeypatch.setattr(mitmproxy.platform, "original_addr", None) + else: + 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("listening") + + host, port, *_ = inst.listen_addrs[0] + reader, writer = await asyncio.open_connection(host, port) + + if failure: + assert await caplog_async.await_log("Transparent mode failure") + writer.close() + await writer.wait_closed() + else: + assert await caplog_async.await_log("client connect") + writer.close() + await writer.wait_closed() + assert await caplog_async.await_log("client disconnect") + + await inst.stop() + assert await caplog_async.await_log("stopped") + + +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) + + system = platform.system() + if system == "Linux": + test_client_name = "linux-x86_64" + elif system == "Darwin": + test_client_name = "macos-x86_64" + elif system == "Windows": + test_client_name = "windows-x86_64.exe" + 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") + + with taddons.context(Proxyserver()): + inst = WireGuardServerInstance.make(f"wireguard:{test_conf}@0", MagicMock()) + + await inst.start() + assert "WireGuard server listening" in caplog.text + + _, port = inst.listen_addrs[0] + + assert inst.is_running + proc = await asyncio.create_subprocess_exec( + test_client_path, + str(port), + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await proc.communicate() + + try: + assert proc.returncode == 0 + except AssertionError: + print(stdout) + print(stderr) + raise + + await inst.stop() + assert "stopped" in caplog.text + + +async def test_wireguard_generate_conf(tmp_path): + with taddons.context(Proxyserver()) as tctx: + tctx.options.confdir = str(tmp_path) + inst = WireGuardServerInstance.make(f"wireguard@0", MagicMock()) + assert not inst.client_conf() # should not error. + + await inst.start() + + assert (tmp_path / "wireguard.conf").exists() + assert inst.client_conf() + assert inst.to_json()["wireguard_conf"] + k = inst.server_key + + inst2 = WireGuardServerInstance.make(f"wireguard@0", MagicMock()) + await inst2.start() + assert k == inst2.server_key + + await inst.stop() + await inst2.stop() + + +async def test_wireguard_invalid_conf(tmp_path): + with taddons.context(Proxyserver()): + # directory instead of filename + inst = WireGuardServerInstance.make(f"wireguard:{tmp_path}", MagicMock()) + + with pytest.raises(ValueError, match="Invalid configuration file"): + await inst.start() + + assert "Invalid configuration file" in repr(inst.last_exception) + + +async def test_tcp_start_error(): + manager = MagicMock() + + 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}" + ): + await inst.start() + tctx.options.listen_host = "127.0.0.1" + tctx.options.listen_port = port + inst3 = ServerInstance.make(f"regular", manager) + with pytest.raises(OSError): + await inst3.start() + + +async def test_invalid_protocol(monkeypatch): + manager = MagicMock() + + with taddons.context(): + inst = ServerInstance.make(f"regular@127.0.0.1:0", manager) + monkeypatch.setattr(inst.mode, "transport_protocol", "invalid_proto") + with pytest.raises(AssertionError, match=f"invalid_proto"): + await inst.start() + + +async def test_udp_start_stop(caplog_async): + 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] + reader, writer = await udp.open_connection(host, port) + + writer.write(b"\x00\x00\x01") + assert await caplog_async.await_log("sent an invalid message") + + writer.close() + + await inst.stop() + 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) + 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}" + ): + await inst2.start() + await inst.stop() + + +async def test_udp_connection_reuse(monkeypatch): + manager = MagicMock() + manager.connections = {} + + monkeypatch.setattr(udp, "DatagramWriter", MagicMock()) + monkeypatch.setattr(DnsInstance, "handle_udp_connection", AsyncMock()) + + 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) + + assert len(inst.manager.connections) == 1 + + +async def test_udp_dual_stack(caplog_async): + caplog_async.set_level("DEBUG") + manager = MagicMock() + manager.connections = {} + + 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] + reader, writer = await udp.open_connection("127.0.0.1", port) + writer.write(b"\x00\x00\x01") + assert await caplog_async.await_log("sent an invalid message") + writer.close() + + if "listening on IPv4 only" not in caplog_async.caplog.text: + caplog_async.clear() + reader, writer = await udp.open_connection("::1", port) + writer.write(b"\x00\x00\x01") + assert await caplog_async.await_log("sent an invalid message") + writer.close() + + await inst.stop() + assert await caplog_async.await_log("stopped") + + +@pytest.fixture() +def patched_local_redirector(monkeypatch): + start_local_redirector = AsyncMock() + monkeypatch.setattr(mitmproxy_rs, "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 = 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 = ServerInstance.make(f"local:curl", manager) + await inst1.start() + await inst1.stop() + + handle_tcp, handle_udp = patched_local_redirector.await_args[0] + + inst2 = ServerInstance.make(f"local:wget", manager) + await inst2.start() + + monkeypatch.setattr(inst2, "handle_tcp_connection", h_tcp := AsyncMock()) + await handle_tcp(Mock()) + assert h_tcp.await_count + + monkeypatch.setattr(inst2, "handle_udp_datagram", h_udp := Mock()) + handle_udp(Mock(), b"", ("", 0), ("", 0)) + assert h_udp.called diff --git a/test/mitmproxy/proxy/test_mode_specs.py b/test/mitmproxy/proxy/test_mode_specs.py new file mode 100644 index 0000000000..d5e4ad29db --- /dev/null +++ b/test/mitmproxy/proxy/test_mode_specs.py @@ -0,0 +1,94 @@ +import dataclasses + +import pytest + +from mitmproxy.proxy.mode_specs import ProxyMode +from mitmproxy.proxy.mode_specs import Socks5Mode + + +def test_parse(): + m = ProxyMode.parse("reverse:https://example.com/@127.0.0.1:443") + m = ProxyMode.from_state(m.get_state()) + + assert m.type_name == "reverse" + assert m.full_spec == "reverse:https://example.com/@127.0.0.1:443" + assert m.data == "https://example.com/" + assert m.custom_listen_host == "127.0.0.1" + assert m.custom_listen_port == 443 + assert repr(m) == "ProxyMode.parse('reverse:https://example.com/@127.0.0.1:443')" + + with pytest.raises(ValueError, match="unknown mode"): + ProxyMode.parse("flibbel") + + with pytest.raises(ValueError, match="invalid port"): + ProxyMode.parse("regular@invalid-port") + + with pytest.raises(ValueError, match="invalid port"): + ProxyMode.parse("regular@99999") + + m.set_state(m.get_state()) + with pytest.raises(dataclasses.FrozenInstanceError): + m.set_state("regular") + + +def test_parse_subclass(): + assert Socks5Mode.parse("socks5") + with pytest.raises(ValueError, match="'regular' is not a spec for a socks5 mode"): + Socks5Mode.parse("regular") + + +def test_listen_addr(): + assert ProxyMode.parse("regular").listen_port() == 8080 + 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("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("reverse:https://1.2.3.4").listen_port() == 8080 + assert ProxyMode.parse("reverse:dns://8.8.8.8").listen_port() == 53 + + +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") + assert ProxyMode.parse("reverse:dtls://127.0.0.1:8004") + assert ProxyMode.parse("wireguard") + assert ProxyMode.parse("wireguard:foo.conf").data == "foo.conf" + assert ProxyMode.parse("wireguard@51821").listen_port() == 51821 + + assert ProxyMode.parse("local") + + with pytest.raises(ValueError, match="invalid port"): + ProxyMode.parse("regular@invalid-port") + + 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") + + with pytest.raises(ValueError, match="takes no arguments"): + ProxyMode.parse("dns:invalid") + + 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_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..ec676405b3 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): diff --git a/test/mitmproxy/proxy/tutils.py b/test/mitmproxy/proxy/tutils.py index 0f78aabf7a..22543e397d 100644 --- a/test/mitmproxy/proxy/tutils.py +++ b/test/mitmproxy/proxy/tutils.py @@ -1,15 +1,23 @@ import collections.abc import difflib 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 @@ -48,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. @@ -137,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()] @@ -222,15 +230,14 @@ 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 - and cmd.level in ("debug", "info") + and cmd.level in (logging.DEBUG, logging.INFO) and ( pos >= len(self.expected) or not isinstance(self.expected[pos], commands.Log) @@ -291,13 +298,13 @@ def __del__(self): class reply(events.Event): args: tuple[Any, ...] - to: Union[commands.Command, int] + to: commands.Command | int side_effect: Callable[[Any], Any] def __init__( self, *args, - to: Union[commands.Command, int] = -1, + to: commands.Command | int = -1, side_effect: Callable[[Any], None] = lambda x: None, ): """Utility method to reply to the latest hook in playbooks.""" @@ -379,7 +386,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) @@ -397,12 +404,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) @@ -431,7 +438,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 eb23f6ebc8..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: @@ -28,9 +28,9 @@ async def test_concurrent(self, addon, tdata): else: assert 0.5 <= end - start < 1 - async def test_concurrent_err(self, tdata): + def test_concurrent_err(self, tdata, caplog): with taddons.context() as tctx: tctx.script( tdata.path("mitmproxy/data/addonscripts/concurrent_decorator_err.py") ) - await tctx.master.await_log("decorator not supported") + assert "decorator not supported" in caplog.text diff --git a/test/mitmproxy/test_addonmanager.py b/test/mitmproxy/test_addonmanager.py index 1686d4923a..c76187ffa6 100644 --- a/test/mitmproxy/test_addonmanager.py +++ b/test/mitmproxy/test_addonmanager.py @@ -1,5 +1,3 @@ -from unittest import mock - import pytest from mitmproxy import addonmanager @@ -9,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 @@ -55,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: @@ -112,6 +112,8 @@ async def test_lifecycle(): a = addonmanager.AddonManager(m) a.add(TAddon("one")) + assert str(a) + with pytest.raises(exceptions.AddonManagerError): a.add(TAddon("one")) with pytest.raises(exceptions.AddonManagerError): @@ -120,14 +122,14 @@ async def test_lifecycle(): f = tflow.tflow() await a.handle_lifecycle(HttpRequestHook(f)) - a._configure_all(o, o.keys()) + a._configure_all(o.keys()) def test_defaults(): assert addons.default_addons() -async def test_mixed_async_sync(): +async def test_mixed_async_sync(caplog): with taddons.context(loadcore=False) as tctx: a = tctx.master.addons @@ -149,31 +151,30 @@ async def test_mixed_async_sync(): a2.running_called = False a.trigger(hooks.RunningHook()) assert a1.running_called - await tctx.master.await_log("called from sync context") + assert "called from sync context" in caplog.text -async def test_loader(): +async def test_loader(caplog): with taddons.context() as tctx: - with mock.patch("mitmproxy.ctx.log.warn") as warn: - 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") - assert not warn.called + # calling this again with the same signature is a no-op. + 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") - assert warn.called + # a different signature should emit a warning though. + loader.add_option("custom_option", bool, True, "help") + assert "Over-riding existing option" in caplog.text - def cmd(a: str) -> str: - return "foo" + def cmd(a: str) -> str: + return "foo" - l.add_command("test.command", cmd) + loader.add_command("test.command", cmd) -async def test_simple(): +async def test_simple(caplog): with taddons.context(loadcore=False) as tctx: a = tctx.master.addons @@ -186,21 +187,22 @@ async def test_simple(): assert len(a) == 0 assert not a.chain + with taddons.context(loadcore=False) as tctx: a.add(TAddon("one")) a.trigger("nonexistent") - await tctx.master.await_log("AssertionError") + assert "AssertionError" in caplog.text f = tflow.tflow() a.trigger(hooks.RunningHook()) a.trigger(HttpResponseHook(f)) - await tctx.master.await_log("not callable") + assert "not callable" in caplog.text + caplog.clear() - tctx.master.clear() + caplog.clear() a.get("one").response = addons a.trigger(HttpResponseHook(f)) - with pytest.raises(AssertionError): - await tctx.master.await_log("not callable", timeout=0.01) + assert "not callable" not in caplog.text a.remove(a.get("one")) assert not a.get("one") @@ -246,7 +248,7 @@ async def test_nesting(): assert not a.get("four") -async def test_old_api(): +async def test_old_api(caplog): with taddons.context(loadcore=False) as tctx: tctx.master.addons.add(AOldAPI()) - await tctx.master.await_log("clientconnect event has been removed") + assert "clientconnect event has been removed" in caplog.text diff --git a/test/mitmproxy/test_certs.py b/test/mitmproxy/test_certs.py index 7ccb52d5c2..d3c33727cb 100644 --- a/test/mitmproxy/test_certs.py +++ b/test/mitmproxy/test_certs.py @@ -1,14 +1,14 @@ 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,10 +65,12 @@ 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"]) @@ -138,11 +140,17 @@ 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"], + ["one.com", "two.com", "*.three.com", "127.0.0.1", "bücher.example"], "Foo Ltd.", ) assert r.cn == "foo.com" - assert r.altnames == ["one.com", "two.com", "*.three.com", "127.0.0.1"] + assert r.altnames == [ + "one.com", + "two.com", + "*.three.com", + "xn--bcher-kva.example", + "127.0.0.1", + ] assert r.organization == "Foo Ltd." r = certs.dummy_cert( diff --git a/test/mitmproxy/test_command.py b/test/mitmproxy/test_command.py index 614e4d7d4d..247a3e3900 100644 --- a/test/mitmproxy/test_command.py +++ b/test/mitmproxy/test_command.py @@ -652,7 +652,7 @@ def empty(self) -> None: pass -async def test_collect_commands(): +async def test_collect_commands(caplog): """ This tests for errors thrown by getattr() or __getattr__ implementations that return an object for .command_name. @@ -665,7 +665,7 @@ async def test_collect_commands(): a = TypeErrAddon() c.collect_commands(a) - await tctx.master.await_log("Could not load") + assert "Could not load" in caplog.text def test_decorator(): 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..038f0ddb08 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,9 @@ 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 😀" + ) assert ( str( dns.ResourceRecord( @@ -111,7 +114,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) diff --git a/test/mitmproxy/test_eventsequence.py b/test/mitmproxy/test_eventsequence.py index 57330f80f5..ac9ebd97ea 100644 --- a/test/mitmproxy/test_eventsequence.py +++ b/test/mitmproxy/test_eventsequence.py @@ -62,6 +62,22 @@ def test_tcp_flow(err): assert isinstance(next(i), layers.tcp.TcpEndHook) +@pytest.mark.parametrize("err", [False, True]) +def test_udp_flow(err): + f = tflow.tudpflow(err=err) + i = eventsequence.iterate(f) + assert isinstance(next(i), layers.udp.UdpStartHook) + assert len(f.messages) == 0 + assert isinstance(next(i), layers.udp.UdpMessageHook) + assert len(f.messages) == 1 + assert isinstance(next(i), layers.udp.UdpMessageHook) + assert len(f.messages) == 2 + if err: + assert isinstance(next(i), layers.udp.UdpErrorHook) + else: + assert isinstance(next(i), layers.udp.UdpEndHook) + + @pytest.mark.parametrize( "resp, err", [ diff --git a/test/mitmproxy/test_flow.py b/test/mitmproxy/test_flow.py index 55beb7d42d..5aa171f3e4 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,10 +44,10 @@ 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 @@ -114,7 +116,7 @@ def test_copy(self): class TestFlowMaster: async def test_load_http_flow_reverse(self): - opts = options.Options(mode="reverse:https://use-this-domain") + opts = options.Options(mode=["reverse:https://use-this-domain"]) s = State() with taddons.context(s, options=opts) as ctx: f = tflow.tflow(resp=True) @@ -122,7 +124,7 @@ async def test_load_http_flow_reverse(self): assert s.flows[0].request.host == "use-this-domain" async def test_all(self): - opts = options.Options(mode="reverse:https://use-this-domain") + opts = options.Options(mode=["reverse:https://use-this-domain"]) s = State() with taddons.context(s, options=opts) as ctx: f = tflow.tflow(req=None) diff --git a/test/mitmproxy/test_flowfilter.py b/test/mitmproxy/test_flowfilter.py index a28540f4db..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: @@ -400,6 +403,132 @@ def q(self, q, o): def test_tcp(self): f = self.flow() assert self.q("~tcp", f) + assert not self.q("~udp", f) + assert not self.q("~http", f) + assert not self.q("~websocket", f) + + def test_ferr(self): + e = self.err() + assert self.q("~e", e) + + def test_body(self): + f = self.flow() + + # Messages sent by client or server + assert self.q("~b hello", f) + assert self.q("~b me", f) + assert not self.q("~b nonexistent", f) + + # Messages sent by client + assert self.q("~bq hello", f) + assert not self.q("~bq me", f) + assert not self.q("~bq nonexistent", f) + + # Messages sent by server + assert self.q("~bs me", f) + assert not self.q("~bs hello", f) + assert not self.q("~bs nonexistent", f) + + def test_src(self): + f = self.flow() + assert self.q("~src 127.0.0.1", f) + assert not self.q("~src foobar", f) + assert self.q("~src :22", f) + assert not self.q("~src :99", f) + assert self.q("~src 127.0.0.1:22", f) + + def test_dst(self): + f = self.flow() + f.server_conn = tflow.tserver_conn() + assert self.q("~dst address", f) + assert not self.q("~dst foobar", f) + assert self.q("~dst :22", f) + assert not self.q("~dst :99", f) + assert self.q("~dst address:22", f) + + def test_and(self): + f = self.flow() + f.server_conn = tflow.tserver_conn() + assert self.q("~b hello & ~b me", f) + assert not self.q("~src wrongaddress & ~b hello", f) + assert self.q("(~src :22 & ~dst :22) & ~b hello", f) + assert not self.q("(~src address:22 & ~dst :22) & ~b nonexistent", f) + assert not self.q("(~src address:22 & ~dst :99) & ~b hello", f) + + def test_or(self): + f = self.flow() + f.server_conn = tflow.tserver_conn() + assert self.q("~b hello | ~b me", f) + assert self.q("~src :22 | ~b me", f) + assert not self.q("~src :99 | ~dst :99", f) + assert self.q("(~src :22 | ~dst :22) | ~b me", f) + + def test_not(self): + f = self.flow() + assert not self.q("! ~src :22", f) + assert self.q("! ~src :99", f) + assert self.q("!~src :99 !~src :99", f) + assert not self.q("!~src :99 !~src :22", f) + + def test_request(self): + f = self.flow() + assert not self.q("~q", f) + + def test_response(self): + f = self.flow() + assert not self.q("~s", f) + + def test_headers(self): + f = self.flow() + assert not self.q("~h whatever", f) + + # Request headers + assert not self.q("~hq whatever", f) + + # Response headers + assert not self.q("~hs whatever", f) + + def test_content_type(self): + f = self.flow() + assert not self.q("~t whatever", f) + + # Request content-type + assert not self.q("~tq whatever", f) + + # Response content-type + assert not self.q("~ts whatever", f) + + def test_code(self): + f = self.flow() + assert not self.q("~c 200", f) + + def test_domain(self): + f = self.flow() + assert not self.q("~d whatever", f) + + def test_method(self): + f = self.flow() + assert not self.q("~m whatever", f) + + def test_url(self): + f = self.flow() + assert not self.q("~u whatever", f) + + +class TestMatchingUDPFlow: + def flow(self): + return tflow.tudpflow() + + def err(self): + return tflow.tudpflow(err=True) + + def q(self, q, o): + return flowfilter.parse(q)(o) + + def test_udp(self): + f = self.flow() + assert self.q("~udp", f) + assert not self.q("~tcp", f) assert not self.q("~http", f) assert not self.q("~websocket", f) diff --git a/test/mitmproxy/test_http.py b/test/mitmproxy/test_http.py index c44bde049a..fecf09e7ba 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") @@ -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() @@ -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): @@ -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() diff --git a/test/mitmproxy/test_master.py b/test/mitmproxy/test_master.py index f74fe68e2c..df7cea8360 100644 --- a/test/mitmproxy/test_master.py +++ b/test/mitmproxy/test_master.py @@ -1,16 +1,23 @@ import asyncio -from mitmproxy.test.taddons import RecordingMaster +from mitmproxy.master import Master async def err(): raise RuntimeError -async def test_exception_handler(): - m = RecordingMaster(None) +class TaskError: + def running(self): + # not assigned to anything + asyncio.create_task(err()) + + +async def test_exception_handler(caplog_async): + caplog_async.set_level("ERROR") + m = Master(None) + m.addons.add(TaskError()) running = asyncio.create_task(m.run()) - asyncio.create_task(err()) - await m.await_log("Traceback", level="error") + await caplog_async.await_log("Traceback") m.shutdown() await running diff --git a/test/mitmproxy/test_optmanager.py b/test/mitmproxy/test_optmanager.py index 769959dc47..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 = { @@ -100,8 +108,8 @@ def test_options(): rec = [] - def sub(opts, updated): - rec.append(copy.copy(opts)) + def sub(updated): + rec.append(copy.copy(o)) o.changed.connect(sub) @@ -159,7 +167,7 @@ def test_subscribe(): else: raise AssertionError - assert len(o.changed.receivers) == 0 + assert len(o._subscriptions) == 0 o.subscribe(r, ["two"]) o.one = 2 @@ -170,7 +178,7 @@ def test_subscribe(): assert len(o.changed.receivers) == 1 del r o.two = 4 - assert len(o.changed.receivers) == 0 + assert len(o._subscriptions) == 0 class binder: def __init__(self): @@ -193,18 +201,18 @@ def test_rollback(): rec = [] - def sub(opts, updated): - rec.append(copy.copy(opts)) + def sub(updated): + rec.append(copy.copy(o)) recerr = [] - def errsub(opts, **kwargs): + def errsub(**kwargs): recerr.append(kwargs) - def err(opts, updated): - if opts.one == 10: + def err(updated): + if o.one == 10: raise exceptions.OptionsError() - if opts.bool is True: + if o.bool is True: raise exceptions.OptionsError() o.changed.connect(sub) @@ -233,8 +241,12 @@ def err(opts, updated): def test_simple(): - assert repr(TO()) - assert "one" in TO() + o = TO() + assert repr(o) + assert "one" in o + + with pytest.raises(Exception, match="No such option"): + assert o.unknown def test_items(): @@ -298,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() @@ -465,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_taddons.py b/test/mitmproxy/test_taddons.py index 6f0a76b9ea..4eb9a86868 100644 --- a/test/mitmproxy/test_taddons.py +++ b/test/mitmproxy/test_taddons.py @@ -1,24 +1,4 @@ -import io - from mitmproxy.test import taddons -from mitmproxy import ctx - - -async def test_recordingmaster(): - with taddons.context() as tctx: - assert not tctx.master.has_log("nonexistent") - ctx.log.error("foo") - assert not tctx.master.has_log("foo", level="debug") - await tctx.master.await_log("foo", level="error") - - -async def test_dumplog(): - with taddons.context() as tctx: - ctx.log.info("testing") - await tctx.master.await_log("testing") - s = io.StringIO() - tctx.master.dump_log(s) - assert s.getvalue() def test_load_script(tdata): 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 5cfc0cf86c..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" @@ -70,3 +69,44 @@ def test_extensions(self): (11, b"\x01\x00"), (10, b"\x00\x06\x00\x1d\x00\x17\x00\x18"), ] + + +DTLS_CLIENT_HELLO_NO_EXTENSIONS = bytes.fromhex( + # No Record or Handshake layer header + "fefd62bf5560a83b2525186d38fb6459837656d7f11" + "fb630cd44683bb9d9681204c50000000c00020003000a00050004000901000000" +) + + +class TestDTLSClientHello: + def test_no_extensions(self): + c = tls.ClientHello(DTLS_CLIENT_HELLO_NO_EXTENSIONS, dtls=True) + assert repr(c) + assert c.sni is None + assert c.cipher_suites == [2, 3, 10, 5, 4, 9] + assert c.alpn_protocols == [] + assert c.extensions == [] + + def test_extensions(self): + # No Record or Handshake layer header + data = bytes.fromhex( + "fefd62bf60ba96532f63c4e53196174ff5016d949420d7f970a6b08a9e2a5a8209af0000" + "000c00020003000a000500040009" + "01000055000d0010000e0403050306030401050106010807ff01000100000a00080006001d" + "00170018000b000201000017000000000010000e00000b6578616d706c652e636f6d0010000e" + "000c02683208687474702f312e31" + ) + c = tls.ClientHello(data, dtls=True) + assert repr(c) + assert c.sni == "example.com" + 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"), + ] diff --git a/test/mitmproxy/test_types.py b/test/mitmproxy/test_types.py index 04eaf4d1c9..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 @@ -30,7 +29,7 @@ def test_bool(): assert b.parse(tctx.master.commands, bool, "false") is False assert b.is_valid(tctx.master.commands, bool, True) is True assert b.is_valid(tctx.master.commands, bool, "foo") is False - with pytest.raises(mitmproxy.exceptions.TypeError): + with pytest.raises(ValueError): b.parse(tctx.master.commands, bool, "foo") @@ -43,7 +42,7 @@ def test_str(): assert b.parse(tctx.master.commands, str, "foo") == "foo" assert b.parse(tctx.master.commands, str, r"foo\nbar") == "foo\nbar" assert b.parse(tctx.master.commands, str, r"\N{BELL}") == "🔔" - with pytest.raises(mitmproxy.exceptions.TypeError): + with pytest.raises(ValueError): b.parse(tctx.master.commands, bool, r"\N{UNKNOWN UNICODE SYMBOL!}") @@ -54,7 +53,7 @@ def test_bytes(): assert b.is_valid(tctx.master.commands, bytes, 1) is False assert b.completion(tctx.master.commands, bytes, "") == [] assert b.parse(tctx.master.commands, bytes, "foo") == b"foo" - with pytest.raises(mitmproxy.exceptions.TypeError): + with pytest.raises(ValueError): b.parse(tctx.master.commands, bytes, "incomplete escape sequence\\") @@ -75,7 +74,7 @@ def test_int(): assert b.completion(tctx.master.commands, int, "b") == [] assert b.parse(tctx.master.commands, int, "1") == 1 assert b.parse(tctx.master.commands, int, "999") == 999 - with pytest.raises(mitmproxy.exceptions.TypeError): + with pytest.raises(ValueError): b.parse(tctx.master.commands, int, "foo") @@ -120,7 +119,7 @@ def test_cmd(): assert b.is_valid(tctx.master.commands, mitmproxy.types.Cmd, "foo") is False assert b.is_valid(tctx.master.commands, mitmproxy.types.Cmd, "cmd1") is True assert b.parse(tctx.master.commands, mitmproxy.types.Cmd, "cmd1") == "cmd1" - with pytest.raises(mitmproxy.exceptions.TypeError): + with pytest.raises(ValueError): assert b.parse(tctx.master.commands, mitmproxy.types.Cmd, "foo") assert len(b.completion(tctx.master.commands, mitmproxy.types.Cmd, "")) == len( tctx.master.commands.commands.keys() @@ -164,7 +163,7 @@ def test_marker(): ) assert b.parse(tctx.master.commands, mitmproxy.types.Marker, "false") == "" - with pytest.raises(mitmproxy.exceptions.TypeError): + with pytest.raises(ValueError): b.parse(tctx.master.commands, mitmproxy.types.Marker, ":bogus:") assert b.is_valid(tctx.master.commands, mitmproxy.types.Marker, "true") is True @@ -232,11 +231,11 @@ def test_flow(): assert b.parse(tctx.master.commands, flow.Flow, "has space") assert b.is_valid(tctx.master.commands, flow.Flow, tflow.tflow()) is True assert b.is_valid(tctx.master.commands, flow.Flow, "xx") is False - with pytest.raises(mitmproxy.exceptions.TypeError): + with pytest.raises(ValueError): b.parse(tctx.master.commands, flow.Flow, "0") - with pytest.raises(mitmproxy.exceptions.TypeError): + with pytest.raises(ValueError): b.parse(tctx.master.commands, flow.Flow, "2") - with pytest.raises(mitmproxy.exceptions.TypeError): + with pytest.raises(ValueError): b.parse(tctx.master.commands, flow.Flow, "err") @@ -257,7 +256,7 @@ def test_flows(): assert len(b.parse(tctx.master.commands, Sequence[flow.Flow], "1")) == 1 assert len(b.parse(tctx.master.commands, Sequence[flow.Flow], "2")) == 2 assert len(b.parse(tctx.master.commands, Sequence[flow.Flow], "has space")) == 1 - with pytest.raises(mitmproxy.exceptions.TypeError): + with pytest.raises(ValueError): b.parse(tctx.master.commands, Sequence[flow.Flow], "err") @@ -269,9 +268,9 @@ def test_data(): assert b.is_valid(tctx.master.commands, mitmproxy.types.Data, [["x"]]) is True assert b.is_valid(tctx.master.commands, mitmproxy.types.Data, [[b"x"]]) is True assert b.is_valid(tctx.master.commands, mitmproxy.types.Data, [[1]]) is False - with pytest.raises(mitmproxy.exceptions.TypeError): + with pytest.raises(ValueError): b.parse(tctx.master.commands, mitmproxy.types.Data, "foo") - with pytest.raises(mitmproxy.exceptions.TypeError): + with pytest.raises(ValueError): b.parse(tctx.master.commands, mitmproxy.types.Data, "foo") @@ -309,7 +308,7 @@ def test_choice(): b.parse(tctx.master.commands, mitmproxy.types.Choice("options"), "one") == "one" ) - with pytest.raises(mitmproxy.exceptions.TypeError): + with pytest.raises(ValueError): b.parse(tctx.master.commands, mitmproxy.types.Choice("options"), "invalid") diff --git a/test/mitmproxy/test_udp.py b/test/mitmproxy/test_udp.py new file mode 100644 index 0000000000..ba652f74f1 --- /dev/null +++ b/test/mitmproxy/test_udp.py @@ -0,0 +1,58 @@ +import pytest + +from mitmproxy import flowfilter +from mitmproxy import udp +from mitmproxy.test import tflow + + +class TestUDPFlow: + def test_copy(self): + f = tflow.tudpflow() + f.get_state() + f2 = f.copy() + a = f.get_state() + b = f2.get_state() + del a["id"] + del b["id"] + assert a == b + assert not f == f2 + assert f is not f2 + + assert f.messages is not f2.messages + + for m in f.messages: + assert m.get_state() + m2 = m.copy() + assert not m == m2 + assert m is not m2 + + a = m.get_state() + b = m2.get_state() + assert a == b + + m = udp.UDPMessage(False, "foo") + m.set_state(f.messages[0].get_state()) + assert m.timestamp == f.messages[0].timestamp + + f = tflow.tudpflow(err=True) + f2 = f.copy() + assert f is not f2 + assert f.error.get_state() == f2.error.get_state() + assert f.error is not f2.error + + def test_match(self): + f = tflow.tudpflow() + assert not flowfilter.match("~b nonexistent", f) + assert flowfilter.match(None, f) + assert not flowfilter.match("~b nonexistent", f) + + f = tflow.tudpflow(err=True) + assert flowfilter.match("~e", f) + + with pytest.raises(ValueError): + flowfilter.match("~", f) + + def test_repr(self): + f = tflow.tudpflow() + assert "UDPFlow" in repr(f) + assert "-> " in repr(f.messages[0]) diff --git a/test/mitmproxy/test_websocket.py b/test/mitmproxy/test_websocket.py index f227d0dc50..08117b0fb0 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: diff --git a/test/mitmproxy/tools/console/conftest.py b/test/mitmproxy/tools/console/conftest.py index 0be6eef146..01bbcbcb38 100644 --- a/test/mitmproxy/tools/console/conftest.py +++ b/test/mitmproxy/tools/console/conftest.py @@ -44,4 +44,5 @@ async def console(monkeypatch) -> ConsoleTestMaster: # noqa opts.server = False opts.console_mouse = False await m.running() - return m + yield m + await m.done() 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_defaultkeys.py b/test/mitmproxy/tools/console/test_defaultkeys.py index b2c44411d1..f9f2d9a2da 100644 --- a/test/mitmproxy/tools/console/test_defaultkeys.py +++ b/test/mitmproxy/tools/console/test_defaultkeys.py @@ -1,31 +1,20 @@ import mitmproxy.types -from mitmproxy import command -from mitmproxy import ctx from mitmproxy.test.tflow import tflow -from mitmproxy.tools.console import defaultkeys -from mitmproxy.tools.console import keymap -from mitmproxy.tools.console import master -async def test_commands_exist(): - command_manager = command.CommandManager(ctx) +async def test_commands_exist(console): + await console.load_flow(tflow()) - km = keymap.Keymap(None) - defaultkeys.map(km) - assert km.bindings - m = master.ConsoleMaster(None) - await m.load_flow(tflow()) - - for binding in km.bindings: + for binding in console.keymap.bindings: try: - parsed, _ = command_manager.parse_partial(binding.command.strip()) + parsed, _ = console.commands.parse_partial(binding.command.strip()) cmd = parsed[0].value args = [a.value for a in parsed[1:] if a.type != mitmproxy.types.Space] - assert cmd in m.commands.commands + assert cmd in console.commands.commands - cmd_obj = m.commands.commands[cmd] + cmd_obj = console.commands.commands[cmd] cmd_obj.prepare_args(args) except Exception as e: raise ValueError(f"Invalid binding: {binding.command}") from e 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_quickhelp.py b/test/mitmproxy/tools/console/test_quickhelp.py new file mode 100644 index 0000000000..e129850d57 --- /dev/null +++ b/test/mitmproxy/tools/console/test_quickhelp.py @@ -0,0 +1,66 @@ +import pytest + +from mitmproxy.test.tflow import tflow +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 +from mitmproxy.tools.console.grideditor import PathEditor +from mitmproxy.tools.console.help import HelpView +from mitmproxy.tools.console.keybindings import KeyBindings +from mitmproxy.tools.console.keymap import Keymap +from mitmproxy.tools.console.options import Options +from mitmproxy.tools.console.overlay import SimpleOverlay + + +@pytest.fixture(scope="module") +def keymap() -> Keymap: + km = Keymap(None) + defaultkeys.map(km) + return km + + +tflow2 = tflow() +tflow2.intercept() +tflow2.backup() +tflow2.marked = "x" + + +@pytest.mark.parametrize( + "widget, flow, is_root_widget", + [ + (FlowListBox, None, False), + (FlowListBox, tflow(), False), + (FlowView, tflow2, True), + (KeyBindings, None, True), + (Options, None, True), + (HelpView, None, False), + (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(): + if isinstance(v, quickhelp.BasicKeyHelp): + key_short = v.key + else: + b = keymap.binding_for_help(v) + if b is None: + raise AssertionError(f"No binding found for help text: {v}") + key_short = b.key_short() + assert len(key_short) + len(title) < 14 + + +def test_make_rows(): + keymap = Keymap(None) + defaultkeys.map(keymap) + + # make sure that we don't crash if a default binding is missing. + keymap.unbind(keymap.binding_for_help("View event log")) + + qh = quickhelp.make(HelpView, None, True) + assert qh.make_rows(keymap) diff --git a/test/mitmproxy/tools/console/test_statusbar.py b/test/mitmproxy/tools/console/test_statusbar.py index a54b983fe3..bcf5f3d606 100644 --- a/test/mitmproxy/tools/console/test_statusbar.py +++ b/test/mitmproxy/tools/console/test_statusbar.py @@ -1,13 +1,10 @@ import pytest -from mitmproxy import options -from mitmproxy.tools.console import statusbar, master +from mitmproxy.tools.console import statusbar -async def test_statusbar(monkeypatch): - o = options.Options() - m = master.ConsoleMaster(o) - m.options.update( +async def test_statusbar(console, monkeypatch): + console.options.update( modify_headers=[":~q:foo:bar"], modify_body=[":~q:foo:bar"], ignore_hosts=["example.com", "example.org"], @@ -21,32 +18,31 @@ async def test_statusbar(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", + mode=["transparent"], ) - - m.options.update(view_order="url", console_focus_follow=True) - monkeypatch.setattr(m.addons.get("clientplayback"), "count", lambda: 42) - monkeypatch.setattr(m.addons.get("serverplayback"), "count", lambda: 42) + console.options.update(view_order="url", console_focus_follow=True) + monkeypatch.setattr(console.addons.get("clientplayback"), "count", lambda: 42) + monkeypatch.setattr(console.addons.get("serverplayback"), "count", lambda: 42) monkeypatch.setattr(statusbar.StatusBar, "refresh", lambda x: None) - bar = statusbar.StatusBar(m) # this already causes a redraw + bar = statusbar.StatusBar(console) # this already causes a redraw assert bar.ib._w @pytest.mark.parametrize( "message,ready_message", [ - ("", [(None, ""), ("warn", "")]), + ("", [("", ""), ("warn", "")]), ( ("info", "Line fits into statusbar"), [("info", "Line fits into statusbar"), ("warn", "")], ), ( "Line doesn't fit into statusbar", - [(None, "Line doesn'\u2026"), ("warn", "(more in eventlog)")], + [("", "Line doesn'\u2026"), ("warn", "(more in eventlog)")], ), ( ("alert", "Two lines.\nFirst fits"), @@ -54,14 +50,14 @@ async def test_statusbar(monkeypatch): ), ( "Two long lines\nFirst doesn't fit", - [(None, "Two long li\u2026"), ("warn", "(more in eventlog)")], + [("", "Two long li\u2026"), ("warn", "(more in eventlog)")], ), ], ) def test_shorten_message(message, ready_message): - assert statusbar.ActionBar.shorten_message(message, max_width=30) == ready_message + assert statusbar.shorten_message(message, max_width=30) == ready_message def test_shorten_message_narrow(): - shorten_msg = statusbar.ActionBar.shorten_message("error", max_width=4) - assert shorten_msg == [(None, "\u2026"), ("warn", "(more in eventlog)")] + shorten_msg = statusbar.shorten_message("error", max_width=4) + assert shorten_msg == [("", "\u2026"), ("warn", "(more in eventlog)")] diff --git a/test/mitmproxy/tools/test_dump.py b/test/mitmproxy/tools/test_dump.py index e84857a163..0e37720750 100644 --- a/test/mitmproxy/tools/test_dump.py +++ b/test/mitmproxy/tools/test_dump.py @@ -1,5 +1,3 @@ -from unittest import mock - import pytest from mitmproxy import options @@ -7,21 +5,16 @@ class TestDumpMaster: - def mkmaster(self, **opts): - o = options.Options(**opts) - m = dump.DumpMaster(o, with_termlog=False, with_dumper=False) - return m - @pytest.mark.parametrize("termlog", [False, True]) - async def test_addons_termlog(self, termlog): - with mock.patch("sys.stdout"): - o = options.Options() - m = dump.DumpMaster(o, with_termlog=termlog) - assert (m.addons.get("termlog") is not None) == termlog + async def test_addons_termlog(self, capsys, termlog): + o = options.Options() + m = dump.DumpMaster(o, with_termlog=termlog) + assert (m.addons.get("termlog") is not None) == termlog + await m.done() @pytest.mark.parametrize("dumper", [False, True]) - async def test_addons_dumper(self, dumper): - with mock.patch("sys.stdout"): - o = options.Options() - m = dump.DumpMaster(o, with_dumper=dumper) - assert (m.addons.get("dumper") is not None) == dumper + async def test_addons_dumper(self, capsys, dumper): + o = options.Options() + m = dump.DumpMaster(o, with_dumper=dumper, with_termlog=False) + assert (m.addons.get("dumper") is not None) == dumper + await m.done() 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 8cd88074fa..616e4c8e7a 100644 --- a/test/mitmproxy/tools/web/test_app.py +++ b/test/mitmproxy/tools/web/test_app.py @@ -1,5 +1,5 @@ -import io import gzip +import io import json import logging import textwrap @@ -8,14 +8,19 @@ from pathlib import Path from typing import Optional from unittest import mock +from unittest.mock import Mock import pytest import tornado.testing from tornado import httpclient from tornado import websocket -from mitmproxy import certs, options, optmanager +from mitmproxy import certs +from mitmproxy import log +from mitmproxy import options +from mitmproxy import optmanager from mitmproxy.http import Headers +from mitmproxy.proxy.mode_servers import ServerInstance from mitmproxy.test import tflow from mitmproxy.tools.web import app from mitmproxy.tools.web import master as webmaster @@ -57,6 +62,11 @@ def test_generate_tflow_js(tdata): 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" @@ -65,13 +75,16 @@ def test_generate_tflow_js(tdata): # language=TypeScript content = ( "/** Auto-generated by test_app.py:test_generate_tflow_js */\n" - "import {HTTPFlow, TCPFlow, DNSFlow} from '../../flow';\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" @@ -82,6 +95,9 @@ def test_generate_tflow_js(tdata): 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), " " ), @@ -110,18 +126,19 @@ def ts_type(t): 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(f" {opt.name}: {ts_type(opt.typespec)};") print("}") print("") - print("export type Option = keyof OptionsState") + print("export type Option = keyof OptionsState;") print("") print("export const defaultState: OptionsState = {") for _, opt in sorted(m.options.items()): @@ -130,21 +147,24 @@ def ts_type(t): ": null", ": undefined" ) ) - print("}") + print("};") ( Path(__file__).parent / "../../../../web/src/js/ducks/_options_gen.ts" ).write_bytes(s.getvalue().encode()) + await m.done() @pytest.mark.usefixtures("no_tornado_logging", "tdata") class TestApp(tornado.testing.AsyncHTTPTestCase): def get_app(self): - async def make_master(): + async def make_master() -> webmaster.WebMaster: o = options.Options(http2=False) return webmaster.WebMaster(o, with_termlog=False) - m = 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" @@ -154,7 +174,26 @@ async def make_master(): f2.id = "43" m.view.add([f, f2]) m.view.add([tflow.tflow(err=True)]) - m.log.info("test log") + m.events._add_log(log.LogEntry("test log", "info")) + m.events.done() + 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) + 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 @@ -175,7 +214,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 @@ -190,6 +233,14 @@ def test_flows_dump(self): resp = self.fetch("/flows/dump") assert b"address" in resp.body + def test_flows_dump_filter(self): + resp = self.fetch("/flows/dump?filter=foo") + assert b"" == resp.body + + def test_flows_dump_filter_error(self): + resp = self.fetch("/flows/dump?filter=[") + assert resp.code == 400 + def test_clear(self): events = self.events.data.copy() flows = list(self.view) @@ -436,8 +487,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 @@ -447,8 +498,30 @@ def test_option_update(self): def test_option_save(self): assert self.fetch("/options/save", method="POST").code == 200 - def test_conf(self): - assert self.fetch("/conf.js").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: diff --git a/test/mitmproxy/tools/web/test_master.py b/test/mitmproxy/tools/web/test_master.py index e69de29bb2..ee0225754a 100644 --- a/test/mitmproxy/tools/web/test_master.py +++ b/test/mitmproxy/tools/web/test_master.py @@ -0,0 +1,20 @@ +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 + ) + 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 + 2}"): + await master.running() + server.close() diff --git a/test/mitmproxy/tools/web/test_static_viewer.py b/test/mitmproxy/tools/web/test_static_viewer.py index 870cd4421d..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): @@ -41,8 +40,7 @@ def test_save_flows(tmpdir): ) -@mock.patch("mitmproxy.ctx.log") -def test_save_flows_content(ctx, tmpdir): +def test_save_flows_content(tmpdir): flows = [tflow.tflow(resp=False), tflow.tflow(resp=True)] with mock.patch("time.time", mock.Mock(side_effect=[1, 2, 2] * 4)): static_viewer.save_flows_content(tmpdir, flows) 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_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 new file mode 100644 index 0000000000..2382e3921a --- /dev/null +++ b/test/mitmproxy/utils/test_magisk.py @@ -0,0 +1,32 @@ +import os + +from cryptography import x509 + +from mitmproxy.test import taddons +from mitmproxy.utils import magisk + + +def test_get_ca(tdata): + with taddons.context() as tctx: + tctx.options.confdir = tdata.path("mitmproxy/data/confdir") + ca = magisk.get_ca_from_files() + assert isinstance(ca, x509.Certificate) + + +def test_subject_hash_old(tdata): + # checks if the hash is the same as that comming form openssl + with taddons.context() as tctx: + tctx.options.confdir = tdata.path("mitmproxy/data/confdir") + ca = magisk.get_ca_from_files() + our_hash = magisk.subject_hash_old(ca) + assert our_hash == "efb15d7d" + + +def test_magisk_write(tdata, tmp_path): + # checks if the hash is the same as that comming form openssl + with taddons.context() as tctx: + tctx.options.confdir = tdata.path("mitmproxy/data/confdir") + magisk_path = tmp_path / "mitmproxy-magisk-module.zip" + magisk.write_magisk_module(magisk_path) + + assert os.path.exists(magisk_path) diff --git a/test/mitmproxy/utils/test_signals.py b/test/mitmproxy/utils/test_signals.py new file mode 100644 index 0000000000..dd856eb587 --- /dev/null +++ b/test/mitmproxy/utils/test_signals.py @@ -0,0 +1,79 @@ +from unittest import mock + +import pytest + +from mitmproxy.utils.signals import AsyncSignal +from mitmproxy.utils.signals import SyncSignal + + +def test_sync_signal() -> None: + m = mock.Mock() + + s = SyncSignal(lambda event: None) + s.connect(m) + s.send("foo") + + assert m.call_args_list == [mock.call("foo")] + + class Foo: + called = None + + def bound(self, event): + self.called = event + + f = Foo() + s.connect(f.bound) + s.send(event="bar") + assert f.called == "bar" + assert m.call_args_list == [mock.call("foo"), mock.call(event="bar")] + + s.disconnect(m) + s.send("baz") + assert f.called == "baz" + assert m.call_count == 2 + + def err(event): + raise RuntimeError + + s.connect(err) + with pytest.raises(RuntimeError): + s.send(42) + + +def test_signal_weakref() -> None: + def m1(): + pass + + def m2(): + pass + + s = SyncSignal(lambda: None) + s.connect(m1) + s.connect(m2) + del m2 + s.send() + assert len(s.receivers) == 1 + + +def test_sync_signal_async_receiver() -> None: + s = SyncSignal(lambda: None) + + with pytest.raises(AssertionError): + s.connect(mock.AsyncMock()) + + +async def test_async_signal() -> None: + s = AsyncSignal(lambda event: None) + m1 = mock.AsyncMock() + m2 = mock.Mock() + + s.connect(m1) + s.connect(m2) + await s.send("foo") + assert m1.call_args_list == m2.call_args_list == [mock.call("foo")] + + s.disconnect(m2) + + await s.send("bar") + assert m1.call_count == 2 + assert m2.call_count == 1 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..f5b2894ac0 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): 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/test/release/test_cibuild.py b/test/release/test_cibuild.py deleted file mode 100644 index c1ec0aed25..0000000000 --- a/test/release/test_cibuild.py +++ /dev/null @@ -1,240 +0,0 @@ -import io -from pathlib import Path - -import pytest - -from release import cibuild - -root = Path(__file__).parent.parent.parent - - -def test_buildenviron_live(): - be = cibuild.BuildEnviron.from_env() - assert be.release_dir - - -def test_buildenviron_common(): - be = cibuild.BuildEnviron( - system="Linux", - root_dir=root, - branch="main", - ) - assert be.release_dir == be.root_dir / "release" - assert be.dist_dir == be.root_dir / "release" / "dist" - assert be.build_dir == be.root_dir / "release" / "build" - assert not be.has_docker_creds - - cs = io.StringIO() - be.dump_info(cs) - assert cs.getvalue() - - be = cibuild.BuildEnviron( - system="Unknown", - root_dir=root, - ) - with pytest.raises(cibuild.BuildError): - be.version - with pytest.raises(cibuild.BuildError): - be.platform_tag - - -def test_buildenviron_pr(monkeypatch): - # Simulates a PR. We build everything, but don't have access to secret - # credential env variables. - monkeypatch.setenv("GITHUB_REF", "refs/pull/42/merge") - monkeypatch.setenv("CI_BUILD_WHEEL", "1") - monkeypatch.setenv("GITHUB_EVENT_NAME", "pull_request") - - be = cibuild.BuildEnviron.from_env() - assert be.branch == "pr-42" - assert be.is_pull_request - assert be.should_build_wheel - assert not be.should_upload_pypi - - -def test_buildenviron_commit(): - # Simulates an ordinary commit on the master branch. - be = cibuild.BuildEnviron( - system="Linux", - root_dir=root, - branch="main", - is_pull_request=False, - should_build_wheel=True, - should_build_pyinstaller=True, - should_build_docker=True, - docker_username="foo", - docker_password="bar", - has_aws_creds=True, - ) - assert be.docker_tag == "mitmproxy/mitmproxy:dev" - assert be.should_upload_docker - assert not be.should_upload_pypi - assert be.should_upload_docker - assert be.should_upload_aws - assert not be.is_prod_release - assert not be.is_maintenance_branch - - -def test_buildenviron_releasetag(): - # Simulates a tagged release on a release branch. - be = cibuild.BuildEnviron( - system="Linux", - root_dir=root, - tag="v0.0.1", - should_build_wheel=True, - should_build_docker=True, - should_build_pyinstaller=True, - has_twine_creds=True, - docker_username="foo", - docker_password="bar", - ) - assert be.tag == "v0.0.1" - assert be.branch is None - assert be.version == "0.0.1" - assert be.upload_dir == "0.0.1" - assert be.docker_tag == "mitmproxy/mitmproxy:0.0.1" - assert be.should_upload_pypi - assert be.should_upload_docker - assert be.is_prod_release - assert not be.is_maintenance_branch - - -def test_buildenviron_namedtag(): - # Simulates a non-release tag on a branch. - be = cibuild.BuildEnviron( - system="Linux", - root_dir=root, - tag="anyname", - should_build_wheel=True, - should_build_docker=True, - should_build_pyinstaller=True, - has_twine_creds=True, - docker_username="foo", - docker_password="bar", - ) - assert be.tag == "anyname" - assert be.branch is None - assert be.version == "anyname" - assert be.upload_dir == "anyname" - assert be.docker_tag == "mitmproxy/mitmproxy:anyname" - assert not be.should_upload_pypi - assert not be.should_upload_docker - assert not be.is_prod_release - assert not be.is_maintenance_branch - - -def test_buildenviron_dev_branch(): - # Simulates a commit on a development branch on the main repo - be = cibuild.BuildEnviron( - system="Linux", - root_dir=root, - branch="mybranch", - should_build_wheel=True, - should_build_docker=True, - should_build_pyinstaller=True, - has_twine_creds=True, - docker_username="foo", - docker_password="bar", - ) - assert be.tag is None - assert be.branch == "mybranch" - assert be.version == "mybranch" - assert be.upload_dir == "branches/mybranch" - assert not be.should_upload_pypi - assert not be.should_upload_docker - assert not be.is_maintenance_branch - - -def test_buildenviron_maintenance_branch(): - # Simulates a commit on a release maintenance branch on the main repo - be = cibuild.BuildEnviron( - system="Linux", - root_dir=root, - branch="v0.x", - should_build_wheel=True, - should_build_docker=True, - should_build_pyinstaller=True, - has_twine_creds=True, - docker_username="foo", - docker_password="bar", - ) - assert be.tag is None - assert be.branch == "v0.x" - assert be.version == "v0.x" - assert be.upload_dir == "branches/v0.x" - assert not be.should_upload_pypi - assert not be.should_upload_docker - assert be.is_maintenance_branch - - -def test_buildenviron_osx(tmp_path): - be = cibuild.BuildEnviron( - system="Darwin", - root_dir=root, - tag="v0.0.1", - ) - assert be.platform_tag == "osx" - assert be.archive_path == be.dist_dir / "mitmproxy-0.0.1-osx.tar.gz" - - with be.archive(tmp_path / "arch"): - pass - assert (tmp_path / "arch").exists() - - -def test_buildenviron_windows(tmp_path): - be = cibuild.BuildEnviron( - system="Windows", - root_dir=root, - tag="v0.0.1", - ) - assert be.platform_tag == "windows" - assert be.archive_path == be.dist_dir / "mitmproxy-0.0.1-windows.zip" - - with be.archive(tmp_path / "arch"): - pass - assert (tmp_path / "arch").exists() - - -@pytest.mark.parametrize( - "version, tag, ok", - [ - ("3.0.0.dev", "", True), # regular snapshot - ("3.0.0.dev", "v3.0.0", False), # forgot to remove ".dev" on bump - ("3.0.0", "", False), # forgot to re-add ".dev" - ("3.0.0", "v4.0.0", False), # version mismatch - ("3.0.0", "v3.0.0", True), # regular release - ("3.0.0.rc1", "v3.0.0.rc1", False), # non-canonical. - ("3.0.0.dev", "anyname", True), # tagged test/dev release - ("3.0.0", "3.0.0", False), # tagged, but without v prefix - ], -) -def test_buildenviron_check_version(version, tag, ok, tmpdir): - tmpdir.mkdir("mitmproxy").join("version.py").write(f'VERSION = "{version}"') - - be = cibuild.BuildEnviron( - root_dir=tmpdir, - system="Windows", - tag=tag, - ) - if ok: - be.check_version() - else: - with pytest.raises(ValueError): - be.check_version() - - -def test_bool_from_env(monkeypatch): - monkeypatch.setenv("FOO", "1") - assert cibuild.bool_from_env("FOO") - - monkeypatch.setenv("FOO", "0") - assert not cibuild.bool_from_env("FOO") - - monkeypatch.setenv("FOO", "false") - assert not cibuild.bool_from_env("FOO") - - monkeypatch.setenv("FOO", "") - assert not cibuild.bool_from_env("FOO") - - monkeypatch.delenv("FOO") - assert not cibuild.bool_from_env("FOO") diff --git a/test/wg-test-client/LICENSE b/test/wg-test-client/LICENSE new file mode 100644 index 0000000000..11f277c48d --- /dev/null +++ b/test/wg-test-client/LICENSE @@ -0,0 +1,89 @@ +The mitmproxy_wireguard test client is available under the same license (MIT) +as the mitmproxy_wireguard Python package and mitmproxy itself: + +-------------------------------------------------------------------------------- + +Copyright (c) 2022, Fabio Valentini and Maximilian Hils + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- + +The test client also contains code from third-party Rust crates, which are +available under the following licenses: + +aead v0.5.1: MIT OR Apache-2.0 +anyhow v1.0.65: MIT OR Apache-2.0 +base64 v0.13.0: MIT/Apache-2.0 +bitflags v1.3.2: MIT/Apache-2.0 +blake2 v0.10.4: MIT OR Apache-2.0 +block-buffer v0.10.3: MIT OR Apache-2.0 +boringtun v0.5.2: BSD-3-Clause +byteorder v1.4.3: Unlicense OR MIT +cfg-if v1.0.0: MIT/Apache-2.0 +chacha20poly1305 v0.10.1: Apache-2.0 OR MIT +chacha20 v0.9.0: Apache-2.0 OR MIT +cipher v0.4.3: MIT OR Apache-2.0 +cpufeatures v0.2.5: MIT OR Apache-2.0 +crypto-common v0.1.6: MIT OR Apache-2.0 +curve25519-dalek v3.2.0: BSD-3-Clause +digest v0.10.5: MIT OR Apache-2.0 +digest v0.9.0: MIT OR Apache-2.0 +generic-array v0.14.6: MIT +getrandom v0.1.16: MIT OR Apache-2.0 +getrandom v0.2.7: MIT OR Apache-2.0 +hex v0.4.3: MIT OR Apache-2.0 +hmac v0.12.1: MIT OR Apache-2.0 +inout v0.1.3: MIT OR Apache-2.0 +ip_network_table-deps-treebitmap v0.5.0: MIT +ip_network_table v0.2.0: BSD-2-Clause +ip_network v0.4.1: BSD-2-Clause +libc v0.2.132: MIT OR Apache-2.0 +lock_api v0.4.8: MIT OR Apache-2.0 +log v0.4.17: MIT OR Apache-2.0 +managed v0.8.0: 0BSD +once_cell v1.14.0: MIT OR Apache-2.0 +opaque-debug v0.3.0: MIT OR Apache-2.0 +parking_lot_core v0.9.3: MIT OR Apache-2.0 +parking_lot v0.12.1: MIT OR Apache-2.0 +pin-project-lite v0.2.9: Apache-2.0 OR MIT +poly1305 v0.8.0: Apache-2.0 OR MIT +rand_core v0.5.1: MIT OR Apache-2.0 +rand_core v0.6.4: MIT OR Apache-2.0 +ring v0.16.20: +scopeguard v1.1.0: MIT/Apache-2.0 +smallvec v1.9.0: MIT OR Apache-2.0 +smoltcp v0.8.1: 0BSD +spin v0.5.2: MIT +subtle v2.4.1: BSD-3-Clause +tracing-core v0.1.29: MIT +tracing v0.1.36: MIT +typenum v1.15.0: MIT OR Apache-2.0 +universal-hash v0.5.0: MIT OR Apache-2.0 +untrusted v0.7.1: ISC +untrusted v0.9.0: ISC +x25519-dalek v2.0.0-pre.1: BSD-3-Clause +zeroize v1.5.7: Apache-2.0 OR MIT + +-------------------------------------------------------------------------------- + +This list of third-party crates and their licenses was collected for v0.1.6 of +the test client by running this command: + +$ cargo tree --prefix none --edges no-build,no-dev,no-proc-macro --format "{p}: {l}" --no-dedupe | sort -u diff --git a/test/wg-test-client/README.md b/test/wg-test-client/README.md new file mode 100644 index 0000000000..cffe47bb9c --- /dev/null +++ b/test/wg-test-client/README.md @@ -0,0 +1,9 @@ +# mitm-wg-test-client + +This directory contains simple test client binaries built from + version v0.1.6. New versions +of the test client binaries are published as release assets on GitHub. + +The test binaries are used for sending WireGuard traffic from userspace in +`tests/mitmproxy/proxy/test_mode_servers.py:test_wireguard`. + diff --git a/test/wg-test-client/linux-x86_64 b/test/wg-test-client/linux-x86_64 new file mode 100755 index 0000000000..e514e644a2 Binary files /dev/null and b/test/wg-test-client/linux-x86_64 differ diff --git a/test/wg-test-client/macos-aarch64 b/test/wg-test-client/macos-aarch64 new file mode 100755 index 0000000000..160cbb9f8c Binary files /dev/null and b/test/wg-test-client/macos-aarch64 differ diff --git a/test/wg-test-client/macos-x86_64 b/test/wg-test-client/macos-x86_64 new file mode 100755 index 0000000000..85b16e972b Binary files /dev/null and b/test/wg-test-client/macos-x86_64 differ diff --git a/test/wg-test-client/test.conf b/test/wg-test-client/test.conf new file mode 100644 index 0000000000..949b1f426a --- /dev/null +++ b/test/wg-test-client/test.conf @@ -0,0 +1,4 @@ +{ + "server_key": "EG47ZWjYjr+Y97TQ1A7sVl7Xn3mMWDnvjU/VxU769ls=", + "client_key": "qG8b7LI/s+ezngWpXqj5A7Nj988hbGL+eQ8ePki0iHk=" +} diff --git a/test/wg-test-client/windows-x86_64.exe b/test/wg-test-client/windows-x86_64.exe new file mode 100755 index 0000000000..ed7287cad2 Binary files /dev/null and b/test/wg-test-client/windows-x86_64.exe differ diff --git a/tox.ini b/tox.ini deleted file mode 100644 index db3d7c6ec3..0000000000 --- a/tox.ini +++ /dev/null @@ -1,54 +0,0 @@ -[tox] -envlist = py, flake8, 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 \ - --full-cov=mitmproxy/ \ - {posargs} - -[testenv:flake8] -deps = - flake8>=3.8.4,<4.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.961 - types-certifi==2021.10.8.3 - types-Flask==1.1.6 - types-Werkzeug==1.0.9 - types-requests==2.28.0 - types-cryptography==3.3.21 - types-pyOpenSSL==22.0.4 - -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..4c242ac2f8 100644 --- a/web/README.md +++ b/web/README.md @@ -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/gulpfile.js b/web/gulpfile.js index b7dafce778..e322e818c8 100644 --- a/web/gulpfile.js +++ b/web/gulpfile.js @@ -1,59 +1,64 @@ 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 +69,40 @@ 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/" }) .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 = ", + 'import * as flowutils from "../flow/utils"\n' + + "export default " + ) + ) .pipe(gulp.dest("src/")); } @@ -111,12 +127,12 @@ const prod = gulp.parallel( 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 231dd3edd1..f8ba1016a4 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1521,14 +1521,12 @@ "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true + "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==", - "dev": true, "requires": { "color-convert": "^2.0.1" }, @@ -1537,7 +1535,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -1545,8 +1542,7 @@ "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==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" } } }, @@ -2070,6 +2066,16 @@ "integrity": "sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "body": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", @@ -2297,7 +2303,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "normalize-path": { "version": "3.0.0", @@ -2825,8 +2835,7 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "decimal.js": { "version": "10.3.1", @@ -2924,6 +2933,11 @@ "integrity": "sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==", "dev": true }, + "dijkstrajs": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.2.tgz", + "integrity": "sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==" + }, "dom-accessibility-api": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.7.tgz", @@ -3013,8 +3027,12 @@ "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==", - "dev": true + "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", @@ -3489,6 +3507,13 @@ "bser": "^2.0.0" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -3516,7 +3541,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, "requires": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -3666,8 +3690,7 @@ "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==", - "dev": true + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-intrinsic": { "version": "1.1.1", @@ -4892,8 +4915,7 @@ "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==", - "dev": true + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-generator-fn": { "version": "2.1.0", @@ -6532,7 +6554,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, "requires": { "p-locate": "^4.1.0" } @@ -7048,6 +7069,13 @@ "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", "dev": true }, + "nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "dev": true, + "optional": true + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -7463,7 +7491,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, "requires": { "p-try": "^2.0.0" } @@ -7472,7 +7499,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, "requires": { "p-limit": "^2.2.0" } @@ -7480,8 +7506,7 @@ "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==", - "dev": true + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "parse-filepath": { "version": "1.0.2", @@ -7536,8 +7561,7 @@ "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==", - "dev": true + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" }, "path-is-absolute": { "version": "1.0.1", @@ -7646,6 +7670,11 @@ "extend-shallow": "^3.0.2" } }, + "pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -7717,6 +7746,12 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "prettier": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", + "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", + "dev": true + }, "pretty-format": { "version": "27.0.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.0.2.tgz", @@ -7830,6 +7865,111 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, + "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" + }, + "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" + } + } + } + }, "qs": { "version": "6.10.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", @@ -8225,8 +8365,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-main-filename": { "version": "1.0.1", @@ -8519,8 +8658,7 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "set-value": { "version": "2.0.1", @@ -9008,7 +9146,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, "requires": { "ansi-regex": "^5.0.0" } diff --git a/web/package.json b/web/package.json index 7fc6982851..df4b2b2346 100644 --- a/web/package.json +++ b/web/package.json @@ -4,7 +4,8 @@ "scripts": { "test": "tsc --noEmit && jest --coverage", "build": "gulp prod", - "start": "gulp" + "start": "gulp", + "prettier": "prettier --write ." }, "dependencies": { "@popperjs/core": "^2.9.3", @@ -13,6 +14,7 @@ "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-popper": "^2.2.5", @@ -43,6 +45,7 @@ "gulp-sourcemaps": "^3.0.0", "jest": "^27.0.6", "jest-fetch-mock": "^3.0.3", + "prettier": "2.8.4", "react-test-renderer": "^17.0.2", "redux-mock-store": "^1.5.4", "through2": "^4.0.2", diff --git a/web/src/css/app.less b/web/src/css/app.less index 644777f9a6..1f0e3a1537 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; } @@ -21,3 +23,4 @@ html { @import (less) "modal.less"; @import (less) "dropdown.less"; @import (less) "command.less"; +@import (less) "capture-setup.less"; diff --git a/web/src/css/capture-setup.less b/web/src/css/capture-setup.less new file mode 100644 index 0000000000..290dab4445 --- /dev/null +++ b/web/src/css/capture-setup.less @@ -0,0 +1,10 @@ +.wireguard-config { + > * { + margin: 0; + } + margin: 1rem 0; + display: flex; + flex-wrap: wrap; + column-gap: 2rem; + align-items: center; +} 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 ec5efab8aa..da789730d4 100644 --- a/web/src/css/contentview.less +++ b/web/src/css/contentview.less @@ -1,17 +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 { + color: darkgreen; + } + .Token_Literal_String { + color: firebrick; + } + .Token_Literal_Number { + color: purple; + } + .Token_Keyword_Constant { + color: blue; + } } diff --git a/web/src/css/dropdown.less b/web/src/css/dropdown.less index cd27c84c07..1ed8ad8e5f 100644 --- a/web/src/css/dropdown.less +++ b/web/src/css/dropdown.less @@ -1,5 +1,4 @@ .dropdown-menu { - // setting a margin is not compatible with popper. margin: 0 !important; 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..b7f5b8f51a 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,7 +21,6 @@ } } - .first-line { font-family: @font-family-monospace; background-color: #428bca; @@ -59,7 +58,6 @@ } } - .inline-input { display: inline; margin: 0 -3px; @@ -68,7 +66,8 @@ 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 +79,10 @@ &[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 +93,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 +126,10 @@ } } -.headers, .trailers { +.headers, +.trailers { .kv-row { - margin-bottom: .3em; + margin-bottom: 0.3em; max-height: 12.4ex; overflow-y: auto; } @@ -162,7 +164,8 @@ overflow-wrap: break-word; } -.connection-table, .timing-table { +.connection-table, +.timing-table { td { overflow: hidden; text-overflow: ellipsis; @@ -176,7 +179,8 @@ dl.cert-attributes { flex-wrap: wrap; margin-bottom: 0; - dt, dd { + dt, + dd { text-overflow: ellipsis; overflow: hidden; } @@ -190,8 +194,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..e911c9cdc3 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; } } @@ -130,7 +134,8 @@ color: @interceptorange; } - .fa-exclamation, .fa-times { + .fa-exclamation, + .fa-times { color: darkred; } } @@ -139,6 +144,10 @@ width: 60px; } + .col-version { + width: 80px; + } + .col-status { width: 50px; } @@ -155,7 +164,9 @@ width: 170px; } - td.col-time, td.col-size, td.col-timestamp { + td.col-time, + td.col-size, + td.col-timestamp { text-align: right; } @@ -170,7 +181,8 @@ font-size: 20px; } - tr:hover .col-quickactions, .col-quickactions.hover { + tr:hover .col-quickactions, + .col-quickactions.hover { 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..b890e0b06e 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"; @@ -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/sprites.less b/web/src/css/sprites.less index 0750116998..96ff54eb40 100644 --- a/web/src/css/sprites.less +++ b/web/src/css/sprites.less @@ -53,6 +53,14 @@ 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); +} 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..663a87b5f6 100644 --- a/web/src/css/vendor-bootstrap-variables.less +++ b/web/src/css/vendor-bootstrap-variables.less @@ -3,5 +3,8 @@ @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..ce2ca1c34b 100644 --- a/web/src/fonts/font-awesome.css +++ b/web/src/fonts/font-awesome.css @@ -5,2333 +5,2339 @@ /* 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/images/resourceUdpIcon.png b/web/src/images/resourceUdpIcon.png new file mode 100644 index 0000000000..1d6ba6f290 Binary files /dev/null and b/web/src/images/resourceUdpIcon.png differ diff --git a/web/src/js/__tests__/backends/staticSpec.tsx b/web/src/js/__tests__/backends/staticSpec.tsx index 003906b002..e892b5aded 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(); @@ -10,8 +10,20 @@ test("static backend", async () => { 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"} - ])) + await waitFor(() => + expect(store.getActions()).toEqual([ + { + type: "FLOWS_RECEIVE", + cmd: "receive", + data: [], + resource: "flows", + }, + { + type: "OPTIONS_RECEIVE", + cmd: "receive", + data: {}, + resource: "options", + }, + ]) + ); }); diff --git a/web/src/js/__tests__/backends/websocketSpec.tsx b/web/src/js/__tests__/backends/websocketSpec.tsx index a8a4163383..134de711df 100644 --- a/web/src/js/__tests__/backends/websocketSpec.tsx +++ b/web/src/js/__tests__/backends/websocketSpec.tsx @@ -1,15 +1,18 @@ -import {enableFetchMocks} from "jest-fetch-mock"; -import {TStore} from "../ducks/tutils"; +import { enableFetchMocks } from "jest-fetch-mock"; +import { TStore } from "../ducks/tutils"; import WebSocketBackend from "../../backends/websocket"; -import {waitFor} from "../test-utils"; +import { waitFor } from "../test-utils"; import * as connectionActions from "../../ducks/connection"; enableFetchMocks(); test("websocket backend", async () => { // @ts-ignore - jest.spyOn(global, 'WebSocket').mockImplementation(() => ({addEventListener: () => 0})); + jest.spyOn(global, "WebSocket").mockImplementation(() => ({ + addEventListener: () => 0, + })); + fetchMock.mockOnceIf("./state", "{}"); fetchMock.mockOnceIf("./flows", "[]"); fetchMock.mockOnceIf("./events", "[]"); fetchMock.mockOnceIf("./options", "{}"); @@ -18,44 +21,78 @@ test("websocket backend", async () => { backend.onOpen(); - await waitFor(() => expect(store.getActions()).toEqual([ - connectionActions.startFetching(), - {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(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(), + ]) + ); store.clearActions(); 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" - }]); + expect(store.getActions()).toEqual([ + { + cmd: "add", + data: { id: "42", level: "info", message: "test" }, + resource: "events", + type: "EVENTS_ADD", + }, + ]); store.clearActions(); 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() - expect(fetchMock.mock.calls).toHaveLength(4); + await waitFor(() => + expect(store.getActions()).toEqual([ + { + type: "EVENTS_RECEIVE", + cmd: "receive", + data: [], + resource: "events", + }, + connectionActions.connectionEstablished(), + ]) + ); + store.clearActions(); + 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); + expect(store.getActions()[0].type).toEqual( + connectionActions.ConnectionState.ERROR + ); store.clearActions(); backend.onError(null); diff --git a/web/src/js/__tests__/components/CaptureSetupSpec.tsx b/web/src/js/__tests__/components/CaptureSetupSpec.tsx new file mode 100644 index 0000000000..9033ed5947 --- /dev/null +++ b/web/src/js/__tests__/components/CaptureSetupSpec.tsx @@ -0,0 +1,10 @@ +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..a418d3ddbf 100644 --- a/web/src/js/__tests__/components/CommandBarSpec.tsx +++ b/web/src/js/__tests__/components/CommandBarSpec.tsx @@ -1,55 +1,86 @@ -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'); + userEvent.type(input, "x"); expect(screen.getByText("[]")).toBeInTheDocument(); userEvent.type(input, "{backspace}"); - userEvent.type(input, 'fl'); + userEvent.type(input, "fl"); userEvent.tab(); - expect(input).toHaveValue('flow.decode'); + expect(input).toHaveValue("flow.decode"); userEvent.tab(); - expect(input).toHaveValue('flow.encode'); + expect(input).toHaveValue("flow.encode"); - fetchMock.mockOnce(JSON.stringify({value: null})); + fetchMock.mockOnce(JSON.stringify({ value: null })); userEvent.type(input, "{enter}"); await waitFor(() => screen.getByText("Command Result")); diff --git a/web/src/js/__tests__/components/EventLog/EventListSpec.tsx b/web/src/js/__tests__/components/EventLog/EventListSpec.tsx index 30e41d336d..53e5c7f0ae 100644 --- a/web/src/js/__tests__/components/EventLog/EventListSpec.tsx +++ b/web/src/js/__tests__/components/EventLog/EventListSpec.tsx @@ -1,22 +1,27 @@ -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 TestUtils from "react-dom/test-utils"; -describe('EventList Component', () => { - let mockEventList = [ - { id: 1, level: 'info', message: 'foo' }, - { id: 2, level: 'error', message: 'bar' } +describe("EventList Component", () => { + let mockEventList = [ + { id: 1, level: "info", message: "foo" }, + { id: 2, level: "error", message: "bar" }, ], - eventLogList = TestUtils.renderIntoDocument() + eventLogList = TestUtils.renderIntoDocument( + + ); - it('should render correctly', () => { - expect(eventLogList.state).toMatchSnapshot() - expect(eventLogList.props).toMatchSnapshot() - }) + 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 handle componentWillUnmount", () => { + window.removeEventListener = jest.fn(); + eventLogList.componentWillUnmount(); + expect(window.removeEventListener).toBeCalledWith( + "resize", + eventLogList.onViewportUpdate + ); + }); +}); diff --git a/web/src/js/__tests__/components/EventLogSpec.tsx b/web/src/js/__tests__/components/EventLogSpec.tsx index e6a74a5463..209f2dd319 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", () => { + let 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) - }) - -}) + 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..cb1d3f1a7a 100644 --- a/web/src/js/__tests__/components/FlowTable/FlowColumnsSpec.tsx +++ b/web/src/js/__tests__/components/FlowTable/FlowColumnsSpec.tsx @@ -1,103 +1,112 @@ -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() + let 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() + let 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() + let 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() + let 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() + let 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() + let 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() - }) + let 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(); - let 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(); + }); +}); diff --git a/web/src/js/__tests__/components/FlowTable/FlowRowSpec.tsx b/web/src/js/__tests__/components/FlowTable/FlowRowSpec.tsx index c1361ac3cc..6f059227f2 100644 --- a/web/src/js/__tests__/components/FlowTable/FlowRowSpec.tsx +++ b/web/src/js/__tests__/components/FlowTable/FlowRowSpec.tsx @@ -1,21 +1,27 @@ -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 { testState } from "../../ducks/tutils"; +import { fireEvent, render, screen } from "../../test-utils"; +import { createAppStore } from "../../../ducks"; test("FlowRow", async () => { const store = createAppStore(testState), tflow2 = store.getState().flows.list[0], - {asFragment} = render( - - - -
    , {store}) - expect(asFragment()).toMatchSnapshot() + { asFragment } = render( + + + + +
    , + { 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( + 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 + ); +}); diff --git a/web/src/js/__tests__/components/FlowTable/FlowTableHeadSpec.tsx b/web/src/js/__tests__/components/FlowTable/FlowTableHeadSpec.tsx index 511c2ccc26..00d1810cf1 100644 --- a/web/src/js/__tests__/components/FlowTable/FlowTableHeadSpec.tsx +++ b/web/src/js/__tests__/components/FlowTable/FlowTableHeadSpec.tsx @@ -1,29 +1,24 @@ -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"; +import { setSort } from "../../../ducks/flows"; test("FlowTableHead Component", async () => { - const store = TStore(), - {asFragment} = render( + { asFragment } = render( - +
    - ) - expect(asFragment()).toMatchSnapshot() + ); + expect(asFragment()).toMatchSnapshot(); - fireEvent.click(screen.getByText("Size")) + fireEvent.click(screen.getByText("Size")); - expect(store.getActions()).toStrictEqual([ - setSort("size", false) - ] - ) -}) + expect(store.getActions()).toStrictEqual([setSort("size", false)]); +}); 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 9f54436191..314deb8fc4 100644 --- a/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.tsx.snap +++ b/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.tsx.snap @@ -214,9 +214,7 @@ exports[`should render columns: quickactions 1`] = ` -
    - + /> @@ -301,3 +299,19 @@ exports[`should render columns: tls 1`] = ` `; + +exports[`should render columns: version 1`] = ` + + + + + + + +
    + HTTP/1.1 +
    +
    +`; diff --git a/web/src/js/__tests__/components/FlowTableSpec.tsx b/web/src/js/__tests__/components/FlowTableSpec.tsx index 9bc67ebf43..4944fd10bf 100644 --- a/web/src/js/__tests__/components/FlowTableSpec.tsx +++ b/web/src/js/__tests__/components/FlowTableSpec.tsx @@ -1,50 +1,57 @@ -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 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"; -window.addEventListener = jest.fn() +window.addEventListener = jest.fn(); -describe('FlowTable Component', () => { +describe("FlowTable Component", () => { let selectFn = jest.fn(), tflow = TFlow(), - store = TStore() + store = TStore(); - it('should render correctly', () => { + it("should render correctly", () => { let provider = renderer.create( - - ), - tree = provider.toJSON() - expect(tree).toMatchSnapshot() - }) + + + ), + tree = provider.toJSON(); + expect(tree).toMatchSnapshot(); + }); let provider = renderer.create( - - - ), - flowTable = provider.root.findByType(FlowTable) + + + + ), + flowTable = provider.root.findByType(FlowTable); - it('should handle componentWillUnmount', () => { - flowTable.instance.UNSAFE_componentWillUnmount() - expect(window.addEventListener).toBeCalledWith('resize', flowTable.instance.onViewportUpdate) - }) + it("should handle componentWillUnmount", () => { + flowTable.instance.UNSAFE_componentWillUnmount(); + expect(window.addEventListener).toBeCalledWith( + "resize", + flowTable.instance.onViewportUpdate + ); + }); - it('should handle componentDidUpdate', () => { + it("should handle componentDidUpdate", () => { // flowTable.shouldScrollIntoView == false - expect(flowTable.instance.componentDidUpdate()).toEqual(undefined) + expect(flowTable.instance.componentDidUpdate()).toEqual(undefined); // rowTop - headHeight < viewportTop - flowTable.instance.shouldScrollIntoView = true - flowTable.instance.componentDidUpdate() + flowTable.instance.shouldScrollIntoView = true; + flowTable.instance.componentDidUpdate(); // rowBottom > viewportTop + viewportHeight - flowTable.instance.shouldScrollIntoView = true - flowTable.instance.componentDidUpdate() - }) + flowTable.instance.shouldScrollIntoView = true; + flowTable.instance.componentDidUpdate(); + }); - it('should handle componentWillReceiveProps', () => { - flowTable.instance.UNSAFE_componentWillReceiveProps({selected: tflow}) - expect(flowTable.instance.shouldScrollIntoView).toBeTruthy() - }) -}) + it("should handle componentWillReceiveProps", () => { + flowTable.instance.UNSAFE_componentWillReceiveProps({ + selected: tflow, + }); + expect(flowTable.instance.shouldScrollIntoView).toBeTruthy(); + }); +}); diff --git a/web/src/js/__tests__/components/FlowViewSpec.tsx b/web/src/js/__tests__/components/FlowViewSpec.tsx index 8a579fa84f..4cf86a3df1 100644 --- a/web/src/js/__tests__/components/FlowViewSpec.tsx +++ b/web/src/js/__tests__/components/FlowViewSpec.tsx @@ -1,16 +1,16 @@ -import * as React from "react" -import {render, screen} from "../test-utils"; +import * as React from "react"; +import { 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"; +import { fireEvent } from "@testing-library/react"; enableFetchMocks(); test("FlowView", async () => { fetchMock.mockReject(new Error("backend missing")); - const {asFragment, store} = render(); + const { asFragment, store } = render(); expect(asFragment()).toMatchSnapshot(); fireEvent.click(screen.getByText("Response")); @@ -30,7 +30,7 @@ test("FlowView", async () => { 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")); @@ -46,4 +46,12 @@ test("FlowView", async () => { fireEvent.click(screen.getByText("Error")); expect(asFragment()).toMatchSnapshot(); + + store.dispatch(flowActions.select(store.getState().flows.list[4].id)); + + fireEvent.click(screen.getByText("Datagrams")); + expect(asFragment()).toMatchSnapshot(); + + fireEvent.click(screen.getByText("Error")); + 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..fb964f3f12 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 { 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() + store.dispatch(connectionActions.startFetching()); + expect(asFragment()).toMatchSnapshot(); - store.dispatch(connectionActions.connectionEstablished()) - expect(asFragment()).toMatchSnapshot() + store.dispatch(connectionActions.connectionEstablished()); + expect(asFragment()).toMatchSnapshot(); - store.dispatch(connectionActions.connectionError("wat")) - expect(asFragment()).toMatchSnapshot() + store.dispatch(connectionActions.connectionError("wat")); + expect(asFragment()).toMatchSnapshot(); - store.dispatch(connectionActions.setOffline()) - expect(asFragment()).toMatchSnapshot() + 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..f25e0e9d9a 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"; - -describe('FileMenu Component', () => { +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", () => { let store = TStore(), fileMenu = renderer.create( - + ), - tree = fileMenu.toJSON() + 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..b00d7a4313 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..5197152613 100644 --- a/web/src/js/__tests__/components/Header/FilterInputSpec.tsx +++ b/web/src/js/__tests__/components/Header/FilterInputSpec.tsx @@ -1,94 +1,109 @@ -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' +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', () => { +describe("FilterInput Component", () => { + it("should render correctly", () => { let filterInput = renderer.create( - undefined} value="42"/> + undefined} + value="42" + /> ), - tree = filterInput.toJSON() - expect(tree).toMatchSnapshot() - }) + 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 componentWillReceiveProps", () => { + filterInput.UNSAFE_componentWillReceiveProps({ value: "foo" }); + expect(filterInput.state.value).toEqual("foo"); + }); - it('should handle isValid', () => { + it("should handle isValid", () => { // valid - expect(filterInput.isValid("~u foo")).toBeTruthy() - expect(filterInput.isValid("~foo bar")).toBeFalsy() - }) + 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) + 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 = "~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.') - }) + 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 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 focus", () => { + filterInput.onFocus(); + expect(filterInput.state.focus).toBeTruthy(); + }); - it('should handle blur', () => { - filterInput.onBlur() - expect(filterInput.state.focus).toBeFalsy() - }) + 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 mouseEnter", () => { + filterInput.onMouseEnter(); + expect(filterInput.state.mousefocus).toBeTruthy(); + }); - it('should handle mouseLeave', () => { - filterInput.onMouseLeave() - expect(filterInput.state.mousefocus).toBeFalsy() - }) + it("should handle mouseLeave", () => { + filterInput.onMouseLeave(); + expect(filterInput.state.mousefocus).toBeFalsy(); + }); - let input = ReactDOM.findDOMNode(filterInput.refs.input) + let input = ReactDOM.findDOMNode(filterInput.refs.input); - it('should handle keyDown', () => { - input.blur = jest.fn() + it("should handle keyDown", () => { + input.blur = jest.fn(); let mockEvent = { key: "Escape", - stopPropagation: jest.fn() - } - filterInput.onKeyDown(mockEvent) - expect(input.blur).toBeCalled() - expect(filterInput.state.mousefocus).toBeFalsy() - expect(mockEvent.stopPropagation).toBeCalled() - }) + 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 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() - }) -}) + it("should handle select", () => { + input.select = jest.fn(); + 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..723d7c9f79 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 StartMenu from "../../../components/Header/StartMenu"; +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..1b3d5cd6da 100644 --- a/web/src/js/__tests__/components/Header/MenuToggleSpec.tsx +++ b/web/src/js/__tests__/components/Header/MenuToggleSpec.tsx @@ -1,44 +1,49 @@ -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" +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', () => { +describe("MenuToggle Component", () => { + it("should render correctly", () => { let changeFn = jest.fn(), menuToggle = renderer.create(

    foo children

    -
    ), - tree = menuToggle.toJSON() - expect(tree).toMatchSnapshot() - }) -}) + + ), + tree = menuToggle.toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); test("OptionsToggle", async () => { const store = TStore(), - {asFragment} = render( - toggle anticache, - {store} + { asFragment } = render( + toggle anticache, + { store } ); - globalThis.fetch = jest.fn() + globalThis.fetch = jest.fn(); expect(asFragment()).toMatchSnapshot(); fireEvent.click(screen.getByText("toggle anticache")); - expect(store.getActions()).toEqual([optionsEditorActions.startUpdate("anticache", true)]) + expect(store.getActions()).toEqual([ + optionsEditorActions.startUpdate("anticache", true), + ]); }); 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..a07c8f082a 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', () => { +describe("OptionMenu Component", () => { + it("should render correctly", () => { let store = TStore(), provider = renderer.create( - + ), - tree = provider.toJSON() - expect(tree).toMatchSnapshot() - }) -}) + tree = provider.toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/web/src/js/__tests__/components/Header/__snapshots__/MainMenuSpec.tsx.snap b/web/src/js/__tests__/components/Header/__snapshots__/MainMenuSpec.tsx.snap index b5164c65cc..b8e4969982 100644 --- a/web/src/js/__tests__/components/Header/__snapshots__/MainMenuSpec.tsx.snap +++ b/web/src/js/__tests__/components/Header/__snapshots__/MainMenuSpec.tsx.snap @@ -26,7 +26,7 @@ exports[`MainMenu 1`] = ` class="form-control" placeholder="Search" type="text" - value="~u /second | ~tcp | ~dns" + value="~u /second | ~tcp | ~dns | ~udp" />
    - Strip cache headers + Strip cache headers + { - - const {asFragment} = render(
    ); + const { asFragment } = render(
    ); expect(asFragment()).toMatchSnapshot(); fireEvent.click(screen.getByText("Options")); @@ -18,5 +16,5 @@ test("Header", async () => { expect(screen.getByText("Open...")).toBeTruthy(); fireEvent.click(screen.getByText("File")); - expect(screen.queryByText("Open...")).toBeNull() + expect(screen.queryByText("Open...")).toBeNull(); }); diff --git a/web/src/js/__tests__/components/Modal/ModalSpec.tsx b/web/src/js/__tests__/components/Modal/ModalSpec.tsx index 21e3790dce..fea1f258bc 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 } 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")); 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..4316a519d6 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' +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', () => { +describe("PureOptionDefault Component", () => { + it("should return null when the value is default", () => { let pureOptionDefault = renderer.create( - - ), - tree = pureOptionDefault.toJSON() - expect(tree).toMatchSnapshot() - }) + + ), + tree = pureOptionDefault.toJSON(); + expect(tree).toMatchSnapshot(); + }); - it('should handle boolean type', () => { + it("should handle boolean type", () => { let pureOptionDefault = renderer.create( - - ), - tree = pureOptionDefault.toJSON() - expect(tree).toMatchSnapshot() - }) - - it('should handle array', () => { - let a = [""], b = [], c = ['c'], + + ), + 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() - }) + + ); + tree = pureOptionDefault.toJSON(); + expect(tree).toMatchSnapshot(); + }); - it('should handle string', () => { + it("should handle string", () => { let pureOptionDefault = renderer.create( - - ), - tree = pureOptionDefault.toJSON() - expect(tree).toMatchSnapshot() - }) + + ), + tree = pureOptionDefault.toJSON(); + expect(tree).toMatchSnapshot(); + }); - it('should handle null value', () => { + it("should handle null value", () => { let pureOptionDefault = renderer.create( - - ), - tree = pureOptionDefault.toJSON() - expect(tree).toMatchSnapshot() - }) - -}) + + ), + 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..47540f73db 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' +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'], +describe("BooleanOption Component", () => { + let BooleanOption = Options["bool"], onChangeFn = jest.fn(), booleanOption = renderer.create( - + ), - tree = booleanOption.toJSON() + tree = booleanOption.toJSON(); - it('should render correctly', () => { - expect(tree).toMatchSnapshot() - }) + it("should render correctly", () => { + expect(tree).toMatchSnapshot(); + }); - it('should handle onChange', () => { + 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'], + 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() - }) + tree = stringOption.toJSON(); - it('should handle onChange', () => { - let mockEvent = { target: { value: 'bar' }} - tree.props.onChange(mockEvent) - expect(onChangeFn).toBeCalledWith(mockEvent.target.value) - }) + 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'], +describe("NumberOption Component", () => { + let NumberOption = Options["int"], onChangeFn = jest.fn(), numberOption = renderer.create( - + ), - tree = numberOption.toJSON() + tree = numberOption.toJSON(); - it('should render correctly', () => { - expect(tree).toMatchSnapshot() - }) + it("should render correctly", () => { + expect(tree).toMatchSnapshot(); + }); - it('should handle onChange', () => { - let mockEvent = {target: { value: '2'}} - tree.props.onChange(mockEvent) - expect(onChangeFn).toBeCalledWith(2) - }) -}) + it("should handle onChange", () => { + let mockEvent = { target: { value: "2" } }; + tree.props.onChange(mockEvent); + expect(onChangeFn).toBeCalledWith(2); + }); +}); -describe('ChoiceOption Component', () => { +describe("ChoiceOption Component", () => { let onChangeFn = jest.fn(), choiceOption = renderer.create( - + ), - tree = choiceOption.toJSON() + tree = choiceOption.toJSON(); - it('should render correctly', () => { - expect(tree).toMatchSnapshot() - }) + 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) - }) -}) + it("should handle onChange", () => { + let mockEvent = { target: { value: "b" } }; + tree.props.onChange(mockEvent); + expect(onChangeFn).toBeCalledWith(mockEvent.target.value); + }); +}); -describe('StringOption Component', () => { +describe("StringOption Component", () => { let onChangeFn = jest.fn(), - StringSequenceOption = Options['sequence of str'], + 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', '']) - }) -}) + 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", ""]); + }); +}); 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 32358e6103..3a0c54b2f5 100644 --- a/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.tsx.snap +++ b/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.tsx.snap @@ -193,6 +193,15 @@ exports[`Modal Component 2`] = ` value="8080" />
    +
    + Default: + + 8080 + + +
    diff --git a/web/src/js/__tests__/components/ProxyAppSpec.tsx b/web/src/js/__tests__/components/ProxyAppSpec.tsx index d5ed3e0fc9..1b10852cd2 100644 --- a/web/src/js/__tests__/components/ProxyAppSpec.tsx +++ b/web/src/js/__tests__/components/ProxyAppSpec.tsx @@ -1,15 +1,21 @@ -import * as React from "react" -import {render, screen, waitFor} from "../test-utils"; +import * as React from "react"; +import { render, screen, waitFor } from "../test-utils"; import ProxyApp from "../../components/ProxyApp"; -import {enableFetchMocks} from "jest-fetch-mock"; -import {ContentViewData} from "../../components/contentviews/useContent"; +import { enableFetchMocks } from "jest-fetch-mock"; +import { ContentViewData } from "../../components/contentviews/useContent"; enableFetchMocks(); test("ProxyApp", async () => { - const cv: ContentViewData = {lines: [[["text", "my data"]]], description: ""} - fetchMock.doMockOnceIf("./flows/flow2/request/content/Auto.json?lines=81", JSON.stringify(cv)); - render(); + const cv: ContentViewData = { + lines: [[["text", "my data"]]], + description: "", + }; + fetchMock.doMockOnceIf( + "./flows/flow2/request/content/Auto.json?lines=513", + JSON.stringify(cv) + ); + render(); expect(screen.getByTitle("Mitmproxy Version")).toBeDefined(); await waitFor(() => screen.getByText("my data")); }); diff --git a/web/src/js/__tests__/components/__snapshots__/CaptureSetupSpec.tsx.snap b/web/src/js/__tests__/components/__snapshots__/CaptureSetupSpec.tsx.snap new file mode 100644 index 0000000000..d37caa1c8e --- /dev/null +++ b/web/src/js/__tests__/components/__snapshots__/CaptureSetupSpec.tsx.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CaptureSetup 1`] = ` + +
    +

    + mitmproxy is running. +

    +

    + No flows have been recorded yet. +
    + Configure your client to use one of the following proxy servers: +

    +
      +
    • + + HTTP(S) proxy listening at 127.0.0.1:8080 and [::1]:8080. +
    • +
    • + + Reverse proxy to example.com (reverse:example.com): +
      + I failed somehow. +
    • +
    • + + SOCKS v5 proxy (socks5) +
    • +
    +
    +
    +`; diff --git a/web/src/js/__tests__/components/__snapshots__/FlowTableSpec.tsx.snap b/web/src/js/__tests__/components/__snapshots__/FlowTableSpec.tsx.snap index bae5c9b0da..3073b79e75 100644 --- a/web/src/js/__tests__/components/__snapshots__/FlowTableSpec.tsx.snap +++ b/web/src/js/__tests__/components/__snapshots__/FlowTableSpec.tsx.snap @@ -9,7 +9,7 @@ exports[`FlowTable Component should render correctly 1`] = ` @@ -72,6 +72,63 @@ exports[`FlowTable Component should render correctly 1`] = ` } } /> + + + +
    + + + + + + + http://address:22/path + + + WSS + + + 200 + + + 43b + + + 5s + + + - TCP Messages + Stream Data -

    - TCP Data -

    @@ -1079,7 +1076,7 @@ exports[`FlowView 8`] = ` class="" href="#" > - TCP Messages + Stream Data `; + +exports[`FlowView 12`] = ` + + + +`; + +exports[`FlowView 13`] = ` + +
    + +
    +
    + error +
    + + 1999-12-31 23:00:07.000 + +
    +
    +
    +
    +
    +`; diff --git a/web/src/js/__tests__/components/__snapshots__/HeaderSpec.tsx.snap b/web/src/js/__tests__/components/__snapshots__/HeaderSpec.tsx.snap index fbcde2c79b..b682adc517 100644 --- a/web/src/js/__tests__/components/__snapshots__/HeaderSpec.tsx.snap +++ b/web/src/js/__tests__/components/__snapshots__/HeaderSpec.tsx.snap @@ -375,7 +375,17 @@ exports[`Header 3`] = ` -  Save... +  Save + + +
  • + + +  Save filtered
  • diff --git a/web/src/js/__tests__/components/common/ButtonSpec.tsx b/web/src/js/__tests__/components/common/ButtonSpec.tsx index 661b4d553a..802073565a 100644 --- a/web/src/js/__tests__/components/common/ButtonSpec.tsx +++ b/web/src/js/__tests__/components/common/ButtonSpec.tsx @@ -1,26 +1,34 @@ -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', () => { +describe("Button Component", () => { + it("should render correctly", () => { let button = renderer.create( - - ), - tree = button.toJSON() - expect(tree).toMatchSnapshot() - }) + + ), + tree = button.toJSON(); + expect(tree).toMatchSnapshot(); + }); - it('should be able to be disabled', () => { + it("should be able to be disabled", () => { let button = renderer.create( - + ), - tree = button.toJSON() - expect(tree).toMatchSnapshot() - }) -}) + 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..ccd8c3f993 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", () => { + let docsLink = renderer.create( + + ), + 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", () => { + let docsLink = renderer.create(), + 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..590aae2f82 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 () => { +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( + 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() + ); + expect(asFragment()).toMatchSnapshot(); - fireEvent.mouseEnter(screen.getByText("submenu")) - await waitFor(() => screen.getByText("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() -}) + fireEvent.mouseLeave(screen.getByText("submenu")); + expect(screen.queryByText("click me")).toBeNull(); + expect(asFragment()).toMatchSnapshot(); +}); -test('MenuItem', async () => { +test("MenuItem", async () => { let click = jest.fn(); - const {asFragment} = render( - wtf - ) - expect(asFragment()).toMatchSnapshot() - fireEvent.click(screen.getByText("wtf")) + 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..77fc2af6a6 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..488656d91e 100644 --- a/web/src/js/__tests__/components/common/SplitterSpec.tsx +++ b/web/src/js/__tests__/components/common/SplitterSpec.tsx @@ -1,84 +1,105 @@ -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'; +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', () => { +describe("Splitter Component", () => { + it("should render correctly", () => { + let splitter = renderer.create(), + tree = splitter.toJSON(); + expect(tree).toMatchSnapshot(); + }); - it('should render correctly', () => { - let splitter = renderer.create(), - tree = splitter.toJSON() - expect(tree).toMatchSnapshot() - }) - - let splitter = TestUtils.renderIntoDocument(), + let splitter = TestUtils.renderIntoDocument(), dom = ReactDOM.findDOMNode(splitter), previousElementSibling = { offsetHeight: 0, offsetWidth: 0, - style: {flex: ''} + 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) - }) + style: { flex: "" }, + }; - it('should handle mouseUp', () => { + 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 + ); + }); - 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 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 mouseMove', () => { - splitter.onMouseMove({pageX: 10, pageY: 10}) - expect(dom.style.transform).toEqual("translate(9px, 0px)") + 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"); + }); - let splitterY = TestUtils.renderIntoDocument() - splitterY.onMouseMove({pageX: 10, pageY: 10}) - expect(ReactDOM.findDOMNode(splitterY).style.transform).toEqual("translate(0px, 10px)") - }) + it("should handle mouseMove", () => { + splitter.onMouseMove({ pageX: 10, pageY: 10 }); + expect(dom.style.transform).toEqual("translate(9px, 0px)"); - it('should handle resize', () => { - let x = jest.spyOn(window, 'setTimeout'); - splitter.onResize() - expect(x).toHaveBeenCalled() - }) + let splitterY = TestUtils.renderIntoDocument(); + splitterY.onMouseMove({ pageX: 10, pageY: 10 }); + expect(ReactDOM.findDOMNode(splitterY).style.transform).toEqual( + "translate(0px, 10px)" + ); + }); - it('should handle componentWillUnmount', () => { - splitter.componentWillUnmount() - expect(previousElementSibling.style.flex).toEqual('') - expect(nextElementSibling.style.flex).toEqual('') - expect(splitter.state.applied).toBeTruthy() - }) + it("should handle resize", () => { + let x = jest.spyOn(window, "setTimeout"); + splitter.onResize(); + expect(x).toHaveBeenCalled(); + }); - it('should handle reset', () => { - splitter.reset(false) - expect(splitter.state.applied).toBeFalsy() + it("should handle componentWillUnmount", () => { + splitter.componentWillUnmount(); + expect(previousElementSibling.style.flex).toEqual(""); + expect(nextElementSibling.style.flex).toEqual(""); + expect(splitter.state.applied).toBeTruthy(); + }); - expect(splitter.reset(true)).toEqual(undefined) - }) + it("should handle reset", () => { + splitter.reset(false); + expect(splitter.state.applied).toBeFalsy(); -}) + expect(splitter.reset(true)).toEqual(undefined); + }); +}); diff --git a/web/src/js/__tests__/components/common/ToggleButtonSpec.tsx b/web/src/js/__tests__/components/common/ToggleButtonSpec.tsx index 23378f5397..62e0d0e08d 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", () => { + let mockFunc = jest.fn(); - it('should render correctly', () => { + it("should render correctly", () => { let checkedButton = renderer.create( - ), - tree = checkedButton.toJSON() - expect(tree).toMatchSnapshot() - }) + + ), + tree = checkedButton.toJSON(); + expect(tree).toMatchSnapshot(); + }); - it('should handle click action', () => { + it("should handle click action", () => { let uncheckButton = renderer.create( - ), - tree = uncheckButton.toJSON() - tree.props.onClick() - expect(mockFunc).toBeCalled() - }) -}) + + ), + tree = uncheckButton.toJSON(); + tree.props.onClick(); + expect(mockFunc).toBeCalled(); + }); +}); 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 0a77d170e7..74ed7d015b 100644 --- a/web/src/js/__tests__/components/contentviews/HttpMessageSpec.tsx +++ b/web/src/js/__tests__/components/contentviews/HttpMessageSpec.tsx @@ -1,27 +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 {SHOW_MAX_LINES} from "../../../components/contentviews/useContent"; +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(SHOW_MAX_LINES).fill([["text", "data"]]).concat( - Array(SHOW_MAX_LINES).fill([["text", "additional"]]) - ); + const lines = Array(512) + .fill([["text", "data"]]) + .concat(Array(512).fill([["text", "additional"]])); fetchMock.mockResponses( JSON.stringify({ - lines: lines.slice(0, SHOW_MAX_LINES + 1), - description: "Auto" - }), JSON.stringify({ + lines: lines.slice(0, 512 + 1), + description: "Auto", + }), + JSON.stringify({ lines, - description: "Auto" - }), JSON.stringify({ + description: "Auto", + }), + JSON.stringify({ lines: Array(5).fill([["text", "rawdata"]]), description: "Raw", }), @@ -33,9 +36,11 @@ test("HttpMessage", async () => { ); 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")); @@ -55,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..551402a7f9 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..8e36a4e7c5 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..bf4bfcea7c 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..0caf9c6328 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..50cfa6e57d 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}/> + let 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..59b8af462d 100644 --- a/web/src/js/__tests__/components/helpers/AutoScrollSpec.tsx +++ b/web/src/js/__tests__/components/helpers/AutoScrollSpec.tsx @@ -1,41 +1,47 @@ 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' +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() +describe("Autoscroll", () => { + let mockFn = jest.fn(); class tComponent extends React.Component { - constructor(props, context){ - super(props, context) - this.state = { vScroll: calcVScroll() } + constructor(props, context) { + super(props, context); + this.state = { vScroll: calcVScroll() }; } - UNSAFE_componentWillUpdate() { - mockFn("foo") - } + UNSAFE_componentWillUpdate() { + mockFn("foo"); + } - componentDidUpdate() { - mockFn("bar") - } + componentDidUpdate() { + mockFn("bar"); + } - render() { - return (

    foo

    ) - } + render() { + return

    foo

    ; + } } - it('should update component', () => { + 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") + 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") - }) -}) + Object.defineProperty(viewport, "scrollHeight", { + value: 0, + writable: true, + }); + autoScroll.componentDidUpdate(); + expect(mockFn).toBeCalledWith("bar"); + }); +}); diff --git a/web/src/js/__tests__/components/helpers/VirtualScrollSpec.tsx b/web/src/js/__tests__/components/helpers/VirtualScrollSpec.tsx index 1dfe56d730..605aa32ec0 100644 --- a/web/src/js/__tests__/components/helpers/VirtualScrollSpec.tsx +++ b/web/src/js/__tests__/components/helpers/VirtualScrollSpec.tsx @@ -1,21 +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 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 new file mode 100644 index 0000000000..a847f318d9 --- /dev/null +++ b/web/src/js/__tests__/ducks/_tbackendstate.ts @@ -0,0 +1,47 @@ +/** Auto-generated by test_app.py:test_generate_state_js */ +import {BackendState} from '../../ducks/backendState'; +export function TBackendState(): Required { + return { + "available": true, + "contentViews": [ + "Auto", + "Raw" + ], + "servers": [ + { + "description": "HTTP(S) proxy", + "full_spec": "regular", + "is_running": true, + "last_exception": null, + "listen_addrs": [ + [ + "127.0.0.1", + 8080 + ], + [ + "::1", + 8080 + ] + ], + "type": "regular" + }, + { + "description": "reverse proxy to example.com", + "full_spec": "reverse:example.com", + "is_running": false, + "last_exception": "I failed somehow.", + "listen_addrs": [], + "type": "reverse" + }, + { + "description": "SOCKS v5 proxy", + "full_spec": "socks5", + "is_running": false, + "last_exception": null, + "listen_addrs": [], + "type": "socks5" + } + ], + "version": "1.2.3" + } +} diff --git a/web/src/js/__tests__/ducks/_tflow.ts b/web/src/js/__tests__/ducks/_tflow.ts index 6cafa28777..025fccdfef 100644 --- a/web/src/js/__tests__/ducks/_tflow.ts +++ b/web/src/js/__tests__/ducks/_tflow.ts @@ -1,5 +1,5 @@ /** Auto-generated by test_app.py:test_generate_tflow_js */ -import {HTTPFlow, TCPFlow, DNSFlow} from '../../flow'; +import {HTTPFlow, TCPFlow, UDPFlow, DNSFlow} from '../../flow'; export function THTTPFlow(): Required { return { "client_conn": { @@ -226,6 +226,72 @@ export function TTCPFlow(): Required { "type": "tcp" } } +export function TUDPFlow(): Required { + return { + "client_conn": { + "alpn": "http/1.1", + "cert": undefined, + "cipher": "cipher", + "id": "0a8833da-88e4-429d-ac54-61cda8a7f91c", + "peername": [ + "127.0.0.1", + 22 + ], + "sni": "address", + "sockname": [ + "", + 0 + ], + "timestamp_end": 946681206, + "timestamp_start": 946681200, + "timestamp_tls_setup": 946681201, + "tls_established": true, + "tls_version": "TLSv1.2" + }, + "comment": "", + "error": { + "msg": "error", + "timestamp": 946681207.0 + }, + "id": "f9f7b2b9-7727-4477-822d-d3526e5b8951", + "intercepted": false, + "is_replay": undefined, + "marked": "", + "messages_meta": { + "contentLength": 12, + "count": 2, + "timestamp_last": 946681204.5 + }, + "modified": false, + "server_conn": { + "address": [ + "address", + 22 + ], + "alpn": undefined, + "cert": undefined, + "cipher": undefined, + "id": "c49f9c2b-a729-4b16-9212-d181717e294b", + "peername": [ + "192.168.0.1", + 22 + ], + "sni": "address", + "sockname": [ + "address", + 22 + ], + "timestamp_end": 946681205, + "timestamp_start": 946681202, + "timestamp_tcp_setup": 946681203, + "timestamp_tls_setup": 946681204, + "tls_established": true, + "tls_version": "TLSv1.2" + }, + "timestamp_created": 946681200, + "type": "udp" + } +} export function TDNSFlow(): Required { return { "client_conn": { diff --git a/web/src/js/__tests__/ducks/commandBarSpec.tsx b/web/src/js/__tests__/ducks/commandBarSpec.tsx index 5262cbcb68..3480bf5980 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..0254411321 100644 --- a/web/src/js/__tests__/ducks/connectionSpec.tsx +++ b/web/src/js/__tests__/ducks/connectionSpec.tsx @@ -1,41 +1,54 @@ -import reduceConnection from "../../ducks/connection" -import * as ConnectionActions from "../../ducks/connection" -import { ConnectionState } from "../../ducks/connection" +import reduceConnection from "../../ducks/connection"; +import * 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..4560f85daf 100644 --- a/web/src/js/__tests__/ducks/eventLogSpec.tsx +++ b/web/src/js/__tests__/ducks/eventLogSpec.tsx @@ -1,38 +1,52 @@ -import reduceEventLog, * as eventLogActions from '../../ducks/eventLog' -import {reduce} from '../../ducks/utils/store' +import reduceEventLog, * as eventLogActions 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", () => { + let state = reduceEventLog(undefined, eventLogActions.add("foo")); + expect( + reduceEventLog(state, eventLogActions.toggleFilter("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", () => { + let 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", () => { + let 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 74b101bacb..03dc030ef8 100644 --- a/web/src/js/__tests__/ducks/flowsSpec.tsx +++ b/web/src/js/__tests__/ducks/flowsSpec.tsx @@ -1,192 +1,225 @@ 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'}) + s = reduceFlows(s, { + type: flowActions.ADD, + data: { id: i }, + cmd: "add", + }); } let 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( + 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', () => { + 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', () => { + 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')) + 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 download action', () => { - let state = reduceFlows(undefined, {}) - expect(reduceFlows(state, flowActions.download())).toEqual(state) - }) - - 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", () => { + let 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(), + b = TFlow(); a.request.scheme = "https"; a.request.method = "POST"; a.request.path = "/foo"; @@ -195,8 +228,7 @@ test("makeSort", () => { Object.keys(FlowColumns).forEach((column, i) => { // @ts-ignore - const sort = flowActions.makeSort({column, desc: i % 2 == 0}); + 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 index efe6ce7f89..c72900fbaa 100644 --- a/web/src/js/__tests__/ducks/indexSpec.tsx +++ b/web/src/js/__tests__/ducks/indexSpec.tsx @@ -1,11 +1,11 @@ -import {rootReducer} from '../../ducks/index' +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() - }) -}) +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/optionsSpec.tsx b/web/src/js/__tests__/ducks/optionsSpec.tsx index 4614b45bc3..b0e71b25ae 100644 --- a/web/src/js/__tests__/ducks/optionsSpec.tsx +++ b/web/src/js/__tests__/ducks/optionsSpec.tsx @@ -1,42 +1,55 @@ -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 * as OptionsEditorActions from "../../ducks/ui/optionsEditor"; +import { enableFetchMocks } from "jest-fetch-mock"; +import { TStore } from "./tutils"; +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', () => { - let 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", () => { + let 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)) + fetchMock.mockResponseOnce("fooerror", { status: 404 }); + await store.dispatch((dispatch) => + OptionsActions.pureSendUpdate("intercept", "~~~", dispatch) + ); expect(store.getActions()).toEqual([ - OptionsEditorActions.updateError("intercept", "fooerror") - ]) + OptionsEditorActions.updateError("intercept", "fooerror"), + ]); store.clearActions(); - fetchMock.mockResponseOnce("", {status: 200}); - await store.dispatch(dispatch => OptionsActions.pureSendUpdate("intercept", "valid", dispatch)) + fetchMock.mockResponseOnce("", { status: 200 }); + await store.dispatch((dispatch) => + OptionsActions.pureSendUpdate("intercept", "valid", dispatch) + ); expect(store.getActions()).toEqual([ - OptionsEditorActions.updateSuccess("intercept") - ]) - + OptionsEditorActions.updateSuccess("intercept"), + ]); }); test("save", async () => { @@ -60,7 +73,7 @@ test("addInterceptFilter", async () => { 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"}'); - - + 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..82d1933862 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/tutils.ts b/web/src/js/__tests__/ducks/tutils.ts index 7a72cb2f82..868f236075 100644 --- a/web/src/js/__tests__/ducks/tutils.ts +++ b/web/src/js/__tests__/ducks/tutils.ts @@ -1,74 +1,76 @@ -import thunk from 'redux-thunk' -import configureStore, {MockStoreCreator, MockStoreEnhanced} from 'redux-mock-store' -import {ConnectionState} from '../../ducks/connection' -import {TDNSFlow, THTTPFlow, TTCPFlow} from './_tflow' -import {AppDispatch, RootState} from "../../ducks"; -import {DNSFlow, HTTPFlow, TCPFlow} from "../../flow"; -import {defaultState as defaultConf} from "../../ducks/conf" -import {defaultState as defaultOptions} from "../../ducks/options" +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"; -const mockStoreCreator: MockStoreCreator = configureStore([thunk]) +const mockStoreCreator: MockStoreCreator = + configureStore([thunk]); -export {THTTPFlow as TFlow, TTCPFlow} +export { THTTPFlow as TFlow, TTCPFlow, TUDPFlow }; const tflow0: HTTPFlow = THTTPFlow(); const tflow1: HTTPFlow = THTTPFlow(); const tflow2: TCPFlow = TTCPFlow(); const tflow3: DNSFlow = TDNSFlow(); -tflow0.modified = true -tflow0.intercepted = true +const tflow4: UDPFlow = TUDPFlow(); +tflow0.modified = true; +tflow0.intercepted = true; tflow1.id = "flow2"; tflow1.request.path = "/second"; export const testState: RootState = { - conf: defaultConf, + 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}, + booleanOption: { isUpdating: true, error: false }, + strOption: { error: true }, intOption: {}, choiceOption: {}, - } + }, }, options: defaultOptions, flows: { @@ -78,29 +80,32 @@ export const testState: RootState = { [tflow1.id]: tflow1, [tflow2.id]: tflow2, [tflow3.id]: tflow3, + [tflow4.id]: tflow4, }, - filter: '~u /second | ~tcp | ~dns', - highlight: '~u /path', + filter: "~u /second | ~tcp | ~dns | ~udp", + highlight: "~u /path", sort: { desc: true, - column: "path" + column: "path", }, - view: [tflow1, tflow2, tflow3], - list: [tflow0, tflow1, tflow2, tflow3], + view: [tflow1, tflow2, tflow3, tflow4], + list: [tflow0, tflow1, tflow2, tflow3, tflow4], listIndex: { [tflow0.id]: 0, [tflow1.id]: 1, [tflow2.id]: 2, - [tflow3.id]: 3 + [tflow3.id]: 3, + [tflow4.id]: 4, }, viewIndex: { [tflow1.id]: 0, [tflow2.id]: 1, [tflow3.id]: 2, + [tflow4.id]: 3, }, }, connection: { - state: ConnectionState.ESTABLISHED + state: ConnectionState.ESTABLISHED, }, eventLog: { visible: true, @@ -109,23 +114,22 @@ 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: "info", message: "foo" }, + { id: "2", level: "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, - } -} - + }, +}; export function TStore(): MockStoreEnhanced { - return mockStoreCreator(testState) + return mockStoreCreator(testState); } diff --git a/web/src/js/__tests__/ducks/ui/flowSpec.tsx b/web/src/js/__tests__/ducks/ui/flowSpec.tsx index 7b1bbdeab6..ad397004f6 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") - }) + ).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..3db54fe7b3 100644 --- a/web/src/js/__tests__/ducks/ui/indexSpec.tsx +++ b/web/src/js/__tests__/ducks/ui/indexSpec.tsx @@ -1,8 +1,8 @@ -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", () => { + let state = reduceUI(undefined, { type: "other" }); + expect(state.hasOwnProperty("flow")).toBeTruthy(); + }); +}); diff --git a/web/src/js/__tests__/ducks/ui/keyboardSpec.tsx b/web/src/js/__tests__/ducks/ui/keyboardSpec.tsx index 514cfb98cf..b1803ee9a0 100644 --- a/web/src/js/__tests__/ducks/ui/keyboardSpec.tsx +++ b/web/src/js/__tests__/ducks/ui/keyboardSpec.tsx @@ -1,20 +1,20 @@ 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"; +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') +jest.mock("../../../utils"); -describe('onKeyDown', () => { +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' - }) + data: { id: i + "", request: true, response: true, type: "http" }, + cmd: "add", + }); } const store = TStore(); @@ -22,150 +22,184 @@ describe('onKeyDown', () => { let createKeyEvent = (key, ctrlKey = false) => { // @ts-ignore - return onKeyDown({key, ctrlKey, preventDefault: jest.fn()}) - } + return onKeyDown({ key, ctrlKey, preventDefault: jest.fn() }); + }; afterEach(() => { - store.clearActions() + 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', () => { + 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", () => { // resume all - store.dispatch(createKeyEvent("A")) - expect(fetchApi).toBeCalledWith('/flows/resume', {method: 'POST'}) + store.dispatch(createKeyEvent("A")); + expect(fetchApi).toBeCalledWith("/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).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", () => { // kill all - store.dispatch(createKeyEvent("X")) - expect(fetchApi).toBeCalledWith('/flows/kill', {method: 'POST'}) + store.dispatch(createKeyEvent("X")); + expect(fetchApi).toBeCalledWith("/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).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 }]); + }); +}); diff --git a/web/src/js/__tests__/ducks/ui/modalSpec.tsx b/web/src/js/__tests__/ducks/ui/modalSpec.tsx index b6ef5e6924..7773ee561f 100644 --- a/web/src/js/__tests__/ducks/ui/modalSpec.tsx +++ b/web/src/js/__tests__/ducks/ui/modalSpec.tsx @@ -1,24 +1,17 @@ -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", () => { + let 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", () => { + let 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..814e782368 100644 --- a/web/src/js/__tests__/ducks/ui/optionEditorSpec.tsx +++ b/web/src/js/__tests__/ducks/ui/optionEditorSpec.tsx @@ -1,33 +1,60 @@ -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"; +import { OptionsState } from "../../../ducks/_options_gen"; -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", () => { + let 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..6b4d6d6b9a 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", () => { + let a = { id: "1" }, + b = { id: "9" }, + 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}, + 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 }, 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", () => { + let a = { id: "1" }, + 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)) + let a: TItem = { id: "1", foo: "foo" }, + updated = { ...a, foo: "bar" }, + 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", () => { + 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"; + }) + ); 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", () => { + 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({ // 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 }, + }); + + 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({ // 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", () => { + 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 }, 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", () => { + 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 }, 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", () => { + let a = { id: "1" }, + b = { id: "2" }, + 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", () => { + let a = { id: "1" }, + b = { id: "2" }, + 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 0d63081b65..271aa04936 100644 --- a/web/src/js/__tests__/flow/utilsSpec.tsx +++ b/web/src/js/__tests__/flow/utilsSpec.tsx @@ -1,92 +1,107 @@ -import * as utils from '../../flow/utils' -import {TFlow, TTCPFlow} 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', () => { +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) - }) + 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', () => { + it("should be possible to get Content-Type", () => { let 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', () => { + 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() - }) + 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( + 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( + ); + 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( + 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', () => { +describe("RequestUtils", () => { + it("should be possible prettify url", () => { let flow = TFlow(); expect(utils.RequestUtils.pretty_url(flow.request)).toEqual( "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", () => { + let 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', () => { +it("should be possible to get an end time", () => { let 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__/test-utils.tsx b/web/src/js/__tests__/test-utils.tsx index b479b089ef..fc5736fdc4 100644 --- a/web/src/js/__tests__/test-utils.tsx +++ b/web/src/js/__tests__/test-utils.tsx @@ -1,31 +1,24 @@ -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 * 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 { createAppStore } from "../ducks"; +import { testState } 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 - } = {} + { store = createAppStore(testState), ...renderOptions } = {} ) { - function Wrapper({children}) { - return {children} + function Wrapper({ children }) { + 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..e530acc954 100644 --- a/web/src/js/__tests__/urlStateSpec.tsx +++ b/web/src/js/__tests__/urlStateSpec.tsx @@ -1,103 +1,114 @@ -import initialize from '../urlState' -import { updateStoreFromUrl, updateUrlFromStore } from '../urlState' +import initialize from "../urlState"; +import { 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 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 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"; + let 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"; + let 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" + 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" }]) - }) + 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(); + let 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"; + let 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', () => { +describe("updateUrlFromStore", () => { let initialState = { - flows: reduceFlows(undefined, {type: "other"}), - ui: reduceUI(undefined, {type: "other"}), - eventLog: reduceEventLog(undefined, {type: "other"}), - commandBar: reduceCommandBar(undefined, {type: "other"}), - } + 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", () => { + let store = mockStore(initialState); + updateUrlFromStore(store); + expect(history.replaceState).toBeCalledWith(undefined, "", "/#/flows"); + }); - it('should update url', () => { + it("should update url", () => { let flows = reduceFlows(undefined, flowsActions.select("123")), state = { ...initialState, - flows: reduceFlows(flows, flowsActions.setFilter('~u foo')) + flows: reduceFlows(flows, flowsActions.setFilter("~u foo")), }, - store = mockStore(state) - updateUrlFromStore(store) - expect(history.replaceState).toBeCalledWith(undefined, '', '/#/flows/123/request?s=~u%20foo') - }) -}) + store = mockStore(state); + updateUrlFromStore(store); + expect(history.replaceState).toBeCalledWith( + undefined, + "", + "/#/flows/123/request?s=~u%20foo" + ); + }); +}); -describe('initialize', () => { +describe("initialize", () => { let initialState = { - flows: reduceFlows(undefined, {type: "other"}), - ui: reduceUI(undefined, {type: "other"}), - eventLog: reduceEventLog(undefined, {type: "other"}), - commandBar: reduceCommandBar(undefined, {type: "other"}), - } + 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", () => { + let 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 7f2becfecc..b91fa84114 100644 --- a/web/src/js/__tests__/utilsSpec.tsx +++ b/web/src/js/__tests__/utilsSpec.tsx @@ -1,78 +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('reverseString', () => { - it('should return reversed string', () => { - let str1 = "abc", str2 = "xyz" - expect(utils.reverseString(str1) > utils.reverseString(str2)).toBeTruthy() - }) -}) +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('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("reverseString", () => { + it("should return reversed string", () => { + let str1 = "abc", + str2 = "xyz"; + expect( + utils.reverseString(str1) > utils.reverseString(str2) + ).toBeTruthy(); + }); +}); - utils.fetchApi('http://foo?bar=1', {method: "POST"}) - expect(fetchMock.mock.calls[0][0]).toEqual( - "http://foo?bar=1" - ) +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"); + }); - 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", () => { + 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] }); + }); +}); diff --git a/web/src/js/app.tsx b/web/src/js/app.tsx index 69fd66788a..ae1e4697f6 100644 --- a/web/src/js/app.tsx +++ b/web/src/js/app.tsx @@ -1,34 +1,33 @@ -import * as React from "react" -import {render} from 'react-dom' -import {Provider} from 'react-redux' +import * as React from "react"; +import { render } from "react-dom"; +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) +useUrlState(store); // @ts-ignore if (window.MITMWEB_STATIC) { // @ts-ignore - window.backend = new StaticBackend(store) + window.backend = new StaticBackend(store); } else { // @ts-ignore - window.backend = new WebSocketBackend(store) + 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', () => { +document.addEventListener("DOMContentLoaded", () => { render( - + , document.getElementById("mitmproxy") - ) -}) + ); +}); diff --git a/web/src/js/backends/static.tsx b/web/src/js/backends/static.tsx index 79d537ba77..bb40bc43c5 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}) + let 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 060b7bd564..58afa0b45b 100644 --- a/web/src/js/backends/websocket.tsx +++ b/web/src/js/backends/websocket.tsx @@ -3,91 +3,100 @@ * 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"; -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("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 + let 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}) + let 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)) + 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)); 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) { // FIXME - console.error("websocket connection errored", arguments) + console.error("websocket connection errored", arguments); } } diff --git a/web/src/js/components/CaptureSetup.tsx b/web/src/js/components/CaptureSetup.tsx new file mode 100644 index 0000000000..a13681919e --- /dev/null +++ b/web/src/js/components/CaptureSetup.tsx @@ -0,0 +1,117 @@ +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..42f3ef915a 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[] = [] +function getAvailableCommands( + commands: AllCommands, + input: string = "" +): string[] { + if (!commands) return []; + let availableCommands: string[] = []; for (const [command, args] of Object.entries(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) { + let 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 index 40fe900ed3..c07cff9bcc 100644 --- a/web/src/js/components/EventLog.jsx +++ b/web/src/js/components/EventLog.jsx @@ -1,75 +1,81 @@ -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' +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) + super(props, context); - this.state = { height: this.props.defaultHeight } + this.state = { height: this.props.defaultHeight }; - this.onDragStart = this.onDragStart.bind(this) - this.onDragMove = this.onDragMove.bind(this) - this.onDragStop = this.onDragStop.bind(this) + 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) + 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 }) + event.preventDefault(); + this.setState({ height: this.dragStart - event.pageY }); } onDragStop(event) { - event.preventDefault() - window.removeEventListener('mousemove', this.onDragMove) + event.preventDefault(); + window.removeEventListener("mousemove", this.onDragMove); } render() { - const { height } = this.state - const { filters, events, toggleFilter, close } = this.props + const { height } = this.state; + const { filters, events, toggleFilter, close } = this.props; return (
    Eventlog
    - {['debug', 'info', 'web', 'warn', 'error'].map(type => ( - toggleFilter(type)}/> - ))} + {["debug", "info", "web", "warn", "error"].map( + (type) => ( + toggleFilter(type)} + /> + ) + )}
    - ) + ); } } export default connect( - state => ({ + (state) => ({ filters: state.eventLog.filters, events: state.eventLog.view, }), @@ -77,4 +83,4 @@ export default connect( close: toggleVisibility, toggleFilter: toggleFilter, } -)(PureEventLog) +)(PureEventLog); diff --git a/web/src/js/components/EventLog/EventList.tsx b/web/src/js/components/EventLog/EventList.tsx index 2b25a31aee..f52f656835 100644 --- a/web/src/js/components/EventLog/EventList.tsx +++ b/web/src/js/components/EventLog/EventList.tsx @@ -1,108 +1,112 @@ -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 ReactDOM from "react-dom"; +import shallowEqual from "shallowequal"; +import AutoScroll from "../helpers/AutoScroll"; +import { calcVScroll, VScroll } from "../helpers/VirtualScroll"; +import { EventLogItem } from "../../ducks/eventLog"; type EventLogListProps = { - events: EventLogItem[] - rowHeight: number -} + events: EventLogItem[]; + rowHeight: number; +}; type EventLogListState = { - vScroll: VScroll -} + vScroll: VScroll; +}; class EventLogList extends Component { - static propTypes = { events: PropTypes.array.isRequired, rowHeight: PropTypes.number, - } + }; static defaultProps = { rowHeight: 18, - } + }; - heights: {[id: string]: number} + heights: { [id: string]: number }; 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() + this.onViewportUpdate(); } onViewportUpdate() { - const viewport = ReactDOM.findDOMNode(this) + const viewport = ReactDOM.findDOMNode(this); 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 + const icon = + { + web: "html5", + debug: "bug", + warn: "exclamation-triangle", + error: "ban", + }[event.level] || "info"; + return ; } -export default AutoScroll(EventLogList) +export default AutoScroll(EventLogList); diff --git a/web/src/js/components/FlowTable.jsx b/web/src/js/components/FlowTable.jsx index 45f12e9d3b..379c67e3a3 100644 --- a/web/src/js/components/FlowTable.jsx +++ b/web/src/js/components/FlowTable.jsx @@ -1,108 +1,126 @@ -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" - +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) + super(props, context); - this.state = { vScroll: calcVScroll() } - this.onViewportUpdate = this.onViewportUpdate.bind(this) + this.state = { vScroll: calcVScroll() }; + this.onViewportUpdate = this.onViewportUpdate.bind(this); } UNSAFE_componentWillMount() { - window.addEventListener('resize', this.onViewportUpdate) + window.addEventListener("resize", this.onViewportUpdate); + } + + componentDidMount() { + this.onViewportUpdate(); } UNSAFE_componentWillUnmount() { - window.removeEventListener('resize', this.onViewportUpdate) + window.removeEventListener("resize", this.onViewportUpdate); } componentDidUpdate() { - this.onViewportUpdate() + this.onViewportUpdate(); if (!this.shouldScrollIntoView) { - return + return; } - this.shouldScrollIntoView = false + this.shouldScrollIntoView = false; - const { rowHeight, flows, selected } = this.props - const viewport = ReactDOM.findDOMNode(this) - const head = ReactDOM.findDOMNode(this.refs.head) + 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 headHeight = head ? head.offsetHeight : 0; - const rowTop = (flows.indexOf(selected) * rowHeight) + headHeight - const rowBottom = rowTop + rowHeight + const rowTop = flows.indexOf(selected) * rowHeight + headHeight; + const rowBottom = rowTop + rowHeight; - const viewportTop = viewport.scrollTop - const viewportHeight = viewport.offsetHeight + const viewportTop = viewport.scrollTop; + const viewportHeight = viewport.offsetHeight; // Account for pinned thead if (rowTop - headHeight < viewportTop) { - viewport.scrollTop = rowTop - headHeight + viewport.scrollTop = rowTop - headHeight; } else if (rowBottom > viewportTop + viewportHeight) { - viewport.scrollTop = rowBottom - viewportHeight + viewport.scrollTop = rowBottom - viewportHeight; } } UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.selected && nextProps.selected !== this.props.selected) { - this.shouldScrollIntoView = true + this.shouldScrollIntoView = true; } } onViewportUpdate() { - const viewport = ReactDOM.findDOMNode(this) - const viewportTop = viewport.scrollTop || 0 + 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)) { - this.setState({ vScroll, viewportTop }) + }); + + 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 + 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 => ( + + {flows.slice(vScroll.start, vScroll.end).map((flow) => ( ))} - +
    - ) + ); } } -export const PureFlowTable = AutoScroll(FlowTable) +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) +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/FlowColumns.tsx b/web/src/js/components/FlowTable/FlowColumns.tsx index 5e0cce01cd..e7d1a659ae 100644 --- a/web/src/js/components/FlowTable/FlowColumns.tsx +++ b/web/src/js/components/FlowTable/FlowColumns.tsx @@ -1,15 +1,21 @@ -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, 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 * 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; @@ -18,178 +24,225 @@ interface FlowColumn { 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 = ""; +tls.sortKey = (flow) => flow.type === "http" && flow.request.scheme; -export const icon: FlowColumn = ({flow}) => { +export const icon: FlowColumn = ({ flow }) => { return ( -
    +
    - ) -} -icon.headerName = '' -icon.sortKey = flow => getIcon(flow) + ); +}; +icon.headerName = ""; +icon.sortKey = (flow) => getIcon(flow); const getIcon = (flow: Flow): string => { - if (flow.type === "tcp" || flow.type === "dns") { - return `resource-icon-${flow.type}` + if (flow.type !== "http") { + if (flow.client_conn.tls_version === "QUIC") { + return `resource-icon-quic`; + } + return `resource-icon-${flow.type}`; } if (flow.websocket) { - return 'resource-icon-websocket' + return "resource-icon-websocket"; } if (!flow.response) { - return 'resource-icon-plain' + return "resource-icon-plain"; } - var contentType = ResponseUtils.getContentType(flow.response) || '' + var contentType = ResponseUtils.getContentType(flow.response) || ""; if (flow.response.status_code === 304) { - return 'resource-icon-not-modified' + return "resource-icon-not-modified"; } if (300 <= flow.response.status_code && flow.response.status_code < 400) { - return 'resource-icon-redirect' + return "resource-icon-redirect"; } - if (contentType.indexOf('image') >= 0) { - return 'resource-icon-image' + if (contentType.indexOf("image") >= 0) { + return "resource-icon-image"; } - if (contentType.indexOf('javascript') >= 0) { - return 'resource-icon-js' + if (contentType.indexOf("javascript") >= 0) { + return "resource-icon-js"; } - if (contentType.indexOf('css') >= 0) { - return 'resource-icon-css' + if (contentType.indexOf("css") >= 0) { + return "resource-icon-css"; } - if (contentType.indexOf('html') >= 0) { - return 'resource-icon-document' + if (contentType.indexOf("html") >= 0) { + return "resource-icon-document"; } - return 'resource-icon-plain' -} + return "resource-icon-plain"; +}; const mainPath = (flow: Flow): string => { switch (flow.type) { case "http": - return RequestUtils.pretty_url(flow.request) + return RequestUtils.pretty_url(flow.request); case "tcp": - return `${flow.client_conn.peername.join(':')} ↔ ${flow.server_conn?.address?.join(':')}` + 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(", ") ?? "...") || "?"}` + return `${flow.request.questions + .map((q) => `${q.name} ${q.type}`) + .join(", ")} = ${ + (flow.response?.answers.map((q) => q.data).join(", ") ?? + "...") || + "?" + }`; } -} +}; -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) +path.headerName = "Path"; +path.sortKey = (flow) => mainPath(flow); -export const method: FlowColumn = ({flow}) => {method.sortKey(flow)} -method.headerName = 'Method' -method.sortKey = 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() + 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(); } -} +}; + +export const version: FlowColumn = ({ flow }) => ( + {version.sortKey(flow)} +); +version.headerName = "Version"; +version.sortKey = (flow) => { + switch (flow.type) { + case "http": + return flow.request.http_version; + default: + return ""; + } +}; -export const status: FlowColumn = ({flow}) => { - let color = 'darkred' +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 => { + + {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 + 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))} - ) }; -size.headerName = 'Size' -size.sortKey = flow => getTotalSize(flow) +export const size: FlowColumn = ({ flow }) => { + return {formatSize(getTotalSize(flow))}; +}; +size.headerName = "Size"; +size.sortKey = (flow) => getTotalSize(flow); -export const time: FlowColumn = ({flow}) => { - const start = startTime(flow), end = endTime(flow); +export const time: FlowColumn = ({ flow }) => { + const start = startTime(flow), + 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); + ); +}; +time.headerName = "Time"; +time.sortKey = (flow) => { + const start = startTime(flow), + end = endTime(flow); return start && end && end - start; -} +}; -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) + ); +}; +timestamp.headerName = "Start time"; +timestamp.sortKey = (flow) => startTime(flow); const markers = { ":red_circle:": "🔴", @@ -199,43 +252,57 @@ const markers = { ":large_blue_circle:": "🔵", ":purple_circle:": "🟣", ":brown_circle:": "🟤", -} +}; -export const quickactions: FlowColumn = ({flow}) => { - const dispatch = useDispatch() - let [open, setOpen] = useState(false) +export const quickactions: FlowColumn = ({ flow }) => { + const dispatch = useDispatch(); + let [open, setOpen] = useState(false); 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} -
    + 0} + > + {resume_or_replay ?
    {resume_or_replay}
    : <>} - ) -} + ); +}; -quickactions.headerName = '' -quickactions.sortKey = flow => 0; +quickactions.headerName = ""; +quickactions.sortKey = (flow) => 0; export default { icon, method, + version, path, quickactions, size, status, time, timestamp, - tls + tls, }; diff --git a/web/src/js/components/FlowTable/FlowRow.tsx b/web/src/js/components/FlowTable/FlowRow.tsx index 8e2d6ac0d4..84ac55a889 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) { +export default React.memo(function FlowRow({ + flow, + selected, + highlighted, +}: FlowRowProps) { const dispatch = useAppDispatch(), - displayColumnNames = useAppSelector(state => state.options.web_columns), + 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, - }) + 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..9f566ce62d 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); + sortDesc = useAppSelector((state) => state.flows.sort.desc), + sortColumn = useAppSelector((state) => state.flows.sort.column), + 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 53df8b8ae1..15153e9b13 100644 --- a/web/src/js/components/FlowView.tsx +++ b/web/src/js/components/FlowView.tsx @@ -1,68 +1,81 @@ -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 * 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 { 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"; 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, connection: Connection, timing: Timing, websocket: WebSocket, - messages: TcpMessages, + tcpmessages: TcpMessages, + udpmessages: UdpMessages, dnsrequest: DnsRequest, dnsresponse: DnsResponse, -} +}; 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 = ["messages"] - break + tabs = ["tcpmessages"]; + break; + case "udp": + 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"); return tabs; } export default function FlowView() { const dispatch = useAppDispatch(), - flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]]), + flow = useAppSelector( + (state) => state.flows.byId[state.flows.selected[0]] + ), tabs = tabsForFlow(flow); - let active = useAppSelector(state => state.ui.flow.tab) + 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]; @@ -70,17 +83,21 @@ export default function FlowView() { return ( - ) + ); } 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 2eb6818f32..51483d2627 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 => ( - - - - - - ))} + {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 => ( - - - - - - - - ))} + {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..c4e7873c0e 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 81c0bc33fc..e380cb85cb 100644 --- a/web/src/js/components/FlowView/Messages.tsx +++ b/web/src/js/components/FlowView/Messages.tsx @@ -1,50 +1,86 @@ -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, SHOW_MAX_LINES, 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(SHOW_MAX_LINES); - const showMore = useCallback(() => setMaxLines(Math.max(1024, maxLines * 2)), [maxLines]); + 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 content = useContent( MessageUtils.getContentURL(flow, "messages", contentView, maxLines + 1), flow.id + messages_meta.count ); - const messages = useMemo(() => content && JSON.parse(content), [content]) || []; + const messages = + useMemo(() => { + if (content) { + try { + return JSON.parse(content); + } catch (e) { + const err: ContentViewData[] = [ + { + description: "Network Error", + lines: [[["error", `${content}`]]], + }, + ]; + return err; + } + } + }, [content]) || []; 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)} - - -
    ; + 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; 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..7657295435 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,15 @@ 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((props) => ( + + ))}
    - ) + ); } -Timing.displayName = "Timing" +Timing.displayName = "Timing"; diff --git a/web/src/js/components/FlowView/UdpMessages.tsx b/web/src/js/components/FlowView/UdpMessages.tsx new file mode 100644 index 0000000000..79e7c3b79c --- /dev/null +++ b/web/src/js/components/FlowView/UdpMessages.tsx @@ -0,0 +1,12 @@ +import { UDPFlow } from "../../flow"; +import * as React from "react"; +import Messages from "./Messages"; + +export default function UdpMessages({ flow }: { flow: UDPFlow }) { + return ( +
    + +
    + ); +} +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 4df2683b26..dfc364bf13 100644 --- a/web/src/js/components/Footer.tsx +++ b/web/src/js/components/Footer.tsx @@ -1,72 +1,85 @@ -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.conf.version); + const version = useAppSelector((state) => state.backendState.version); let { - mode, intercept, showhost, upstream_cert, rawtcp, dns_server, http2, websocket, anticache, anticomp, - stickyauth, stickycookie, stream_large_bodies, listen_host, listen_port, server, ssl_insecure - } = useAppSelector(state => state.options); + 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 (
    - {mode && mode !== "regular" && ( - {mode} mode + {mode && (mode.length !== 1 || mode[0] !== "regular") && ( + {mode.join(",")} )} {intercept && ( - Intercept: {intercept} + + Intercept: {intercept} + )} {ssl_insecure && ( ssl_insecure )} - {showhost && ( - showhost - )} + {showhost && showhost} {!upstream_cert && ( no-upstream-cert )} - {!rawtcp && ( - no-raw-tcp - )} - {dns_server && ( - dns-server - )} - {!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} - ) - } + {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..c25c269679 100644 --- a/web/src/js/components/Header.tsx +++ b/web/src/js/components/Header.tsx @@ -1,12 +1,12 @@ -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, { 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"; interface Menu { (): JSX.Element; @@ -15,7 +15,9 @@ interface Menu { } export default function Header() { - const selectedFlows = useAppSelector(state => state.flows.selected.filter(id => id in state.flows.byId)), + const selectedFlows = useAppSelector((state) => + state.flows.selected.filter((id) => id in state.flows.byId) + ), [ActiveMenu, setActiveMenu] = useState(() => StartMenu), [wasFlowSelected, setWasFlowSelected] = useState(false); @@ -25,40 +27,42 @@ export default function Header() { setActiveMenu(() => FlowMenu); setWasFlowSelected(true); } - entries.push(FlowMenu) + entries.push(FlowMenu); } else { if (wasFlowSelected) { setWasFlowSelected(false); } if (ActiveMenu === FlowMenu) { - setActiveMenu(() => StartMenu) + setActiveMenu(() => StartMenu); } } function handleClick(active: Menu, e) { - e.preventDefault() - setActiveMenu(() => active) + e.preventDefault(); + setActiveMenu(() => active); } return (
    - +
    - ) + ); } diff --git a/web/src/js/components/Header/ConnectionIndicator.tsx b/web/src/js/components/Header/ConnectionIndicator.tsx index e3246d6784..29823481d2 100644 --- a/web/src/js/components/Header/ConnectionIndicator.tsx +++ b/web/src/js/components/Header/ConnectionIndicator.tsx @@ -1,27 +1,40 @@ 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) + const connState = useAppSelector((state) => state.connection.state), + 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; + return ( + offline + ); default: const exhaustiveCheck: never = connState; throw "unknown connection state"; } -}) +}); diff --git a/web/src/js/components/Header/FileMenu.tsx b/web/src/js/components/Header/FileMenu.tsx index e7af16a796..f7e5bc7c1f 100644 --- a/web/src/js/components/Header/FileMenu.tsx +++ b/web/src/js/components/Header/FileMenu.tsx @@ -1,43 +1,62 @@ -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 { useDispatch } from "react-redux"; +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"; export default React.memo(function FileMenu() { - const dispatch = useDispatch(); + const dispatch = useDispatch(), + 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 }} />
  • - dispatch(flowsActions.download())}> -  Save... + location.replace("/flows/dump")}> + +  Save + + 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..26fb94667e 100644 --- a/web/src/js/components/Header/FilterDocs.tsx +++ b/web/src/js/components/Header/FilterDocs.tsx @@ -1,64 +1,78 @@ -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..64f17feced 100644 --- a/web/src/js/components/Header/FilterInput.tsx +++ b/web/src/js/components/Header/FilterInput.tsx @@ -1,123 +1,135 @@ -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 ReactDOM from "react-dom"; +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 + ) => { type: string; filter?: string; highlight?: string } | void; +}; type FilterInputState = { - value: string - focus: boolean - mousefocus: boolean -} - -export default class FilterInput extends Component { - + value: string; + focus: boolean; + mousefocus: boolean; +}; + +export default class FilterInput extends Component< + FilterInputProps, + FilterInputState +> { 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 }); + ReactDOM.findDOMNode(this.refs.input).focus(); } blur() { - ReactDOM.findDOMNode(this.refs.input).blur() + ReactDOM.findDOMNode(this.refs.input).blur(); } select() { - ReactDOM.findDOMNode(this.refs.input).select() + ReactDOM.findDOMNode(this.refs.input).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/FlowMenu.tsx b/web/src/js/components/Header/FlowMenu.tsx index 9d6e6cceb1..cbfa7b456a 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]]) + 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..4a6b3b3b0d 100644 --- a/web/src/js/components/Header/MenuToggle.tsx +++ b/web/src/js/components/Header/MenuToggle.tsx @@ -1,38 +1,35 @@ 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 { 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"; 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) { +export function OptionsToggle({ name, children }: OptionsToggleProps) { const dispatch = useAppDispatch(), - value = useAppSelector(state => state.options[name]); + value = useAppSelector((state) => state.options[name]); return ( {children} - ) + ); } - export function EventlogToggle() { const dispatch = useDispatch(), - visible = useAppSelector(state => state.eventLog.visible); + visible = useAppSelector((state) => state.eventLog.visible); return ( Display Event Log - ) + ); } export function CommandBarToggle() { const dispatch = useDispatch(), - visible = useAppSelector(state => state.commandBar.visible); + 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..7fdc3466f2 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,7 +31,8 @@ export default function OptionMenu() {
    - Strip cache headers + Strip cache headers{" "} + Use host header for display @@ -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 index 0cc363482e..58f57e3e3d 100644 --- a/web/src/js/components/Header/StartMenu.tsx +++ b/web/src/js/components/Header/StartMenu.tsx @@ -1,78 +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"; +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" +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))} - /> + 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))} - /> + 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))} - /> + 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 0e615d4c79..c430156ec3 100644 --- a/web/src/js/components/MainView.tsx +++ b/web/src/js/components/MainView.tsx @@ -1,16 +1,21 @@ -import * as React from "react" -import Splitter from './common/Splitter' -import FlowTable from './FlowTable' -import FlowView from './FlowView' -import {useAppSelector} from "../ducks"; +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"; export default function MainView() { - const hasSelection = useAppSelector(state => !!state.flows.byId[state.flows.selected[0]]) + const hasSelection = useAppSelector( + (state) => !!state.flows.byId[state.flows.selected[0]] + ); + const hasFlows = useAppSelector((state) => state.flows.list.length > 0); return (
    - - {hasSelection && } - {hasSelection && } + {hasFlows ? : } + + {hasSelection && } + {hasSelection && }
    - ) + ); } diff --git a/web/src/js/components/Modal/Modal.tsx b/web/src/js/components/Modal/Modal.tsx index d9a475eb91..e5cd230346 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 index 1b2ec1a70f..7ea333139e 100644 --- a/web/src/js/components/Modal/Option.jsx +++ b/web/src/js/components/Modal/Option.jsx @@ -1,149 +1,156 @@ -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' +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 => { +const stopPropagation = (e) => { if (e.key !== "Escape") { - e.stopPropagation() + e.stopPropagation(); } -} +}; BooleanOption.propTypes = { value: PropTypes.bool.isRequired, onChange: PropTypes.func.isRequired, -} +}; -function BooleanOption({value, onChange, ...props}) { +function BooleanOption({ value, onChange, ...props }) { return (
    - ) + ); } StringOption.propTypes = { value: PropTypes.string, onChange: PropTypes.func.isRequired, -} +}; -function StringOption({value, onChange, ...props}) { +function StringOption({ value, onChange, ...props }) { return ( - onChange(e.target.value)} - {...props} + onChange(e.target.value)} + {...props} /> - ) + ); } function Optional(Component) { - return function ({onChange, ...props}) { - return onChange(x ? x : null)} - {...props} - /> - } + 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}) { +function NumberOption({ value, onChange, ...props }) { return ( - onChange(parseInt(e.target.value))} - {...props} + onChange(parseInt(e.target.value))} + {...props} /> - ) + ); } ChoicesOption.propTypes = { value: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, -} +}; -export function ChoicesOption({value, onChange, choices, ...props}) { +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