Repository: alufers/mitmproxy2swagger Branch: master Commit: 2b3b3d83abf1 Files: 37 Total size: 18.6 MB Directory structure: gitextract_xv5jft2n/ ├── .dockerignore ├── .flake8 ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── build_docker.yml │ ├── python.yml │ └── release.yml ├── .gitignore ├── .markdownlint.yaml ├── .mypy.ini ├── .pre-commit-config.yaml ├── .yamllint ├── Dockerfile ├── README.md ├── example_outputs/ │ ├── lisek-out.swagger.yml │ └── lisek-static.html ├── mitmproxy2swagger/ │ ├── __init__.py │ ├── console_util.py │ ├── har_capture_reader.py │ ├── mitmproxy2swagger.py │ ├── mitmproxy_capture_reader.py │ ├── swagger_util.py │ ├── test_mitmproxy2swagger.py │ ├── test_openapi_compliance.py │ └── testing_util.py ├── pyproject.toml ├── specs.yml └── testdata/ ├── form_data_flows ├── generic_keys_flows ├── generic_keys_testclient.py ├── generic_keys_testserver.py ├── msgpack_flows ├── msgpack_testclient.py ├── msgpack_testserver.py ├── sklep.lisek.app.har ├── test_flows ├── testclient.py └── testserver.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ testdata testcase docs example_outputs .github .venv .coverage ================================================ FILE: .flake8 ================================================ [flake8] max-line-length = 120 extend-ignore = E203,E501 ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "pip" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" ================================================ FILE: .github/workflows/build_docker.yml ================================================ name: Create and publish a Docker image on: push: branches: [master] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build-and-push-image: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Log in to the Container registry uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@dbef88086f6cef02e264edb7dbf63250c17cef6c with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} flavor: | latest=auto - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@0d103c3126aa41d772a8362f6aa67afac040f80c - name: Build and push Docker image uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 with: context: . push: true platforms: linux/amd64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} ================================================ FILE: .github/workflows/python.yml ================================================ # This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Python on: push: branches: [master] pull_request: branches: [master] permissions: contents: read jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python 3.12 uses: actions/setup-python@v5 with: python-version: "3.12" - uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install poetry uses: abatilo/actions-poetry@v2 with: poetry-version: "1.8.5" - name: Install dependencies run: | poetry install - name: Run Python tests run: | poetry run pytest --cov - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 ================================================ FILE: .github/workflows/release.yml ================================================ name: Release package on: workflow_dispatch: inputs: release-type: type: choice description: 'Release type (one of): patch, minor, major, prepatch, preminor, premajor, prerelease' default: 'patch' options: - 'patch' - 'minor' - 'major' required: true jobs: release: runs-on: ubuntu-latest steps: # Checkout project repository - uses: actions/checkout@v4 - name: Set up Python 3.12 uses: actions/setup-python@v5 with: python-version: "3.12" - uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install poetry uses: abatilo/actions-poetry@v2 with: poetry-version: "1.8.5" - name: Install dependencies run: | poetry install - name: Run Python lint checks run: | poetry run pre-commit run --all-files - name: Run Python tests run: | poetry run pytest --cov - name: Git configuration run: | git config --global user.email "bot@example.com" git config --global user.name "GitHub Actions" - name: Bump release version run: | poetry version ${{ github.event.inputs.release-type }} echo "NEW_VERSION=$(poetry version --short)" >> $GITHUB_ENV env: RELEASE_TYPE: ${{ github.event.inputs.release-type }} - name: Build package run: | poetry lock poetry build - name: Commit pyproject.toml and poetry.lock run: | git add pyproject.toml poetry.lock git commit -m "chore: release ${{ env.NEW_VERSION }}" git tag ${{ env.NEW_VERSION }} git push origin master - name: Build docker image uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc with: context: . push: false platforms: linux/amd64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} # Push repository changes - name: Publish package run: | poetry publish --build env: POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} - name: Push docker image uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc with: context: . push: false platforms: linux/amd64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} ================================================ FILE: .gitignore ================================================ flows __pycache__ ass.yaml lisek.swagger.yml dist dupsko.yaml dist /.mypy_cache/ .DS_Store .idea flow* swagger .vscode .coverage ================================================ FILE: .markdownlint.yaml ================================================ default: true line-length: false no-inline-html: false ================================================ FILE: .mypy.ini ================================================ [mypy] [mypy-json_stream.*] ignore_missing_imports = True ================================================ FILE: .pre-commit-config.yaml ================================================ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks exclude: ^(api_protobuf/.*|docs/protobuf_docs\.md|dhaul_openapi/messages/.*)$ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: check-yaml - id: end-of-file-fixer exclude: ^testdata/.* - id: trailing-whitespace exclude: ^testdata/.* - id: check-json - id: detect-private-key - id: fix-encoding-pragma - id: check-merge-conflict - id: check-added-large-files - repo: https://github.com/psf/black rev: 24.10.0 hooks: - id: black args: [--config=pyproject.toml] - repo: https://github.com/PyCQA/isort rev: 5.13.2 hooks: - id: isort - repo: https://github.com/pre-commit/mirrors-mypy rev: "v1.13.0" hooks: - id: mypy - repo: https://github.com/PyCQA/docformatter rev: eb1df347edd128b30cd3368dddc3aa65edcfac38 # TODO: Switch back to upstream docformatter # after https://github.com/PyCQA/docformatter/issues/289 is fixed hooks: - id: docformatter args: - --in-place - --config=pyproject.toml - repo: https://github.com/PyCQA/autoflake rev: v2.3.1 hooks: - id: autoflake - repo: https://github.com/pycqa/flake8 rev: 7.1.1 hooks: - id: flake8 entry: flake8 - repo: https://github.com/netromdk/vermin rev: v1.6.0 hooks: - id: vermin # specify your target version here, OR in a Vermin config file as usual: args: ["-t=3.9-", "--violations"] # (if your target is specified in a Vermin config, you may omit the 'args' entry entirely) - repo: https://github.com/adrienverge/yamllint.git rev: v1.35.1 hooks: - id: yamllint - repo: https://github.com/igorshubovych/markdownlint-cli rev: v0.42.0 hooks: - id: markdownlint-fix - id: markdownlint ================================================ FILE: .yamllint ================================================ extends: default yaml-files: - "*.yaml" - "*.yml" - ".yamllint" rules: line-length: max: 120 level: warning document-start: disable truthy: disable indentation: disable ================================================ FILE: Dockerfile ================================================ FROM python:3.12-slim-bookworm AS builder COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ ENV UV_HTTP_TIMEOUT=100 \ UV_NO_CACHE=1 WORKDIR /app RUN uv pip install --system poetry poetry-plugin-export COPY pyproject.toml poetry.lock ./ RUN uv venv /venv && \ poetry config warnings.export false && \ poetry export -f requirements.txt -o requirements.txt && \ VIRTUAL_ENV=/venv uv pip install -r requirements.txt COPY . . RUN poetry build && \ VIRTUAL_ENV=/venv uv pip install dist/*.whl FROM python:3.12-slim-bookworm AS final ENV PYTHONFAULTHANDLER=1 \ PYTHONHASHSEED=random \ PYTHONUNBUFFERED=1 WORKDIR /app COPY --from=builder /venv /venv ENV PATH="/venv/bin:${PATH}" ENTRYPOINT [ "mitmproxy2swagger" ] ================================================ FILE: README.md ================================================ # mitmproxy2swagger [![PyPI version](https://badge.fury.io/py/mitmproxy2swagger.svg)](https://badge.fury.io/py/mitmproxy2swagger) [![Arch Linux repository](https://img.shields.io/badge/archlinux-mitmproxy2swagger-blue)](https://archlinux.org/packages/extra/any/mitmproxy2swagger/) A tool for automatically converting [mitmproxy](https://mitmproxy.org/) captures to [OpenAPI 3.0](https://swagger.io/specification/) specifications. This means that you can automatically reverse-engineer REST APIs by just running the apps and capturing the traffic. --- **🆕 NEW!** Added support for processing HAR exported from the browser DevTools. See [Usage - HAR](#har) for more details. --- ## Installation First you will need python3 and pip3. ```bash $ pip install mitmproxy2swagger # ... or ... $ pip3 install mitmproxy2swagger # ... or ... $ git clone git@github.com:alufers/mitmproxy2swagger.git $ cd mitmproxy2swagger $ docker build -t mitmproxy2swagger . ``` Then clone the repo and run `mitmproxy2swagger` as per examples below. ## Usage ### Mitmproxy To create a specification by inspecting HTTP traffic you will need to: 1. Capture the traffic by using the mitmproxy tool. I personally recommend using mitmweb, which is a web interface built-in to mitmproxy. ```bash $ mitmweb Web server listening at http://127.0.0.1:8081/ Proxy server listening at http://*:9999 ... ``` **IMPORTANT** To configure your client to use the proxy exposed by mitm proxy, please consult the [mitmproxy documentation](https://docs.mitmproxy.org/stable/) for more information. 2. Save the traffic to a flow file. In mitmweb you can do this by using the "File" menu and selecting "Save": ![A screenshot showing the location of the "Save" option in the "File" menu](./docs/mitmweb_save.png) 3. Run the first pass of mitmproxy2swagger: ```bash $ mitmproxy2swagger -i -o -p # ... or ... $ docker run -it -v $PWD:/app mitmproxy2swagger mitmproxy2swagger -i -o -p ``` Please note that you can use an existing schema, in which case the existing schema will be extended with the new data. You can also run it a few times with different flow captures, the captured data will be safely merged. `` is the base url of the API you wish to reverse-engineer. You will need to obtain it by observing the requests being made in mitmproxy. For example if an app has made requests like these: ```http https://api.example.com/v1/login https://api.example.com/v1/users/2 https://api.example.com/v1/users/2/profile ``` The likely prefix is `https://api.example.com/v1`. 4. Running the first pass should have created a section in the schema file like this: ```yaml x-path-templates: # Remove the ignore: prefix to generate an endpoint with its URL # Lines that are closer to the top take precedence, the matching is greedy - ignore:/addresses - ignore:/basket - ignore:/basket/add - ignore:/basket/checkouts - ignore:/basket/coupons/attach/{id} - ignore:/basket/coupons/attach/104754 ``` You should edit the schema file with a text editor and remove the `ignore:` prefix from the paths you wish to be generated. You can also adjust the parameters appearing in the paths. 5. Run the second pass of mitmproxy2swagger: ```bash $ mitmproxy2swagger -i -o -p [--examples] # ... or ... $ docker run -it -v $PWD:/app mitmproxy2swagger mitmproxy2swagger -i -o -p [--examples] ``` Run the command a second time (with the same schema file). It will pick up the edited lines and generate endpoint descriptions. Please note that mitmproxy2swagger will not overwrite existing endpoint descriptions, if you want to overwrite them, you can delete them before running the second pass. Passing `--examples` will add example data to requests and responses. Take caution when using this option, as it may add sensitive data (tokens, passwords, personal information etc.) to the schema. Passing `--headers` will add headers data to requests and responses. Take caution when using this option, as it may add sensitive data (tokens, passwords, personal information etc.) to the schema. ### HAR 1. Capture and export the traffic from the browser DevTools. In the browser DevTools, go to the Network tab and click the "Export HAR" button. ![A screenshot showing where the export har button is located](./docs/export_har_button.png) 2. Continue the same way you would do with the mitmproxy dump. `mitmproxy2swagger` will automatically detect the HAR file and process it. ## Example output See the [examples](./example_outputs/). You will find a generated schema there and an html file with the generated documentation (via [redoc-cli](https://www.npmjs.com/package/redoc-cli)). See the generated html file [here](https://raw.githack.com/alufers/mitmproxy2swagger/master/example_outputs/lisek-static.html). ## Development and contributing This project uses: - [poetry](https://python-poetry.org/) for dependency management - [pre-commit](https://pre-commit.com/) for code formatting and linting - [pytest](https://docs.pytest.org/en/stable/) for unit testing To install the dependencies: ```bash poetry install ``` Run linters: ```bash pre-commit run --all-files ``` Install pre-commit hooks: ```bash pre-commit install ``` Run tests: ```bash poetry run pytest ``` Run tests with coverage: ```bash poetry run pytest --cov=mitmproxy2swagger ``` ## License MIT ================================================ FILE: example_outputs/lisek-out.swagger.yml ================================================ openapi: 3.0.0 info: title: flows/flows_lisek_filtered Mitmproxy2Swagger version: 1.0.0 servers: - url: https://api2.lisek.app/api description: The default server paths: /mobileversion: get: summary: GET mobileversion responses: '200': description: OK content: application/json: schema: type: object properties: value: type: array items: type: object properties: platform: type: string minVersion: type: string appCode: type: string success: type: boolean errors: type: array items: {} /darkstores: get: summary: GET darkstores responses: '200': description: OK content: application/json: schema: type: object properties: value: type: array items: type: object properties: id: type: number name: type: string identifier: type: string virtual: type: boolean address: type: object properties: id: type: number street: type: string streetNo: type: string city: type: string doorNo: type: string floor: type: string latitude: type: number longitude: type: number postalCode: type: string porch: type: string comments: type: string courierComment: type: string isCurrent: type: boolean isCurrentNotVirtual: type: boolean label: type: object isVirtual: type: boolean area: type: array items: type: string openingTime: type: object properties: openHour: type: number openMinute: type: number closeHour: type: number closeMinute: type: number courierSplitEnabled: type: boolean sortKey: type: number sliderId: type: number available: type: boolean unavailableMessage: type: string unavailableUntilTime: type: object orderStackingEnabled: type: string success: type: boolean errors: type: array items: {} /users/login: post: summary: POST login responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: token: type: string refreshToken: type: string expires: type: number user: type: object properties: id: type: number email: type: string firstname: type: string lastname: type: string phone: type: string newsletter: type: boolean sendInfo: type: boolean roles: type: array items: type: string franchises: type: array items: {} success: type: boolean errors: type: array items: {} requestBody: content: application/json: schema: type: object properties: Token: type: string GrantType: type: string /darkstores/{id}: get: summary: GET darkstores by id responses: '200': description: OK content: application/json: schema: type: object properties: value: type: array items: type: object properties: area: type: string success: type: boolean errors: type: array items: {} parameters: - name: id in: path required: true schema: type: string /users/profile: get: summary: GET profile responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: firstName: type: string lastName: type: string phone: type: string dateOfBirth: type: string email: type: string newsletter: type: boolean sendInfo: type: boolean smsCode: type: object success: type: boolean errors: type: array items: {} /addresses: get: summary: GET addresses responses: '200': description: OK content: application/json: schema: type: object properties: value: type: array items: {} success: type: boolean errors: type: array items: {} /favourites: get: summary: GET favourites responses: '200': description: OK content: application/json: schema: type: object properties: value: type: array items: {} success: type: boolean errors: type: array items: {} /basket/setlocation: post: summary: POST setlocation responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: id: type: number userId: type: number addressId: type: object address: type: object darkstoreId: type: number items: type: array items: {} coupons: type: array items: {} itemTotal: type: number total: type: number delivery: type: number standardDeliveryCost: type: number freeShippingThreshold: type: number discountThreshold: type: number discountValue: type: number discountDelivery: type: number discountFromCrossingThreshold: type: number totalForThresholdDiscount: type: number totalForFreeShipping: type: number totalWithoutDiscount: type: number isDiscountThresholdCrossed: type: boolean isFreeShipping: type: boolean paperBagFee: type: number checkoutTrialDate: type: object insufficientStock: type: boolean totalWeight: type: number isBanned: type: boolean success: type: boolean errors: type: array items: {} requestBody: content: application/json: schema: type: object properties: latitude: type: number longitude: type: number /darkstores/{lat}/{lon}: get: summary: GET darkstores by lat responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: id: type: number name: type: string identifier: type: string virtual: type: boolean address: type: object properties: id: type: number street: type: string streetNo: type: string city: type: string doorNo: type: string floor: type: string latitude: type: number longitude: type: number postalCode: type: string porch: type: string comments: type: object courierComment: type: object isCurrent: type: boolean isCurrentNotVirtual: type: boolean label: type: object isVirtual: type: boolean area: type: object openingTime: type: object properties: openHour: type: number openMinute: type: number closeHour: type: number closeMinute: type: number courierSplitEnabled: type: boolean sortKey: type: number sliderId: type: number available: type: boolean unavailableMessage: type: string unavailableUntilTime: type: object orderStackingEnabled: type: string success: type: boolean errors: type: array items: {} parameters: - name: lat in: path required: true schema: type: string - name: lon in: path required: true schema: type: string /transactionsettings: get: summary: GET transactionsettings responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: discountThreshold: type: number maxWeight: type: number minOrder: type: number freeShippingThreshold: type: number deliveryCost: type: number discountPercent: type: number maxStackSize: type: number maxStackTimeInMins: type: number startStackFromClosest: type: boolean paperBagEan: type: string paperBagFee: type: number paperBagPerXItems: type: number paperBagPerXPrice: type: object paperBagInfiniteStock: type: boolean alcoholCategories: type: string success: type: boolean errors: type: array items: {} /inventory/{id}/full: get: summary: GET full by id responses: '200': description: OK content: application/json: schema: type: object properties: value: type: array items: type: object properties: name: type: string key: type: string number: type: number order: type: number imagePath: type: string iconPath: type: object parentId: type: object alcohol: type: boolean backgroundColor: type: string subCategories: type: array items: type: object properties: name: type: string key: type: object number: type: number order: type: number imagePath: type: string iconPath: type: object parentId: type: number alcohol: type: boolean backgroundColor: type: string subCategories: type: array items: {} products: type: array items: type: object properties: quantity: type: number ean: type: string maxQuantity: type: number headline: type: string title: type: string subTitle: type: string price: type: number packInfo: type: string imagePath: type: string isHit: type: boolean isPromo: type: boolean priceBeforePromo: type: object promoText: type: string isNew: type: boolean isCold: type: boolean showOutOfStock: type: boolean weight: type: string vat: type: number categoryId: type: number subCategoryId: type: object subCategoryName: type: object id: type: number id: type: number products: type: array items: {} id: type: number success: type: boolean errors: type: array items: {} parameters: - name: id in: path required: true schema: type: number /inventory/{id}/stocks: get: summary: GET stocks by id responses: '200': description: OK content: application/json: schema: type: object properties: value: type: array items: type: object properties: e: type: string q: type: number success: type: boolean errors: type: array items: {} parameters: - name: id in: path required: true schema: type: number /sliders/{id}: get: summary: GET sliders by id responses: '200': description: OK content: application/json: schema: type: object properties: value: type: array items: type: object properties: link: type: string type: type: number position: type: number categoryId: type: object mainCategoryId: type: object success: type: boolean errors: type: array items: {} parameters: - name: id in: path required: true schema: type: number /push: post: summary: POST push responses: '200': description: OK content: application/json: schema: type: object properties: success: type: boolean errors: type: array items: {} requestBody: content: application/json: schema: type: object properties: token: type: string platform: type: string appVersion: type: string /inventory/promo/{id}: get: summary: GET promo by id responses: '200': description: OK content: application/json: schema: type: object properties: value: type: array items: type: object properties: ean: type: string type: type: string success: type: boolean errors: type: array items: {} parameters: - name: id in: path required: true schema: type: number /users-sms/code: post: summary: POST code responses: '200': description: OK content: application/json: schema: type: object properties: success: type: boolean errors: type: array items: {} requestBody: content: application/json: schema: type: object properties: phone: type: string /users-sms/login: post: summary: POST login responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: token: type: string refreshToken: type: string expires: type: number user: type: object properties: id: type: number email: type: string firstname: type: string lastname: type: string phone: type: string newsletter: type: boolean sendInfo: type: boolean roles: type: array items: type: string franchises: type: array items: {} success: type: boolean errors: type: array items: {} requestBody: content: application/json: schema: type: object properties: phone: type: string code: type: string /orders/user/{id}/{id1}/{id2}: get: summary: GET user by id responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: page: type: number pageSize: type: number count: type: number items: type: array items: {} success: type: boolean errors: type: array items: {} parameters: - name: id in: path required: true schema: type: number - name: id1 in: path required: true schema: type: number - name: id2 in: path required: true schema: type: number - name: active in: query required: false schema: type: string /basket/setaddress/{id}: post: summary: POST setaddress by id responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: id: type: number userId: type: number addressId: type: number address: type: object properties: id: type: number street: type: string streetNo: type: string city: type: string doorNo: type: string floor: type: string latitude: type: number longitude: type: number postalCode: type: string porch: type: object comments: type: string courierComment: type: object isCurrent: type: boolean isCurrentNotVirtual: type: boolean label: type: object isVirtual: type: boolean darkstoreId: type: number items: type: array items: type: object properties: ean: type: string title: type: string subtitle: type: string imagePath: type: string quantity: type: number quantityAvailable: type: number price: type: number insufficientStock: type: boolean stockItemId: type: number vat: type: number weight: type: number maxQuantity: type: number isRemovable: type: boolean isExcludedFromDiscount: type: boolean isFreebie: type: boolean id: type: number coupons: type: array items: {} itemTotal: type: number total: type: number delivery: type: number standardDeliveryCost: type: number freeShippingThreshold: type: number discountThreshold: type: number discountValue: type: number discountDelivery: type: number discountFromCrossingThreshold: type: number totalForThresholdDiscount: type: number totalForFreeShipping: type: number totalWithoutDiscount: type: number isDiscountThresholdCrossed: type: boolean isFreeShipping: type: boolean paperBagFee: type: number checkoutTrialDate: type: object insufficientStock: type: boolean totalWeight: type: number isBanned: type: boolean success: type: boolean errors: type: array items: {} parameters: - name: id in: path required: true schema: type: number /basket/add: post: summary: POST basket add responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: id: type: number userId: type: number addressId: type: number address: type: object properties: id: type: number street: type: string streetNo: type: string city: type: string doorNo: type: string floor: type: string latitude: type: number longitude: type: number postalCode: type: string porch: type: object comments: type: string courierComment: type: object isCurrent: type: boolean isCurrentNotVirtual: type: boolean label: type: object isVirtual: type: boolean darkstoreId: type: number items: type: array items: type: object properties: ean: type: string title: type: string subtitle: type: string imagePath: type: string quantity: type: number quantityAvailable: type: number price: type: number insufficientStock: type: boolean stockItemId: type: number vat: type: number weight: type: number maxQuantity: type: number isRemovable: type: boolean isExcludedFromDiscount: type: boolean isFreebie: type: boolean id: type: number coupons: type: array items: {} itemTotal: type: number total: type: number delivery: type: number standardDeliveryCost: type: number freeShippingThreshold: type: number discountThreshold: type: number discountValue: type: number discountDelivery: type: number discountFromCrossingThreshold: type: number totalForThresholdDiscount: type: number totalForFreeShipping: type: number totalWithoutDiscount: type: number isDiscountThresholdCrossed: type: boolean isFreeShipping: type: boolean paperBagFee: type: number checkoutTrialDate: type: object insufficientStock: type: boolean totalWeight: type: number isBanned: type: boolean success: type: boolean errors: type: array items: {} requestBody: content: application/json: schema: type: object properties: ean: type: string quantity: type: number /basket/remove: post: summary: POST remove responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: id: type: number userId: type: number addressId: type: number address: type: object properties: id: type: number street: type: string streetNo: type: string city: type: string doorNo: type: string floor: type: string latitude: type: number longitude: type: number postalCode: type: string porch: type: object comments: type: string courierComment: type: object isCurrent: type: boolean isCurrentNotVirtual: type: boolean label: type: object isVirtual: type: boolean darkstoreId: type: number items: type: array items: type: object properties: ean: type: string title: type: string subtitle: type: string imagePath: type: string quantity: type: number quantityAvailable: type: number price: type: number insufficientStock: type: boolean stockItemId: type: number vat: type: number weight: type: number maxQuantity: type: number isRemovable: type: boolean isExcludedFromDiscount: type: boolean isFreebie: type: boolean id: type: number coupons: type: array items: {} itemTotal: type: number total: type: number delivery: type: number standardDeliveryCost: type: number freeShippingThreshold: type: number discountThreshold: type: number discountValue: type: number discountDelivery: type: number discountFromCrossingThreshold: type: number totalForThresholdDiscount: type: number totalForFreeShipping: type: number totalWithoutDiscount: type: number isDiscountThresholdCrossed: type: boolean isFreeShipping: type: boolean paperBagFee: type: number checkoutTrialDate: type: object insufficientStock: type: boolean totalWeight: type: number isBanned: type: boolean success: type: boolean errors: type: array items: {} parameters: - name: basketItemId in: query required: false schema: type: number requestBody: content: application/json: schema: type: object properties: ean: type: string quantity: type: number /basket/checkouts: post: summary: POST checkouts responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: estimatedWaitTimeInMinutes: type: object canDeliver: type: boolean success: type: boolean errors: type: array items: {} /v1/ordertips: get: summary: GET ordertips responses: '200': description: OK content: application/json: schema: type: object properties: value: type: array items: type: number success: type: boolean errors: type: array items: {} /payu/methods: get: summary: GET methods responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: payByLinks: type: array items: type: object properties: value: type: string name: type: string brandImageUrl: type: string status: type: string minAmount: type: number maxAmount: type: number cardTokens: type: array items: {} pexTokens: type: array items: {} success: type: boolean errors: type: array items: {} parameters: - name: extended in: query required: false schema: type: number /coupons: get: summary: GET coupons responses: '200': description: OK content: application/json: schema: type: object properties: value: type: array items: {} success: type: boolean errors: type: array items: {} /scheduleslots/{id}: get: summary: GET scheduleslots by id responses: '200': description: OK content: application/json: schema: type: object properties: value: type: array items: type: object properties: id: type: number date: type: number hour: type: number minutes: type: number status: type: string size: type: number free: type: number configurationId: type: number success: type: boolean errors: type: array items: {} parameters: - name: id in: path required: true schema: type: number /coupons/activate/{promocode}: post: summary: POST coupons activate by promocode responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object success: type: boolean errors: type: array items: type: object properties: code: type: string message: type: string translateKey: type: string parameters: - name: promocode in: path required: true schema: type: string /basket/coupons/attach/{id}: post: summary: POST coupons attach by id responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: id: type: number userId: type: number addressId: type: number address: type: object properties: id: type: number street: type: string streetNo: type: string city: type: string doorNo: type: string floor: type: string latitude: type: number longitude: type: number postalCode: type: string porch: type: object comments: type: string courierComment: type: object isCurrent: type: boolean isCurrentNotVirtual: type: boolean label: type: object isVirtual: type: boolean darkstoreId: type: number items: type: array items: type: object properties: ean: type: string title: type: string subtitle: type: string imagePath: type: string quantity: type: number quantityAvailable: type: number price: type: number insufficientStock: type: boolean stockItemId: type: number vat: type: number weight: type: number maxQuantity: type: number isRemovable: type: boolean isExcludedFromDiscount: type: boolean isFreebie: type: boolean id: type: number coupons: type: array items: type: object properties: id: type: number name: type: string display: type: string description: type: string validTill: type: string validFrom: type: string freeShipping: type: boolean noPaperBags: type: boolean fixedDiscount: type: number percentDiscount: type: number referal: type: boolean gift: type: boolean valid: type: object minOrderValue: type: number calculatedDiscount: type: object itemTotal: type: number total: type: number delivery: type: number standardDeliveryCost: type: number freeShippingThreshold: type: number discountThreshold: type: number discountValue: type: number discountDelivery: type: number discountFromCrossingThreshold: type: number totalForThresholdDiscount: type: number totalForFreeShipping: type: number totalWithoutDiscount: type: number isDiscountThresholdCrossed: type: boolean isFreeShipping: type: boolean paperBagFee: type: number checkoutTrialDate: type: object insufficientStock: type: boolean totalWeight: type: number isBanned: type: boolean success: type: boolean errors: type: array items: {} parameters: - name: id in: path required: true schema: type: number /basket: get: summary: GET basket responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: id: type: number userId: type: number addressId: type: number address: type: object properties: id: type: number street: type: string streetNo: type: string city: type: string doorNo: type: string floor: type: string latitude: type: number longitude: type: number postalCode: type: string porch: type: object comments: type: string courierComment: type: object isCurrent: type: boolean isCurrentNotVirtual: type: boolean label: type: object isVirtual: type: boolean darkstoreId: type: number items: type: array items: type: object properties: ean: type: string title: type: string subtitle: type: string imagePath: type: string quantity: type: number quantityAvailable: type: number price: type: number insufficientStock: type: boolean stockItemId: type: number vat: type: number weight: type: number maxQuantity: type: number isRemovable: type: boolean isExcludedFromDiscount: type: boolean isFreebie: type: boolean id: type: number coupons: type: array items: type: object properties: id: type: number name: type: string display: type: string description: type: string validTill: type: string validFrom: type: string freeShipping: type: boolean noPaperBags: type: boolean fixedDiscount: type: number percentDiscount: type: number referal: type: boolean gift: type: boolean valid: type: object minOrderValue: type: number calculatedDiscount: type: object itemTotal: type: number total: type: number delivery: type: number standardDeliveryCost: type: number freeShippingThreshold: type: number discountThreshold: type: number discountValue: type: number discountDelivery: type: number discountFromCrossingThreshold: type: number totalForThresholdDiscount: type: number totalForFreeShipping: type: number totalWithoutDiscount: type: number isDiscountThresholdCrossed: type: boolean isFreeShipping: type: boolean paperBagFee: type: number checkoutTrialDate: type: object insufficientStock: type: boolean totalWeight: type: number isBanned: type: boolean success: type: boolean errors: type: array items: {} /couriers/{id}/available/detail: get: summary: GET detail by id responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: estimatedWaitTimeInMinutes: type: number canDeliver: type: boolean success: type: boolean errors: type: array items: {} parameters: - name: id in: path required: true schema: type: number /orders/{id}: post: summary: POST orders by id responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: estimatedWaitTimeInMinutes: type: object orderId: type: number canDeliver: type: boolean paymentId: type: object success: type: boolean errors: type: array items: {} parameters: - name: id in: path required: true schema: type: string get: summary: GET orders by id responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: id: type: number courier: type: object customer: type: object properties: id: type: number firstname: type: string lastname: type: string phone: type: string email: type: string address: type: object properties: id: type: number street: type: string streetNo: type: string city: type: string doorNo: type: string floor: type: string latitude: type: number longitude: type: number postalCode: type: string porch: type: object comments: type: string courierComment: type: object isCurrent: type: boolean isCurrentNotVirtual: type: boolean label: type: object isVirtual: type: boolean pendingRefund: type: number refunded: type: number total: type: number discount: type: number delivery: type: number printed: type: boolean items: type: array items: type: object properties: id: type: number name: type: string quantity: type: number price: type: number returned: type: boolean onReceipt: type: boolean product: type: object properties: ean: type: string headline: type: string title: type: string subTitle: type: string weight: type: string imagePath: type: string alcohol: type: boolean pickInfo: type: object properties: shelf: type: number level: type: number levelColor: type: string path: type: number quantityAvailable: type: number darkstore: type: object properties: id: type: number name: type: string identifier: type: string virtual: type: boolean address: type: object properties: id: type: number street: type: string streetNo: type: string city: type: string doorNo: type: string floor: type: string latitude: type: number longitude: type: number postalCode: type: string porch: type: string comments: type: object courierComment: type: object isCurrent: type: boolean isCurrentNotVirtual: type: boolean label: type: object isVirtual: type: boolean area: type: object openingTime: type: object courierSplitEnabled: type: boolean sortKey: type: number sliderId: type: number available: type: boolean unavailableMessage: type: object unavailableUntilTime: type: object orderStackingEnabled: type: string isVirtualSplitEnabled: type: boolean status: type: string paymentStatus: type: string tipPaymentStatus: type: object tipAmount: type: object feedback: type: object deliveryDistance: type: number customerTotalOrderCount: type: number customerTotalSpent: type: number customerLastOrderDate: type: object date: type: number acceptDate: type: object atLocationDate: type: object deliveryDate: type: object pickingDate: type: object pickingCompletedDate: type: object paymentCompletedDate: type: object courierNotifiedDate: type: object courierReturnDate: type: object estimatedDeliveryTime: type: object courierTimerStartDate: type: object scheduled: type: boolean canWait: type: boolean alcohol: type: boolean ageVerification: type: object scheduleSlot: type: object courierDetail: type: object coupons: type: array items: type: object properties: id: type: number name: type: string display: type: string description: type: string validTill: type: string validFrom: type: string freeShipping: type: boolean noPaperBags: type: boolean fixedDiscount: type: number percentDiscount: type: number referal: type: boolean gift: type: boolean valid: type: object minOrderValue: type: number calculatedDiscount: type: object deliveryEstimations: type: array items: {} returns: type: object darkstorePickingZone: type: object deliveryTimes: type: object properties: communicatedDelayToRidingTime: type: number actualDelayToRidingTime: type: number orderAcceptTime: type: number pickingTime: type: number atPickerStationTime: type: number ridingTime: type: number atLocationTime: type: number returnTime: type: number deliveryTime: type: number totalToCustomerTime: type: number courierCycleTime: type: number picker: type: boolean darkstoreId: type: number darkstoreName: type: string cancelReason: type: object stackId: type: object isVip: type: boolean isLongDistance: type: boolean color: type: object zoneNumber: type: object success: type: boolean errors: type: array items: {} parameters: - name: id in: path required: true schema: type: number /payu/orders/{id}: post: summary: POST orders by id responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: status: type: object properties: statusCode: type: string redirectUri: type: object orderId: type: string success: type: boolean errors: type: array items: {} parameters: - name: id in: path required: true schema: type: number - name: extended in: query required: false schema: type: number requestBody: content: application/json: schema: type: object properties: continueUrl: type: string paymentMethod: type: object properties: type: type: string authorizationCode: type: string blikData: type: object properties: register: type: boolean /orders/{id}/estimateddeliverydate: get: summary: GET estimateddeliverydate by id responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: estimatedDeliveryDate: type: string calculationInfo: type: string success: type: boolean errors: type: array items: {} parameters: - name: id in: path required: true schema: type: number /users/active: post: summary: POST active responses: '200': description: OK content: application/json: schema: type: object properties: success: type: boolean errors: type: array items: {} ================================================ FILE: example_outputs/lisek-static.html ================================================ flows/flows_lisek_filtered Mitmproxy2Swagger

flows/flows_lisek_filtered Mitmproxy2Swagger (1.0.0)

Download OpenAPI specification:Download

GET mobileversion

Responses

Response samples

Content type
application/json
{
  • "value": [
    ],
  • "success": true,
  • "errors": [
    ]
}

GET darkstores

Responses

Response samples

Content type
application/json
{
  • "value": [
    ],
  • "success": true,
  • "errors": [
    ]
}

POST login

Request Body schema: application/json
Token
string
GrantType
string

Responses

Request samples

Content type
application/json
{
  • "Token": "string",
  • "GrantType": "string"
}

Response samples

Content type
application/json
{
  • "value": {
    },
  • "success": true,
  • "errors": [
    ]
}

GET darkstores by id

path Parameters
id
required
string

Responses

Response samples

Content type
application/json
{
  • "value": [
    ],
  • "success": true,
  • "errors": [
    ]
}

GET profile

Responses

Response samples

Content type
application/json
{
  • "value": {
    },
  • "success": true,
  • "errors": [
    ]
}

GET addresses

Responses

Response samples

Content type
application/json
{
  • "value": [
    ],
  • "success": true,
  • "errors": [
    ]
}

GET favourites

Responses

Response samples

Content type
application/json
{
  • "value": [
    ],
  • "success": true,
  • "errors": [
    ]
}

POST setlocation

Request Body schema: application/json
latitude
number
longitude
number

Responses

Request samples

Content type
application/json
{
  • "latitude": 0,
  • "longitude": 0
}

Response samples

Content type
application/json
{
  • "value": {
    },
  • "success": true,
  • "errors": [
    ]
}

GET darkstores by lat

path Parameters
lat
required
string
lon
required
string

Responses

Response samples

Content type
application/json
{
  • "value": {
    },
  • "success": true,
  • "errors": [
    ]
}

GET transactionsettings

Responses

Response samples

Content type
application/json
{
  • "value": {
    },
  • "success": true,
  • "errors": [
    ]
}

GET full by id

path Parameters
id
required
number

Responses

Response samples

Content type
application/json
{
  • "value": [
    ],
  • "success": true,
  • "errors": [
    ]
}

GET stocks by id

path Parameters
id
required
number

Responses

Response samples

Content type
application/json
{
  • "value": [
    ],
  • "success": true,
  • "errors": [
    ]
}

GET sliders by id

path Parameters
id
required
number

Responses

Response samples

Content type
application/json
{
  • "value": [
    ],
  • "success": true,
  • "errors": [
    ]
}

POST push

Request Body schema: application/json
token
string
platform
string
appVersion
string

Responses

Request samples

Content type
application/json
{
  • "token": "string",
  • "platform": "string",
  • "appVersion": "string"
}

Response samples

Content type
application/json
{
  • "success": true,
  • "errors": [
    ]
}

GET promo by id

path Parameters
id
required
number

Responses

Response samples

Content type
application/json
{
  • "value": [
    ],
  • "success": true,
  • "errors": [
    ]
}

POST code

Request Body schema: application/json
phone
string

Responses

Request samples

Content type
application/json
{
  • "phone": "string"
}

Response samples

Content type
application/json
{
  • "success": true,
  • "errors": [
    ]
}

POST login

Request Body schema: application/json
phone
string
code
string

Responses

Request samples

Content type
application/json
{
  • "phone": "string",
  • "code": "string"
}

Response samples

Content type
application/json
{
  • "value": {
    },
  • "success": true,
  • "errors": [
    ]
}

GET user by id

path Parameters
id
required
number
id1
required
number
id2
required
number
query Parameters
active
string

Responses

Response samples

Content type
application/json
{
  • "value": {
    },
  • "success": true,
  • "errors": [
    ]
}

POST setaddress by id

path Parameters
id
required
number

Responses

Response samples

Content type
application/json
{
  • "value": {
    },
  • "success": true,
  • "errors": [
    ]
}

POST basket add

Request Body schema: application/json
ean
string
quantity
number

Responses

Request samples

Content type
application/json
{
  • "ean": "string",
  • "quantity": 0
}

Response samples

Content type
application/json
{
  • "value": {
    },
  • "success": true,
  • "errors": [
    ]
}

POST remove

query Parameters
basketItemId
number
Request Body schema: application/json
ean
string
quantity
number

Responses

Request samples

Content type
application/json
{
  • "ean": "string",
  • "quantity": 0
}

Response samples

Content type
application/json
{
  • "value": {
    },
  • "success": true,
  • "errors": [
    ]
}

POST checkouts

Responses

Response samples

Content type
application/json
{
  • "value": {
    },
  • "success": true,
  • "errors": [
    ]
}

GET ordertips

Responses

Response samples

Content type
application/json
{
  • "value": [
    ],
  • "success": true,
  • "errors": [
    ]
}

GET methods

query Parameters
extended
number

Responses

Response samples

Content type
application/json
{
  • "value": {
    },
  • "success": true,
  • "errors": [
    ]
}

GET coupons

Responses

Response samples

Content type
application/json
{
  • "value": [
    ],
  • "success": true,
  • "errors": [
    ]
}

GET scheduleslots by id

path Parameters
id
required
number

Responses

Response samples

Content type
application/json
{
  • "value": [
    ],
  • "success": true,
  • "errors": [
    ]
}

POST coupons activate by promocode

path Parameters
promocode
required
string

Responses

Response samples

Content type
application/json
{
  • "value": { },
  • "success": true,
  • "errors": [
    ]
}

POST coupons attach by id

path Parameters
id
required
number

Responses

Response samples

Content type
application/json
{
  • "value": {
    },
  • "success": true,
  • "errors": [
    ]
}

GET basket

Responses

Response samples

Content type
application/json
{
  • "value": {
    },
  • "success": true,
  • "errors": [
    ]
}

GET detail by id

path Parameters
id
required
number

Responses

Response samples

Content type
application/json
{
  • "value": {
    },
  • "success": true,
  • "errors": [
    ]
}

POST orders by id

path Parameters
id
required
string

Responses

Response samples

Content type
application/json
{
  • "value": {
    },
  • "success": true,
  • "errors": [
    ]
}

GET orders by id

path Parameters
id
required
number

Responses

Response samples

Content type
application/json
{
  • "value": {
    },
  • "success": true,
  • "errors": [
    ]
}

POST orders by id

path Parameters
id
required
number
query Parameters
extended
number
Request Body schema: application/json
continueUrl
string
object

Responses

Request samples

Content type
application/json
{
  • "continueUrl": "string",
  • "paymentMethod": {
    }
}

Response samples

Content type
application/json
{
  • "value": {
    },
  • "success": true,
  • "errors": [
    ]
}

GET estimateddeliverydate by id

path Parameters
id
required
number

Responses

Response samples

Content type
application/json
{
  • "value": {
    },
  • "success": true,
  • "errors": [
    ]
}

POST active

Responses

Response samples

Content type
application/json
{
  • "success": true,
  • "errors": [
    ]
}
================================================ FILE: mitmproxy2swagger/__init__.py ================================================ ================================================ FILE: mitmproxy2swagger/console_util.py ================================================ # -*- coding: utf-8 -*- import sys ANSI_RGB = "\033[38;2;{};{};{}m" ANSI_RGB_BG = "\033[48;2;{};{};{}m" ANSI_RED = "\033[31m" ANSI_RESET = "\033[0m" RAINBOW_COLORS = [ (255, 0, 0), (255, 127, 0), (255, 255, 0), (127, 255, 0), (0, 255, 0), (0, 255, 127), (0, 255, 255), (0, 127, 255), (0, 0, 255), (127, 0, 255), (255, 0, 255), (255, 0, 127), ] def rgb_interpolate(start, end, progress): return tuple(int(start[i] + (end[i] - start[i]) * progress) for i in range(3)) # take a value from 0 to 1 and return an interpolated color from the rainbow def rainbow_at_position(progress): idx_a = int(progress * float(len(RAINBOW_COLORS) - 1)) idx_b = idx_a + 1 return rgb_interpolate( RAINBOW_COLORS[idx_a], RAINBOW_COLORS[idx_b], progress * float(len(RAINBOW_COLORS) - 1) - idx_a, ) def print_progress_bar(progress=0.0): sys.stdout.write("\r") progress_bar_contents = "" PROGRESS_LENGTH = 30 blocks = ["▉", "▊", "▋", "▌", "▍", "▎", "▏"] for i in range(PROGRESS_LENGTH): interpolated = rainbow_at_position(i / PROGRESS_LENGTH) # check if should print a full block if i < int(progress * PROGRESS_LENGTH): interpolated_2nd_half = rainbow_at_position((i + 0.5) / PROGRESS_LENGTH) progress_bar_contents += ANSI_RGB.format(*interpolated) progress_bar_contents += ANSI_RGB_BG.format(*interpolated_2nd_half) progress_bar_contents += "▌" # check if should print a non-full block elif i < int((progress * PROGRESS_LENGTH) + 0.5): progress_bar_contents += ANSI_RESET progress_bar_contents += ANSI_RGB.format(*interpolated) progress_bar_contents += blocks[ int((progress * PROGRESS_LENGTH) + 0.5) - i - 1 ] # otherwise, print a space else: progress_bar_contents += ANSI_RESET progress_bar_contents += " " progress_bar_contents += ANSI_RESET sys.stdout.write("[{}] {:.1f}%".format(progress_bar_contents, progress * 100)) sys.stdout.flush() ================================================ FILE: mitmproxy2swagger/har_capture_reader.py ================================================ # -*- coding: utf-8 -*- import os from base64 import b64decode from typing import Iterator, Union import json_stream # a heuristic to determine if a file is a har archive def har_archive_heuristic(file_path: str) -> int: val = 0 # if has the har extension if file_path.endswith(".har"): val += 25 # read the first 2048 bytes with open(file_path, "rb") as f: data = f.read(2048) # if file contains only ascii characters after remove EOL characters if ( data.decode("utf-8", "ignore") .replace("\r", "") .replace("\n", "") .isprintable() is True ): val += 25 # sign of a JSON file if data[0:1] == b"{": val += 23 # sign of Chrome OR Firefox export if b'"WebInspector"' in data or b'"Firefox"' in data: val += 15 if b'"entries"' in data: val += 15 if b'"version"' in data: val += 15 return val class HarFlowWrapper: def __init__(self, flow: dict): self.flow = flow def get_url(self): return self.flow["request"]["url"] def get_matching_url(self, prefix) -> Union[str, None]: """Get the requests URL if the prefix matches the URL, None otherwise.""" if self.flow["request"]["url"].startswith(prefix): return self.flow["request"]["url"] return None def get_method(self): return self.flow["request"]["method"] def get_request_headers(self): headers = {} for kv in self.flow["request"]["headers"]: k = kv["name"] v = kv["value"] # create list on key if it does not exist headers[k] = headers.get(k, []) headers[k].append(v) def get_request_body(self): if ( "request" in self.flow and "postData" in self.flow["request"] and "text" in self.flow["request"]["postData"] ): return self.flow["request"]["postData"]["text"] return None def get_response_status_code(self): return self.flow["response"]["status"] def get_response_reason(self): return self.flow["response"]["statusText"] def get_response_headers(self): headers = {} for kv in self.flow["response"]["headers"]: k = kv["name"] v = kv["value"] # create list on key if it does not exist headers[k] = headers.get(k, []) headers[k].append(v) return headers def get_response_body(self): if ( "response" in self.flow and "content" in self.flow["response"] and "text" in self.flow["response"]["content"] ): try: if ( "encoding" in self.flow["response"]["content"] and self.flow["response"]["content"]["encoding"] == "base64" ): return b64decode(self.flow["response"]["content"]["text"]).decode() except UnicodeDecodeError: return None return self.flow["response"]["content"]["text"] return None class HarCaptureReader: def __init__(self, file_path: str, progress_callback=None): self.file_path = file_path self.progress_callback = progress_callback def captured_requests(self) -> Iterator[HarFlowWrapper]: har_file_size = os.path.getsize(self.file_path) with open(self.file_path, "r", encoding="utf-8") as f: data = json_stream.load(f) for entry in data["log"]["entries"].persistent(): if self.progress_callback: self.progress_callback(f.tell() / har_file_size) yield HarFlowWrapper(entry) def name(self): return "har" ================================================ FILE: mitmproxy2swagger/mitmproxy2swagger.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- """Converts a mitmproxy dump file to a swagger schema.""" import argparse import json import os import re import sys import traceback import urllib from typing import Any, Optional, Sequence, Union import msgpack import ruamel.yaml from mitmproxy.exceptions import FlowReadException from mitmproxy2swagger import console_util, swagger_util from mitmproxy2swagger.har_capture_reader import HarCaptureReader, har_archive_heuristic from mitmproxy2swagger.mitmproxy_capture_reader import ( MitmproxyCaptureReader, mitmproxy_dump_file_huristic, ) def path_to_regex(path): # replace the path template with a regex path = re.escape(path) path = path.replace(r"\{", "(?P<") path = path.replace(r"\}", ">[^/]+)") path = path.replace(r"\*", ".*") return "^" + path + "$" def strip_query_string(path): # remove the query string from the path return path.split("?")[0] def set_key_if_not_exists(dict, key, value): if key not in dict: dict[key] = value def progress_callback(progress): console_util.print_progress_bar(progress) def detect_input_format(file_path): har_score = har_archive_heuristic(file_path) mitmproxy_score = mitmproxy_dump_file_huristic(file_path) if "MITMPROXY2SWAGGER_DEBUG" in os.environ: print("har score: " + str(har_score)) print("mitmproxy score: " + str(mitmproxy_score)) if har_score > mitmproxy_score: return HarCaptureReader(file_path, progress_callback) return MitmproxyCaptureReader(file_path, progress_callback) def main(override_args: Optional[Sequence[str]] = None): parser = argparse.ArgumentParser( description="Converts a mitmproxy dump file or HAR to a swagger schema." ) parser.add_argument( "-i", "--input", help="The input mitmproxy dump file or HAR dump file (from DevTools)", required=True, ) parser.add_argument( "-o", "--output", help="The output swagger schema file (yaml). If it exists, new endpoints will be added", required=True, ) parser.add_argument("-p", "--api-prefix", help="The api prefix", required=True) parser.add_argument( "-e", "--examples", action="store_true", help="Include examples in the schema. This might expose sensitive information.", ) parser.add_argument( "-hd", "--headers", action="store_true", help="Include headers in the schema. This might expose sensitive information.", ) parser.add_argument( "-f", "--format", choices=["flow", "har"], help="Override the input file format auto-detection.", ) parser.add_argument( "-r", "--param-regex", default="[0-9]+", help="Regex to match parameters in the API paths. Path segments that match this regex will be turned into parameter placeholders.", ) parser.add_argument( "-s", "--suppress-params", action="store_true", help="Do not include API paths that have the original parameter values, only the ones with placeholders.", ) args = parser.parse_args(override_args) try: args.param_regex = re.compile("^" + args.param_regex + "$") except re.error as e: print( f"{console_util.ANSI_RED}Invalid path parameter regex: {e}{console_util.ANSI_RESET}" ) sys.exit(1) yaml = ruamel.yaml.YAML() capture_reader: Union[MitmproxyCaptureReader, HarCaptureReader] if args.format == "flow" or args.format == "mitmproxy": capture_reader = MitmproxyCaptureReader(args.input, progress_callback) elif args.format == "har": capture_reader = HarCaptureReader(args.input, progress_callback) else: capture_reader = detect_input_format(args.input) swagger = None # try loading the existing swagger file try: base_dir = os.getcwd() relative_path = args.output abs_path = os.path.join(base_dir, relative_path) with open(abs_path, "r") as f: swagger = yaml.load(f) except FileNotFoundError: print("No existing swagger file found. Creating new one.") if swagger is None: swagger = ruamel.yaml.comments.CommentedMap( { "openapi": "3.0.0", "info": { "title": args.input + " Mitmproxy2Swagger", "version": "1.0.0", }, } ) # strip the trailing slash from the api prefix args.api_prefix = args.api_prefix.rstrip("/") if "servers" not in swagger or swagger["servers"] is None: swagger["servers"] = [] # add the server if it doesn't exist if not any(server["url"] == args.api_prefix for server in swagger["servers"]): swagger["servers"].append( {"url": args.api_prefix, "description": "The default server"} ) if "paths" not in swagger or swagger["paths"] is None: swagger["paths"] = {} if "x-path-templates" not in swagger or swagger["x-path-templates"] is None: swagger["x-path-templates"] = [] path_templates = [] for path in swagger["paths"]: path_templates.append(path) # also add paths from the the x-path-templates array if "x-path-templates" in swagger and swagger["x-path-templates"] is not None: for path in swagger["x-path-templates"]: path_templates.append(path) # new endpoints will be added here so that they can be added as comments in the swagger file new_path_templates = [] path_template_regexes = [re.compile(path_to_regex(path)) for path in path_templates] try: for req in capture_reader.captured_requests(): # strip the api prefix from the url url = req.get_matching_url(args.api_prefix) if url is None: continue method = req.get_method().lower() path = strip_query_string(url).removeprefix(args.api_prefix) status = req.get_response_status_code() # check if the path matches any of the path templates, and save the index path_template_index = None for i, path_template_regex in enumerate(path_template_regexes): if path_template_regex.match(path): path_template_index = i break if path_template_index is None: if path in new_path_templates: continue new_path_templates.append(path) continue path_template_to_set = path_templates[path_template_index] set_key_if_not_exists(swagger["paths"], path_template_to_set, {}) set_key_if_not_exists( swagger["paths"][path_template_to_set], method, { "summary": swagger_util.path_template_to_endpoint_name( method, path_template_to_set ), "responses": {}, }, ) params = swagger_util.url_to_params(url, path_template_to_set) if args.headers: headers_request = swagger_util.request_to_headers( req.get_request_headers() ) if headers_request is not None and len(headers_request) > 0: set_key_if_not_exists( swagger["paths"][path_template_to_set][method], "parameters", headers_request, ) if params is not None and len(params) > 0: set_key_if_not_exists( swagger["paths"][path_template_to_set][method], "parameters", params ) if method not in ["get", "head"]: body = req.get_request_body() if body is not None: body_val = None content_type = None # try to parse the body as json try: body_val = json.loads(req.get_request_body()) content_type = "application/json" except UnicodeDecodeError: pass except json.decoder.JSONDecodeError: pass # try to parse the body as msgpack, if it's not json if body_val is None: try: body_val = msgpack.loads(req.get_request_body()) content_type = "application/msgpack" except Exception: pass if content_type is None: # try to parse the body as form data try: body_val_bytes: Any = dict( urllib.parse.parse_qsl( body, encoding="utf-8", keep_blank_values=True ) ) body_val = {} did_find_anything = False for key, value in body_val_bytes.items(): did_find_anything = True body_val[key.decode("utf-8")] = value.decode("utf-8") if did_find_anything: content_type = "application/x-www-form-urlencoded" else: body_val = None except UnicodeDecodeError: pass if body_val is not None: content_to_set = { "content": { content_type: { "schema": swagger_util.value_to_schema(body_val) } } } if args.examples: content_to_set["content"][content_type]["example"] = ( swagger_util.limit_example_size(body_val) ) set_key_if_not_exists( swagger["paths"][path_template_to_set][method], "requestBody", content_to_set, ) response_body = req.get_response_body() if response_body is not None: # try parsing the response as json try: response_parsed = json.loads(response_body) response_content_type = "application/json" except UnicodeDecodeError: response_parsed = None except json.decoder.JSONDecodeError: response_parsed = None if response_parsed is None: # try parsing the response as msgpack, if it's not json try: response_parsed = msgpack.loads(response_body) response_content_type = "application/msgpack" except Exception: response_parsed = None if response_parsed is not None: resp_data_to_set = { "description": req.get_response_reason(), "content": { response_content_type: { "schema": swagger_util.value_to_schema(response_parsed) } }, } if args.examples: resp_data_to_set["content"][response_content_type][ "example" ] = swagger_util.limit_example_size(response_parsed) if args.headers: resp_data_to_set["headers"] = swagger_util.response_to_headers( req.get_response_headers() ) set_key_if_not_exists( swagger["paths"][path_template_to_set][method]["responses"], str(status), resp_data_to_set, ) if ( "responses" in swagger["paths"][path_template_to_set][method] and len(swagger["paths"][path_template_to_set][method]["responses"]) == 0 ): # add a default response if there were no responses detected, # this is for compliance with the OpenAPI spec content_type = ( req.get_response_headers().get("content-type") or "text/plain" ) swagger["paths"][path_template_to_set][method]["responses"]["200"] = { "description": "OK", "content": {}, } except FlowReadException as e: print(f"Flow file corrupted: {e}") traceback.print_exception(*sys.exc_info()) print( f"{console_util.ANSI_RED}Failed to parse the input file as '{capture_reader.name()}'. " ) if not args.format: print( f"It might happen that the input format as incorrectly detected. Please try using '--format flow' or '--format har' to specify the input format.{console_util.ANSI_RESET}" ) sys.exit(1) except ValueError as e: print(f"ValueError: {e}") # print stack trace traceback.print_exception(*sys.exc_info()) print( f"{console_util.ANSI_RED}Failed to parse the input file as '{capture_reader.name()}'. " ) if not args.format: print( f"It might happen that the input format as incorrectly detected. Please try using '--format flow' or '--format har' to specify the input format.{console_util.ANSI_RESET}" ) sys.exit(1) def is_param(param_value): return args.param_regex.match(param_value) is not None new_path_templates.sort() # add suggested path templates # basically inspects urls and replaces segments containing only numbers with a parameter new_path_templates_with_suggestions = [] for path in new_path_templates: # check if path contains number-only segments segments = path.split("/") has_param = any(is_param(segment) for segment in segments) if has_param: # replace digit segments with {id}, {id1}, {id2} etc new_segments = [] param_id = 0 for segment in segments: if is_param(segment): param_name = "id" + str(param_id) if param_id == 0: param_name = "id" new_segments.append("{" + param_name + "}") param_id += 1 else: new_segments.append(segment) suggested_path = "/".join(new_segments) # prepend the suggested path to the new_path_templates list if suggested_path not in new_path_templates_with_suggestions: new_path_templates_with_suggestions.append("ignore:" + suggested_path) if not has_param or not args.suppress_params: new_path_templates_with_suggestions.append("ignore:" + path) # remove the ending comments not to add them twice # append the contents of new_path_templates_with_suggestions to swagger['x-path-templates'] for path in new_path_templates_with_suggestions: swagger["x-path-templates"].append(path) # remove elements already generated swagger["x-path-templates"] = [ path for path in swagger["x-path-templates"] if path not in swagger["paths"] ] # remove duplicates while preserving order def f7(seq): seen = set() seen_add = seen.add return [x for x in seq if not (x in seen or seen_add(x))] swagger["x-path-templates"] = f7(swagger["x-path-templates"]) swagger["x-path-templates"] = ruamel.yaml.comments.CommentedSeq( swagger["x-path-templates"] ) swagger["x-path-templates"].yaml_set_start_comment( "Remove the ignore: prefix to generate an endpoint with its URL\nLines that are closer to the top take precedence, the matching is greedy" ) # save the swagger file with open(args.output, "w") as f: yaml.dump(swagger, f) print("Done!") if __name__ == "__main__": main() ================================================ FILE: mitmproxy2swagger/mitmproxy_capture_reader.py ================================================ # -*- coding: utf-8 -*- import os import typing from typing import Iterator from urllib.parse import urlparse from mitmproxy import http from mitmproxy import io as iom from mitmproxy.exceptions import FlowReadException def mitmproxy_dump_file_huristic(file_path: str) -> int: val = 0 if "flow" in file_path: val += 1 if "mitmproxy" in file_path: val += 1 # read the first 2048 bytes with open(file_path, "rb") as f: data = f.read(2048) # if file contains non-ascii characters after remove EOL characters if ( data.decode("utf-8", "ignore") .replace("\r", "") .replace("\n", "") .isprintable() is False ): val += 50 # if first character of the byte array is a digit if data[0:1].decode("utf-8", "ignore").isdigit() is True: val += 5 # if it contains the word status_code if b"status_code" in data: val += 5 if b"regular" in data: val += 10 return val class MitmproxyFlowWrapper: def __init__(self, flow: http.HTTPFlow): self.flow = flow def get_url(self) -> str: return self.flow.request.url def get_matching_url(self, prefix) -> typing.Union[str, None]: """Get the requests URL if the prefix matches the URL, None otherwise. This takes into account a quirk of mitmproxy where it sometimes puts the raw IP address in the URL instead of the hostname. Then the hostname is in the Host header. """ if self.flow.request.url.startswith(prefix): return self.flow.request.url # All the stuff where the real hostname could be replacement_hostnames = [ self.flow.request.headers.get("Host", ""), self.flow.request.host_header, self.flow.request.host, ] for replacement_hostname in replacement_hostnames: if replacement_hostname is not None and replacement_hostname != "": fixed_url = ( urlparse(self.flow.request.url) ._replace(netloc=replacement_hostname) .geturl() ) if fixed_url.startswith(prefix): return fixed_url return None def get_method(self) -> str: return self.flow.request.method def get_request_headers(self) -> dict[str, typing.List[str]]: headers: dict[str, typing.List[str]] = {} for k, v in self.flow.request.headers.items(multi=True): # create list on key if it does not exist headers[k] = headers.get(k, []) headers[k].append(v) return headers def get_request_body(self): return self.flow.request.content def get_response_status_code(self): return self.flow.response.status_code def get_response_reason(self): return self.flow.response.reason def get_response_headers(self): headers = {} for k, v in self.flow.response.headers.items(multi=True): # create list on key if it does not exist headers[k] = headers.get(k, []) headers[k].append(v) return headers def get_response_body(self): return self.flow.response.content class MitmproxyCaptureReader: def __init__(self, file_path, progress_callback=None): self.file_path = file_path self.progress_callback = progress_callback def captured_requests(self) -> Iterator[MitmproxyFlowWrapper]: with open(self.file_path, "rb") as logfile: logfile_size = os.path.getsize(self.file_path) freader = iom.FlowReader(logfile) try: for f in freader.stream(): if self.progress_callback: self.progress_callback(logfile.tell() / logfile_size) if isinstance(f, http.HTTPFlow): if f.response is None: print( "[warn] flow without response: {}".format(f.request.url) ) continue yield MitmproxyFlowWrapper(f) except FlowReadException as e: print(f"Flow file corrupted: {e}") def name(self): return "flow" ================================================ FILE: mitmproxy2swagger/swagger_util.py ================================================ # -*- coding: utf-8 -*- import urllib import uuid from typing import Any, List VERBS = [ "add", "create", "delete", "get", "attach", "detach", "update", "push", "extendedcreate", "activate", ] # generate a name for the endpoint from the path template # POST /api/v1/things/{id}/create -> POST create thing by id def path_template_to_endpoint_name(method, path_template): path_template = path_template.strip("/") segments = path_template.split("/") # remove params to a separate array params = [] for idx, segment in enumerate(segments): if segment.startswith("{") and segment.endswith("}"): params.append(segment) segments[idx] = "{}" # remove them from the segments segments = [segment for segment in segments if segment != "{}"] # reverse the segments segments.reverse() name_parts = [] for segment in segments: if segment in VERBS: # prepend to the name_parts name_parts.insert(0, segment.lower()) else: name_parts.insert(0, segment.lower()) break for param in params: name_parts.append("by " + param.replace("{", "").replace("}", "")) break return method.upper() + " " + " ".join(name_parts) # when given an url and its path template, generates the parameters section of the request def url_to_params(url, path_template): path_template = path_template.strip("/") segments = path_template.split("/") url_segments = url.split("?")[0].strip("/").split("/") params = [] for idx, segment in enumerate(segments): if segment.startswith("{") and segment.endswith("}"): params.append( { "name": segment.replace("{", "").replace("}", ""), "in": "path", "required": True, "schema": { "type": "number" if url_segments[idx].isdigit() else "string" }, } ) query_string = urllib.parse.urlparse(url).query if query_string: query_params = urllib.parse.parse_qs(query_string) for key in query_params: params.append( { "name": key, "in": "query", "required": False, "schema": { "type": "number" if query_params[key][0].isdigit() else "string" }, } ) return params def request_to_headers(headers: dict[str, List[Any]], add_example: bool = False): """When given an url and its path template, generates the parameters section of the request.""" params = [] if headers: for key in headers: h = { "name": key, "in": "header", "required": False, "schema": {"type": "number" if headers[key][0].isdigit() else "string"}, } if add_example: h["example"] = headers[key][0] params.append(h) return params def response_to_headers(headers): header = {} if headers: for key in headers: header[key] = { "description": headers[key][0], "schema": {"type": "number" if headers[key][0].isdigit() else "string"}, } return header def value_to_schema(value): # check if value is a number if type(value) is int or type(value) is float: return {"type": "number"} # check if value is a boolean elif isinstance(value, bool): return {"type": "boolean"} # check if value is a string elif isinstance(value, str): return {"type": "string"} # check if value is a list elif isinstance(value, list): if len(value) == 0: return {"type": "array", "items": {}} return {"type": "array", "items": value_to_schema(value[0])} # check if value is a dict elif isinstance(value, dict): all_keys_are_numeric = all(is_numeric_string(key) for key in value) all_keys_are_uuid = all(is_uuid(key) for key in value) keys_are_generic = all_keys_are_numeric or all_keys_are_uuid if keys_are_generic and len(value) > 0: return { "type": "object", "additionalProperties": value_to_schema(list(value.values())[0]), } return { "type": "object", "properties": {key: value_to_schema(value[key]) for key in value}, } # if it is none, return null elif value is None: return {"type": "object", "nullable": True} def is_uuid(key): return isinstance(key, str) and is_valid_uuid(key) def is_numeric_string(key): return isinstance(key, str) and key.isnumeric() def is_valid_uuid(val): try: uuid.UUID(str(val)) return True except ValueError: return False MAX_EXAMPLE_ARRAY_ELEMENTS = 10 MAX_EXAMPLE_OBJECT_PROPERTIES = 150 # recursively scan an example value and limit the number of elements and properties def limit_example_size(example): if isinstance(example, list): new_list = [] for element in example: if len(new_list) >= MAX_EXAMPLE_ARRAY_ELEMENTS: break new_list.append(limit_example_size(element)) return new_list elif isinstance(example, dict): new_dict = {} for key in example: if len(new_dict) >= MAX_EXAMPLE_OBJECT_PROPERTIES: break new_dict[key] = limit_example_size(example[key]) return new_dict else: return example ================================================ FILE: mitmproxy2swagger/test_mitmproxy2swagger.py ================================================ # -*- coding: utf-8 -*- from .testing_util import get_nested_key, mitmproxy2swagger_e2e_test def test_mitmproxy2swagger_generates_swagger_from_har(): data = mitmproxy2swagger_e2e_test( "testdata/sklep.lisek.app.har", "https://api2.lisek.app/" ) assert data is not None assert "paths" in data assert len(data["paths"]) > 3 # check if any paths were generated # assert "/api/darkstores" in data["paths"] # assert ( # "get" in data["paths"]["/api/darkstores"] # ) # check if the method was generated def test_mitmproxy2swagger_generates_swagger_from_mitmproxy_flow_file(): data = mitmproxy2swagger_e2e_test( "testdata/test_flows", "https://httpbin.org/", [ "--format", "flow", ], ) assert data is not None assert "paths" in data assert len(data["paths"]) == 3 # 4 paths in the test file assert get_nested_key(data, "paths./get.get.responses.200.content") is not None def test_mitmproxy2swagger_generates_swagger_from_mitmproxy_flow_file_with_form_data(): data = mitmproxy2swagger_e2e_test( "testdata/form_data_flows", "https://httpbin.org/", [ "--format", "flow", ], ) assert data is not None assert ( get_nested_key( data, "paths./post.post.requestBody.content.application/x-www-form-urlencoded.schema", ) is not None ) def test_mitmproxy2swagger_generates_swagger_from_mitmproxy_flow_file_with_generic_keys(): data = mitmproxy2swagger_e2e_test( "testdata/generic_keys_flows", "http://localhost:8082/", [ "--format", "flow", ], ) assert data is not None assert ( get_nested_key( data, "paths./.post.responses.200.content.application/json.schema.properties.numeric.properties", ) is None ) assert ( get_nested_key( data, "paths./.post.responses.200.content.application/json.schema.properties.uuid.properties", ) is None ) assert ( get_nested_key( data, "paths./.post.responses.200.content.application/json.schema.properties.numeric.additionalProperties", ) is not None ) assert ( get_nested_key( data, "paths./.post.responses.200.content.application/json.schema.properties.numeric.additionalProperties", ) is not None ) assert ( get_nested_key( data, "paths./.post.responses.200.content.application/json.schema.properties.mixed.properties", ) is not None ) assert ( get_nested_key( data, "paths./.post.responses.200.content.application/json.schema.properties.mixed.additionalProperties", ) is None ) def test_mitmproxy2swagger_generates_headers_for_flow_files(): data = mitmproxy2swagger_e2e_test( "testdata/form_data_flows", "https://httpbin.org/", [ "--format", "flow", "--headers", ], ) assert data is not None assert ( get_nested_key(data, "paths./post.post.responses.200.headers.content-type") is not None ) def test_mitmproxy2swagger_parses_msgpack_requests_and_responses(): data = mitmproxy2swagger_e2e_test( "testdata/msgpack_flows", "http://localhost:8082/", [ "--format", "flow", ], ) assert data is not None assert ( get_nested_key(data, "paths./.post.responses.200.content.application/msgpack") is not None ) assert ( get_nested_key( data, "paths./.post.responses.200.content.application/msgpack.schema.properties.new_field.type", ) == "string" ) assert ( get_nested_key(data, "paths./.post.requestBody.content.application/msgpack") is not None ) assert ( get_nested_key( data, "paths./.post.requestBody.content.application/msgpack.schema.properties.field1.type", ) == "string" ) ================================================ FILE: mitmproxy2swagger/test_openapi_compliance.py ================================================ # -*- coding: utf-8 -*- from openapi_spec_validator import validate_spec from mitmproxy2swagger.testing_util import mitmproxy2swagger_e2e_test def test_mitmproxy2swagger_compliance_from_mitmproxy_flow_file(): data = mitmproxy2swagger_e2e_test( "testdata/test_flows", "https://httpbin.org/", [ "--format", "flow", ], ) assert data is not None validate_spec(data) def test_mitmproxy2swagger_compliance_from_mitmproxy_flow_file_with_headers(): data = mitmproxy2swagger_e2e_test( "testdata/test_flows", "https://httpbin.org/", [ "--format", "flow", "--headers", ], ) assert data is not None validate_spec(data) def test_mitmproxy2swagger_compliance_from_har_file_with_headers(): data = mitmproxy2swagger_e2e_test( "testdata/sklep.lisek.app.har", "https://api2.lisek.app/", [ "--format", "har", "--headers", ], ) assert data is not None validate_spec(data) def test_mitmproxy2swagger_compliance_from_form_data_file_with_headers(): data = mitmproxy2swagger_e2e_test( "testdata/form_data_flows", "https://httpbin.org/", [ "--format", "flow", "--headers", ], ) assert data is not None validate_spec(data) def test_mitmproxy2swagger_compliance_from_msgpack_file_with_headers(): data = mitmproxy2swagger_e2e_test( "testdata/msgpack_flows", "http://localhost:8082/", [ "--format", "flow", "--headers", ], ) assert data is not None validate_spec(data) def test_mitmproxy2swagger_compliance_from_generic_keys_file_with_headers(): data = mitmproxy2swagger_e2e_test( "testdata/generic_keys_flows", "http://localhost:8082/", [ "--format", "flow", "--headers", ], ) assert data is not None validate_spec(data) ================================================ FILE: mitmproxy2swagger/testing_util.py ================================================ # -*- coding: utf-8 -*- import tempfile from typing import Any, List, Optional import ruamel.yaml as ruamel from .mitmproxy2swagger import main def get_nested_key(obj: Any, path: str) -> Any: """Gets a nested key from a dict.""" keys = path.split(".") for key in keys: if not isinstance(obj, dict): return None if key not in obj: return None obj = obj[key] return obj def mitmproxy2swagger_e2e_test( input_file: str, url_prefix: str, extra_args: Optional[List[str]] = None ) -> Any: """Runs mitmproxy2swagger on the given input file twice, and returns the detected endpoints.""" yaml_tmp_path = tempfile.mktemp(suffix=".yaml", prefix="sklep.lisek.") main( [ "-i", input_file, "-o", yaml_tmp_path, "-p", url_prefix, ] + (extra_args or []) ) yaml = ruamel.YAML() data = None # try to parse the file with open(yaml_tmp_path, "r") as f: data = yaml.load(f.read()) assert data is not None assert "x-path-templates" in data assert "servers" in data # remove all of the ignore:prefixes in x-path-templates data["x-path-templates"] = [ x.replace("ignore:", "") for x in data["x-path-templates"] ] # save the file with open(yaml_tmp_path, "w") as f: yaml.dump(data, f) # run mitmproxy2swagger again main( [ "-i", input_file, "-o", yaml_tmp_path, "-p", url_prefix, ] + (extra_args or []) ) # load the file again with open(yaml_tmp_path, "r") as f: data = yaml.load(f.read()) return data ================================================ FILE: pyproject.toml ================================================ [tool.poetry] name = "mitmproxy2swagger" version = "0.14.0" description = "" authors = ["alufers "] readme = "README.md" license = "MIT" [tool.poetry.dependencies] python = "^3.10" mitmproxy = "^11.0.2" "ruamel.yaml" = ">=0.17.32,<0.19.0" json-stream = "^2.3.2" msgpack = "^1.0.7" [tool.poetry.group.dev.dependencies] black = "^24.10.0" isort = "^5.12.0" mypy = "^1.13.0" flake8 = "^7.1.1" docformatter = {extras = ["tomli"], version = "^1.7.1"} pre-commit = "^4.0.1" pytest = "^8.3.3" pytest-asyncio = ">=0.20.3,<0.25.0" vermin = "^1.5.1" openapi-spec-validator = ">=0.5.6,<0.8.0" pytest-cov = "^6.0.0" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] mitmproxy2swagger = 'mitmproxy2swagger.mitmproxy2swagger:main' [tool.black] line-length = 88 target-version = ['py310'] [tool.isort] profile = "black" [tool.mypy] python_version = "3.10" [tool.docformatter] recursive = true wrap-summaries = 88 [tool.pytest.ini_options] asyncio_default_fixture_loop_scope = "function" ================================================ FILE: specs.yml ================================================ openapi: 3.0.0 info: title: testdata/sklep.lisek.app.har Mitmproxy2Swagger version: 1.0.0 servers: - url: https://sklep.lisek.app description: The default server - url: https://api2.lisek.app description: The default server paths: /api/inventory/image: get: summary: GET image responses: {} parameters: - name: url in: query required: false schema: type: string - name: width in: query required: false schema: type: number - name: height in: query required: false schema: type: number - name: noEnlarge in: query required: false schema: type: string /api/settings/countries: get: summary: GET countries responses: '200': description: OK content: application/json: schema: type: object properties: value: type: array items: type: object properties: countryCode: type: string areaCode: type: string maxPhoneLength: type: number success: type: boolean errors: type: array items: {} errorTexts: type: string options: summary: OPTIONS countries responses: {} /api/darkstores: get: summary: GET darkstores responses: '200': description: OK content: application/json: schema: type: object properties: value: type: array items: type: object properties: id: type: number name: type: string identifier: type: string virtual: type: boolean address: type: object properties: id: type: number street: type: string streetNo: type: string city: type: string doorNo: type: string floor: type: string latitude: type: number longitude: type: number postalCode: type: string porch: type: string comments: type: string courierComment: type: string isCurrent: type: boolean isCurrentNotVirtual: type: boolean label: type: object isVirtual: type: boolean darkstoreId: type: number fullInfo: type: object area: type: array items: type: string openingTime: type: object properties: openHour: type: number openMinute: type: number closeHour: type: number closeMinute: type: number localTime: type: boolean courierSplitEnabled: type: boolean sortKey: type: number sliderId: type: number available: type: boolean unavailableMessage: type: string unavailableFromTime: type: object unavailableUntilTime: type: string orderStackingEnabled: type: string darkstoreRuntimeSettigs: type: object properties: pickerStackRemovePermission: type: string isModifyStackSortOrderAllowed: type: boolean isAddressVisibleForCourier: type: object transportTypeForCouriers: type: object alcoholReceiptPrintingGroup: type: object properties: groupName: type: string pharmacyReceiptPrintingGroup: type: object properties: groupName: type: string additionalBasketFees: type: object properties: longDistanceFeeMinDistanceInM: type: object longDistanceFee: type: object isVirtualSplitEnabled: type: boolean additionalInfoText: type: object success: type: boolean errors: type: array items: {} errorTexts: type: string options: summary: OPTIONS darkstores responses: {} /api/darkstores/default: get: summary: GET default responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: id: type: number name: type: string identifier: type: string virtual: type: boolean address: type: object properties: id: type: number street: type: string streetNo: type: string city: type: string doorNo: type: string floor: type: string latitude: type: number longitude: type: number postalCode: type: string porch: type: string comments: type: string courierComment: type: object isCurrent: type: boolean isCurrentNotVirtual: type: boolean label: type: object isVirtual: type: boolean darkstoreId: type: number fullInfo: type: object area: type: array items: type: string openingTime: type: object properties: openHour: type: number openMinute: type: number closeHour: type: number closeMinute: type: number localTime: type: boolean courierSplitEnabled: type: boolean sortKey: type: number sliderId: type: number available: type: boolean unavailableMessage: type: string unavailableFromTime: type: object unavailableUntilTime: type: string orderStackingEnabled: type: string darkstoreRuntimeSettigs: type: object properties: pickerStackRemovePermission: type: string isModifyStackSortOrderAllowed: type: boolean isAddressVisibleForCourier: type: boolean transportTypeForCouriers: type: object alcoholReceiptPrintingGroup: type: object properties: groupName: type: string pharmacyReceiptPrintingGroup: type: object properties: groupName: type: string additionalBasketFees: type: object properties: longDistanceFeeMinDistanceInM: type: object longDistanceFee: type: object isVirtualSplitEnabled: type: boolean additionalInfoText: type: object success: type: boolean errors: type: array items: {} errorTexts: type: string options: summary: OPTIONS default responses: {} /api/users/anonymous: post: summary: POST anonymous responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: token: type: string refreshToken: type: string expires: type: number user: type: object properties: id: type: number email: type: string firstname: type: string lastname: type: string phone: type: string newsletter: type: boolean newsletterEmail: type: boolean newsletterPhone: type: boolean sendInfo: type: boolean roles: type: array items: type: string franchises: type: object anonymousClientSource: type: string registerClientSource: type: string anonymous: type: boolean success: type: boolean errors: type: array items: {} errorTexts: type: string requestBody: content: application/json: schema: type: string options: summary: OPTIONS anonymous responses: {} /api/inventory/settings: get: summary: GET settings responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: storageBaseUrl: type: string productBaseUrl: type: string imageTransformationUrl: type: string success: type: boolean errors: type: array items: {} errorTexts: type: string options: summary: OPTIONS settings responses: {} /api/transactionsettings: get: summary: GET transactionsettings responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: discountThreshold: type: number maxWeight: type: number minOrder: type: number freeShippingThreshold: type: number deliveryCost: type: number discountPercent: type: number maxStackSize: type: number maxStackTimeInMins: type: number startStackFromClosest: type: boolean paperBagEan: type: string paperBagFee: type: number paperBagPerXItems: type: number paperBagPerXPrice: type: object paperBagInfiniteStock: type: boolean alcoholCategories: type: string verifyAgeCategories: type: string excludeFromAlcoholAndVerifyAgeEans: type: string success: type: boolean errors: type: array items: {} errorTexts: type: string options: summary: OPTIONS transactionsettings responses: {} /api/users/profile: get: summary: GET profile responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: firstName: type: string lastName: type: string phone: type: string dateOfBirth: type: object email: type: string newsletter: type: boolean sendInfo: type: boolean smsCode: type: object newsletterPhone: type: boolean newsletterEmail: type: boolean anonymous: type: boolean success: type: boolean errors: type: array items: {} errorTexts: type: string options: summary: OPTIONS profile responses: {} /api/deliveryfees: get: summary: GET deliveryfees responses: '200': description: OK content: application/json: schema: type: object properties: value: type: array items: type: object properties: sortKey: type: number valueFrom: type: number valueTo: type: number fee: type: number success: type: boolean errors: type: array items: {} errorTexts: type: string options: summary: OPTIONS deliveryfees responses: {} /api/inventory/promo/5/full: get: summary: GET full responses: '200': description: OK content: application/json: schema: type: object properties: value: type: array items: type: object properties: product: type: object properties: ean: type: string headline: type: string title: type: string subTitle: type: string price: type: number vat: type: number isAlcohol: type: boolean verifyAge: type: boolean maxQuantity: type: number packInfo: type: string mainCategoryId: type: number subCategoryId: type: number imagePath: type: string imagePathLastComponent: type: string isHit: type: boolean isPromo: type: boolean isNew: type: boolean isCold: type: boolean isBundle: type: boolean dimensions: type: string information: type: string ingredients: type: string dietary: type: string storage: type: string misk: type: string contact: type: object weight: type: string nutrition: type: string pickInfo: type: object properties: shelf: type: number level: type: number levelColor: type: object path: type: number quantity: type: number supplier: type: string isThermalBagRequired: type: boolean priceBeforePromo: type: object promoText: type: string packUnit: type: string packAmount: type: number pricePerUnitText: type: string id: type: number ean: type: string type: type: string success: type: boolean errors: type: array items: {} errorTexts: type: string options: summary: OPTIONS full responses: {} /api/sliders/5: get: summary: GET 5 responses: '200': description: OK content: application/json: schema: type: object properties: value: type: array items: type: object properties: link: type: string type: type: number position: type: number categoryId: type: object mainCategoryId: type: object containsAlco: type: boolean redirectUrl: type: object webRedirectUrl: type: object redirectable: type: boolean isExternalUrl: type: boolean success: type: boolean errors: type: array items: {} errorTexts: type: string options: summary: OPTIONS 5 responses: {} /api/inventory/5/categories/31/products: get: summary: GET products responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: id: type: number name: type: string key: type: string number: type: number order: type: number imagePath: type: string iconPath: type: string mobileCategoryView: type: object mobileMainCategoryView: type: object webCategoryMainView: type: object parentId: type: object alcohol: type: boolean verifyAge: type: boolean backgroundColor: type: string subCategories: type: array items: {} products: type: array items: type: object properties: id: type: number quantity: type: number ean: type: string maxQuantity: type: number headline: type: string title: type: string subTitle: type: string price: type: number packInfo: type: string imagePath: type: string isHit: type: boolean isPromo: type: boolean priceBeforePromo: type: object isAlcohol: type: boolean verifyAge: type: boolean promoText: type: string isNew: type: boolean isCold: type: boolean showOutOfStock: type: boolean weight: type: string vat: type: number categoryId: type: number subCategoryId: type: number subCategoryName: type: string stockItemId: type: number isBundle: type: boolean bundleId: type: object packUnit: type: string packAmount: type: number pricePerUnitText: type: object sortOrder: type: number isSubCategorySorted: type: boolean stockItemId: type: number categorySeo: type: object success: type: boolean errors: type: array items: {} errorTexts: type: string options: summary: OPTIONS products responses: {} /api/inventory/5/categories/73/products: get: summary: GET products responses: '200': description: OK content: application/json: schema: type: object properties: value: type: object properties: id: type: number name: type: string key: type: string number: type: number order: type: number imagePath: type: string iconPath: type: string mobileCategoryView: type: object mobileMainCategoryView: type: object webCategoryMainView: type: object parentId: type: object alcohol: type: boolean verifyAge: type: boolean backgroundColor: type: string subCategories: type: array items: {} products: type: array items: type: object properties: id: type: number quantity: type: number ean: type: string maxQuantity: type: number headline: type: string title: type: string subTitle: type: string price: type: number packInfo: type: string imagePath: type: string isHit: type: boolean isPromo: type: boolean priceBeforePromo: type: object isAlcohol: type: boolean verifyAge: type: boolean promoText: type: string isNew: type: boolean isCold: type: boolean showOutOfStock: type: boolean weight: type: string vat: type: number categoryId: type: number subCategoryId: type: number subCategoryName: type: string stockItemId: type: number isBundle: type: boolean bundleId: type: object packUnit: type: string packAmount: type: number pricePerUnitText: type: object sortOrder: type: number isSubCategorySorted: type: boolean stockItemId: type: number categorySeo: type: object success: type: boolean errors: type: array items: {} errorTexts: type: string options: summary: OPTIONS products responses: {} /api/inventory/5/stocks: get: summary: GET stocks responses: '200': description: OK content: application/json: schema: type: object properties: value: type: array items: type: object properties: e: type: string q: type: number success: type: boolean errors: type: array items: {} errorTexts: type: string options: summary: OPTIONS stocks responses: {} x-path-templates: # Remove the ignore: prefix to generate an endpoint with its URL # Lines that are closer to the top take precedence, the matching is greedy - ignore:/ - ignore:/favicon.svg - ignore:/serverBuild/assets/LisekFooter.png - ignore:/serverBuild/assets/LogoText.png - ignore:/serverBuild/assets/apple.png - ignore:/serverBuild/assets/basicInformationsDays.jpg - ignore:/serverBuild/assets/basicInformationsDelivery.jpg - ignore:/serverBuild/assets/basicInformationsHours.jpg - ignore:/serverBuild/assets/hand.png - ignore:/serverBuild/assets/leftHand.png - ignore:/serverBuild/assets/lisek_logo.png - ignore:/serverBuild/assets/logoOrange.png - ignore:/serverBuild/assets/logoWhite.png - ignore:/static/css/main.7ef204a2.css - ignore:/static/js/main.ca596ece.js - ignore:/static/media/hand.9bb7bbeaf6bcf0bbf6ce.png - ignore:/static/media/leftHand.e9bb7969d7df1ed82595.png - ignore:/static/media/lisek_logo.3dc197ddbb239a0876c3.png - /api/inventory/{id}/categories/{id1}/products - /api/inventory/{id}/stocks - /api/inventory/promo/{id}/full - /api/sliders/{id} ================================================ FILE: testdata/form_data_flows ================================================ 9643:4:type;4:http;7:version;2:18#9:websocket;0:~8:response;870:6:reason;0:,11:status_code;3:200#13:timestamp_end;18:1681562856.2977424^15:timestamp_start;17:1681562856.296917^8:trailers;0:~7:content;457:{ "args": {}, "data": "", "files": {}, "form": { "param1": "value1", "param2": "value2" }, "headers": { "Accept": "*/*", "Content-Length": "27", "Content-Type": "application/x-www-form-urlencoded", "Host": "httpbin.org", "User-Agent": "curl/8.0.1", "X-Amzn-Trace-Id": "Root=1-643a9ce8-32da318c3f70c7e22d766f7a" }, "json": null, "origin": "185.191.246.39", "url": "https://httpbin.org/post" } ,7:headers;230:40:4:date,29:Sat, 15 Apr 2023 12:47:36 GMT,]36:12:content-type,16:application/json,]24:14:content-length,3:457,]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;421: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;17:1681562856.166926^15:timestamp_start;18:1681562856.1657524^8:trailers;0:~7:content;27:param1=value1¶m2=value2,7:headers;135:28:10:user-agent,10:curl/8.0.1,]15:6:accept,3:*/*,]53:12:content-type,33:application/x-www-form-urlencoded,]23:14:content-length,2:27,]]12:http_version;8:HTTP/2.0,}17:timestamp_created;17:1681562856.165837^7:comment;0:;8:metadata;0:}6:marked;0:;9:is_replay;0:~11:intercepted;5:false!11:server_conn;7528: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;18:1681562856.1617825^19:timestamp_tcp_setup;17:1681562855.901848^15:timestamp_start;17:1681562855.763347^13:timestamp_end;18:1681562856.2998886^14:source_address;24:12:10.123.1.192;5:50914#]3:sni;11:httpbin.org;10:ip_address;23:13:34.193.132.77;3:443#]2:id;36:903841ae-5f9c-4e52-86af-43f72c2b6462;4:alpn;2:h2,7:address;21:11:httpbin.org;3:443#]}11:client_conn;533: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;18:1681562856.1647072^15:timestamp_start;17:1681562855.762146^13:timestamp_end;18:1681562856.2994134^3:sni;11:httpbin.org;8:mitmcert;0:~2:id;36:d0e9fa9a-7203-4e31-a339-c0e3b51c5ae7;11:cipher_name;22:TLS_AES_256_GCM_SHA384;4:alpn;2:h2,7:address;20:9:127.0.0.1;5:37838#]}5:error;0:~2:id;36:89824d21-9acf-4449-956d-0cf76a777b19;} ================================================ FILE: testdata/generic_keys_flows ================================================ 3299:9:websocket;0:~8:response;1105:6:reason;2:OK,11:status_code;3:200#13:timestamp_end;18:1709848351.4360309^15:timestamp_start;17:1709848351.435767^8:trailers;0:~7:content;765:{"numeric": {"1234": {"lorem": "ipsum", "dolor": "sit", "amet": "consectetur"}, "5678": {"lorem": "ipsum", "dolor": "sit", "amet": "consectetur"}, "0000": {"lorem": "ipsum", "dolor": "sit", "amet": "consectetur"}}, "uuid": {"123e4567-e89b-12d3-a456-426614174000": {"lorem": "ipsum", "dolor": "sit", "amet": "consectetur"}, "123e4567-e89b-12d3-a456-426614174001": {"lorem": "ipsum", "dolor": "sit", "amet": "consectetur"}, "123e4567-e89b-12d3-a456-426614174002": {"lorem": "ipsum", "dolor": "sit", "amet": "consectetur"}}, "mixed": {"1234": {"lorem": "ipsum", "dolor": "sit", "amet": "consectetur"}, "123e4567-e89b-12d3-a456-426614174000": {"lorem": "ipsum", "dolor": "sit", "amet": "consectetur"}, "0000": {"lorem": "ipsum", "dolor": "sit", "amet": "consectetur"}}},7:headers;155:39:6:Server,26:BaseHTTP/0.6 Python/3.11.7,]40:4:Date,29:Thu, 07 Mar 2024 21:52:31 GMT,]36:12:Content-type,16:application/json,]24:14:Content-length,3:765,]]12:http_version;8:HTTP/1.0,}7:request;1003:4:path;1:/,9:authority;0:,6:scheme;4:http,6:method;4:POST,4:port;4:8082#4:host;9:localhost;13:timestamp_end;17:1709848351.433835^15:timestamp_start;16:1709848351.43342^8:trailers;0:~7:content;532:{"numeric": {"1234": {"lorem": "ipsum", "dolor": "sit", "amet": "consectetur"}, "5678": {"lorem": "ipsum", "dolor": "sit", "amet": "consectetur"}}, "uuid": {"123e4567-e89b-12d3-a456-426614174000": {"lorem": "ipsum", "dolor": "sit", "amet": "consectetur"}, "123e4567-e89b-12d3-a456-426614174001": {"lorem": "ipsum", "dolor": "sit", "amet": "consectetur"}}, "mixed": {"1234": {"lorem": "ipsum", "dolor": "sit", "amet": "consectetur"}, "123e4567-e89b-12d3-a456-426614174000": {"lorem": "ipsum", "dolor": "sit", "amet": "consectetur"}}},7:headers;232:25:4:Host,14:localhost:8082,]40:10:User-Agent,22:python-requests/2.31.0,]36:15:Accept-Encoding,13:gzip, deflate,]15:6:Accept,3:*/*,]28:10:Connection,10:keep-alive,]36:12:Content-Type,16:application/json,]24:14:Content-Length,3:532,]]12:http_version;8:HTTP/1.1,}6:backup;0:~17:timestamp_created;17:1709848351.433495^7:comment;0:;8:metadata;0:}6:marked;0:;9:is_replay;0:~11:intercepted;5:false!11:server_conn;466:3:via;0:~19:timestamp_tcp_setup;18:1709848351.4350889^7:address;19:9:localhost;4:8082#]19:timestamp_tls_setup;0:~13:timestamp_end;17:1709848351.436529^15:timestamp_start;17:1709848351.434129^3:sni;0:~11:tls_version;0:~11:cipher_list;0:]6:cipher;0:~11:alpn_offers;0:]4:alpn;0:~16:certificate_list;0:]3:tls;5:false!5:error;0:~18:transport_protocol;3:tcp;2:id;36:48b3aee0-862d-4ed5-b160-594334eaaa19;8:sockname;20:9:127.0.0.1;5:61640#]8:peername;19:9:127.0.0.1;4:8082#]}11:client_conn;421:10:proxy_mode;7:regular;8:mitmcert;0:~19:timestamp_tls_setup;0:~13:timestamp_end;17:1709848351.436451^15:timestamp_start;17:1709848351.432699^3:sni;0:~11:tls_version;0:~11:cipher_list;0:]6:cipher;0:~11:alpn_offers;0:]4:alpn;0:~16:certificate_list;0:]3:tls;5:false!5:error;0:~18:transport_protocol;3:tcp;2:id;36:50acece3-e5e3-460f-b761-be89aa458880;8:sockname;21:3:::1;4:8080#1:0#1:0#]8:peername;22:3:::1;5:61638#1:0#1:0#]}5:error;0:~2:id;36:14ae0961-3df8-41b0-8ddb-510082083178;4:type;4:http;7:version;2:20#} ================================================ FILE: testdata/generic_keys_testclient.py ================================================ # -*- coding: utf-8 -*- import json from testclient import testclient # Sample data data = { "numeric": { "1234": { "lorem": "ipsum", "dolor": "sit", "amet": "consectetur", }, "5678": { "lorem": "ipsum", "dolor": "sit", "amet": "consectetur", }, }, "uuid": { "123e4567-e89b-12d3-a456-426614174000": { "lorem": "ipsum", "dolor": "sit", "amet": "consectetur", }, "123e4567-e89b-12d3-a456-426614174001": { "lorem": "ipsum", "dolor": "sit", "amet": "consectetur", }, }, "mixed": { "1234": { "lorem": "ipsum", "dolor": "sit", "amet": "consectetur", }, "123e4567-e89b-12d3-a456-426614174000": { "lorem": "ipsum", "dolor": "sit", "amet": "consectetur", }, }, } testclient( "application/json", lambda: json.dumps(data), lambda content: json.loads(content), ) ================================================ FILE: testdata/generic_keys_testserver.py ================================================ # -*- coding: utf-8 -*- import json from testserver import TestServerHandler, launchServerWith class GenericKeysHandler(TestServerHandler): def transform_data(self, raw_data): data = json.loads(raw_data) data["numeric"]["0000"] = { "lorem": "ipsum", "dolor": "sit", "amet": "consectetur", } data["uuid"]["123e4567-e89b-12d3-a456-426614174002"] = { "lorem": "ipsum", "dolor": "sit", "amet": "consectetur", } data["mixed"]["0000"] = { "lorem": "ipsum", "dolor": "sit", "amet": "consectetur", } # Encode the modified data return bytes(json.dumps(data), "utf-8") if __name__ == "__main__": launchServerWith(GenericKeysHandler) ================================================ FILE: testdata/msgpack_flows ================================================ 2099:9:websocket;0:~8:response;393:6:reason;2:OK,11:status_code;3:200#13:timestamp_end;18:1704284722.2074344^15:timestamp_start;18:1704284722.2070074^8:trailers;0:~7:content;51:field1value1field2value2new_fieldAdded Field,7:headers;157:39:6:Server,26:BaseHTTP/0.6 Python/3.11.6,]40:4:Date,29:Wed, 03 Jan 2024 12:25:22 GMT,]39:12:Content-type,19:application/msgpack,]23:14:Content-length,2:51,]]12:http_version;8:HTTP/1.0,}7:request;514:4:path;1:/,9:authority;0:,6:scheme;4:http,6:method;4:POST,4:port;4:8082#4:host;9:localhost;13:timestamp_end;18:1704284722.2042184^15:timestamp_start;18:1704284722.2037127^8:trailers;0:~7:content;29:field1value1field2value2,7:headers;244:25:4:Host,14:localhost:8082,]40:10:User-Agent,22:python-requests/2.31.0,]46:15:Accept-Encoding,23:gzip, deflate, br, zstd,]15:6:Accept,3:*/*,]28:10:Connection,10:keep-alive,]39:12:Content-Type,19:application/msgpack,]23:14:Content-Length,2:29,]]12:http_version;8:HTTP/1.1,}6:backup;0:~17:timestamp_created;17:1704284722.203833^7:comment;0:;8:metadata;0:}6:marked;0:;9:is_replay;0:~11:intercepted;5:false!11:server_conn;467:3:via;0:~19:timestamp_tcp_setup;18:1704284722.2062707^7:address;19:9:localhost;4:8082#]19:timestamp_tls_setup;0:~13:timestamp_end;17:1704284722.208372^15:timestamp_start;18:1704284722.2047434^3:sni;0:~11:tls_version;0:~11:cipher_list;0:]6:cipher;0:~11:alpn_offers;0:]4:alpn;0:~16:certificate_list;0:]3:tls;5:false!5:error;0:~18:transport_protocol;3:tcp;2:id;36:0d4b5407-f87c-4a56-b1be-0eb43eef8855;8:sockname;20:9:127.0.0.1;5:47522#]8:peername;19:9:127.0.0.1;4:8082#]}11:client_conn;423:10:proxy_mode;7:regular;8:mitmcert;0:~19:timestamp_tls_setup;0:~13:timestamp_end;18:1704284722.2085643^15:timestamp_start;18:1704284722.2027926^3:sni;0:~11:tls_version;0:~11:cipher_list;0:]6:cipher;0:~11:alpn_offers;0:]4:alpn;0:~16:certificate_list;0:]3:tls;5:false!5:error;0:~18:transport_protocol;3:tcp;2:id;36:d298f071-2025-4766-b64d-6a868949b78c;8:sockname;21:3:::1;4:8080#1:0#1:0#]8:peername;22:3:::1;5:60670#1:0#1:0#]}5:error;0:~2:id;36:acc4fde4-062c-4fbd-b7a8-5a46d3a0bcfc;4:type;4:http;7:version;2:20#} ================================================ FILE: testdata/msgpack_testclient.py ================================================ # -*- coding: utf-8 -*- import msgpack from testclient import testclient # Sample MessagePack data msgpack_data = {"field1": "value1", "field2": "value2"} testclient( "application/msgpack", lambda: msgpack.packb(msgpack_data), lambda content: msgpack.unpackb(content), ) ================================================ FILE: testdata/msgpack_testserver.py ================================================ # -*- coding: utf-8 -*- import msgpack from testserver import TestServerHandler, launchServerWith class MessagePackHandler(TestServerHandler): def transform_data(self, raw_data): data = msgpack.unpackb(raw_data, raw=False) # Add a new field to the data data["new_field"] = "Added Field" # Encode the modified data as MessagePack return msgpack.packb(data, use_bin_type=True) if __name__ == "__main__": launchServerWith(MessagePackHandler) ================================================ FILE: testdata/sklep.lisek.app.har ================================================ [File too large to display: 17.1 MB] ================================================ FILE: testdata/test_flows ================================================ 9330:4:type;4:http;7:version;2:18#9:websocket;0:~8:response;669:6:reason;0:,11:status_code;3:200#13:timestamp_end;18:1681560864.5100336^15:timestamp_start;18:1681560864.5091748^8:trailers;0:~7:content;255:{ "args": {}, "headers": { "Accept": "*/*", "Host": "httpbin.org", "User-Agent": "curl/8.0.1", "X-Amzn-Trace-Id": "Root=1-643a9520-69df77c0122f50262a42a72b" }, "origin": "185.191.246.39", "url": "https://httpbin.org/get" } ,7:headers;230:40:4:date,29:Sat, 15 Apr 2023 12:14:24 GMT,]36:12:content-type,16:application/json,]24:14:content-length,3:255,]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;307:4:path;4:/get,9:authority;11:httpbin.org,6:scheme;5:https,6:method;3:GET,4:port;3:443#4:host;11:httpbin.org;13:timestamp_end;18:1681560864.3789876^15:timestamp_start;18:1681560864.3779225^8:trailers;0:~7:content;0:,7:headers;51:28:10:user-agent,10:curl/8.0.1,]15:6:accept,3:*/*,]]12:http_version;8:HTTP/2.0,}17:timestamp_created;18:1681560864.3780086^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;18:1681560864.3739192^19:timestamp_tcp_setup;18:1681560864.1132984^15:timestamp_start;18:1681560863.9839542^13:timestamp_end;18:1681560864.5125768^14:source_address;24:12:10.123.1.192;5:41702#]3:sni;11:httpbin.org;10:ip_address;23:13:107.22.139.22;3:443#]2:id;36:084956c5-1385-4454-8fb0-1790213cebb2;4:alpn;2:h2,7:address;21:11:httpbin.org;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:1681560864.377041^15:timestamp_start;17:1681560863.982756^13:timestamp_end;18:1681560864.5118477^3:sni;11:httpbin.org;8:mitmcert;0:~2:id;36:f61b070d-3526-4d24-bcdd-3b0325450cd9;11:cipher_name;22:TLS_AES_256_GCM_SHA384;4:alpn;2:h2,7:address;20:9:127.0.0.1;5:49070#]}5:error;0:~2:id;36:8dddfa8d-d637-43c7-a5c1-f81f69afe121;}9592:4:type;4:http;7:version;2:18#9:websocket;0:~8:response;841:6:reason;0:,11:status_code;3:200#13:timestamp_end;17:1681560867.414883^15:timestamp_start;18:1681560867.4140136^8:trailers;0:~7:content;428:{ "args": {}, "data": "", "files": {}, "form": { "{\"a\":1}": "" }, "headers": { "Accept": "*/*", "Content-Length": "7", "Content-Type": "application/x-www-form-urlencoded", "Host": "httpbin.org", "User-Agent": "curl/8.0.1", "X-Amzn-Trace-Id": "Root=1-643a9522-7cef244e5eb7ed6c6b3ddea8" }, "json": null, "origin": "185.191.246.39", "url": "https://httpbin.org/post" } ,7:headers;230:40:4:date,29:Sat, 15 Apr 2023 12:14:27 GMT,]36:12:content-type,16:application/json,]24:14:content-length,3:428,]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;399: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;17:1681560866.837843^15:timestamp_start;18:1681560866.8367467^8:trailers;0:~7:content;7:{"a":1},7:headers;134:28:10:user-agent,10:curl/8.0.1,]15:6:accept,3:*/*,]22:14:content-length,1:7,]53:12:content-type,33:application/x-www-form-urlencoded,]]12:http_version;8:HTTP/2.0,}17:timestamp_created;17:1681560866.836836^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:1681560866.832675^19:timestamp_tcp_setup;18:1681560866.5705638^15:timestamp_start;18:1681560866.4401593^13:timestamp_end;18:1681560867.4179919^14:source_address;24:12:10.123.1.192;5:51808#]3:sni;11:httpbin.org;10:ip_address;23:13:34.235.32.249;3:443#]2:id;36:9487b118-71d6-4fca-9bfc-253d5f1b3448;4:alpn;2:h2,7:address;21:11:httpbin.org;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:1681560866.835816^15:timestamp_start;18:1681560866.4387221^13:timestamp_end;17:1681560867.417156^3:sni;11:httpbin.org;8:mitmcert;0:~2:id;36:0ba9a3de-e7f6-4dfa-b54b-2c00c6955e23;11:cipher_name;22:TLS_AES_256_GCM_SHA384;4:alpn;2:h2,7:address;20:9:127.0.0.1;5:49086#]}5:error;0:~2:id;36:fbb50234-b867-4bd9-99f5-3d0b3c8f4c8f;}9598:4:type;4:http;7:version;2:18#9:websocket;0:~8:response;843:6:reason;0:,11:status_code;3:200#13:timestamp_end;18:1681560874.8873558^15:timestamp_start;18:1681560874.8865132^8:trailers;0:~7:content;429:{ "args": {}, "data": "", "files": {}, "form": { "{\"a\":1}": "" }, "headers": { "Accept": "*/*", "Content-Length": "7", "Content-Type": "application/x-www-form-urlencoded", "Host": "httpbin.org", "User-Agent": "curl/8.0.1", "X-Amzn-Trace-Id": "Root=1-643a952a-5d03a95e63c2203b4297f609" }, "json": null, "origin": "185.191.246.39", "url": "https://httpbin.org/patch" } ,7:headers;230:40:4:date,29:Sat, 15 Apr 2023 12:14:34 GMT,]36:12:content-type,16:application/json,]24:14:content-length,3:429,]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;402:4:path;6:/patch,9:authority;11:httpbin.org,6:scheme;5:https,6:method;5:PATCH,4:port;3:443#4:host;11:httpbin.org;13:timestamp_end;18:1681560874.7564108^15:timestamp_start;18:1681560874.7553363^8:trailers;0:~7:content;7:{"a":1},7:headers;134:28:10:user-agent,10:curl/8.0.1,]15:6:accept,3:*/*,]22:14:content-length,1:7,]53:12:content-type,33:application/x-www-form-urlencoded,]]12:http_version;8:HTTP/2.0,}17:timestamp_created;18:1681560874.7554216^7:comment;0:;8:metadata;0:}6:marked;0:;9:is_replay;0:~11:intercepted;5:false!11:server_conn;7528: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;18:1681560874.7513738^19:timestamp_tcp_setup;17:1681560874.489256^15:timestamp_start;17:1681560874.359904^13:timestamp_end;18:1681560874.8898144^14:source_address;24:12:10.123.1.192;5:37992#]3:sni;11:httpbin.org;10:ip_address;23:13:34.193.132.77;3:443#]2:id;36:dde87769-1017-459d-b745-f356b071161a;4:alpn;2:h2,7:address;21:11:httpbin.org;3:443#]}11:client_conn;533: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;18:1681560874.7544107^15:timestamp_start;18:1681560874.3587303^13:timestamp_end;17:1681560874.889114^3:sni;11:httpbin.org;8:mitmcert;0:~2:id;36:ea2f18a3-06c7-4651-90ad-6b1c699a5cab;11:cipher_name;22:TLS_AES_256_GCM_SHA384;4:alpn;2:h2,7:address;20:9:127.0.0.1;5:34018#]}5:error;0:~2:id;36:93edf24d-fa87-4039-a21f-5ed4a75b9109;}9386:4:type;4:http;7:version;2:18#9:websocket;0:~8:response;698:6:reason;0:,11:status_code;3:200#13:timestamp_end;17:1681561853.212749^15:timestamp_start;18:1681561853.2118688^8:trailers;0:~7:content;285:{ "args": {}, "headers": { "Accept": "*/*", "Host": "httpbin.org", "Special-Header": "Yes", "User-Agent": "curl/8.0.1", "X-Amzn-Trace-Id": "Root=1-643a98fc-54ded9b1073a7ecd1e75e8f4" }, "origin": "185.191.246.39", "url": "https://httpbin.org/get" } ,7:headers;230:40:4:date,29:Sat, 15 Apr 2023 12:30:53 GMT,]36:12:content-type,16:application/json,]24:14:content-length,3:285,]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;335:4:path;4:/get,9:authority;11:httpbin.org,6:scheme;5:https,6:method;3:GET,4:port;3:443#4:host;11:httpbin.org;13:timestamp_end;18:1681561852.9074075^15:timestamp_start;18:1681561852.9063168^8:trailers;0:~7:content;0:,7:headers;79:28:10:user-agent,10:curl/8.0.1,]15:6:accept,3:*/*,]24:14:special-header,3:Yes,]]12:http_version;8:HTTP/2.0,}17:timestamp_created;17:1681561852.906412^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:1681561852.902216^19:timestamp_tcp_setup;18:1681561852.6425617^15:timestamp_start;18:1681561852.5023966^13:timestamp_end;18:1681561853.2151525^14:source_address;24:12:10.123.1.192;5:52728#]3:sni;11:httpbin.org;10:ip_address;23:13:34.235.32.249;3:443#]2:id;36:f911dd43-02ea-4f30-9d77-f11a26d5caac;4:alpn;2:h2,7:address;21:11:httpbin.org;3:443#]}11:client_conn;533: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:1681561852.905259^15:timestamp_start;18:1681561852.5008457^13:timestamp_end;18:1681561853.2144651^3:sni;11:httpbin.org;8:mitmcert;0:~2:id;36:f7772820-8aae-454a-a9c8-09f609016430;11:cipher_name;22:TLS_AES_256_GCM_SHA384;4:alpn;2:h2,7:address;20:9:127.0.0.1;5:32874#]}5:error;0:~2:id;36:fd3491bf-d73c-4424-aeb0-a56ba0c69bcd;} ================================================ FILE: testdata/testclient.py ================================================ # -*- coding: utf-8 -*- from typing import Any, Callable import requests # type: ignore def testclient( contentType: str, getData: Callable[[], Any], decodeData: Callable[[Any], Any], ) -> None: url = "http://localhost:8082" headers = {"Content-Type": contentType} response = requests.post( url, data=getData(), headers=headers, proxies={"http": "http://localhost:8080", "https": "http://localhost:8080"}, ) # Print the response print(response.status_code) print(response.headers) # convert the response data from MessagePack to JSON data = decodeData(response.content) print(data) ================================================ FILE: testdata/testserver.py ================================================ # -*- coding: utf-8 -*- import http.server import socketserver from typing import Type class TestServerHandler(http.server.BaseHTTPRequestHandler): def do_POST(self): content_length = int(self.headers["Content-Length"]) raw_data = self.rfile.read(content_length) try: # Decode received data print(raw_data) modified_data = self.transform_data(raw_data) # Send the response self.send_response(200) self.send_header("Content-type", self.headers["Content-type"]) self.send_header("Content-length", len(modified_data)) self.end_headers() self.wfile.write(modified_data) except Exception as e: print(f"Error processing request: {str(e)}") self.send_response(500) self.send_header("Content-type", "text/plain") self.end_headers() self.wfile.write(f"Error processing request: {str(e)}".encode()) def transform_data(self, raw_data): raise NotImplementedError("Subclasses must implement this method") def launchServerWith(handler: Type[TestServerHandler]): PORT = 8082 with socketserver.TCPServer(("", PORT), handler) as httpd: print(f"Serving on port {PORT}") httpd.serve_forever()