[
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "// For format details, see https://aka.ms/devcontainer.json. For config options, see the\n// README at: https://github.com/devcontainers/templates/tree/main/src/python\n{\n\t\"name\": \"DNSChef-NG\",\n\t// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile\n\t\"image\": \"mcr.microsoft.com/devcontainers/python:1-3.11\",\n\t\"features\": {\n\t\t\"ghcr.io/devcontainers-contrib/features/poetry:2\": {}\n\t},\n\t\"customizations\": {\n\t\t\"vscode\": {\n\t\t\t\"extensions\": [\n\t\t\t\t\"tamasfe.even-better-toml\"\n\t\t\t]\n\t\t}\n\t}\n\n\t// Features to add to the dev container. More info: https://containers.dev/features.\n\t// \"features\": {},\n\n\t// Use 'forwardPorts' to make a list of ports inside the container available locally.\n\t// \"forwardPorts\": [],\n\n\t// Use 'postCreateCommand' to run commands after the container is created.\n\t// \"postCreateCommand\": \"pip3 install --user -r requirements.txt\",\n\n\t// Configure tool-specific properties.\n\t// \"customizations\": {},\n\n\t// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.\n\t// \"remoteUser\": \"root\"\n}\n"
  },
  {
    "path": ".dockerignore",
    "content": "tests\n__pycache__\n*.pyc"
  },
  {
    "path": ".github/workflows/python-package.yml",
    "content": "# This workflow will install Python dependencies, run tests and lint with a variety of Python versions\n# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python\n\nname: Python package\n\non:\n  push:\n    branches: [ \"main\" ]\n  pull_request:\n    branches: [ \"main\" ]\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [\"3.11\"]\n\n    steps:\n    - uses: actions/checkout@v3\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@v3\n      with:\n        python-version: ${{ matrix.python-version }}\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        python3 -m pip install --user pipx\n        pipx install poetry\n        poetry install --all-extras\n    - name: Lint with ruff\n      run: |\n        # stop the build if there are Python syntax errors or undefined names\n        poetry run ruff --select=E9,F63,F7,F82 --exit-zero --statistics .\n        poetry run ruff --select=E9,F63,F7,F82 --show-source .\n        # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide\n        poetry run ruff --exit-zero --statistics .\n    - name: Test with pytest\n      run: |\n        poetry run pytest"
  },
  {
    "path": ".github/workflows/python-publish-test.yml",
    "content": "name: Upload Package to PyPi Testing\n\non:\n  workflow_dispatch:\n  #release:\n  #  types: [published]\n\npermissions:\n  contents: read\n\njobs:\n  deploy:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v3\n    - name: Set up Python\n      uses: actions/setup-python@v3\n      with:\n        python-version: '3.11'\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install pipx\n        pipx install poetry\n    - name: Build and publish package\n      run: |\n        poetry config pypi-token.testpypi ${{ secrets.PYPI_TESTING_API_TOKEN }}\n        poetry config repositories.testpypi https://test.pypi.org/legacy/\n        poetry publish -r testpypi --build"
  },
  {
    "path": ".github/workflows/python-publish.yml",
    "content": "name: Upload Package to PyPi\n\non:\n  release:\n    types: [published]\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  deploy:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v3\n    - name: Set up Python\n      uses: actions/setup-python@v3\n      with:\n        python-version: '3.11'\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install pipx\n        pipx install poetry\n    - name: Build and publish package\n      run: |\n        poetry config pypi-token.pypi ${{ secrets.PYPI_API_TOKEN }}\n        poetry publish --build"
  },
  {
    "path": ".gitignore",
    "content": ".vscode\n.DS_Store\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n*.log\n.ruff_cache\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/"
  },
  {
    "path": "CHANGELOG",
    "content": "Version 0.5\n\n* Complete re-write, now fully asynchronous (uses Python's AsyncIO library)\n\nVersion 0.4\n\n* Ported to Python 3.6+\n* Made everything a bit more PEP8 compliant\n* Improved logging\n* Removed IPy library (replaced with built-in ipaddress library)\n\nVersion 0.3\n\n* Added support for the latest version of the dnslib library - 0.9.3\n* Added support for logging. (idea by kafeine)\n* Added support for SRV, DNSKEY, and RRSIG records. (idea by mubix)\n* Added support for TCP remote nameserver connections. (idea by mubix)\n* DNS name matching is now case insensitive.\n* Various small bug fixes and performance tweaks.\n* Python libraries are no longer bundled with the distribution, but\n  compiled in the Windows binary.\n\nVersion 0.2.1\n\n* Fixed a Python 2.6 compatibility issue. (thanks Mehran Goudarzi)\n\nVersion 0.2\n\n* Added IPv6 support.\n* Added AAAA, MX, CNAME, NS, SOA and NAPTR support.\n* Added support for ANY queries (returns all known fake records).\n* Changed file format to support more DNS record types.\n* Added alternative DNS port support (contributed by fnv).\n* Added alternative listening port support for the server (contributed by Mark Straver).\n* Updated bundled dnslib library to the latest version - 0.8.2.\n* Included IPy library for IPv6 support.\n\nVersion 0.1\n\n* First public release\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM python:3.11-slim as build-stage\n\nWORKDIR /tmp/code\n\nCOPY . .\n\nRUN pip wheel --wheel-dir ./dist '.[api]'\n\nFROM python:3.11-slim\n\nWORKDIR /app\n\nCOPY --from=build-stage /tmp/code/dist/ .\n\nRUN pip install --no-cache-dir --no-index --find-links . dnschef[api]\n\nEXPOSE 80 53/udp 53/tcp\n\nCMD [\"uvicorn\", \"dnschef.api:app\", \"--host\", \"0.0.0.0\", \"--port\", \"80\"]\n\n# If using a proxy\n#CMD [\"uvicorn\", \"app.main:app\", \"--proxy-headers\", \"--host\", \"0.0.0.0\", \"--port\", \"80\"]"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (C) 2014 Peter Kacherginsky, Marcello Salvati\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met: \n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer. \n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution. \n3. Neither the name of the copyright holder nor the names of its contributors\n   may be used to endorse or promote products derived from this software without \n   specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
  },
  {
    "path": "Makefile",
    "content": ".PHONY: tests\n\ndefault: build\n\nclean:\n\trm -f -r build/\n\trm -f -r bin/\n\trm -f -r dist/\n\trm -f -r *.egg-info\n\tfind . -name '*.pyc' -exec rm -f {} +\n\tfind . -name '*.pyo' -exec rm -f {} +\n\tfind . -name '*~' -exec rm -f  {} +\n\tfind . -name '__pycache__' -exec rm -rf {} +\n\tfind . -name '.pytest_cache' -exec rm -rf {} +\n\ntests:\n\truff --select=E9,F63,F7,F82 --show-source .\n\tpython -m pytest\n\nrequirements:\n\tpoetry export -f requirements.txt > requirements.txt\n\tpoetry export -f requirements.txt --extras=api > requirements-api.txt\n\tpoetry export -f requirements.txt --with=dev > requirements-dev.txt"
  },
  {
    "path": "README.md",
    "content": "> [!NOTE]\n> This is an updated version of [DNSChef](https://github.com/iphelix/dnschef) originally written by [@iphelix](https://github.com/iphelix)\n\n``` \n     _                _           __                    \n    | |  v0.7        | |         / _|                   \n  __| |_ __  ___  ___| |__   ___| |_ ______ _ __   __ _ \n / _` | '_ \\/ __|/ __| '_ \\ / _ \\  _|______| '_ \\ / _` |\n| (_| | | | \\__ \\ (__| | | |  __/ |        | | | | (_| |\n \\__,_|_| |_|___/\\___|_| |_|\\___|_|        |_| |_|\\__, |\n                                                   __/ |\n                                                  |___/ \n       D O C U M E N T A T I O N\n```\n\nDNSChef is a highly configurable DNS proxy for Penetration Testers and Malware Analysts. A DNS proxy (aka \"Fake DNS\") is a tool used for application network traffic analysis among other uses. For example, a DNS proxy can be used to fake requests for \"badguy.com\" to point to a local machine for termination or interception instead of a real host somewhere on the Internet. \n\nThere are several DNS Proxies out there. Most will simply point all DNS queries a single IP address or implement only rudimentary filtering. DNSChef was developed as part of a penetration test where there was a need for a more configurable system. As a result, DNSChef is cross-platform application capable of forging responses based on inclusive and exclusive domain lists, supporting multiple DNS record types, matching domains with wildcards, proxying true responses for nonmatching domains, defining external configuration files, IPv6 and many other features. You can find detailed explanation of each of the features and suggested uses below.\n\nThe use of DNS Proxy is recommended in situations where it is not possible to force an application to use some other proxy server directly. For example, some mobile applications completely ignore OS HTTP Proxy settings. In these cases, the use of a DNS proxy server such as DNSChef will allow you to trick that application into forwarding connections to the desired destination.\n\n## New Features \n\n- Requires Python 3.11+\n- Supports staging files over DNS (only over `A`,`AAAA`,`TXT` for now...)\n- Config file is now TOML\n- Optional HTTP API (allows you to query logs and update config remotely)\n- Fully async for increased performance (uses AsyncIO)\n- Structured logging and a number of QOL improvements\n- Is now a Python package\n- Dockerized\n- Includes a number of the PRs and fixes from the original repo\n\n## Installing\n\nTo install the latest release you should use [pipx](https://pypa.github.io/pipx/) (unless you're a piece of shit who enjoys sloppy stakes):\n\n    pipx install dnschef-ng\n\nIf you want the HTTP API (requires some extra dependencies):\n    \n    pipx install dnschef-ng[api]\n\nInstall latest version from Git using pipx:\n\n     pipx install git+https://github.com/byt3bl33d3r/dnschef-ng.git\n\nInstall latest version from Git using pipx with the deps for the HTTP API:\n\n     pipx install \"git+https://github.com/byt3bl33d3r/dnschef-ng.git#egg=dnschef-ng[api]\"\n\n\n## Setting up a DNS Proxy\n\nBefore you can start using DNSChef, you must configure your machine to use a DNS nameserver with the tool running on it. You have several options based on the operating system you are going to use:\n\n- **Linux** - Edit */etc/resolv.conf* to include a line on the very top with your traffic analysis host (e.g add \"nameserver 127.0.0.1\"  if you are running locally). Alternatively, you can add a DNS server address using tools such as Network Manager. Inside the Network Manager open IPv4 Settings, select *Automatic (DHCP) addresses only* or *Manual* from the *Method* drop down box and edit *DNS Servers* text box to include an IP address with DNSChef running.\n\n- **Windows** - Select *Network Connections* from the *Control Panel*. Next select one of the connections (e.g. \"Local Area Connection\"), right-click on it and select properties. From within a newly appearing dialog box, select *Internet Protocol (TCP/IP)* and click on properties. At last select *Use the following DNS server addresses* radio button and enter the IP address with DNSChef running. For example, if running locally enter 127.0.0.1.\n\n- **OS X** - Open *System Preferences* and click on the *Network* icon. Select the active interface and fill in the *DNS Server* field. If you are using Airport then you will have to click on *Advanced...* button and edit DNS servers from there. Alternatively, you can edit */etc/resolv.conf* and add a fake nameserver to the very top there (e.g \"nameserver 127.0.0.1\").\n\n- **iOS** - Open *Settings* and select *General*. Next select on *Wi-Fi* and click on a blue arrow to the right of an active Access Point from the list. Edit DNS entry to point to the host with DNSChef running. Make sure you have disabled Cellular interface (if available).\n\n- **Android** - Open *Settings* and select *Wireless and network*.  Click on *Wi-Fi settings* and select *Advanced* after pressing the *Options* button on the phone. Enable *Use static IP* checkbox and configure a custom DNS server.\n\nIf you do not have the ability to modify device's DNS settings manually, then you still have several options involving techniques such as [ARP Spoofing](http://en.wikipedia.org/wiki/ARP_spoofing), [Rogue DHCP](http://www.yersinia.net/doc.htm) and other creative methods.\n\nAt last you need to configure a fake service where DNSChef will point all of the requests. For example, if you are trying to intercept web traffic, you must bring up either a separate web server running on port 80 or set up a web proxy (e.g. Burp) to intercept traffic. DNSChef will point queries to your proxy/server host with properly configured services.\n\n## Running DNSChef\n\nDNSChef is a cross-platform application developed in Python which should run on most platforms which have a Python interpreter. This guide will concentrate on Unix environments; however, all of the examples below were tested to work on Windows as well.\n\nLet's get a taste of DNSChef with its most basic monitoring functionality. Execute the following command as root (required to start a server on port 53):\n\n    # ./dnschef.py\n        \n              _                _          __  \n             | | version 0.2  | |        / _| \n           __| |_ __  ___  ___| |__   ___| |_ \n          / _` | '_ \\/ __|/ __| '_ \\ / _ \\  _|\n         | (_| | | | \\__ \\ (__| | | |  __/ |  \n          \\__,_|_| |_|___/\\___|_| |_|\\___|_|  \n                       iphelix@thesprawl.org  \n\n    [*] DNSChef started on interface: 127.0.0.1 \n    [*] Using the following nameservers: 8.8.8.8\n    [*] No parameters were specified. Running in full proxy mode\n\nWithout any parameters, DNSChef will run in full proxy mode. This means that all requests will simply be forwarded to an upstream DNS server (8.8.8.8 by default) and returned back to the quering host. For example, let's query an \"A\" record for a domain and observe results:\n\n    $ host -t A thesprawl.org\n    thesprawl.org has address 108.59.3.64\n\nDNSChef will print the following log line showing time, source IP address, type of record requested and most importantly which name was queried:\n\n    [23:54:03] 127.0.0.1: proxying the response of type 'A' for thesprawl.org\n\nThis mode is useful for simple application monitoring where you need to figure out which domains it uses for its communications.\n\nDNSChef has full support for IPv6 which can be activated using *-6* or *--ipv6** flags. It works exactly as IPv4 mode with the exception that default listening interface is switched to ::1 and default DNS server is switched to 2001:4860:4860::8888. Here is a sample output:\n\n    # ./dnschef.py -6\n              _                _          __\n             | | version 0.2  | |        / _|\n           __| |_ __  ___  ___| |__   ___| |_\n          / _` | '_ \\/ __|/ __| '_ \\ / _ \\  _|\n         | (_| | | | \\__ \\ (__| | | |  __/ |\n          \\__,_|_| |_|___/\\___|_| |_|\\___|_|\n                       iphelix@thesprawl.org\n\n    [*] Using IPv6 mode.\n    [*] DNSChef started on interface: ::1\n    [*] Using the following nameservers: 2001:4860:4860::8888\n    [*] No parameters were specified. Running in full proxy mode\n    [00:35:44] ::1: proxying the response of type 'A' for thesprawl.org\n    [00:35:44] ::1: proxying the response of type 'AAAA' for thesprawl.org\n    [00:35:44] ::1: proxying the response of type 'MX' for thesprawl.org\n\nNOTE: By default, DNSChef creates a UDP listener. You can use TCP instead with the *--tcp* argument discussed later.\n\n## Running the DNSChef HTTP API\n\n> [!WARNING]\n> The API has no authentication. Allow/deny access at the network level through security groups, iptables, firewall etc..\n\n`uvicorn dnschef.api:app`\n\nYou can then view the OpenAPI documentation at `http://127.0.0.1:8000/docs`\n\n```\n$ uvicorn dnschef.api:app\nINFO:     Started server process [28327]\nINFO:     Waiting for application startup.\n          _                _          __  \n         | | version 0.6.0  | |        / _| \n       __| |_ __  ___  ___| |__   ___| |_ \n      / _` | '_ \\/ __|/ __| '_ \\ / _ \\  _|\n     | (_| | | | \\__ \\ (__| | | |  __/ |  \n      \\__,_|_| |_|___/\\___|_| |_|\\___|_|  \n                @iphelix // @byt3bl33d3r  \n\n2023-09-28 11:24:59 cooking replies                domain=*.thesprawl.org record=192.0.2.1 section=A\n2023-09-28 11:24:59 cooking replies                domain=*.thesprawl.org record=2001:db8::1 section=AAAA\n-- SNIP --\n2023-09-28 11:24:59 cooking replies                domain=*.thesprawl.org record=1 . alpn=h2 ipv4hint=127.0.0.1 ipv6hint=::1 section=HTTPS\nINFO:     Application startup complete.\n2023-09-28 11:24:59 DNSChef is active              interface=127.0.0.1 ipv6=False nameservers=['8.8.8.8'] port=53 tcp=False\nINFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)\n```\n\n## Intercept all responses\n\nNow, that you know how to start DNSChef let's configure it to fake all replies to point to 127.0.0.1 using the *--fakeip* parameter:\n\n    # ./dnschef.py --fakeip 127.0.0.1 -q\n    [*] DNSChef started on interface: 127.0.0.1 \n    [*] Using the following nameservers: 8.8.8.8\n    [*] Cooking all A replies to point to 127.0.0.1\n    [23:55:57] 127.0.0.1: cooking the response of type 'A' for google.com to 127.0.0.1\n    [23:55:57] 127.0.0.1: proxying the response of type 'AAAA' for google.com\n    [23:55:57] 127.0.0.1: proxying the response of type 'MX' for google.com\n\nIn the above output you an see that DNSChef was configured to proxy all requests to 127.0.0.1. The first line of log at 08:11:23 shows that we have \"cooked\" the \"A\" record response to point to 127.0.0.1. However, further requests for 'AAAA' and 'MX' records are simply proxied from a real DNS server. Let's see the output from requesting program:\n\n    $ host google.com localhost\n    google.com has address 127.0.0.1\n    google.com has IPv6 address 2001:4860:4001:803::1001\n    google.com mail is handled by 10 aspmx.l.google.com.\n    google.com mail is handled by 40 alt3.aspmx.l.google.com.\n    google.com mail is handled by 30 alt2.aspmx.l.google.com.\n    google.com mail is handled by 20 alt1.aspmx.l.google.com.\n    google.com mail is handled by 50 alt4.aspmx.l.google.com.\n\nAs you can see the program was tricked to use 127.0.0.1 for the IPv4 address. However, the information obtained from IPv6 (AAAA) and mail (MX) records appears completely legitimate. The goal of DNSChef is to have the least impact on the correct operation of the program, so if an application relies on a specific mailserver it will correctly obtain one through this proxied request.\n\nLet's fake one more request to illustrate how to target multiple records at the same time:\n\n    # ./dnschef.py --fakeip 127.0.0.1 --fakeipv6 ::1 -q\n    [*] DNSChef started on interface: 127.0.0.1 \n    [*] Using the following nameservers: 8.8.8.8\n    [*] Cooking all A replies to point to 127.0.0.1\n    [*] Cooking all AAAA replies to point to ::1\n    [00:02:14] 127.0.0.1: cooking the response of type 'A' for google.com to 127.0.0.1\n    [00:02:14] 127.0.0.1: cooking the response of type 'AAAA' for google.com to ::1\n    [00:02:14] 127.0.0.1: proxying the response of type 'MX' for google.com\n\nIn addition to the --fakeip flag, I have now specified --fakeipv6 designed to fake 'AAAA' record queries. Here is an updated program output:\n\n    $ host google.com localhost\n    google.com has address 127.0.0.1\n    google.com has IPv6 address ::1\n    google.com mail is handled by 10 aspmx.l.google.com.\n    google.com mail is handled by 40 alt3.aspmx.l.google.com.\n    google.com mail is handled by 30 alt2.aspmx.l.google.com.\n    google.com mail is handled by 20 alt1.aspmx.l.google.com.\n    google.com mail is handled by 50 alt4.aspmx.l.google.com.\n\nOnce more all of the records not explicitly overriden by the application were proxied and returned from the real DNS server. However, IPv4 (A) and IPv6 (AAAA) were both faked to point to a local machine.\n\nDNSChef supports multiple record types:\n\nRecord |  Description | Argument | Example\n---|---|---|---\nA     | IPv4 address |--fakeip   | --fakeip 192.0.2.1\nAAAA  | IPv6 address |--fakeipv6 | --fakeipv6 2001:db8::1\nMX    | Mail server  |--fakemail | --fakemail mail.fake.com\nCNAME | CNAME record |--fakealias| --fakealias www.fake.com\nNS    | Name server  |--fakens   | --fakens ns.fake.com\n\nNOTE: For usability not all DNS record types are exposed on the command line. Additional records such as PTR, TXT, SOA, etc. can be specified using the --file flag and an appropriate record header. See the [external definitions file](#external-definitions-file) section below for details.\n\nAt last let's observe how the application handles queries of type ANY:\n\n    # ./dnschef.py --fakeip 127.0.0.1 --fakeipv6 ::1 --fakemail mail.fake.com --fakealias www.fake.com --fakens ns.fake.com -q\n    [*] DNSChef started on interface: 127.0.0.1 \n    [*] Using the following nameservers: 8.8.8.8\n    [*] Cooking all A replies to point to 127.0.0.1\n    [*] Cooking all AAAA replies to point to ::1\n    [*] Cooking all MX replies to point to mail.fake.com\n    [*] Cooking all CNAME replies to point to www.fake.com\n    [*] Cooking all NS replies to point to ns.fake.com\n    [00:17:29] 127.0.0.1: cooking the response of type 'ANY' for google.com with all known fake records.\n\nDNS ANY record queries results in DNSChef returning every faked record that it knows about for an applicable domain. Here is the output that the program will see:\n\n    # host -t ANY google.com localhost\n    google.com has address 127.0.0.1\n    google.com has IPv6 address ::1\n    google.com mail is handled by 10 mail.fake.com.\n    google.com is an alias for www.fake.com.\n    google.com name server ns.fake.com.\n\n## Filtering domains\n\n\nUsing the above example, consider you only want to intercept requests for *thesprawl.org* and leave queries to all other domains such as *webfaction.com* without modification. You can use the *--fakedomains* parameter as illustrated below:\n\n    # ./dnschef.py --fakeip 127.0.0.1 --fakedomains thesprawl.org -q\n    [*] DNSChef started on interface: 127.0.0.1\n    [*] Using the following nameservers: 8.8.8.8  \n    [*] Cooking replies to point to 127.0.0.1 matching: thesprawl.org\n    [00:23:37] 127.0.0.1: cooking the response of type 'A' for thesprawl.org to 127.0.0.1\n    [00:23:52] 127.0.0.1: proxying the response of type 'A' for mx9.webfaction.com\n\nFrom the above example the request for *thesprawl.org* was faked; however, the request for *mx9.webfaction.com* was left alone. Filtering domains is very useful when you attempt to isolate a single application without breaking the rest.\n\n**NOTE**: DNSChef will not verify whether the domain exists or not before faking the response. If you have specified a domain it will always resolve to a fake value whether it really exists or not.\n\n## Reverse filtering\n\nIn another situation you may need to fake responses for all requests except a defined list of domains. You can accomplish this task using the *--truedomains* parameter as follows:\n\n    # ./dnschef.py --fakeip 127.0.0.1 --truedomains thesprawl.org,*.webfaction.com -q\n    [*] DNSChef started on interface: 127.0.0.1\n    [*] Using the following nameservers: 8.8.8.8  \n    [*] Cooking replies to point to 127.0.0.1 not matching: *.webfaction.com, thesprawl.org\n    [00:27:57] 127.0.0.1: proxying the response of type 'A' for mx9.webfaction.com\n    [00:28:05] 127.0.0.1: cooking the response of type 'A' for google.com to 127.0.0.1\n\nThere are several things going on in the above example. First notice the use of a wildcard (*). All domains matching *.webfaction.com will be reverse matched and resolved to their true values. The request for 'google.com' returned 127.0.0.1 because it was not on the list of excluded domains.\n\n**NOTE**: Wildcards are position specific. A mask of type *.thesprawl.org will match www.thesprawl.org but not www.test.thesprawl.org. However, a mask of type *.*.thesprawl.org will match thesprawl.org, www.thesprawl.org and www.test.thesprawl.org.\n\n## External definitions file\n\nThere may be situations where defining a single fake DNS record for all matching domains may not be sufficient. You can use an external file with a collection of DOMAIN=RECORD pairs defining exactly where you want the request to go.\n\nFor example, let create the following definitions file and call it `dnschef.toml`:\n\n```toml\n[A]\n\"*.google.com\"=\"192.0.2.1\"\n\"thesprawl.org\"=\"192.0.2.2\"\n\"*.wordpress.*\"=\"192.0.2.3\"\n```\n\nNotice the section header `[A]`, it defines the record type to DNSChef. Now let's carefully observe the output of multiple queries:\n\n    # ./dnschef.py --file dnschef.toml -q\n    [*] DNSChef started on interface: 127.0.0.1 \n    [*] Using the following nameservers: 8.8.8.8\n    [+] Cooking A replies for domain *.google.com with '192.0.2.1'\n    [+] Cooking A replies for domain thesprawl.org with '192.0.2.2'\n    [+] Cooking A replies for domain *.wordpress.* with '192.0.2.3'\n    [00:43:54] 127.0.0.1: cooking the response of type 'A' for google.com to 192.0.2.1\n    [00:44:05] 127.0.0.1: cooking the response of type 'A' for www.google.com to 192.0.2.1\n    [00:44:19] 127.0.0.1: cooking the response of type 'A' for thesprawl.org to 192.0.2.2\n    [00:44:29] 127.0.0.1: proxying the response of type 'A' for www.thesprawl.org\n    [00:44:40] 127.0.0.1: cooking the response of type 'A' for www.wordpress.org to 192.0.2.3\n    [00:44:51] 127.0.0.1: cooking the response of type 'A' for wordpress.com to 192.0.2.3\n    [00:45:02] 127.0.0.1: proxying the response of type 'A' for slashdot.org\n\nBoth *google.com* and *www.google.com* matched the *\\*.google.com* entry and correctly resolved to *192.0.2.1*. On the other hand *www.thesprawl.org* request was simply proxied instead of being modified. At last all variations of *wordpress.com*, *www.wordpress.org*, etc. matched the *\\*.wordpress.\\** mask and correctly resolved to *192.0.2.3*. At last an undefined *slashdot.org* query was simply proxied with a real response.\n\nYou can specify section headers for all other supported DNS record types including the ones not explicitly exposed on the command line: [A], [AAAA], [MX], [NS], [CNAME], [PTR], [NAPTR] and [SOA]. For example, let's define a new [PTR] section in the `dnschef.toml` file:\n\n```toml\n[PTR]\n\"*.2.0.192.in-addr.arpa\"=\"fake.com\"\n```\n\nLet's observe DNSChef's behavior with this new record type:\n\n     ./dnschef.py --file dnschef.toml -q\n    [sudo] password for iphelix: \n    [*] DNSChef started on interface: 127.0.0.1 \n    [*] Using the following nameservers: 8.8.8.8\n    [+] Cooking PTR replies for domain *.2.0.192.in-addr.arpa with 'fake.com'\n    [00:11:34] 127.0.0.1: cooking the response of type 'PTR' for 1.2.0.192.in-addr.arpa to fake.com\n\nAnd here is what a client might see when performing reverse DNS queries:\n\n    $ host 192.0.2.1 localhost\n    1.2.0.192.in-addr.arpa domain name pointer fake.com.\n\nSome records require exact formatting. Good examples are SOA and NAPTR\n\n```toml\n[SOA]\n\"*.thesprawl.org\" = \"ns.fake.com. hostmaster.fake.com. 1 10800 3600 604800 3600\"\n\n[NAPTR]\n\"*.thesprawl.org\" = \"100 10 U E2U+sip !^.*$!sip:customer-service@fake.com! .\"\n```\n\nSee sample `dnschef.toml` file for additional examples.\n\n## File Staging\n\nDNSChef can \"stage\" any file through DNS. Currently file staging is only supported with `A`, `AAAA` and `TXT` records (will be adding more). To instruct DNSChef to stage a file, add the following section to your `dnschef.toml`:\n\n```toml\n[A]\n\"*.wat.org\" = { file = \"/home/payload.exe\", chunk_size = 4 }\n\n[AAAA]\n\"*.gorgetowngeronimos.org\" = { file = \"/home/payload.exe\", chunk_size = 16 }\n```\n\n> [!NOTE]\n> The `chunk_size` setting is optional and it's behavior is highly dependent on the query type. Example: As `A` queries return an IPv4 address, the maximum allowed `chunk_size` is 4 bytes. Setting the `chunk_size` to anything above 4 will be ignored.\n\nAn `A` query to `*.wat.org` containing a number in the DNS name will now return the corresponding chunk of the file. E.g the query `ns0.wat.org` will return an IPv4 address containing the first chunk of the file (4 bytes). A query for `test1.wat.org` will return the second chunk of the file etc...\n\nWhen using wildcard domains like the above examples, the \"chunk\" numbers can be placed anywhere  and don't have to be put together. E.g an `A` query for `1aliens2.wat.org` will return the 12th chunk of the file.\n\n\n`TXT` records support additional options for file staging as they allow more flexibility:\n\n```toml\n[TXT]\n\"ns*.dungbeetle.org\" = { file = \"~/payload.exe\", chunk_size = 189, response_format = \"{prefix}test-{chunk}\", response_prefix_pool = [\"atlassian-domain-verification=\", \"onetrust-domain-verification=\", \"docusign=\" ] }\n```\n\nWith this configuration, any `TXT` query to `ns*.dungbeetle.org` will return a chunk of our file located locally on the filesystem at `~/payload.exe`.\n\nThe `response_format` and `response_prefix_pool` settings are optional but allow you to further customize the DNS `TXT` response.\n\nThe `response_format` setting defines the format of the `TXT` response: \n\n- The `{prefix}` variable will be randomly substituted with one of the values defined in the `response_prefix_pool` array.\n\n- The `{chunk}` variable will be replaced with the file chunk.\n\nWith the above configuration, a `TXT` query to `ns1.dungbeetle.org` will return the following response:\n\n```\ndocusign=test-<BASE64_ENCODED_FILE_CHUNK_N1>\n```\n\nIf you perform another `TXT` query (e.g. `ns10.dungbeetle.org`), you'll see that the prefix will change:\n\n```\natlassian-domain-verification=test-<BASE64_ENCODED_FILE_CHUNK_N10>\n```\n\n## Advanced Filtering\n\nYou can mix and match input from a file and command line. For example the following command uses both `--file` and `--fakedomains` parameters:\n\n    # ./dnschef.py --file dnschef.toml --fakeip 6.6.6.6 --fakedomains=thesprawl.org,slashdot.org -q\n    [*] DNSChef started on interface: 127.0.0.1 \n    [*] Using the following nameservers: 8.8.8.8\n    [+] Cooking A replies for domain *.google.com with '192.0.2.1'\n    [+] Cooking A replies for domain thesprawl.org with '192.0.2.2'\n    [+] Cooking A replies for domain *.wordpress.* with '192.0.2.3'\n    [*] Cooking A replies to point to 6.6.6.6 matching: *.wordpress.*, *.google.com, thesprawl.org\n    [*] Cooking A replies to point to 6.6.6.6 matching: slashdot.org, *.wordpress.*, *.google.com, thesprawl.org\n    [00:49:05] 127.0.0.1: cooking the response of type 'A' for google.com to 192.0.2.1\n    [00:49:15] 127.0.0.1: cooking the response of type 'A' for slashdot.org to 6.6.6.6\n    [00:49:31] 127.0.0.1: cooking the response of type 'A' for thesprawl.org to 6.6.6.6\n    [00:50:08] 127.0.0.1: proxying the response of type 'A' for tor.com\n\nNotice the definition for *thesprawl.org* in the command line parameter took precedence over *dnschef.toml*. This could be useful if you want to override values in the configuration file. slashdot.org still resolves to the fake IP address because it was specified in the *--fakedomains* parameter. tor.com request is simply proxied since it was not specified in either command line or the configuration file.\n\n## Other configurations\n\nFor security reasons, DNSChef listens on a local 127.0.0.1 (or ::1 for IPv6) interface by default. You can make DNSChef listen on another interface using the *--interface* parameter:\n\n    # ./dnschef.py --interface 0.0.0.0 -q\n    [*] DNSChef started on interface: 0.0.0.0\n    [*] Using the following nameservers: 8.8.8.8 \n    [*] No parameters were specified. Running in full proxy mode\n    [00:50:53] 192.0.2.105: proxying the response of type 'A' for thesprawl.org\n\nor for IPv6:\n\n    # ./dnschef.py -6 --interface :: -q\n    [*] Using IPv6 mode.\n    [*] DNSChef started on interface: ::\n    [*] Using the following nameservers: 2001:4860:4860::8888\n    [*] No parameters were specified. Running in full proxy mode\n    [00:57:46] 2001:db8::105: proxying the response of type 'A' for thesprawl.org\n\nBy default, DNSChef uses Google's public DNS server to make proxy requests. However, you can define a custom list of nameservers using the *--nameservers* parameter:\n\n    # ./dnschef.py --nameservers 4.2.2.1,4.2.2.2 -q\n    [*] DNSChef started on interface: 127.0.0.1 \n    [*] Using the following nameservers: 4.2.2.1, 4.2.2.2\n    [*] No parameters were specified. Running in full proxy mode\n    [00:55:08] 127.0.0.1: proxying the response of type 'A' for thesprawl.org\n\nIt is possible to specify non-standard nameserver port using IP#PORT notation:\n\n    # ./dnschef.py --nameservers 192.0.2.2#5353 -q\n    [*] DNSChef started on interface: 127.0.0.1 \n    [*] Using the following nameservers: 192.0.2.2#5353\n    [*] No parameters were specified. Running in full proxy mode\n    [02:03:12] 127.0.0.1: proxying the response of type 'A' for thesprawl.org\n\nAt the same time it is possible to start DNSChef itself on an alternative port using the `-p port#` parameter:\n\n    # ./dnschef.py -p 5353 -q\n    [*] Listening on an alternative port 5353\n    [*] DNSChef started on interface: 127.0.0.1 \n    [*] Using the following nameservers: 8.8.8.8\n    [*] No parameters were specified. Running in full proxy mode\n\nDNS protocol can be used over UDP (default) or TCP. DNSChef implements a TCP mode which can be activated with the `--tcp` flag.\n"
  },
  {
    "path": "TODO",
    "content": "[*] Run in MiTM mode and inject fake DNS responses.\n"
  },
  {
    "path": "dnschef/__init__.py",
    "content": "import importlib.metadata\n\n__version__ = importlib.metadata.version(\"dnschef-ng\")\n"
  },
  {
    "path": "dnschef/__main__.py",
    "content": "#!/usr/bin/env python3\n\n#\n# DNSChef is a highly configurable DNS Proxy for Penetration Testers \n# and Malware Analysts. Please visit http://thesprawl.org/projects/dnschef/\n# for the latest version and documentation. Please forward all issues and\n# concerns to iphelix [at] thesprawl.org.\n\n# Copyright (C) 2019 Peter Kacherginsky, Marcello Salvati\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are met: \n#\n# 1. Redistributions of source code must retain the above copyright notice, this\n#    list of conditions and the following disclaimer. \n# 2. Redistributions in binary form must reproduce the above copyright notice,\n#    this list of conditions and the following disclaimer in the documentation\n#    and/or other materials provided with the distribution.\n# 3. Neither the name of the copyright holder nor the names of its contributors\n#    may be used to endorse or promote products derived from this software without \n#    specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\n# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\n# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nfrom dnschef import kitchen\nfrom dnschef.protocols import start_server\n\nfrom dnschef.logger import log, plain_formatter, debug_formatter\nfrom dnschef.utils import header, parse_config_file\n\nfrom argparse import ArgumentParser\n\nimport asyncio\nimport logging\nimport logging.handlers\nimport sys\n\ndef main():\n    # Parse command line arguments\n    parser = ArgumentParser(usage = \"dnschef.py [options]:\\n\" + header, description=\"DNSChef is a highly configurable DNS Proxy for Penetration Testers and Malware Analysts. It is capable of fine configuration of which DNS replies to modify or to simply proxy with real responses. In order to take advantage of the tool you must either manually configure or poison DNS server entry to point to DNSChef. The tool requires root privileges to run on privileged ports.\" )\n\n    fakegroup = parser.add_argument_group(\"Fake DNS records:\")\n    fakegroup.add_argument('--fakeip', metavar=\"192.0.2.1\", help='IP address to use for matching DNS queries. If you use this parameter without specifying domain names, then all \\'A\\' queries will be spoofed. Consider using --file argument if you need to define more than one IP address.')\n    fakegroup.add_argument('--fakeipv6', metavar=\"2001:db8::1\", help='IPv6 address to use for matching DNS queries. If you use this parameter without specifying domain names, then all \\'AAAA\\' queries will be spoofed. Consider using --file argument if you need to define more than one IPv6 address.')\n    fakegroup.add_argument('--fakemail', metavar=\"mail.fake.com\", help='MX name to use for matching DNS queries. If you use this parameter without specifying domain names, then all \\'MX\\' queries will be spoofed. Consider using --file argument if you need to define more than one MX record.')\n    fakegroup.add_argument('--fakealias', metavar=\"www.fake.com\", help='CNAME name to use for matching DNS queries. If you use this parameter without specifying domain names, then all \\'CNAME\\' queries will be spoofed. Consider using --file argument if you need to define more than one CNAME record.')\n    fakegroup.add_argument('--fakens', metavar=\"ns.fake.com\", help='NS name to use for matching DNS queries. If you use this parameter without specifying domain names, then all \\'NS\\' queries will be spoofed. Consider using --file argument if you need to define more than one NS record.')\n    fakegroup.add_argument('--file', help=\"Specify a file containing a list of DOMAIN=IP pairs (one pair per line) used for DNS responses. For example: google.com=1.1.1.1 will force all queries to 'google.com' to be resolved to '1.1.1.1'. IPv6 addresses will be automatically detected. You can be even more specific by combining --file with other arguments. However, data obtained from the file will take precedence over others.\")\n\n    mexclusivegroup = parser.add_mutually_exclusive_group()\n    mexclusivegroup.add_argument('--fakedomains', metavar=\"thesprawl.org,google.com\", help='A comma separated list of domain names which will be resolved to FAKE values specified in the the above parameters. All other domain names will be resolved to their true values.')\n    mexclusivegroup.add_argument('--truedomains', metavar=\"thesprawl.org,google.com\", help='A comma separated list of domain names which will be resolved to their TRUE values. All other domain names will be resolved to fake values specified in the above parameters.')\n\n    rungroup = parser.add_argument_group(\"Optional runtime parameters.\")\n    rungroup.add_argument(\"--logfile\", metavar=\"FILE\", help=\"Specify a log file to record all activity\")\n    rungroup.add_argument(\"--nameservers\", metavar=\"8.8.8.8#53 or 4.2.2.1#53#tcp or 2001:4860:4860::8888\", default='8.8.8.8', help='A comma separated list of alternative DNS servers to use with proxied requests. Nameservers can have either IP or IP#PORT format. A randomly selected server from the list will be used for proxy requests when provided with multiple servers. By default, the tool uses Google\\'s public DNS server 8.8.8.8 when running in IPv4 mode and 2001:4860:4860::8888 when running in IPv6 mode.')\n    rungroup.add_argument(\"-i\",\"--interface\", metavar=\"127.0.0.1 or ::1\", default=\"127.0.0.1\", help='Define an interface to use for the DNS listener. By default, the tool uses 127.0.0.1 for IPv4 mode and ::1 for IPv6 mode.')\n    rungroup.add_argument(\"-t\",\"--tcp\", action=\"store_true\", default=False, help=\"Use TCP DNS proxy instead of the default UDP.\")\n    rungroup.add_argument(\"-6\",\"--ipv6\", action=\"store_true\", default=False, help=\"Run in IPv6 mode.\")\n    rungroup.add_argument(\"-p\",\"--port\", metavar=53, default=53, type=int, help='Port number to listen for DNS requests.')\n    rungroup.add_argument(\"-v\", \"--verbose\", action=\"store_true\", dest=\"verbose\", default=False, help=\"Run in verbose mode\")\n\n    options = parser.parse_args()\n\n    # Print program header\n    print(header)\n\n    if options.verbose:\n        log.setLevel(logging.DEBUG)\n        log.handlers[0].setFormatter(debug_formatter)\n        log.debug(\"running in verbose mode\")\n\n    if not (options.fakeip or options.fakeipv6) and (options.fakedomains or options.truedomains):\n        log.error(\"you have forgotten to specify which IP to use for fake responses\")\n        sys.exit(0)\n\n    # Adjust defaults for IPv6\n    if options.ipv6:\n        if options.interface == \"127.0.0.1\":\n            options.interface = \"::1\"\n\n        if options.nameservers == \"8.8.8.8\":\n            options.nameservers = \"2001:4860:4860::8888\"\n\n    # Use alternative DNS servers\n    if options.nameservers:\n        nameservers = options.nameservers.split(',')\n\n    # External file definitions\n    if options.file:\n        kitchen.CONFIG = parse_config_file(options.file)\n\n    # DNS Record and Domain Name definitions\n    if options.fakeip or options.fakeipv6 or options.fakemail or options.fakealias or options.fakens:\n        fakeip     = options.fakeip\n        fakeipv6   = options.fakeipv6\n        fakemail   = options.fakemail\n        fakealias  = options.fakealias\n        fakens     = options.fakens\n\n        if options.fakedomains:\n            for domain in options.fakedomains.split(','):\n\n                # Make domain case insensitive\n                domain = domain.lower()\n                domain = domain.strip()\n\n                if fakeip:\n                    kitchen.CONFIG[\"A\"][domain] = fakeip\n                    log.info(f\"cooking A replies to point to {options.fakeip} matching: {domain}\")\n\n                if fakeipv6:\n                    kitchen.CONFIG[\"AAAA\"][domain] = fakeipv6\n                    log.info(f\"cooking AAAA replies to point to {options.fakeipv6} matching: {domain}\")\n\n                if fakemail:\n                    kitchen.CONFIG[\"MX\"][domain] = fakemail\n                    log.info(f\"cooking MX replies to point to {options.fakemail} matching: {domain}\")\n\n                if fakealias:\n                    kitchen.CONFIG[\"CNAME\"][domain] = fakealias\n                    log.info(f\"cooking CNAME replies to point to {options.fakealias} matching: {domain}\")\n\n                if fakens:\n                    kitchen.CONFIG[\"NS\"][domain] = fakens\n                    log.info(f\"cooking NS replies to point to {options.fakens} matching: {domain}\")\n\n        elif options.truedomains:\n            for domain in options.truedomains.split(','):\n\n                # Make domain case insensitive\n                domain = domain.lower()\n                domain = domain.strip()\n\n                if fakeip:\n                    kitchen.CONFIG[\"A\"][domain] = False\n                    log.info(f\"cooking A replies to point to {options.fakeip} not matching: {domain}\")\n                    kitchen.CONFIG[\"A\"]['*'] = fakeip\n\n                if fakeipv6:\n                    kitchen.CONFIG[\"AAAA\"][domain] = False\n                    log.info(f\"cooking AAAA replies to point to {options.fakeipv6} not matching: {domain}\")\n                    kitchen.CONFIG[\"AAAA\"]['*'] = fakeipv6\n\n                if fakemail:\n                    kitchen.CONFIG[\"MX\"][domain] = False\n                    log.info(f\"cooking MX replies to point to {options.fakemail} not matching: {domain}\")\n                    kitchen.CONFIG[\"MX\"]['*'] = fakemail\n\n                if fakealias:\n                    kitchen.CONFIG[\"CNAME\"][domain] = False\n                    log.info(f\"cooking CNAME replies to point to {options.fakealias} not matching: {domain}\")\n                    kitchen.CONFIG[\"CNAME\"]['*'] = fakealias\n\n                if fakens:\n                    kitchen.CONFIG[\"NS\"][domain] = False\n                    log.info(f\"cooking NS replies to point to {options.fakens} not matching: {domain}\")\n                    kitchen.CONFIG[\"NS\"]['*'] = fakealias\n\n        else:\n            if fakeip:\n                kitchen.CONFIG[\"A\"]['*'] = fakeip\n                log.info(f\"cooking all A replies to point to {fakeip}\")\n\n            if fakeipv6:\n                kitchen.CONFIG[\"AAAA\"]['*'] = fakeipv6\n                log.info(f\"cooking all AAAA replies to point to {fakeipv6}\")\n\n            if fakemail:\n                kitchen.CONFIG[\"MX\"]['*'] = fakemail\n                log.info(f\"cooking all MX replies to point to {fakemail}\")\n\n            if fakealias:\n                kitchen.CONFIG[\"CNAME\"]['*'] = fakealias\n                log.info(f\"cooking all CNAME replies to point to {fakealias}\")\n\n            if fakens:\n                kitchen.CONFIG[\"NS\"]['*'] = fakens\n                log.info(f\"cooking all NS replies to point to {fakens}\")\n\n    # Proxy all DNS requests\n    if not options.fakeip and not options.fakeipv6 and not options.fakemail and not options.fakealias and not options.fakens and not options.file:\n        log.info(\"running in full proxy mode as no parameters were specified\")\n\n    if options.logfile:\n        fh = logging.handlers.WatchedFileHandler(options.logfile)\n        fh.setFormatter(plain_formatter)\n        fh.setLevel(\n            logging.INFO\n            if not options.verbose \n            else logging.DEBUG\n        )\n        log.addHandler(fh)\n\n    # Launch DNSChef\n    asyncio.run(start_server(\n        interface=options.interface,\n        nameservers=nameservers,\n        tcp=options.tcp,\n        ipv6=options.ipv6,\n        port=options.port\n    ))\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "dnschef/api.py",
    "content": "from dnschef import __version__\nfrom dnschef import kitchen\nfrom dnschef.protocols import start_server\nfrom dnschef.utils import header, parse_config_file\nfrom dnschef.logger import (\n    log,\n    plain_formatter,\n    json_capture_formatter,\n    capturer\n)\n\nfrom enum import Enum\nfrom typing import Optional, List\n\nfrom fastapi import FastAPI\nfrom pydantic_settings import BaseSettings\nfrom pydantic import BaseModel, FilePath #,IPvAnyAddress\nfrom dnslib import RDMAP\n\nimport logging\nimport logging.handlers\nimport asyncio\n\nDnsQueryType = Enum(\"DnsQueryType\", {r:r for r in RDMAP.keys()})\n\nclass Record(BaseModel):\n    type: DnsQueryType\n    domain: str\n    value: str\n\nclass Settings(BaseSettings):\n    interface: str = \"127.0.0.1\"\n    nameservers: List[str] = [ \"8.8.8.8\" ]\n    ipv6: bool = False\n    tcp: bool = False\n    port: int = 53\n    configfile: FilePath = \"dnschef.toml\"\n\nsettings = Settings()\napp = FastAPI(\n    title='DNSChef-NG',\n    version=__version__\n)\n\n@app.on_event(\"startup\")\nasync def startup_event():\n    print(header)\n    kitchen.CONFIG = parse_config_file(settings.configfile)\n\n    # Log to file\n    fh = logging.handlers.WatchedFileHandler(\"dnschef.log\")\n    fh.setFormatter(plain_formatter)\n    log.addHandler(fh)\n\n    # This will effectively duplicate all logs and save them in capturer.entries in JSON format\n    jh = logging.StreamHandler()\n    jh.setFormatter(json_capture_formatter)\n    log.addHandler(jh)\n\n    # Launch DNSChef\n    asyncio.create_task(\n        start_server(\n            interface=settings.interface,\n            nameservers=settings.nameservers,\n            tcp=settings.tcp,\n            ipv6=settings.ipv6,\n            port=settings.port\n        )\n    )\n\n\"\"\"\n@app.on_event(\"shutdown\")\nasync def shutdown_event():\n    dns_chef_coroutine.cancel()\n    log.debug(\"Shutting down DNSChef API\")\n\"\"\"\n\n@app.put(\"/\")\nasync def add_record(record: Record):\n    kitchen.CONFIG[record.type.value][record.domain] = record.value\n    return 200\n\n\n@app.delete(\"/\")\nasync def delete_record(record: Record):\n    del kitchen.CONFIG[record.type.value][record.domain]\n    return 200\n\n\n@app.get(\"/\")\nasync def get_records():\n    return kitchen.CONFIG\n\n\n@app.get(\"/logs\")\nasync def get_logs(type: Optional[DnsQueryType] = None, name: Optional[str] = None):\n    events = ['cooking response', 'proxying response']\n\n    if not type and not name:\n        return capturer.entries\n\n    if type and name:\n        filter_expression = lambda l: l['event'] in events and l['type'] == type.value and name in l['name']\n    elif type:\n        filter_expression = lambda l: l['event'] in events and l['type'] == type.value\n    elif name:\n        filter_expression = lambda l: l['event'] in events and name in l['name']\n\n    return list(\n        filter(\n            filter_expression,\n            capturer.entries\n        )\n    )\n"
  },
  {
    "path": "dnschef/kitchen.py",
    "content": "from dnslib import *\nfrom ipaddress import IPv4Address, IPv6Address\n\nfrom dnschef.logger import log\n\nimport difflib\nimport fnmatch\nimport base64\nimport io\nimport itertools\nimport pathlib\nimport asyncio\nimport random\nimport time\n\nCONFIG = {r: {} for r in RDMAP.keys()}\n\ndef chunk_string(string_to_chunk: str, chunk_size: int):\n    data = io.StringIO(string_to_chunk)\n    while True:\n        piece = data.read(chunk_size)\n        if not piece:\n            break\n        yield piece\n\n\ndef chunk_file(file_path: pathlib.Path, chunk_size: int):\n    with file_path.open('rb') as f:\n        while True:\n            piece = f.read(chunk_size)\n            if not piece:\n                break\n            yield piece\n\n\ndef get_file_chunk(file_path, chunk_index, chunk_size):\n    return next(itertools.islice(\n        chunk_file(file_path, chunk_size),\n        chunk_index,\n        chunk_index + 1\n    ), b'')\n\n\nasync def stage_file(qname, record, chunk_size: int):\n    loop = asyncio.get_event_loop()\n\n    file_to_stage = pathlib.Path(record['file'])\n    if file_to_stage.exists() and file_to_stage.is_file():\n        chunk_index = int(''.join([ c for c in qname.split('.')[0] if c.isdigit() ]))\n\n        file_chunk = await loop.run_in_executor(None, get_file_chunk, file_to_stage, chunk_index, chunk_size)\n        return file_chunk\n\n\nclass DNSKitchen:\n\n    async def do_default(self, addr, qname, qtype, record):\n        if record[-1] == \".\": record = record[:-1]\n        return RR(qname, getattr(QTYPE, qtype), rdata=RDMAP[qtype](record))\n\n    async def do_A(self, addr, qname, qtype, record):\n        if isinstance(record, dict):\n            chunk_size = record.get('chunk_size', 4)\n            if chunk_size > 4:\n                log.warning(f\"chunk_size {chunk_size} is too large for A record, defaulting to 4\")\n                chunk_size = 4\n\n            record = await stage_file(qname, record, chunk_size)\n            if record and len(record) < 4: \n                record = record.ljust(4, b'\\x00')\n\n        if record:\n            ipv4_hex_tuple = list(map(int, IPv4Address(record).packed))\n            return RR(qname, getattr(QTYPE, qtype), rdata=RDMAP[qtype](ipv4_hex_tuple))\n\n    async def do_TXT(self, addr, qname, qtype, record):\n        if isinstance(record, dict):\n            chunk_size = record.get('chunk_size')\n            prefix = random.choice(record.get('response_prefix_pool', ['']))\n            response_format = record.get('response_format', '{prefix}{chunk}')\n\n            space_left = 255 - len(response_format.format(prefix=prefix, chunk=''))\n            max_data_len = ( space_left // 4 ) * 3\n\n            if chunk_size:\n                max_data_len = min(chunk_size, max_data_len)\n                if chunk_size > max_data_len:\n                    log.warning(f\"chunk_size {chunk_size} is too large for the TXT record, defaulting to {max_data_len}\")\n\n            record = await stage_file(qname, record, chunk_size=max_data_len)\n            if record:\n                record = response_format.format(prefix=prefix, chunk=base64.b64encode(record).decode())\n\n        if record:\n            # dnslib doesn't like trailing dots\n            record = record.rstrip('.')\n            return RR(qname, getattr(QTYPE, qtype), rdata=RDMAP[qtype](record))\n\n    async def do_AAAA(self, addr, qname, qtype, record):\n        if isinstance(record, dict):\n            chunk_size = record.get('chunk_size', 16)\n            if chunk_size > 16:\n                log.warning(f\"chunk_size {chunk_size} is too large for AAAA record, defaulting to 16\")\n                chunk_size = 16\n\n            record = await stage_file(qname, record, chunk_size)\n            if record and len(record) < 16:\n                record = record.ljust(16, b'\\x00')\n\n        if record:\n            ipv6_hex_tuple = list(map(int, IPv6Address(record).packed))\n            return RR(qname, getattr(QTYPE, qtype), rdata=RDMAP[qtype](ipv6_hex_tuple))\n\n    async def do_HTTPS(self, addr, qname, qtype, record):\n        kv_pairs = record.split(\" \")\n        mydata = RDMAP[qtype].fromZone(kv_pairs)\n        return RR(qname, getattr(QTYPE, qtype), rdata=mydata)\n\n    async def do_SOA(self, addr, qname, qtype, record):\n        mname, rname, t1, t2, t3, t4, t5 = record.split(\" \")\n        times = tuple([int(t) for t in [t1, t2, t3, t4, t5]])\n\n        # dnslib doesn't like trailing dots\n        if mname[-1] == \".\": mname = mname[:-1]\n        if rname[-1] == \".\": rname = rname[:-1]\n\n        return RR(qname, getattr(QTYPE, qtype), rdata=RDMAP[qtype](mname, rname, times))\n\n    async def do_NAPTR(self, addr, qname, qtype, record):\n        order, preference, flags, service, regexp, replacement = list(map(lambda x: x.encode(), record.split(\" \")))\n        order = int(order)\n        preference = int(preference)\n\n        # dnslib doesn't like trailing dots\n        if replacement[-1] == \".\": replacement = replacement[:-1]\n\n        return RR(qname, getattr(QTYPE, qtype), rdata=RDMAP[qtype](order, preference, flags, service, regexp, DNSLabel(replacement)))\n\n    async def do_SRV(self, addr, qname, qtype, record):\n        priority, weight, port, target = record.split(\" \")\n        priority = int(priority)\n        weight = int(weight)\n        port = int(port)\n        if target[-1] == \".\": target = target[:-1]\n\n        return RR(qname, getattr(QTYPE, qtype), rdata=RDMAP[qtype](priority, weight, port, target))\n\n    async def do_DNSKEY(self, addr, qname, qtype, record):\n        flags, protocol, algorithm, key = record.split(\" \")\n        flags = int(flags)\n        protocol = int(protocol)\n        algorithm = int(algorithm)\n        key = base64.b64decode((\"\".join(key)).encode('ascii'))\n\n        return RR(qname, getattr(QTYPE, qtype), rdata=RDMAP[qtype](flags, protocol, algorithm, key))\n\n    async def do_RRSIG(self, addr, qname, qtype, record):\n        covered, algorithm, labels, orig_ttl, sig_exp, sig_inc, key_tag, name, sig = record.split(\" \")\n\n        covered = getattr(QTYPE, covered)  # NOTE: Covered QTYPE\n        algorithm = int(algorithm)\n        labels = int(labels)\n        orig_ttl = int(orig_ttl)\n        sig_exp = int(time.mktime(time.strptime(sig_exp + 'GMT', \"%Y%m%d%H%M%S%Z\")))\n        sig_inc = int(time.mktime(time.strptime(sig_inc + 'GMT', \"%Y%m%d%H%M%S%Z\")))\n        key_tag = int(key_tag)\n        if name[-1] == '.': name = name[:-1]\n        sig = base64.b64decode( (\"\".join(sig)).encode('ascii') )\n\n        return RR(qname, getattr(QTYPE, qtype), rdata=RDMAP[qtype](covered, algorithm, labels, orig_ttl, sig_exp, sig_inc, key_tag, name, sig))\n\n    # Find appropriate ip address to use for a queried name.\n    def findnametodns(self, qname, qtype):\n        # Make qname case insensitive\n        qname = qname.lower()\n\n        matched_domains = [\n            k for k,_ in CONFIG[qtype].items() \n            if (k == '*' or qname.count('.') == k.count('.')) and fnmatch.fnmatch(qname, k)\n        ]\n\n        if matched_domains:\n            top_matched_domains = list(sorted(\n                matched_domains,\n                key=lambda domain: difflib.SequenceMatcher(a=domain, b=qname).quick_ratio(),\n                reverse=True\n            ))\n\n            #return { qtype: { k:v for k,v in CONFIG[qtype].items() if k == top_matched_domains[0] } }\n            return CONFIG[qtype][top_matched_domains[0]]\n\n    async def we_cookin(self, logger, d, qtype, qname, addr):\n        # Create a custom response to the query\n        response = DNSRecord(\n            DNSHeader(id=d.header.id, bitmap=d.header.bitmap, qr=1, aa=1, ra=1), \n            q=d.q\n        )\n\n        cooked_reply = self.findnametodns(qname, qtype)\n\n        # Check if there is a fake record for the current request qtype\n        if CONFIG.get(qtype) and cooked_reply:\n            logger.info(\"cooking response\")\n\n            response_func = getattr(\n                self,\n                f\"do_{qtype}\",\n                self.do_default\n            )\n\n            answer = await response_func(addr, qname, qtype, cooked_reply)\n            if answer:\n                response.add_answer(answer)\n\n            return response\n"
  },
  {
    "path": "dnschef/logger.py",
    "content": "from rich.traceback import install\n\nimport logging\nimport logging.handlers\nimport structlog\n\ninstall(show_locals=True)\n\ncapturer = structlog.testing.LogCapture()\ntimestamper = structlog.processors.TimeStamper(fmt=\"%Y-%m-%d %H:%M:%S\")\n\nplain_formatter = structlog.stdlib.ProcessorFormatter(\n    processors = [\n        structlog.stdlib.ProcessorFormatter.remove_processors_meta,\n        #structlog.stdlib.add_log_level,\n        structlog.dev.ConsoleRenderer(colors=False),\n    ]\n)\n\ncolored_formatter = structlog.stdlib.ProcessorFormatter(\n    processors = [\n        structlog.stdlib.ProcessorFormatter.remove_processors_meta,\n        #structlog.stdlib.add_log_level,\n        structlog.dev.ConsoleRenderer(colors=True, exception_formatter=structlog.dev.rich_traceback),\n    ]\n)\n\ndebug_formatter = structlog.stdlib.ProcessorFormatter(\n    processors = [\n        structlog.stdlib.ProcessorFormatter.remove_processors_meta,\n        structlog.stdlib.add_log_level,\n        #structlog.stdlib.add_logger_name,\n        structlog.dev.ConsoleRenderer(colors=True, exception_formatter=structlog.dev.rich_traceback),\n    ]\n)\n\njson_capture_formatter = structlog.stdlib.ProcessorFormatter(\n    processors = [\n        structlog.stdlib.ProcessorFormatter.remove_processors_meta,\n        capturer,\n        structlog.processors.JSONRenderer(),\n    ]\n)\n\nsh = logging.StreamHandler()\nsh.setFormatter(colored_formatter)\n\ndnschef_logger = logging.getLogger(\"dnschef\")\ndnschef_logger.setLevel(logging.INFO)\ndnschef_logger.addHandler(sh)\n\nstructlog.configure(\n    processors=[\n        #structlog.stdlib.filter_by_level,\n        #structlog.stdlib.add_logger_name,\n        #structlog.stdlib.add_log_level,\n        structlog.stdlib.PositionalArgumentsFormatter(),\n        timestamper,\n        structlog.processors.StackInfoRenderer(),\n        structlog.processors.format_exc_info,\n        #structlog.processors.UnicodeDecoder(),\n        structlog.stdlib.ProcessorFormatter.wrap_for_formatter\n    ],\n    logger_factory=structlog.stdlib.LoggerFactory(),\n    wrapper_class=structlog.stdlib.BoundLogger,\n    cache_logger_on_first_use=True,\n)\n\nlog = structlog.get_logger(\"dnschef\")\n"
  },
  {
    "path": "dnschef/protocols.py",
    "content": "import asyncio\nimport socket\nimport re\nimport random\nimport functools\nimport enum\nfrom dnslib import DNSRecord, QR, QTYPE\nfrom typing import List\nfrom dnschef.logger import log\n\nfrom dnschef import kitchen\n\nclass ClientProtocol(enum.Enum):\n    UDP = 1\n    TCP = 2\n\nclass UdpDnsClientProtocol:\n    def __init__(self, request, on_con_lost):\n        self.transport = None\n        self.request = request\n        self.on_con_lost = on_con_lost\n\n    def connection_made(self, transport):\n        self.transport = transport\n        log.debug('sending', request=self.request)\n        self.transport.sendto(self.request)\n\n    def datagram_received(self, data, addr):\n        log.debug(\"received\", addr=addr, data=data)\n        self.reply = data\n        self.transport.close()\n\n    def error_received(self, exc):\n        log.exception('error received')\n\n    def connection_lost(self, exc):\n        log.debug(\"connection closed\")\n        self.on_con_lost.set_result(True)\n\nclass TcpDnsClientProtocol(asyncio.Protocol):\n    def __init__(self, request, on_con_lost):\n        self.request = request\n        self.on_con_lost = on_con_lost\n\n    def connection_made(self, transport):\n        self.transport = transport\n        log.debug('sending', request=self.request)\n        self.transport.write(self.request)\n\n    def data_received(self, data):\n        addr = self.transport.get_extra_info('peername')\n        log.debug(\"received\", addr=addr, data=data)\n        self.reply = data\n        self.transport.close()\n\n    def connection_lost(self, exc):\n        log.debug(\"connection closed\")\n        # The socket has been closed\n        self.on_con_lost.set_result(True)\n\n# Obtain a response from a real DNS server.\nasync def proxy_request(request, host, protocol: ClientProtocol, port: int = 53):\n    loop = asyncio.get_running_loop()\n    on_con_lost = loop.create_future()\n\n    if protocol == ClientProtocol.UDP:\n        transport, protocol = await loop.create_datagram_endpoint(\n            lambda: UdpDnsClientProtocol(request, on_con_lost),\n            remote_addr=(host, int(port)))\n    else:\n        transport, protocol = await loop.create_connection(\n            lambda: TcpDnsClientProtocol(request, on_con_lost),\n            host=host,\n            port=int(port)\n        )\n\n    try:\n        await on_con_lost\n    finally:\n        transport.close()\n        return protocol.reply\n\nclass UdpDnsServerProtocol:\n    def __init__(self, nameservers, dns_kitchen):\n        self.nameservers = [ re.split('[:#]', ns) for ns in nameservers ]\n        self.dns_kitchen = dns_kitchen\n\n    def connection_made(self, transport):\n        self.transport = transport\n\n    def datagram_received(self, data, addr):\n        logger = log.bind(address=addr[0], proto=\"udp\")\n\n        try:\n            d = DNSRecord.parse(data)\n        except Exception:\n            logger.error(\"invalid DNS request\")\n        else:\n            # Only Process DNS Queries\n            if not QR[d.header.qr] == \"QUERY\":\n                logger.warning(\"received a non-query DNS request\")\n                return\n\n            qtype = QTYPE[d.q.qtype]\n            qname = str(d.q.qname).rstrip('.')\n            logger = logger.bind(name=qname, type=qtype)\n\n            def _cooked_cb(future):\n                response = future.result()\n                if response:\n                    logger.debug(\"dns packet\", packet=response.pack())\n                    self.transport.sendto(response.pack(), addr)\n                else:\n                    logger.info(\"proxying response\")\n                    task = asyncio.create_task(\n                        proxy_request(\n                            data, \n                            *random.choice(self.nameservers),\n                            protocol=ClientProtocol.UDP\n                        )\n                    )\n                    task.add_done_callback(functools.partial(\n                        lambda c, t, a: t.sendto(c.result(), a), t=self.transport, a=addr\n                    ))\n\n            task = asyncio.create_task(self.dns_kitchen.we_cookin(logger, d, qtype, qname, addr))\n            task.add_done_callback(_cooked_cb)\n\nclass TcpDnsServerProtocol(asyncio.Protocol):\n    def __init__(self, nameservers, dns_kitchen):\n        self.nameservers = [ re.split('[:#]', ns) for ns in nameservers ]\n        self.dns_kitchen = dns_kitchen\n\n    def connection_made(self, transport):\n        self.transport = transport\n\n    def data_received(self, data):\n        addr = self.transport.get_extra_info('peername')\n        logger = log.bind(address=addr[0], proto=\"tcp\")\n\n        try:\n            d = DNSRecord.parse(data[2:])\n        except Exception:\n            logger.error(\"invalid DNS request\")\n        else:\n            # Only Process DNS Queries\n            if not QR[d.header.qr] == \"QUERY\":\n                logger.warning(\"received a non-query DNS request\")\n                return\n\n            qtype = QTYPE[d.q.qtype]\n            qname = str(d.q.qname).rstrip('.')\n            logger = logger.bind(name=qname, type=qtype)\n\n            def _cooked_cb(future):\n                response = future.result()\n                if response:\n                    logger.debug(\"dns packet\", packet=response.pack())\n                    self.transport.write(\n                        len(response.pack()).to_bytes(2, byteorder='big') + response.pack()\n                    )\n                else:\n                    logger.info(\"proxying response\")\n                    task = asyncio.create_task(\n                        proxy_request(\n                            data,\n                            *random.choice(self.nameservers),\n                            protocol=ClientProtocol.TCP\n                        )\n                    )\n                    task.add_done_callback(functools.partial(\n                        lambda c, t: t.write(c.result()), t=self.transport\n                    ))\n\n            task = asyncio.create_task(self.dns_kitchen.we_cookin(logger, d, qtype, qname, addr))\n            task.add_done_callback(_cooked_cb)\n\nasync def start_server(interface: str, nameservers: List[str], tcp: bool = False, ipv6: bool = False, port: int = 53):\n    loop = asyncio.get_running_loop()\n\n    family= socket.AF_INET if not ipv6 else socket.AF_INET6\n    if tcp:\n        server = await loop.create_server(\n            lambda: TcpDnsServerProtocol(nameservers, kitchen.DNSKitchen()),\n            host=interface, \n            port=int(port),\n            family=family\n        )\n\n    transport, protocol = await loop.create_datagram_endpoint(\n        lambda: UdpDnsServerProtocol(nameservers, kitchen.DNSKitchen()),\n        local_addr=(interface, int(port)),\n        family=family\n    )\n\n    log.info(\"DNSChef is active\", interface=interface, tcp=tcp, ipv6=ipv6, port=port, nameservers=nameservers)\n\n    while True:\n        await asyncio.sleep(1)\n"
  },
  {
    "path": "dnschef/utils.py",
    "content": "import tomllib\nfrom dnschef import __version__\nfrom dnschef.logger import log\nfrom dnslib import RDMAP\n\nheader  = \"          _                _          __  \\n\"\nheader += \"         | |  v{}      | |        / _| \\n\".format(__version__)\nheader += \"       __| |_ __  ___  ___| |__   ___| |_ \\n\"\nheader += \"      / _` | '_ \\/ __|/ __| '_ \\ / _ \\  _|\\n\"\nheader += \"     | (_| | | | \\__ \\ (__| | | |  __/ |  \\n\"\nheader += \"      \\__,_|_| |_|___/\\___|_| |_|\\___|_|  \\n\"\nheader += \"                @iphelix // @byt3bl33d3r  \\n\"\n\n\ndef parse_config_file(config_file: str = \"dnschef.toml\"):\n    log.debug(\"Parsing config file\", path=config_file)\n    with open(config_file, 'rb') as f:\n        config = tomllib.load(f)\n\n    for record, domains in config.items():\n        if record not in RDMAP:\n            log.warning(f\"DNS record '{record}' is not supported. Contents will be ignored.\")\n            continue\n\n        for domain, values in domains.items():\n            if isinstance(values, dict):\n                log.info(\"cooking file staging\", section=record, domain=domain.lower(), file=values['file'])\n            else:\n                log.info(\"cooking replies\", section=record, domain=domain.lower(), reply=values)\n\n    return config\n"
  },
  {
    "path": "dnschef.toml",
    "content": "[A]  # Queries for IPv4 address records\n\"*.thesprawl.org\" = \"100.100.100.100\"\n\"*.test.thesprawl.org\"  = \"127.0.0.1\"\n\"*.*.thesprawl.org\" = \"1.1.1.1\"\n\"c.*.*.thesprawl.org\" = \"1.1.2.2\"\n\"fuck.shit.com\" = \"192.168.0.1\"\n\"*.wat.org\" = { file = \"./requirements.txt\", chunk_size = 122 }\n\n[AAAA]  # Queries for IPv6 address records\n\"*.thesprawl.org\" = \"2001:db8::1\"\n\"*.wat.org\" = { file = \"./requirements.txt\", chunk_size = 122 }\n\n[MX]    # Queries for mail server records\n\"*.thesprawl.org\" = \"mail.fake.com\"\n\n[NS]    # Queries for mail server records\n\"*.thesprawl.org\" = \"ns.fake.com\"\n\n[CNAME] # Queries for alias records\n\"*.thesprawl.org\" = \"www.fake.com\"\n\n[TXT]   # Queries for text records\n\"*.thesprawl.org\" = \"fake message\"\n\"ok.thesprawl.org\" = \"fake message\"\n\"*.something.wattahog.org\" = \"fuck off\"\n\"ns*.shit.fuck.org\" = { file = \"./requirements.txt\", chunk_size = 189, response_format = \"{prefix}test-{chunk}\", response_prefix_pool = [\"atlassian-domain-verification=\", \"onetrust-domain-verification=\", \"docusign=\" ] }\n\n[TXT.\"*.wattahog.org\"]\nfile = \"./requirements.txt\"\nchunk_size = 189\nresponse_format = \"{prefix}test-{chunk}\"\nresponse_prefix_pool = [ \"atlassian-domain-verification=\", \"onetrust-domain-verification=\" , \"docusign=\" ]\n\n[PTR]\n\"*.2.0.192.in-addr.arpa\" = \"fake.com\"\n\n[SOA]\n# FORMAT: mname rname t1 t2 t3 t4 t5\n\"*.thesprawl.org\" = \"ns.fake.com. hostmaster.fake.com. 1 10800 3600 604800 3600\"\n\n[NAPTR]\n# FORMAT: order preference flags service regexp replacement\n\"*.thesprawl.org\" = \"100 10 U E2U+sip !^.*$!sip:customer-service@fake.com! .\"\n\n[SRV]\n# FORMAT: priority weight port target\n\"*.*.thesprawl.org\" = \"0 5 5060 sipserver.fake.com\"\n\n[DNSKEY]\n# FORMAT: flags protocol algorithm base64(key)\n\"*.thesprawl.org\" = \"256 3 5 AQPSKmynfzW4kyBv015MUG2DeIQ3Cbl+BBZH4b/0PY1kxkmvHjcZc8nokfzj31GajIQKY+5CptLr3buXA10hWqTkF7H6RfoRqXQeogmMHfpftf6zMv1LyBUgia7za6ZEzOJBOztyvhjL742iU/TpPSEDhm2SNKLijfUppn1UaNvv4w==\"\n\n[RRSIG]\n# FORMAT: covered algorithm labels labels orig_ttl sig_exp sig_inc key_tag name base64(sig)\n\"*.thesprawl.org\" = \"A 5 3 86400 20030322173103 20030220173103 2642 thesprawl.org. oJB1W6WNGv+ldvQ3WDG0MQkg5IEhjRip8WTrPYGv07h108dUKGMeDPKijVCHX3DDKdfb+v6oB9wfuh3DTJXUAfI/M0zmO/zz8bW0Rznl8O3tGNazPwQKkRN20XPXV6nwwfoXmJQbsLNrLfkGJ5D6fwFm8nN+6pBzeDQfsS3Ap3o=\"\n\n[HTTPS]\n# FORMAT: priority target key=value pairs\n\"*.thesprawl.org\" = \"1 . alpn=h2 ipv4hint=127.0.0.1 ipv6hint=::1\""
  },
  {
    "path": "docker-compose.yml",
    "content": "version: \"3\"\nservices:\n  dnschef:\n    image: dnschef:latest\n    container_name: dnschef\n    ports:\n      - \"53:53/udp\"\n      - \"53:53/tcp\"\n    expose:\n      - \"80\"\n    volumes:\n      - ./dnschef.toml:/etc/dnschef.toml\n    environment:\n      - INTERFACE=0.0.0.0\n      - NAMESERVERS=8.8.8.8\n      - PORT=53\n      - TCP=false\n      - IPV6=false\n      - CONFIGFILE=/etc/dnschef.toml"
  },
  {
    "path": "pyproject.toml",
    "content": "[tool.poetry]\nname = \"dnschef-ng\"\nversion = \"0.7.2\"\ndescription = \"A highly configurable DNS proxy for Penetration Testers and Malware Analysts\"\nauthors = [\"iphelix <iphelix@thesprawl.org>\",\"byt3bl33d3r <byt3bl33d3r@pm.me>\"]\nreadme = \"README.md\"\nlicense = \"BSD-3-Clause\"\npackages = [{include = \"dnschef\"}]\nclassifiers = [\n    \"Environment :: Console\",\n    \"Programming Language :: Python :: 3\",\n    \"Topic :: Security\",\n]\nexclude = [\"tests\"]\n\n[tool.poetry.scripts]\ndnschef = 'dnschef.__main__:main'\ndnschef-ng = 'dnschef.__main__:main'\n\n[tool.poetry.dependencies]\npython = \"^3.11\"\ndnslib = \"^0.9.23\"\nrich = \"^13.5.3\"\nstructlog = \"^23.1.0\"\nfastapi = { version = \"^0.103.1\", optional = true }\nuvicorn = { version = \"^0.23.2\", optional = true }\npydantic-settings = { version = \"^2.0.3\", optional = true }\n\n[tool.poetry.extras]\napi = [\"fastapi\", \"uvicorn\", \"pydantic-settings\"]\n\n[tool.poetry.group.dev.dependencies]\npytest = \"^7.4.2\"\npytest-asyncio = \"^0.21.1\"\npoetry-plugin-export = \"^1.6.0\"\nruff = \"^0.1.6\"\ndnspython = \"^2.4.2\"\npytest-cov = \"^4.1.0\"\nhttpx = \"^0.25.1\"\n\n[tool.pytest.ini_options]\naddopts = \"--cov=dnschef\"\nlog_cli = false\nlog_cli_level = \"INFO\"\nlog_cli_format = \"%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)\"\nlog_cli_date_format = \"%Y-%m-%d %H:%M:%S\"\nfilterwarnings = [\n    # note the use of single quote below to denote \"raw\" strings in TOML\n    'ignore:`general_plain_validator_function` is deprecated',\n]\n\n[build-system]\nrequires = [\"poetry-core\"]\nbuild-backend = \"poetry.core.masonry.api\""
  },
  {
    "path": "requirements-api.txt",
    "content": "annotated-types==0.6.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43 \\\n    --hash=sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d\nanyio==3.7.1 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780 \\\n    --hash=sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5\nclick==8.1.7 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \\\n    --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de\ncolorama==0.4.6 ; python_version >= \"3.11\" and python_version < \"4.0\" and platform_system == \"Windows\" \\\n    --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \\\n    --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6\ndnslib==0.9.23 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:310196d3e38ce2051b61eebbd2f1d08fcc934fa3360f22031864d16efe8bca77 \\\n    --hash=sha256:46137e8ef6ef52b24a16d47e0786a99dd103ab1e71eea616f21371accbccc557 \\\n    --hash=sha256:9eb851ac721eea51834d43795478ac9b48272c61ba97cc4a160668b50aff39ec\nfastapi==0.103.2 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:3270de872f0fe9ec809d4bd3d4d890c6d5cc7b9611d721d6438f9dacc8c4ef2e \\\n    --hash=sha256:75a11f6bfb8fc4d2bec0bd710c2d5f2829659c0e8c0afd5560fdda6ce25ec653\nh11==0.14.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \\\n    --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761\nidna==3.4 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \\\n    --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2\nmarkdown-it-py==3.0.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \\\n    --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb\nmdurl==0.1.2 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \\\n    --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba\npydantic-core==2.14.5 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b \\\n    --hash=sha256:074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b \\\n    --hash=sha256:079206491c435b60778cf2b0ee5fd645e61ffd6e70c47806c9ed51fc75af078d \\\n    --hash=sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8 \\\n    --hash=sha256:0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124 \\\n    --hash=sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189 \\\n    --hash=sha256:16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c \\\n    --hash=sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d \\\n    --hash=sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f \\\n    --hash=sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520 \\\n    --hash=sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4 \\\n    --hash=sha256:3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6 \\\n    --hash=sha256:3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955 \\\n    --hash=sha256:34708cc82c330e303f4ce87758828ef6e457681b58ce0e921b6e97937dd1e2a3 \\\n    --hash=sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b \\\n    --hash=sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a \\\n    --hash=sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68 \\\n    --hash=sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3 \\\n    --hash=sha256:45e95333b8418ded64745f14574aa9bfc212cb4fbeed7a687b0c6e53b5e188cd \\\n    --hash=sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de \\\n    --hash=sha256:49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b \\\n    --hash=sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634 \\\n    --hash=sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7 \\\n    --hash=sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459 \\\n    --hash=sha256:4e47a76848f92529879ecfc417ff88a2806438f57be4a6a8bf2961e8f9ca9ec7 \\\n    --hash=sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3 \\\n    --hash=sha256:531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331 \\\n    --hash=sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf \\\n    --hash=sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d \\\n    --hash=sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36 \\\n    --hash=sha256:5c7d5b5005f177764e96bd584d7bf28d6e26e96f2a541fdddb934c486e36fd59 \\\n    --hash=sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937 \\\n    --hash=sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc \\\n    --hash=sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093 \\\n    --hash=sha256:652c1988019752138b974c28f43751528116bcceadad85f33a258869e641d753 \\\n    --hash=sha256:6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706 \\\n    --hash=sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca \\\n    --hash=sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260 \\\n    --hash=sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997 \\\n    --hash=sha256:6c327e9cd849b564b234da821236e6bcbe4f359a42ee05050dc79d8ed2a91588 \\\n    --hash=sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71 \\\n    --hash=sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb \\\n    --hash=sha256:6e4d090e73e0725b2904fdbdd8d73b8802ddd691ef9254577b708d413bf3006e \\\n    --hash=sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69 \\\n    --hash=sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5 \\\n    --hash=sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07 \\\n    --hash=sha256:77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1 \\\n    --hash=sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0 \\\n    --hash=sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd \\\n    --hash=sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8 \\\n    --hash=sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944 \\\n    --hash=sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26 \\\n    --hash=sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda \\\n    --hash=sha256:88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4 \\\n    --hash=sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9 \\\n    --hash=sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00 \\\n    --hash=sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe \\\n    --hash=sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6 \\\n    --hash=sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada \\\n    --hash=sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4 \\\n    --hash=sha256:a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7 \\\n    --hash=sha256:a717aef6971208f0851a2420b075338e33083111d92041157bbe0e2713b37325 \\\n    --hash=sha256:a71891847f0a73b1b9eb86d089baee301477abef45f7eaf303495cd1473613e4 \\\n    --hash=sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b \\\n    --hash=sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88 \\\n    --hash=sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04 \\\n    --hash=sha256:abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863 \\\n    --hash=sha256:af36f36538418f3806048f3b242a1777e2540ff9efaa667c27da63d2749dbce0 \\\n    --hash=sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911 \\\n    --hash=sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b \\\n    --hash=sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e \\\n    --hash=sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144 \\\n    --hash=sha256:ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5 \\\n    --hash=sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720 \\\n    --hash=sha256:c0b97ec434041827935044bbbe52b03d6018c2897349670ff8fe11ed24d1d4ab \\\n    --hash=sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d \\\n    --hash=sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789 \\\n    --hash=sha256:c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec \\\n    --hash=sha256:c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2 \\\n    --hash=sha256:c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db \\\n    --hash=sha256:c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f \\\n    --hash=sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef \\\n    --hash=sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3 \\\n    --hash=sha256:ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209 \\\n    --hash=sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc \\\n    --hash=sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651 \\\n    --hash=sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8 \\\n    --hash=sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e \\\n    --hash=sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66 \\\n    --hash=sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7 \\\n    --hash=sha256:d81e6987b27bc7d101c8597e1cd2bcaa2fee5e8e0f356735c7ed34368c471550 \\\n    --hash=sha256:dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd \\\n    --hash=sha256:de790a3b5aa2124b8b78ae5faa033937a72da8efe74b9231698b5a1dd9be3405 \\\n    --hash=sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27 \\\n    --hash=sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093 \\\n    --hash=sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077 \\\n    --hash=sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113 \\\n    --hash=sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3 \\\n    --hash=sha256:ec1e72d6412f7126eb7b2e3bfca42b15e6e389e1bc88ea0069d0cc1742f477c6 \\\n    --hash=sha256:ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf \\\n    --hash=sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed \\\n    --hash=sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88 \\\n    --hash=sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe \\\n    --hash=sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18 \\\n    --hash=sha256:fe0a5a1025eb797752136ac8b4fa21aa891e3d74fd340f864ff982d649691867\npydantic-settings==2.1.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:26b1492e0a24755626ac5e6d715e9077ab7ad4fb5f19a8b7ed7011d52f36141c \\\n    --hash=sha256:7621c0cb5d90d1140d2f0ef557bdf03573aac7035948109adf2574770b77605a\npydantic==2.5.2 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0 \\\n    --hash=sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd\npygments==2.17.2 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c \\\n    --hash=sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367\npython-dotenv==1.0.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba \\\n    --hash=sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a\nrich==13.7.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa \\\n    --hash=sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235\nsniffio==1.3.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101 \\\n    --hash=sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384\nstarlette==0.27.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75 \\\n    --hash=sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91\nstructlog==23.2.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:16a167e87b9fa7fae9a972d5d12805ef90e04857a93eba479d4be3801a6a1482 \\\n    --hash=sha256:334666b94707f89dbc4c81a22a8ccd34449f0201d5b1ee097a030b577fa8c858\ntyping-extensions==4.8.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \\\n    --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef\nuvicorn==0.23.2 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53 \\\n    --hash=sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a\n"
  },
  {
    "path": "requirements-dev.txt",
    "content": "anyio==3.7.1 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780 \\\n    --hash=sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5\nbuild==1.0.3 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:538aab1b64f9828977f84bc63ae570b060a8ed1be419e7870b8b4fc5e6ea553b \\\n    --hash=sha256:589bf99a67df7c9cf07ec0ac0e5e2ea5d4b37ac63301c4986d1acb126aa83f8f\ncachecontrol[filecache]==0.13.1 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:95dedbec849f46dda3137866dc28b9d133fc9af55f5b805ab1291833e4457aa4 \\\n    --hash=sha256:f012366b79d2243a6118309ce73151bf52a38d4a5dac8ea57f09bd29087e506b\ncertifi==2023.11.17 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1 \\\n    --hash=sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474\ncffi==1.16.0 ; python_version >= \"3.11\" and python_version < \"4.0\" and (sys_platform == \"darwin\" or sys_platform == \"linux\") \\\n    --hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \\\n    --hash=sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a \\\n    --hash=sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417 \\\n    --hash=sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab \\\n    --hash=sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520 \\\n    --hash=sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36 \\\n    --hash=sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743 \\\n    --hash=sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8 \\\n    --hash=sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed \\\n    --hash=sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684 \\\n    --hash=sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56 \\\n    --hash=sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324 \\\n    --hash=sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d \\\n    --hash=sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235 \\\n    --hash=sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e \\\n    --hash=sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088 \\\n    --hash=sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000 \\\n    --hash=sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7 \\\n    --hash=sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e \\\n    --hash=sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673 \\\n    --hash=sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c \\\n    --hash=sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe \\\n    --hash=sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2 \\\n    --hash=sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 \\\n    --hash=sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8 \\\n    --hash=sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a \\\n    --hash=sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0 \\\n    --hash=sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b \\\n    --hash=sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896 \\\n    --hash=sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e \\\n    --hash=sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9 \\\n    --hash=sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2 \\\n    --hash=sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b \\\n    --hash=sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6 \\\n    --hash=sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404 \\\n    --hash=sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f \\\n    --hash=sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0 \\\n    --hash=sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4 \\\n    --hash=sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc \\\n    --hash=sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936 \\\n    --hash=sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba \\\n    --hash=sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872 \\\n    --hash=sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb \\\n    --hash=sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614 \\\n    --hash=sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1 \\\n    --hash=sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d \\\n    --hash=sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969 \\\n    --hash=sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b \\\n    --hash=sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4 \\\n    --hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \\\n    --hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \\\n    --hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357\ncharset-normalizer==3.3.2 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \\\n    --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \\\n    --hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \\\n    --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \\\n    --hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \\\n    --hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \\\n    --hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \\\n    --hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \\\n    --hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \\\n    --hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \\\n    --hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \\\n    --hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \\\n    --hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \\\n    --hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \\\n    --hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \\\n    --hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \\\n    --hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \\\n    --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \\\n    --hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \\\n    --hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \\\n    --hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \\\n    --hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \\\n    --hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \\\n    --hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \\\n    --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \\\n    --hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \\\n    --hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \\\n    --hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \\\n    --hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \\\n    --hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \\\n    --hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \\\n    --hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \\\n    --hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \\\n    --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \\\n    --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \\\n    --hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \\\n    --hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \\\n    --hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \\\n    --hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \\\n    --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \\\n    --hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \\\n    --hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \\\n    --hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \\\n    --hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \\\n    --hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \\\n    --hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \\\n    --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \\\n    --hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \\\n    --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \\\n    --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \\\n    --hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \\\n    --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \\\n    --hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \\\n    --hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \\\n    --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \\\n    --hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \\\n    --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \\\n    --hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \\\n    --hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \\\n    --hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \\\n    --hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \\\n    --hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \\\n    --hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \\\n    --hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \\\n    --hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \\\n    --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \\\n    --hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \\\n    --hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \\\n    --hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \\\n    --hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \\\n    --hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \\\n    --hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \\\n    --hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \\\n    --hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \\\n    --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \\\n    --hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \\\n    --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \\\n    --hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \\\n    --hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \\\n    --hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \\\n    --hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \\\n    --hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \\\n    --hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \\\n    --hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \\\n    --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \\\n    --hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \\\n    --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \\\n    --hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \\\n    --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \\\n    --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561\ncleo==2.1.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:0b2c880b5d13660a7ea651001fb4acb527696c01f15c9ee650f377aa543fd523 \\\n    --hash=sha256:4a31bd4dd45695a64ee3c4758f583f134267c2bc518d8ae9a29cf237d009b07e\ncolorama==0.4.6 ; python_version >= \"3.11\" and python_version < \"4.0\" and (sys_platform == \"win32\" or os_name == \"nt\") \\\n    --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \\\n    --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6\ncoverage[toml]==7.3.2 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1 \\\n    --hash=sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63 \\\n    --hash=sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9 \\\n    --hash=sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312 \\\n    --hash=sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3 \\\n    --hash=sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb \\\n    --hash=sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25 \\\n    --hash=sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92 \\\n    --hash=sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda \\\n    --hash=sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148 \\\n    --hash=sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6 \\\n    --hash=sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216 \\\n    --hash=sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a \\\n    --hash=sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640 \\\n    --hash=sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836 \\\n    --hash=sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c \\\n    --hash=sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f \\\n    --hash=sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2 \\\n    --hash=sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901 \\\n    --hash=sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed \\\n    --hash=sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a \\\n    --hash=sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074 \\\n    --hash=sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc \\\n    --hash=sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84 \\\n    --hash=sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083 \\\n    --hash=sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f \\\n    --hash=sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c \\\n    --hash=sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c \\\n    --hash=sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637 \\\n    --hash=sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2 \\\n    --hash=sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82 \\\n    --hash=sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f \\\n    --hash=sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce \\\n    --hash=sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef \\\n    --hash=sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f \\\n    --hash=sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611 \\\n    --hash=sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c \\\n    --hash=sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76 \\\n    --hash=sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9 \\\n    --hash=sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce \\\n    --hash=sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9 \\\n    --hash=sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf \\\n    --hash=sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf \\\n    --hash=sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9 \\\n    --hash=sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6 \\\n    --hash=sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2 \\\n    --hash=sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a \\\n    --hash=sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a \\\n    --hash=sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf \\\n    --hash=sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738 \\\n    --hash=sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a \\\n    --hash=sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4\ncrashtest==0.4.1 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce \\\n    --hash=sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5\ncryptography==41.0.5 ; python_version >= \"3.11\" and python_version < \"4.0\" and sys_platform == \"linux\" \\\n    --hash=sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf \\\n    --hash=sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84 \\\n    --hash=sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e \\\n    --hash=sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8 \\\n    --hash=sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7 \\\n    --hash=sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1 \\\n    --hash=sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88 \\\n    --hash=sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86 \\\n    --hash=sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179 \\\n    --hash=sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81 \\\n    --hash=sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20 \\\n    --hash=sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548 \\\n    --hash=sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d \\\n    --hash=sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d \\\n    --hash=sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5 \\\n    --hash=sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1 \\\n    --hash=sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147 \\\n    --hash=sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936 \\\n    --hash=sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797 \\\n    --hash=sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696 \\\n    --hash=sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72 \\\n    --hash=sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da \\\n    --hash=sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723\ndistlib==0.3.7 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057 \\\n    --hash=sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8\ndnslib==0.9.23 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:310196d3e38ce2051b61eebbd2f1d08fcc934fa3360f22031864d16efe8bca77 \\\n    --hash=sha256:46137e8ef6ef52b24a16d47e0786a99dd103ab1e71eea616f21371accbccc557 \\\n    --hash=sha256:9eb851ac721eea51834d43795478ac9b48272c61ba97cc4a160668b50aff39ec\ndnspython==2.4.2 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8 \\\n    --hash=sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984\ndulwich==0.21.6 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:008ff08629ab16d3638a9f36cfc6f5bd74b4d594657f2dc1583d8d3201794571 \\\n    --hash=sha256:18697b58e0fc5972de68b529b08ac9ddda3f39af27bcf3f6999635ed3da7ef68 \\\n    --hash=sha256:1fedd924763a5d640348db43a267a394aa80d551228ad45708e0b0cc2130bb62 \\\n    --hash=sha256:22798e9ba59e32b8faff5d9067e2b5a308f6b0fba9b1e1e928571ad278e7b36c \\\n    --hash=sha256:24ad45928a65f39ea0f451f9989b7aaedba9893d48c3189b544a70c6a1043f71 \\\n    --hash=sha256:28acbd08d6b38720d99cc01da9dd307a2e0585e00436c95bcac6357b9a9a6f76 \\\n    --hash=sha256:28c9724a167c84a83fc6238e0781f4702b5fe8c53ede31604525fb1a9d1833f4 \\\n    --hash=sha256:2a3fc071e5b14f164191286f7ffc02f60fe8b439d01fad0832697cc08c2237dd \\\n    --hash=sha256:30fbe87e8b51f3813c131e2841c86d007434d160bd16db586b40d47f31dd05b0 \\\n    --hash=sha256:32d3a35caad6879d04711b358b861142440a543f5f4e02df67b13cbcd57f84a6 \\\n    --hash=sha256:32d7acfe3fe2ce4502446d8f7a5ab34cfd24c9ff8961e60337638410906a8fbb \\\n    --hash=sha256:3b1682e8e826471ea3c22b8521435e93799e3db8ad05dd3c8f9b1aaacfa78147 \\\n    --hash=sha256:40623cc39a3f1634663d22d87f86e2e406cc8ff17ae7a3edc7fcf963c288992f \\\n    --hash=sha256:4e09d0b4e985b371aa6728773781b19298d361a00772e20f98522868cf7edc6f \\\n    --hash=sha256:4fdc2f081bc3e9e120079c2cea4be213e3f127335aca7c0ab0c19fe791270caa \\\n    --hash=sha256:513d045e74307eeb31592255c38f37042c9aa68ce845a167943018ab5138b0e3 \\\n    --hash=sha256:54342cf96fe8a44648505c65f23d18889595762003a168d67d7263df66143bd2 \\\n    --hash=sha256:5d2ccf3d355850674f75655154a6519bf1f1664176c670109fa7041019b286f9 \\\n    --hash=sha256:5e58171a5d70f7910f73d25ff82a058edff09a4c1c3bd1de0dc6b1fbc9a42c3e \\\n    --hash=sha256:6592ef2d16ac61a27022647cf64a048f5be6e0a6ab2ebc7322bfbe24fb2b971b \\\n    --hash=sha256:6c91e1ed20d3d9a6aaaed9e75adae37272b3fcbcc72bab1eb09574806da88563 \\\n    --hash=sha256:6fe957564108f74325d0d042d85e0c67ef470921ca92b6e7d330c7c49a3b9c1d \\\n    --hash=sha256:7f89bee4c97372e8aaf8ffaf5899f1bcd5184b5306d7eaf68738c1101ceba10e \\\n    --hash=sha256:81d10aa50c0a9a6dd495990c639358e3a3bbff39e17ff302179be6e93b573da7 \\\n    --hash=sha256:81e237a6b1b20c79ef62ca19a8fb231f5519bab874b9a1c2acf9c05edcabd600 \\\n    --hash=sha256:847bb52562a211b596453a602e75739350c86d7edb846b5b1c46896a5c86b9bb \\\n    --hash=sha256:8b84450766a3b151c3676fec3e3ed76304e52a84d5d69ade0f34fff2782c1b41 \\\n    --hash=sha256:8dfb50b3915e223a97f50fbac0dbc298d5fffeaac004eeeb3d552c57fe38416f \\\n    --hash=sha256:99577b2b37f64bc87280079245fb2963494c345d7db355173ecec7ab3d64b949 \\\n    --hash=sha256:a1ac20dfcfd6057efb8499158d23f2c059f933aefa381e192100e6d8bc25d562 \\\n    --hash=sha256:a2912c8a845c8ccbc79d068a89db7172e355adeb84eb31f062cd3a406d528b30 \\\n    --hash=sha256:a3da632648ee27b64bb5b285a3a94fddf297a596891cca12ac0df43c4f59448f \\\n    --hash=sha256:a64eca1601e79c16df78afe08da9ac9497b934cbc5765990ca7d89a4b87453d9 \\\n    --hash=sha256:a780e2a0ff208c4f218e72eff8d13f9aff485ff9a6f3066c22abe4ec8cec7dcd \\\n    --hash=sha256:a89b19f4960e759915dbc23a4dd0abc067b55d8d65e9df50961b73091b87b81a \\\n    --hash=sha256:a9b52a08d49731375662936d05a12c4a64a6fe0ce257111f62638e475fb5d26d \\\n    --hash=sha256:a9b6f8a16f32190aa88c37ef013858b3e01964774bc983900bd0d74ecb6576e6 \\\n    --hash=sha256:b0545f0fa9444a0eb84977d08e302e3f55fd7c34a0466ec28bedc3c839b2fc1f \\\n    --hash=sha256:b1c9e55233f19cd19c484f607cd90ab578ac50ebfef607f77e3b35c2b6049470 \\\n    --hash=sha256:bf469cd5076623c2aad69d01ce9d5392fcb38a5faef91abe1501be733453e37d \\\n    --hash=sha256:bf90f2f9328a82778cf85ab696e4a7926918c3f315c75fc432ba31346bfa89b7 \\\n    --hash=sha256:c04df87098053b7767b46fc04b7943d75443f91c73560ca50157cdc22e27a5d3 \\\n    --hash=sha256:c2f2683e0598f7c7071ef08a0822f062d8744549a0d45f2c156741033b7e3d7d \\\n    --hash=sha256:c816be529680659b6a19798287b4ec6de49040f58160d40b1b2934fd6c28e93f \\\n    --hash=sha256:ceabe8f96edfb9183034a860f5dc77586700b517457032867b64a03c44e5cf96 \\\n    --hash=sha256:cef50c0a19f322b7150248b8fa0862ce1652dec657e340c4020573721e85f215 \\\n    --hash=sha256:d7cd9fb896c65e4c28cb9332f2be192817805978dd8dc299681c4fe83c631158 \\\n    --hash=sha256:d9002094198e57e88fe77412d3aa64dd05978046ae725a16123ba621a7704628 \\\n    --hash=sha256:daa3584beabfcf0da76df57535a23c80ff6d8ccde6ddbd23bdc79d317a0e20a7 \\\n    --hash=sha256:e07f145c7b0d82a9f77d157f493a61900e913d1c1f8b1f40d07d919ffb0929a4 \\\n    --hash=sha256:e0dee3840c3c72e1d60c8f87a7a715d8eac023b9e1b80199d97790f7a1c60d9c \\\n    --hash=sha256:e1ac882afa890ef993b8502647e6c6d2b3977ce56e3fe80058ce64607cbc7107 \\\n    --hash=sha256:e8ed878553f0b76facbb620b455fafa0943162fe8e386920717781e490444efa \\\n    --hash=sha256:ed2f1f638b9adfba862719693b371ffe5d58e94d552ace9a23dea0fb0db6f468 \\\n    --hash=sha256:edc21c3784dd9d9b85abd9fe53f81a884e2cdcc4e5e09ada17287420d64cfd46 \\\n    --hash=sha256:eee8aba4dec4d0a52737a8a141f3456229c87dcfd7961f8115786a27b6ebefed\nfastjsonschema==2.19.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:b9fd1a2dd6971dbc7fee280a95bd199ae0dd9ce22beb91cc75e9c1c528a5170e \\\n    --hash=sha256:e25df6647e1bc4a26070b700897b07b542ec898dd4f1f6ea013e7f6a88417225\nfilelock==3.13.1 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e \\\n    --hash=sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c\nh11==0.14.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \\\n    --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761\nhttpcore==1.0.2 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7 \\\n    --hash=sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535\nhttpx==0.25.1 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a \\\n    --hash=sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0\nidna==3.4 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \\\n    --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2\nimportlib-metadata==6.8.0 ; python_version >= \"3.11\" and python_version < \"3.12\" \\\n    --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \\\n    --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743\niniconfig==2.0.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \\\n    --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374\ninstaller==0.7.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53 \\\n    --hash=sha256:a26d3e3116289bb08216e0d0f7d925fcef0b0194eedfa0c944bcaaa106c4b631\njaraco-classes==3.3.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb \\\n    --hash=sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621\njeepney==0.8.0 ; python_version >= \"3.11\" and python_version < \"4.0\" and sys_platform == \"linux\" \\\n    --hash=sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806 \\\n    --hash=sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755\nkeyring==24.3.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:4446d35d636e6a10b8bce7caa66913dd9eca5fd222ca03a3d42c38608ac30836 \\\n    --hash=sha256:e730ecffd309658a08ee82535a3b5ec4b4c8669a9be11efb66249d8e0aeb9a25\nmarkdown-it-py==3.0.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \\\n    --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb\nmdurl==0.1.2 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \\\n    --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba\nmore-itertools==10.1.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:626c369fa0eb37bac0291bce8259b332fd59ac792fa5497b59837309cd5b114a \\\n    --hash=sha256:64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6\nmsgpack==1.0.7 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862 \\\n    --hash=sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d \\\n    --hash=sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3 \\\n    --hash=sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672 \\\n    --hash=sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0 \\\n    --hash=sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9 \\\n    --hash=sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee \\\n    --hash=sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46 \\\n    --hash=sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524 \\\n    --hash=sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819 \\\n    --hash=sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc \\\n    --hash=sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc \\\n    --hash=sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1 \\\n    --hash=sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82 \\\n    --hash=sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81 \\\n    --hash=sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6 \\\n    --hash=sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d \\\n    --hash=sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2 \\\n    --hash=sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c \\\n    --hash=sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87 \\\n    --hash=sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84 \\\n    --hash=sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e \\\n    --hash=sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95 \\\n    --hash=sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f \\\n    --hash=sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b \\\n    --hash=sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93 \\\n    --hash=sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf \\\n    --hash=sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61 \\\n    --hash=sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c \\\n    --hash=sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8 \\\n    --hash=sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d \\\n    --hash=sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c \\\n    --hash=sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4 \\\n    --hash=sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba \\\n    --hash=sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415 \\\n    --hash=sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee \\\n    --hash=sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d \\\n    --hash=sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9 \\\n    --hash=sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075 \\\n    --hash=sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f \\\n    --hash=sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7 \\\n    --hash=sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681 \\\n    --hash=sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329 \\\n    --hash=sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1 \\\n    --hash=sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf \\\n    --hash=sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c \\\n    --hash=sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5 \\\n    --hash=sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b \\\n    --hash=sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5 \\\n    --hash=sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e \\\n    --hash=sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b \\\n    --hash=sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad \\\n    --hash=sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd \\\n    --hash=sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7 \\\n    --hash=sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002 \\\n    --hash=sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc\npackaging==23.2 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \\\n    --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7\npexpect==4.8.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937 \\\n    --hash=sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c\npkginfo==1.9.6 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546 \\\n    --hash=sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046\nplatformdirs==3.11.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3 \\\n    --hash=sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e\npluggy==1.3.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12 \\\n    --hash=sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7\npoetry-core==1.8.1 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:194832b24f3283e01c5402eae71a6aae850ecdfe53f50a979c76bf7aa5010ffa \\\n    --hash=sha256:67a76c671da2a70e55047cddda83566035b701f7e463b32a2abfeac6e2a16376\npoetry-plugin-export==1.6.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:091939434984267a91abf2f916a26b00cff4eee8da63ec2a24ba4b17cf969a59 \\\n    --hash=sha256:2dce6204c9318f1f6509a11a03921fb3f461b201840b59f1c237b6ab454dabcf\npoetry==1.7.1 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:03d3807a0fb3bc1028cc3707dfd646aae629d58e476f7e7f062437680741c561 \\\n    --hash=sha256:b348a70e7d67ad9c0bd3d0ea255bc6df84c24cf4b16f8d104adb30b425d6ff32\nptyprocess==0.7.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \\\n    --hash=sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220\npycparser==2.21 ; python_version >= \"3.11\" and python_version < \"4.0\" and (sys_platform == \"darwin\" or sys_platform == \"linux\") \\\n    --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \\\n    --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206\npygments==2.17.2 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c \\\n    --hash=sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367\npyproject-hooks==1.0.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8 \\\n    --hash=sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5\npytest-asyncio==0.21.1 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d \\\n    --hash=sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b\npytest-cov==4.1.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6 \\\n    --hash=sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a\npytest==7.4.3 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac \\\n    --hash=sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5\npywin32-ctypes==0.2.2 ; python_version >= \"3.11\" and python_version < \"4.0\" and sys_platform == \"win32\" \\\n    --hash=sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60 \\\n    --hash=sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7\nrapidfuzz==3.5.2 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:00be97f9219355945c46f37ac9fa447046e6f7930f7c901e5d881120d1695458 \\\n    --hash=sha256:04e1e02b182283c43c866e215317735e91d22f5d34e65400121c04d5ed7ed859 \\\n    --hash=sha256:089a7e96e5032821af5964d8457fcb38877cc321cdd06ad7c5d6e3d852264cb9 \\\n    --hash=sha256:0fef4705459842ef8f79746d6f6a0b5d2b6a61a145d7d8bbe10b2e756ea337c8 \\\n    --hash=sha256:1062425c8358a547ae5ebad148f2e0f02417716a571b803b0c68e4d552e99d32 \\\n    --hash=sha256:120316824333e376b88b284724cfd394c6ccfcb9818519eab5d58a502e5533f0 \\\n    --hash=sha256:12424a06ad9bd0cbf5f7cea1015e78d924a0034a0e75a5a7b39c0703dcd94095 \\\n    --hash=sha256:1962d5ccf8602589dbf8e85246a0ee2b4050d82fade1568fb76f8a4419257704 \\\n    --hash=sha256:1a047d6e58833919d742bbc0dfa66d1de4f79e8562ee195007d3eae96635df39 \\\n    --hash=sha256:1a4a7832737f87583f3863dc62e6f56dd4a9fefc5f04a7bdcb4c433a0f36bb1b \\\n    --hash=sha256:1d5a686ea258931aaa38019204bdc670bbe14b389a230b1363d84d6cf4b9dc38 \\\n    --hash=sha256:1dd2542e5103fb8ca46500a979ae14d1609dcba11d2f9fe01e99eec03420e193 \\\n    --hash=sha256:22877c027c492b7dc7e3387a576a33ed5aad891104aa90da2e0844c83c5493ef \\\n    --hash=sha256:25510b5d142c47786dbd27cfd9da7cae5bdea28d458379377a3644d8460a3404 \\\n    --hash=sha256:27689361c747b5f7b8a26056bc60979875323f1c3dcaaa9e2fec88f03b20a365 \\\n    --hash=sha256:2bacce6bbc0362f0789253424269cc742b1f45e982430387db3abe1d0496e371 \\\n    --hash=sha256:2cf9f2ed4a97b388cffd48d534452a564c2491f68f4fd5bc140306f774ceb63a \\\n    --hash=sha256:2d876dba9a11fcf60dcf1562c5a84ef559db14c2ceb41e1ad2d93cd1dc085889 \\\n    --hash=sha256:2da3a24c2f7dfca7f26ba04966b848e3bbeb93e54d899908ff88dfe3e1def9dc \\\n    --hash=sha256:2fbaf546f15a924613f89d609ff66b85b4f4c2307ac14d93b80fe1025b713138 \\\n    --hash=sha256:32d580df0e130ed85400ff77e1c32d965e9bc7be29ac4072ab637f57e26d29fb \\\n    --hash=sha256:358a0fbc49343de20fee8ebdb33c7fa8f55a9ff93ff42d1ffe097d2caa248f1b \\\n    --hash=sha256:365e544aba3ac13acf1a62cb2e5909ad2ba078d0bfc7d69b1f801dfd673b9782 \\\n    --hash=sha256:40139552961018216b8cd88f6df4ecbbe984f907a62a5c823ccd907132c29a14 \\\n    --hash=sha256:43fb368998b9703fa8c63db292a8ab9e988bf6da0c8a635754be8e69da1e7c1d \\\n    --hash=sha256:467a4d730ae3bade87dba6bd769e837ab97e176968ce20591fe8f7bf819115b1 \\\n    --hash=sha256:51b5166be86e09e011e92d9862b1fe64c4c7b9385f443fb535024e646d890460 \\\n    --hash=sha256:53df7aea3cf301633cfa2b4b2c2d2441a87dfc878ef810e5b4eddcd3e68723ad \\\n    --hash=sha256:54576669c1502b751b534bd76a4aeaaf838ed88b30af5d5c1b7d0a3ca5d4f7b5 \\\n    --hash=sha256:54f0061028723c026020f5bb20649c22bc8a0d9f5363c283bdc5901d4d3bff01 \\\n    --hash=sha256:58e3e21f6f13a7cca265cce492bc797425bd4cb2025fdd161a9e86a824ad65ce \\\n    --hash=sha256:58ee34350f8c292dd24a050186c0e18301d80da904ef572cf5fda7be6a954929 \\\n    --hash=sha256:5afc1fcf1830f9bb87d3b490ba03691081b9948a794ea851befd2643069a30c1 \\\n    --hash=sha256:6541ffb70097885f7302cd73e2efd77be99841103023c2f9408551f27f45f7a5 \\\n    --hash=sha256:666928ee735562a909d81bd2f63207b3214afd4ca41f790ab3025d066975c814 \\\n    --hash=sha256:66be181965aff13301dd5f9b94b646ce39d99c7fe2fd5de1656f4ca7fafcb38c \\\n    --hash=sha256:6b2ad5516f7068c7d9cbcda8ac5906c589e99bc427df2e1050282ee2d8bc2d58 \\\n    --hash=sha256:73e14617a520c0f1bc15eb78c215383477e5ca70922ecaff1d29c63c060e04ca \\\n    --hash=sha256:75d8a52bf8d1aa2ac968ae4b21b83b94fc7e5ea3dfbab34811fc60f32df505b2 \\\n    --hash=sha256:76639dca5eb0afc6424ac5f42d43d3bd342ac710e06f38a8c877d5b96de09589 \\\n    --hash=sha256:7cdf92116e9dfe40da17f921cdbfa0039dde9eb158914fa5f01b1e67a20b19cb \\\n    --hash=sha256:7fb21e182dc6d83617e88dea002963d5cf99cf5eabbdbf04094f503d8fe8d723 \\\n    --hash=sha256:84be69ea65f64fa01e5c4976be9826a5aa949f037508887add42da07420d65d6 \\\n    --hash=sha256:8501d7875b176930e6ed9dbc1bc35adb37ef312f6106bd6bb5c204adb90160ac \\\n    --hash=sha256:852b3f93c15fce58b8dc668bd54123713bfdbbb0796ba905ea5df99cfd083132 \\\n    --hash=sha256:8658c1045766e87e0038323aa38b4a9f49b7f366563271f973c8890a98aa24b5 \\\n    --hash=sha256:8f2df3968738a38d2a0058b5e721753f5d3d602346a1027b0dde31b0476418f3 \\\n    --hash=sha256:8f808dcb0088a7a496cc9895e66a7b8de55ffea0eb9b547c75dfb216dd5f76ed \\\n    --hash=sha256:908ff2de9c442b379143d1da3c886c63119d4eba22986806e2533cee603fe64b \\\n    --hash=sha256:97b043fe8185ec53bb3ff0e59deb89425c0fc6ece6e118939963aab473505801 \\\n    --hash=sha256:97f811ca7709c6ee8c0b55830f63b3d87086f4abbcbb189b4067e1cd7014db7b \\\n    --hash=sha256:99c9fc5265566fb94731dc6826f43c5109e797078264e6389a36d47814473692 \\\n    --hash=sha256:9cdbe8e80cc186d55f748a34393533a052d855357d5398a1ccb71a5021b58e8d \\\n    --hash=sha256:9e9b395743e12c36a3167a3a9fd1b4e11d92fb0aa21ec98017ee6df639ed385e \\\n    --hash=sha256:a42c7a8c62b29c4810e39da22b42524295fcb793f41c395c2cb07c126b729e83 \\\n    --hash=sha256:a8162d81486de85ab1606e48e076431b66d44cf431b2b678e9cae458832e7147 \\\n    --hash=sha256:abafeb82f85a651a9d6d642a33dc021606bc459c33e250925b25d6b9e7105a2e \\\n    --hash=sha256:ada0d8d57e0f556ef38c24fee71bfe8d0db29c678bff2acd1819fc1b74f331c2 \\\n    --hash=sha256:af5221e4f7800db3e84c46b79dba4112e3b3cc2678f808bdff4fcd2487073846 \\\n    --hash=sha256:affb8fe36157c2dc8a7bc45b6a1875eb03e2c49167a1d52789144bdcb7ab3b8c \\\n    --hash=sha256:b2e8b369f23f00678f6e673572209a5d3b0832f4991888e3df97af7b8b9decf3 \\\n    --hash=sha256:b4e9ded8e80530bd7205a7a2b01802f934a4695ca9e9fbe1ce9644f5e0697864 \\\n    --hash=sha256:b581107ec0c610cdea48b25f52030770be390db4a9a73ca58b8d70fa8a5ec32e \\\n    --hash=sha256:b61f77d834f94b0099fa9ed35c189b7829759d4e9c2743697a130dd7ba62259f \\\n    --hash=sha256:b685abb8b6d97989f6c69556d7934e0e533aa8822f50b9517ff2da06a1d29f23 \\\n    --hash=sha256:b847a49377e64e92e11ef3d0a793de75451526c83af015bdafdd5d04de8a058a \\\n    --hash=sha256:bf3093443751e5a419834162af358d1e31dec75f84747a91dbbc47b2c04fc085 \\\n    --hash=sha256:bff7d3127ebc5cd908f3a72f6517f31f5247b84666137556a8fcc5177c560939 \\\n    --hash=sha256:c04f9f1310ce414ab00bdcbf26d0906755094bfc59402cb66a7722c6f06d70b2 \\\n    --hash=sha256:c1d33a622572d384f4c90b5f7a139328246ab5600141e90032b521c2127bd605 \\\n    --hash=sha256:c29958265e4c2b937269e804b8a160c027ee1c2627d6152655008a8b8083630e \\\n    --hash=sha256:c5075ce7b9286624cafcf36720ef1cfb2946d75430b87cb4d1f006e82cd71244 \\\n    --hash=sha256:d05146497672f869baf41147d5ec1222788c70e5b8b0cfcd6e95597c75b5b96b \\\n    --hash=sha256:d4b05a8f4ab7e7344459394094587b033fe259eea3a8720035e8ba30e79ab39b \\\n    --hash=sha256:d55de67c48f06b7772541e8d4c062a2679205799ce904236e2836cb04c106442 \\\n    --hash=sha256:db45028eae2fda7a24759c69ebeb2a7fbcc1a326606556448ed43ee480237a3c \\\n    --hash=sha256:dd6384780c2a16097d47588844cd677316a90e0f41ef96ff485b62d58de79dcf \\\n    --hash=sha256:de89585268ed8ee44e80126814cae63ff6b00d08416481f31b784570ef07ec59 \\\n    --hash=sha256:df8fae2515a1e4936affccac3e7d506dd904de5ff82bc0b1433b4574a51b9bfb \\\n    --hash=sha256:dfc63fabb7d8da8483ca836bae7e55766fe39c63253571e103c034ba8ea80950 \\\n    --hash=sha256:e0f448b0eacbcc416feb634e1232a48d1cbde5e60f269c84e4fb0912f7bbb001 \\\n    --hash=sha256:e3f2be79d4114d01f383096dbee51b57df141cb8b209c19d0cf65f23a24e75ba \\\n    --hash=sha256:e414e1ca40386deda4291aa2d45062fea0fbaa14f95015738f8bb75c4d27f862 \\\n    --hash=sha256:e5fd627e604ddc02db2ddb9ddc4a91dd92b7a6d6378fcf30bb37b49229072b89 \\\n    --hash=sha256:f2059cd73b7ea779a9307d7a78ed743f0e3d33b88ccdcd84569abd2953cd859f \\\n    --hash=sha256:f6da61cc38c1a95efc5edcedf258759e6dbab73191651a28c5719587f32a56ad \\\n    --hash=sha256:f823fd1977071486739f484e27092765d693da6beedaceece54edce1dfeec9b2 \\\n    --hash=sha256:fa4c0612893716bbb6595066ca9ecb517c982355abe39ba9d1f4ab834ace91ad \\\n    --hash=sha256:fb379ac0ddfc86c5542a225d194f76ed468b071b6f79ff57c4b72e635605ad7d \\\n    --hash=sha256:fdfdb3685b631d8efbb6d6d3d86eb631be2b408d9adafcadc11e63e3f9c96dec\nrequests-toolbelt==1.0.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \\\n    --hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06\nrequests==2.31.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \\\n    --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1\nrich==13.7.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa \\\n    --hash=sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235\nruff==0.1.6 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:03910e81df0d8db0e30050725a5802441c2022ea3ae4fe0609b76081731accbc \\\n    --hash=sha256:05991ee20d4ac4bb78385360c684e4b417edd971030ab12a4fbd075ff535050e \\\n    --hash=sha256:137852105586dcbf80c1717facb6781555c4e99f520c9c827bd414fac67ddfb6 \\\n    --hash=sha256:1610e14750826dfc207ccbcdd7331b6bd285607d4181df9c1c6ae26646d6848a \\\n    --hash=sha256:1b09f29b16c6ead5ea6b097ef2764b42372aebe363722f1605ecbcd2b9207184 \\\n    --hash=sha256:1cf5f701062e294f2167e66d11b092bba7af6a057668ed618a9253e1e90cfd76 \\\n    --hash=sha256:3a0cd909d25f227ac5c36d4e7e681577275fb74ba3b11d288aff7ec47e3ae745 \\\n    --hash=sha256:4558b3e178145491e9bc3b2ee3c4b42f19d19384eaa5c59d10acf6e8f8b57e33 \\\n    --hash=sha256:491262006e92f825b145cd1e52948073c56560243b55fb3b4ecb142f6f0e9543 \\\n    --hash=sha256:5c549ed437680b6105a1299d2cd30e4964211606eeb48a0ff7a93ef70b902248 \\\n    --hash=sha256:683aa5bdda5a48cb8266fcde8eea2a6af4e5700a392c56ea5fb5f0d4bfdc0240 \\\n    --hash=sha256:87455a0c1f739b3c069e2f4c43b66479a54dea0276dd5d4d67b091265f6fd1dc \\\n    --hash=sha256:88b8cdf6abf98130991cbc9f6438f35f6e8d41a02622cc5ee130a02a0ed28703 \\\n    --hash=sha256:bd98138a98d48a1c36c394fd6b84cd943ac92a08278aa8ac8c0fdefcf7138f35 \\\n    --hash=sha256:e8fd1c62a47aa88a02707b5dd20c5ff20d035d634aa74826b42a1da77861b5ff \\\n    --hash=sha256:ea284789861b8b5ca9d5443591a92a397ac183d4351882ab52f6296b4fdd5462 \\\n    --hash=sha256:fd89b45d374935829134a082617954120d7a1470a9f0ec0e7f3ead983edc48cc\nsecretstorage==3.3.3 ; python_version >= \"3.11\" and python_version < \"4.0\" and sys_platform == \"linux\" \\\n    --hash=sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77 \\\n    --hash=sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99\nshellingham==1.5.4 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 \\\n    --hash=sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de\nsniffio==1.3.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101 \\\n    --hash=sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384\nstructlog==23.2.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:16a167e87b9fa7fae9a972d5d12805ef90e04857a93eba479d4be3801a6a1482 \\\n    --hash=sha256:334666b94707f89dbc4c81a22a8ccd34449f0201d5b1ee097a030b577fa8c858\ntomlkit==0.12.3 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4 \\\n    --hash=sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba\ntrove-classifiers==2023.11.22 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:533df77e284fd645d90deeafd3ef710d290884efafe4f5009aa1663f95aec992 \\\n    --hash=sha256:c31a7e92f965f060a244b57d8ed5ee6f53fcb413ee17ce790e00577cb369ad99\nurllib3==2.1.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3 \\\n    --hash=sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54\nvirtualenv==20.24.7 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:69050ffb42419c91f6c1284a7b24e0475d793447e35929b488bf6a0aade39353 \\\n    --hash=sha256:a18b3fd0314ca59a2e9f4b556819ed07183b3e9a3702ecfe213f593d44f7b3fd\nxattr==0.10.1 ; python_version >= \"3.11\" and python_version < \"4.0\" and sys_platform == \"darwin\" \\\n    --hash=sha256:042ad818cda6013162c0bfd3816f6b74b7700e73c908cde6768da824686885f8 \\\n    --hash=sha256:0aedf55b116beb6427e6f7958ccd80a8cbc80e82f87a4cd975ccb61a8d27b2ee \\\n    --hash=sha256:0e14bd5965d3db173d6983abdc1241c22219385c22df8b0eb8f1846c15ce1fee \\\n    --hash=sha256:13279fe8f7982e3cdb0e088d5cb340ce9cbe5ef92504b1fd80a0d3591d662f68 \\\n    --hash=sha256:148466e5bb168aba98f80850cf976e931469a3c6eb11e9880d9f6f8b1e66bd06 \\\n    --hash=sha256:16a660a883e703b311d1bbbcafc74fa877585ec081cd96e8dd9302c028408ab1 \\\n    --hash=sha256:183ad611a2d70b5a3f5f7aadef0fcef604ea33dcf508228765fd4ddac2c7321d \\\n    --hash=sha256:199b20301b6acc9022661412346714ce764d322068ef387c4de38062474db76c \\\n    --hash=sha256:1dc9b9f580ef4b8ac5e2c04c16b4d5086a611889ac14ecb2e7e87170623a0b75 \\\n    --hash=sha256:1e2973e72faa87ca29d61c23b58c3c89fe102d1b68e091848b0e21a104123503 \\\n    --hash=sha256:1f0563196ee54756fe2047627d316977dc77d11acd7a07970336e1a711e934db \\\n    --hash=sha256:209fb84c09b41c2e4cf16dd2f481bb4a6e2e81f659a47a60091b9bcb2e388840 \\\n    --hash=sha256:2677d40b95636f3482bdaf64ed9138fb4d8376fb7933f434614744780e46e42d \\\n    --hash=sha256:295b3ab335fcd06ca0a9114439b34120968732e3f5e9d16f456d5ec4fa47a0a2 \\\n    --hash=sha256:3725746a6502f40f72ef27e0c7bfc31052a239503ff3eefa807d6b02a249be22 \\\n    --hash=sha256:3e5825b5fc99ecdd493b0cc09ec35391e7a451394fdf623a88b24726011c950d \\\n    --hash=sha256:3e739d624491267ec5bb740f4eada93491de429d38d2fcdfb97b25efe1288eca \\\n    --hash=sha256:3ff0dbe4a6ce2ce065c6de08f415bcb270ecfd7bf1655a633ddeac695ce8b250 \\\n    --hash=sha256:40039f1532c4456fd0f4c54e9d4e01eb8201248c321c6c6856262d87e9a99593 \\\n    --hash=sha256:436e1aaf23c07e15bed63115f1712d2097e207214fc6bcde147c1efede37e2c5 \\\n    --hash=sha256:46c32cd605673606b9388a313b0050ee7877a0640d7561eea243ace4fa2cc5a6 \\\n    --hash=sha256:475c38da0d3614cc5564467c4efece1e38bd0705a4dbecf8deeb0564a86fb010 \\\n    --hash=sha256:485539262c2b1f5acd6b6ea56e0da2bc281a51f74335c351ea609c23d82c9a79 \\\n    --hash=sha256:49626096ddd72dcc1654aadd84b103577d8424f26524a48d199847b5d55612d0 \\\n    --hash=sha256:4abef557028c551d59cf2fb3bf63f2a0c89f00d77e54c1c15282ecdd56943496 \\\n    --hash=sha256:5267e5f9435c840d2674194150b511bef929fa7d3bc942a4a75b9eddef18d8d8 \\\n    --hash=sha256:5b49d591cf34cda2079fd7a5cb2a7a1519f54dc2e62abe3e0720036f6ed41a85 \\\n    --hash=sha256:5bc40570155beb85e963ae45300a530223d9822edfdf09991b880e69625ba38a \\\n    --hash=sha256:5dc6099e76e33fa3082a905fe59df766b196534c705cf7a2e3ad9bed2b8a180e \\\n    --hash=sha256:636ebdde0277bce4d12d2ef2550885804834418fee0eb456b69be928e604ecc4 \\\n    --hash=sha256:6b8705ac6791426559c1a5c2b88bb2f0e83dc5616a09b4500899bfff6a929302 \\\n    --hash=sha256:6b905e808df61b677eb972f915f8a751960284358b520d0601c8cbc476ba2df6 \\\n    --hash=sha256:7298455ccf3a922d403339781b10299b858bb5ec76435445f2da46fb768e31a5 \\\n    --hash=sha256:772b22c4ff791fe5816a7c2a1c9fcba83f9ab9bea138eb44d4d70f34676232b4 \\\n    --hash=sha256:7880c8a54c18bc091a4ce0adc5c6d81da1c748aec2fe7ac586d204d6ec7eca5b \\\n    --hash=sha256:789bd406d1aad6735e97b20c6d6a1701e1c0661136be9be862e6a04564da771f \\\n    --hash=sha256:7f9be588a4b6043b03777d50654c6079af3da60cc37527dbb80d36ec98842b1e \\\n    --hash=sha256:80638d1ce7189dc52f26c234cee3522f060fadab6a8bc3562fe0ddcbe11ba5a4 \\\n    --hash=sha256:8068df3ebdfa9411e58d5ae4a05d807ec5994645bb01af66ec9f6da718b65c5b \\\n    --hash=sha256:827b5a97673b9997067fde383a7f7dc67342403093b94ea3c24ae0f4f1fec649 \\\n    --hash=sha256:89c93b42c3ba8aedbc29da759f152731196c2492a2154371c0aae3ef8ba8301b \\\n    --hash=sha256:8faaacf311e2b5cc67c030c999167a78a9906073e6abf08eaa8cf05b0416515c \\\n    --hash=sha256:925284a4a28e369459b2b7481ea22840eed3e0573a4a4c06b6b0614ecd27d0a7 \\\n    --hash=sha256:986c2305c6c1a08f78611eb38ef9f1f47682774ce954efb5a4f3715e8da00d5f \\\n    --hash=sha256:9d4c306828a45b41b76ca17adc26ac3dc00a80e01a5ba85d71df2a3e948828f2 \\\n    --hash=sha256:a126eb38e14a2f273d584a692fe36cff760395bf7fc061ef059224efdb4eb62c \\\n    --hash=sha256:a3878e1aff8eca64badad8f6d896cb98c52984b1e9cd9668a3ab70294d1ef92d \\\n    --hash=sha256:a5ea974930e876bc5c146f54ac0f85bb39b7b5de2b6fc63f90364712ae368ebe \\\n    --hash=sha256:a606280b0c9071ef52572434ecd3648407b20df3d27af02c6592e84486b05894 \\\n    --hash=sha256:a9a7a807ab538210ff8532220d8fc5e2d51c212681f63dbd4e7ede32543b070f \\\n    --hash=sha256:aa32f1b45fed9122bed911de0fcc654da349e1f04fa4a9c8ef9b53e1cc98b91e \\\n    --hash=sha256:b0e919c24f5b74428afa91507b15e7d2ef63aba98e704ad13d33bed1288dca81 \\\n    --hash=sha256:b27dfc13b193cb290d5d9e62f806bb9a99b00cd73bb6370d556116ad7bb5dc12 \\\n    --hash=sha256:b34df5aad035d0343bd740a95ca30db99b776e2630dca9cc1ba8e682c9cc25ea \\\n    --hash=sha256:b7bc4ae264aa679aacf964abf3ea88e147eb4a22aea6af8c6d03ebdebd64cfd6 \\\n    --hash=sha256:c0cd2d02ef2fb45ecf2b0da066a58472d54682c6d4f0452dfe7ae2f3a76a42ea \\\n    --hash=sha256:c12e7d81ffaa0605b3ac8c22c2994a8e18a9cf1c59287a1b7722a2289c952ec5 \\\n    --hash=sha256:c3024a9ff157247c8190dd0eb54db4a64277f21361b2f756319d9d3cf20e475f \\\n    --hash=sha256:c4120090dac33eddffc27e487f9c8f16b29ff3f3f8bcb2251b2c6c3f974ca1e1 \\\n    --hash=sha256:c5d3d0e728bace64b74c475eb4da6148cd172b2d23021a1dcd055d92f17619ac \\\n    --hash=sha256:cc6b8d5ca452674e1a96e246a3d2db5f477aecbc7c945c73f890f56323e75203 \\\n    --hash=sha256:ceaa26bef8fcb17eb59d92a7481c2d15d20211e217772fb43c08c859b01afc6a \\\n    --hash=sha256:d1ef954d0655f93a34d07d0cc7e02765ec779ff0b59dc898ee08c6326ad614d5 \\\n    --hash=sha256:d60c27922ec80310b45574351f71e0dd3a139c5295e8f8b19d19c0010196544f \\\n    --hash=sha256:e31d062cfe1aaeab6ba3db6bd255f012d105271018e647645941d6609376af18 \\\n    --hash=sha256:e8c014c371391f28f8cd27d73ea59f42b30772cd640b5a2538ad4f440fd9190b \\\n    --hash=sha256:ec0956a8ab0f0d3f9011ba480f1e1271b703d11542375ef73eb8695a6bd4b78b \\\n    --hash=sha256:f1be6e733e9698f645dbb98565bb8df9b75e80e15a21eb52787d7d96800e823b \\\n    --hash=sha256:f24a7c04ff666d0fe905dfee0a84bc899d624aeb6dccd1ea86b5c347f15c20c1 \\\n    --hash=sha256:f55a2dd73a12a1ae5113c5d9cd4b4ab6bf7950f4d76d0a1a0c0c4264d50da61d \\\n    --hash=sha256:fc354f086f926a1c7f04886f97880fed1a26d20e3bc338d0d965fd161dbdb8ab \\\n    --hash=sha256:ffcb57ca1be338d69edad93cf59aac7c6bb4dbb92fd7bf8d456c69ea42f7e6d2\nzipp==3.17.0 ; python_version >= \"3.11\" and python_version < \"3.12\" \\\n    --hash=sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31 \\\n    --hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0\n"
  },
  {
    "path": "requirements.txt",
    "content": "dnslib==0.9.23 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:310196d3e38ce2051b61eebbd2f1d08fcc934fa3360f22031864d16efe8bca77 \\\n    --hash=sha256:46137e8ef6ef52b24a16d47e0786a99dd103ab1e71eea616f21371accbccc557 \\\n    --hash=sha256:9eb851ac721eea51834d43795478ac9b48272c61ba97cc4a160668b50aff39ec\nmarkdown-it-py==3.0.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \\\n    --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb\nmdurl==0.1.2 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \\\n    --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba\npygments==2.17.2 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c \\\n    --hash=sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367\nrich==13.7.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa \\\n    --hash=sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235\nstructlog==23.2.0 ; python_version >= \"3.11\" and python_version < \"4.0\" \\\n    --hash=sha256:16a167e87b9fa7fae9a972d5d12805ef90e04857a93eba479d4be3801a6a1482 \\\n    --hash=sha256:334666b94707f89dbc4c81a22a8ccd34449f0201d5b1ee097a030b577fa8c858\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/conftest.py",
    "content": "import pytest\nimport pytest_asyncio\nimport logging\nimport asyncio\nimport contextlib\nimport random\nimport string\nimport dns.asyncresolver\n\nfrom dnschef import kitchen\nfrom dnschef.api import app\nfrom dnschef.protocols import start_server\nfrom dnschef.utils import parse_config_file\nfrom dnschef.logger import log, json_capture_formatter #,debug_formatter\nfrom fastapi.testclient import TestClient\n\n#log.setLevel(logging.DEBUG)\n#log.handlers[0].setFormatter(debug_formatter)\n\njh = logging.StreamHandler()\njh.setFormatter(json_capture_formatter)\nlog.addHandler(jh)\n\n@pytest.fixture\ndef random_string():\n    return ''.join(random.choices(string.ascii_letters, k=6))\n\n@pytest.fixture\ndef random_string_gen():\n    def _random_gen():\n        while True:\n            yield ''.join(random.choices(string.ascii_letters, k=6))\n\n    return _random_gen()\n\n@pytest.fixture\ndef api_test_client():\n    return TestClient(app)\n\n@pytest.fixture(scope=\"session\")\ndef event_loop():\n    loop = asyncio.get_event_loop_policy().new_event_loop()\n    yield loop\n    loop.close()\n\n@pytest_asyncio.fixture(scope=\"session\")\nasync def dns_client():\n    resolver = dns.asyncresolver.Resolver()\n    resolver.nameservers = ['127.0.0.1']\n    resolver.port = 5553\n    yield resolver\n\n@pytest.fixture(scope=\"session\")\ndef config_file():\n    return parse_config_file(\"tests/dnschef-tests.toml\")\n\n@pytest_asyncio.fixture(scope=\"session\", autouse=True)\nasync def start_dnschef(config_file):\n    kitchen.CONFIG = config_file\n\n    server_task = asyncio.create_task(\n        start_server(\n            interface=\"127.0.0.1\",\n            nameservers=[\"8.8.8.8\"],\n            tcp=True,\n            ipv6=False,\n            port=5553\n    ))\n\n    yield\n\n    server_task.cancel()\n\n    with contextlib.suppress(asyncio.CancelledError):\n        await server_task\n"
  },
  {
    "path": "tests/dnschef-tests.toml",
    "content": "[A]  # Queries for IPv4 address records\n\"*.thesprawl.org\" = \"100.100.100.100\"\n\"*.test.thesprawl.org\"  = \"127.0.0.1\"\n\"*.*.thesprawl.org\" = \"1.1.1.1\"\n\"c.*.*.thesprawl.org\" = \"1.1.2.2\"\n\"fuck.shit.com\" = \"192.168.0.1\"\n\"*.wat.org\" = { file = \"tests/small-bin-test\", chunk_size = 122 }\n\n[AAAA]  # Queries for IPv6 address records\n\"*.thesprawl.org\" = \"2001:db8::1\"\n\"*.wat.org\" = { file = \"tests/small-bin-test\", chunk_size = 122 }\n\n[MX]    # Queries for mail server records\n\"*.thesprawl.org\" = \"mail.fake.com\"\n\n[NS]    # Queries for mail server records\n\"*.thesprawl.org\" = \"ns.fake.com\"\n\n[CNAME] # Queries for alias records\n\"*.thesprawl.org\" = \"www.fake.com\"\n\n[TXT]   # Queries for text records\n\"*.thesprawl.org\" = \"fake message\"\n\"ok.thesprawl.org\" = \"fake message\"\n\"*.something.wattahog.org\" = \"fuck off\"\n\"wa*.aint.nothing.org\" = \"sequoia banshee boogers\"\n\"ns*.shit.fuck.org\" = { file = \"tests/thicc-bin-test\", chunk_size = 189, response_format = \"{prefix}test-{chunk}\", response_prefix_pool = [\"atlassian-domain-verification=\", \"onetrust-domain-verification=\", \"docusign=\" ] }\n\"ns*.fronted.brick.org\" = { file = \"tests/thicc-bin-test\" }\n\"ns*.filtered.crack.org\" = { file = \"tests/thicc-bin-test\", chunk_size = 50, response_format = \"{prefix}test-{chunk}\", response_prefix_pool = [\"atlassian-domain-verification=\", \"onetrust-domain-verification=\", \"docusign=\" ] }\n\n[TXT.\"*.wattahog.org\"]\nfile = \"tests/thicc-bin-test\"\nchunk_size = 189\nresponse_format = \"{prefix}test-{chunk}\"\nresponse_prefix_pool = [ \"atlassian-domain-verification=\", \"onetrust-domain-verification=\" , \"docusign=\" ]\n\n[PTR]\n\"*.2.0.192.in-addr.arpa\" = \"fake.com\"\n\"*.thesprawl.org\" = \"fake.com\"\n\n[SOA]\n# FORMAT: mname rname t1 t2 t3 t4 t5\n\"*.thesprawl.org\" = \"ns.fake.com. hostmaster.fake.com. 1 10800 3600 604800 3600\"\n\n[NAPTR]\n# FORMAT: order preference flags service regexp replacement\n\"*.thesprawl.org\" = \"100 10 U E2U+sip !^.*$!sip:customer-service@fake.com! .\"\n\n[SRV]\n# FORMAT: priority weight port target\n\"*.thesprawl.org\" = \"0 5 5060 sipserver.fake.com\"\n\n[DNSKEY]\n# FORMAT: flags protocol algorithm base64(key)\n\"*.thesprawl.org\" = \"256 3 5 AQPSKmynfzW4kyBv015MUG2DeIQ3Cbl+BBZH4b/0PY1kxkmvHjcZc8nokfzj31GajIQKY+5CptLr3buXA10hWqTkF7H6RfoRqXQeogmMHfpftf6zMv1LyBUgia7za6ZEzOJBOztyvhjL742iU/TpPSEDhm2SNKLijfUppn1UaNvv4w==\"\n\n[RRSIG]\n# FORMAT: covered algorithm labels labels orig_ttl sig_exp sig_inc key_tag name base64(sig)\n\"*.thesprawl.org\" = \"A 5 3 86400 20030322173103 20030220173103 2642 thesprawl.org. oJB1W6WNGv+ldvQ3WDG0MQkg5IEhjRip8WTrPYGv07h108dUKGMeDPKijVCHX3DDKdfb+v6oB9wfuh3DTJXUAfI/M0zmO/zz8bW0Rznl8O3tGNazPwQKkRN20XPXV6nwwfoXmJQbsLNrLfkGJ5D6fwFm8nN+6pBzeDQfsS3Ap3o=\"\n\n[HTTPS]\n# FORMAT: priority target key=value pairs\n\"*.thesprawl.org\" = \"1 . alpn=h2 ipv4hint=127.0.0.1 ipv6hint=::1\""
  },
  {
    "path": "tests/small-bin-test",
    "content": "#!/bin/sh\ncmd=${0##*/}\nexec grep -F \"$@\"\n"
  },
  {
    "path": "tests/test_dns_server.py",
    "content": "import pytest\nimport difflib\nfrom dnslib import RDMAP\n\n@pytest.mark.asyncio\nasync def test_proxy_request(dns_client):\n    for proto in [False, True]:\n        await dns_client.resolve(\"google.com\", \"A\", tcp=proto)\n\n@pytest.mark.asyncio\nasync def test_fake_A_response(dns_client):\n    for proto in [False, True]:\n        answers = await dns_client.resolve(\"fuck.shit.com\", \"A\", tcp=proto)\n        assert answers[0].address == \"192.168.0.1\"\n\n@pytest.mark.asyncio\nasync def test_correct_wildcard_behavior(dns_client):\n    for proto in [False, True]:\n        answers = await dns_client.resolve(\"thesprawl.org\", \"A\", tcp=proto, raise_on_no_answer = False)\n        assert not len(answers)\n\n        answers = await dns_client.resolve(\"test.thesprawl.org\", \"A\", tcp=proto)\n        assert answers[0].address == \"100.100.100.100\"\n\n        answers = await dns_client.resolve(\"err.thesprawl.org\", \"A\", tcp=proto)\n        assert answers[0].address == \"100.100.100.100\"\n\n        answers = await dns_client.resolve(\"ok.test.thesprawl.org\", \"A\", tcp=proto)\n        assert answers[0].address == \"127.0.0.1\"\n\n        answers = await dns_client.resolve(\"not.bad.thesprawl.org\", \"A\", tcp=proto)\n        assert answers[0].address == \"1.1.1.1\"\n\n        answers = await dns_client.resolve(\"c.bad.wat.thesprawl.org\", \"A\", tcp=proto)\n        assert answers[0].address == \"1.1.2.2\"\n\n        answers = await dns_client.resolve(\"wa1.aint.nothing.org\", \"TXT\", tcp=proto)\n        assert answers[0].to_text().strip('\"') == 'sequoia banshee boogers'\n\n        answers = await dns_client.resolve(\"wattahog.aint.nothing.org\", \"TXT\", tcp=proto)\n        assert answers[0].to_text().strip('\"') == 'sequoia banshee boogers'\n\n\n@pytest.mark.asyncio\nasync def test_fake_wildcard_records(dns_client, random_string, config_file):\n    for proto in [False, True]:\n        for record in RDMAP.keys():\n            if record == \"RRSIG\" or record not in config_file:\n                continue\n\n            answers = await dns_client.resolve(\n                f\"{random_string}.thesprawl.org\",\n                record,\n                tcp=proto\n            )\n\n            #assert answers[0].to_text().replace('\"', '').rstrip('.') == config_file[record][\"*.thesprawl.org\"]\n\n            assert difflib.SequenceMatcher(\n                a=answers[0].to_text(),\n                b=config_file[record][\"*.thesprawl.org\"]\n            ).quick_ratio() > 0.86\n"
  },
  {
    "path": "tests/test_file_staging.py",
    "content": "import pytest\nimport hashlib\nfrom base64 import b64decode\nfrom ipaddress import IPv4Address, IPv6Address\n\n\ndef compare_file_digests(tmp_file_path, orig_file_path):\n    with tmp_file_path.open('rb') as staged_file:\n        with open(orig_file_path, 'rb') as orig_file:\n            staged_file_digest = hashlib.file_digest(staged_file, \"md5\").digest()\n            orig_file_digest = hashlib.file_digest(orig_file, \"md5\").digest()\n\n            return staged_file_digest == orig_file_digest\n\n@pytest.mark.asyncio\nasync def test_A_file_staging(dns_client, tmp_path, random_string_gen):\n    orig_file_path = \"tests/small-bin-test\"\n    for proto in [False, True]:\n        chunk_n  = 0\n        tmp_file_path = tmp_path / next(random_string_gen)\n        with tmp_file_path.open('ab') as f:\n            while True:\n                answers = await dns_client.resolve(f\"lala{chunk_n}dayum.wat.org\", \"A\", tcp=proto, raise_on_no_answer=False)\n                print(list(answers))\n                for answer in answers:\n                    data = IPv4Address(answer.address).packed\n                    data = data.replace(b'\\x00', b'')\n                    f.write(data)\n\n                if not len(answers):\n                    break\n\n                chunk_n += 1\n\n        assert compare_file_digests(tmp_file_path, orig_file_path) == True\n\n@pytest.mark.asyncio\nasync def test_AAAA_file_staging(dns_client, tmp_path, random_string_gen):\n    orig_file_path = \"tests/small-bin-test\"\n    for proto in [False, True]:\n        chunk_n  = 0\n        tmp_file_path = tmp_path / next(random_string_gen)\n        with tmp_file_path.open('ab') as f:\n            while True:\n                answers = await dns_client.resolve(f\"lala{chunk_n}dayum.wat.org\", \"AAAA\", tcp=proto, raise_on_no_answer=False)\n                for answer in answers:\n                    data = IPv6Address(answer.address).packed\n                    data = data.replace(b'\\x00', b'')\n                    f.write(data)\n\n                if not len(answers):\n                    break\n\n                chunk_n += 1\n\n        assert compare_file_digests(tmp_file_path, orig_file_path) == True\n\n@pytest.mark.asyncio\nasync def test_TXT_file_staging(dns_client, tmp_path, random_string_gen):\n    orig_file_path = \"tests/thicc-bin-test\"\n    for proto in [False, True]:\n        chunk_n  = 0\n        tmp_file_path = tmp_path / next(random_string_gen)\n        with tmp_file_path.open('ab') as f:\n            while True:\n                answers = await dns_client.resolve(f\"ns{chunk_n}.fronted.brick.org\", \"TXT\", tcp=proto, raise_on_no_answer=False)\n                for answer in answers:\n                    f.write(b64decode(answer.to_text().strip('\"')))\n\n                if not len(answers):\n                    break\n\n                chunk_n += 1\n\n        assert compare_file_digests(tmp_file_path, orig_file_path) == True\n"
  },
  {
    "path": "tests/test_http_api.py",
    "content": "import json\n\ndef test_get_records(api_test_client, config_file):\n    r = api_test_client.get(\"/\")\n    assert r.status_code == 200\n    assert r.json() == config_file\n\ndef test_add_record(api_test_client):\n    r = api_test_client.put(\n        \"/\",\n        json={\"type\": \"A\", \"domain\": \"*.nashvillenibblers.com\", \"value\": \"192.168.69.69\"}\n    )\n    assert r.status_code == 200\n\n    r = api_test_client.get(\"/\")\n    assert r.status_code == 200\n    assert r.json()[\"A\"][\"*.nashvillenibblers.com\"] == \"192.168.69.69\"\n\ndef test_delete_record(api_test_client):\n    r = api_test_client.request(\n        method=\"DELETE\",\n        url=\"/\",\n        content=json.dumps({\"type\": \"A\", \"domain\": \"*.nashvillenibblers.com\", \"value\": \"192.168.69.69\"}).encode()\n    )\n    assert r.status_code == 200\n\n    r = api_test_client.get(\"/\")\n    assert r.status_code == 200\n    assert not r.json()[\"A\"].get(\"*.nashvillenibblers.com\", None)\n\ndef test_logs(api_test_client):\n    r = api_test_client.get(\"/logs\")\n    assert r.status_code == 200\n\n    r = api_test_client.get(\n        \"/logs\",\n        params={\"type\": \"A\"}\n    )\n    assert r.status_code == 200\n    assert len(r.json())\n\n    r = api_test_client.get(\n        \"/logs\",\n        params={\"name\": \"fuck.shit.com\"}\n    )\n    assert r.status_code == 200\n    assert len(r.json())\n\n    r = api_test_client.get(\n        \"/logs\",\n        params={\"name\": \"fuck.shit.com\", \"type\": \"A\"}\n    )\n    assert r.status_code == 200\n    assert len(r.json())\n"
  },
  {
    "path": "tests/test_util.py",
    "content": "import pytest\nfrom dnschef import __version__\nfrom dnschef.utils import header\n\n@pytest.mark.asyncio\nasync def test_config_parse(config_file):\n    assert len(config_file)\n\n@pytest.mark.asyncio\nasync def test_header():\n    assert __version__ in header\n"
  }
]