diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..430e6b7 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,60 @@ +name: Build and Upload Artifacts + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + strategy: + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + + runs-on: ${{ matrix.os }} + + steps: + - name: Check-out repository + uses: actions/checkout@v4 + with: + ref: main + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + cache: 'pip' + cache-dependency-path: | + **/pyproject*.toml + + - name: Install Dependencies + run: | + python -m pip install poetry==1.8.5 + poetry config virtualenvs.in-project true + poetry lock --no-update + poetry install + + + - name: Build Executable with Nuitka + uses: Nuitka/Nuitka-Action@main + with: + nuitka-version: main + script-name: vinetrimmer1.py + mode: onefile + windows-console-mode: force + include-data-dir: ./vinetrimmer/=vinetrimmer/ + follow-imports: true + + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ runner.os }} Build + path: | + build/*.exe + build/*.bin + build/*.app/**/* + include-hidden-files: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aaf8f06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,197 @@ +/misc/ +/Temp/ +/Downloads/ +/vinetrimmer/Cache/ +/vinetrimmer/Cookies/ +/vinetrimmer/Logs/ + + +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +Tests/ +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python + +devices/ + +scalable/43.xml +scalable/40.xml +pdm.lock +.pdm-python +/.idea/.gitignore +/license.xml +/.idea/misc.xml +/.idea/modules.xml +/.idea/inspectionProfiles/profiles_settings.xml +/.idea/pyplayready.iml +/.idea/vcs.xml diff --git a/.idea/PlayReady-Amazon-Tool-main.iml b/.idea/PlayReady-Amazon-Tool-main.iml new file mode 100644 index 0000000..f58bb3c --- /dev/null +++ b/.idea/PlayReady-Amazon-Tool-main.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..1a8b156 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml new file mode 100644 index 0000000..c16d827 --- /dev/null +++ b/.idea/material_theme_project_new.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/poetry.xml b/.idea/runConfigurations/poetry.xml new file mode 100644 index 0000000..7534934 --- /dev/null +++ b/.idea/runConfigurations/poetry.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/ATVP.txt b/ATVP.txt new file mode 100644 index 0000000..22c42d6 --- /dev/null +++ b/ATVP.txt @@ -0,0 +1,85 @@ +https://tv.apple.com/us/show/ray-donovan/umc.cmc.hr7pnm1wbx98w1h3pg7dfbey +https://tv.apple.com/us/show/party-down/umc.cmc.6myol1kgcd19kerlujhtcr8kg +https://tv.apple.com/us/show/mythic-quest/umc.cmc.1nfdfd5zlk05fo1bwwetzldy3 +https://tv.apple.com/us/show/the-completely-made-up-adventures-of-dick-turpin/umc.cmc.37r7vskzmm8hk2pfbzaxlcwzg +https://tv.apple.com/us/show/the-office-superfan-episodes/umc.cmc.3r3om9j6edlrnznl5pfassikv +https://tv.apple.com/us/show/trailer-park-boys-the-swearnet-show/umc.cmc.71tbyxchxiwotaysuuztm8p54 +https://tv.apple.com/us/show/fridays/umc.cmc.ve44y99fmo41lok4mx7azvfi +https://tv.apple.com/us/show/utopia/umc.cmc.4uzbqvarwjrbkqz92796oelqj + +https://tv.apple.com/us/movie/oceans-eleven/umc.cmc.4mt9j4jqou4mlup1pc9riyo63 +https://tv.apple.com/us/movie/bullet-train/umc.cmc.5erhpztw3spfkfi0daabkmaq0 +https://tv.apple.com/us/movie/burn-after-reading/umc.cmc.7jdvh85z66fxnfoxg7cpxjc7w +https://tv.apple.com/us/movie/snatch/umc.cmc.52ds3rgwysa2qq3f1ve5vsed5 +https://tv.apple.com/us/movie/bad-education/umc.cmc.yu0edomvzp43frs48ppnbcn9 +https://tv.apple.com/us/movie/american-psycho/umc.cmc.4k3idfzm0x9j5okdedo2s4l50 +https://tv.apple.com/us/movie/reservoir-dogs/umc.cmc.7iuglffk5zzbfzcki63wclmdo +https://tv.apple.com/us/movie/matchstick-men/umc.cmc.lu6ko9g1d8pmr8uf3vagwbw5 +https://tv.apple.com/us/movie/lock-stock-and-two-smoking-barrels/umc.cmc.6h9x0cz5gx347hod2h03icb4z +https://tv.apple.com/us/movie/american-hustle/umc.cmc.3tyc5zjamj769wybyabz90ln +https://tv.apple.com/us/movie/midnight-run/umc.cmc.1f02nu6ah611n1tsm7vrzmdnh +https://tv.apple.com/us/show/devs/umc.cmc.ggsldeofp8msuxqwfw7gevbd +https://tv.apple.com/us/show/legion/umc.cmc.1qs2k3j6y79dkp2muruy3tfgy +https://tv.apple.com/us/show/a-murder-at-the-end-of-the-world/umc.cmc.1lsxve21epalnb4me2cad6xel +https://tv.apple.com/us/movie/gattaca/umc.cmc.5ezqaeo0qqkflzp5ofr6s33ny +https://tv.apple.com/us/show/trying/umc.cmc.6muy4la7lj1omu5nci4bt2m66 +https://tv.apple.com/us/show/this-way-up/umc.cmc.56948mmn5dgege1ql6ijmipu2 +https://tv.apple.com/us/show/gavin-and-stacey/umc.cmc.238a52ehihka52ra3numzbsjs +https://tv.apple.com/us/show/breeders/umc.cmc.3en8wopxszlklshswogli6krr +https://tv.apple.com/us/movie/about-a-boy/umc.cmc.3gem0qakjpyuxutj6sobh3z39 +https://tv.apple.com/us/movie/the-skeleton-twins/umc.cmc.2g8xdqv37e0sirwudfi41u7xa +https://tv.apple.com/us/movie/instant-family/umc.cmc.5ug70ylvyv69d6hpxevfh44fi +https://tv.apple.com/us/movie/about-time/umc.cmc.2vfd635fg0hxxlsqcetlzw8mp +https://tv.apple.com/us/show/youre-the-worst/umc.cmc.1tm22s9y05150wiwsk5e7mdqw +https://tv.apple.com/us/show/rain-dogs/umc.cmc.php72zujxqf4logv2q880aps +https://tv.apple.com/us/show/its-always-sunny-in-philadelphia/umc.cmc.ukor9ll2s04re94sh7689z5y +https://tv.apple.com/us/show/the-it-crowd/umc.cmc.14z35ywo1dbp5e81sh0yj6520 +https://tv.apple.com/us/show/silicon-valley/umc.cmc.4v7y09m6sa22lpe3bqomh27a +https://tv.apple.com/us/show/dead-pixels/umc.cmc.1lwecm1x1e2c8ufo0hrkggq8q +https://tv.apple.com/us/show/superstore/umc.cmc.3kea4yvxqzaonxp9g7iigp1d5 +https://tv.apple.com/us/show/what-we-do-in-the-shadows/umc.cmc.2bss05wnkfezkywhivjc7ikml +https://tv.apple.com/us/show/arrested-development/umc.cmc.2o9q1cdgu5880nfjtystky5pz +https://tv.apple.com/us/show/community/umc.cmc.4nernxq22u93vwrjpt6kfk3m5 +https://tv.apple.com/us/show/parks-and-recreation/umc.cmc.4rhzbt3nmsycvgt4nilhbuyae +https://tv.apple.com/us/show/betas/umc.cmc.4u7ljqjpblmzxgnbhwnzh1a0r +https://tv.apple.com/us/show/the-league/umc.cmc.145fot332z5stngwfykfmx9st +https://tv.apple.com/us/show/difficult-people/umc.cmc.1xq8e6mjorh8a4fs5w2fi3o87 +https://tv.apple.com/us/show/year-of-the-rabbit/umc.cmc.fnlixoorkhopjo3vy83c8wkk +https://tv.apple.com/us/show/bored-to-death/umc.cmc.rs4hlv86qqcxde2he91kk0wm +https://tv.apple.com/us/show/blackadder/umc.cmc.3qs9hyo5c66je1v5m2bdjcy09 +https://tv.apple.com/us/movie/tom-jones/umc.cmc.34cb2q35xhn74pfkyho9kcbej +https://tv.apple.com/us/show/plebs/umc.cmc.9fff97cmmp6s6pv0bmrw83fe +https://tv.apple.com/us/show/absolutely-fabulous/umc.cmc.1h4vlcvvmw6tlhld1ylxonj6 +https://tv.apple.com/us/movie/your-highness/umc.cmc.3bqdgohnxq3770rrlaur2zk40 +https://tv.apple.com/us/show/upstart-crow/umc.cmc.3d1f8o7akwzpzubsb3mgc84i5 +https://tv.apple.com/us/show/peep-show/umc.cmc.448lyp9cd81fpg1m2blz6hwau +https://tv.apple.com/us/show/drunk-history-uk/umc.cmc.36gdb51ghpvc33vfo8xbypg7r +https://tv.apple.com/us/show/wellington-paranormal/umc.cmc.233r2ier7l3vwggztv22z48u1 +https://tv.apple.com/us/show/the-office/umc.cmc.6ll0rkl6kvx4fv6tgzmnubxe6 +https://tv.apple.com/us/show/the-larry-sanders-show/umc.cmc.3w4wp4iasdpolyr0ph98cyiav +https://tv.apple.com/us/show/portlandia/umc.cmc.k20h4a8hmspbb1m0z3362sdi +https://tv.apple.com/us/show/the-state/umc.cmc.5af6lx6evkseyhotjzhr16oot +https://tv.apple.com/us/show/drunk-history/umc.cmc.2fai5tmqz2z6g9iy8er8ft11m +https://tv.apple.com/us/show/upright-citizens-brigade/umc.cmc.638n6gvt13rg3w8g24h1chmdr +https://tv.apple.com/us/show/kroll-show/umc.cmc.6wjdipahuh4bbuqn2lfrvkihd +https://tv.apple.com/us/show/comedy-bang-bang/umc.cmc.4gsknoov72z0j00yw5suel6jp +https://tv.apple.com/us/show/robot-chicken/umc.cmc.5vh2acwvaldmwdqj1qibtl02u +https://tv.apple.com/us/show/key--peele/umc.cmc.3zn0d5n33pl7buheotza1oyfs +https://tv.apple.com/us/show/umc.cmc.3p5hey1eeuk85gse9d27zaa5q +https://tv.apple.com/us/show/red-dwarf/umc.cmc.rdyyed48i59bca0cnr5rov5s +https://tv.apple.com/us/movie/the-trip/umc.cmc.dkvyuytopxriedof0rl378mk +https://tv.apple.com/us/show/this-fool/umc.cmc.2inku964u5u7ua55gw7k84jrz +https://tv.apple.com/us/show/sealab-2021/umc.cmc.58yrhf06pngkw7lnv9g1awh6o +https://tv.apple.com/us/movie/in-bruges/umc.cmc.3hfzzmfjj39s02vwynx3b91ih +https://tv.apple.com/us/show/get-shorty/umc.cmc.7gvn6fekgfpq5fc72pgi1c47o +https://tv.apple.com/us/season/season-43/umc.cmc.3ays0nyep4svqlqku2pjelspx?showId=umc.cmc.3p5hey1eeuk85gse9d27zaa5q#see-all/more-seasons-in-series | Saturday Night Live - Apple TV +https://tv.apple.com/us/show/the-larry-sanders-show/umc.cmc.3w4wp4iasdpolyr0ph98cyiav | The Larry Sanders Show - Apple TV +https://tv.apple.com/us/show/portlandia/umc.cmc.k20h4a8hmspbb1m0z3362sdi | Portlandia - Apple TV +https://tv.apple.com/us/show/kroll-show/umc.cmc.6wjdipahuh4bbuqn2lfrvkihd | Kroll Show - Apple TV +https://tv.apple.com/us/show/comedy-bang-bang/umc.cmc.4gsknoov72z0j00yw5suel6jp | Comedy Bang! Bang! - Apple TV +https://tv.apple.com/us/show/key--peele/umc.cmc.3zn0d5n33pl7buheotza1oyfs | Key & Peele - Apple TV +https://tv.apple.com/us/show/robot-chicken/umc.cmc.5vh2acwvaldmwdqj1qibtl02u | Robot Chicken - Apple TV +https://tv.apple.com/us/show/the-state/umc.cmc.5af6lx6evkseyhotjzhr16oot | The State - Apple TV +https://tv.apple.com/us/show/upright-citizens-brigade/umc.cmc.638n6gvt13rg3w8g24h1chmdr | Upright Citizens Brigade - Apple TV +https://tv.apple.com/us/show/fridays/umc.cmc.ve44y99fmo41lok4mx7azvfi | Fridays - Apple TV +https://tv.apple.com/us/show/drunk-history/umc.cmc.2fai5tmqz2z6g9iy8er8ft11m | Drunk History - Apple TV \ No newline at end of file diff --git a/How.to.use.txt b/How.to.use.txt new file mode 100644 index 0000000..a06ac80 --- /dev/null +++ b/How.to.use.txt @@ -0,0 +1,149 @@ +1. Install python 3.12.xx, 3.11.xx, 3.10.xx, 3.9.xx or 3.8.xx, be sure to add python to PATH while installing it + +2. Install Microsoft Visual C++ Redistributable + +https://aka.ms/vs/17/release/vc_redist.x64.exe + +3. Run install.bat + +4. For Netflix go to the folder vinetrimmer and add your email and password of Netflix in vinetrimmer\vinetrimmer.yml (open the file with Notepad++), you do not need adding your credentials for Amazon, Peacock, GooglePlay and Hulu, just the cookies file, the cookies file name will always be default.txt. + +5. Install this Firefox add-on + +https://addons.mozilla.org/en-US/firefox/addon/cookies-txt-one-click/ + +for Chrome install this extension + +https://chrome.google.com/webstore/detail/open-cookiestxt/gdocmgbfkjnnpapoeobnolbbkoibbcif + +and go to netflix.com and download the cookies, rename the file to default.txt and put it in + +vinetrimmer\Cookies\Netflix + +For Amazon go to amazon.com and download the cookies, rename the file to default.txt and put it in + +vinetrimmer\Cookies\Amazon + +For Peacock go to peacocktv.com and download the cookies, rename the file to default.txt and put it in + +vinetrimmer\Cookies\Peacock + +For Hulu go to https://www.hulu.com and download the cookies, rename the file to default.txt and put it in + +vinetrimmer\Cookies\Hulu + +6. For Netflix you will need to add the esn in Netflix.yml which is in the folder vinetrimmer\config\services +use the examples from the file Netflix.yml from the folder example_configs to understand how to add the correct esn. + +7. For downloading the video of an episode from Netflix use (to download in HIGH profile use -p HPL and to download in MAIN profile use -p MPL) + +poetry run vt dl -q 540 -al en -sl en -w S01E1 Netflix -p HPL 80189685 + +The script does not support decrypting the videos with MAIN or HIGH profiles in 1080p or 720p (only MAIN profile in 480p and HIGH profile in 540p will get decrypted) using cdm l3, you will need cdm l1 for MAIN or HIGH 1080p and 720p. + +If you want to downlod the video for a specific seasons or all the seasons use + +poetry run vt dl -q 540 -al en -sl en -w S01 Netflix -p HPL 80189685 + +poetry run vt dl -q 540 -al en -sl en -w S01,S03 Netflix -p HPL 80189685 + +poetry run vt dl -q 540 -al en -sl en -w S01-S05 Netflix -p HPL 80189685 + +To download the video of a movie use + +poetry run vt dl -q 540 -al en -sl en Netflix -p HPL 81252357 + +To download multiple audio languages use + +poetry run vt dl -q 540 -al en,es,fr -sl en Netflix -p HPL 81252357 + +To downlaod all the audio languages use + +poetry run vt dl -q 540 -al all -sl en Netflix -p HPL 81252357 + +If you did not choose a language for the audio the script will download the original audio language, and if you did not choose a language for the subtitles the script will download all the available subtitles. + +8. To download the video from amazon.com you will need to get the asin for the videos, +use this userscript for getting it + +https://greasyfork.org/en/scripts/381997-amazon-video-asin-display + +For downloading the video of an episode from amazon.com use + +poetry run vt dl -al en -sl en -w S06E1 Amazon -vq SD B09LGW8D3Q + +The script supports only downloading the videos in SD with cdm l3. + +If you want to downlod the video for a specific seasons use + +poetry run vt dl -al en -sl en -w S06 Amazon -vq SD B09LGW8D3Q + +9. For downloading the video of an episode from Peacock use + +poetry run vt dl -q 1080 -al en -w S01E1 Peacock /tv/the-office/4902514835143843112 + +For downloading the video of a movie from Peacock use + +poetry run vt dl Peacock /movies/action-and-adventure/4got10/3ae2c66c-2b1a-3a34-a84f-930d60612b95 + +10. For downloading the video of an episode from DisneyPlus use (cdm l3 is 720p only) + +poetry run vt dl -q 720 -w S01E01 DisneyPlus -s browser 57TL7zLNu2wf + +11. For downloading the video of a movie from Hulu in 1080p use + +poetry run vt dl Hulu -m 4800d468-b587-44de-bad8-2646588bfa6b + +12. If you got an error message while trying to download from google play about SAPISID remove from the cookies file the extra lines with the word SAPISID, just keep the first line with the word SAPISID + +13. For downloading a movie from ParamountPlus use + +poetry run vt dl https://www.paramountplus.com/movies/video/7juDj4xQPZG1xtvD0pDPRQWgv1Vj6xox/ + +For downloading an episode from ParamountPlus use + +poetry run vt dl -w S01E1 https://www.paramountplus.com/shows/1883/ + + +for more options use + +poetry run vt dl -h + +for the extra options in the scripts use + +poetry run vt dl Hulu -h + +instead of Hulu add the service name + +the extra options will be added after the service name + +To use your cdm l3 in the folder vinetrimmer\devices create a new folder for the cdm l3 and copy the files device_private_key and device_client_id_blob to it like vinetrimmer\devices\generic_4464_l3 and create a new text file and rename it to wv.json and in the file wv.json add something like + +{ +"name": "generic_4464_l3", +"description": "android generic 4464 l3", +"security_level": 3, +"session_id_type": "android", +"private_key_available": "True", +"vmp": "False", +"send_key_control_nonce": "True" +} + +change the name and the description to match the cdm l3 you have. + +for cdm l1 use + +"security_level": 1, + +open the file vinetrimmer\vinetrimmer.yml with Notepad++ and add your cdm l3 name like this + +cdm: + default: 'generic_4464_l3' + + + + + + + + diff --git a/README.md b/README.md index 34ca92a..7418d67 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,210 @@ -# VT-PR -VineTrimmer with PlayReady support, WIP +# VineTrimmer-PlayReady +A tool to download and remove DRM from streaming services. A version of an old fork of [devine](https://github.com/devine-dl/devine). +Modified to remove Playready DRM instead of Widevine. + +## Features + - Progress Bars for decryption ([mp4decrypt](https://github.com/chu23465/bentoOldFork), Shaka) + - Refresh Token fixed for Amazon service + - Reprovision .prd after a week + - ISM manifest support (Microsoft Smooth Streaming) (Few features to be added) + - N_m3u8DL-RE downloader support + + +## Usage + +1. Run `install.bat` + +2. Activate venv using `venv.cmd`. + + +### Config + +`vinetrimmer.yml` located within the `/vinetrimmer/` folder. + +`decryptor:` either `mp4decrypt` or `packager` + +(shaka-packager fails to decrypt files downloaded from MSS manifests) + +`tag:` tag for your release group + +CDM can be configured per service or per profile. + +``` +cdm: + default: {text} + Amazon: {text} +``` + +All other option can be left to defaults, unless you know what you are doing. + +### General Options + +Usage: vt.cmd [OPTIONS] COMMAND [ARGS]... + +Options: +| Command line argument | Description | Default Value | +|----------------------------|-----------------------------------------------------------------------------------------------|-----------------------------------| +| -d, --debug | Flag to enable debug logging | False | +| -p, --profile | Profile to use when multiple profiles are defined for a service. | "default" | +| -q, --quality | Download Resolution | 1080 | +| -v, --vcodec | Video Codec | H264 | +| -a, --acodec | Audio Codec | None | +| -vb, --vbitrate | Video Bitrate | Max | +| -ab, --abitrate | Audio Bitrate | Max | +| -aa, --atmos | Prefer Atmos Audio | False | +| -r, --range | Video Color Range `HDR`, `HDR10`, `DV`, `SDR` | SDR | +| -w, --wanted | Wanted episodes, e.g. `S01-S05,S07`, `S01E01-S02E03`, `S02-S02E03` | Default to all | +| -al, --alang | Language wanted for audio. | Defaults to original language | +| -sl, --slang | Language wanted for subtitles. | Defaults to original language | +| --proxy | Proxy URI to use. If a 2-letter country is provided, it will try get a proxy from the config. | None | +| -A, --audio-only | Only download audio tracks. | False | +| -S, --subs-only | Only download subtitle tracks. | False | +| -C, --chapters-only | Only download chapters. | False | +| -ns, --no-subs | Do not download subtitle tracks. | False | +| -na, --no-audio | Do not download audio tracks. | False | +| -nv, --no-video | Do not download video tracks. | False | +| -nc, --no-chapters | Do not download chapters tracks. | False | +| -ad, --audio-description | Download audio description tracks. | False | +| --list | Skip downloading and list available tracks and what tracks would have been downloaded. | False | +| --selected | List selected tracks and what tracks are downloaded. | False | +| --cdm | Override the CDM that will be used for decryption. | None | +| --keys | Skip downloading, retrieve the decryption keys (via CDM or Key Vaults) and print them. | False | +| --cache | Disable the use of the CDM and only retrieve decryption keys from Key Vaults. If a needed key is unable to be retrieved from any Key Vaults, the title is skipped.| False | +| --no-cache | Disable the use of Key Vaults and only retrieve decryption keys from the CDM. | False | +| --no-proxy | Force disable all proxy use. | False | +| -nm, --no-mux | Do not mux the downloaded and decrypted tracks. | False | +| --mux | Force muxing when using --audio-only/--subs-only/--chapters-only. | False | +| -?, -h, --help | Show this message and exit. | | + + +COMMAND :- + +| Alaias | Command | Service Link | +|--------|---------------|--------------------------------------------| +| AMZN | Amazon | https://amazon.com, https://primevideo.com | +| ATVP | AppleTVPlus | https://tv.apple.com | +| MAX | Max | https://max.com | +| NF | Netflix | https://netflix.com | + +### Amazon Specific Options + +Usage: vt.cmd AMZN [OPTIONS] [TITLE] + + Service code for Amazon VOD (https://amazon.com) and Amazon Prime Video (https://primevideo.com). + + Authorization: Cookies + + Security: + ``` + UHD@L1/SL3000 + FHD@L3(ChromeCDM)/SL2000 + SD@L3 + + Certain SL2000 can do UHD + ``` + Maintains their own license server like Netflix, be cautious. + + Region is chosen automatically based on domain extension found in cookies. + Prime Video specific code will be run if the ASIN is detected to be a prime video variant. + Use 'Amazon Video ASIN Display' for Tampermonkey addon for ASIN + https://greasyfork.org/en/scripts/381997-amazon-video-asin-display + + vt dl --list -z uk -q 1080 Amazon B09SLGYLK8 + +Below flags to be passed after the `AMZN` or `Amazon` keyword in command. + +| Command Line Switch | Description | +|-------------------------------------|-----------------------------------------------------------------------------------------------------| +| -b, --bitrate | Video Bitrate Mode to download in. CVBR=Constrained Variable Bitrate, CBR=Constant Bitrate. (CVBR or CBR or CVBR+CBR) | +| -c, --cdn | CDN to download from, defaults to the CDN with the highest weight set by Amazon. | +| -vq, --vquality | Manifest quality to request. (SD or HD or UHD) | +| -s, --single | Force single episode/season instead of getting series ASIN. | +| -am, --amanifest | Manifest to use for audio. Defaults to H265 if the video manifest is missing 640k audio. (CVBR or CBR or H265) | +| -aq, --aquality | Manifest quality to request for audio. Defaults to the same as --quality. (SD or HD or UHD) | +| -ism, --ism | Set manifest override to SmoothStreaming. Defaults to DASH w/o this flag. | +| -?, -h, --help | Show this message and exit. | + +To get UHD/4k with Amazon, navigate to - + +``` +https://www.primevideo.com/region/eu/ontv/code?ref_=atv_auth_red_aft +``` + +Login and get to the code pair page. Extract cookies from that page using [Open Cookies.txt](https://chromewebstore.google.com/detail/open-cookiestxt/gdocmgbfkjnnpapoeobnolbbkoibbcif). + +Save it to the path `vinetrimmer/Cookies/Amazon/default.txt`. + +When caching cookies, use a profile without PIN. Otherwise it causes errors. + +### Peacock + + - PCOK bans leaked certs quickly (for 4k), be cautious. + +### Example Command + +Amazon Example: + +```bash +poetry run vt dl -al en -sl en --selected -q 2160 -r HDR -w S01E18-S01E25 AMZN -b CBR --ism 0IQZZIJ6W6TT2CXPT6ZOZYX396 +``` + +Above command: + - gets english subtitles + audio, + - selects the HDR + 4K track, + - gets episodes from S01E18 to S01E25 from Amazon + - with CBR bitrate, + - tries to force ISM + - and the title-ID is 0IQZZIJ6W6TT2CXPT6ZOZYX396 + +AppleTV Example: + +```bash +poetry run vt dl -al en -sl en --list -q 720 --proxy http://192.168.0.99:9766 -w S01E01 ATVP umc.cmc.1nfdfd5zlk05fo1bwwetzldy3 +``` + +Above command: + - gets english subtitles + audio, + - lists all possible qualities, + - selects 720p video track, + - uses the proxy for licensing, + - gets the first episode of first season (i.e S01E01) + - of the title umc.cmc.1nfdfd5zlk05fo1bwwetzldy3 + + +## Proxy +I recommend [Windscribe](https://windscribe.com/). You can sign up, getting 10 GB of traffic credit every month for free. We use the VPN for everything except downloading video/audio. +Tested so far on Amazon, AppleTVPlus, Max. + +### Steps: +1. For each service, within get_tracks() function we do this below. + ```python + for track in tracks: + track.needs_proxy = False + ``` + + This flag signals that this track does not need a proxy and a proxy will not be passed to downloader even if proxy given in CLI options. + +2. Download Windscribe app and install it. + +3. Go to `Options` -> `Connection` -> `Split Tunneling`. Enable it. + + Set `Mode` as `Inclusive`. + +5. Go to `Options` -> `Connection` -> `Proxy Gateway`. Enable it. Select `Proxy Type` as `HTTP`. + + Copy the `IP` field (will look something like `192.168.0.141:9766`) + + Pass above copied to Vinetrimmer with the proxy flag like below. + + ```bash + ...(other flags)... --proxy http://192.168.0.141:9766 ....... + ``` + +## Other + - For `--keys` to work with ATVP you need to pass the `--no-subs` flag also + - Nuikta compile is an option to run on various linux distributions. + - Errors arise when running VT within Docker or Conda like python distributions. Make sure to use proper python3. + - To use programs in `scripts` folder, first activate venv then, then - + ```bash + poetry run python scripts/ParseKeybox.py + ``` diff --git a/assets/icon.ico b/assets/icon.ico new file mode 100644 index 0000000..fdb0d4a Binary files /dev/null and b/assets/icon.ico differ diff --git a/binaries/N_m3u8DL-RE-samplefile.txt b/binaries/N_m3u8DL-RE-samplefile.txt new file mode 100644 index 0000000..ccbed25 --- /dev/null +++ b/binaries/N_m3u8DL-RE-samplefile.txt @@ -0,0 +1 @@ +N_m3u8DL-RE.exe http://avodsls3ww-s.akamaihd.net/ondemand/iad_2/c5a2/7992/6e31/4ed5-8011-893c8d4e98a6/0bc9f599-85c7-450d-b829-b69fb27d4bd6.ism/manifest --thread-count 96 --log-level ERROR --write-meta-json False --http-request-timeout 8 \ No newline at end of file diff --git a/binaries/N_m3u8DL-RE.exe b/binaries/N_m3u8DL-RE.exe new file mode 100644 index 0000000..ca7e57b Binary files /dev/null and b/binaries/N_m3u8DL-RE.exe differ diff --git a/binaries/XstreamDL-CLI.zip b/binaries/XstreamDL-CLI.zip new file mode 100644 index 0000000..d306890 Binary files /dev/null and b/binaries/XstreamDL-CLI.zip differ diff --git a/binaries/aria2c.exe b/binaries/aria2c.exe new file mode 100644 index 0000000..5004e10 Binary files /dev/null and b/binaries/aria2c.exe differ diff --git a/binaries/avcodec-57.dll b/binaries/avcodec-57.dll new file mode 100644 index 0000000..bc90a8d Binary files /dev/null and b/binaries/avcodec-57.dll differ diff --git a/binaries/avformat-57.dll b/binaries/avformat-57.dll new file mode 100644 index 0000000..5dcc389 Binary files /dev/null and b/binaries/avformat-57.dll differ diff --git a/binaries/avutil-55.dll b/binaries/avutil-55.dll new file mode 100644 index 0000000..1ea9f12 Binary files /dev/null and b/binaries/avutil-55.dll differ diff --git a/binaries/ccextractor.exe b/binaries/ccextractor.exe new file mode 100644 index 0000000..01c40a3 Binary files /dev/null and b/binaries/ccextractor.exe differ diff --git a/binaries/ccxgui.exe b/binaries/ccxgui.exe new file mode 100644 index 0000000..111823d Binary files /dev/null and b/binaries/ccxgui.exe differ diff --git a/binaries/curl.exe b/binaries/curl.exe new file mode 100644 index 0000000..b98410d Binary files /dev/null and b/binaries/curl.exe differ diff --git a/binaries/data/flutter_assets/AssetManifest.json b/binaries/data/flutter_assets/AssetManifest.json new file mode 100644 index 0000000..db3f701 --- /dev/null +++ b/binaries/data/flutter_assets/AssetManifest.json @@ -0,0 +1 @@ +{"assets/ccextractor":["assets/ccextractor"],"assets/ccx.svg":["assets/ccx.svg"],"packages/cupertino_icons/assets/CupertinoIcons.ttf":["packages/cupertino_icons/assets/CupertinoIcons.ttf"]} \ No newline at end of file diff --git a/binaries/data/flutter_assets/FontManifest.json b/binaries/data/flutter_assets/FontManifest.json new file mode 100644 index 0000000..464ab58 --- /dev/null +++ b/binaries/data/flutter_assets/FontManifest.json @@ -0,0 +1 @@ +[{"family":"MaterialIcons","fonts":[{"asset":"fonts/MaterialIcons-Regular.otf"}]},{"family":"packages/cupertino_icons/CupertinoIcons","fonts":[{"asset":"packages/cupertino_icons/assets/CupertinoIcons.ttf"}]}] \ No newline at end of file diff --git a/binaries/data/flutter_assets/NOTICES.Z b/binaries/data/flutter_assets/NOTICES.Z new file mode 100644 index 0000000..61c8789 Binary files /dev/null and b/binaries/data/flutter_assets/NOTICES.Z differ diff --git a/binaries/data/flutter_assets/assets/ccextractor b/binaries/data/flutter_assets/assets/ccextractor new file mode 100644 index 0000000..02c4036 Binary files /dev/null and b/binaries/data/flutter_assets/assets/ccextractor differ diff --git a/binaries/data/flutter_assets/assets/ccx.svg b/binaries/data/flutter_assets/assets/ccx.svg new file mode 100644 index 0000000..abac770 --- /dev/null +++ b/binaries/data/flutter_assets/assets/ccx.svg @@ -0,0 +1,167 @@ + + + + diff --git a/binaries/data/flutter_assets/fonts/MaterialIcons-Regular.otf b/binaries/data/flutter_assets/fonts/MaterialIcons-Regular.otf new file mode 100644 index 0000000..3246ad5 Binary files /dev/null and b/binaries/data/flutter_assets/fonts/MaterialIcons-Regular.otf differ diff --git a/binaries/data/flutter_assets/packages/cupertino_icons/assets/CupertinoIcons.ttf b/binaries/data/flutter_assets/packages/cupertino_icons/assets/CupertinoIcons.ttf new file mode 100644 index 0000000..79ba7ea Binary files /dev/null and b/binaries/data/flutter_assets/packages/cupertino_icons/assets/CupertinoIcons.ttf differ diff --git a/binaries/data/icudtl.dat b/binaries/data/icudtl.dat new file mode 100644 index 0000000..01ce223 Binary files /dev/null and b/binaries/data/icudtl.dat differ diff --git a/binaries/ffmpeg.exe b/binaries/ffmpeg.exe new file mode 100644 index 0000000..db2987e Binary files /dev/null and b/binaries/ffmpeg.exe differ diff --git a/binaries/ffplay.exe b/binaries/ffplay.exe new file mode 100644 index 0000000..42ad6e0 Binary files /dev/null and b/binaries/ffplay.exe differ diff --git a/binaries/ffprobe.exe b/binaries/ffprobe.exe new file mode 100644 index 0000000..2a6c4b3 Binary files /dev/null and b/binaries/ffprobe.exe differ diff --git a/binaries/file_selector_windows_plugin.dll b/binaries/file_selector_windows_plugin.dll new file mode 100644 index 0000000..2b2291f Binary files /dev/null and b/binaries/file_selector_windows_plugin.dll differ diff --git a/binaries/flutter_windows.dll b/binaries/flutter_windows.dll new file mode 100644 index 0000000..265a63c Binary files /dev/null and b/binaries/flutter_windows.dll differ diff --git a/binaries/mkvmerge.exe b/binaries/mkvmerge.exe new file mode 100644 index 0000000..feb5a11 Binary files /dev/null and b/binaries/mkvmerge.exe differ diff --git a/binaries/mp4box.exe b/binaries/mp4box.exe new file mode 100644 index 0000000..31a9fcb Binary files /dev/null and b/binaries/mp4box.exe differ diff --git a/binaries/mp4decrypt.exe b/binaries/mp4decrypt.exe new file mode 100644 index 0000000..af51068 Binary files /dev/null and b/binaries/mp4decrypt.exe differ diff --git a/binaries/mp4decrypt_1.exe b/binaries/mp4decrypt_1.exe new file mode 100644 index 0000000..4956c22 Binary files /dev/null and b/binaries/mp4decrypt_1.exe differ diff --git a/binaries/mp4dump.exe b/binaries/mp4dump.exe new file mode 100644 index 0000000..012476e Binary files /dev/null and b/binaries/mp4dump.exe differ diff --git a/binaries/msvcp140.dll b/binaries/msvcp140.dll new file mode 100644 index 0000000..21794fc Binary files /dev/null and b/binaries/msvcp140.dll differ diff --git a/binaries/mux_atmos.txt b/binaries/mux_atmos.txt new file mode 100644 index 0000000..2c67b35 --- /dev/null +++ b/binaries/mux_atmos.txt @@ -0,0 +1 @@ +ffmpeg -i correct_file.eac3 -map 0 -c:a copy correct_file.mp4 \ No newline at end of file diff --git a/binaries/packager-old.exe b/binaries/packager-old.exe new file mode 100644 index 0000000..743ad08 Binary files /dev/null and b/binaries/packager-old.exe differ diff --git a/binaries/packager.exe b/binaries/packager.exe new file mode 100644 index 0000000..d35fd8c Binary files /dev/null and b/binaries/packager.exe differ diff --git a/binaries/swresample-2.dll b/binaries/swresample-2.dll new file mode 100644 index 0000000..b79f9b8 Binary files /dev/null and b/binaries/swresample-2.dll differ diff --git a/binaries/swscale-4.dll b/binaries/swscale-4.dll new file mode 100644 index 0000000..05541cc Binary files /dev/null and b/binaries/swscale-4.dll differ diff --git a/binaries/url_launcher_windows_plugin.dll b/binaries/url_launcher_windows_plugin.dll new file mode 100644 index 0000000..8fb33e3 Binary files /dev/null and b/binaries/url_launcher_windows_plugin.dll differ diff --git a/binaries/vcruntime140.dll b/binaries/vcruntime140.dll new file mode 100644 index 0000000..c403a55 Binary files /dev/null and b/binaries/vcruntime140.dll differ diff --git a/binaries/vcruntime140_1.dll b/binaries/vcruntime140_1.dll new file mode 100644 index 0000000..f12cb6f Binary files /dev/null and b/binaries/vcruntime140_1.dll differ diff --git a/binaries/window_size_plugin.dll b/binaries/window_size_plugin.dll new file mode 100644 index 0000000..bbcb113 Binary files /dev/null and b/binaries/window_size_plugin.dll differ diff --git a/binary.txt b/binary.txt new file mode 100644 index 0000000..6a02f5a --- /dev/null +++ b/binary.txt @@ -0,0 +1,23 @@ +dl -al en -sl en --keys -q 2160 --cdm hisense_smarttv_he55a7000euwts_sl3000 -r HDR --selected -w S05E08-S05E24 AMZN -b CBR -vq UHD 0IQZZIJ6W6TT2CXPT6ZOZYX396 + + +--include-data-files=/path/to/scan=folder_name=**/*.txt +--include-data-files=/path/to/file/*.txt=folder_name/some.txt +--include-data-files="./vinetrimmer/services/*.py"=vinetrimmer/services/ + +--onefile --> if this flag then figure out how to set the directories to NOT TEMP folder + +python -m nuitka --onefile --assume-yes-for-downloads --windows-console-mode=disable --show-progress --standalone --output-dir=dist --static-libpython=no vinetrimmer1.py --include-data-dir=./vinetrimmer/=vinetrimmer --include-data-dir=./binaries/=binaries --include-data-dir=./scripts/=scripts +python -m nuitka --onefile --standalone --output-dir=dist vinetrimmer1.py --include-data-dir=./vinetrimmer/services/=vinetrimmer/services --include-data-dir=./vinetrimmer/config/=vinetrimmer/config/ --include-data-dir=./vinetrimmer/config/Services/=vinetrimmer/config/Services/ --include-data-dir=./scripts/=scripts +python -m nuitka --onefile --standalone --windows-console-mode=attach --output-dir=dist vinetrimmer1.py --include-data-dir=./vinetrimmer/=vinetrimmer/ --include-data-dir=./vinetrimmer/services/*.py=vinetrimmer/services/=**/*.py --include-data-dir=./vinetrimmer/config/=vinetrimmer/config/ --include-data-dir=./vinetrimmer/config/Services/=vinetrimmer/config/Services/ --include-data-dir=./scripts/=scripts/ +python -m nuitka --onefile --standalone --windows-console-mode=attach --output-dir=dist vinetrimmer1.py --include-data-dir=./vinetrimmer/=vinetrimmer/ --include-data-files=./vinetrimmer/services/*.py=vinetrimmer/services/ --include-data-dir=./vinetrimmer/config/=vinetrimmer/config/ --include-data-dir=./vinetrimmer/config/Services/=vinetrimmer/config/Services/ --include-data-dir=./scripts/=scripts/ +python -m nuitka --mode=standalone --output-dir=dist --windows-console-mode=force vinetrimmer1.py --include-data-dir=./vinetrimmer/=vinetrimmer/ --include-data-files="./vinetrimmer/services/*.py"=vinetrimmer/services/ --include-data-dir=./vinetrimmer/config/=vinetrimmer/config/ --include-data-dir=./vinetrimmer/config/Services/=vinetrimmer/config/Services/ --include-data-dir=./scripts/=scripts/ +python -m nuitka --onefile --follow-imports --output-dir=dist --windows-console-mode=force vinetrimmer1.py --include-data-dir=./vinetrimmer/=vinetrimmer/ --include-data-files="./vinetrimmer/services/*.py"=vinetrimmer/services/ --include-data-dir=./vinetrimmer/config/=vinetrimmer/config/ --include-data-dir=./vinetrimmer/config/Services/=vinetrimmer/config/Services/ --include-data-dir=./scripts/=scripts/ --include-data-files="./vinetrimmer/config/*.py"=vinetrimmer/config/ +python -m nuitka --onefile --follow-imports --output-dir=dist --standalone --clang --windows-console-mode=force --show-memory vinetrimmer1.py --include-data-dir=./vinetrimmer/=vinetrimmer/ --include-data-files="./vinetrimmer/services/*.py"=vinetrimmer/services/ --include-data-dir=./vinetrimmer/config/=vinetrimmer/config/ --include-data-dir=./vinetrimmer/config/Services/=vinetrimmer/config/Services/ --include-data-dir=./scripts/=scripts/ --include-data-files="./vinetrimmer/config/*.py"=vinetrimmer/config/ + +python -m nuitka --follow-imports --output-dir=dist --standalone --clang --windows-console-mode=force --show-memory vinetrimmer1.py --include-data-dir=./vinetrimmer/=vinetrimmer/ --include-data-files="./vinetrimmer/services/*.py"=vinetrimmer/services/ --include-data-dir=./vinetrimmer/config/=vinetrimmer/config/ --include-data-dir=./vinetrimmer/config/Services/=vinetrimmer/config/Services/ --include-data-dir=./scripts/=scripts/ --include-data-files="./vinetrimmer/config/*.py"=vinetrimmer/config/ +python -m nuitka --onefile --follow-imports --output-dir=dist --standalone --clang --windows-console-mode=force --show-memory vinetrimmer1.py --include-data-dir=./vinetrimmer/=vinetrimmer/ --include-data-dir=./vinetrimmer/config/=vinetrimmer/config/ --include-data-dir=./vinetrimmer/config/Services/=vinetrimmer/config/Services/ --include-data-dir=./scripts/=scripts/ --include-data-files="./vinetrimmer/config/*.py"=vinetrimmer/config/ +nuitka --output-dir=dist --standalone --windows-console-mode=force vinetrimmer1.py --include-data-dir=./vinetrimmer/=vinetrimmer/ + + +nuitka --onefile --output-dir=dist --windows-console-mode=force vt.py --include-data-dir=./vinetrimmer/=vinetrimmer/ \ No newline at end of file diff --git a/commands.txt b/commands.txt new file mode 100644 index 0000000..0567e9c --- /dev/null +++ b/commands.txt @@ -0,0 +1,36 @@ +https://www.primevideo.com/region/eu/storefront + +poetry run vt dl -al en -sl en -r HDR --list AMZN 0H7LY5ZKKBM1MIW0244WE9O2C4 +poetry run vt dl -al en -sl en --list AMZN 0H7LY5ZKKBM1MIW0244WE9O2C4 +poetry run vt dl -al en --selected --keys AMZN 0H7LY5ZKKBM1MIW0244WE9O2C4 +poetry run vt dl -al en --selected AMZN 0H7LY5ZKKBM1MIW0244WE9O2C4 + +poetry run vt dl -q 2160 -al en -sl en --list AMZN 0H7LY5ZKKBM1MIW0244WE9O2C4 +poetry run vt dl -q 2160 -al en -sl en --keys AMZN 0H7LY5ZKKBM1MIW0244WE9O2C4 --bitrate CVBR+CBR +poetry run vt dl -al en -sl en --selected AMZN -b CBR https://www.primevideo.com/detail/0I1GTXP9ZKTV7AAD7E1LCWJCUX/ + +poetry run vt dl -al en -sl en -q 2160 --keys -r HDR AMZN -b CBR 0OSAJR8S2YWRSQCYS4J8MEGEXI +poetry run vt dl -al en -sl en -q 2160 -r HDR --selected -w S05E08-S05E24 AMZN -b CBR 0IQZZIJ6W6TT2CXPT6ZOZYX396 + +python vinetrimmer1.py dl -al en -sl en -q 2160 -r HDR --selected -w S05E09-S05E24 AMZN -b CBR 0IQZZIJ6W6TT2CXPT6ZOZYX396 +poetry run vt dl -al en -sl en --selected -q 2160 -r HDR -w S01E18-S01E25 AMZN -b CBR --ism 0IQZZIJ6W6TT2CXPT6ZOZYX396 + +Atmos audio download AMZN to fix --> poetry run vt dl -al en -aa -sl en --selected --debug -w S01E01 -A AMZN -b CBR --ism 0HAQAA7JM43QWX0H6GUD3IOF70 + +http://ABHIRCQAAAAAAAAMCX3W7WLVKL54A.s3-bom-ww.cf.smooth.row.aiv-cdn.net/e5b0/2fe1/032c/4fae-b896-aca9d8bef3d4/170b36b1-856d-4c69-bbf6-feb6c979185a.ism/manifest +poetry run vt dl -al en -sl en -r HDR -w S01E01 --list -q 2160 AMZN https://www.primevideo.com/detail/0HU52DR3U1R0FGI3KSUL00XYY7 +https://www.primevideo.com/detail/0HU52DR3U1R0FGI3KSUL00XYY7/ +https://ABAKS6NAAAAAAAAMBIBDKKUP3ONNU.s3-iad-2.cf.smooth.row.aiv-cdn.net/357a/1bb0/c1f3/4a6b-b709-d6f2edf5b709/15eab8ec-d8ac-4c23-96fc-f5d89f459829.ism/manifest +http://ABHIRCQAAAAAAAAMHLTVNGLHRCITQ.s3-bom-ww.cf.smooth.row.aiv-cdn.net/e7ab/7c49/9743/4e53-ab5c-6d15516ecf15/52bf7e61-51cd-4e5d-bd68-834706f17789.ism/manifest +https://www.primevideo.com/region/eu/detail/0KYRVT4JDB957NXZO72E2MIFW5/ +https://m-5884s3.ll.smooth.row.aiv-cdn.net/iad_2/3572/bbdc/73b4/404d-a100-802b1d9de4c6/862e2506-c20e-4ba7-bacc-d6b4775e7b62.ism/manifest + +poetry run vt dl -al en -sl en -w S01E01 Max https://play.max.com/show/c8ea8e19-cae7-4683-9b62-cdbbed744784 +UHD +poetry run vt dl -al en -sl en --keys Max https://play.max.com/show/5756c2bf-36f8-4890-b1f9-ef168f1d8e9c +poetry run vt dl -al en -sl en -w S02E05-S02E10 --selected --proxy http://192.168.0.99:9766 Max +poetry run vt dl -al en -sl en --list -w S01E01 --proxy http://192.168.0.99:9766 Max + +poetry run vt dl -al all --selected --proxy http://192.168.0.99:9766 --debug -w S01E01 ATVP umc.cmc.7gvn6fekgfpq5fc72pgi1c47o +poetry run vt dl -al en -sl en --selected --debug -q 720 --proxy http://192.168.0.99:9766 -w S01E01 ATVP umc.cmc.1nfdfd5zlk05fo1bwwetzldy3 +poetry run vt dl -al en -sl en --selected --proxy http://192.168.0.99:9766 -w S01E01 ATVP umc.cmc.1nfdfd5zlk05fo1bwwetzldy3 \ No newline at end of file diff --git a/fix.txt b/fix.txt new file mode 100644 index 0000000..f06071a --- /dev/null +++ b/fix.txt @@ -0,0 +1,54 @@ +D:\PlayReady-Amazon-Tool-main>poetry run vt dl -al en -sl en --selected --keys --cdm hisense_smarttv_he55a7000euwts_sl3000 AMZN -vq UHD -b CVBR+CBR https://www.primevideo.com/detail/0I1GTXP9ZKTV7AAD7E1LCWJCUX/ +2025-02-07 22:26:57 [I] vt : vinetrimmer - Widevine DRM downloader and decrypter +2025-02-07 22:26:57 [I] vt : [Root Config] : D:\PlayReady-Amazon-Tool-main\vinetrimmer\vinetrimmer.yml +2025-02-07 22:26:57 [I] vt : [Service Configs] : D:\PlayReady-Amazon-Tool-main\vinetrimmer\Services +2025-02-07 22:26:57 [I] vt : [Cookies] : D:\PlayReady-Amazon-Tool-main\vinetrimmer\Cookies +2025-02-07 22:26:57 [I] vt : [CDM Devices] : D:\PlayReady-Amazon-Tool-main\vinetrimmer\devices +2025-02-07 22:26:57 [I] vt : [Cache] : D:\PlayReady-Amazon-Tool-main\vinetrimmer\Cache +2025-02-07 22:26:57 [I] vt : [Logs] : D:\PlayReady-Amazon-Tool-main\vinetrimmer\Logs +2025-02-07 22:26:57 [I] vt : [Temp Files] : D:\PlayReady-Amazon-Tool-main\Temp +2025-02-07 22:26:57 [I] vt : [Downloads] : D:\PlayReady-Amazon-Tool-main\Downloads +2025-02-07 22:26:57 [I] dl : + 1 Local Vault +2025-02-07 22:26:57 [I] dl : + 0 Remote Vaults +2025-02-07 22:26:57 [I] dl : + Loaded Device: hisense_smarttv_he55a7000euwts_sl3000 (L3000) +2025-02-07 22:26:57 [I] AMZN : Getting Account Region +2025-02-07 22:26:59 [I] AMZN : + Region: us +2025-02-07 22:26:59 [I] AMZN : + Using cached device bearer +2025-02-07 22:26:59 [I] AMZN : Retrieving Titles +2025-02-07 22:27:00 [I] Titles : Title: I Was Not Ready Da +2025-02-07 22:27:00 [I] AMZN : Getting tracks for I Was Not Ready Da (2020) [amzn1.dv.gti.30baee18-aa4c-1fc2-72cc-6e11d5e627d9] +2025-02-07 22:27:01 [I] AMZN : + Detected encodingVersion=2 +2025-02-07 22:27:01 [I] AMZN : + Downloading CVBR MPD +2025-02-07 22:27:02 [I] AMZN : + Detected encodingVersion=2 +2025-02-07 22:27:02 [I] AMZN : + Downloading CBR MPD +Traceback (most recent call last): + File "", line 1, in + File "D:\PlayReady-Amazon-Tool-main\.venv\lib\site-packages\click\core.py", line 1161, in __call__ + return self.main(*args, **kwargs) + File "D:\PlayReady-Amazon-Tool-main\.venv\lib\site-packages\click\core.py", line 1082, in main + rv = self.invoke(ctx) + File "D:\PlayReady-Amazon-Tool-main\.venv\lib\site-packages\click\core.py", line 1443, in invoke + return ctx.invoke(self.callback, **ctx.params) + File "D:\PlayReady-Amazon-Tool-main\.venv\lib\site-packages\click\core.py", line 788, in invoke + return __callback(*args, **kwargs) + File "D:\PlayReady-Amazon-Tool-main\vinetrimmer\vinetrimmer.py", line 72, in main + dl() + File "D:\PlayReady-Amazon-Tool-main\.venv\lib\site-packages\click\core.py", line 1161, in __call__ + return self.main(*args, **kwargs) + File "D:\PlayReady-Amazon-Tool-main\.venv\lib\site-packages\click\core.py", line 1082, in main + rv = self.invoke(ctx) + File "D:\PlayReady-Amazon-Tool-main\.venv\lib\site-packages\click\core.py", line 1697, in invoke + return _process_result(sub_ctx.command.invoke(sub_ctx)) + File "D:\PlayReady-Amazon-Tool-main\.venv\lib\site-packages\click\core.py", line 1666, in _process_result + value = ctx.invoke(self._result_callback, value, **ctx.params) + File "D:\PlayReady-Amazon-Tool-main\.venv\lib\site-packages\click\core.py", line 788, in invoke + return __callback(*args, **kwargs) + File "D:\PlayReady-Amazon-Tool-main\.venv\lib\site-packages\click\decorators.py", line 33, in new_func + return f(get_current_context(), *args, **kwargs) + File "D:\PlayReady-Amazon-Tool-main\vinetrimmer\commands\dl.py", line 309, in result + title.tracks.add(service.get_tracks(title), warn_only=True) + File "D:\PlayReady-Amazon-Tool-main\vinetrimmer\services\amazon.py", line 321, in get_tracks + manifest, chosen_manifest, tracks = self.get_best_quality(title) + File "D:\PlayReady-Amazon-Tool-main\vinetrimmer\services\amazon.py", line 1051, in get_best_quality + best_quality = max(track_list, key=lambda x: x['max_size']) +TypeError: '>' not supported between instances of 'NoneType' and 'NoneType' \ No newline at end of file diff --git a/help.bat b/help.bat new file mode 100644 index 0000000..79a36fd --- /dev/null +++ b/help.bat @@ -0,0 +1,3 @@ +@echo off +poetry run vt dl -h +pause diff --git a/install.bat b/install.bat new file mode 100644 index 0000000..4a8fcab --- /dev/null +++ b/install.bat @@ -0,0 +1,6 @@ +@echo off +python -m pip install poetry==1.8.5 +poetry config virtualenvs.in-project true +poetry lock --no-update +poetry install +pause \ No newline at end of file diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..6a28fa8 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,2732 @@ +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. + +[[package]] +name = "altgraph" +version = "0.17.4" +description = "Python graph (network) package" +optional = false +python-versions = "*" +files = [ + {file = "altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"}, + {file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"}, +] + +[[package]] +name = "anyio" +version = "4.5.2" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, + {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = "*" +files = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] + +[[package]] +name = "beautifulsoup4" +version = "4.8.2" +description = "Screen-scraping library" +optional = false +python-versions = "*" +files = [ + {file = "beautifulsoup4-4.8.2-py2-none-any.whl", hash = "sha256:e1505eeed31b0f4ce2dbb3bc8eb256c04cc2b3b72af7d551a4ab6efd5cbe5dae"}, + {file = "beautifulsoup4-4.8.2-py3-none-any.whl", hash = "sha256:9fbb4d6e48ecd30bcacc5b63b94088192dcda178513b2ae3c394229f8911b887"}, + {file = "beautifulsoup4-4.8.2.tar.gz", hash = "sha256:05fd825eb01c290877657a56df4c6e4c311b3965bda790c613a3d6fb01a5462a"}, +] + +[package.dependencies] +soupsieve = ">=1.2" + +[package.extras] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "brotli" +version = "1.1.0" +description = "Python bindings for the Brotli compression library" +optional = false +python-versions = "*" +files = [ + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"}, + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec"}, + {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, + {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b"}, + {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, + {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839"}, + {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, + {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, + {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5"}, + {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7"}, + {file = "Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0"}, + {file = "Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b"}, + {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52"}, + {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"}, + {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"}, + {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c"}, + {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"}, + {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a"}, + {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, + {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb"}, + {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, + {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, + {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, +] + +[[package]] +name = "brotlicffi" +version = "1.1.0.0" +description = "Python CFFI bindings to the Brotli library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:246f1d1a90279bb6069de3de8d75a8856e073b8ff0b09dcca18ccc14cec85979"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc4bc5d82bc56ebd8b514fb8350cfac4627d6b0743382e46d033976a5f80fab6"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c26ecb14386a44b118ce36e546ce307f4810bc9598a6e6cb4f7fca725ae7e6"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca72968ae4eaf6470498d5c2887073f7efe3b1e7d7ec8be11a06a79cc810e990"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:add0de5b9ad9e9aa293c3aa4e9deb2b61e99ad6c1634e01d01d98c03e6a354cc"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b6068e0f3769992d6b622a1cd2e7835eae3cf8d9da123d7f51ca9c1e9c333e5"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8557a8559509b61e65083f8782329188a250102372576093c88930c875a69838"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a7ae37e5d79c5bdfb5b4b99f2715a6035e6c5bf538c3746abc8e26694f92f33"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391151ec86bb1c683835980f4816272a87eaddc46bb91cbf44f62228b84d8cca"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2f3711be9290f0453de8eed5275d93d286abe26b08ab4a35d7452caa1fef532f"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a807d760763e398bbf2c6394ae9da5815901aa93ee0a37bca5efe78d4ee3171"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa8ca0623b26c94fccc3a1fdd895be1743b838f3917300506d04aa3346fd2a14"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3de0cf28a53a3238b252aca9fed1593e9d36c1d116748013339f0949bfc84112"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6be5ec0e88a4925c91f3dea2bb0013b3a2accda6f77238f76a34a1ea532a1cb0"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d9eb71bb1085d996244439154387266fd23d6ad37161f6f52f1cd41dd95a3808"}, + {file = "brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13"}, +] + +[package.dependencies] +cffi = ">=1.0.0" + +[[package]] +name = "build" +version = "1.2.2.post1" +description = "A simple, correct Python build frontend" +optional = false +python-versions = ">=3.8" +files = [ + {file = "build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5"}, + {file = "build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "os_name == \"nt\""} +importlib-metadata = {version = ">=4.6", markers = "python_full_version < \"3.10.2\""} +packaging = ">=19.1" +pyproject_hooks = "*" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["furo (>=2023.08.17)", "sphinx (>=7.0,<8.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)", "sphinx-issues (>=3.0.0)"] +test = ["build[uv,virtualenv]", "filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "setuptools (>=56.0.0)", "setuptools (>=56.0.0)", "setuptools (>=67.8.0)", "wheel (>=0.36.0)"] +typing = ["build[uv]", "importlib-metadata (>=5.1)", "mypy (>=1.9.0,<1.10.0)", "tomli", "typing-extensions (>=3.7.4.3)"] +uv = ["uv (>=0.1.18)"] +virtualenv = ["virtualenv (>=20.0.35)"] + +[[package]] +name = "cachecontrol" +version = "0.14.2" +description = "httplib2 caching for requests" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cachecontrol-0.14.2-py3-none-any.whl", hash = "sha256:ebad2091bf12d0d200dfc2464330db638c5deb41d546f6d7aca079e87290f3b0"}, + {file = "cachecontrol-0.14.2.tar.gz", hash = "sha256:7d47d19f866409b98ff6025b6a0fca8e4c791fb31abbd95f622093894ce903a2"}, +] + +[package.dependencies] +filelock = {version = ">=3.8.0", optional = true, markers = "extra == \"filecache\""} +msgpack = ">=0.5.2,<2.0.0" +requests = ">=2.16.0" + +[package.extras] +dev = ["CacheControl[filecache,redis]", "build", "cherrypy", "codespell[tomli]", "furo", "mypy", "pytest", "pytest-cov", "ruff", "sphinx", "sphinx-copybutton", "tox", "types-redis", "types-requests"] +filecache = ["filelock (>=3.8.0)"] +redis = ["redis (>=2.10.5)"] + +[[package]] +name = "certifi" +version = "2024.12.14" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +files = [ + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, +] + +[[package]] +name = "cleo" +version = "2.1.0" +description = "Cleo allows you to create beautiful and testable command-line interfaces." +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "cleo-2.1.0-py3-none-any.whl", hash = "sha256:4a31bd4dd45695a64ee3c4758f583f134267c2bc518d8ae9a29cf237d009b07e"}, + {file = "cleo-2.1.0.tar.gz", hash = "sha256:0b2c880b5d13660a7ea651001fb4acb527696c01f15c9ee650f377aa543fd523"}, +] + +[package.dependencies] +crashtest = ">=0.4.1,<0.5.0" +rapidfuzz = ">=3.0.0,<4.0.0" + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coloredlogs" +version = "15.0.1" +description = "Colored terminal output for Python's logging module" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, + {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, +] + +[package.dependencies] +humanfriendly = ">=9.1" + +[package.extras] +cron = ["capturer (>=2.4)"] + +[[package]] +name = "construct" +version = "2.8.8" +description = "A powerful declarative parser/builder for binary data" +optional = false +python-versions = "*" +files = [ + {file = "construct-2.8.8.tar.gz", hash = "sha256:1b84b8147f6fd15bcf64b737c3e8ac5100811ad80c830cb4b2545140511c4157"}, +] + +[[package]] +name = "crashtest" +version = "0.4.1" +description = "Manage Python errors with ease" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "crashtest-0.4.1-py3-none-any.whl", hash = "sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5"}, + {file = "crashtest-0.4.1.tar.gz", hash = "sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce"}, +] + +[[package]] +name = "crccheck" +version = "1.3.0" +description = "Calculation library for CRCs and checksums" +optional = false +python-versions = "*" +files = [ + {file = "crccheck-1.3.0-py3-none-any.whl", hash = "sha256:278ec53d6f417f197f7e0e29b485093d4879b0bc7a2d29b657ef8242e633b48d"}, + {file = "crccheck-1.3.0.tar.gz", hash = "sha256:5384f437de610ade5c3d8689efc80ccd1267b8c452ade83411fd8500a1024f3e"}, +] + +[[package]] +name = "cryptography" +version = "43.0.3" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, + {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, + {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, + {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, + {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, + {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, + {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "cssutils" +version = "2.11.1" +description = "A CSS Cascading Style Sheets library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cssutils-2.11.1-py3-none-any.whl", hash = "sha256:a67bfdfdff4f3867fab43698ec4897c1a828eca5973f4073321b3bccaf1199b1"}, + {file = "cssutils-2.11.1.tar.gz", hash = "sha256:0563a76513b6af6eebbe788c3bf3d01c920e46b3f90c8416738c5cfc773ff8e2"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["cssselect", "importlib-resources", "jaraco.test (>=5.1)", "lxml", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "distlib" +version = "0.3.9" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + +[[package]] +name = "dulwich" +version = "0.21.7" +description = "Python Git Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "dulwich-0.21.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d4c0110798099bb7d36a110090f2688050703065448895c4f53ade808d889dd3"}, + {file = "dulwich-0.21.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2bc12697f0918bee324c18836053644035362bb3983dc1b210318f2fed1d7132"}, + {file = "dulwich-0.21.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:471305af74790827fcbafe330fc2e8bdcee4fb56ca1177c8c481b1c8f806c4a4"}, + {file = "dulwich-0.21.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d54c9d0e845be26f65f954dff13a1cd3f2b9739820c19064257b8fd7435ab263"}, + {file = "dulwich-0.21.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12d61334a575474e707614f2e93d6ed4cdae9eb47214f9277076d9e5615171d3"}, + {file = "dulwich-0.21.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e274cebaf345f0b1e3b70197f2651de92b652386b68020cfd3bf61bc30f6eaaa"}, + {file = "dulwich-0.21.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:817822f970e196e757ae01281ecbf21369383285b9f4a83496312204cf889b8c"}, + {file = "dulwich-0.21.7-cp310-cp310-win32.whl", hash = "sha256:7836da3f4110ce684dcd53489015fb7fa94ed33c5276e3318b8b1cbcb5b71e08"}, + {file = "dulwich-0.21.7-cp310-cp310-win_amd64.whl", hash = "sha256:4a043b90958cec866b4edc6aef5fe3c2c96a664d0b357e1682a46f6c477273c4"}, + {file = "dulwich-0.21.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ce8db196e79c1f381469410d26fb1d8b89c6b87a4e7f00ff418c22a35121405c"}, + {file = "dulwich-0.21.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:62bfb26bdce869cd40be443dfd93143caea7089b165d2dcc33de40f6ac9d812a"}, + {file = "dulwich-0.21.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c01a735b9a171dcb634a97a3cec1b174cfbfa8e840156870384b633da0460f18"}, + {file = "dulwich-0.21.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa4d14767cf7a49c9231c2e52cb2a3e90d0c83f843eb6a2ca2b5d81d254cf6b9"}, + {file = "dulwich-0.21.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bca4b86e96d6ef18c5bc39828ea349efb5be2f9b1f6ac9863f90589bac1084d"}, + {file = "dulwich-0.21.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7b5624b02ef808cdc62dabd47eb10cd4ac15e8ac6df9e2e88b6ac6b40133673"}, + {file = "dulwich-0.21.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c3a539b4696a42fbdb7412cb7b66a4d4d332761299d3613d90a642923c7560e1"}, + {file = "dulwich-0.21.7-cp311-cp311-win32.whl", hash = "sha256:675a612ce913081beb0f37b286891e795d905691dfccfb9bf73721dca6757cde"}, + {file = "dulwich-0.21.7-cp311-cp311-win_amd64.whl", hash = "sha256:460ba74bdb19f8d498786ae7776745875059b1178066208c0fd509792d7f7bfc"}, + {file = "dulwich-0.21.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4c51058ec4c0b45dc5189225b9e0c671b96ca9713c1daf71d622c13b0ab07681"}, + {file = "dulwich-0.21.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4bc4c5366eaf26dda3fdffe160a3b515666ed27c2419f1d483da285ac1411de0"}, + {file = "dulwich-0.21.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a0650ec77d89cb947e3e4bbd4841c96f74e52b4650830112c3057a8ca891dc2f"}, + {file = "dulwich-0.21.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f18f0a311fb7734b033a3101292b932158cade54b74d1c44db519e42825e5a2"}, + {file = "dulwich-0.21.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c589468e5c0cd84e97eb7ec209ab005a2cb69399e8c5861c3edfe38989ac3a8"}, + {file = "dulwich-0.21.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d62446797163317a397a10080c6397ffaaca51a7804c0120b334f8165736c56a"}, + {file = "dulwich-0.21.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e84cc606b1f581733df4350ca4070e6a8b30be3662bbb81a590b177d0c996c91"}, + {file = "dulwich-0.21.7-cp312-cp312-win32.whl", hash = "sha256:c3d1685f320907a52c40fd5890627945c51f3a5fa4bcfe10edb24fec79caadec"}, + {file = "dulwich-0.21.7-cp312-cp312-win_amd64.whl", hash = "sha256:6bd69921fdd813b7469a3c77bc75c1783cc1d8d72ab15a406598e5a3ba1a1503"}, + {file = "dulwich-0.21.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7d8ab29c660125db52106775caa1f8f7f77a69ed1fe8bc4b42bdf115731a25bf"}, + {file = "dulwich-0.21.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0d2e4485b98695bf95350ce9d38b1bb0aaac2c34ad00a0df789aa33c934469b"}, + {file = "dulwich-0.21.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e138d516baa6b5bafbe8f030eccc544d0d486d6819b82387fc0e285e62ef5261"}, + {file = "dulwich-0.21.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f34bf9b9fa9308376263fd9ac43143c7c09da9bc75037bb75c6c2423a151b92c"}, + {file = "dulwich-0.21.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2e2c66888207b71cd1daa2acb06d3984a6bc13787b837397a64117aa9fc5936a"}, + {file = "dulwich-0.21.7-cp37-cp37m-win32.whl", hash = "sha256:10893105c6566fc95bc2a67b61df7cc1e8f9126d02a1df6a8b2b82eb59db8ab9"}, + {file = "dulwich-0.21.7-cp37-cp37m-win_amd64.whl", hash = "sha256:460b3849d5c3d3818a80743b4f7a0094c893c559f678e56a02fff570b49a644a"}, + {file = "dulwich-0.21.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:74700e4c7d532877355743336c36f51b414d01e92ba7d304c4f8d9a5946dbc81"}, + {file = "dulwich-0.21.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c92e72c43c9e9e936b01a57167e0ea77d3fd2d82416edf9489faa87278a1cdf7"}, + {file = "dulwich-0.21.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d097e963eb6b9fa53266146471531ad9c6765bf390849230311514546ed64db2"}, + {file = "dulwich-0.21.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:808e8b9cc0aa9ac74870b49db4f9f39a52fb61694573f84b9c0613c928d4caf8"}, + {file = "dulwich-0.21.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1957b65f96e36c301e419d7adaadcff47647c30eb072468901bb683b1000bc5"}, + {file = "dulwich-0.21.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4b09bc3a64fb70132ec14326ecbe6e0555381108caff3496898962c4136a48c6"}, + {file = "dulwich-0.21.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5882e70b74ac3c736a42d3fdd4f5f2e6570637f59ad5d3e684760290b58f041"}, + {file = "dulwich-0.21.7-cp38-cp38-win32.whl", hash = "sha256:29bb5c1d70eba155ded41ed8a62be2f72edbb3c77b08f65b89c03976292f6d1b"}, + {file = "dulwich-0.21.7-cp38-cp38-win_amd64.whl", hash = "sha256:25c3ab8fb2e201ad2031ddd32e4c68b7c03cb34b24a5ff477b7a7dcef86372f5"}, + {file = "dulwich-0.21.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8929c37986c83deb4eb500c766ee28b6670285b512402647ee02a857320e377c"}, + {file = "dulwich-0.21.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc1e11be527ac06316539b57a7688bcb1b6a3e53933bc2f844397bc50734e9ae"}, + {file = "dulwich-0.21.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0fc3078a1ba04c588fabb0969d3530efd5cd1ce2cf248eefb6baf7cbc15fc285"}, + {file = "dulwich-0.21.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dcbd29ba30ba2c5bfbab07a61a5f20095541d5ac66d813056c122244df4ac0"}, + {file = "dulwich-0.21.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8869fc8ec3dda743e03d06d698ad489b3705775fe62825e00fa95aa158097fc0"}, + {file = "dulwich-0.21.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d96ca5e0dde49376fbcb44f10eddb6c30284a87bd03bb577c59bb0a1f63903fa"}, + {file = "dulwich-0.21.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e0064363bd5e814359657ae32517fa8001e8573d9d040bd997908d488ab886ed"}, + {file = "dulwich-0.21.7-cp39-cp39-win32.whl", hash = "sha256:869eb7be48243e695673b07905d18b73d1054a85e1f6e298fe63ba2843bb2ca1"}, + {file = "dulwich-0.21.7-cp39-cp39-win_amd64.whl", hash = "sha256:404b8edeb3c3a86c47c0a498699fc064c93fa1f8bab2ffe919e8ab03eafaaad3"}, + {file = "dulwich-0.21.7-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e598d743c6c0548ebcd2baf94aa9c8bfacb787ea671eeeb5828cfbd7d56b552f"}, + {file = "dulwich-0.21.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a2d76c96426e791556836ef43542b639def81be4f1d6d4322cd886c115eae1"}, + {file = "dulwich-0.21.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6c88acb60a1f4d31bd6d13bfba465853b3df940ee4a0f2a3d6c7a0778c705b7"}, + {file = "dulwich-0.21.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ecd315847dea406a4decfa39d388a2521e4e31acde3bd9c2609c989e817c6d62"}, + {file = "dulwich-0.21.7-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d05d3c781bc74e2c2a2a8f4e4e2ed693540fbe88e6ac36df81deac574a6dad99"}, + {file = "dulwich-0.21.7-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6de6f8de4a453fdbae8062a6faa652255d22a3d8bce0cd6d2d6701305c75f2b3"}, + {file = "dulwich-0.21.7-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e25953c7acbbe4e19650d0225af1c0c0e6882f8bddd2056f75c1cc2b109b88ad"}, + {file = "dulwich-0.21.7-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:4637cbd8ed1012f67e1068aaed19fcc8b649bcf3e9e26649826a303298c89b9d"}, + {file = "dulwich-0.21.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:858842b30ad6486aacaa607d60bab9c9a29e7c59dc2d9cb77ae5a94053878c08"}, + {file = "dulwich-0.21.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:739b191f61e1c4ce18ac7d520e7a7cbda00e182c3489552408237200ce8411ad"}, + {file = "dulwich-0.21.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:274c18ec3599a92a9b67abaf110e4f181a4f779ee1aaab9e23a72e89d71b2bd9"}, + {file = "dulwich-0.21.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2590e9b431efa94fc356ae33b38f5e64f1834ec3a94a6ac3a64283b206d07aa3"}, + {file = "dulwich-0.21.7-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed60d1f610ef6437586f7768254c2a93820ccbd4cfdac7d182cf2d6e615969bb"}, + {file = "dulwich-0.21.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8278835e168dd097089f9e53088c7a69c6ca0841aef580d9603eafe9aea8c358"}, + {file = "dulwich-0.21.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffc27fb063f740712e02b4d2f826aee8bbed737ed799962fef625e2ce56e2d29"}, + {file = "dulwich-0.21.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:61e3451bd3d3844f2dca53f131982553be4d1b1e1ebd9db701843dd76c4dba31"}, + {file = "dulwich-0.21.7.tar.gz", hash = "sha256:a9e9c66833cea580c3ac12927e4b9711985d76afca98da971405d414de60e968"}, +] + +[package.dependencies] +urllib3 = ">=1.25" + +[package.extras] +fastimport = ["fastimport"] +https = ["urllib3 (>=1.24.1)"] +paramiko = ["paramiko"] +pgp = ["gpg"] + +[[package]] +name = "ecpy" +version = "1.2.5" +description = "Pure Pyhton Elliptic Curve Library" +optional = false +python-versions = "*" +files = [ + {file = "ECPy-1.2.5-py3-none-any.whl", hash = "sha256:559c92e42406d9d1a6b2b8fc26e6ad7bc985f33903b72f426a56cb1073a25ce3"}, + {file = "ECPy-1.2.5.tar.gz", hash = "sha256:9635cffb9b6ecf7fd7f72aea1665829ac74a1d272006d0057d45a621aae20228"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fastjsonschema" +version = "2.21.1" +description = "Fastest Python implementation of JSON schema" +optional = false +python-versions = "*" +files = [ + {file = "fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667"}, + {file = "fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4"}, +] + +[package.extras] +devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] + +[[package]] +name = "filelock" +version = "3.16.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +typing = ["typing-extensions (>=4.12.2)"] + +[[package]] +name = "flake8" +version = "3.9.2" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, + {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, +] + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.7.0,<2.8.0" +pyflakes = ">=2.3.0,<2.4.0" + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "0.16.3" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.7" +files = [ + {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, + {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, +] + +[package.dependencies] +anyio = ">=3.0,<5.0" +certifi = "*" +h11 = ">=0.13,<0.15" +sniffio = "==1.*" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "httpx" +version = "0.23.3" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.7" +files = [ + {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, + {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, +] + +[package.dependencies] +certifi = "*" +httpcore = ">=0.15.0,<0.17.0" +rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<13)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "humanfriendly" +version = "10.0" +description = "Human friendly output for text interfaces using Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, + {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, +] + +[package.dependencies] +pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] + +[[package]] +name = "importlib-resources" +version = "6.4.5" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717"}, + {file = "importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065"}, +] + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] +type = ["pytest-mypy"] + +[[package]] +name = "installer" +version = "0.7.0" +description = "A library for installing Python wheels." +optional = false +python-versions = ">=3.7" +files = [ + {file = "installer-0.7.0-py3-none-any.whl", hash = "sha256:05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53"}, + {file = "installer-0.7.0.tar.gz", hash = "sha256:a26d3e3116289bb08216e0d0f7d925fcef0b0194eedfa0c944bcaaa106c4b631"}, +] + +[[package]] +name = "iso8601" +version = "2.1.0" +description = "Simple module to parse ISO 8601 dates" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242"}, + {file = "iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df"}, +] + +[[package]] +name = "isodate" +version = "0.6.1" +description = "An ISO 8601 date/time/duration parser and formatter" +optional = false +python-versions = "*" +files = [ + {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"}, + {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.extras] +colors = ["colorama (>=0.4.6)"] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +description = "Utility functions for Python class constructs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, + {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jeepney" +version = "0.8.0" +description = "Low-level, pure Python DBus protocol wrapper." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, + {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, +] + +[package.extras] +test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["async_generator", "trio"] + +[[package]] +name = "jsonpickle" +version = "2.2.0" +description = "Python library for serializing any arbitrary object graph into JSON" +optional = false +python-versions = ">=2.7" +files = [ + {file = "jsonpickle-2.2.0-py2.py3-none-any.whl", hash = "sha256:de7f2613818aa4f234138ca11243d6359ff83ae528b2185efdd474f62bcf9ae1"}, + {file = "jsonpickle-2.2.0.tar.gz", hash = "sha256:7b272918b0554182e53dc340ddd62d9b7f902fec7e7b05620c04f3ccef479a0e"}, +] + +[package.extras] +docs = ["jaraco.packaging (>=3.2)", "rst.linker (>=1.9)", "sphinx"] +testing = ["ecdsa", "enum34", "feedparser", "jsonlib", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-black-multipy", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-flake8 (<1.1.0)", "pytest-flake8 (>=1.1.1)", "scikit-learn", "sqlalchemy"] +testing-libs = ["simplejson", "ujson", "yajl"] + +[[package]] +name = "keyring" +version = "24.3.1" +description = "Store and access your passwords safely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "keyring-24.3.1-py3-none-any.whl", hash = "sha256:df38a4d7419a6a60fea5cef1e45a948a3e8430dd12ad88b0f423c5c143906218"}, + {file = "keyring-24.3.1.tar.gz", hash = "sha256:c3327b6ffafc0e8befbdb597cacdb4928ffe5c1212f7645f186e6d9957a898db"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +importlib-resources = {version = "*", markers = "python_version < \"3.9\""} +"jaraco.classes" = "*" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +completion = ["shtab (>=1.1.0)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "langcodes" +version = "3.4.1" +description = "Tools for labeling human languages with IETF language tags" +optional = false +python-versions = ">=3.8" +files = [ + {file = "langcodes-3.4.1-py3-none-any.whl", hash = "sha256:68f686fc3d358f222674ecf697ddcee3ace3c2fe325083ecad2543fd28a20e77"}, + {file = "langcodes-3.4.1.tar.gz", hash = "sha256:a24879fed238013ac3af2424b9d1124e38b4a38b2044fd297c8ff38e5912e718"}, +] + +[package.dependencies] +language-data = ">=1.2" + +[package.extras] +build = ["build", "twine"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "language-data" +version = "1.3.0" +description = "Supplementary data about languages used by the langcodes module" +optional = false +python-versions = "*" +files = [ + {file = "language_data-1.3.0-py3-none-any.whl", hash = "sha256:e2ee943551b5ae5f89cd0e801d1fc3835bb0ef5b7e9c3a4e8e17b2b214548fbf"}, + {file = "language_data-1.3.0.tar.gz", hash = "sha256:7600ef8aa39555145d06c89f0c324bf7dab834ea0b0a439d8243762e3ebad7ec"}, +] + +[package.dependencies] +marisa-trie = ">=1.1.0" + +[package.extras] +build = ["build", "twine"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "lxml" +version = "4.9.4" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +files = [ + {file = "lxml-4.9.4-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e214025e23db238805a600f1f37bf9f9a15413c7bf5f9d6ae194f84980c78722"}, + {file = "lxml-4.9.4-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ec53a09aee61d45e7dbe7e91252ff0491b6b5fee3d85b2d45b173d8ab453efc1"}, + {file = "lxml-4.9.4-cp27-cp27m-win32.whl", hash = "sha256:7d1d6c9e74c70ddf524e3c09d9dc0522aba9370708c2cb58680ea40174800013"}, + {file = "lxml-4.9.4-cp27-cp27m-win_amd64.whl", hash = "sha256:cb53669442895763e61df5c995f0e8361b61662f26c1b04ee82899c2789c8f69"}, + {file = "lxml-4.9.4-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:647bfe88b1997d7ae8d45dabc7c868d8cb0c8412a6e730a7651050b8c7289cf2"}, + {file = "lxml-4.9.4-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4d973729ce04784906a19108054e1fd476bc85279a403ea1a72fdb051c76fa48"}, + {file = "lxml-4.9.4-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:056a17eaaf3da87a05523472ae84246f87ac2f29a53306466c22e60282e54ff8"}, + {file = "lxml-4.9.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:aaa5c173a26960fe67daa69aa93d6d6a1cd714a6eb13802d4e4bd1d24a530644"}, + {file = "lxml-4.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:647459b23594f370c1c01768edaa0ba0959afc39caeeb793b43158bb9bb6a663"}, + {file = "lxml-4.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bdd9abccd0927673cffe601d2c6cdad1c9321bf3437a2f507d6b037ef91ea307"}, + {file = "lxml-4.9.4-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:00e91573183ad273e242db5585b52670eddf92bacad095ce25c1e682da14ed91"}, + {file = "lxml-4.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a602ed9bd2c7d85bd58592c28e101bd9ff9c718fbde06545a70945ffd5d11868"}, + {file = "lxml-4.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:de362ac8bc962408ad8fae28f3967ce1a262b5d63ab8cefb42662566737f1dc7"}, + {file = "lxml-4.9.4-cp310-cp310-win32.whl", hash = "sha256:33714fcf5af4ff7e70a49731a7cc8fd9ce910b9ac194f66eaa18c3cc0a4c02be"}, + {file = "lxml-4.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:d3caa09e613ece43ac292fbed513a4bce170681a447d25ffcbc1b647d45a39c5"}, + {file = "lxml-4.9.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:359a8b09d712df27849e0bcb62c6a3404e780b274b0b7e4c39a88826d1926c28"}, + {file = "lxml-4.9.4-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:43498ea734ccdfb92e1886dfedaebeb81178a241d39a79d5351ba2b671bff2b2"}, + {file = "lxml-4.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4855161013dfb2b762e02b3f4d4a21cc7c6aec13c69e3bffbf5022b3e708dd97"}, + {file = "lxml-4.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c71b5b860c5215fdbaa56f715bc218e45a98477f816b46cfde4a84d25b13274e"}, + {file = "lxml-4.9.4-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9a2b5915c333e4364367140443b59f09feae42184459b913f0f41b9fed55794a"}, + {file = "lxml-4.9.4-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d82411dbf4d3127b6cde7da0f9373e37ad3a43e89ef374965465928f01c2b979"}, + {file = "lxml-4.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:273473d34462ae6e97c0f4e517bd1bf9588aa67a1d47d93f760a1282640e24ac"}, + {file = "lxml-4.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:389d2b2e543b27962990ab529ac6720c3dded588cc6d0f6557eec153305a3622"}, + {file = "lxml-4.9.4-cp311-cp311-win32.whl", hash = "sha256:8aecb5a7f6f7f8fe9cac0bcadd39efaca8bbf8d1bf242e9f175cbe4c925116c3"}, + {file = "lxml-4.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:c7721a3ef41591341388bb2265395ce522aba52f969d33dacd822da8f018aff8"}, + {file = "lxml-4.9.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:dbcb2dc07308453db428a95a4d03259bd8caea97d7f0776842299f2d00c72fc8"}, + {file = "lxml-4.9.4-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:01bf1df1db327e748dcb152d17389cf6d0a8c5d533ef9bab781e9d5037619229"}, + {file = "lxml-4.9.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e8f9f93a23634cfafbad6e46ad7d09e0f4a25a2400e4a64b1b7b7c0fbaa06d9d"}, + {file = "lxml-4.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3f3f00a9061605725df1816f5713d10cd94636347ed651abdbc75828df302b20"}, + {file = "lxml-4.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:953dd5481bd6252bd480d6ec431f61d7d87fdcbbb71b0d2bdcfc6ae00bb6fb10"}, + {file = "lxml-4.9.4-cp312-cp312-win32.whl", hash = "sha256:266f655d1baff9c47b52f529b5f6bec33f66042f65f7c56adde3fcf2ed62ae8b"}, + {file = "lxml-4.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:f1faee2a831fe249e1bae9cbc68d3cd8a30f7e37851deee4d7962b17c410dd56"}, + {file = "lxml-4.9.4-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:23d891e5bdc12e2e506e7d225d6aa929e0a0368c9916c1fddefab88166e98b20"}, + {file = "lxml-4.9.4-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e96a1788f24d03e8d61679f9881a883ecdf9c445a38f9ae3f3f193ab6c591c66"}, + {file = "lxml-4.9.4-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:5557461f83bb7cc718bc9ee1f7156d50e31747e5b38d79cf40f79ab1447afd2d"}, + {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:fdb325b7fba1e2c40b9b1db407f85642e32404131c08480dd652110fc908561b"}, + {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d74d4a3c4b8f7a1f676cedf8e84bcc57705a6d7925e6daef7a1e54ae543a197"}, + {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ac7674d1638df129d9cb4503d20ffc3922bd463c865ef3cb412f2c926108e9a4"}, + {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:ddd92e18b783aeb86ad2132d84a4b795fc5ec612e3545c1b687e7747e66e2b53"}, + {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bd9ac6e44f2db368ef8986f3989a4cad3de4cd55dbdda536e253000c801bcc7"}, + {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:bc354b1393dce46026ab13075f77b30e40b61b1a53e852e99d3cc5dd1af4bc85"}, + {file = "lxml-4.9.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:f836f39678cb47c9541f04d8ed4545719dc31ad850bf1832d6b4171e30d65d23"}, + {file = "lxml-4.9.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:9c131447768ed7bc05a02553d939e7f0e807e533441901dd504e217b76307745"}, + {file = "lxml-4.9.4-cp36-cp36m-win32.whl", hash = "sha256:bafa65e3acae612a7799ada439bd202403414ebe23f52e5b17f6ffc2eb98c2be"}, + {file = "lxml-4.9.4-cp36-cp36m-win_amd64.whl", hash = "sha256:6197c3f3c0b960ad033b9b7d611db11285bb461fc6b802c1dd50d04ad715c225"}, + {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:7b378847a09d6bd46047f5f3599cdc64fcb4cc5a5a2dd0a2af610361fbe77b16"}, + {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:1343df4e2e6e51182aad12162b23b0a4b3fd77f17527a78c53f0f23573663545"}, + {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6dbdacf5752fbd78ccdb434698230c4f0f95df7dd956d5f205b5ed6911a1367c"}, + {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:506becdf2ecaebaf7f7995f776394fcc8bd8a78022772de66677c84fb02dd33d"}, + {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca8e44b5ba3edb682ea4e6185b49661fc22b230cf811b9c13963c9f982d1d964"}, + {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9d9d5726474cbbef279fd709008f91a49c4f758bec9c062dfbba88eab00e3ff9"}, + {file = "lxml-4.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:bbdd69e20fe2943b51e2841fc1e6a3c1de460d630f65bde12452d8c97209464d"}, + {file = "lxml-4.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8671622256a0859f5089cbe0ce4693c2af407bc053dcc99aadff7f5310b4aa02"}, + {file = "lxml-4.9.4-cp37-cp37m-win32.whl", hash = "sha256:dd4fda67f5faaef4f9ee5383435048ee3e11ad996901225ad7615bc92245bc8e"}, + {file = "lxml-4.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6bee9c2e501d835f91460b2c904bc359f8433e96799f5c2ff20feebd9bb1e590"}, + {file = "lxml-4.9.4-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:1f10f250430a4caf84115b1e0f23f3615566ca2369d1962f82bef40dd99cd81a"}, + {file = "lxml-4.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b505f2bbff50d261176e67be24e8909e54b5d9d08b12d4946344066d66b3e43"}, + {file = "lxml-4.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:1449f9451cd53e0fd0a7ec2ff5ede4686add13ac7a7bfa6988ff6d75cff3ebe2"}, + {file = "lxml-4.9.4-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:4ece9cca4cd1c8ba889bfa67eae7f21d0d1a2e715b4d5045395113361e8c533d"}, + {file = "lxml-4.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59bb5979f9941c61e907ee571732219fa4774d5a18f3fa5ff2df963f5dfaa6bc"}, + {file = "lxml-4.9.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b1980dbcaad634fe78e710c8587383e6e3f61dbe146bcbfd13a9c8ab2d7b1192"}, + {file = "lxml-4.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9ae6c3363261021144121427b1552b29e7b59de9d6a75bf51e03bc072efb3c37"}, + {file = "lxml-4.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bcee502c649fa6351b44bb014b98c09cb00982a475a1912a9881ca28ab4f9cd9"}, + {file = "lxml-4.9.4-cp38-cp38-win32.whl", hash = "sha256:a8edae5253efa75c2fc79a90068fe540b197d1c7ab5803b800fccfe240eed33c"}, + {file = "lxml-4.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:701847a7aaefef121c5c0d855b2affa5f9bd45196ef00266724a80e439220e46"}, + {file = "lxml-4.9.4-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:f610d980e3fccf4394ab3806de6065682982f3d27c12d4ce3ee46a8183d64a6a"}, + {file = "lxml-4.9.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:aa9b5abd07f71b081a33115d9758ef6077924082055005808f68feccb27616bd"}, + {file = "lxml-4.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:365005e8b0718ea6d64b374423e870648ab47c3a905356ab6e5a5ff03962b9a9"}, + {file = "lxml-4.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:16b9ec51cc2feab009e800f2c6327338d6ee4e752c76e95a35c4465e80390ccd"}, + {file = "lxml-4.9.4-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:a905affe76f1802edcac554e3ccf68188bea16546071d7583fb1b693f9cf756b"}, + {file = "lxml-4.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fd814847901df6e8de13ce69b84c31fc9b3fb591224d6762d0b256d510cbf382"}, + {file = "lxml-4.9.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:91bbf398ac8bb7d65a5a52127407c05f75a18d7015a270fdd94bbcb04e65d573"}, + {file = "lxml-4.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f99768232f036b4776ce419d3244a04fe83784bce871b16d2c2e984c7fcea847"}, + {file = "lxml-4.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bb5bd6212eb0edfd1e8f254585290ea1dadc3687dd8fd5e2fd9a87c31915cdab"}, + {file = "lxml-4.9.4-cp39-cp39-win32.whl", hash = "sha256:88f7c383071981c74ec1998ba9b437659e4fd02a3c4a4d3efc16774eb108d0ec"}, + {file = "lxml-4.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:936e8880cc00f839aa4173f94466a8406a96ddce814651075f95837316369899"}, + {file = "lxml-4.9.4-pp310-pypy310_pp73-macosx_11_0_x86_64.whl", hash = "sha256:f6c35b2f87c004270fa2e703b872fcc984d714d430b305145c39d53074e1ffe0"}, + {file = "lxml-4.9.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:606d445feeb0856c2b424405236a01c71af7c97e5fe42fbc778634faef2b47e4"}, + {file = "lxml-4.9.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a1bdcbebd4e13446a14de4dd1825f1e778e099f17f79718b4aeaf2403624b0f7"}, + {file = "lxml-4.9.4-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0a08c89b23117049ba171bf51d2f9c5f3abf507d65d016d6e0fa2f37e18c0fc5"}, + {file = "lxml-4.9.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:232fd30903d3123be4c435fb5159938c6225ee8607b635a4d3fca847003134ba"}, + {file = "lxml-4.9.4-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:231142459d32779b209aa4b4d460b175cadd604fed856f25c1571a9d78114771"}, + {file = "lxml-4.9.4-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:520486f27f1d4ce9654154b4494cf9307b495527f3a2908ad4cb48e4f7ed7ef7"}, + {file = "lxml-4.9.4-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:562778586949be7e0d7435fcb24aca4810913771f845d99145a6cee64d5b67ca"}, + {file = "lxml-4.9.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a9e7c6d89c77bb2770c9491d988f26a4b161d05c8ca58f63fb1f1b6b9a74be45"}, + {file = "lxml-4.9.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:786d6b57026e7e04d184313c1359ac3d68002c33e4b1042ca58c362f1d09ff58"}, + {file = "lxml-4.9.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95ae6c5a196e2f239150aa4a479967351df7f44800c93e5a975ec726fef005e2"}, + {file = "lxml-4.9.4-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:9b556596c49fa1232b0fff4b0e69b9d4083a502e60e404b44341e2f8fb7187f5"}, + {file = "lxml-4.9.4-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:cc02c06e9e320869d7d1bd323df6dd4281e78ac2e7f8526835d3d48c69060683"}, + {file = "lxml-4.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:857d6565f9aa3464764c2cb6a2e3c2e75e1970e877c188f4aeae45954a314e0c"}, + {file = "lxml-4.9.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c42ae7e010d7d6bc51875d768110c10e8a59494855c3d4c348b068f5fb81fdcd"}, + {file = "lxml-4.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f10250bb190fb0742e3e1958dd5c100524c2cc5096c67c8da51233f7448dc137"}, + {file = "lxml-4.9.4.tar.gz", hash = "sha256:b1541e50b78e15fa06a2670157a1962ef06591d4c998b998047fff5e3236880e"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (==0.29.37)"] + +[[package]] +name = "m3u8" +version = "0.9.0" +description = "Python m3u8 parser" +optional = false +python-versions = ">=3.5" +files = [ + {file = "m3u8-0.9.0-py3-none-any.whl", hash = "sha256:7dde0a20cf985422593810006dd371a1e3e7afd33a76277111eba3f220288902"}, + {file = "m3u8-0.9.0.tar.gz", hash = "sha256:3ee058855c430dc364db6b8026269d2b4c1894b198bcc5c824039c551c05f497"}, +] + +[package.dependencies] +iso8601 = "*" + +[[package]] +name = "macholib" +version = "1.16.3" +description = "Mach-O header analysis and editing" +optional = false +python-versions = "*" +files = [ + {file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"}, + {file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"}, +] + +[package.dependencies] +altgraph = ">=0.17" + +[[package]] +name = "marisa-trie" +version = "1.2.1" +description = "Static memory-efficient and fast Trie-like structures for Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "marisa_trie-1.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a2eb41d2f9114d8b7bd66772c237111e00d2bae2260824560eaa0a1e291ce9e8"}, + {file = "marisa_trie-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9e956e6a46f604b17d570901e66f5214fb6f658c21e5e7665deace236793cef6"}, + {file = "marisa_trie-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bd45142501300e7538b2e544905580918b67b1c82abed1275fe4c682c95635fa"}, + {file = "marisa_trie-1.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8443d116c612cfd1961fbf76769faf0561a46d8e317315dd13f9d9639ad500c"}, + {file = "marisa_trie-1.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:875a6248e60fbb48d947b574ffa4170f34981f9e579bde960d0f9a49ea393ecc"}, + {file = "marisa_trie-1.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:746a7c60a17fccd3cfcfd4326926f02ea4fcdfc25d513411a0c4fc8e4a1ca51f"}, + {file = "marisa_trie-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e70869737cc0e5bd903f620667da6c330d6737048d1f44db792a6af68a1d35be"}, + {file = "marisa_trie-1.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06b099dd743676dbcd8abd8465ceac8f6d97d8bfaabe2c83b965495523b4cef2"}, + {file = "marisa_trie-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d2a82eb21afdaf22b50d9b996472305c05ca67fc4ff5a026a220320c9c961db6"}, + {file = "marisa_trie-1.2.1-cp310-cp310-win32.whl", hash = "sha256:8951e7ce5d3167fbd085703b4cbb3f47948ed66826bef9a2173c379508776cf5"}, + {file = "marisa_trie-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:5685a14b3099b1422c4f59fa38b0bf4b5342ee6cc38ae57df9666a0b28eeaad3"}, + {file = "marisa_trie-1.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed3fb4ed7f2084597e862bcd56c56c5529e773729a426c083238682dba540e98"}, + {file = "marisa_trie-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fe69fb9ffb2767746181f7b3b29bbd3454d1d24717b5958e030494f3d3cddf3"}, + {file = "marisa_trie-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4728ed3ae372d1ea2cdbd5eaa27b8f20a10e415d1f9d153314831e67d963f281"}, + {file = "marisa_trie-1.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cf4f25cf895692b232f49aa5397af6aba78bb679fb917a05fce8d3cb1ee446d"}, + {file = "marisa_trie-1.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cca7f96236ffdbf49be4b2e42c132e3df05968ac424544034767650913524de"}, + {file = "marisa_trie-1.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7eb20bf0e8b55a58d2a9b518aabc4c18278787bdba476c551dd1c1ed109e509"}, + {file = "marisa_trie-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b1ec93f0d1ee6d7ab680a6d8ea1a08bf264636358e92692072170032dda652ba"}, + {file = "marisa_trie-1.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e2699255d7ac610dee26d4ae7bda5951d05c7d9123a22e1f7c6a6f1964e0a4e4"}, + {file = "marisa_trie-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c484410911182457a8a1a0249d0c09c01e2071b78a0a8538cd5f7fa45589b13a"}, + {file = "marisa_trie-1.2.1-cp311-cp311-win32.whl", hash = "sha256:ad548117744b2bcf0e3d97374608be0a92d18c2af13d98b728d37cd06248e571"}, + {file = "marisa_trie-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:436f62d27714970b9cdd3b3c41bdad046f260e62ebb0daa38125ef70536fc73b"}, + {file = "marisa_trie-1.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:638506eacf20ca503fff72221a7e66a6eadbf28d6a4a6f949fcf5b1701bb05ec"}, + {file = "marisa_trie-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de1665eaafefa48a308e4753786519888021740501a15461c77bdfd57638e6b4"}, + {file = "marisa_trie-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f713af9b8aa66a34cd3a78c7d150a560a75734713abe818a69021fd269e927fa"}, + {file = "marisa_trie-1.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2a7d00f53f4945320b551bccb826b3fb26948bde1a10d50bb9802fabb611b10"}, + {file = "marisa_trie-1.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98042040d1d6085792e8d0f74004fc0f5f9ca6091c298f593dd81a22a4643854"}, + {file = "marisa_trie-1.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6532615111eec2c79e711965ece0bc95adac1ff547a7fff5ffca525463116deb"}, + {file = "marisa_trie-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:20948e40ab2038e62b7000ca6b4a913bc16c91a2c2e6da501bd1f917eeb28d51"}, + {file = "marisa_trie-1.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66b23e5b35dd547f85bf98db7c749bc0ffc57916ade2534a6bbc32db9a4abc44"}, + {file = "marisa_trie-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6704adf0247d2dda42e876b793be40775dff46624309ad99bc7537098bee106d"}, + {file = "marisa_trie-1.2.1-cp312-cp312-win32.whl", hash = "sha256:3ad356442c2fea4c2a6f514738ddf213d23930f942299a2b2c05df464a00848a"}, + {file = "marisa_trie-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:f2806f75817392cedcacb24ac5d80b0350dde8d3861d67d045c1d9b109764114"}, + {file = "marisa_trie-1.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b5ea16e69bfda0ac028c921b58de1a4aaf83d43934892977368579cd3c0a2554"}, + {file = "marisa_trie-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9f627f4e41be710b6cb6ed54b0128b229ac9d50e2054d9cde3af0fef277c23cf"}, + {file = "marisa_trie-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5e649f3dc8ab5476732094f2828cc90cac3be7c79bc0c8318b6fda0c1d248db4"}, + {file = "marisa_trie-1.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46e528ee71808c961baf8c3ce1c46a8337ec7a96cc55389d11baafe5b632f8e9"}, + {file = "marisa_trie-1.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36aa4401a1180615f74d575571a6550081d84fc6461e9aefc0bb7b2427af098e"}, + {file = "marisa_trie-1.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce59bcd2cda9bb52b0e90cc7f36413cd86c3d0ce7224143447424aafb9f4aa48"}, + {file = "marisa_trie-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f4cd800704a5fc57e53c39c3a6b0c9b1519ebdbcb644ede3ee67a06eb542697d"}, + {file = "marisa_trie-1.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2428b495003c189695fb91ceeb499f9fcced3a2dce853e17fa475519433c67ff"}, + {file = "marisa_trie-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:735c363d9aaac82eaf516a28f7c6b95084c2e176d8231c87328dc80e112a9afa"}, + {file = "marisa_trie-1.2.1-cp313-cp313-win32.whl", hash = "sha256:eba6ca45500ca1a042466a0684aacc9838e7f20fe2605521ee19f2853062798f"}, + {file = "marisa_trie-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:aa7cd17e1c690ce96c538b2f4aae003d9a498e65067dd433c52dd069009951d4"}, + {file = "marisa_trie-1.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5e43891a37b0d7f618819fea14bd951289a0a8e3dd0da50c596139ca83ebb9b1"}, + {file = "marisa_trie-1.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6946100a43f933fad6bc458c502a59926d80b321d5ac1ed2ff9c56605360496f"}, + {file = "marisa_trie-1.2.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4177dc0bd1374e82be9b2ba4d0c2733b0a85b9d154ceeea83a5bee8c1e62fbf"}, + {file = "marisa_trie-1.2.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f35c2603a6be168088ed1db6ad1704b078aa8f39974c60888fbbced95dcadad4"}, + {file = "marisa_trie-1.2.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d659fda873d8dcb2c14c2c331de1dee21f5a902d7f2de7978b62c6431a8850ef"}, + {file = "marisa_trie-1.2.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:b0ef26733d3c836be79e812071e1a431ce1f807955a27a981ebb7993d95f842b"}, + {file = "marisa_trie-1.2.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:536ea19ce6a2ce61c57fed4123ecd10d18d77a0db45cd2741afff2b8b68f15b3"}, + {file = "marisa_trie-1.2.1-cp37-cp37m-win32.whl", hash = "sha256:0ee6cf6a16d9c3d1c94e21c8e63c93d8b34bede170ca4e937e16e1c0700d399f"}, + {file = "marisa_trie-1.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7e7b1786e852e014d03e5f32dbd991f9a9eb223dd3fa9a2564108b807e4b7e1c"}, + {file = "marisa_trie-1.2.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:952af3a5859c3b20b15a00748c36e9eb8316eb2c70bd353ae1646da216322908"}, + {file = "marisa_trie-1.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24a81aa7566e4ec96fc4d934581fe26d62eac47fc02b35fa443a0bb718b471e8"}, + {file = "marisa_trie-1.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9c9b32b14651a6dcf9e8857d2df5d29d322a1ea8c0be5c8ffb88f9841c4ec62b"}, + {file = "marisa_trie-1.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ac170d20b97beb75059ba65d1ccad6b434d777c8992ab41ffabdade3b06dd74"}, + {file = "marisa_trie-1.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da4e4facb79614cc4653cfd859f398e4db4ca9ab26270ff12610e50ed7f1f6c6"}, + {file = "marisa_trie-1.2.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25688f34cac3bec01b4f655ffdd6c599a01f0bd596b4a79cf56c6f01a7df3560"}, + {file = "marisa_trie-1.2.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:1db3213b451bf058d558f6e619bceff09d1d130214448a207c55e1526e2773a1"}, + {file = "marisa_trie-1.2.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:d5648c6dcc5dc9200297fb779b1663b8a4467bda034a3c69bd9c32d8afb33b1d"}, + {file = "marisa_trie-1.2.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5bd39a4e1cc839a88acca2889d17ebc3f202a5039cd6059a13148ce75c8a6244"}, + {file = "marisa_trie-1.2.1-cp38-cp38-win32.whl", hash = "sha256:594f98491a96c7f1ffe13ce292cef1b4e63c028f0707effdea0f113364c1ae6c"}, + {file = "marisa_trie-1.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:5fe5a286f997848a410eebe1c28657506adaeb405220ee1e16cfcfd10deb37f2"}, + {file = "marisa_trie-1.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c0fe2ace0cb1806badbd1c551a8ec2f8d4cf97bf044313c082ef1acfe631ddca"}, + {file = "marisa_trie-1.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:67f0c2ec82c20a02c16fc9ba81dee2586ef20270127c470cb1054767aa8ba310"}, + {file = "marisa_trie-1.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a3c98613180cf1730e221933ff74b454008161b1a82597e41054127719964188"}, + {file = "marisa_trie-1.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:429858a0452a7bedcf67bc7bb34383d00f666c980cb75a31bcd31285fbdd4403"}, + {file = "marisa_trie-1.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2eacb84446543082ec50f2fb563f1a94c96804d4057b7da8ed815958d0cdfbe"}, + {file = "marisa_trie-1.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:852d7bcf14b0c63404de26e7c4c8d5d65ecaeca935e93794331bc4e2f213660b"}, + {file = "marisa_trie-1.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e58788004adda24c401d1751331618ed20c507ffc23bfd28d7c0661a1cf0ad16"}, + {file = "marisa_trie-1.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aefe0973cc4698e0907289dc0517ab0c7cdb13d588201932ff567d08a50b0e2e"}, + {file = "marisa_trie-1.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6c50c861faad0a5c091bd763e0729f958c316e678dfa065d3984fbb9e4eacbcd"}, + {file = "marisa_trie-1.2.1-cp39-cp39-win32.whl", hash = "sha256:b1ce340da608530500ab4f963f12d6bfc8d8680900919a60dbdc9b78c02060a4"}, + {file = "marisa_trie-1.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:ce37d8ca462bb64cc13f529b9ed92f7b21fe8d1f1679b62e29f9cb7d0e888b49"}, + {file = "marisa_trie-1.2.1.tar.gz", hash = "sha256:3a27c408e2aefc03e0f1d25b2ff2afb85aac3568f6fa2ae2a53b57a2e87ce29d"}, +] + +[package.dependencies] +setuptools = "*" + +[package.extras] +test = ["hypothesis", "pytest", "readme-renderer"] + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = "*" +files = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] + +[[package]] +name = "more-itertools" +version = "10.5.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.8" +files = [ + {file = "more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6"}, + {file = "more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef"}, +] + +[[package]] +name = "msgpack" +version = "1.1.0" +description = "MessagePack serializer" +optional = false +python-versions = ">=3.8" +files = [ + {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, + {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, + {file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"}, + {file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"}, + {file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"}, + {file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"}, + {file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"}, + {file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"}, + {file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"}, + {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"}, + {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"}, + {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec"}, + {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96"}, + {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870"}, + {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7"}, + {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb"}, + {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f"}, + {file = "msgpack-1.1.0-cp38-cp38-win32.whl", hash = "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b"}, + {file = "msgpack-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb"}, + {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1"}, + {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48"}, + {file = "msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c"}, + {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468"}, + {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74"}, + {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846"}, + {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346"}, + {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b"}, + {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8"}, + {file = "msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd"}, + {file = "msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325"}, + {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, +] + +[[package]] +name = "mutagen" +version = "1.47.0" +description = "read and write audio tags for many formats" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719"}, + {file = "mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pefile" +version = "2024.8.26" +description = "Python PE parsing module" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f"}, + {file = "pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632"}, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pkginfo" +version = "1.12.0" +description = "Query metadata from sdists / bdists / installed packages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pkginfo-1.12.0-py3-none-any.whl", hash = "sha256:dcd589c9be4da8973eceffa247733c144812759aa67eaf4bbf97016a02f39088"}, + {file = "pkginfo-1.12.0.tar.gz", hash = "sha256:8ad91a0445a036782b9366ef8b8c2c50291f83a553478ba8580c73d3215700cf"}, +] + +[package.extras] +testing = ["pytest", "pytest-cov", "wheel"] + +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "poetry" +version = "1.8.5" +description = "Python dependency management and packaging made easy." +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "poetry-1.8.5-py3-none-any.whl", hash = "sha256:5505fba69bf2a792b5d7402d21839c853644337392b745109b86a23010cce5f3"}, + {file = "poetry-1.8.5.tar.gz", hash = "sha256:eb2c88d224f58f36df8f7b36d6c380c07d1001bca28bde620f68fc086e881b70"}, +] + +[package.dependencies] +build = ">=1.0.3,<2.0.0" +cachecontrol = {version = ">=0.14.0,<0.15.0", extras = ["filecache"]} +cleo = ">=2.1.0,<3.0.0" +crashtest = ">=0.4.1,<0.5.0" +dulwich = ">=0.21.2,<0.22.0" +fastjsonschema = ">=2.18.0,<3.0.0" +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} +installer = ">=0.7.0,<0.8.0" +keyring = ">=24.0.0,<25.0.0" +packaging = ">=23.1" +pexpect = ">=4.7.0,<5.0.0" +pkginfo = ">=1.12,<2.0" +platformdirs = ">=3.0.0,<5" +poetry-core = "1.9.1" +poetry-plugin-export = ">=1.6.0,<2.0.0" +pyproject-hooks = ">=1.0.0,<2.0.0" +requests = ">=2.26,<3.0" +requests-toolbelt = ">=1.0.0,<2.0.0" +shellingham = ">=1.5,<2.0" +tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""} +tomlkit = ">=0.11.4,<1.0.0" +trove-classifiers = ">=2022.5.19" +virtualenv = ">=20.26.6,<21.0.0" +xattr = {version = ">=1.0.0,<2.0.0", markers = "sys_platform == \"darwin\""} + +[[package]] +name = "poetry-core" +version = "1.9.1" +description = "Poetry PEP 517 Build Backend" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "poetry_core-1.9.1-py3-none-any.whl", hash = "sha256:6f45dd3598e0de8d9b0367360253d4c5d4d0110c8f5c71120a14f0e0f116c1a0"}, + {file = "poetry_core-1.9.1.tar.gz", hash = "sha256:7a2d49214bf58b4f17f99d6891d947a9836c9899a67a5069f52d7b67217f61b8"}, +] + +[[package]] +name = "poetry-plugin-export" +version = "1.8.0" +description = "Poetry plugin to export the dependencies to various formats" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "poetry_plugin_export-1.8.0-py3-none-any.whl", hash = "sha256:adbe232cfa0cc04991ea3680c865cf748bff27593b9abcb1f35fb50ed7ba2c22"}, + {file = "poetry_plugin_export-1.8.0.tar.gz", hash = "sha256:1fa6168a85d59395d835ca564bc19862a7c76061e60c3e7dfaec70d50937fc61"}, +] + +[package.dependencies] +poetry = ">=1.8.0,<3.0.0" +poetry-core = ">=1.7.0,<3.0.0" + +[[package]] +name = "pproxy" +version = "2.7.9" +description = "Proxy server that can tunnel among remote servers by regex rules." +optional = false +python-versions = ">=3.6" +files = [ + {file = "pproxy-2.7.9-py3-none-any.whl", hash = "sha256:a073d02616a47c43e1d20a547918c307dbda598c6d53869b165025f3cfe58e80"}, +] + +[package.extras] +accelerated = ["pycryptodome (>=3.7.2)", "uvloop (>=0.13.0)"] +daemon = ["python-daemon (>=2.2.3)"] +quic = ["aioquic (>=0.9.7)"] +sshtunnel = ["asyncssh (>=2.5.0)"] + +[[package]] +name = "protobuf" +version = "4.25.5" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "protobuf-4.25.5-cp310-abi3-win32.whl", hash = "sha256:5e61fd921603f58d2f5acb2806a929b4675f8874ff5f330b7d6f7e2e784bbcd8"}, + {file = "protobuf-4.25.5-cp310-abi3-win_amd64.whl", hash = "sha256:4be0571adcbe712b282a330c6e89eae24281344429ae95c6d85e79e84780f5ea"}, + {file = "protobuf-4.25.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:b2fde3d805354df675ea4c7c6338c1aecd254dfc9925e88c6d31a2bcb97eb173"}, + {file = "protobuf-4.25.5-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:919ad92d9b0310070f8356c24b855c98df2b8bd207ebc1c0c6fcc9ab1e007f3d"}, + {file = "protobuf-4.25.5-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:fe14e16c22be926d3abfcb500e60cab068baf10b542b8c858fa27e098123e331"}, + {file = "protobuf-4.25.5-cp38-cp38-win32.whl", hash = "sha256:98d8d8aa50de6a2747efd9cceba361c9034050ecce3e09136f90de37ddba66e1"}, + {file = "protobuf-4.25.5-cp38-cp38-win_amd64.whl", hash = "sha256:b0234dd5a03049e4ddd94b93400b67803c823cfc405689688f59b34e0742381a"}, + {file = "protobuf-4.25.5-cp39-cp39-win32.whl", hash = "sha256:abe32aad8561aa7cc94fc7ba4fdef646e576983edb94a73381b03c53728a626f"}, + {file = "protobuf-4.25.5-cp39-cp39-win_amd64.whl", hash = "sha256:7a183f592dc80aa7c8da7ad9e55091c4ffc9497b3054452d629bb85fa27c2a45"}, + {file = "protobuf-4.25.5-py3-none-any.whl", hash = "sha256:0aebecb809cae990f8129ada5ca273d9d670b76d9bfc9b1809f0a9c02b7dbf41"}, + {file = "protobuf-4.25.5.tar.gz", hash = "sha256:7f8249476b4a9473645db7f8ab42b02fe1488cbe5fb72fddd445e0665afd8584"}, +] + +[[package]] +name = "protobuf3" +version = "3.20.2" +description = "protobuf3" +optional = false +python-versions = ">=3.7,<4.0" +files = [] +develop = true + +[package.dependencies] +requests = "^2.32.3" + +[package.source] +type = "directory" +url = "scripts/protobuf3" + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pycaption" +version = "2.1.1" +description = "Closed caption converter" +optional = false +python-versions = ">=3.6,<4.0" +files = [ + {file = "pycaption-2.1.1-py3-none-any.whl", hash = "sha256:c1c1c0e6968366f78b612763c429120efa88f3fa09e85833b739db92ec94d6ee"}, + {file = "pycaption-2.1.1.tar.gz", hash = "sha256:07eb8887c5933cf78d554df23fc14419cf0e8a763432925bf5d1cb0be9e18b82"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.8.1" +cssutils = ">=2.0.0" +lxml = ">=4.9.1" + +[package.extras] +dev = ["pytest", "pytest-lazy-fixture"] +transcript = ["nltk"] + +[[package]] +name = "pycodestyle" +version = "2.7.0" +description = "Python style guide checker" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, + {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, +] + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pycryptodome" +version = "3.21.0" +description = "Cryptographic library for Python" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "pycryptodome-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dad9bf36eda068e89059d1f07408e397856be9511d7113ea4b586642a429a4fd"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a1752eca64c60852f38bb29e2c86fca30d7672c024128ef5d70cc15868fa10f4"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ba4cc304eac4d4d458f508d4955a88ba25026890e8abff9b60404f76a62c55e"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cb087b8612c8a1a14cf37dd754685be9a8d9869bed2ffaaceb04850a8aeef7e"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:26412b21df30b2861424a6c6d5b1d8ca8107612a4cfa4d0183e71c5d200fb34a"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:cc2269ab4bce40b027b49663d61d816903a4bd90ad88cb99ed561aadb3888dd3"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:0fa0a05a6a697ccbf2a12cec3d6d2650b50881899b845fac6e87416f8cb7e87d"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6cce52e196a5f1d6797ff7946cdff2038d3b5f0aba4a43cb6bf46b575fd1b5bb"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:a915597ffccabe902e7090e199a7bf7a381c5506a747d5e9d27ba55197a2c568"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e74c522d630766b03a836c15bff77cb657c5fdf098abf8b1ada2aebc7d0819"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:a3804675283f4764a02db05f5191eb8fec2bb6ca34d466167fc78a5f05bbe6b3"}, + {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4"}, + {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b"}, + {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e"}, + {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8"}, + {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1"}, + {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a"}, + {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2"}, + {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93"}, + {file = "pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764"}, + {file = "pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53"}, + {file = "pycryptodome-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca"}, + {file = "pycryptodome-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:590ef0898a4b0a15485b05210b4a1c9de8806d3ad3d47f74ab1dc07c67a6827f"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35e442630bc4bc2e1878482d6f59ea22e280d7121d7adeaedba58c23ab6386b"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff99f952db3db2fbe98a0b355175f93ec334ba3d01bbde25ad3a5a33abc02b58"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8acd7d34af70ee63f9a849f957558e49a98f8f1634f86a59d2be62bb8e93f71c"}, + {file = "pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297"}, +] + +[[package]] +name = "pycryptodomex" +version = "3.21.0" +description = "Cryptographic library for Python" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "pycryptodomex-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dbeb84a399373df84a69e0919c1d733b89e049752426041deeb30d68e9867822"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a192fb46c95489beba9c3f002ed7d93979423d1b2a53eab8771dbb1339eb3ddd"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:1233443f19d278c72c4daae749872a4af3787a813e05c3561c73ab0c153c7b0f"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbb07f88e277162b8bfca7134b34f18b400d84eac7375ce73117f865e3c80d4c"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:e859e53d983b7fe18cb8f1b0e29d991a5c93be2c8dd25db7db1fe3bd3617f6f9"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:ef046b2e6c425647971b51424f0f88d8a2e0a2a63d3531817968c42078895c00"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:da76ebf6650323eae7236b54b1b1f0e57c16483be6e3c1ebf901d4ada47563b6"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c07e64867a54f7e93186a55bec08a18b7302e7bee1b02fd84c6089ec215e723a"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:56435c7124dd0ce0c8bdd99c52e5d183a0ca7fdcd06c5d5509423843f487dd0b"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65d275e3f866cf6fe891411be9c1454fb58809ccc5de6d3770654c47197acd65"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:5241bdb53bcf32a9568770a6584774b1b8109342bd033398e4ff2da052123832"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:34325b84c8b380675fd2320d0649cdcbc9cf1e0d1526edbe8fce43ed858cdc7e"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:103c133d6cd832ae7266feb0a65b69e3a5e4dbbd6f3a3ae3211a557fd653f516"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77ac2ea80bcb4b4e1c6a596734c775a1615d23e31794967416afc14852a639d3"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aa0cf13a1a1128b3e964dc667e5fe5c6235f7d7cfb0277213f0e2a783837cc2"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46eb1f0c8d309da63a2064c28de54e5e614ad17b7e2f88df0faef58ce192fc7b"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:cc7e111e66c274b0df5f4efa679eb31e23c7545d702333dfd2df10ab02c2a2ce"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:770d630a5c46605ec83393feaa73a9635a60e55b112e1fb0c3cea84c2897aa0a"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:52e23a0a6e61691134aa8c8beba89de420602541afaae70f66e16060fdcd677e"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-win32.whl", hash = "sha256:a3d77919e6ff56d89aada1bd009b727b874d464cb0e2e3f00a49f7d2e709d76e"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b0e9765f93fe4890f39875e6c90c96cb341767833cfa767f41b490b506fa9ec0"}, + {file = "pycryptodomex-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:feaecdce4e5c0045e7a287de0c4351284391fe170729aa9182f6bd967631b3a8"}, + {file = "pycryptodomex-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:365aa5a66d52fd1f9e0530ea97f392c48c409c2f01ff8b9a39c73ed6f527d36c"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3efddfc50ac0ca143364042324046800c126a1d63816d532f2e19e6f2d8c0c31"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df2608682db8279a9ebbaf05a72f62a321433522ed0e499bc486a6889b96bf3"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5823d03e904ea3e53aebd6799d6b8ec63b7675b5d2f4a4bd5e3adcb512d03b37"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:27e84eeff24250ffec32722334749ac2a57a5fd60332cd6a0680090e7c42877e"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ef436cdeea794015263853311f84c1ff0341b98fc7908e8a70595a68cefd971"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a1058e6dfe827f4209c5cae466e67610bcd0d66f2f037465daa2a29d92d952b"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba09a5b407cbb3bcb325221e346a140605714b5e880741dc9a1e9ecf1688d42"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8a9d8342cf22b74a746e3c6c9453cb0cfbb55943410e3a2619bd9164b48dc9d9"}, + {file = "pycryptodomex-3.21.0.tar.gz", hash = "sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c"}, +] + +[[package]] +name = "pyflakes" +version = "2.3.1" +description = "passive checker of Python programs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, + {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, +] + +[[package]] +name = "pyhulu" +version = "1.1.2" +description = "Python library for interacting with the E2E encrypted Hulu API" +optional = false +python-versions = "*" +files = [ + {file = "pyhulu-1.1.2-py3-none-any.whl", hash = "sha256:b42aaf2c976d00b7cc562de33a2302c420e04cf8f4c2d9803ca8b2baf8f9ac8f"}, +] + +[package.dependencies] +pycryptodomex = "*" +requests = "*" + +[[package]] +name = "pyinstaller" +version = "4.5.1" +description = "PyInstaller bundles a Python application and all its dependencies into a single package." +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyinstaller-4.5.1-py3-none-macosx_10_13_universal2.whl", hash = "sha256:ecc2baadeeefd2b6fbf39d13c65d4aa603afdda1c6aaaebc4577ba72893fee9e"}, + {file = "pyinstaller-4.5.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4d848cd782ee0893d7ad9fe2bfe535206a79f0b6760cecc5f2add831258b9322"}, + {file = "pyinstaller-4.5.1-py3-none-manylinux2014_i686.whl", hash = "sha256:8f747b190e6ad30e2d2fd5da9a64636f61aac8c038c0b7f685efa92c782ea14f"}, + {file = "pyinstaller-4.5.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c587da8f521a7ce1b9efb4e3d0117cd63c92dc6cedff24590aeef89372f53012"}, + {file = "pyinstaller-4.5.1-py3-none-win32.whl", hash = "sha256:fed9f5e4802769a416a8f2ca171c6be961d1861cc05a0b71d20dfe05423137e9"}, + {file = "pyinstaller-4.5.1-py3-none-win_amd64.whl", hash = "sha256:aae456205c68355f9597411090576bb31b614e53976b4c102d072bbe5db8392a"}, + {file = "pyinstaller-4.5.1.tar.gz", hash = "sha256:30733baaf8971902286a0ddf77e5499ac5f7bf8e7c39163e83d4f8c696ef265e"}, +] + +[package.dependencies] +altgraph = "*" +macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} +pefile = {version = ">=2017.8.1", markers = "sys_platform == \"win32\""} +pyinstaller-hooks-contrib = ">=2020.6" +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +setuptools = "*" + +[package.extras] +encryption = ["tinyaes (>=1.0.0)"] +hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] + +[[package]] +name = "pyinstaller-hooks-contrib" +version = "2025.0" +description = "Community maintained hooks for PyInstaller" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyinstaller_hooks_contrib-2025.0-py3-none-any.whl", hash = "sha256:3c0623799c3f81a37293127f485d65894c20fd718f722cb588785a3e52581ad1"}, + {file = "pyinstaller_hooks_contrib-2025.0.tar.gz", hash = "sha256:6dc0b55a1acaab2ffee36ed4a05b073aa0a22e46f25fb5c66a31e217454135ed"}, +] + +[package.dependencies] +importlib_metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} +packaging = ">=22.0" +setuptools = ">=42.0.0" + +[[package]] +name = "pymediainfo" +version = "5.1.0" +description = "A Python wrapper for the mediainfo library." +optional = false +python-versions = ">=3.6" +files = [ + {file = "pymediainfo-5.1.0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:99bd7e562dc2040c7c60f7a57a603be964c95e2d6b34684fdc5c6046c12dba71"}, + {file = "pymediainfo-5.1.0-py3-none-win32.whl", hash = "sha256:5df10ff7d65631a68f80846f91b06d8844cc307588788443f1716c7543d4ff8e"}, + {file = "pymediainfo-5.1.0-py3-none-win_amd64.whl", hash = "sha256:ffba87bbac9b020526d34b7aef3094b6e9afb23b208051ffef0fbd4833007455"}, + {file = "pymediainfo-5.1.0.tar.gz", hash = "sha256:d996c69d50081a24d6dca9679abf43ffd2be368b065f953c2c9082e5d649c734"}, +] + +[[package]] +name = "pymp4" +version = "1.4.0" +description = "Python parser for MP4 boxes" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "pymp4-1.4.0-py3-none-any.whl", hash = "sha256:3401666c1e2a97ac94dffb18c5a5dcbd46d0a436da5272d378a6f9f6506dd12d"}, + {file = "pymp4-1.4.0.tar.gz", hash = "sha256:bc9e77732a8a143d34c38aa862a54180716246938e4bf3e07585d19252b77bb5"}, +] + +[package.dependencies] +construct = "2.8.8" + +[[package]] +name = "pymysql" +version = "1.1.1" +description = "Pure Python MySQL Driver" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c"}, + {file = "pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0"}, +] + +[package.dependencies] +cryptography = {version = "*", optional = true, markers = "extra == \"rsa\""} + +[package.extras] +ed25519 = ["PyNaCl (>=1.4.0)"] +rsa = ["cryptography"] + +[[package]] +name = "pyplayready" +version = "0.5.0" +description = "pyplayready CDM (Content Decryption Module) implementation in Python." +optional = false +python-versions = ">=3.8,<4.0" +files = [] +develop = true + +[package.dependencies] +click = "^8.1.7" +construct = "^2.8.8" +ECPy = "^1.2.5" +pycryptodome = "^3.21.0" +PyYAML = "^6.0.1" +requests = "^2.32.3" +xmltodict = "^0.14.2" + +[package.source] +type = "directory" +url = "scripts/pyplayready" + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +description = "Wrappers to call pyproject.toml-based build backend hooks." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"}, + {file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"}, +] + +[[package]] +name = "pyreadline3" +version = "3.5.4" +description = "A python implementation of GNU readline." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, + {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, +] + +[package.extras] +dev = ["build", "flake8", "mypy", "pytest", "twine"] + +[[package]] +name = "pysocks" +version = "1.7.1" +description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, +] + +[[package]] +name = "pysubs2" +version = "1.7.3" +description = "A library for editing subtitle files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pysubs2-1.7.3-py3-none-any.whl", hash = "sha256:de438c868d2c656781c4a78f220ec3a6fd6d52be49266c81fe912d2527002d44"}, + {file = "pysubs2-1.7.3.tar.gz", hash = "sha256:b0130f373390736754531be4e68a0fa521e825fa15cc8ff506e4f8ca2c17459a"}, +] + +[[package]] +name = "pywidevine" +version = "1.8.0" +description = "Widevine CDM (Content Decryption Module) implementation in Python." +optional = false +python-versions = ">=3.8,<4.0" +files = [] +develop = true + +[package.dependencies] +click = "^8.1.7" +protobuf = "^4.25.1" +pycryptodome = "^3.19.0" +PyYAML = "^6.0.1" +requests = "^2.31.0" +Unidecode = "^1.3.7" + +[package.extras] +serve = ["aiohttp (>=3.9.1,<4.0.0)"] + +[package.source] +type = "directory" +url = "scripts/pywidevine" + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "rapidfuzz" +version = "3.9.7" +description = "rapid fuzzy string matching" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rapidfuzz-3.9.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ccf68e30b80e903f2309f90a438dbd640dd98e878eeb5ad361a288051ee5b75c"}, + {file = "rapidfuzz-3.9.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:696a79018ef989bf1c9abd9005841cee18005ccad4748bad8a4c274c47b6241a"}, + {file = "rapidfuzz-3.9.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4eebf6c93af0ae866c22b403a84747580bb5c10f0d7b51c82a87f25405d4dcb"}, + {file = "rapidfuzz-3.9.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e9125377fa3d21a8abd4fbdbcf1c27be73e8b1850f0b61b5b711364bf3b59db"}, + {file = "rapidfuzz-3.9.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c12d180b17a22d107c8747de9c68d0b9c1d15dcda5445ff9bf9f4ccfb67c3e16"}, + {file = "rapidfuzz-3.9.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1318d42610c26dcd68bd3279a1bf9e3605377260867c9a8ed22eafc1bd93a7c"}, + {file = "rapidfuzz-3.9.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5fa6e3c6e0333051c1f3a49f0807b3366f4131c8d6ac8c3e05fd0d0ce3755c"}, + {file = "rapidfuzz-3.9.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fcf79b686962d7bec458a0babc904cb4fa319808805e036b9d5a531ee6b9b835"}, + {file = "rapidfuzz-3.9.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8b01153c7466d0bad48fba77a303d5a768e66f24b763853469f47220b3de4661"}, + {file = "rapidfuzz-3.9.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:94baaeea0b4f8632a6da69348b1e741043eba18d4e3088d674d3f76586b6223d"}, + {file = "rapidfuzz-3.9.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6c5b32875646cb7f60c193ade99b2e4b124f19583492115293cd00f6fb198b17"}, + {file = "rapidfuzz-3.9.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:110b6294396bc0a447648627479c9320f095c2034c0537f687592e0f58622638"}, + {file = "rapidfuzz-3.9.7-cp310-cp310-win32.whl", hash = "sha256:3445a35c4c8d288f2b2011eb61bce1227c633ce85a3154e727170f37c0266bb2"}, + {file = "rapidfuzz-3.9.7-cp310-cp310-win_amd64.whl", hash = "sha256:0d1415a732ee75e74a90af12020b77a0b396b36c60afae1bde3208a78cd2c9fc"}, + {file = "rapidfuzz-3.9.7-cp310-cp310-win_arm64.whl", hash = "sha256:836f4d88b8bd0fff2ebe815dcaab8aa6c8d07d1d566a7e21dd137cf6fe11ed5b"}, + {file = "rapidfuzz-3.9.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d098ce6162eb5e48fceb0745455bc950af059df6113eec83e916c129fca11408"}, + {file = "rapidfuzz-3.9.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:048d55d36c02c6685a2b2741688503c3d15149694506655b6169dcfd3b6c2585"}, + {file = "rapidfuzz-3.9.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c33211cfff9aec425bb1bfedaf94afcf337063aa273754f22779d6dadebef4c2"}, + {file = "rapidfuzz-3.9.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6d9db2fa4e9be171e9bb31cf2d2575574774966b43f5b951062bb2e67885852"}, + {file = "rapidfuzz-3.9.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4e049d5ad61448c9a020d1061eba20944c4887d720c4069724beb6ea1692507"}, + {file = "rapidfuzz-3.9.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cfa74aac64c85898b93d9c80bb935a96bf64985e28d4ee0f1a3d1f3bf11a5106"}, + {file = "rapidfuzz-3.9.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:965693c2e9efd425b0f059f5be50ef830129f82892fa1858e220e424d9d0160f"}, + {file = "rapidfuzz-3.9.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8501000a5eb8037c4b56857724797fe5a8b01853c363de91c8d0d0ad56bef319"}, + {file = "rapidfuzz-3.9.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d92c552c6b7577402afdd547dcf5d31ea6c8ae31ad03f78226e055cfa37f3c6"}, + {file = "rapidfuzz-3.9.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1ee2086f490cb501d86b7e386c1eb4e3a0ccbb0c99067089efaa8c79012c8952"}, + {file = "rapidfuzz-3.9.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1de91e7fd7f525e10ea79a6e62c559d1b0278ec097ad83d9da378b6fab65a265"}, + {file = "rapidfuzz-3.9.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a4da514d13f4433e16960a17f05b67e0af30ac771719c9a9fb877e5004f74477"}, + {file = "rapidfuzz-3.9.7-cp311-cp311-win32.whl", hash = "sha256:a40184c67db8252593ec518e17fb8a6e86d7259dc9f2d6c0bf4ff4db8cf1ad4b"}, + {file = "rapidfuzz-3.9.7-cp311-cp311-win_amd64.whl", hash = "sha256:c4f28f1930b09a2c300357d8465b388cecb7e8b2f454a5d5425561710b7fd07f"}, + {file = "rapidfuzz-3.9.7-cp311-cp311-win_arm64.whl", hash = "sha256:675b75412a943bb83f1f53e2e54fd18c80ef15ed642dc6eb0382d1949419d904"}, + {file = "rapidfuzz-3.9.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1ef6a1a8f0b12f8722f595f15c62950c9a02d5abc64742561299ffd49f6c6944"}, + {file = "rapidfuzz-3.9.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:32532af1d70c6ec02ea5ac7ee2766dfff7c8ae8c761abfe8da9e527314e634e8"}, + {file = "rapidfuzz-3.9.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae1a38bade755aa9dd95a81cda949e1bf9cd92b79341ccc5e2189c9e7bdfc5ec"}, + {file = "rapidfuzz-3.9.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d73ee2df41224c87336448d279b5b6a3a75f36e41dd3dcf538c0c9cce36360d8"}, + {file = "rapidfuzz-3.9.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be3a1fc3e2ab3bdf93dc0c83c00acca8afd2a80602297d96cf4a0ba028333cdf"}, + {file = "rapidfuzz-3.9.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:603f48f621272a448ff58bb556feb4371252a02156593303391f5c3281dfaeac"}, + {file = "rapidfuzz-3.9.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:268f8e1ca50fc61c0736f3fe9d47891424adf62d96ed30196f30f4bd8216b41f"}, + {file = "rapidfuzz-3.9.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f8bf3f0d02935751d8660abda6044821a861f6229f7d359f98bcdcc7e66c39b"}, + {file = "rapidfuzz-3.9.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b997ff3b39d4cee9fb025d6c46b0a24bd67595ce5a5b652a97fb3a9d60beb651"}, + {file = "rapidfuzz-3.9.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca66676c8ef6557f9b81c5b2b519097817a7c776a6599b8d6fcc3e16edd216fe"}, + {file = "rapidfuzz-3.9.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:35d3044cb635ca6b1b2b7b67b3597bd19f34f1753b129eb6d2ae04cf98cd3945"}, + {file = "rapidfuzz-3.9.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5a93c9e60904cb76e7aefef67afffb8b37c4894f81415ed513db090f29d01101"}, + {file = "rapidfuzz-3.9.7-cp312-cp312-win32.whl", hash = "sha256:579d107102c0725f7c79b4e79f16d3cf4d7c9208f29c66b064fa1fd4641d5155"}, + {file = "rapidfuzz-3.9.7-cp312-cp312-win_amd64.whl", hash = "sha256:953b3780765c8846866faf891ee4290f6a41a6dacf4fbcd3926f78c9de412ca6"}, + {file = "rapidfuzz-3.9.7-cp312-cp312-win_arm64.whl", hash = "sha256:7c20c1474b068c4bd45bf2fd0ad548df284f74e9a14a68b06746c56e3aa8eb70"}, + {file = "rapidfuzz-3.9.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fde81b1da9a947f931711febe2e2bee694e891f6d3e6aa6bc02c1884702aea19"}, + {file = "rapidfuzz-3.9.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:47e92c155a14f44511ea8ebcc6bc1535a1fe8d0a7d67ad3cc47ba61606df7bcf"}, + {file = "rapidfuzz-3.9.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8772b745668260c5c4d069c678bbaa68812e6c69830f3771eaad521af7bc17f8"}, + {file = "rapidfuzz-3.9.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:578302828dd97ee2ba507d2f71d62164e28d2fc7bc73aad0d2d1d2afc021a5d5"}, + {file = "rapidfuzz-3.9.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc3e6081069eea61593f1d6839029da53d00c8c9b205c5534853eaa3f031085c"}, + {file = "rapidfuzz-3.9.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b1c2d504eddf97bc0f2eba422c8915576dbf025062ceaca2d68aecd66324ad9"}, + {file = "rapidfuzz-3.9.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb76e5a21034f0307c51c5a2fc08856f698c53a4c593b17d291f7d6e9d09ca3"}, + {file = "rapidfuzz-3.9.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d4ba2318ef670ce505f42881a5d2af70f948124646947341a3c6ccb33cd70369"}, + {file = "rapidfuzz-3.9.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:057bb03f39e285047d7e9412e01ecf31bb2d42b9466a5409d715d587460dd59b"}, + {file = "rapidfuzz-3.9.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a8feac9006d5c9758438906f093befffc4290de75663dbb2098461df7c7d28dd"}, + {file = "rapidfuzz-3.9.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:95b8292383e717e10455f2c917df45032b611141e43d1adf70f71b1566136b11"}, + {file = "rapidfuzz-3.9.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e9fbf659537d246086d0297628b3795dc3e4a384101ecc01e5791c827b8d7345"}, + {file = "rapidfuzz-3.9.7-cp313-cp313-win32.whl", hash = "sha256:1dc516ac6d32027be2b0196bedf6d977ac26debd09ca182376322ad620460feb"}, + {file = "rapidfuzz-3.9.7-cp313-cp313-win_amd64.whl", hash = "sha256:b4f86e09d3064dca0b014cd48688964036a904a2d28048f00c8f4640796d06a8"}, + {file = "rapidfuzz-3.9.7-cp313-cp313-win_arm64.whl", hash = "sha256:19c64d8ddb2940b42a4567b23f1681af77f50a5ff6c9b8e85daba079c210716e"}, + {file = "rapidfuzz-3.9.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fbda3dd68d8b28ccb20ffb6f756fefd9b5ba570a772bedd7643ed441f5793308"}, + {file = "rapidfuzz-3.9.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2379e0b2578ad3ac7004f223251550f08bca873ff76c169b09410ec562ad78d8"}, + {file = "rapidfuzz-3.9.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d1eff95362f993b0276fd3839aee48625b09aac8938bb0c23b40d219cba5dc5"}, + {file = "rapidfuzz-3.9.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd9360e30041690912525a210e48a897b49b230768cc8af1c702e5395690464f"}, + {file = "rapidfuzz-3.9.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a93cd834b3c315ab437f0565ee3a2f42dd33768dc885ccbabf9710b131cf70d2"}, + {file = "rapidfuzz-3.9.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff196996240db7075f62c7bc4506f40a3c80cd4ae3ab0e79ac6892283a90859"}, + {file = "rapidfuzz-3.9.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948dcee7aaa1cd14358b2a7ef08bf0be42bf89049c3a906669874a715fc2c937"}, + {file = "rapidfuzz-3.9.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95751f505a301af1aaf086c19f34536056d6c8efa91b2240de532a3db57b543"}, + {file = "rapidfuzz-3.9.7-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:90db86fa196eecf96cb6db09f1083912ea945c50c57188039392d810d0b784e1"}, + {file = "rapidfuzz-3.9.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:3171653212218a162540a3c8eb8ae7d3dcc8548540b69eaecaf3b47c14d89c90"}, + {file = "rapidfuzz-3.9.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:36dd6e820379c37a1ffefc8a52b648758e867cd9d78ee5b5dc0c9a6a10145378"}, + {file = "rapidfuzz-3.9.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:7b702de95666a1f7d5c6b47eacadfe2d2794af3742d63d2134767d13e5d1c713"}, + {file = "rapidfuzz-3.9.7-cp38-cp38-win32.whl", hash = "sha256:9030e7238c0df51aed5c9c5ed8eee2bdd47a2ae788e562c1454af2851c3d1906"}, + {file = "rapidfuzz-3.9.7-cp38-cp38-win_amd64.whl", hash = "sha256:f847fb0fbfb72482b1c05c59cbb275c58a55b73708a7f77a83f8035ee3c86497"}, + {file = "rapidfuzz-3.9.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:97f2ce529d2a70a60c290f6ab269a2bbf1d3b47b9724dccc84339b85f7afb044"}, + {file = "rapidfuzz-3.9.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e2957fdad10bb83b1982b02deb3604a3f6911a5e545f518b59c741086f92d152"}, + {file = "rapidfuzz-3.9.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d5262383634626eb45c536017204b8163a03bc43bda880cf1bdd7885db9a163"}, + {file = "rapidfuzz-3.9.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:364587827d7cbd41afa0782adc2d2d19e3f07d355b0750a02a8e33ad27a9c368"}, + {file = "rapidfuzz-3.9.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecc24af7f905f3d6efb371a01680116ffea8d64e266618fb9ad1602a9b4f7934"}, + {file = "rapidfuzz-3.9.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9dc86aa6b29d174713c5f4caac35ffb7f232e3e649113e8d13812b35ab078228"}, + {file = "rapidfuzz-3.9.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3dcfbe7266e74a707173a12a7b355a531f2dcfbdb32f09468e664330da14874"}, + {file = "rapidfuzz-3.9.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b23806fbdd6b510ba9ac93bb72d503066263b0fba44b71b835be9f063a84025f"}, + {file = "rapidfuzz-3.9.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5551d68264c1bb6943f542da83a4dc8940ede52c5847ef158698799cc28d14f5"}, + {file = "rapidfuzz-3.9.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:13d8675a1fa7e2b19650ca7ef9a6ec01391d4bb12ab9e0793e8eb024538b4a34"}, + {file = "rapidfuzz-3.9.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9b6a5de507b9be6de688dae40143b656f7a93b10995fb8bd90deb555e7875c60"}, + {file = "rapidfuzz-3.9.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:111a20a3c090cf244d9406e60500b6c34b2375ba3a5009e2b38fd806fe38e337"}, + {file = "rapidfuzz-3.9.7-cp39-cp39-win32.whl", hash = "sha256:22589c0b8ccc6c391ce7f776c93a8c92c96ab8d34e1a19f1bd2b12a235332632"}, + {file = "rapidfuzz-3.9.7-cp39-cp39-win_amd64.whl", hash = "sha256:6f83221db5755b8f34222e40607d87f1176a8d5d4dbda4a55a0f0b67d588a69c"}, + {file = "rapidfuzz-3.9.7-cp39-cp39-win_arm64.whl", hash = "sha256:3665b92e788578c3bb334bd5b5fa7ee1a84bafd68be438e3110861d1578c63a0"}, + {file = "rapidfuzz-3.9.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d7df9c2194c7ec930b33c991c55dbd0c10951bd25800c0b7a7b571994ebbced5"}, + {file = "rapidfuzz-3.9.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:68bd888eafd07b09585dcc8bc2716c5ecdb7eed62827470664d25588982b2873"}, + {file = "rapidfuzz-3.9.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1230e0f9026851a6a432beaa0ce575dda7b39fe689b576f99a0704fbb81fc9c"}, + {file = "rapidfuzz-3.9.7-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3b36e1c61b796ae1777f3e9e11fd39898b09d351c9384baf6e3b7e6191d8ced"}, + {file = "rapidfuzz-3.9.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dba13d86806fcf3fe9c9919f58575e0090eadfb89c058bde02bcc7ab24e4548"}, + {file = "rapidfuzz-3.9.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1f1a33e84056b7892c721d84475d3bde49a145126bc4c6efe0d6d0d59cb31c29"}, + {file = "rapidfuzz-3.9.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3492c7a42b7fa9f0051d7fcce9893e95ed91c97c9ec7fb64346f3e070dd318ed"}, + {file = "rapidfuzz-3.9.7-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:ece45eb2af8b00f90d10f7419322e8804bd42fb1129026f9bfe712c37508b514"}, + {file = "rapidfuzz-3.9.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcd14cf4876f04b488f6e54a7abd3e9b31db5f5a6aba0ce90659917aaa8c088"}, + {file = "rapidfuzz-3.9.7-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:521c58c72ed8a612b25cda378ff10dee17e6deb4ee99a070b723519a345527b9"}, + {file = "rapidfuzz-3.9.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18669bb6cdf7d40738526d37e550df09ba065b5a7560f3d802287988b6cb63cf"}, + {file = "rapidfuzz-3.9.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7abe2dbae81120a64bb4f8d3fcafe9122f328c9f86d7f327f174187a5af4ed86"}, + {file = "rapidfuzz-3.9.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a3c0783910911f4f24655826d007c9f4360f08107410952c01ee3df98c713eb2"}, + {file = "rapidfuzz-3.9.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:03126f9a040ff21d2a110610bfd6b93b79377ce8b4121edcb791d61b7df6eec5"}, + {file = "rapidfuzz-3.9.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:591908240f4085e2ade5b685c6e8346e2ed44932cffeaac2fb32ddac95b55c7f"}, + {file = "rapidfuzz-3.9.7-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9012d86c6397edbc9da4ac0132de7f8ee9d6ce857f4194d5684c4ddbcdd1c5c"}, + {file = "rapidfuzz-3.9.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df596ddd3db38aa513d4c0995611267b3946e7cbe5a8761b50e9306dfec720ee"}, + {file = "rapidfuzz-3.9.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3ed5adb752f4308fcc8f4fb6f8eb7aa4082f9d12676fda0a74fa5564242a8107"}, + {file = "rapidfuzz-3.9.7.tar.gz", hash = "sha256:f1c7296534c1afb6f495aa95871f14ccdc197c6db42965854e483100df313030"}, +] + +[package.extras] +full = ["numpy"] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +PySocks = {version = ">=1.5.6,<1.5.7 || >1.5.7", optional = true, markers = "extra == \"socks\""} +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-file" +version = "2.1.0" +description = "File transport adapter for Requests" +optional = false +python-versions = "*" +files = [ + {file = "requests_file-2.1.0-py2.py3-none-any.whl", hash = "sha256:cf270de5a4c5874e84599fc5778303d496c10ae5e870bfa378818f35d21bda5c"}, + {file = "requests_file-2.1.0.tar.gz", hash = "sha256:0f549a3f3b0699415ac04d167e9cb39bccfb730cb832b4d20be3d9867356e658"}, +] + +[package.dependencies] +requests = ">=1.0.0" + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +description = "A utility belt for advanced users of python-requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, + {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, +] + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +optional = false +python-versions = "*" +files = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] + +[package.dependencies] +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "secretstorage" +version = "3.3.3" +description = "Python bindings to FreeDesktop.org Secret Service API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, + {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, +] + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + +[[package]] +name = "setuptools" +version = "75.3.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd"}, + {file = "setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.12.*)", "pytest-mypy"] + +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "soupsieve" +version = "2.6" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, +] + +[[package]] +name = "tldextract" +version = "3.5.0" +description = "Accurately separates a URL's subdomain, domain, and public suffix, using the Public Suffix List (PSL). By default, this includes the public ICANN TLDs and their exceptions. You can optionally support the Public Suffix List's private domains as well." +optional = false +python-versions = ">=3.7" +files = [ + {file = "tldextract-3.5.0-py3-none-any.whl", hash = "sha256:2cb271ca8d06ea1630a1361b58edad14e0cf81f34ce3c90b052854528fe2a281"}, + {file = "tldextract-3.5.0.tar.gz", hash = "sha256:4df1c65b95be61d59428e8611e955e54e6f1d4483d3e8d5733d3a9062155e910"}, +] + +[package.dependencies] +filelock = ">=3.0.8" +idna = "*" +requests = ">=2.1.0" +requests-file = ">=1.4" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "tomlkit" +version = "0.13.2" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] +discord = ["requests"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "trove-classifiers" +version = "2025.1.15.22" +description = "Canonical source for classifiers on PyPI (pypi.org)." +optional = false +python-versions = "*" +files = [ + {file = "trove_classifiers-2025.1.15.22-py3-none-any.whl", hash = "sha256:5f19c789d4f17f501d36c94dbbf969fb3e8c2784d008e6f5164dd2c3d6a2b07c"}, + {file = "trove_classifiers-2025.1.15.22.tar.gz", hash = "sha256:90af74358d3a01b3532bc7b3c88d8c6a094c2fd50a563d13d9576179326d7ed9"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "unidecode" +version = "1.3.8" +description = "ASCII transliterations of Unicode text" +optional = false +python-versions = ">=3.5" +files = [ + {file = "Unidecode-1.3.8-py3-none-any.whl", hash = "sha256:d130a61ce6696f8148a3bd8fe779c99adeb4b870584eeb9526584e9aa091fd39"}, + {file = "Unidecode-1.3.8.tar.gz", hash = "sha256:cfdb349d46ed3873ece4586b96aa75258726e2fa8ec21d6f00a591d98806c2f4"}, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "validators" +version = "0.18.2" +description = "Python Data Validation for Humans™." +optional = false +python-versions = ">=3.4" +files = [ + {file = "validators-0.18.2-py3-none-any.whl", hash = "sha256:0143dcca8a386498edaf5780cbd5960da1a4c85e0719f3ee5c9b41249c4fefbd"}, + {file = "validators-0.18.2.tar.gz", hash = "sha256:37cd9a9213278538ad09b5b9f9134266e7c226ab1fede1d500e29e0a8fbb9ea6"}, +] + +[package.dependencies] +decorator = ">=3.4.0" +six = ">=1.4.0" + +[package.extras] +test = ["flake8 (>=2.4.0)", "isort (>=4.2.2)", "pytest (>=2.2.3)"] + +[[package]] +name = "virtualenv" +version = "20.29.1" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +files = [ + {file = "virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779"}, + {file = "virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "websocket-client" +version = "1.8.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, + {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + +[[package]] +name = "websockets" +version = "13.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websockets-13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee"}, + {file = "websockets-13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7"}, + {file = "websockets-13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6"}, + {file = "websockets-13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b"}, + {file = "websockets-13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa"}, + {file = "websockets-13.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700"}, + {file = "websockets-13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c"}, + {file = "websockets-13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0"}, + {file = "websockets-13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f"}, + {file = "websockets-13.1-cp310-cp310-win32.whl", hash = "sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe"}, + {file = "websockets-13.1-cp310-cp310-win_amd64.whl", hash = "sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a"}, + {file = "websockets-13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19"}, + {file = "websockets-13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5"}, + {file = "websockets-13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd"}, + {file = "websockets-13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02"}, + {file = "websockets-13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7"}, + {file = "websockets-13.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096"}, + {file = "websockets-13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084"}, + {file = "websockets-13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3"}, + {file = "websockets-13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9"}, + {file = "websockets-13.1-cp311-cp311-win32.whl", hash = "sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f"}, + {file = "websockets-13.1-cp311-cp311-win_amd64.whl", hash = "sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557"}, + {file = "websockets-13.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc"}, + {file = "websockets-13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49"}, + {file = "websockets-13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd"}, + {file = "websockets-13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0"}, + {file = "websockets-13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6"}, + {file = "websockets-13.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9"}, + {file = "websockets-13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68"}, + {file = "websockets-13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14"}, + {file = "websockets-13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf"}, + {file = "websockets-13.1-cp312-cp312-win32.whl", hash = "sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c"}, + {file = "websockets-13.1-cp312-cp312-win_amd64.whl", hash = "sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3"}, + {file = "websockets-13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6"}, + {file = "websockets-13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708"}, + {file = "websockets-13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418"}, + {file = "websockets-13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a"}, + {file = "websockets-13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f"}, + {file = "websockets-13.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5"}, + {file = "websockets-13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135"}, + {file = "websockets-13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2"}, + {file = "websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6"}, + {file = "websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d"}, + {file = "websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2"}, + {file = "websockets-13.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c7934fd0e920e70468e676fe7f1b7261c1efa0d6c037c6722278ca0228ad9d0d"}, + {file = "websockets-13.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:149e622dc48c10ccc3d2760e5f36753db9cacf3ad7bc7bbbfd7d9c819e286f23"}, + {file = "websockets-13.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a569eb1b05d72f9bce2ebd28a1ce2054311b66677fcd46cf36204ad23acead8c"}, + {file = "websockets-13.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95df24ca1e1bd93bbca51d94dd049a984609687cb2fb08a7f2c56ac84e9816ea"}, + {file = "websockets-13.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8dbb1bf0c0a4ae8b40bdc9be7f644e2f3fb4e8a9aca7145bfa510d4a374eeb7"}, + {file = "websockets-13.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:035233b7531fb92a76beefcbf479504db8c72eb3bff41da55aecce3a0f729e54"}, + {file = "websockets-13.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e4450fc83a3df53dec45922b576e91e94f5578d06436871dce3a6be38e40f5db"}, + {file = "websockets-13.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:463e1c6ec853202dd3657f156123d6b4dad0c546ea2e2e38be2b3f7c5b8e7295"}, + {file = "websockets-13.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6d6855bbe70119872c05107e38fbc7f96b1d8cb047d95c2c50869a46c65a8e96"}, + {file = "websockets-13.1-cp38-cp38-win32.whl", hash = "sha256:204e5107f43095012b00f1451374693267adbb832d29966a01ecc4ce1db26faf"}, + {file = "websockets-13.1-cp38-cp38-win_amd64.whl", hash = "sha256:485307243237328c022bc908b90e4457d0daa8b5cf4b3723fd3c4a8012fce4c6"}, + {file = "websockets-13.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9b37c184f8b976f0c0a231a5f3d6efe10807d41ccbe4488df8c74174805eea7d"}, + {file = "websockets-13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:163e7277e1a0bd9fb3c8842a71661ad19c6aa7bb3d6678dc7f89b17fbcc4aeb7"}, + {file = "websockets-13.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b889dbd1342820cc210ba44307cf75ae5f2f96226c0038094455a96e64fb07a"}, + {file = "websockets-13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:586a356928692c1fed0eca68b4d1c2cbbd1ca2acf2ac7e7ebd3b9052582deefa"}, + {file = "websockets-13.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7bd6abf1e070a6b72bfeb71049d6ad286852e285f146682bf30d0296f5fbadfa"}, + {file = "websockets-13.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2aad13a200e5934f5a6767492fb07151e1de1d6079c003ab31e1823733ae79"}, + {file = "websockets-13.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:df01aea34b6e9e33572c35cd16bae5a47785e7d5c8cb2b54b2acdb9678315a17"}, + {file = "websockets-13.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e54affdeb21026329fb0744ad187cf812f7d3c2aa702a5edb562b325191fcab6"}, + {file = "websockets-13.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ef8aa8bdbac47f4968a5d66462a2a0935d044bf35c0e5a8af152d58516dbeb5"}, + {file = "websockets-13.1-cp39-cp39-win32.whl", hash = "sha256:deeb929efe52bed518f6eb2ddc00cc496366a14c726005726ad62c2dd9017a3c"}, + {file = "websockets-13.1-cp39-cp39-win_amd64.whl", hash = "sha256:7c65ffa900e7cc958cd088b9a9157a8141c991f8c53d11087e6fb7277a03f81d"}, + {file = "websockets-13.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238"}, + {file = "websockets-13.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5"}, + {file = "websockets-13.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9"}, + {file = "websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6"}, + {file = "websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a"}, + {file = "websockets-13.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23"}, + {file = "websockets-13.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9156c45750b37337f7b0b00e6248991a047be4aa44554c9886fe6bdd605aab3b"}, + {file = "websockets-13.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:80c421e07973a89fbdd93e6f2003c17d20b69010458d3a8e37fb47874bd67d51"}, + {file = "websockets-13.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82d0ba76371769d6a4e56f7e83bb8e81846d17a6190971e38b5de108bde9b0d7"}, + {file = "websockets-13.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9875a0143f07d74dc5e1ded1c4581f0d9f7ab86c78994e2ed9e95050073c94d"}, + {file = "websockets-13.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11e38ad8922c7961447f35c7b17bffa15de4d17c70abd07bfbe12d6faa3e027"}, + {file = "websockets-13.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4059f790b6ae8768471cddb65d3c4fe4792b0ab48e154c9f0a04cefaabcd5978"}, + {file = "websockets-13.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:25c35bf84bf7c7369d247f0b8cfa157f989862c49104c5cf85cb5436a641d93e"}, + {file = "websockets-13.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:83f91d8a9bb404b8c2c41a707ac7f7f75b9442a0a876df295de27251a856ad09"}, + {file = "websockets-13.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a43cfdcddd07f4ca2b1afb459824dd3c6d53a51410636a2c7fc97b9a8cf4842"}, + {file = "websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48a2ef1381632a2f0cb4efeff34efa97901c9fbc118e01951ad7cfc10601a9bb"}, + {file = "websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bf774c754c35dbb487360b12c5727adab887f1622b8aed5755880a21c4a20"}, + {file = "websockets-13.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:95858ca14a9f6fa8413d29e0a585b31b278388aa775b8a81fa24830123874678"}, + {file = "websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f"}, + {file = "websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878"}, +] + +[[package]] +name = "xattr" +version = "1.1.4" +description = "Python wrapper for extended filesystem attributes" +optional = false +python-versions = ">=3.8" +files = [ + {file = "xattr-1.1.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:acb85b6249e9f3ea10cbb56df1021d43f4027212f0d004304bc9075dc7f54769"}, + {file = "xattr-1.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1a848ab125c0fafdc501ccd83b4c9018bba576a037a4ca5960a22f39e295552e"}, + {file = "xattr-1.1.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:467ee77471d26ae5187ee7081b82175b5ca56ead4b71467ec2e6119d1b08beed"}, + {file = "xattr-1.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fd35f46cb0154f7033f9d5d0960f226857acb0d1e0d71fd7af18ed84663007c"}, + {file = "xattr-1.1.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d956478e9bb98a1efd20ebc6e5703497c1d2d690d5a13c4df4abf59881eed50"}, + {file = "xattr-1.1.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f25dfdcd974b700fb04a40e14a664a80227ee58e02ea062ac241f0d7dc54b4e"}, + {file = "xattr-1.1.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33b63365c1fcbc80a79f601575bac0d6921732e0245b776876f3db3fcfefe22d"}, + {file = "xattr-1.1.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:544542be95c9b49e211f0a463758f200de88ba6d5a94d3c4f42855a484341acd"}, + {file = "xattr-1.1.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac14c9893f3ea046784b7702be30889b200d31adcd2e6781a8a190b6423f9f2d"}, + {file = "xattr-1.1.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bb4bbe37ba95542081890dd34fa5347bef4651e276647adaa802d5d0d7d86452"}, + {file = "xattr-1.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3da489ecef798705f9a39ea8cea4ead0d1eeed55f92c345add89740bd930bab6"}, + {file = "xattr-1.1.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:798dd0cbe696635a6f74b06fc430818bf9c3b24314e1502eadf67027ab60c9b0"}, + {file = "xattr-1.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b2b6361626efad5eb5a6bf8172c6c67339e09397ee8140ec41258737bea9681"}, + {file = "xattr-1.1.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7fa20a0c9ce022d19123b1c5b848d00a68b837251835a7929fe041ee81dcd0"}, + {file = "xattr-1.1.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e20eeb08e2c57fc7e71f050b1cfae35cbb46105449853a582bf53fd23c5379e"}, + {file = "xattr-1.1.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:477370e75821bded901487e5e752cffe554d1bd3bd4839b627d4d1ee8c95a093"}, + {file = "xattr-1.1.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a8682091cd34a9f4a93c8aaea4101aae99f1506e24da00a3cc3dd2eca9566f21"}, + {file = "xattr-1.1.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2e079b3b1a274ba2121cf0da38bbe5c8d2fb1cc49ecbceb395ce20eb7d69556d"}, + {file = "xattr-1.1.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ae6579dea05bf9f335a082f711d5924a98da563cac72a2d550f5b940c401c0e9"}, + {file = "xattr-1.1.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd6038ec9df2e67af23c212693751481d5f7e858156924f14340376c48ed9ac7"}, + {file = "xattr-1.1.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:608b2877526674eb15df4150ef4b70b7b292ae00e65aecaae2f192af224be200"}, + {file = "xattr-1.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54dad1a6a998c6a23edfd25e99f4d38e9b942d54e518570044edf8c767687ea"}, + {file = "xattr-1.1.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0dab6ff72bb2b508f3850c368f8e53bd706585012676e1f71debba3310acde8"}, + {file = "xattr-1.1.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a3c54c6af7cf09432b2c461af257d5f4b1cb2d59eee045f91bacef44421a46d"}, + {file = "xattr-1.1.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e346e05a158d554639fbf7a0db169dc693c2d2260c7acb3239448f1ff4a9d67f"}, + {file = "xattr-1.1.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3ff6d9e2103d0d6e5fcd65b85a2005b66ea81c0720a37036445faadc5bbfa424"}, + {file = "xattr-1.1.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7a2ee4563c6414dfec0d1ac610f59d39d5220531ae06373eeb1a06ee37cd193f"}, + {file = "xattr-1.1.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878df1b38cfdadf3184ad8c7b0f516311128d5597b60ac0b3486948953658a83"}, + {file = "xattr-1.1.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c9b8350244a1c5454f93a8d572628ff71d7e2fc2f7480dcf4c4f0e8af3150fe"}, + {file = "xattr-1.1.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a46bf48fb662b8bd745b78bef1074a1e08f41a531168de62b5d7bd331dadb11a"}, + {file = "xattr-1.1.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83fc3c07b583777b1dda6355329f75ca6b7179fe0d1002f1afe0ef96f7e3b5de"}, + {file = "xattr-1.1.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6308b19cff71441513258699f0538394fad5d66e1d324635207a97cb076fd439"}, + {file = "xattr-1.1.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48c00ddc15ddadc9c729cd9504dabf50adb3d9c28f647d4ac9a3df45a046b1a0"}, + {file = "xattr-1.1.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a06136196f26293758e1b244200b73156a0274af9a7349fa201c71c7af3bb9e8"}, + {file = "xattr-1.1.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8fc2631a3c6cfcdc71f7f0f847461839963754e76a2015de71e7e71e3304abc0"}, + {file = "xattr-1.1.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d6e1e835f9c938d129dd45e7eb52ebf7d2d6816323dab93ce311bf331f7d2328"}, + {file = "xattr-1.1.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:60dea2d369a6484e8b7136224fc2971e10e2c46340d83ab780924afe78c90066"}, + {file = "xattr-1.1.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:85c2b778b09d919523f80f244d799a142302582d76da18903dc693207c4020b0"}, + {file = "xattr-1.1.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ee0abba9e1b890d39141714ff43e9666864ca635ea8a5a2194d989e6b17fe862"}, + {file = "xattr-1.1.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e4174ba7f51f46b95ea7918d907c91cd579575d59e6a2f22ca36a0551026737"}, + {file = "xattr-1.1.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2b05e52e99d82d87528c54c2c5c8c5fb0ba435f85ac6545511aeea136e49925"}, + {file = "xattr-1.1.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a3696fad746be37de34eb73c60ea67144162bd08106a5308a90ce9dea9a3287"}, + {file = "xattr-1.1.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a3a7149439a26b68904c14fdc4587cde4ac7d80303e9ff0fefcfd893b698c976"}, + {file = "xattr-1.1.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:507b36a126ce900dbfa35d4e2c2db92570c933294cba5d161ecd6a89f7b52f43"}, + {file = "xattr-1.1.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9392b417b54923e031041940d396b1d709df1d3779c6744454e1f1c1f4dad4f5"}, + {file = "xattr-1.1.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e9f00315e6c02943893b77f544776b49c756ac76960bea7cb8d7e1b96aefc284"}, + {file = "xattr-1.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c8f98775065260140efb348b1ff8d50fd66ddcbf0c685b76eb1e87b380aaffb3"}, + {file = "xattr-1.1.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b471c6a515f434a167ca16c5c15ff34ee42d11956baa749173a8a4e385ff23e7"}, + {file = "xattr-1.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee0763a1b7ceb78ba2f78bee5f30d1551dc26daafcce4ac125115fa1def20519"}, + {file = "xattr-1.1.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:099e6e9ce7999b403d36d9cf943105a3d25d8233486b54ec9d1b78623b050433"}, + {file = "xattr-1.1.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3e56faef9dde8d969f0d646fb6171883693f88ae39163ecd919ec707fbafa85"}, + {file = "xattr-1.1.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:328156d4e594c9ae63e1072503c168849e601a153ad37f0290743544332d6b6f"}, + {file = "xattr-1.1.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a57a55a27c7864d6916344c9a91776afda6c3b8b2209f8a69b79cdba93fbe128"}, + {file = "xattr-1.1.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3c19cdde08b040df1e99d2500bf8a9cff775ab0e6fa162bf8afe6d84aa93ed04"}, + {file = "xattr-1.1.4-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c72667f19d3a9acf324aed97f58861d398d87e42314731e7c6ab3ac7850c971"}, + {file = "xattr-1.1.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:67ae934d75ea2563fc48a27c5945749575c74a6de19fdd38390917ddcb0e4f24"}, + {file = "xattr-1.1.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a1b0c348dd8523554dc535540d2046c0c8a535bb086561d8359f3667967b6ca"}, + {file = "xattr-1.1.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22284255d2a8e8f3da195bd8e8d43ce674dbc7c38d38cb6ecfb37fae7755d31f"}, + {file = "xattr-1.1.4-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b38aac5ef4381c26d3ce147ca98fba5a78b1e5bcd6be6755b4908659f2705c6d"}, + {file = "xattr-1.1.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:803f864af528f6f763a5be1e7b1ccab418e55ae0e4abc8bda961d162f850c991"}, + {file = "xattr-1.1.4-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:40354ebfb5cecd60a5fbb9833a8a452d147486b0ffec547823658556625d98b5"}, + {file = "xattr-1.1.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2abaf5d06be3361bfa8e0db2ee123ba8e92beab5bceed5e9d7847f2145a32e04"}, + {file = "xattr-1.1.4-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e638e5ffedc3565242b5fa3296899d35161bad771f88d66277b58f03a1ba9fe"}, + {file = "xattr-1.1.4-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0597e919d116ec39997804288d77bec3777228368efc0f2294b84a527fc4f9c2"}, + {file = "xattr-1.1.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee9455c501d19f065527afda974418b3ef7c61e85d9519d122cd6eb3cb7a00"}, + {file = "xattr-1.1.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:89ed62ce430f5789e15cfc1ccabc172fd8b349c3a17c52d9e6c64ecedf08c265"}, + {file = "xattr-1.1.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e25b824f4b9259cd8bb6e83c4873cf8bf080f6e4fa034a02fe778e07aba8d345"}, + {file = "xattr-1.1.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8fba66faa0016dfc0af3dd7ac5782b5786a1dfb851f9f3455e266f94c2a05a04"}, + {file = "xattr-1.1.4-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ec4b0c3e0a7bcd103f3cf31dd40c349940b2d4223ce43d384a3548992138ef1"}, + {file = "xattr-1.1.4.tar.gz", hash = "sha256:b7b02ecb2270da5b7e7deaeea8f8b528c17368401c2b9d5f63e91f545b45d372"}, +] + +[package.dependencies] +cffi = ">=1.16.0" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "xmltodict" +version = "0.14.2" +description = "Makes working with XML feel like you are working with JSON" +optional = false +python-versions = ">=3.6" +files = [ + {file = "xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac"}, + {file = "xmltodict-0.14.2.tar.gz", hash = "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553"}, +] + +[[package]] +name = "yt-dlp" +version = "2022.11.11" +description = "A youtube-dl fork with additional features and patches" +optional = false +python-versions = ">=3.7" +files = [ + {file = "yt-dlp-2022.11.11.tar.gz", hash = "sha256:f6b962023c17a77151476f0f6ed71be87d017629ba5d9994528bc548521191b6"}, + {file = "yt_dlp-2022.11.11-py2.py3-none-any.whl", hash = "sha256:8bb7bd9ab2e6ecf4db7627e9151ce00572ae7ee24dedc78f611e7467b0ccd7d9"}, +] + +[package.dependencies] +brotli = {version = "*", markers = "platform_python_implementation == \"CPython\""} +brotlicffi = {version = "*", markers = "platform_python_implementation != \"CPython\""} +certifi = "*" +mutagen = "*" +pycryptodomex = "*" +websockets = "*" + +[[package]] +name = "zipp" +version = "3.20.2" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, + {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "3e35149e73c237381b87aa360390154341cf8dcd84a45219e0c9c743fef46686" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8857f7b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,64 @@ +[build-system] +requires = ['poetry-core>=1.0.0'] +build-backend = 'poetry.core.masonry.api' + +[tool.poetry] +name = 'vinetrimmer' +version = '0.1.0' +description = 'Playready DRM downloader and decrypter' +authors = [] + +[tool.poetry.dependencies] +python = "^3.8" +appdirs = "^1.4.4" +beautifulsoup4 = "~4.8.2" +click = "^8.0.1" +cffi = "^1.16.0" +coloredlogs = "^15.0" +construct = "2.8.8" +crccheck = "^1.0" +cryptography = "^43.0.3" +ecpy = "^1.2.5" +httpx = "^0.23.0" +isodate = "^0.6.1" +jsonpickle = "^2.0.0" +langcodes = { extras = ["data"], version = "^3.1.0" } +lxml = "^4.6.3" +m3u8 = "^0.9.0" +marisa-trie = "^1.1.0" +poetry = "1.8.5" +pproxy = "^2.7.7" +protobuf3 = { path = "./scripts/protobuf3", develop = true } +pycaption = "^2.1.1" +pycryptodome = "^3.21.0" +pycryptodomex = "^3.4.3" +pyhulu = "^1.1.2" +pymediainfo = "^5.0.3" +PyMySQL = { extras = ["rsa"], version = "^1.0.2" } +pymp4 = "^1.4.0" +pyplayready = { path = "./scripts/pyplayready", develop = true } +pywidevine = { path = "./scripts/pywidevine", develop = true } +pysubs2 = "^1.6.1" +PyYAML = "^6.0.1" +requests = { extras = ["socks"], version = "2.32.3" } +tldextract = "^3.1.0" +toml = "^0.10.2" +tqdm = "^4.67.0" +Unidecode = "^1.2.0" +validators = "^0.18.2" +websocket-client = "^1.1.0" +xmltodict = "^0.14.0" +yt-dlp = "^2022.11.11" + +[tool.poetry.dev-dependencies] +flake8 = "^3.8.4" +isort = "^5.9.2" +pyinstaller = "^4.4" + +[tool.poetry.scripts] +vt = 'vinetrimmer.vinetrimmer:main' + +[tool.isort] +line_length = 120 +classes = ['CTV', 'FPS', 'IO', 'iTunes', 'MP4', 'TVNOW'] +extend_skip = ['vinetrimmer/vendor'] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b6d8bb8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +requests +pycryptodome +ecpy +construct +click \ No newline at end of file diff --git a/scripts/AddKeysToKeyVault.py b/scripts/AddKeysToKeyVault.py new file mode 100644 index 0000000..ecdfe16 --- /dev/null +++ b/scripts/AddKeysToKeyVault.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 + +import argparse +import re +import sqlite3 +import sys + +from vinetrimmer.utils.AtomicSQL import AtomicSQL + +""" +Add keys to key vault. File should have one KID:KEY per-line. +Optionally you can also put `:` at the end (after `KEY`). +""" + +parser = argparse.ArgumentParser( + "Key Vault DB batch adder/updater", + description="Simple script to add or update key information to a vinetrimmer key vault db" +) +parser.add_argument( + "-t", "--table", + help="table to store keys to. (e.g. amazon, netflix, disneyplus)", + required=True) +parser.add_argument( + "-i", "--input", + help="data used to parse from", + required=True) +parser.add_argument( + "-o", "--output", + help="key store db that will receive keys", + required=True) +parser.add_argument( + "-d", "--dry-run", + help="execute it, but never actually save/commit changes.", + action="store_true", required=False) +args = parser.parse_args() + +output_db = AtomicSQL() +output_db_id = output_db.load(sqlite3.connect(args.output)) + +# get all keys from input db +add_count = 0 +update_count = 0 +existed_count = 0 + +if args.input == "-": + input_ = sys.stdin.read() +else: + with open(args.input, encoding="utf-8") as fd: + input_ = fd.read() + +for line in input_.splitlines(keepends=False): + match = re.search(r"^(?P<kid>[0-9a-fA-F]{32}):(?P<key>[0-9a-fA-F]{32})(:(?P<title>[\w .:-]*))?$", line) + if not match: + continue + kid = match.group("kid").lower() + key = match.group("key").lower() + title = match.group("title") or None + + exists = output_db.safe_execute( + output_db_id, + lambda db, cursor: cursor.execute( + f"SELECT title FROM `{args.table}` WHERE `kid`=:kid", + {"kid": kid} + ) + ).fetchone() + + if exists: + if title and not exists[0]: + update_count += 1 + print(f"Updating {args.table} {kid}: {title}") + output_db.safe_execute( + output_db_id, + lambda db, cursor: cursor.execute( + f"UPDATE `{args.table}` SET `title`=:title", + {"title": title} + ) + ) + else: + existed_count += 1 + print(f"Key {args.table} {kid} already exists in the db with no differences, skipping...") + else: + add_count += 1 + print(f"Adding {args.table} {kid} ({title}): {key}") + output_db.safe_execute( + output_db_id, + lambda db, cursor: cursor.execute( + f"INSERT INTO `{args.table}` (kid, key_, title) VALUES (:kid, :key, :title)", + {"kid": kid, "key": key, "title": title} + ) + ) + +if args.dry_run: + print("--dry run enabled, have not commited any changes.") +else: + output_db.commit(output_db_id) + +print( + "Done!\n" + f"{add_count} added, {update_count} updated in some way, {existed_count} already existed (skipped)" +) diff --git a/scripts/ClientIDGen/ClientIDGen.py b/scripts/ClientIDGen/ClientIDGen.py new file mode 100644 index 0000000..a61a0d2 --- /dev/null +++ b/scripts/ClientIDGen/ClientIDGen.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 + +import argparse +import base64 + +import yaml + +from vinetrimmer.utils.widevine.protos.widevine_pb2 import ClientIdentificationRaw + +parser = argparse.ArgumentParser("Widevine Client ID building tool.") +parser.add_argument("-q", "--quiet", + help="do not print the generated client id", + action="store_true") +parser.add_argument("-c", "--config", + help="configuration yaml file", + default="config.yml") +parser.add_argument("-o", "--output", + default="device_client_id_blob", + help="output filename") +args = parser.parse_args() + +with open(args.config) as fd: + config = yaml.safe_load(fd) + +with open(config["token"], "rb") as fd: + token = fd.read() + +ci = ClientIdentificationRaw() +ci.Type = ClientIdentificationRaw.DEVICE_CERTIFICATE +ci.Token = token + +for name, value in config["client_info"].items(): + nv = ci.ClientInfo.add() + nv.Name = name + if name == "device_id": + value = base64.b64decode(value) + nv.Value = value + +capabilities = ClientIdentificationRaw.ClientCapabilities() +caps = config["capabilities"] +if "client_token" in caps: + capabilities.ClientToken = caps["client_token"] +if "session_token" in caps: + capabilities.SessionToken = caps["session_token"] +if "video_resolution_constraints" in caps: + capabilities.VideoResolutionConstraints = caps["video_resolution_constraints"] +if "max_hdcp_version" in caps: + max_hdcp_version = caps["max_hdcp_version"] + if str(max_hdcp_version).isdigit(): + max_hdcp_version = int(max_hdcp_version) + else: + max_hdcp_version = ClientIdentificationRaw.ClientCapabilities.HdcpVersion.Value(max_hdcp_version) + capabilities.MaxHdcpVersion = max_hdcp_version +if "oem_crypto_api_version" in caps: + capabilities.OemCryptoApiVersion = int(caps["oem_crypto_api_version"]) +# I have not seen any of the following in use: +if "anti_rollback_usage_table" in caps: + capabilities.AntiRollbackUsageTable = caps["anti_rollback_usage_table"] +if "srm_version" in caps: + capabilities.SrmVersion = int(caps["srm_version"]) +if "can_update_srm" in caps: + capabilities.ClientToken = caps["can_update_srm"] +# is it possible to refactor this? +if "supported_certificate_key_type" in caps: + supported_certificate_key_type = caps["supported_certificate_key_type"] + if str(supported_certificate_key_type).isdigit(): + supported_certificate_key_type = int(supported_certificate_key_type) + else: + supported_certificate_key_type = ClientIdentificationRaw.ClientCapabilities.CertificateKeyType.Value( + supported_certificate_key_type + ) + capabilities.SupportedCertificateKeyType.append(supported_certificate_key_type) +ci._ClientCapabilities.CopyFrom(capabilities) + +if not args.quiet: + print(ci) + +with open(args.output, "wb") as fd: + fd.write(ci.SerializeToString()) diff --git a/scripts/ClientIDGen/config.example.yml b/scripts/ClientIDGen/config.example.yml new file mode 100644 index 0000000..4376e70 --- /dev/null +++ b/scripts/ClientIDGen/config.example.yml @@ -0,0 +1,20 @@ +# NOTE! +# This client id gen script may use outdated ClientIdentification values. +# Just letting you know, do whatever you wish, but yeah + +token: 'token.bin' + +client_info: + company_name: 'motorola' + model_name: 'Nexus 6' + architecture_name: 'armeabi-v7a' + device_name: 'shamu' + product_name: 'shamu' + build_info: 'google/shamu/shamu:5.1.1/LMY48M/2167285:user/release-keys' + device_id: 'TU1JX0VGRkYwRkU2NUQ5OA==' + os_version: '5.1.12' + +capabilities: + session_token: 1 + max_hdcp_version: 'HDCP_V2_2' + oem_crypto_api_version: 11 diff --git a/scripts/GetVikiManifestFree.py b/scripts/GetVikiManifestFree.py new file mode 100644 index 0000000..48fa620 --- /dev/null +++ b/scripts/GetVikiManifestFree.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 + +import re +import sys + +import requests +from Cryptodome.Cipher import AES + +# create a session with a user agent +http = requests.Session() +http.headers.update({ + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0" +}) +# get player fragment page +fragment = http.get(sys.argv[1].replace("/videos/", "/player5_fragment/")).text +# get encrypted manifest.xml urls for both hls and dash +encrypted_manifests = {k: bytes.fromhex(re.findall( + r'<source\s+type="application/' + v + r'"\s+src=".+?/e-stream-url\?stream=(.+?)"', + fragment +)[0][0]) for k, v in {"hls": "x-mpegURL", "dash": r"dash\+xml"}.items()} + +# decrypt all manifest.xml urls in manifests +m = re.search(r"^\s*chabi:\s*'(.+?)'", fragment, re.MULTILINE) +if not m: + raise ValueError("Unable to get key") +key = m.group(1).encode() + +m = re.search(r"^\s*ecta:\s*'(.+?)'", fragment, re.MULTILINE) +if not m: + raise ValueError("Unable to get key") +iv = m.group(1).encode() + +manifests = {k: AES.new(key, AES.MODE_CBC, iv).decrypt(v).decode("utf-8") for k, v in encrypted_manifests.items()} +# print em out +print(manifests) diff --git a/scripts/MergeKeyStores.py b/scripts/MergeKeyStores.py new file mode 100644 index 0000000..8321ac9 --- /dev/null +++ b/scripts/MergeKeyStores.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 + +import argparse +import json +import sqlite3 + +from vinetrimmer.utils.AtomicSQL import AtomicSQL + +parser = argparse.ArgumentParser( + "Key Store DB merger", + description="Simple script to merge vinetrimmer key store db's into one" +) +parser.add_argument( + "-i", "--input", + help="key store db that will send keys", + required=True) +parser.add_argument( + "-o", "--output", + help="key store db that will receive keys", + required=True) +args = parser.parse_args() + +add_count = 0 +update_count = 0 +existed_count = 0 + +input_db = AtomicSQL() +input_db_id = input_db.load(sqlite3.connect(args.input)) + +output_db = AtomicSQL() +output_db_id = output_db.load(sqlite3.connect(args.output)) + +# get all keys from input db +input_keys = input_db.safe_execute( + input_db_id, + lambda db, cursor: cursor.execute("SELECT * FROM `keys`") +).fetchall() + +for i, service, title, pssh_b64, pssh_sha1, content_keys in input_keys: + exists = output_db.safe_execute( + output_db_id, + lambda db, cursor: cursor.execute( + """ + SELECT "id","service","title","pssh_b64","pssh_sha1","content_keys" FROM `keys` WHERE `service`=:service AND + (`pssh_b64`=:pssh_b64 or `pssh_sha1`=:pssh_sha1) + """, + { + "service": service, + "pssh_b64": pssh_b64, + "pssh_sha1": pssh_sha1 + } + ) + ).fetchone() + if exists: + has_differences = ( + json.loads(exists[5]) != json.loads(content_keys) or + title != exists[2] or + pssh_b64 != exists[3] or + pssh_sha1 != exists[4] + ) + if has_differences: + update_count += 1 + content_keys = list(set(json.loads(exists[5])) | set(json.loads(content_keys))) + print(f"Updating {title} {service} {pssh_b64}: {content_keys}") + output_db.safe_execute( + output_db_id, + lambda db, cursor: cursor.execute( + """ + UPDATE `keys` SET `service`=:service, `title`=:title, `pssh_b64`=:new_pssh_b64, + `pssh_sha1`=:new_pssh_sha1, `content_keys`=:content_keys WHERE `service`=:service AND + (`pssh_b64`=:pssh_b64 or `pssh_sha1`=:pssh_sha1) + """, + { + "service": service, + "title": title or exists[2], + "pssh_b64": pssh_b64, + "new_pssh_b64": pssh_b64 or exists[3], + "pssh_sha1": pssh_sha1, + "new_pssh_sha1": pssh_sha1 or exists[4], + "content_keys": json.dumps(content_keys, separators=(",", ":")) + } + ) + ) + else: + existed_count += 1 + print(f"Key {title} {service} {pssh_b64} already exists in the db with no differences, skipping...") + else: + add_count += 1 + print(f"Adding {title} {service} {pssh_b64}: {content_keys}") + output_db.safe_execute( + output_db_id, + lambda db, cursor: cursor.execute( + """ + INSERT INTO `keys` (service, title, pssh_b64, pssh_sha1, content_keys) + VALUES (:service, :title, :pssh_b64, :pssh_sha1, :content_keys) + """, + { + "service": service, + "title": title, + "pssh_b64": pssh_b64, + "pssh_sha1": pssh_sha1, + "content_keys": json.dumps(content_keys, separators=(",", ":")) + } + ) + ) + +output_db.commit(output_db_id) + +print( + "Done!\n" + f"{add_count} added, {update_count} updated in some way, {existed_count} already existed (no difference)" +) diff --git a/scripts/ParseClientID.py b/scripts/ParseClientID.py new file mode 100644 index 0000000..0befb70 --- /dev/null +++ b/scripts/ParseClientID.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +import argparse + +from vinetrimmer.utils.widevine.device import LocalDevice +from vinetrimmer.utils.widevine.protos.widevine_pb2 import ClientIdentification + +parser = argparse.ArgumentParser( + "Client identification parser", + description="Simple script to read a client id blob to see information about it" +) +parser.add_argument( + "input", + help="client id blob bin path or path to a wvd file", +) +args = parser.parse_args() + +client_id = ClientIdentification() +is_wvd = args.input.lower().endswith(".wvd") + +with open(args.input, "rb") as fd: + data = fd.read() + +if is_wvd: + client_id = LocalDevice.load(data).client_id +else: + client_id.ParseFromString(data) + +print(client_id) diff --git a/scripts/ParseKeybox.py b/scripts/ParseKeybox.py new file mode 100644 index 0000000..1368814 --- /dev/null +++ b/scripts/ParseKeybox.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +import argparse + +from vinetrimmer.utils.widevine.keybox import Keybox + +parser = argparse.ArgumentParser( + "Keybox parser", + description="Simple script to read a keybox to see information about it" +) +parser.add_argument( + "-k", "--keybox", + help="keybox path", + required=True) +args = parser.parse_args() + +keybox = Keybox.load(args.keybox) +print(repr(keybox)) diff --git a/scripts/ParsePSSH.py b/scripts/ParsePSSH.py new file mode 100644 index 0000000..650ccf4 --- /dev/null +++ b/scripts/ParsePSSH.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 + +import argparse +import base64 + +from vinetrimmer.utils.widevine.protos.widevine_pb2 import WidevineCencHeader +from vinetrimmer.vendor.pymp4.parser import Box + +parser = argparse.ArgumentParser( + "PSSH parser", + description="Simple script to read a PSSH to see information about it" +) +parser.add_argument( + "input", +) +args = parser.parse_args() + +args.input = base64.b64decode(args.input.encode("utf-8")) +box = Box.parse(args.input) +cenc_header = WidevineCencHeader() +cenc_header.ParseFromString(box.init_data) + +print("pssh box:") +print(box) + +print("init_data parsed as WidevineCencHeader:") +print(cenc_header) + +print("init_data's key_id as hex:") +print(cenc_header.key_id[0].hex()) diff --git a/scripts/TOMLtoYAML.py b/scripts/TOMLtoYAML.py new file mode 100644 index 0000000..f9d12da --- /dev/null +++ b/scripts/TOMLtoYAML.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import argparse +import json +import os + +import toml +import yaml + +parser = argparse.ArgumentParser() +parser.add_argument("path", help="directory containing .toml files to convert") +args = parser.parse_args() + +for root, dirs, files in os.walk(args.path): + for f in files: + if f.endswith(".toml"): + data = toml.load(os.path.join(root, f)) + # Convert to a real dict instead of weird toml object that pyyaml can't handle + data = json.loads(json.dumps(data)) + with open(os.path.join(root, f"{os.path.splitext(f)[0]}.yml"), "w") as fd: + print(f"Writing {os.path.realpath(fd.name)}") + fd.write(yaml.safe_dump(data, sort_keys=False)) diff --git a/scripts/UpdateLocalKeyVault.py b/scripts/UpdateLocalKeyVault.py new file mode 100644 index 0000000..19d6c0d --- /dev/null +++ b/scripts/UpdateLocalKeyVault.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 + +import argparse +import json +import sqlite3 + +from vinetrimmer.utils.AtomicSQL import AtomicSQL + + +class LocalVault: + def __init__(self, vault_path): + """ + Update local key vault to newer system. + This should ONLY be run if you have the old structure with keys in a table named `keys`. + It will move and update the structure of the items in `keys` to their respective new locations and structure. + :param vault_path: sqlite db path + """ + self.adb = AtomicSQL() + self.ticket = self.adb.load(sqlite3.connect(vault_path)) + if not self.table_exists("keys"): + return + rows = self.adb.safe_execute( + self.ticket, + lambda db, cursor: cursor.execute("SELECT `service`, `title`, `content_keys` FROM `keys`") + ).fetchall() + for service, title, content_keys in rows: + service = service.lower() + content_keys = json.loads(content_keys) + if not self.table_exists(service): + self.create_table(service) + for kid, key in [x.split(":") for x in content_keys]: + print(f"Inserting: {kid} {key} {title}") + existing_row, existing_title = self.row_exists(service, kid, key) + if existing_row: + if title and not existing_title: + print(" -- exists, but the title doesn't, so ill merge") + self.adb.safe_execute( + self.ticket, + lambda db, cursor: cursor.execute( + f"UPDATE `{service}` SET `title`=? WHERE `kid`=? AND `key_`=?", + (title, kid, key) + ) + ) + continue + print(" -- skipping (exists already)") + continue + self.adb.safe_execute( + self.ticket, + lambda db, cursor: cursor.execute( + f"INSERT INTO `{service}` (kid, key_, title) VALUES (?, ?, ?)", + (kid, key, title) + ) + ) + self.adb.commit(self.ticket) + + def row_exists(self, table, kid, key): + return self.adb.safe_execute( + self.ticket, + lambda db, cursor: cursor.execute( + f"SELECT count(id), title FROM `{table}` WHERE kid=? AND key_=?", + [kid, key] + ) + ).fetchone() + + def table_exists(self, name): + return self.adb.safe_execute( + self.ticket, + lambda db, cursor: cursor.execute( + "SELECT count(name) FROM sqlite_master WHERE type='table' AND name=?", + [name.lower()] + ) + ).fetchone()[0] == 1 + + def create_table(self, name): + self.adb.safe_execute( + self.ticket, + lambda db, cursor: cursor.execute( + """ + CREATE TABLE {} ( + "id" INTEGER NOT NULL UNIQUE, + "kid" TEXT NOT NULL COLLATE NOCASE, + "key_" TEXT NOT NULL COLLATE NOCASE, + "title" TEXT NULL, + PRIMARY KEY("id" AUTOINCREMENT), + UNIQUE("kid", "key_") + ); + """.format(name.lower()) + ) + ) + + +parser = argparse.ArgumentParser() +parser.add_argument( + "-i", "--input", + help="vault", + required=True) +args = parser.parse_args() + +LocalVault(args.input) diff --git a/scripts/VMPBlobGen/README.md b/scripts/VMPBlobGen/README.md new file mode 100644 index 0000000..1adc6a5 --- /dev/null +++ b/scripts/VMPBlobGen/README.md @@ -0,0 +1,8 @@ +# VMPBlobGen + +Notes on VMP: + +- Android doesn't require (or use!) a VMP blob (the oemcrypto hardware backs it and HDCP controls the path) +- Chrome and WidevineCDM both have signature files. The widevinecdm.dll and chrome.exe sign both the signature files, + then sign with the private key and inject to the license request in field 7, but you need a server cert to encrypt + the challenge otherwise. diff --git a/scripts/VMPBlobGen/VMPBlobGen.py b/scripts/VMPBlobGen/VMPBlobGen.py new file mode 100644 index 0000000..f153658 --- /dev/null +++ b/scripts/VMPBlobGen/VMPBlobGen.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 + +import os +import sys +from hashlib import sha512 + +from vinetrimmer.utils.widevine.protos.widevine_pb2 import FileHashes +from vinetrimmer.utils.widevine.vmp import WidevineSignatureReader + +""" +Script that generates a VMP blob for chromecdm +""" + +WIN32_FILES = [ + "chrome.exe", + "chrome.dll", + "chrome_child.dll", + "widevinecdmadapter.dll", + "widevinecdm.dll" +] + + +def sha512file(filename): + """Compute SHA-512 digest of file.""" + sha = sha512() + with open(filename, "rb") as fd: + for b in iter(lambda: fd.read(0x10000), b''): + sha.update(b) + return sha.digest() + + +def build_vmp_field(filenames): + """ + Create and fill out a FileHashes object. + + `filenames` is an array of pairs of filenames like (file, file_signature) + such as ("module.dll", "module.dll.sig"). This does not validate the signature + against the codesign root CA, or even the sha512 hash against the current signature+signer + """ + file_hashes = FileHashes() + + for basename, file, sig in filenames: + signature = WidevineSignatureReader.from_file(sig) + s = file_hashes.signatures.add() + s.filename = basename + s.test_signing = False # we can't check this without parsing signer + s.SHA512Hash = sha512file(file) + s.main_exe = signature.mainexe + s.signature = signature.signature + + file_hashes.signer = signature.signer + return file_hashes.SerializeToString() + + +def get_files_with_signatures(path, required_files=None, random_order=False, sig_ext="sig"): + """ + use on chrome dir (a given version). + random_order would put any files it found in the dir with sigs, + it's not the right way to do it and the browser does not do this. + this function can still fail (generate wrong output) in subtle ways if + the Chrome dir has copies of the exe/sigs, especially if those copies are modified in some way + """ + if not required_files: + required_files = WIN32_FILES + + all_files = [] + sig_files = [] + for dir_path, _, filenames in os.walk(path): + for filename in filenames: + full_path = os.path.join(dir_path, filename) + all_files.append(full_path) + if filename.endswith(sig_ext): + sig_files.append(full_path) + + base_names = [] + for path in sig_files: + orig_path = os.path.splitext(path)[0] + if orig_path not in all_files: + print("signature file {} lacks original file {}".format(path, orig_path)) + base_names.append(path.name) + + if not set(base_names).issuperset(set(required_files)): + # or should just make this warn as the next exception would be more specific + raise ValueError("Missing a binary/signature pair from {}".format(required_files)) + + files_to_hash = [] + if random_order: + for path in sig_files: + orig_path = os.path.splitext(path)[0] + files_to_hash.append((os.path.basename(orig_path), orig_path, path)) + else: + for basename in required_files: + found_file = False + for path in sig_files: + orig_path = os.path.splitext(path)[0] + if orig_path.endswith(basename): + files_to_hash.append((basename, orig_path, path)) + found_file = True + break + if not found_file: + raise Exception("Failed to locate a file sig/pair for {}".format(basename)) + + return files_to_hash + + +def make_vmp_buff(browser_dir, file_msg_out): + with open(file_msg_out, "wb") as fd: + fd.write(build_vmp_field(get_files_with_signatures(browser_dir))) + + +if len(sys.argv) < 3: + print("Usage: {} BrowserPathWithVersion OutputPBMessage.bin".format(sys.argv[0])) +else: + make_vmp_buff(sys.argv[1], sys.argv[2]) diff --git a/scripts/WVD/JsonWVDtoStructWVD.py b/scripts/WVD/JsonWVDtoStructWVD.py new file mode 100644 index 0000000..5ec77e4 --- /dev/null +++ b/scripts/WVD/JsonWVDtoStructWVD.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 + +import argparse +import base64 +import json +import os + +from vinetrimmer.utils.widevine.device import LocalDevice + +""" +Code to convert common folder/file structure to a vinetrimmer WVD. +""" + +parser = argparse.ArgumentParser( + "JsonWVDtoStructWVD", + description="Simple script to read cdm data from old wvd json and write it into a new WVD struct file." +) +parser.add_argument( + "-i", "--input", + help="path to wvd json file", + required=False) +parser.add_argument( + "-d", "--dir", + help="path to MULTIPLE wvd json files", + required=False) +args = parser.parse_args() + +files = [] +if args.dir: + files.extend(os.listdir(args.dir)) +elif args.input: + files.append(args.input) + +for file in files: + if not file.lower().endswith(".wvd") or os.path.splitext(file)[0].endswith(".struct"): + continue + + if not os.path.isfile(file): + raise ValueError("Not a file or doesn't exist...") + + print(f"Generating wvd struct file for {file}...") + + with open(file, encoding="utf-8") as fd: + wvd_json = json.load(fd) + + device = LocalDevice( + type=LocalDevice.Types[wvd_json["device_type"].upper()], + security_level=wvd_json["security_level"], + flags={ + "send_key_control_nonce": wvd_json["send_key_control_nonce"] + }, + private_key=base64.b64decode(wvd_json["device_private_key"]), + client_id=base64.b64decode(wvd_json["device_client_id_blob"]), + vmp=base64.b64decode(wvd_json["device_vmp_blob"]) if wvd_json.get("device_vmp_blob") else None + ) + + out = os.path.join(os.path.dirname(file), "structs", os.path.basename(file)) + os.makedirs(os.path.dirname(out), exist_ok=True) + + device.dump(out) + + print(device) + print(f"Done: {file}") + +print("Done") diff --git a/scripts/WVD/MakeWVD.py b/scripts/WVD/MakeWVD.py new file mode 100644 index 0000000..70b7dd2 --- /dev/null +++ b/scripts/WVD/MakeWVD.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +import argparse +import json +import os +import re +import sys + +from vinetrimmer.utils.widevine.device import LocalDevice + +""" +Code to convert common folder/file structure to a vinetrimmer WVD. +""" + +parser = argparse.ArgumentParser() +parser.add_argument("dirs", metavar="DIR", nargs="+", help="Directory containing device files") +args = parser.parse_args() + +configs = [] +for d in args.dirs: + for root, dirs, files in os.walk(d): + for f in files: + if f == "wv.json": + configs.append(os.path.join(root, f)) + +if not configs: + print("No wv.json file found in any of the specified directories.") + sys.exit(1) + +for f in configs: + d = os.path.dirname(f) + + print(f"Generating WVD struct file for {os.path.abspath(d)}...") + + with open(f, encoding="utf-8") as fd: + config = json.load(fd) + + device = LocalDevice.from_dir(d) + + # we cannot output to /data/CDM_Devices etc. as the CWD might not align up + # also best to keep the security level and system id definition on the filename for easy referencing + name = re.sub(r"_lvl\d$", "", config["name"]) + out_path = f"{name}_l{device.security_level}_{device.system_id}.wvd" + + device.dump(out_path) + + print(device) + + print(f"Done, saved to: {os.path.abspath(out_path)}") + print() diff --git a/scripts/protobuf3/READEME.MD b/scripts/protobuf3/READEME.MD new file mode 100644 index 0000000..e69de29 diff --git a/scripts/protobuf3/protobuf3/__init__.py b/scripts/protobuf3/protobuf3/__init__.py new file mode 100644 index 0000000..7dff9a9 --- /dev/null +++ b/scripts/protobuf3/protobuf3/__init__.py @@ -0,0 +1,33 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# Copyright 2007 Google Inc. All Rights Reserved. + +__version__ = '3.20.2' diff --git a/scripts/protobuf3/protobuf3/any_pb2.py b/scripts/protobuf3/protobuf3/any_pb2.py new file mode 100644 index 0000000..9121193 --- /dev/null +++ b/scripts/protobuf3/protobuf3/any_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/any.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19google/protobuf/any.proto\x12\x0fgoogle.protobuf\"&\n\x03\x41ny\x12\x10\n\x08type_url\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c\x42v\n\x13\x63om.google.protobufB\x08\x41nyProtoP\x01Z,google.golang.org/protobuf/types/known/anypb\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.any_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\010AnyProtoP\001Z,google.golang.org/protobuf/types/known/anypb\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _ANY._serialized_start=46 + _ANY._serialized_end=84 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/api_pb2.py b/scripts/protobuf3/protobuf3/api_pb2.py new file mode 100644 index 0000000..1721b10 --- /dev/null +++ b/scripts/protobuf3/protobuf3/api_pb2.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/api.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import source_context_pb2 as google_dot_protobuf_dot_source__context__pb2 +from google.protobuf import type_pb2 as google_dot_protobuf_dot_type__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19google/protobuf/api.proto\x12\x0fgoogle.protobuf\x1a$google/protobuf/source_context.proto\x1a\x1agoogle/protobuf/type.proto\"\x81\x02\n\x03\x41pi\x12\x0c\n\x04name\x18\x01 \x01(\t\x12(\n\x07methods\x18\x02 \x03(\x0b\x32\x17.google.protobuf.Method\x12(\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.Option\x12\x0f\n\x07version\x18\x04 \x01(\t\x12\x36\n\x0esource_context\x18\x05 \x01(\x0b\x32\x1e.google.protobuf.SourceContext\x12&\n\x06mixins\x18\x06 \x03(\x0b\x32\x16.google.protobuf.Mixin\x12\'\n\x06syntax\x18\x07 \x01(\x0e\x32\x17.google.protobuf.Syntax\"\xd5\x01\n\x06Method\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x18\n\x10request_type_url\x18\x02 \x01(\t\x12\x19\n\x11request_streaming\x18\x03 \x01(\x08\x12\x19\n\x11response_type_url\x18\x04 \x01(\t\x12\x1a\n\x12response_streaming\x18\x05 \x01(\x08\x12(\n\x07options\x18\x06 \x03(\x0b\x32\x17.google.protobuf.Option\x12\'\n\x06syntax\x18\x07 \x01(\x0e\x32\x17.google.protobuf.Syntax\"#\n\x05Mixin\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04root\x18\x02 \x01(\tBv\n\x13\x63om.google.protobufB\x08\x41piProtoP\x01Z,google.golang.org/protobuf/types/known/apipb\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.api_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\010ApiProtoP\001Z,google.golang.org/protobuf/types/known/apipb\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _API._serialized_start=113 + _API._serialized_end=370 + _METHOD._serialized_start=373 + _METHOD._serialized_end=586 + _MIXIN._serialized_start=588 + _MIXIN._serialized_end=623 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/compiler/__init__.py b/scripts/protobuf3/protobuf3/compiler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/protobuf3/protobuf3/compiler/plugin_pb2.py b/scripts/protobuf3/protobuf3/compiler/plugin_pb2.py new file mode 100644 index 0000000..715a891 --- /dev/null +++ b/scripts/protobuf3/protobuf3/compiler/plugin_pb2.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/compiler/plugin.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%google/protobuf/compiler/plugin.proto\x12\x18google.protobuf.compiler\x1a google/protobuf/descriptor.proto\"F\n\x07Version\x12\r\n\x05major\x18\x01 \x01(\x05\x12\r\n\x05minor\x18\x02 \x01(\x05\x12\r\n\x05patch\x18\x03 \x01(\x05\x12\x0e\n\x06suffix\x18\x04 \x01(\t\"\xba\x01\n\x14\x43odeGeneratorRequest\x12\x18\n\x10\x66ile_to_generate\x18\x01 \x03(\t\x12\x11\n\tparameter\x18\x02 \x01(\t\x12\x38\n\nproto_file\x18\x0f \x03(\x0b\x32$.google.protobuf.FileDescriptorProto\x12;\n\x10\x63ompiler_version\x18\x03 \x01(\x0b\x32!.google.protobuf.compiler.Version\"\xc1\x02\n\x15\x43odeGeneratorResponse\x12\r\n\x05\x65rror\x18\x01 \x01(\t\x12\x1a\n\x12supported_features\x18\x02 \x01(\x04\x12\x42\n\x04\x66ile\x18\x0f \x03(\x0b\x32\x34.google.protobuf.compiler.CodeGeneratorResponse.File\x1a\x7f\n\x04\x46ile\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x17\n\x0finsertion_point\x18\x02 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x0f \x01(\t\x12?\n\x13generated_code_info\x18\x10 \x01(\x0b\x32\".google.protobuf.GeneratedCodeInfo\"8\n\x07\x46\x65\x61ture\x12\x10\n\x0c\x46\x45\x41TURE_NONE\x10\x00\x12\x1b\n\x17\x46\x45\x41TURE_PROTO3_OPTIONAL\x10\x01\x42W\n\x1c\x63om.google.protobuf.compilerB\x0cPluginProtosZ)google.golang.org/protobuf/types/pluginpb') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.compiler.plugin_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\034com.google.protobuf.compilerB\014PluginProtosZ)google.golang.org/protobuf/types/pluginpb' + _VERSION._serialized_start=101 + _VERSION._serialized_end=171 + _CODEGENERATORREQUEST._serialized_start=174 + _CODEGENERATORREQUEST._serialized_end=360 + _CODEGENERATORRESPONSE._serialized_start=363 + _CODEGENERATORRESPONSE._serialized_end=684 + _CODEGENERATORRESPONSE_FILE._serialized_start=499 + _CODEGENERATORRESPONSE_FILE._serialized_end=626 + _CODEGENERATORRESPONSE_FEATURE._serialized_start=628 + _CODEGENERATORRESPONSE_FEATURE._serialized_end=684 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/descriptor.py b/scripts/protobuf3/protobuf3/descriptor.py new file mode 100644 index 0000000..ad70be9 --- /dev/null +++ b/scripts/protobuf3/protobuf3/descriptor.py @@ -0,0 +1,1224 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Descriptors essentially contain exactly the information found in a .proto +file, in types that make this information accessible in Python. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + +import threading +import warnings + +from google.protobuf.internal import api_implementation + +_USE_C_DESCRIPTORS = False +if api_implementation.Type() == 'cpp': + # Used by MakeDescriptor in cpp mode + import binascii + import os + from google.protobuf.pyext import _message + _USE_C_DESCRIPTORS = True + + +class Error(Exception): + """Base error for this module.""" + + +class TypeTransformationError(Error): + """Error transforming between python proto type and corresponding C++ type.""" + + +if _USE_C_DESCRIPTORS: + # This metaclass allows to override the behavior of code like + # isinstance(my_descriptor, FieldDescriptor) + # and make it return True when the descriptor is an instance of the extension + # type written in C++. + class DescriptorMetaclass(type): + def __instancecheck__(cls, obj): + if super(DescriptorMetaclass, cls).__instancecheck__(obj): + return True + if isinstance(obj, cls._C_DESCRIPTOR_CLASS): + return True + return False +else: + # The standard metaclass; nothing changes. + DescriptorMetaclass = type + + +class _Lock(object): + """Wrapper class of threading.Lock(), which is allowed by 'with'.""" + + def __new__(cls): + self = object.__new__(cls) + self._lock = threading.Lock() # pylint: disable=protected-access + return self + + def __enter__(self): + self._lock.acquire() + + def __exit__(self, exc_type, exc_value, exc_tb): + self._lock.release() + + +_lock = threading.Lock() + + +def _Deprecated(name): + if _Deprecated.count > 0: + _Deprecated.count -= 1 + warnings.warn( + 'Call to deprecated create function %s(). Note: Create unlinked ' + 'descriptors is going to go away. Please use get/find descriptors from ' + 'generated code or query the descriptor_pool.' + % name, + category=DeprecationWarning, stacklevel=3) + + +# Deprecated warnings will print 100 times at most which should be enough for +# users to notice and do not cause timeout. +_Deprecated.count = 100 + + +_internal_create_key = object() + + +class DescriptorBase(metaclass=DescriptorMetaclass): + + """Descriptors base class. + + This class is the base of all descriptor classes. It provides common options + related functionality. + + Attributes: + has_options: True if the descriptor has non-default options. Usually it + is not necessary to read this -- just call GetOptions() which will + happily return the default instance. However, it's sometimes useful + for efficiency, and also useful inside the protobuf implementation to + avoid some bootstrapping issues. + """ + + if _USE_C_DESCRIPTORS: + # The class, or tuple of classes, that are considered as "virtual + # subclasses" of this descriptor class. + _C_DESCRIPTOR_CLASS = () + + def __init__(self, options, serialized_options, options_class_name): + """Initialize the descriptor given its options message and the name of the + class of the options message. The name of the class is required in case + the options message is None and has to be created. + """ + self._options = options + self._options_class_name = options_class_name + self._serialized_options = serialized_options + + # Does this descriptor have non-default options? + self.has_options = (options is not None) or (serialized_options is not None) + + def _SetOptions(self, options, options_class_name): + """Sets the descriptor's options + + This function is used in generated proto2 files to update descriptor + options. It must not be used outside proto2. + """ + self._options = options + self._options_class_name = options_class_name + + # Does this descriptor have non-default options? + self.has_options = options is not None + + def GetOptions(self): + """Retrieves descriptor options. + + This method returns the options set or creates the default options for the + descriptor. + """ + if self._options: + return self._options + + from google.protobuf import descriptor_pb2 + try: + options_class = getattr(descriptor_pb2, + self._options_class_name) + except AttributeError: + raise RuntimeError('Unknown options class name %s!' % + (self._options_class_name)) + + with _lock: + if self._serialized_options is None: + self._options = options_class() + else: + self._options = _ParseOptions(options_class(), + self._serialized_options) + + return self._options + + +class _NestedDescriptorBase(DescriptorBase): + """Common class for descriptors that can be nested.""" + + def __init__(self, options, options_class_name, name, full_name, + file, containing_type, serialized_start=None, + serialized_end=None, serialized_options=None): + """Constructor. + + Args: + options: Protocol message options or None + to use default message options. + options_class_name (str): The class name of the above options. + name (str): Name of this protocol message type. + full_name (str): Fully-qualified name of this protocol message type, + which will include protocol "package" name and the name of any + enclosing types. + file (FileDescriptor): Reference to file info. + containing_type: if provided, this is a nested descriptor, with this + descriptor as parent, otherwise None. + serialized_start: The start index (inclusive) in block in the + file.serialized_pb that describes this descriptor. + serialized_end: The end index (exclusive) in block in the + file.serialized_pb that describes this descriptor. + serialized_options: Protocol message serialized options or None. + """ + super(_NestedDescriptorBase, self).__init__( + options, serialized_options, options_class_name) + + self.name = name + # TODO(falk): Add function to calculate full_name instead of having it in + # memory? + self.full_name = full_name + self.file = file + self.containing_type = containing_type + + self._serialized_start = serialized_start + self._serialized_end = serialized_end + + def CopyToProto(self, proto): + """Copies this to the matching proto in descriptor_pb2. + + Args: + proto: An empty proto instance from descriptor_pb2. + + Raises: + Error: If self couldn't be serialized, due to to few constructor + arguments. + """ + if (self.file is not None and + self._serialized_start is not None and + self._serialized_end is not None): + proto.ParseFromString(self.file.serialized_pb[ + self._serialized_start:self._serialized_end]) + else: + raise Error('Descriptor does not contain serialization.') + + +class Descriptor(_NestedDescriptorBase): + + """Descriptor for a protocol message type. + + Attributes: + name (str): Name of this protocol message type. + full_name (str): Fully-qualified name of this protocol message type, + which will include protocol "package" name and the name of any + enclosing types. + containing_type (Descriptor): Reference to the descriptor of the type + containing us, or None if this is top-level. + fields (list[FieldDescriptor]): Field descriptors for all fields in + this type. + fields_by_number (dict(int, FieldDescriptor)): Same + :class:`FieldDescriptor` objects as in :attr:`fields`, but indexed + by "number" attribute in each FieldDescriptor. + fields_by_name (dict(str, FieldDescriptor)): Same + :class:`FieldDescriptor` objects as in :attr:`fields`, but indexed by + "name" attribute in each :class:`FieldDescriptor`. + nested_types (list[Descriptor]): Descriptor references + for all protocol message types nested within this one. + nested_types_by_name (dict(str, Descriptor)): Same Descriptor + objects as in :attr:`nested_types`, but indexed by "name" attribute + in each Descriptor. + enum_types (list[EnumDescriptor]): :class:`EnumDescriptor` references + for all enums contained within this type. + enum_types_by_name (dict(str, EnumDescriptor)): Same + :class:`EnumDescriptor` objects as in :attr:`enum_types`, but + indexed by "name" attribute in each EnumDescriptor. + enum_values_by_name (dict(str, EnumValueDescriptor)): Dict mapping + from enum value name to :class:`EnumValueDescriptor` for that value. + extensions (list[FieldDescriptor]): All extensions defined directly + within this message type (NOT within a nested type). + extensions_by_name (dict(str, FieldDescriptor)): Same FieldDescriptor + objects as :attr:`extensions`, but indexed by "name" attribute of each + FieldDescriptor. + is_extendable (bool): Does this type define any extension ranges? + oneofs (list[OneofDescriptor]): The list of descriptors for oneof fields + in this message. + oneofs_by_name (dict(str, OneofDescriptor)): Same objects as in + :attr:`oneofs`, but indexed by "name" attribute. + file (FileDescriptor): Reference to file descriptor. + + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.Descriptor + + def __new__( + cls, + name=None, + full_name=None, + filename=None, + containing_type=None, + fields=None, + nested_types=None, + enum_types=None, + extensions=None, + options=None, + serialized_options=None, + is_extendable=True, + extension_ranges=None, + oneofs=None, + file=None, # pylint: disable=redefined-builtin + serialized_start=None, + serialized_end=None, + syntax=None, + create_key=None): + _message.Message._CheckCalledFromGeneratedFile() + return _message.default_pool.FindMessageTypeByName(full_name) + + # NOTE(tmarek): The file argument redefining a builtin is nothing we can + # fix right now since we don't know how many clients already rely on the + # name of the argument. + def __init__(self, name, full_name, filename, containing_type, fields, + nested_types, enum_types, extensions, options=None, + serialized_options=None, + is_extendable=True, extension_ranges=None, oneofs=None, + file=None, serialized_start=None, serialized_end=None, # pylint: disable=redefined-builtin + syntax=None, create_key=None): + """Arguments to __init__() are as described in the description + of Descriptor fields above. + + Note that filename is an obsolete argument, that is not used anymore. + Please use file.name to access this as an attribute. + """ + if create_key is not _internal_create_key: + _Deprecated('Descriptor') + + super(Descriptor, self).__init__( + options, 'MessageOptions', name, full_name, file, + containing_type, serialized_start=serialized_start, + serialized_end=serialized_end, serialized_options=serialized_options) + + # We have fields in addition to fields_by_name and fields_by_number, + # so that: + # 1. Clients can index fields by "order in which they're listed." + # 2. Clients can easily iterate over all fields with the terse + # syntax: for f in descriptor.fields: ... + self.fields = fields + for field in self.fields: + field.containing_type = self + self.fields_by_number = dict((f.number, f) for f in fields) + self.fields_by_name = dict((f.name, f) for f in fields) + self._fields_by_camelcase_name = None + + self.nested_types = nested_types + for nested_type in nested_types: + nested_type.containing_type = self + self.nested_types_by_name = dict((t.name, t) for t in nested_types) + + self.enum_types = enum_types + for enum_type in self.enum_types: + enum_type.containing_type = self + self.enum_types_by_name = dict((t.name, t) for t in enum_types) + self.enum_values_by_name = dict( + (v.name, v) for t in enum_types for v in t.values) + + self.extensions = extensions + for extension in self.extensions: + extension.extension_scope = self + self.extensions_by_name = dict((f.name, f) for f in extensions) + self.is_extendable = is_extendable + self.extension_ranges = extension_ranges + self.oneofs = oneofs if oneofs is not None else [] + self.oneofs_by_name = dict((o.name, o) for o in self.oneofs) + for oneof in self.oneofs: + oneof.containing_type = self + self.syntax = syntax or "proto2" + + @property + def fields_by_camelcase_name(self): + """Same FieldDescriptor objects as in :attr:`fields`, but indexed by + :attr:`FieldDescriptor.camelcase_name`. + """ + if self._fields_by_camelcase_name is None: + self._fields_by_camelcase_name = dict( + (f.camelcase_name, f) for f in self.fields) + return self._fields_by_camelcase_name + + def EnumValueName(self, enum, value): + """Returns the string name of an enum value. + + This is just a small helper method to simplify a common operation. + + Args: + enum: string name of the Enum. + value: int, value of the enum. + + Returns: + string name of the enum value. + + Raises: + KeyError if either the Enum doesn't exist or the value is not a valid + value for the enum. + """ + return self.enum_types_by_name[enum].values_by_number[value].name + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.DescriptorProto. + + Args: + proto: An empty descriptor_pb2.DescriptorProto. + """ + # This function is overridden to give a better doc comment. + super(Descriptor, self).CopyToProto(proto) + + +# TODO(robinson): We should have aggressive checking here, +# for example: +# * If you specify a repeated field, you should not be allowed +# to specify a default value. +# * [Other examples here as needed]. +# +# TODO(robinson): for this and other *Descriptor classes, we +# might also want to lock things down aggressively (e.g., +# prevent clients from setting the attributes). Having +# stronger invariants here in general will reduce the number +# of runtime checks we must do in reflection.py... +class FieldDescriptor(DescriptorBase): + + """Descriptor for a single field in a .proto file. + + Attributes: + name (str): Name of this field, exactly as it appears in .proto. + full_name (str): Name of this field, including containing scope. This is + particularly relevant for extensions. + index (int): Dense, 0-indexed index giving the order that this + field textually appears within its message in the .proto file. + number (int): Tag number declared for this field in the .proto file. + + type (int): (One of the TYPE_* constants below) Declared type. + cpp_type (int): (One of the CPPTYPE_* constants below) C++ type used to + represent this field. + + label (int): (One of the LABEL_* constants below) Tells whether this + field is optional, required, or repeated. + has_default_value (bool): True if this field has a default value defined, + otherwise false. + default_value (Varies): Default value of this field. Only + meaningful for non-repeated scalar fields. Repeated fields + should always set this to [], and non-repeated composite + fields should always set this to None. + + containing_type (Descriptor): Descriptor of the protocol message + type that contains this field. Set by the Descriptor constructor + if we're passed into one. + Somewhat confusingly, for extension fields, this is the + descriptor of the EXTENDED message, not the descriptor + of the message containing this field. (See is_extension and + extension_scope below). + message_type (Descriptor): If a composite field, a descriptor + of the message type contained in this field. Otherwise, this is None. + enum_type (EnumDescriptor): If this field contains an enum, a + descriptor of that enum. Otherwise, this is None. + + is_extension: True iff this describes an extension field. + extension_scope (Descriptor): Only meaningful if is_extension is True. + Gives the message that immediately contains this extension field. + Will be None iff we're a top-level (file-level) extension field. + + options (descriptor_pb2.FieldOptions): Protocol message field options or + None to use default field options. + + containing_oneof (OneofDescriptor): If the field is a member of a oneof + union, contains its descriptor. Otherwise, None. + + file (FileDescriptor): Reference to file descriptor. + """ + + # Must be consistent with C++ FieldDescriptor::Type enum in + # descriptor.h. + # + # TODO(robinson): Find a way to eliminate this repetition. + TYPE_DOUBLE = 1 + TYPE_FLOAT = 2 + TYPE_INT64 = 3 + TYPE_UINT64 = 4 + TYPE_INT32 = 5 + TYPE_FIXED64 = 6 + TYPE_FIXED32 = 7 + TYPE_BOOL = 8 + TYPE_STRING = 9 + TYPE_GROUP = 10 + TYPE_MESSAGE = 11 + TYPE_BYTES = 12 + TYPE_UINT32 = 13 + TYPE_ENUM = 14 + TYPE_SFIXED32 = 15 + TYPE_SFIXED64 = 16 + TYPE_SINT32 = 17 + TYPE_SINT64 = 18 + MAX_TYPE = 18 + + # Must be consistent with C++ FieldDescriptor::CppType enum in + # descriptor.h. + # + # TODO(robinson): Find a way to eliminate this repetition. + CPPTYPE_INT32 = 1 + CPPTYPE_INT64 = 2 + CPPTYPE_UINT32 = 3 + CPPTYPE_UINT64 = 4 + CPPTYPE_DOUBLE = 5 + CPPTYPE_FLOAT = 6 + CPPTYPE_BOOL = 7 + CPPTYPE_ENUM = 8 + CPPTYPE_STRING = 9 + CPPTYPE_MESSAGE = 10 + MAX_CPPTYPE = 10 + + _PYTHON_TO_CPP_PROTO_TYPE_MAP = { + TYPE_DOUBLE: CPPTYPE_DOUBLE, + TYPE_FLOAT: CPPTYPE_FLOAT, + TYPE_ENUM: CPPTYPE_ENUM, + TYPE_INT64: CPPTYPE_INT64, + TYPE_SINT64: CPPTYPE_INT64, + TYPE_SFIXED64: CPPTYPE_INT64, + TYPE_UINT64: CPPTYPE_UINT64, + TYPE_FIXED64: CPPTYPE_UINT64, + TYPE_INT32: CPPTYPE_INT32, + TYPE_SFIXED32: CPPTYPE_INT32, + TYPE_SINT32: CPPTYPE_INT32, + TYPE_UINT32: CPPTYPE_UINT32, + TYPE_FIXED32: CPPTYPE_UINT32, + TYPE_BYTES: CPPTYPE_STRING, + TYPE_STRING: CPPTYPE_STRING, + TYPE_BOOL: CPPTYPE_BOOL, + TYPE_MESSAGE: CPPTYPE_MESSAGE, + TYPE_GROUP: CPPTYPE_MESSAGE + } + + # Must be consistent with C++ FieldDescriptor::Label enum in + # descriptor.h. + # + # TODO(robinson): Find a way to eliminate this repetition. + LABEL_OPTIONAL = 1 + LABEL_REQUIRED = 2 + LABEL_REPEATED = 3 + MAX_LABEL = 3 + + # Must be consistent with C++ constants kMaxNumber, kFirstReservedNumber, + # and kLastReservedNumber in descriptor.h + MAX_FIELD_NUMBER = (1 << 29) - 1 + FIRST_RESERVED_FIELD_NUMBER = 19000 + LAST_RESERVED_FIELD_NUMBER = 19999 + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.FieldDescriptor + + def __new__(cls, name, full_name, index, number, type, cpp_type, label, + default_value, message_type, enum_type, containing_type, + is_extension, extension_scope, options=None, + serialized_options=None, + has_default_value=True, containing_oneof=None, json_name=None, + file=None, create_key=None): # pylint: disable=redefined-builtin + _message.Message._CheckCalledFromGeneratedFile() + if is_extension: + return _message.default_pool.FindExtensionByName(full_name) + else: + return _message.default_pool.FindFieldByName(full_name) + + def __init__(self, name, full_name, index, number, type, cpp_type, label, + default_value, message_type, enum_type, containing_type, + is_extension, extension_scope, options=None, + serialized_options=None, + has_default_value=True, containing_oneof=None, json_name=None, + file=None, create_key=None): # pylint: disable=redefined-builtin + """The arguments are as described in the description of FieldDescriptor + attributes above. + + Note that containing_type may be None, and may be set later if necessary + (to deal with circular references between message types, for example). + Likewise for extension_scope. + """ + if create_key is not _internal_create_key: + _Deprecated('FieldDescriptor') + + super(FieldDescriptor, self).__init__( + options, serialized_options, 'FieldOptions') + self.name = name + self.full_name = full_name + self.file = file + self._camelcase_name = None + if json_name is None: + self.json_name = _ToJsonName(name) + else: + self.json_name = json_name + self.index = index + self.number = number + self.type = type + self.cpp_type = cpp_type + self.label = label + self.has_default_value = has_default_value + self.default_value = default_value + self.containing_type = containing_type + self.message_type = message_type + self.enum_type = enum_type + self.is_extension = is_extension + self.extension_scope = extension_scope + self.containing_oneof = containing_oneof + if api_implementation.Type() == 'cpp': + if is_extension: + self._cdescriptor = _message.default_pool.FindExtensionByName(full_name) + else: + self._cdescriptor = _message.default_pool.FindFieldByName(full_name) + else: + self._cdescriptor = None + + @property + def camelcase_name(self): + """Camelcase name of this field. + + Returns: + str: the name in CamelCase. + """ + if self._camelcase_name is None: + self._camelcase_name = _ToCamelCase(self.name) + return self._camelcase_name + + @property + def has_presence(self): + """Whether the field distinguishes between unpopulated and default values. + + Raises: + RuntimeError: singular field that is not linked with message nor file. + """ + if self.label == FieldDescriptor.LABEL_REPEATED: + return False + if (self.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE or + self.containing_oneof): + return True + if hasattr(self.file, 'syntax'): + return self.file.syntax == 'proto2' + if hasattr(self.message_type, 'syntax'): + return self.message_type.syntax == 'proto2' + raise RuntimeError( + 'has_presence is not ready to use because field %s is not' + ' linked with message type nor file' % self.full_name) + + @staticmethod + def ProtoTypeToCppProtoType(proto_type): + """Converts from a Python proto type to a C++ Proto Type. + + The Python ProtocolBuffer classes specify both the 'Python' datatype and the + 'C++' datatype - and they're not the same. This helper method should + translate from one to another. + + Args: + proto_type: the Python proto type (descriptor.FieldDescriptor.TYPE_*) + Returns: + int: descriptor.FieldDescriptor.CPPTYPE_*, the C++ type. + Raises: + TypeTransformationError: when the Python proto type isn't known. + """ + try: + return FieldDescriptor._PYTHON_TO_CPP_PROTO_TYPE_MAP[proto_type] + except KeyError: + raise TypeTransformationError('Unknown proto_type: %s' % proto_type) + + +class EnumDescriptor(_NestedDescriptorBase): + + """Descriptor for an enum defined in a .proto file. + + Attributes: + name (str): Name of the enum type. + full_name (str): Full name of the type, including package name + and any enclosing type(s). + + values (list[EnumValueDescriptor]): List of the values + in this enum. + values_by_name (dict(str, EnumValueDescriptor)): Same as :attr:`values`, + but indexed by the "name" field of each EnumValueDescriptor. + values_by_number (dict(int, EnumValueDescriptor)): Same as :attr:`values`, + but indexed by the "number" field of each EnumValueDescriptor. + containing_type (Descriptor): Descriptor of the immediate containing + type of this enum, or None if this is an enum defined at the + top level in a .proto file. Set by Descriptor's constructor + if we're passed into one. + file (FileDescriptor): Reference to file descriptor. + options (descriptor_pb2.EnumOptions): Enum options message or + None to use default enum options. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.EnumDescriptor + + def __new__(cls, name, full_name, filename, values, + containing_type=None, options=None, + serialized_options=None, file=None, # pylint: disable=redefined-builtin + serialized_start=None, serialized_end=None, create_key=None): + _message.Message._CheckCalledFromGeneratedFile() + return _message.default_pool.FindEnumTypeByName(full_name) + + def __init__(self, name, full_name, filename, values, + containing_type=None, options=None, + serialized_options=None, file=None, # pylint: disable=redefined-builtin + serialized_start=None, serialized_end=None, create_key=None): + """Arguments are as described in the attribute description above. + + Note that filename is an obsolete argument, that is not used anymore. + Please use file.name to access this as an attribute. + """ + if create_key is not _internal_create_key: + _Deprecated('EnumDescriptor') + + super(EnumDescriptor, self).__init__( + options, 'EnumOptions', name, full_name, file, + containing_type, serialized_start=serialized_start, + serialized_end=serialized_end, serialized_options=serialized_options) + + self.values = values + for value in self.values: + value.type = self + self.values_by_name = dict((v.name, v) for v in values) + # Values are reversed to ensure that the first alias is retained. + self.values_by_number = dict((v.number, v) for v in reversed(values)) + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.EnumDescriptorProto. + + Args: + proto (descriptor_pb2.EnumDescriptorProto): An empty descriptor proto. + """ + # This function is overridden to give a better doc comment. + super(EnumDescriptor, self).CopyToProto(proto) + + +class EnumValueDescriptor(DescriptorBase): + + """Descriptor for a single value within an enum. + + Attributes: + name (str): Name of this value. + index (int): Dense, 0-indexed index giving the order that this + value appears textually within its enum in the .proto file. + number (int): Actual number assigned to this enum value. + type (EnumDescriptor): :class:`EnumDescriptor` to which this value + belongs. Set by :class:`EnumDescriptor`'s constructor if we're + passed into one. + options (descriptor_pb2.EnumValueOptions): Enum value options message or + None to use default enum value options options. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.EnumValueDescriptor + + def __new__(cls, name, index, number, + type=None, # pylint: disable=redefined-builtin + options=None, serialized_options=None, create_key=None): + _message.Message._CheckCalledFromGeneratedFile() + # There is no way we can build a complete EnumValueDescriptor with the + # given parameters (the name of the Enum is not known, for example). + # Fortunately generated files just pass it to the EnumDescriptor() + # constructor, which will ignore it, so returning None is good enough. + return None + + def __init__(self, name, index, number, + type=None, # pylint: disable=redefined-builtin + options=None, serialized_options=None, create_key=None): + """Arguments are as described in the attribute description above.""" + if create_key is not _internal_create_key: + _Deprecated('EnumValueDescriptor') + + super(EnumValueDescriptor, self).__init__( + options, serialized_options, 'EnumValueOptions') + self.name = name + self.index = index + self.number = number + self.type = type + + +class OneofDescriptor(DescriptorBase): + """Descriptor for a oneof field. + + Attributes: + name (str): Name of the oneof field. + full_name (str): Full name of the oneof field, including package name. + index (int): 0-based index giving the order of the oneof field inside + its containing type. + containing_type (Descriptor): :class:`Descriptor` of the protocol message + type that contains this field. Set by the :class:`Descriptor` constructor + if we're passed into one. + fields (list[FieldDescriptor]): The list of field descriptors this + oneof can contain. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.OneofDescriptor + + def __new__( + cls, name, full_name, index, containing_type, fields, options=None, + serialized_options=None, create_key=None): + _message.Message._CheckCalledFromGeneratedFile() + return _message.default_pool.FindOneofByName(full_name) + + def __init__( + self, name, full_name, index, containing_type, fields, options=None, + serialized_options=None, create_key=None): + """Arguments are as described in the attribute description above.""" + if create_key is not _internal_create_key: + _Deprecated('OneofDescriptor') + + super(OneofDescriptor, self).__init__( + options, serialized_options, 'OneofOptions') + self.name = name + self.full_name = full_name + self.index = index + self.containing_type = containing_type + self.fields = fields + + +class ServiceDescriptor(_NestedDescriptorBase): + + """Descriptor for a service. + + Attributes: + name (str): Name of the service. + full_name (str): Full name of the service, including package name. + index (int): 0-indexed index giving the order that this services + definition appears within the .proto file. + methods (list[MethodDescriptor]): List of methods provided by this + service. + methods_by_name (dict(str, MethodDescriptor)): Same + :class:`MethodDescriptor` objects as in :attr:`methods_by_name`, but + indexed by "name" attribute in each :class:`MethodDescriptor`. + options (descriptor_pb2.ServiceOptions): Service options message or + None to use default service options. + file (FileDescriptor): Reference to file info. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.ServiceDescriptor + + def __new__( + cls, + name=None, + full_name=None, + index=None, + methods=None, + options=None, + serialized_options=None, + file=None, # pylint: disable=redefined-builtin + serialized_start=None, + serialized_end=None, + create_key=None): + _message.Message._CheckCalledFromGeneratedFile() # pylint: disable=protected-access + return _message.default_pool.FindServiceByName(full_name) + + def __init__(self, name, full_name, index, methods, options=None, + serialized_options=None, file=None, # pylint: disable=redefined-builtin + serialized_start=None, serialized_end=None, create_key=None): + if create_key is not _internal_create_key: + _Deprecated('ServiceDescriptor') + + super(ServiceDescriptor, self).__init__( + options, 'ServiceOptions', name, full_name, file, + None, serialized_start=serialized_start, + serialized_end=serialized_end, serialized_options=serialized_options) + self.index = index + self.methods = methods + self.methods_by_name = dict((m.name, m) for m in methods) + # Set the containing service for each method in this service. + for method in self.methods: + method.containing_service = self + + def FindMethodByName(self, name): + """Searches for the specified method, and returns its descriptor. + + Args: + name (str): Name of the method. + Returns: + MethodDescriptor or None: the descriptor for the requested method, if + found. + """ + return self.methods_by_name.get(name, None) + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.ServiceDescriptorProto. + + Args: + proto (descriptor_pb2.ServiceDescriptorProto): An empty descriptor proto. + """ + # This function is overridden to give a better doc comment. + super(ServiceDescriptor, self).CopyToProto(proto) + + +class MethodDescriptor(DescriptorBase): + + """Descriptor for a method in a service. + + Attributes: + name (str): Name of the method within the service. + full_name (str): Full name of method. + index (int): 0-indexed index of the method inside the service. + containing_service (ServiceDescriptor): The service that contains this + method. + input_type (Descriptor): The descriptor of the message that this method + accepts. + output_type (Descriptor): The descriptor of the message that this method + returns. + client_streaming (bool): Whether this method uses client streaming. + server_streaming (bool): Whether this method uses server streaming. + options (descriptor_pb2.MethodOptions or None): Method options message, or + None to use default method options. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.MethodDescriptor + + def __new__(cls, + name, + full_name, + index, + containing_service, + input_type, + output_type, + client_streaming=False, + server_streaming=False, + options=None, + serialized_options=None, + create_key=None): + _message.Message._CheckCalledFromGeneratedFile() # pylint: disable=protected-access + return _message.default_pool.FindMethodByName(full_name) + + def __init__(self, + name, + full_name, + index, + containing_service, + input_type, + output_type, + client_streaming=False, + server_streaming=False, + options=None, + serialized_options=None, + create_key=None): + """The arguments are as described in the description of MethodDescriptor + attributes above. + + Note that containing_service may be None, and may be set later if necessary. + """ + if create_key is not _internal_create_key: + _Deprecated('MethodDescriptor') + + super(MethodDescriptor, self).__init__( + options, serialized_options, 'MethodOptions') + self.name = name + self.full_name = full_name + self.index = index + self.containing_service = containing_service + self.input_type = input_type + self.output_type = output_type + self.client_streaming = client_streaming + self.server_streaming = server_streaming + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.MethodDescriptorProto. + + Args: + proto (descriptor_pb2.MethodDescriptorProto): An empty descriptor proto. + + Raises: + Error: If self couldn't be serialized, due to too few constructor + arguments. + """ + if self.containing_service is not None: + from google.protobuf import descriptor_pb2 + service_proto = descriptor_pb2.ServiceDescriptorProto() + self.containing_service.CopyToProto(service_proto) + proto.CopyFrom(service_proto.method[self.index]) + else: + raise Error('Descriptor does not contain a service.') + + +class FileDescriptor(DescriptorBase): + """Descriptor for a file. Mimics the descriptor_pb2.FileDescriptorProto. + + Note that :attr:`enum_types_by_name`, :attr:`extensions_by_name`, and + :attr:`dependencies` fields are only set by the + :py:mod:`google.protobuf.message_factory` module, and not by the generated + proto code. + + Attributes: + name (str): Name of file, relative to root of source tree. + package (str): Name of the package + syntax (str): string indicating syntax of the file (can be "proto2" or + "proto3") + serialized_pb (bytes): Byte string of serialized + :class:`descriptor_pb2.FileDescriptorProto`. + dependencies (list[FileDescriptor]): List of other :class:`FileDescriptor` + objects this :class:`FileDescriptor` depends on. + public_dependencies (list[FileDescriptor]): A subset of + :attr:`dependencies`, which were declared as "public". + message_types_by_name (dict(str, Descriptor)): Mapping from message names + to their :class:`Descriptor`. + enum_types_by_name (dict(str, EnumDescriptor)): Mapping from enum names to + their :class:`EnumDescriptor`. + extensions_by_name (dict(str, FieldDescriptor)): Mapping from extension + names declared at file scope to their :class:`FieldDescriptor`. + services_by_name (dict(str, ServiceDescriptor)): Mapping from services' + names to their :class:`ServiceDescriptor`. + pool (DescriptorPool): The pool this descriptor belongs to. When not + passed to the constructor, the global default pool is used. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.FileDescriptor + + def __new__(cls, name, package, options=None, + serialized_options=None, serialized_pb=None, + dependencies=None, public_dependencies=None, + syntax=None, pool=None, create_key=None): + # FileDescriptor() is called from various places, not only from generated + # files, to register dynamic proto files and messages. + # pylint: disable=g-explicit-bool-comparison + if serialized_pb == b'': + # Cpp generated code must be linked in if serialized_pb is '' + try: + return _message.default_pool.FindFileByName(name) + except KeyError: + raise RuntimeError('Please link in cpp generated lib for %s' % (name)) + elif serialized_pb: + return _message.default_pool.AddSerializedFile(serialized_pb) + else: + return super(FileDescriptor, cls).__new__(cls) + + def __init__(self, name, package, options=None, + serialized_options=None, serialized_pb=None, + dependencies=None, public_dependencies=None, + syntax=None, pool=None, create_key=None): + """Constructor.""" + if create_key is not _internal_create_key: + _Deprecated('FileDescriptor') + + super(FileDescriptor, self).__init__( + options, serialized_options, 'FileOptions') + + if pool is None: + from google.protobuf import descriptor_pool + pool = descriptor_pool.Default() + self.pool = pool + self.message_types_by_name = {} + self.name = name + self.package = package + self.syntax = syntax or "proto2" + self.serialized_pb = serialized_pb + + self.enum_types_by_name = {} + self.extensions_by_name = {} + self.services_by_name = {} + self.dependencies = (dependencies or []) + self.public_dependencies = (public_dependencies or []) + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.FileDescriptorProto. + + Args: + proto: An empty descriptor_pb2.FileDescriptorProto. + """ + proto.ParseFromString(self.serialized_pb) + + +def _ParseOptions(message, string): + """Parses serialized options. + + This helper function is used to parse serialized options in generated + proto2 files. It must not be used outside proto2. + """ + message.ParseFromString(string) + return message + + +def _ToCamelCase(name): + """Converts name to camel-case and returns it.""" + capitalize_next = False + result = [] + + for c in name: + if c == '_': + if result: + capitalize_next = True + elif capitalize_next: + result.append(c.upper()) + capitalize_next = False + else: + result += c + + # Lower-case the first letter. + if result and result[0].isupper(): + result[0] = result[0].lower() + return ''.join(result) + + +def _OptionsOrNone(descriptor_proto): + """Returns the value of the field `options`, or None if it is not set.""" + if descriptor_proto.HasField('options'): + return descriptor_proto.options + else: + return None + + +def _ToJsonName(name): + """Converts name to Json name and returns it.""" + capitalize_next = False + result = [] + + for c in name: + if c == '_': + capitalize_next = True + elif capitalize_next: + result.append(c.upper()) + capitalize_next = False + else: + result += c + + return ''.join(result) + + +def MakeDescriptor(desc_proto, package='', build_file_if_cpp=True, + syntax=None): + """Make a protobuf Descriptor given a DescriptorProto protobuf. + + Handles nested descriptors. Note that this is limited to the scope of defining + a message inside of another message. Composite fields can currently only be + resolved if the message is defined in the same scope as the field. + + Args: + desc_proto: The descriptor_pb2.DescriptorProto protobuf message. + package: Optional package name for the new message Descriptor (string). + build_file_if_cpp: Update the C++ descriptor pool if api matches. + Set to False on recursion, so no duplicates are created. + syntax: The syntax/semantics that should be used. Set to "proto3" to get + proto3 field presence semantics. + Returns: + A Descriptor for protobuf messages. + """ + if api_implementation.Type() == 'cpp' and build_file_if_cpp: + # The C++ implementation requires all descriptors to be backed by the same + # definition in the C++ descriptor pool. To do this, we build a + # FileDescriptorProto with the same definition as this descriptor and build + # it into the pool. + from google.protobuf import descriptor_pb2 + file_descriptor_proto = descriptor_pb2.FileDescriptorProto() + file_descriptor_proto.message_type.add().MergeFrom(desc_proto) + + # Generate a random name for this proto file to prevent conflicts with any + # imported ones. We need to specify a file name so the descriptor pool + # accepts our FileDescriptorProto, but it is not important what that file + # name is actually set to. + proto_name = binascii.hexlify(os.urandom(16)).decode('ascii') + + if package: + file_descriptor_proto.name = os.path.join(package.replace('.', '/'), + proto_name + '.proto') + file_descriptor_proto.package = package + else: + file_descriptor_proto.name = proto_name + '.proto' + + _message.default_pool.Add(file_descriptor_proto) + result = _message.default_pool.FindFileByName(file_descriptor_proto.name) + + if _USE_C_DESCRIPTORS: + return result.message_types_by_name[desc_proto.name] + + full_message_name = [desc_proto.name] + if package: full_message_name.insert(0, package) + + # Create Descriptors for enum types + enum_types = {} + for enum_proto in desc_proto.enum_type: + full_name = '.'.join(full_message_name + [enum_proto.name]) + enum_desc = EnumDescriptor( + enum_proto.name, full_name, None, [ + EnumValueDescriptor(enum_val.name, ii, enum_val.number, + create_key=_internal_create_key) + for ii, enum_val in enumerate(enum_proto.value)], + create_key=_internal_create_key) + enum_types[full_name] = enum_desc + + # Create Descriptors for nested types + nested_types = {} + for nested_proto in desc_proto.nested_type: + full_name = '.'.join(full_message_name + [nested_proto.name]) + # Nested types are just those defined inside of the message, not all types + # used by fields in the message, so no loops are possible here. + nested_desc = MakeDescriptor(nested_proto, + package='.'.join(full_message_name), + build_file_if_cpp=False, + syntax=syntax) + nested_types[full_name] = nested_desc + + fields = [] + for field_proto in desc_proto.field: + full_name = '.'.join(full_message_name + [field_proto.name]) + enum_desc = None + nested_desc = None + if field_proto.json_name: + json_name = field_proto.json_name + else: + json_name = None + if field_proto.HasField('type_name'): + type_name = field_proto.type_name + full_type_name = '.'.join(full_message_name + + [type_name[type_name.rfind('.')+1:]]) + if full_type_name in nested_types: + nested_desc = nested_types[full_type_name] + elif full_type_name in enum_types: + enum_desc = enum_types[full_type_name] + # Else type_name references a non-local type, which isn't implemented + field = FieldDescriptor( + field_proto.name, full_name, field_proto.number - 1, + field_proto.number, field_proto.type, + FieldDescriptor.ProtoTypeToCppProtoType(field_proto.type), + field_proto.label, None, nested_desc, enum_desc, None, False, None, + options=_OptionsOrNone(field_proto), has_default_value=False, + json_name=json_name, create_key=_internal_create_key) + fields.append(field) + + desc_name = '.'.join(full_message_name) + return Descriptor(desc_proto.name, desc_name, None, None, fields, + list(nested_types.values()), list(enum_types.values()), [], + options=_OptionsOrNone(desc_proto), + create_key=_internal_create_key) diff --git a/scripts/protobuf3/protobuf3/descriptor_database.py b/scripts/protobuf3/protobuf3/descriptor_database.py new file mode 100644 index 0000000..073eddc --- /dev/null +++ b/scripts/protobuf3/protobuf3/descriptor_database.py @@ -0,0 +1,177 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Provides a container for DescriptorProtos.""" + +__author__ = 'matthewtoia@google.com (Matt Toia)' + +import warnings + + +class Error(Exception): + pass + + +class DescriptorDatabaseConflictingDefinitionError(Error): + """Raised when a proto is added with the same name & different descriptor.""" + + +class DescriptorDatabase(object): + """A container accepting FileDescriptorProtos and maps DescriptorProtos.""" + + def __init__(self): + self._file_desc_protos_by_file = {} + self._file_desc_protos_by_symbol = {} + + def Add(self, file_desc_proto): + """Adds the FileDescriptorProto and its types to this database. + + Args: + file_desc_proto: The FileDescriptorProto to add. + Raises: + DescriptorDatabaseConflictingDefinitionError: if an attempt is made to + add a proto with the same name but different definition than an + existing proto in the database. + """ + proto_name = file_desc_proto.name + if proto_name not in self._file_desc_protos_by_file: + self._file_desc_protos_by_file[proto_name] = file_desc_proto + elif self._file_desc_protos_by_file[proto_name] != file_desc_proto: + raise DescriptorDatabaseConflictingDefinitionError( + '%s already added, but with different descriptor.' % proto_name) + else: + return + + # Add all the top-level descriptors to the index. + package = file_desc_proto.package + for message in file_desc_proto.message_type: + for name in _ExtractSymbols(message, package): + self._AddSymbol(name, file_desc_proto) + for enum in file_desc_proto.enum_type: + self._AddSymbol(('.'.join((package, enum.name))), file_desc_proto) + for enum_value in enum.value: + self._file_desc_protos_by_symbol[ + '.'.join((package, enum_value.name))] = file_desc_proto + for extension in file_desc_proto.extension: + self._AddSymbol(('.'.join((package, extension.name))), file_desc_proto) + for service in file_desc_proto.service: + self._AddSymbol(('.'.join((package, service.name))), file_desc_proto) + + def FindFileByName(self, name): + """Finds the file descriptor proto by file name. + + Typically the file name is a relative path ending to a .proto file. The + proto with the given name will have to have been added to this database + using the Add method or else an error will be raised. + + Args: + name: The file name to find. + + Returns: + The file descriptor proto matching the name. + + Raises: + KeyError if no file by the given name was added. + """ + + return self._file_desc_protos_by_file[name] + + def FindFileContainingSymbol(self, symbol): + """Finds the file descriptor proto containing the specified symbol. + + The symbol should be a fully qualified name including the file descriptor's + package and any containing messages. Some examples: + + 'some.package.name.Message' + 'some.package.name.Message.NestedEnum' + 'some.package.name.Message.some_field' + + The file descriptor proto containing the specified symbol must be added to + this database using the Add method or else an error will be raised. + + Args: + symbol: The fully qualified symbol name. + + Returns: + The file descriptor proto containing the symbol. + + Raises: + KeyError if no file contains the specified symbol. + """ + try: + return self._file_desc_protos_by_symbol[symbol] + except KeyError: + # Fields, enum values, and nested extensions are not in + # _file_desc_protos_by_symbol. Try to find the top level + # descriptor. Non-existent nested symbol under a valid top level + # descriptor can also be found. The behavior is the same with + # protobuf C++. + top_level, _, _ = symbol.rpartition('.') + try: + return self._file_desc_protos_by_symbol[top_level] + except KeyError: + # Raise the original symbol as a KeyError for better diagnostics. + raise KeyError(symbol) + + def FindFileContainingExtension(self, extendee_name, extension_number): + # TODO(jieluo): implement this API. + return None + + def FindAllExtensionNumbers(self, extendee_name): + # TODO(jieluo): implement this API. + return [] + + def _AddSymbol(self, name, file_desc_proto): + if name in self._file_desc_protos_by_symbol: + warn_msg = ('Conflict register for file "' + file_desc_proto.name + + '": ' + name + + ' is already defined in file "' + + self._file_desc_protos_by_symbol[name].name + '"') + warnings.warn(warn_msg, RuntimeWarning) + self._file_desc_protos_by_symbol[name] = file_desc_proto + + +def _ExtractSymbols(desc_proto, package): + """Pulls out all the symbols from a descriptor proto. + + Args: + desc_proto: The proto to extract symbols from. + package: The package containing the descriptor type. + + Yields: + The fully qualified name found in the descriptor. + """ + message_name = package + '.' + desc_proto.name if package else desc_proto.name + yield message_name + for nested_type in desc_proto.nested_type: + for symbol in _ExtractSymbols(nested_type, message_name): + yield symbol + for enum_type in desc_proto.enum_type: + yield '.'.join((message_name, enum_type.name)) diff --git a/scripts/protobuf3/protobuf3/descriptor_pb2.py b/scripts/protobuf3/protobuf3/descriptor_pb2.py new file mode 100644 index 0000000..f570386 --- /dev/null +++ b/scripts/protobuf3/protobuf3/descriptor_pb2.py @@ -0,0 +1,1925 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/descriptor.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR = _descriptor.FileDescriptor( + name='google/protobuf/descriptor.proto', + package='google.protobuf', + syntax='proto2', + serialized_options=None, + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n google/protobuf/descriptor.proto\x12\x0fgoogle.protobuf\"G\n\x11\x46ileDescriptorSet\x12\x32\n\x04\x66ile\x18\x01 \x03(\x0b\x32$.google.protobuf.FileDescriptorProto\"\xdb\x03\n\x13\x46ileDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07package\x18\x02 \x01(\t\x12\x12\n\ndependency\x18\x03 \x03(\t\x12\x19\n\x11public_dependency\x18\n \x03(\x05\x12\x17\n\x0fweak_dependency\x18\x0b \x03(\x05\x12\x36\n\x0cmessage_type\x18\x04 \x03(\x0b\x32 .google.protobuf.DescriptorProto\x12\x37\n\tenum_type\x18\x05 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProto\x12\x38\n\x07service\x18\x06 \x03(\x0b\x32\'.google.protobuf.ServiceDescriptorProto\x12\x38\n\textension\x18\x07 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12-\n\x07options\x18\x08 \x01(\x0b\x32\x1c.google.protobuf.FileOptions\x12\x39\n\x10source_code_info\x18\t \x01(\x0b\x32\x1f.google.protobuf.SourceCodeInfo\x12\x0e\n\x06syntax\x18\x0c \x01(\t\"\xa9\x05\n\x0f\x44\x65scriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x34\n\x05\x66ield\x18\x02 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12\x38\n\textension\x18\x06 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12\x35\n\x0bnested_type\x18\x03 \x03(\x0b\x32 .google.protobuf.DescriptorProto\x12\x37\n\tenum_type\x18\x04 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProto\x12H\n\x0f\x65xtension_range\x18\x05 \x03(\x0b\x32/.google.protobuf.DescriptorProto.ExtensionRange\x12\x39\n\noneof_decl\x18\x08 \x03(\x0b\x32%.google.protobuf.OneofDescriptorProto\x12\x30\n\x07options\x18\x07 \x01(\x0b\x32\x1f.google.protobuf.MessageOptions\x12\x46\n\x0ereserved_range\x18\t \x03(\x0b\x32..google.protobuf.DescriptorProto.ReservedRange\x12\x15\n\rreserved_name\x18\n \x03(\t\x1a\x65\n\x0e\x45xtensionRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\x12\x37\n\x07options\x18\x03 \x01(\x0b\x32&.google.protobuf.ExtensionRangeOptions\x1a+\n\rReservedRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\"g\n\x15\x45xtensionRangeOptions\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xd5\x05\n\x14\x46ieldDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x03 \x01(\x05\x12:\n\x05label\x18\x04 \x01(\x0e\x32+.google.protobuf.FieldDescriptorProto.Label\x12\x38\n\x04type\x18\x05 \x01(\x0e\x32*.google.protobuf.FieldDescriptorProto.Type\x12\x11\n\ttype_name\x18\x06 \x01(\t\x12\x10\n\x08\x65xtendee\x18\x02 \x01(\t\x12\x15\n\rdefault_value\x18\x07 \x01(\t\x12\x13\n\x0boneof_index\x18\t \x01(\x05\x12\x11\n\tjson_name\x18\n \x01(\t\x12.\n\x07options\x18\x08 \x01(\x0b\x32\x1d.google.protobuf.FieldOptions\x12\x17\n\x0fproto3_optional\x18\x11 \x01(\x08\"\xb6\x02\n\x04Type\x12\x0f\n\x0bTYPE_DOUBLE\x10\x01\x12\x0e\n\nTYPE_FLOAT\x10\x02\x12\x0e\n\nTYPE_INT64\x10\x03\x12\x0f\n\x0bTYPE_UINT64\x10\x04\x12\x0e\n\nTYPE_INT32\x10\x05\x12\x10\n\x0cTYPE_FIXED64\x10\x06\x12\x10\n\x0cTYPE_FIXED32\x10\x07\x12\r\n\tTYPE_BOOL\x10\x08\x12\x0f\n\x0bTYPE_STRING\x10\t\x12\x0e\n\nTYPE_GROUP\x10\n\x12\x10\n\x0cTYPE_MESSAGE\x10\x0b\x12\x0e\n\nTYPE_BYTES\x10\x0c\x12\x0f\n\x0bTYPE_UINT32\x10\r\x12\r\n\tTYPE_ENUM\x10\x0e\x12\x11\n\rTYPE_SFIXED32\x10\x0f\x12\x11\n\rTYPE_SFIXED64\x10\x10\x12\x0f\n\x0bTYPE_SINT32\x10\x11\x12\x0f\n\x0bTYPE_SINT64\x10\x12\"C\n\x05Label\x12\x12\n\x0eLABEL_OPTIONAL\x10\x01\x12\x12\n\x0eLABEL_REQUIRED\x10\x02\x12\x12\n\x0eLABEL_REPEATED\x10\x03\"T\n\x14OneofDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12.\n\x07options\x18\x02 \x01(\x0b\x32\x1d.google.protobuf.OneofOptions\"\xa4\x02\n\x13\x45numDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x38\n\x05value\x18\x02 \x03(\x0b\x32).google.protobuf.EnumValueDescriptorProto\x12-\n\x07options\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.EnumOptions\x12N\n\x0ereserved_range\x18\x04 \x03(\x0b\x32\x36.google.protobuf.EnumDescriptorProto.EnumReservedRange\x12\x15\n\rreserved_name\x18\x05 \x03(\t\x1a/\n\x11\x45numReservedRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\"l\n\x18\x45numValueDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x02 \x01(\x05\x12\x32\n\x07options\x18\x03 \x01(\x0b\x32!.google.protobuf.EnumValueOptions\"\x90\x01\n\x16ServiceDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x36\n\x06method\x18\x02 \x03(\x0b\x32&.google.protobuf.MethodDescriptorProto\x12\x30\n\x07options\x18\x03 \x01(\x0b\x32\x1f.google.protobuf.ServiceOptions\"\xc1\x01\n\x15MethodDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\ninput_type\x18\x02 \x01(\t\x12\x13\n\x0boutput_type\x18\x03 \x01(\t\x12/\n\x07options\x18\x04 \x01(\x0b\x32\x1e.google.protobuf.MethodOptions\x12\x1f\n\x10\x63lient_streaming\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1f\n\x10server_streaming\x18\x06 \x01(\x08:\x05\x66\x61lse\"\xa5\x06\n\x0b\x46ileOptions\x12\x14\n\x0cjava_package\x18\x01 \x01(\t\x12\x1c\n\x14java_outer_classname\x18\x08 \x01(\t\x12\"\n\x13java_multiple_files\x18\n \x01(\x08:\x05\x66\x61lse\x12)\n\x1djava_generate_equals_and_hash\x18\x14 \x01(\x08\x42\x02\x18\x01\x12%\n\x16java_string_check_utf8\x18\x1b \x01(\x08:\x05\x66\x61lse\x12\x46\n\x0coptimize_for\x18\t \x01(\x0e\x32).google.protobuf.FileOptions.OptimizeMode:\x05SPEED\x12\x12\n\ngo_package\x18\x0b \x01(\t\x12\"\n\x13\x63\x63_generic_services\x18\x10 \x01(\x08:\x05\x66\x61lse\x12$\n\x15java_generic_services\x18\x11 \x01(\x08:\x05\x66\x61lse\x12\"\n\x13py_generic_services\x18\x12 \x01(\x08:\x05\x66\x61lse\x12#\n\x14php_generic_services\x18* \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x17 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x10\x63\x63_enable_arenas\x18\x1f \x01(\x08:\x04true\x12\x19\n\x11objc_class_prefix\x18$ \x01(\t\x12\x18\n\x10\x63sharp_namespace\x18% \x01(\t\x12\x14\n\x0cswift_prefix\x18\' \x01(\t\x12\x18\n\x10php_class_prefix\x18( \x01(\t\x12\x15\n\rphp_namespace\x18) \x01(\t\x12\x1e\n\x16php_metadata_namespace\x18, \x01(\t\x12\x14\n\x0cruby_package\x18- \x01(\t\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\":\n\x0cOptimizeMode\x12\t\n\x05SPEED\x10\x01\x12\r\n\tCODE_SIZE\x10\x02\x12\x10\n\x0cLITE_RUNTIME\x10\x03*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08&\x10\'\"\x84\x02\n\x0eMessageOptions\x12&\n\x17message_set_wire_format\x18\x01 \x01(\x08:\x05\x66\x61lse\x12.\n\x1fno_standard_descriptor_accessor\x18\x02 \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x11\n\tmap_entry\x18\x07 \x01(\x08\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05J\x04\x08\x05\x10\x06J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\tJ\x04\x08\t\x10\n\"\xbe\x03\n\x0c\x46ieldOptions\x12:\n\x05\x63type\x18\x01 \x01(\x0e\x32#.google.protobuf.FieldOptions.CType:\x06STRING\x12\x0e\n\x06packed\x18\x02 \x01(\x08\x12?\n\x06jstype\x18\x06 \x01(\x0e\x32$.google.protobuf.FieldOptions.JSType:\tJS_NORMAL\x12\x13\n\x04lazy\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x0funverified_lazy\x18\x0f \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x13\n\x04weak\x18\n \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\"/\n\x05\x43Type\x12\n\n\x06STRING\x10\x00\x12\x08\n\x04\x43ORD\x10\x01\x12\x10\n\x0cSTRING_PIECE\x10\x02\"5\n\x06JSType\x12\r\n\tJS_NORMAL\x10\x00\x12\r\n\tJS_STRING\x10\x01\x12\r\n\tJS_NUMBER\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05\"^\n\x0cOneofOptions\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x93\x01\n\x0b\x45numOptions\x12\x13\n\x0b\x61llow_alias\x18\x02 \x01(\x08\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x05\x10\x06\"}\n\x10\x45numValueOptions\x12\x19\n\ndeprecated\x18\x01 \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"{\n\x0eServiceOptions\x12\x19\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xad\x02\n\rMethodOptions\x12\x19\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lse\x12_\n\x11idempotency_level\x18\" \x01(\x0e\x32/.google.protobuf.MethodOptions.IdempotencyLevel:\x13IDEMPOTENCY_UNKNOWN\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\"P\n\x10IdempotencyLevel\x12\x17\n\x13IDEMPOTENCY_UNKNOWN\x10\x00\x12\x13\n\x0fNO_SIDE_EFFECTS\x10\x01\x12\x0e\n\nIDEMPOTENT\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x9e\x02\n\x13UninterpretedOption\x12;\n\x04name\x18\x02 \x03(\x0b\x32-.google.protobuf.UninterpretedOption.NamePart\x12\x18\n\x10identifier_value\x18\x03 \x01(\t\x12\x1a\n\x12positive_int_value\x18\x04 \x01(\x04\x12\x1a\n\x12negative_int_value\x18\x05 \x01(\x03\x12\x14\n\x0c\x64ouble_value\x18\x06 \x01(\x01\x12\x14\n\x0cstring_value\x18\x07 \x01(\x0c\x12\x17\n\x0f\x61ggregate_value\x18\x08 \x01(\t\x1a\x33\n\x08NamePart\x12\x11\n\tname_part\x18\x01 \x02(\t\x12\x14\n\x0cis_extension\x18\x02 \x02(\x08\"\xd5\x01\n\x0eSourceCodeInfo\x12:\n\x08location\x18\x01 \x03(\x0b\x32(.google.protobuf.SourceCodeInfo.Location\x1a\x86\x01\n\x08Location\x12\x10\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01\x12\x10\n\x04span\x18\x02 \x03(\x05\x42\x02\x10\x01\x12\x18\n\x10leading_comments\x18\x03 \x01(\t\x12\x19\n\x11trailing_comments\x18\x04 \x01(\t\x12!\n\x19leading_detached_comments\x18\x06 \x03(\t\"\xa7\x01\n\x11GeneratedCodeInfo\x12\x41\n\nannotation\x18\x01 \x03(\x0b\x32-.google.protobuf.GeneratedCodeInfo.Annotation\x1aO\n\nAnnotation\x12\x10\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01\x12\x13\n\x0bsource_file\x18\x02 \x01(\t\x12\r\n\x05\x62\x65gin\x18\x03 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x04 \x01(\x05\x42~\n\x13\x63om.google.protobufB\x10\x44\x65scriptorProtosH\x01Z-google.golang.org/protobuf/types/descriptorpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1aGoogle.Protobuf.Reflection' + ) +else: + DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n google/protobuf/descriptor.proto\x12\x0fgoogle.protobuf\"G\n\x11\x46ileDescriptorSet\x12\x32\n\x04\x66ile\x18\x01 \x03(\x0b\x32$.google.protobuf.FileDescriptorProto\"\xdb\x03\n\x13\x46ileDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07package\x18\x02 \x01(\t\x12\x12\n\ndependency\x18\x03 \x03(\t\x12\x19\n\x11public_dependency\x18\n \x03(\x05\x12\x17\n\x0fweak_dependency\x18\x0b \x03(\x05\x12\x36\n\x0cmessage_type\x18\x04 \x03(\x0b\x32 .google.protobuf.DescriptorProto\x12\x37\n\tenum_type\x18\x05 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProto\x12\x38\n\x07service\x18\x06 \x03(\x0b\x32\'.google.protobuf.ServiceDescriptorProto\x12\x38\n\textension\x18\x07 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12-\n\x07options\x18\x08 \x01(\x0b\x32\x1c.google.protobuf.FileOptions\x12\x39\n\x10source_code_info\x18\t \x01(\x0b\x32\x1f.google.protobuf.SourceCodeInfo\x12\x0e\n\x06syntax\x18\x0c \x01(\t\"\xa9\x05\n\x0f\x44\x65scriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x34\n\x05\x66ield\x18\x02 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12\x38\n\textension\x18\x06 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProto\x12\x35\n\x0bnested_type\x18\x03 \x03(\x0b\x32 .google.protobuf.DescriptorProto\x12\x37\n\tenum_type\x18\x04 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProto\x12H\n\x0f\x65xtension_range\x18\x05 \x03(\x0b\x32/.google.protobuf.DescriptorProto.ExtensionRange\x12\x39\n\noneof_decl\x18\x08 \x03(\x0b\x32%.google.protobuf.OneofDescriptorProto\x12\x30\n\x07options\x18\x07 \x01(\x0b\x32\x1f.google.protobuf.MessageOptions\x12\x46\n\x0ereserved_range\x18\t \x03(\x0b\x32..google.protobuf.DescriptorProto.ReservedRange\x12\x15\n\rreserved_name\x18\n \x03(\t\x1a\x65\n\x0e\x45xtensionRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\x12\x37\n\x07options\x18\x03 \x01(\x0b\x32&.google.protobuf.ExtensionRangeOptions\x1a+\n\rReservedRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\"g\n\x15\x45xtensionRangeOptions\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xd5\x05\n\x14\x46ieldDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x03 \x01(\x05\x12:\n\x05label\x18\x04 \x01(\x0e\x32+.google.protobuf.FieldDescriptorProto.Label\x12\x38\n\x04type\x18\x05 \x01(\x0e\x32*.google.protobuf.FieldDescriptorProto.Type\x12\x11\n\ttype_name\x18\x06 \x01(\t\x12\x10\n\x08\x65xtendee\x18\x02 \x01(\t\x12\x15\n\rdefault_value\x18\x07 \x01(\t\x12\x13\n\x0boneof_index\x18\t \x01(\x05\x12\x11\n\tjson_name\x18\n \x01(\t\x12.\n\x07options\x18\x08 \x01(\x0b\x32\x1d.google.protobuf.FieldOptions\x12\x17\n\x0fproto3_optional\x18\x11 \x01(\x08\"\xb6\x02\n\x04Type\x12\x0f\n\x0bTYPE_DOUBLE\x10\x01\x12\x0e\n\nTYPE_FLOAT\x10\x02\x12\x0e\n\nTYPE_INT64\x10\x03\x12\x0f\n\x0bTYPE_UINT64\x10\x04\x12\x0e\n\nTYPE_INT32\x10\x05\x12\x10\n\x0cTYPE_FIXED64\x10\x06\x12\x10\n\x0cTYPE_FIXED32\x10\x07\x12\r\n\tTYPE_BOOL\x10\x08\x12\x0f\n\x0bTYPE_STRING\x10\t\x12\x0e\n\nTYPE_GROUP\x10\n\x12\x10\n\x0cTYPE_MESSAGE\x10\x0b\x12\x0e\n\nTYPE_BYTES\x10\x0c\x12\x0f\n\x0bTYPE_UINT32\x10\r\x12\r\n\tTYPE_ENUM\x10\x0e\x12\x11\n\rTYPE_SFIXED32\x10\x0f\x12\x11\n\rTYPE_SFIXED64\x10\x10\x12\x0f\n\x0bTYPE_SINT32\x10\x11\x12\x0f\n\x0bTYPE_SINT64\x10\x12\"C\n\x05Label\x12\x12\n\x0eLABEL_OPTIONAL\x10\x01\x12\x12\n\x0eLABEL_REQUIRED\x10\x02\x12\x12\n\x0eLABEL_REPEATED\x10\x03\"T\n\x14OneofDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12.\n\x07options\x18\x02 \x01(\x0b\x32\x1d.google.protobuf.OneofOptions\"\xa4\x02\n\x13\x45numDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x38\n\x05value\x18\x02 \x03(\x0b\x32).google.protobuf.EnumValueDescriptorProto\x12-\n\x07options\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.EnumOptions\x12N\n\x0ereserved_range\x18\x04 \x03(\x0b\x32\x36.google.protobuf.EnumDescriptorProto.EnumReservedRange\x12\x15\n\rreserved_name\x18\x05 \x03(\t\x1a/\n\x11\x45numReservedRange\x12\r\n\x05start\x18\x01 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x05\"l\n\x18\x45numValueDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x02 \x01(\x05\x12\x32\n\x07options\x18\x03 \x01(\x0b\x32!.google.protobuf.EnumValueOptions\"\x90\x01\n\x16ServiceDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x36\n\x06method\x18\x02 \x03(\x0b\x32&.google.protobuf.MethodDescriptorProto\x12\x30\n\x07options\x18\x03 \x01(\x0b\x32\x1f.google.protobuf.ServiceOptions\"\xc1\x01\n\x15MethodDescriptorProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\ninput_type\x18\x02 \x01(\t\x12\x13\n\x0boutput_type\x18\x03 \x01(\t\x12/\n\x07options\x18\x04 \x01(\x0b\x32\x1e.google.protobuf.MethodOptions\x12\x1f\n\x10\x63lient_streaming\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1f\n\x10server_streaming\x18\x06 \x01(\x08:\x05\x66\x61lse\"\xa5\x06\n\x0b\x46ileOptions\x12\x14\n\x0cjava_package\x18\x01 \x01(\t\x12\x1c\n\x14java_outer_classname\x18\x08 \x01(\t\x12\"\n\x13java_multiple_files\x18\n \x01(\x08:\x05\x66\x61lse\x12)\n\x1djava_generate_equals_and_hash\x18\x14 \x01(\x08\x42\x02\x18\x01\x12%\n\x16java_string_check_utf8\x18\x1b \x01(\x08:\x05\x66\x61lse\x12\x46\n\x0coptimize_for\x18\t \x01(\x0e\x32).google.protobuf.FileOptions.OptimizeMode:\x05SPEED\x12\x12\n\ngo_package\x18\x0b \x01(\t\x12\"\n\x13\x63\x63_generic_services\x18\x10 \x01(\x08:\x05\x66\x61lse\x12$\n\x15java_generic_services\x18\x11 \x01(\x08:\x05\x66\x61lse\x12\"\n\x13py_generic_services\x18\x12 \x01(\x08:\x05\x66\x61lse\x12#\n\x14php_generic_services\x18* \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x17 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x10\x63\x63_enable_arenas\x18\x1f \x01(\x08:\x04true\x12\x19\n\x11objc_class_prefix\x18$ \x01(\t\x12\x18\n\x10\x63sharp_namespace\x18% \x01(\t\x12\x14\n\x0cswift_prefix\x18\' \x01(\t\x12\x18\n\x10php_class_prefix\x18( \x01(\t\x12\x15\n\rphp_namespace\x18) \x01(\t\x12\x1e\n\x16php_metadata_namespace\x18, \x01(\t\x12\x14\n\x0cruby_package\x18- \x01(\t\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\":\n\x0cOptimizeMode\x12\t\n\x05SPEED\x10\x01\x12\r\n\tCODE_SIZE\x10\x02\x12\x10\n\x0cLITE_RUNTIME\x10\x03*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08&\x10\'\"\x84\x02\n\x0eMessageOptions\x12&\n\x17message_set_wire_format\x18\x01 \x01(\x08:\x05\x66\x61lse\x12.\n\x1fno_standard_descriptor_accessor\x18\x02 \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x11\n\tmap_entry\x18\x07 \x01(\x08\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05J\x04\x08\x05\x10\x06J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\tJ\x04\x08\t\x10\n\"\xbe\x03\n\x0c\x46ieldOptions\x12:\n\x05\x63type\x18\x01 \x01(\x0e\x32#.google.protobuf.FieldOptions.CType:\x06STRING\x12\x0e\n\x06packed\x18\x02 \x01(\x08\x12?\n\x06jstype\x18\x06 \x01(\x0e\x32$.google.protobuf.FieldOptions.JSType:\tJS_NORMAL\x12\x13\n\x04lazy\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x0funverified_lazy\x18\x0f \x01(\x08:\x05\x66\x61lse\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x13\n\x04weak\x18\n \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\"/\n\x05\x43Type\x12\n\n\x06STRING\x10\x00\x12\x08\n\x04\x43ORD\x10\x01\x12\x10\n\x0cSTRING_PIECE\x10\x02\"5\n\x06JSType\x12\r\n\tJS_NORMAL\x10\x00\x12\r\n\tJS_STRING\x10\x01\x12\r\n\tJS_NUMBER\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05\"^\n\x0cOneofOptions\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x93\x01\n\x0b\x45numOptions\x12\x13\n\x0b\x61llow_alias\x18\x02 \x01(\x08\x12\x19\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x05\x10\x06\"}\n\x10\x45numValueOptions\x12\x19\n\ndeprecated\x18\x01 \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"{\n\x0eServiceOptions\x12\x19\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lse\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xad\x02\n\rMethodOptions\x12\x19\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lse\x12_\n\x11idempotency_level\x18\" \x01(\x0e\x32/.google.protobuf.MethodOptions.IdempotencyLevel:\x13IDEMPOTENCY_UNKNOWN\x12\x43\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOption\"P\n\x10IdempotencyLevel\x12\x17\n\x13IDEMPOTENCY_UNKNOWN\x10\x00\x12\x13\n\x0fNO_SIDE_EFFECTS\x10\x01\x12\x0e\n\nIDEMPOTENT\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x9e\x02\n\x13UninterpretedOption\x12;\n\x04name\x18\x02 \x03(\x0b\x32-.google.protobuf.UninterpretedOption.NamePart\x12\x18\n\x10identifier_value\x18\x03 \x01(\t\x12\x1a\n\x12positive_int_value\x18\x04 \x01(\x04\x12\x1a\n\x12negative_int_value\x18\x05 \x01(\x03\x12\x14\n\x0c\x64ouble_value\x18\x06 \x01(\x01\x12\x14\n\x0cstring_value\x18\x07 \x01(\x0c\x12\x17\n\x0f\x61ggregate_value\x18\x08 \x01(\t\x1a\x33\n\x08NamePart\x12\x11\n\tname_part\x18\x01 \x02(\t\x12\x14\n\x0cis_extension\x18\x02 \x02(\x08\"\xd5\x01\n\x0eSourceCodeInfo\x12:\n\x08location\x18\x01 \x03(\x0b\x32(.google.protobuf.SourceCodeInfo.Location\x1a\x86\x01\n\x08Location\x12\x10\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01\x12\x10\n\x04span\x18\x02 \x03(\x05\x42\x02\x10\x01\x12\x18\n\x10leading_comments\x18\x03 \x01(\t\x12\x19\n\x11trailing_comments\x18\x04 \x01(\t\x12!\n\x19leading_detached_comments\x18\x06 \x03(\t\"\xa7\x01\n\x11GeneratedCodeInfo\x12\x41\n\nannotation\x18\x01 \x03(\x0b\x32-.google.protobuf.GeneratedCodeInfo.Annotation\x1aO\n\nAnnotation\x12\x10\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01\x12\x13\n\x0bsource_file\x18\x02 \x01(\t\x12\r\n\x05\x62\x65gin\x18\x03 \x01(\x05\x12\x0b\n\x03\x65nd\x18\x04 \x01(\x05\x42~\n\x13\x63om.google.protobufB\x10\x44\x65scriptorProtosH\x01Z-google.golang.org/protobuf/types/descriptorpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1aGoogle.Protobuf.Reflection') + +if _descriptor._USE_C_DESCRIPTORS == False: + _FIELDDESCRIPTORPROTO_TYPE = _descriptor.EnumDescriptor( + name='Type', + full_name='google.protobuf.FieldDescriptorProto.Type', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='TYPE_DOUBLE', index=0, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_FLOAT', index=1, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_INT64', index=2, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_UINT64', index=3, number=4, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_INT32', index=4, number=5, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_FIXED64', index=5, number=6, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_FIXED32', index=6, number=7, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_BOOL', index=7, number=8, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_STRING', index=8, number=9, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_GROUP', index=9, number=10, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_MESSAGE', index=10, number=11, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_BYTES', index=11, number=12, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_UINT32', index=12, number=13, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_ENUM', index=13, number=14, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_SFIXED32', index=14, number=15, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_SFIXED64', index=15, number=16, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_SINT32', index=16, number=17, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_SINT64', index=17, number=18, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FIELDDESCRIPTORPROTO_TYPE) + + _FIELDDESCRIPTORPROTO_LABEL = _descriptor.EnumDescriptor( + name='Label', + full_name='google.protobuf.FieldDescriptorProto.Label', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='LABEL_OPTIONAL', index=0, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='LABEL_REQUIRED', index=1, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='LABEL_REPEATED', index=2, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FIELDDESCRIPTORPROTO_LABEL) + + _FILEOPTIONS_OPTIMIZEMODE = _descriptor.EnumDescriptor( + name='OptimizeMode', + full_name='google.protobuf.FileOptions.OptimizeMode', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='SPEED', index=0, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='CODE_SIZE', index=1, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='LITE_RUNTIME', index=2, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FILEOPTIONS_OPTIMIZEMODE) + + _FIELDOPTIONS_CTYPE = _descriptor.EnumDescriptor( + name='CType', + full_name='google.protobuf.FieldOptions.CType', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='STRING', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='CORD', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='STRING_PIECE', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_CTYPE) + + _FIELDOPTIONS_JSTYPE = _descriptor.EnumDescriptor( + name='JSType', + full_name='google.protobuf.FieldOptions.JSType', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='JS_NORMAL', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='JS_STRING', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='JS_NUMBER', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_JSTYPE) + + _METHODOPTIONS_IDEMPOTENCYLEVEL = _descriptor.EnumDescriptor( + name='IdempotencyLevel', + full_name='google.protobuf.MethodOptions.IdempotencyLevel', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='IDEMPOTENCY_UNKNOWN', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='NO_SIDE_EFFECTS', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='IDEMPOTENT', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_METHODOPTIONS_IDEMPOTENCYLEVEL) + + + _FILEDESCRIPTORSET = _descriptor.Descriptor( + name='FileDescriptorSet', + full_name='google.protobuf.FileDescriptorSet', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='file', full_name='google.protobuf.FileDescriptorSet.file', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _FILEDESCRIPTORPROTO = _descriptor.Descriptor( + name='FileDescriptorProto', + full_name='google.protobuf.FileDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.FileDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='package', full_name='google.protobuf.FileDescriptorProto.package', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='dependency', full_name='google.protobuf.FileDescriptorProto.dependency', index=2, + number=3, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='public_dependency', full_name='google.protobuf.FileDescriptorProto.public_dependency', index=3, + number=10, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='weak_dependency', full_name='google.protobuf.FileDescriptorProto.weak_dependency', index=4, + number=11, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='message_type', full_name='google.protobuf.FileDescriptorProto.message_type', index=5, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='enum_type', full_name='google.protobuf.FileDescriptorProto.enum_type', index=6, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='service', full_name='google.protobuf.FileDescriptorProto.service', index=7, + number=6, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='extension', full_name='google.protobuf.FileDescriptorProto.extension', index=8, + number=7, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.FileDescriptorProto.options', index=9, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='source_code_info', full_name='google.protobuf.FileDescriptorProto.source_code_info', index=10, + number=9, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='syntax', full_name='google.protobuf.FileDescriptorProto.syntax', index=11, + number=12, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _DESCRIPTORPROTO_EXTENSIONRANGE = _descriptor.Descriptor( + name='ExtensionRange', + full_name='google.protobuf.DescriptorProto.ExtensionRange', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='start', full_name='google.protobuf.DescriptorProto.ExtensionRange.start', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='end', full_name='google.protobuf.DescriptorProto.ExtensionRange.end', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.DescriptorProto.ExtensionRange.options', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _DESCRIPTORPROTO_RESERVEDRANGE = _descriptor.Descriptor( + name='ReservedRange', + full_name='google.protobuf.DescriptorProto.ReservedRange', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='start', full_name='google.protobuf.DescriptorProto.ReservedRange.start', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='end', full_name='google.protobuf.DescriptorProto.ReservedRange.end', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _DESCRIPTORPROTO = _descriptor.Descriptor( + name='DescriptorProto', + full_name='google.protobuf.DescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.DescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='field', full_name='google.protobuf.DescriptorProto.field', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='extension', full_name='google.protobuf.DescriptorProto.extension', index=2, + number=6, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='nested_type', full_name='google.protobuf.DescriptorProto.nested_type', index=3, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='enum_type', full_name='google.protobuf.DescriptorProto.enum_type', index=4, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='extension_range', full_name='google.protobuf.DescriptorProto.extension_range', index=5, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='oneof_decl', full_name='google.protobuf.DescriptorProto.oneof_decl', index=6, + number=8, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.DescriptorProto.options', index=7, + number=7, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='reserved_range', full_name='google.protobuf.DescriptorProto.reserved_range', index=8, + number=9, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='reserved_name', full_name='google.protobuf.DescriptorProto.reserved_name', index=9, + number=10, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_DESCRIPTORPROTO_EXTENSIONRANGE, _DESCRIPTORPROTO_RESERVEDRANGE, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _EXTENSIONRANGEOPTIONS = _descriptor.Descriptor( + name='ExtensionRangeOptions', + full_name='google.protobuf.ExtensionRangeOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.ExtensionRangeOptions.uninterpreted_option', index=0, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _FIELDDESCRIPTORPROTO = _descriptor.Descriptor( + name='FieldDescriptorProto', + full_name='google.protobuf.FieldDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.FieldDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='number', full_name='google.protobuf.FieldDescriptorProto.number', index=1, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='label', full_name='google.protobuf.FieldDescriptorProto.label', index=2, + number=4, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='type', full_name='google.protobuf.FieldDescriptorProto.type', index=3, + number=5, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='type_name', full_name='google.protobuf.FieldDescriptorProto.type_name', index=4, + number=6, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='extendee', full_name='google.protobuf.FieldDescriptorProto.extendee', index=5, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='default_value', full_name='google.protobuf.FieldDescriptorProto.default_value', index=6, + number=7, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='oneof_index', full_name='google.protobuf.FieldDescriptorProto.oneof_index', index=7, + number=9, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='json_name', full_name='google.protobuf.FieldDescriptorProto.json_name', index=8, + number=10, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.FieldDescriptorProto.options', index=9, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='proto3_optional', full_name='google.protobuf.FieldDescriptorProto.proto3_optional', index=10, + number=17, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _FIELDDESCRIPTORPROTO_TYPE, + _FIELDDESCRIPTORPROTO_LABEL, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _ONEOFDESCRIPTORPROTO = _descriptor.Descriptor( + name='OneofDescriptorProto', + full_name='google.protobuf.OneofDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.OneofDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.OneofDescriptorProto.options', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE = _descriptor.Descriptor( + name='EnumReservedRange', + full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='start', full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange.start', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='end', full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange.end', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _ENUMDESCRIPTORPROTO = _descriptor.Descriptor( + name='EnumDescriptorProto', + full_name='google.protobuf.EnumDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.EnumDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='value', full_name='google.protobuf.EnumDescriptorProto.value', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.EnumDescriptorProto.options', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='reserved_range', full_name='google.protobuf.EnumDescriptorProto.reserved_range', index=3, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='reserved_name', full_name='google.protobuf.EnumDescriptorProto.reserved_name', index=4, + number=5, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _ENUMVALUEDESCRIPTORPROTO = _descriptor.Descriptor( + name='EnumValueDescriptorProto', + full_name='google.protobuf.EnumValueDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.EnumValueDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='number', full_name='google.protobuf.EnumValueDescriptorProto.number', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.EnumValueDescriptorProto.options', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _SERVICEDESCRIPTORPROTO = _descriptor.Descriptor( + name='ServiceDescriptorProto', + full_name='google.protobuf.ServiceDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.ServiceDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='method', full_name='google.protobuf.ServiceDescriptorProto.method', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.ServiceDescriptorProto.options', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _METHODDESCRIPTORPROTO = _descriptor.Descriptor( + name='MethodDescriptorProto', + full_name='google.protobuf.MethodDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.MethodDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='input_type', full_name='google.protobuf.MethodDescriptorProto.input_type', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='output_type', full_name='google.protobuf.MethodDescriptorProto.output_type', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.MethodDescriptorProto.options', index=3, + number=4, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='client_streaming', full_name='google.protobuf.MethodDescriptorProto.client_streaming', index=4, + number=5, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='server_streaming', full_name='google.protobuf.MethodDescriptorProto.server_streaming', index=5, + number=6, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _FILEOPTIONS = _descriptor.Descriptor( + name='FileOptions', + full_name='google.protobuf.FileOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='java_package', full_name='google.protobuf.FileOptions.java_package', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_outer_classname', full_name='google.protobuf.FileOptions.java_outer_classname', index=1, + number=8, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_multiple_files', full_name='google.protobuf.FileOptions.java_multiple_files', index=2, + number=10, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_generate_equals_and_hash', full_name='google.protobuf.FileOptions.java_generate_equals_and_hash', index=3, + number=20, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_string_check_utf8', full_name='google.protobuf.FileOptions.java_string_check_utf8', index=4, + number=27, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='optimize_for', full_name='google.protobuf.FileOptions.optimize_for', index=5, + number=9, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='go_package', full_name='google.protobuf.FileOptions.go_package', index=6, + number=11, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='cc_generic_services', full_name='google.protobuf.FileOptions.cc_generic_services', index=7, + number=16, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_generic_services', full_name='google.protobuf.FileOptions.java_generic_services', index=8, + number=17, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='py_generic_services', full_name='google.protobuf.FileOptions.py_generic_services', index=9, + number=18, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='php_generic_services', full_name='google.protobuf.FileOptions.php_generic_services', index=10, + number=42, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.FileOptions.deprecated', index=11, + number=23, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='cc_enable_arenas', full_name='google.protobuf.FileOptions.cc_enable_arenas', index=12, + number=31, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=True, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='objc_class_prefix', full_name='google.protobuf.FileOptions.objc_class_prefix', index=13, + number=36, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='csharp_namespace', full_name='google.protobuf.FileOptions.csharp_namespace', index=14, + number=37, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='swift_prefix', full_name='google.protobuf.FileOptions.swift_prefix', index=15, + number=39, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='php_class_prefix', full_name='google.protobuf.FileOptions.php_class_prefix', index=16, + number=40, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='php_namespace', full_name='google.protobuf.FileOptions.php_namespace', index=17, + number=41, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='php_metadata_namespace', full_name='google.protobuf.FileOptions.php_metadata_namespace', index=18, + number=44, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='ruby_package', full_name='google.protobuf.FileOptions.ruby_package', index=19, + number=45, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.FileOptions.uninterpreted_option', index=20, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _FILEOPTIONS_OPTIMIZEMODE, + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _MESSAGEOPTIONS = _descriptor.Descriptor( + name='MessageOptions', + full_name='google.protobuf.MessageOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='message_set_wire_format', full_name='google.protobuf.MessageOptions.message_set_wire_format', index=0, + number=1, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='no_standard_descriptor_accessor', full_name='google.protobuf.MessageOptions.no_standard_descriptor_accessor', index=1, + number=2, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.MessageOptions.deprecated', index=2, + number=3, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='map_entry', full_name='google.protobuf.MessageOptions.map_entry', index=3, + number=7, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.MessageOptions.uninterpreted_option', index=4, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _FIELDOPTIONS = _descriptor.Descriptor( + name='FieldOptions', + full_name='google.protobuf.FieldOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='ctype', full_name='google.protobuf.FieldOptions.ctype', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='packed', full_name='google.protobuf.FieldOptions.packed', index=1, + number=2, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='jstype', full_name='google.protobuf.FieldOptions.jstype', index=2, + number=6, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='lazy', full_name='google.protobuf.FieldOptions.lazy', index=3, + number=5, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='unverified_lazy', full_name='google.protobuf.FieldOptions.unverified_lazy', index=4, + number=15, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.FieldOptions.deprecated', index=5, + number=3, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='weak', full_name='google.protobuf.FieldOptions.weak', index=6, + number=10, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.FieldOptions.uninterpreted_option', index=7, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _FIELDOPTIONS_CTYPE, + _FIELDOPTIONS_JSTYPE, + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _ONEOFOPTIONS = _descriptor.Descriptor( + name='OneofOptions', + full_name='google.protobuf.OneofOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.OneofOptions.uninterpreted_option', index=0, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _ENUMOPTIONS = _descriptor.Descriptor( + name='EnumOptions', + full_name='google.protobuf.EnumOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='allow_alias', full_name='google.protobuf.EnumOptions.allow_alias', index=0, + number=2, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.EnumOptions.deprecated', index=1, + number=3, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.EnumOptions.uninterpreted_option', index=2, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _ENUMVALUEOPTIONS = _descriptor.Descriptor( + name='EnumValueOptions', + full_name='google.protobuf.EnumValueOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.EnumValueOptions.deprecated', index=0, + number=1, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.EnumValueOptions.uninterpreted_option', index=1, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _SERVICEOPTIONS = _descriptor.Descriptor( + name='ServiceOptions', + full_name='google.protobuf.ServiceOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.ServiceOptions.deprecated', index=0, + number=33, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.ServiceOptions.uninterpreted_option', index=1, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _METHODOPTIONS = _descriptor.Descriptor( + name='MethodOptions', + full_name='google.protobuf.MethodOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.MethodOptions.deprecated', index=0, + number=33, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='idempotency_level', full_name='google.protobuf.MethodOptions.idempotency_level', index=1, + number=34, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.MethodOptions.uninterpreted_option', index=2, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _METHODOPTIONS_IDEMPOTENCYLEVEL, + ], + serialized_options=None, + is_extendable=True, + syntax='proto2', + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _UNINTERPRETEDOPTION_NAMEPART = _descriptor.Descriptor( + name='NamePart', + full_name='google.protobuf.UninterpretedOption.NamePart', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name_part', full_name='google.protobuf.UninterpretedOption.NamePart.name_part', index=0, + number=1, type=9, cpp_type=9, label=2, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='is_extension', full_name='google.protobuf.UninterpretedOption.NamePart.is_extension', index=1, + number=2, type=8, cpp_type=7, label=2, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _UNINTERPRETEDOPTION = _descriptor.Descriptor( + name='UninterpretedOption', + full_name='google.protobuf.UninterpretedOption', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.UninterpretedOption.name', index=0, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='identifier_value', full_name='google.protobuf.UninterpretedOption.identifier_value', index=1, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='positive_int_value', full_name='google.protobuf.UninterpretedOption.positive_int_value', index=2, + number=4, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='negative_int_value', full_name='google.protobuf.UninterpretedOption.negative_int_value', index=3, + number=5, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='double_value', full_name='google.protobuf.UninterpretedOption.double_value', index=4, + number=6, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='string_value', full_name='google.protobuf.UninterpretedOption.string_value', index=5, + number=7, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='aggregate_value', full_name='google.protobuf.UninterpretedOption.aggregate_value', index=6, + number=8, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_UNINTERPRETEDOPTION_NAMEPART, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _SOURCECODEINFO_LOCATION = _descriptor.Descriptor( + name='Location', + full_name='google.protobuf.SourceCodeInfo.Location', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='path', full_name='google.protobuf.SourceCodeInfo.Location.path', index=0, + number=1, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='span', full_name='google.protobuf.SourceCodeInfo.Location.span', index=1, + number=2, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='leading_comments', full_name='google.protobuf.SourceCodeInfo.Location.leading_comments', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='trailing_comments', full_name='google.protobuf.SourceCodeInfo.Location.trailing_comments', index=3, + number=4, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='leading_detached_comments', full_name='google.protobuf.SourceCodeInfo.Location.leading_detached_comments', index=4, + number=6, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _SOURCECODEINFO = _descriptor.Descriptor( + name='SourceCodeInfo', + full_name='google.protobuf.SourceCodeInfo', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='location', full_name='google.protobuf.SourceCodeInfo.location', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_SOURCECODEINFO_LOCATION, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + + _GENERATEDCODEINFO_ANNOTATION = _descriptor.Descriptor( + name='Annotation', + full_name='google.protobuf.GeneratedCodeInfo.Annotation', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='path', full_name='google.protobuf.GeneratedCodeInfo.Annotation.path', index=0, + number=1, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='source_file', full_name='google.protobuf.GeneratedCodeInfo.Annotation.source_file', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='begin', full_name='google.protobuf.GeneratedCodeInfo.Annotation.begin', index=2, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='end', full_name='google.protobuf.GeneratedCodeInfo.Annotation.end', index=3, + number=4, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _GENERATEDCODEINFO = _descriptor.Descriptor( + name='GeneratedCodeInfo', + full_name='google.protobuf.GeneratedCodeInfo', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='annotation', full_name='google.protobuf.GeneratedCodeInfo.annotation', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_GENERATEDCODEINFO_ANNOTATION, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + ) + + _FILEDESCRIPTORSET.fields_by_name['file'].message_type = _FILEDESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['message_type'].message_type = _DESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['enum_type'].message_type = _ENUMDESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['service'].message_type = _SERVICEDESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['extension'].message_type = _FIELDDESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['options'].message_type = _FILEOPTIONS + _FILEDESCRIPTORPROTO.fields_by_name['source_code_info'].message_type = _SOURCECODEINFO + _DESCRIPTORPROTO_EXTENSIONRANGE.fields_by_name['options'].message_type = _EXTENSIONRANGEOPTIONS + _DESCRIPTORPROTO_EXTENSIONRANGE.containing_type = _DESCRIPTORPROTO + _DESCRIPTORPROTO_RESERVEDRANGE.containing_type = _DESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['field'].message_type = _FIELDDESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['extension'].message_type = _FIELDDESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['nested_type'].message_type = _DESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['enum_type'].message_type = _ENUMDESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['extension_range'].message_type = _DESCRIPTORPROTO_EXTENSIONRANGE + _DESCRIPTORPROTO.fields_by_name['oneof_decl'].message_type = _ONEOFDESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['options'].message_type = _MESSAGEOPTIONS + _DESCRIPTORPROTO.fields_by_name['reserved_range'].message_type = _DESCRIPTORPROTO_RESERVEDRANGE + _EXTENSIONRANGEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _FIELDDESCRIPTORPROTO.fields_by_name['label'].enum_type = _FIELDDESCRIPTORPROTO_LABEL + _FIELDDESCRIPTORPROTO.fields_by_name['type'].enum_type = _FIELDDESCRIPTORPROTO_TYPE + _FIELDDESCRIPTORPROTO.fields_by_name['options'].message_type = _FIELDOPTIONS + _FIELDDESCRIPTORPROTO_TYPE.containing_type = _FIELDDESCRIPTORPROTO + _FIELDDESCRIPTORPROTO_LABEL.containing_type = _FIELDDESCRIPTORPROTO + _ONEOFDESCRIPTORPROTO.fields_by_name['options'].message_type = _ONEOFOPTIONS + _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE.containing_type = _ENUMDESCRIPTORPROTO + _ENUMDESCRIPTORPROTO.fields_by_name['value'].message_type = _ENUMVALUEDESCRIPTORPROTO + _ENUMDESCRIPTORPROTO.fields_by_name['options'].message_type = _ENUMOPTIONS + _ENUMDESCRIPTORPROTO.fields_by_name['reserved_range'].message_type = _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE + _ENUMVALUEDESCRIPTORPROTO.fields_by_name['options'].message_type = _ENUMVALUEOPTIONS + _SERVICEDESCRIPTORPROTO.fields_by_name['method'].message_type = _METHODDESCRIPTORPROTO + _SERVICEDESCRIPTORPROTO.fields_by_name['options'].message_type = _SERVICEOPTIONS + _METHODDESCRIPTORPROTO.fields_by_name['options'].message_type = _METHODOPTIONS + _FILEOPTIONS.fields_by_name['optimize_for'].enum_type = _FILEOPTIONS_OPTIMIZEMODE + _FILEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _FILEOPTIONS_OPTIMIZEMODE.containing_type = _FILEOPTIONS + _MESSAGEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _FIELDOPTIONS.fields_by_name['ctype'].enum_type = _FIELDOPTIONS_CTYPE + _FIELDOPTIONS.fields_by_name['jstype'].enum_type = _FIELDOPTIONS_JSTYPE + _FIELDOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _FIELDOPTIONS_CTYPE.containing_type = _FIELDOPTIONS + _FIELDOPTIONS_JSTYPE.containing_type = _FIELDOPTIONS + _ONEOFOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _ENUMOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _ENUMVALUEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _SERVICEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _METHODOPTIONS.fields_by_name['idempotency_level'].enum_type = _METHODOPTIONS_IDEMPOTENCYLEVEL + _METHODOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _METHODOPTIONS_IDEMPOTENCYLEVEL.containing_type = _METHODOPTIONS + _UNINTERPRETEDOPTION_NAMEPART.containing_type = _UNINTERPRETEDOPTION + _UNINTERPRETEDOPTION.fields_by_name['name'].message_type = _UNINTERPRETEDOPTION_NAMEPART + _SOURCECODEINFO_LOCATION.containing_type = _SOURCECODEINFO + _SOURCECODEINFO.fields_by_name['location'].message_type = _SOURCECODEINFO_LOCATION + _GENERATEDCODEINFO_ANNOTATION.containing_type = _GENERATEDCODEINFO + _GENERATEDCODEINFO.fields_by_name['annotation'].message_type = _GENERATEDCODEINFO_ANNOTATION + DESCRIPTOR.message_types_by_name['FileDescriptorSet'] = _FILEDESCRIPTORSET + DESCRIPTOR.message_types_by_name['FileDescriptorProto'] = _FILEDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['DescriptorProto'] = _DESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['ExtensionRangeOptions'] = _EXTENSIONRANGEOPTIONS + DESCRIPTOR.message_types_by_name['FieldDescriptorProto'] = _FIELDDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['OneofDescriptorProto'] = _ONEOFDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['EnumDescriptorProto'] = _ENUMDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['EnumValueDescriptorProto'] = _ENUMVALUEDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['ServiceDescriptorProto'] = _SERVICEDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['MethodDescriptorProto'] = _METHODDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['FileOptions'] = _FILEOPTIONS + DESCRIPTOR.message_types_by_name['MessageOptions'] = _MESSAGEOPTIONS + DESCRIPTOR.message_types_by_name['FieldOptions'] = _FIELDOPTIONS + DESCRIPTOR.message_types_by_name['OneofOptions'] = _ONEOFOPTIONS + DESCRIPTOR.message_types_by_name['EnumOptions'] = _ENUMOPTIONS + DESCRIPTOR.message_types_by_name['EnumValueOptions'] = _ENUMVALUEOPTIONS + DESCRIPTOR.message_types_by_name['ServiceOptions'] = _SERVICEOPTIONS + DESCRIPTOR.message_types_by_name['MethodOptions'] = _METHODOPTIONS + DESCRIPTOR.message_types_by_name['UninterpretedOption'] = _UNINTERPRETEDOPTION + DESCRIPTOR.message_types_by_name['SourceCodeInfo'] = _SOURCECODEINFO + DESCRIPTOR.message_types_by_name['GeneratedCodeInfo'] = _GENERATEDCODEINFO + _sym_db.RegisterFileDescriptor(DESCRIPTOR) + +else: + _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.descriptor_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _FILEDESCRIPTORSET._serialized_start=53 + _FILEDESCRIPTORSET._serialized_end=124 + _FILEDESCRIPTORPROTO._serialized_start=127 + _FILEDESCRIPTORPROTO._serialized_end=602 + _DESCRIPTORPROTO._serialized_start=605 + _DESCRIPTORPROTO._serialized_end=1286 + _DESCRIPTORPROTO_EXTENSIONRANGE._serialized_start=1140 + _DESCRIPTORPROTO_EXTENSIONRANGE._serialized_end=1241 + _DESCRIPTORPROTO_RESERVEDRANGE._serialized_start=1243 + _DESCRIPTORPROTO_RESERVEDRANGE._serialized_end=1286 + _EXTENSIONRANGEOPTIONS._serialized_start=1288 + _EXTENSIONRANGEOPTIONS._serialized_end=1391 + _FIELDDESCRIPTORPROTO._serialized_start=1394 + _FIELDDESCRIPTORPROTO._serialized_end=2119 + _FIELDDESCRIPTORPROTO_TYPE._serialized_start=1740 + _FIELDDESCRIPTORPROTO_TYPE._serialized_end=2050 + _FIELDDESCRIPTORPROTO_LABEL._serialized_start=2052 + _FIELDDESCRIPTORPROTO_LABEL._serialized_end=2119 + _ONEOFDESCRIPTORPROTO._serialized_start=2121 + _ONEOFDESCRIPTORPROTO._serialized_end=2205 + _ENUMDESCRIPTORPROTO._serialized_start=2208 + _ENUMDESCRIPTORPROTO._serialized_end=2500 + _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE._serialized_start=2453 + _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE._serialized_end=2500 + _ENUMVALUEDESCRIPTORPROTO._serialized_start=2502 + _ENUMVALUEDESCRIPTORPROTO._serialized_end=2610 + _SERVICEDESCRIPTORPROTO._serialized_start=2613 + _SERVICEDESCRIPTORPROTO._serialized_end=2757 + _METHODDESCRIPTORPROTO._serialized_start=2760 + _METHODDESCRIPTORPROTO._serialized_end=2953 + _FILEOPTIONS._serialized_start=2956 + _FILEOPTIONS._serialized_end=3761 + _FILEOPTIONS_OPTIMIZEMODE._serialized_start=3686 + _FILEOPTIONS_OPTIMIZEMODE._serialized_end=3744 + _MESSAGEOPTIONS._serialized_start=3764 + _MESSAGEOPTIONS._serialized_end=4024 + _FIELDOPTIONS._serialized_start=4027 + _FIELDOPTIONS._serialized_end=4473 + _FIELDOPTIONS_CTYPE._serialized_start=4354 + _FIELDOPTIONS_CTYPE._serialized_end=4401 + _FIELDOPTIONS_JSTYPE._serialized_start=4403 + _FIELDOPTIONS_JSTYPE._serialized_end=4456 + _ONEOFOPTIONS._serialized_start=4475 + _ONEOFOPTIONS._serialized_end=4569 + _ENUMOPTIONS._serialized_start=4572 + _ENUMOPTIONS._serialized_end=4719 + _ENUMVALUEOPTIONS._serialized_start=4721 + _ENUMVALUEOPTIONS._serialized_end=4846 + _SERVICEOPTIONS._serialized_start=4848 + _SERVICEOPTIONS._serialized_end=4971 + _METHODOPTIONS._serialized_start=4974 + _METHODOPTIONS._serialized_end=5275 + _METHODOPTIONS_IDEMPOTENCYLEVEL._serialized_start=5184 + _METHODOPTIONS_IDEMPOTENCYLEVEL._serialized_end=5264 + _UNINTERPRETEDOPTION._serialized_start=5278 + _UNINTERPRETEDOPTION._serialized_end=5564 + _UNINTERPRETEDOPTION_NAMEPART._serialized_start=5513 + _UNINTERPRETEDOPTION_NAMEPART._serialized_end=5564 + _SOURCECODEINFO._serialized_start=5567 + _SOURCECODEINFO._serialized_end=5780 + _SOURCECODEINFO_LOCATION._serialized_start=5646 + _SOURCECODEINFO_LOCATION._serialized_end=5780 + _GENERATEDCODEINFO._serialized_start=5783 + _GENERATEDCODEINFO._serialized_end=5950 + _GENERATEDCODEINFO_ANNOTATION._serialized_start=5871 + _GENERATEDCODEINFO_ANNOTATION._serialized_end=5950 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/descriptor_pool.py b/scripts/protobuf3/protobuf3/descriptor_pool.py new file mode 100644 index 0000000..911372a --- /dev/null +++ b/scripts/protobuf3/protobuf3/descriptor_pool.py @@ -0,0 +1,1295 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Provides DescriptorPool to use as a container for proto2 descriptors. + +The DescriptorPool is used in conjection with a DescriptorDatabase to maintain +a collection of protocol buffer descriptors for use when dynamically creating +message types at runtime. + +For most applications protocol buffers should be used via modules generated by +the protocol buffer compiler tool. This should only be used when the type of +protocol buffers used in an application or library cannot be predetermined. + +Below is a straightforward example on how to use this class:: + + pool = DescriptorPool() + file_descriptor_protos = [ ... ] + for file_descriptor_proto in file_descriptor_protos: + pool.Add(file_descriptor_proto) + my_message_descriptor = pool.FindMessageTypeByName('some.package.MessageType') + +The message descriptor can be used in conjunction with the message_factory +module in order to create a protocol buffer class that can be encoded and +decoded. + +If you want to get a Python class for the specified proto, use the +helper functions inside google.protobuf.message_factory +directly instead of this class. +""" + +__author__ = 'matthewtoia@google.com (Matt Toia)' + +import collections +import warnings + +from google.protobuf import descriptor +from google.protobuf import descriptor_database +from google.protobuf import text_encoding + + +_USE_C_DESCRIPTORS = descriptor._USE_C_DESCRIPTORS # pylint: disable=protected-access + + +def _Deprecated(func): + """Mark functions as deprecated.""" + + def NewFunc(*args, **kwargs): + warnings.warn( + 'Call to deprecated function %s(). Note: Do add unlinked descriptors ' + 'to descriptor_pool is wrong. Use Add() or AddSerializedFile() ' + 'instead.' % func.__name__, + category=DeprecationWarning) + return func(*args, **kwargs) + NewFunc.__name__ = func.__name__ + NewFunc.__doc__ = func.__doc__ + NewFunc.__dict__.update(func.__dict__) + return NewFunc + + +def _NormalizeFullyQualifiedName(name): + """Remove leading period from fully-qualified type name. + + Due to b/13860351 in descriptor_database.py, types in the root namespace are + generated with a leading period. This function removes that prefix. + + Args: + name (str): The fully-qualified symbol name. + + Returns: + str: The normalized fully-qualified symbol name. + """ + return name.lstrip('.') + + +def _OptionsOrNone(descriptor_proto): + """Returns the value of the field `options`, or None if it is not set.""" + if descriptor_proto.HasField('options'): + return descriptor_proto.options + else: + return None + + +def _IsMessageSetExtension(field): + return (field.is_extension and + field.containing_type.has_options and + field.containing_type.GetOptions().message_set_wire_format and + field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and + field.label == descriptor.FieldDescriptor.LABEL_OPTIONAL) + + +class DescriptorPool(object): + """A collection of protobufs dynamically constructed by descriptor protos.""" + + if _USE_C_DESCRIPTORS: + + def __new__(cls, descriptor_db=None): + # pylint: disable=protected-access + return descriptor._message.DescriptorPool(descriptor_db) + + def __init__(self, descriptor_db=None): + """Initializes a Pool of proto buffs. + + The descriptor_db argument to the constructor is provided to allow + specialized file descriptor proto lookup code to be triggered on demand. An + example would be an implementation which will read and compile a file + specified in a call to FindFileByName() and not require the call to Add() + at all. Results from this database will be cached internally here as well. + + Args: + descriptor_db: A secondary source of file descriptors. + """ + + self._internal_db = descriptor_database.DescriptorDatabase() + self._descriptor_db = descriptor_db + self._descriptors = {} + self._enum_descriptors = {} + self._service_descriptors = {} + self._file_descriptors = {} + self._toplevel_extensions = {} + # TODO(jieluo): Remove _file_desc_by_toplevel_extension after + # maybe year 2020 for compatibility issue (with 3.4.1 only). + self._file_desc_by_toplevel_extension = {} + self._top_enum_values = {} + # We store extensions in two two-level mappings: The first key is the + # descriptor of the message being extended, the second key is the extension + # full name or its tag number. + self._extensions_by_name = collections.defaultdict(dict) + self._extensions_by_number = collections.defaultdict(dict) + + def _CheckConflictRegister(self, desc, desc_name, file_name): + """Check if the descriptor name conflicts with another of the same name. + + Args: + desc: Descriptor of a message, enum, service, extension or enum value. + desc_name (str): the full name of desc. + file_name (str): The file name of descriptor. + """ + for register, descriptor_type in [ + (self._descriptors, descriptor.Descriptor), + (self._enum_descriptors, descriptor.EnumDescriptor), + (self._service_descriptors, descriptor.ServiceDescriptor), + (self._toplevel_extensions, descriptor.FieldDescriptor), + (self._top_enum_values, descriptor.EnumValueDescriptor)]: + if desc_name in register: + old_desc = register[desc_name] + if isinstance(old_desc, descriptor.EnumValueDescriptor): + old_file = old_desc.type.file.name + else: + old_file = old_desc.file.name + + if not isinstance(desc, descriptor_type) or ( + old_file != file_name): + error_msg = ('Conflict register for file "' + file_name + + '": ' + desc_name + + ' is already defined in file "' + + old_file + '". Please fix the conflict by adding ' + 'package name on the proto file, or use different ' + 'name for the duplication.') + if isinstance(desc, descriptor.EnumValueDescriptor): + error_msg += ('\nNote: enum values appear as ' + 'siblings of the enum type instead of ' + 'children of it.') + + raise TypeError(error_msg) + + return + + def Add(self, file_desc_proto): + """Adds the FileDescriptorProto and its types to this pool. + + Args: + file_desc_proto (FileDescriptorProto): The file descriptor to add. + """ + + self._internal_db.Add(file_desc_proto) + + def AddSerializedFile(self, serialized_file_desc_proto): + """Adds the FileDescriptorProto and its types to this pool. + + Args: + serialized_file_desc_proto (bytes): A bytes string, serialization of the + :class:`FileDescriptorProto` to add. + + Returns: + FileDescriptor: Descriptor for the added file. + """ + + # pylint: disable=g-import-not-at-top + from google.protobuf import descriptor_pb2 + file_desc_proto = descriptor_pb2.FileDescriptorProto.FromString( + serialized_file_desc_proto) + file_desc = self._ConvertFileProtoToFileDescriptor(file_desc_proto) + file_desc.serialized_pb = serialized_file_desc_proto + return file_desc + + # Add Descriptor to descriptor pool is dreprecated. Please use Add() + # or AddSerializedFile() to add a FileDescriptorProto instead. + @_Deprecated + def AddDescriptor(self, desc): + self._AddDescriptor(desc) + + # Never call this method. It is for internal usage only. + def _AddDescriptor(self, desc): + """Adds a Descriptor to the pool, non-recursively. + + If the Descriptor contains nested messages or enums, the caller must + explicitly register them. This method also registers the FileDescriptor + associated with the message. + + Args: + desc: A Descriptor. + """ + if not isinstance(desc, descriptor.Descriptor): + raise TypeError('Expected instance of descriptor.Descriptor.') + + self._CheckConflictRegister(desc, desc.full_name, desc.file.name) + + self._descriptors[desc.full_name] = desc + self._AddFileDescriptor(desc.file) + + # Add EnumDescriptor to descriptor pool is dreprecated. Please use Add() + # or AddSerializedFile() to add a FileDescriptorProto instead. + @_Deprecated + def AddEnumDescriptor(self, enum_desc): + self._AddEnumDescriptor(enum_desc) + + # Never call this method. It is for internal usage only. + def _AddEnumDescriptor(self, enum_desc): + """Adds an EnumDescriptor to the pool. + + This method also registers the FileDescriptor associated with the enum. + + Args: + enum_desc: An EnumDescriptor. + """ + + if not isinstance(enum_desc, descriptor.EnumDescriptor): + raise TypeError('Expected instance of descriptor.EnumDescriptor.') + + file_name = enum_desc.file.name + self._CheckConflictRegister(enum_desc, enum_desc.full_name, file_name) + self._enum_descriptors[enum_desc.full_name] = enum_desc + + # Top enum values need to be indexed. + # Count the number of dots to see whether the enum is toplevel or nested + # in a message. We cannot use enum_desc.containing_type at this stage. + if enum_desc.file.package: + top_level = (enum_desc.full_name.count('.') + - enum_desc.file.package.count('.') == 1) + else: + top_level = enum_desc.full_name.count('.') == 0 + if top_level: + file_name = enum_desc.file.name + package = enum_desc.file.package + for enum_value in enum_desc.values: + full_name = _NormalizeFullyQualifiedName( + '.'.join((package, enum_value.name))) + self._CheckConflictRegister(enum_value, full_name, file_name) + self._top_enum_values[full_name] = enum_value + self._AddFileDescriptor(enum_desc.file) + + # Add ServiceDescriptor to descriptor pool is dreprecated. Please use Add() + # or AddSerializedFile() to add a FileDescriptorProto instead. + @_Deprecated + def AddServiceDescriptor(self, service_desc): + self._AddServiceDescriptor(service_desc) + + # Never call this method. It is for internal usage only. + def _AddServiceDescriptor(self, service_desc): + """Adds a ServiceDescriptor to the pool. + + Args: + service_desc: A ServiceDescriptor. + """ + + if not isinstance(service_desc, descriptor.ServiceDescriptor): + raise TypeError('Expected instance of descriptor.ServiceDescriptor.') + + self._CheckConflictRegister(service_desc, service_desc.full_name, + service_desc.file.name) + self._service_descriptors[service_desc.full_name] = service_desc + + # Add ExtensionDescriptor to descriptor pool is dreprecated. Please use Add() + # or AddSerializedFile() to add a FileDescriptorProto instead. + @_Deprecated + def AddExtensionDescriptor(self, extension): + self._AddExtensionDescriptor(extension) + + # Never call this method. It is for internal usage only. + def _AddExtensionDescriptor(self, extension): + """Adds a FieldDescriptor describing an extension to the pool. + + Args: + extension: A FieldDescriptor. + + Raises: + AssertionError: when another extension with the same number extends the + same message. + TypeError: when the specified extension is not a + descriptor.FieldDescriptor. + """ + if not (isinstance(extension, descriptor.FieldDescriptor) and + extension.is_extension): + raise TypeError('Expected an extension descriptor.') + + if extension.extension_scope is None: + self._toplevel_extensions[extension.full_name] = extension + + try: + existing_desc = self._extensions_by_number[ + extension.containing_type][extension.number] + except KeyError: + pass + else: + if extension is not existing_desc: + raise AssertionError( + 'Extensions "%s" and "%s" both try to extend message type "%s" ' + 'with field number %d.' % + (extension.full_name, existing_desc.full_name, + extension.containing_type.full_name, extension.number)) + + self._extensions_by_number[extension.containing_type][ + extension.number] = extension + self._extensions_by_name[extension.containing_type][ + extension.full_name] = extension + + # Also register MessageSet extensions with the type name. + if _IsMessageSetExtension(extension): + self._extensions_by_name[extension.containing_type][ + extension.message_type.full_name] = extension + + @_Deprecated + def AddFileDescriptor(self, file_desc): + self._InternalAddFileDescriptor(file_desc) + + # Never call this method. It is for internal usage only. + def _InternalAddFileDescriptor(self, file_desc): + """Adds a FileDescriptor to the pool, non-recursively. + + If the FileDescriptor contains messages or enums, the caller must explicitly + register them. + + Args: + file_desc: A FileDescriptor. + """ + + self._AddFileDescriptor(file_desc) + # TODO(jieluo): This is a temporary solution for FieldDescriptor.file. + # FieldDescriptor.file is added in code gen. Remove this solution after + # maybe 2020 for compatibility reason (with 3.4.1 only). + for extension in file_desc.extensions_by_name.values(): + self._file_desc_by_toplevel_extension[ + extension.full_name] = file_desc + + def _AddFileDescriptor(self, file_desc): + """Adds a FileDescriptor to the pool, non-recursively. + + If the FileDescriptor contains messages or enums, the caller must explicitly + register them. + + Args: + file_desc: A FileDescriptor. + """ + + if not isinstance(file_desc, descriptor.FileDescriptor): + raise TypeError('Expected instance of descriptor.FileDescriptor.') + self._file_descriptors[file_desc.name] = file_desc + + def FindFileByName(self, file_name): + """Gets a FileDescriptor by file name. + + Args: + file_name (str): The path to the file to get a descriptor for. + + Returns: + FileDescriptor: The descriptor for the named file. + + Raises: + KeyError: if the file cannot be found in the pool. + """ + + try: + return self._file_descriptors[file_name] + except KeyError: + pass + + try: + file_proto = self._internal_db.FindFileByName(file_name) + except KeyError as error: + if self._descriptor_db: + file_proto = self._descriptor_db.FindFileByName(file_name) + else: + raise error + if not file_proto: + raise KeyError('Cannot find a file named %s' % file_name) + return self._ConvertFileProtoToFileDescriptor(file_proto) + + def FindFileContainingSymbol(self, symbol): + """Gets the FileDescriptor for the file containing the specified symbol. + + Args: + symbol (str): The name of the symbol to search for. + + Returns: + FileDescriptor: Descriptor for the file that contains the specified + symbol. + + Raises: + KeyError: if the file cannot be found in the pool. + """ + + symbol = _NormalizeFullyQualifiedName(symbol) + try: + return self._InternalFindFileContainingSymbol(symbol) + except KeyError: + pass + + try: + # Try fallback database. Build and find again if possible. + self._FindFileContainingSymbolInDb(symbol) + return self._InternalFindFileContainingSymbol(symbol) + except KeyError: + raise KeyError('Cannot find a file containing %s' % symbol) + + def _InternalFindFileContainingSymbol(self, symbol): + """Gets the already built FileDescriptor containing the specified symbol. + + Args: + symbol (str): The name of the symbol to search for. + + Returns: + FileDescriptor: Descriptor for the file that contains the specified + symbol. + + Raises: + KeyError: if the file cannot be found in the pool. + """ + try: + return self._descriptors[symbol].file + except KeyError: + pass + + try: + return self._enum_descriptors[symbol].file + except KeyError: + pass + + try: + return self._service_descriptors[symbol].file + except KeyError: + pass + + try: + return self._top_enum_values[symbol].type.file + except KeyError: + pass + + try: + return self._file_desc_by_toplevel_extension[symbol] + except KeyError: + pass + + # Try fields, enum values and nested extensions inside a message. + top_name, _, sub_name = symbol.rpartition('.') + try: + message = self.FindMessageTypeByName(top_name) + assert (sub_name in message.extensions_by_name or + sub_name in message.fields_by_name or + sub_name in message.enum_values_by_name) + return message.file + except (KeyError, AssertionError): + raise KeyError('Cannot find a file containing %s' % symbol) + + def FindMessageTypeByName(self, full_name): + """Loads the named descriptor from the pool. + + Args: + full_name (str): The full name of the descriptor to load. + + Returns: + Descriptor: The descriptor for the named type. + + Raises: + KeyError: if the message cannot be found in the pool. + """ + + full_name = _NormalizeFullyQualifiedName(full_name) + if full_name not in self._descriptors: + self._FindFileContainingSymbolInDb(full_name) + return self._descriptors[full_name] + + def FindEnumTypeByName(self, full_name): + """Loads the named enum descriptor from the pool. + + Args: + full_name (str): The full name of the enum descriptor to load. + + Returns: + EnumDescriptor: The enum descriptor for the named type. + + Raises: + KeyError: if the enum cannot be found in the pool. + """ + + full_name = _NormalizeFullyQualifiedName(full_name) + if full_name not in self._enum_descriptors: + self._FindFileContainingSymbolInDb(full_name) + return self._enum_descriptors[full_name] + + def FindFieldByName(self, full_name): + """Loads the named field descriptor from the pool. + + Args: + full_name (str): The full name of the field descriptor to load. + + Returns: + FieldDescriptor: The field descriptor for the named field. + + Raises: + KeyError: if the field cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + message_name, _, field_name = full_name.rpartition('.') + message_descriptor = self.FindMessageTypeByName(message_name) + return message_descriptor.fields_by_name[field_name] + + def FindOneofByName(self, full_name): + """Loads the named oneof descriptor from the pool. + + Args: + full_name (str): The full name of the oneof descriptor to load. + + Returns: + OneofDescriptor: The oneof descriptor for the named oneof. + + Raises: + KeyError: if the oneof cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + message_name, _, oneof_name = full_name.rpartition('.') + message_descriptor = self.FindMessageTypeByName(message_name) + return message_descriptor.oneofs_by_name[oneof_name] + + def FindExtensionByName(self, full_name): + """Loads the named extension descriptor from the pool. + + Args: + full_name (str): The full name of the extension descriptor to load. + + Returns: + FieldDescriptor: The field descriptor for the named extension. + + Raises: + KeyError: if the extension cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + try: + # The proto compiler does not give any link between the FileDescriptor + # and top-level extensions unless the FileDescriptorProto is added to + # the DescriptorDatabase, but this can impact memory usage. + # So we registered these extensions by name explicitly. + return self._toplevel_extensions[full_name] + except KeyError: + pass + message_name, _, extension_name = full_name.rpartition('.') + try: + # Most extensions are nested inside a message. + scope = self.FindMessageTypeByName(message_name) + except KeyError: + # Some extensions are defined at file scope. + scope = self._FindFileContainingSymbolInDb(full_name) + return scope.extensions_by_name[extension_name] + + def FindExtensionByNumber(self, message_descriptor, number): + """Gets the extension of the specified message with the specified number. + + Extensions have to be registered to this pool by calling :func:`Add` or + :func:`AddExtensionDescriptor`. + + Args: + message_descriptor (Descriptor): descriptor of the extended message. + number (int): Number of the extension field. + + Returns: + FieldDescriptor: The descriptor for the extension. + + Raises: + KeyError: when no extension with the given number is known for the + specified message. + """ + try: + return self._extensions_by_number[message_descriptor][number] + except KeyError: + self._TryLoadExtensionFromDB(message_descriptor, number) + return self._extensions_by_number[message_descriptor][number] + + def FindAllExtensions(self, message_descriptor): + """Gets all the known extensions of a given message. + + Extensions have to be registered to this pool by build related + :func:`Add` or :func:`AddExtensionDescriptor`. + + Args: + message_descriptor (Descriptor): Descriptor of the extended message. + + Returns: + list[FieldDescriptor]: Field descriptors describing the extensions. + """ + # Fallback to descriptor db if FindAllExtensionNumbers is provided. + if self._descriptor_db and hasattr( + self._descriptor_db, 'FindAllExtensionNumbers'): + full_name = message_descriptor.full_name + all_numbers = self._descriptor_db.FindAllExtensionNumbers(full_name) + for number in all_numbers: + if number in self._extensions_by_number[message_descriptor]: + continue + self._TryLoadExtensionFromDB(message_descriptor, number) + + return list(self._extensions_by_number[message_descriptor].values()) + + def _TryLoadExtensionFromDB(self, message_descriptor, number): + """Try to Load extensions from descriptor db. + + Args: + message_descriptor: descriptor of the extended message. + number: the extension number that needs to be loaded. + """ + if not self._descriptor_db: + return + # Only supported when FindFileContainingExtension is provided. + if not hasattr( + self._descriptor_db, 'FindFileContainingExtension'): + return + + full_name = message_descriptor.full_name + file_proto = self._descriptor_db.FindFileContainingExtension( + full_name, number) + + if file_proto is None: + return + + try: + self._ConvertFileProtoToFileDescriptor(file_proto) + except: + warn_msg = ('Unable to load proto file %s for extension number %d.' % + (file_proto.name, number)) + warnings.warn(warn_msg, RuntimeWarning) + + def FindServiceByName(self, full_name): + """Loads the named service descriptor from the pool. + + Args: + full_name (str): The full name of the service descriptor to load. + + Returns: + ServiceDescriptor: The service descriptor for the named service. + + Raises: + KeyError: if the service cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + if full_name not in self._service_descriptors: + self._FindFileContainingSymbolInDb(full_name) + return self._service_descriptors[full_name] + + def FindMethodByName(self, full_name): + """Loads the named service method descriptor from the pool. + + Args: + full_name (str): The full name of the method descriptor to load. + + Returns: + MethodDescriptor: The method descriptor for the service method. + + Raises: + KeyError: if the method cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + service_name, _, method_name = full_name.rpartition('.') + service_descriptor = self.FindServiceByName(service_name) + return service_descriptor.methods_by_name[method_name] + + def _FindFileContainingSymbolInDb(self, symbol): + """Finds the file in descriptor DB containing the specified symbol. + + Args: + symbol (str): The name of the symbol to search for. + + Returns: + FileDescriptor: The file that contains the specified symbol. + + Raises: + KeyError: if the file cannot be found in the descriptor database. + """ + try: + file_proto = self._internal_db.FindFileContainingSymbol(symbol) + except KeyError as error: + if self._descriptor_db: + file_proto = self._descriptor_db.FindFileContainingSymbol(symbol) + else: + raise error + if not file_proto: + raise KeyError('Cannot find a file containing %s' % symbol) + return self._ConvertFileProtoToFileDescriptor(file_proto) + + def _ConvertFileProtoToFileDescriptor(self, file_proto): + """Creates a FileDescriptor from a proto or returns a cached copy. + + This method also has the side effect of loading all the symbols found in + the file into the appropriate dictionaries in the pool. + + Args: + file_proto: The proto to convert. + + Returns: + A FileDescriptor matching the passed in proto. + """ + if file_proto.name not in self._file_descriptors: + built_deps = list(self._GetDeps(file_proto.dependency)) + direct_deps = [self.FindFileByName(n) for n in file_proto.dependency] + public_deps = [direct_deps[i] for i in file_proto.public_dependency] + + file_descriptor = descriptor.FileDescriptor( + pool=self, + name=file_proto.name, + package=file_proto.package, + syntax=file_proto.syntax, + options=_OptionsOrNone(file_proto), + serialized_pb=file_proto.SerializeToString(), + dependencies=direct_deps, + public_dependencies=public_deps, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + scope = {} + + # This loop extracts all the message and enum types from all the + # dependencies of the file_proto. This is necessary to create the + # scope of available message types when defining the passed in + # file proto. + for dependency in built_deps: + scope.update(self._ExtractSymbols( + dependency.message_types_by_name.values())) + scope.update((_PrefixWithDot(enum.full_name), enum) + for enum in dependency.enum_types_by_name.values()) + + for message_type in file_proto.message_type: + message_desc = self._ConvertMessageDescriptor( + message_type, file_proto.package, file_descriptor, scope, + file_proto.syntax) + file_descriptor.message_types_by_name[message_desc.name] = ( + message_desc) + + for enum_type in file_proto.enum_type: + file_descriptor.enum_types_by_name[enum_type.name] = ( + self._ConvertEnumDescriptor(enum_type, file_proto.package, + file_descriptor, None, scope, True)) + + for index, extension_proto in enumerate(file_proto.extension): + extension_desc = self._MakeFieldDescriptor( + extension_proto, file_proto.package, index, file_descriptor, + is_extension=True) + extension_desc.containing_type = self._GetTypeFromScope( + file_descriptor.package, extension_proto.extendee, scope) + self._SetFieldType(extension_proto, extension_desc, + file_descriptor.package, scope) + file_descriptor.extensions_by_name[extension_desc.name] = ( + extension_desc) + self._file_desc_by_toplevel_extension[extension_desc.full_name] = ( + file_descriptor) + + for desc_proto in file_proto.message_type: + self._SetAllFieldTypes(file_proto.package, desc_proto, scope) + + if file_proto.package: + desc_proto_prefix = _PrefixWithDot(file_proto.package) + else: + desc_proto_prefix = '' + + for desc_proto in file_proto.message_type: + desc = self._GetTypeFromScope( + desc_proto_prefix, desc_proto.name, scope) + file_descriptor.message_types_by_name[desc_proto.name] = desc + + for index, service_proto in enumerate(file_proto.service): + file_descriptor.services_by_name[service_proto.name] = ( + self._MakeServiceDescriptor(service_proto, index, scope, + file_proto.package, file_descriptor)) + + self._file_descriptors[file_proto.name] = file_descriptor + + # Add extensions to the pool + file_desc = self._file_descriptors[file_proto.name] + for extension in file_desc.extensions_by_name.values(): + self._AddExtensionDescriptor(extension) + for message_type in file_desc.message_types_by_name.values(): + for extension in message_type.extensions: + self._AddExtensionDescriptor(extension) + + return file_desc + + def _ConvertMessageDescriptor(self, desc_proto, package=None, file_desc=None, + scope=None, syntax=None): + """Adds the proto to the pool in the specified package. + + Args: + desc_proto: The descriptor_pb2.DescriptorProto protobuf message. + package: The package the proto should be located in. + file_desc: The file containing this message. + scope: Dict mapping short and full symbols to message and enum types. + syntax: string indicating syntax of the file ("proto2" or "proto3") + + Returns: + The added descriptor. + """ + + if package: + desc_name = '.'.join((package, desc_proto.name)) + else: + desc_name = desc_proto.name + + if file_desc is None: + file_name = None + else: + file_name = file_desc.name + + if scope is None: + scope = {} + + nested = [ + self._ConvertMessageDescriptor( + nested, desc_name, file_desc, scope, syntax) + for nested in desc_proto.nested_type] + enums = [ + self._ConvertEnumDescriptor(enum, desc_name, file_desc, None, + scope, False) + for enum in desc_proto.enum_type] + fields = [self._MakeFieldDescriptor(field, desc_name, index, file_desc) + for index, field in enumerate(desc_proto.field)] + extensions = [ + self._MakeFieldDescriptor(extension, desc_name, index, file_desc, + is_extension=True) + for index, extension in enumerate(desc_proto.extension)] + oneofs = [ + # pylint: disable=g-complex-comprehension + descriptor.OneofDescriptor( + desc.name, + '.'.join((desc_name, desc.name)), + index, + None, + [], + _OptionsOrNone(desc), + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + for index, desc in enumerate(desc_proto.oneof_decl) + ] + extension_ranges = [(r.start, r.end) for r in desc_proto.extension_range] + if extension_ranges: + is_extendable = True + else: + is_extendable = False + desc = descriptor.Descriptor( + name=desc_proto.name, + full_name=desc_name, + filename=file_name, + containing_type=None, + fields=fields, + oneofs=oneofs, + nested_types=nested, + enum_types=enums, + extensions=extensions, + options=_OptionsOrNone(desc_proto), + is_extendable=is_extendable, + extension_ranges=extension_ranges, + file=file_desc, + serialized_start=None, + serialized_end=None, + syntax=syntax, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + for nested in desc.nested_types: + nested.containing_type = desc + for enum in desc.enum_types: + enum.containing_type = desc + for field_index, field_desc in enumerate(desc_proto.field): + if field_desc.HasField('oneof_index'): + oneof_index = field_desc.oneof_index + oneofs[oneof_index].fields.append(fields[field_index]) + fields[field_index].containing_oneof = oneofs[oneof_index] + + scope[_PrefixWithDot(desc_name)] = desc + self._CheckConflictRegister(desc, desc.full_name, desc.file.name) + self._descriptors[desc_name] = desc + return desc + + def _ConvertEnumDescriptor(self, enum_proto, package=None, file_desc=None, + containing_type=None, scope=None, top_level=False): + """Make a protobuf EnumDescriptor given an EnumDescriptorProto protobuf. + + Args: + enum_proto: The descriptor_pb2.EnumDescriptorProto protobuf message. + package: Optional package name for the new message EnumDescriptor. + file_desc: The file containing the enum descriptor. + containing_type: The type containing this enum. + scope: Scope containing available types. + top_level: If True, the enum is a top level symbol. If False, the enum + is defined inside a message. + + Returns: + The added descriptor + """ + + if package: + enum_name = '.'.join((package, enum_proto.name)) + else: + enum_name = enum_proto.name + + if file_desc is None: + file_name = None + else: + file_name = file_desc.name + + values = [self._MakeEnumValueDescriptor(value, index) + for index, value in enumerate(enum_proto.value)] + desc = descriptor.EnumDescriptor(name=enum_proto.name, + full_name=enum_name, + filename=file_name, + file=file_desc, + values=values, + containing_type=containing_type, + options=_OptionsOrNone(enum_proto), + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + scope['.%s' % enum_name] = desc + self._CheckConflictRegister(desc, desc.full_name, desc.file.name) + self._enum_descriptors[enum_name] = desc + + # Add top level enum values. + if top_level: + for value in values: + full_name = _NormalizeFullyQualifiedName( + '.'.join((package, value.name))) + self._CheckConflictRegister(value, full_name, file_name) + self._top_enum_values[full_name] = value + + return desc + + def _MakeFieldDescriptor(self, field_proto, message_name, index, + file_desc, is_extension=False): + """Creates a field descriptor from a FieldDescriptorProto. + + For message and enum type fields, this method will do a look up + in the pool for the appropriate descriptor for that type. If it + is unavailable, it will fall back to the _source function to + create it. If this type is still unavailable, construction will + fail. + + Args: + field_proto: The proto describing the field. + message_name: The name of the containing message. + index: Index of the field + file_desc: The file containing the field descriptor. + is_extension: Indication that this field is for an extension. + + Returns: + An initialized FieldDescriptor object + """ + + if message_name: + full_name = '.'.join((message_name, field_proto.name)) + else: + full_name = field_proto.name + + if field_proto.json_name: + json_name = field_proto.json_name + else: + json_name = None + + return descriptor.FieldDescriptor( + name=field_proto.name, + full_name=full_name, + index=index, + number=field_proto.number, + type=field_proto.type, + cpp_type=None, + message_type=None, + enum_type=None, + containing_type=None, + label=field_proto.label, + has_default_value=False, + default_value=None, + is_extension=is_extension, + extension_scope=None, + options=_OptionsOrNone(field_proto), + json_name=json_name, + file=file_desc, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + + def _SetAllFieldTypes(self, package, desc_proto, scope): + """Sets all the descriptor's fields's types. + + This method also sets the containing types on any extensions. + + Args: + package: The current package of desc_proto. + desc_proto: The message descriptor to update. + scope: Enclosing scope of available types. + """ + + package = _PrefixWithDot(package) + + main_desc = self._GetTypeFromScope(package, desc_proto.name, scope) + + if package == '.': + nested_package = _PrefixWithDot(desc_proto.name) + else: + nested_package = '.'.join([package, desc_proto.name]) + + for field_proto, field_desc in zip(desc_proto.field, main_desc.fields): + self._SetFieldType(field_proto, field_desc, nested_package, scope) + + for extension_proto, extension_desc in ( + zip(desc_proto.extension, main_desc.extensions)): + extension_desc.containing_type = self._GetTypeFromScope( + nested_package, extension_proto.extendee, scope) + self._SetFieldType(extension_proto, extension_desc, nested_package, scope) + + for nested_type in desc_proto.nested_type: + self._SetAllFieldTypes(nested_package, nested_type, scope) + + def _SetFieldType(self, field_proto, field_desc, package, scope): + """Sets the field's type, cpp_type, message_type and enum_type. + + Args: + field_proto: Data about the field in proto format. + field_desc: The descriptor to modify. + package: The package the field's container is in. + scope: Enclosing scope of available types. + """ + if field_proto.type_name: + desc = self._GetTypeFromScope(package, field_proto.type_name, scope) + else: + desc = None + + if not field_proto.HasField('type'): + if isinstance(desc, descriptor.Descriptor): + field_proto.type = descriptor.FieldDescriptor.TYPE_MESSAGE + else: + field_proto.type = descriptor.FieldDescriptor.TYPE_ENUM + + field_desc.cpp_type = descriptor.FieldDescriptor.ProtoTypeToCppProtoType( + field_proto.type) + + if (field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE + or field_proto.type == descriptor.FieldDescriptor.TYPE_GROUP): + field_desc.message_type = desc + + if field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM: + field_desc.enum_type = desc + + if field_proto.label == descriptor.FieldDescriptor.LABEL_REPEATED: + field_desc.has_default_value = False + field_desc.default_value = [] + elif field_proto.HasField('default_value'): + field_desc.has_default_value = True + if (field_proto.type == descriptor.FieldDescriptor.TYPE_DOUBLE or + field_proto.type == descriptor.FieldDescriptor.TYPE_FLOAT): + field_desc.default_value = float(field_proto.default_value) + elif field_proto.type == descriptor.FieldDescriptor.TYPE_STRING: + field_desc.default_value = field_proto.default_value + elif field_proto.type == descriptor.FieldDescriptor.TYPE_BOOL: + field_desc.default_value = field_proto.default_value.lower() == 'true' + elif field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM: + field_desc.default_value = field_desc.enum_type.values_by_name[ + field_proto.default_value].number + elif field_proto.type == descriptor.FieldDescriptor.TYPE_BYTES: + field_desc.default_value = text_encoding.CUnescape( + field_proto.default_value) + elif field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE: + field_desc.default_value = None + else: + # All other types are of the "int" type. + field_desc.default_value = int(field_proto.default_value) + else: + field_desc.has_default_value = False + if (field_proto.type == descriptor.FieldDescriptor.TYPE_DOUBLE or + field_proto.type == descriptor.FieldDescriptor.TYPE_FLOAT): + field_desc.default_value = 0.0 + elif field_proto.type == descriptor.FieldDescriptor.TYPE_STRING: + field_desc.default_value = u'' + elif field_proto.type == descriptor.FieldDescriptor.TYPE_BOOL: + field_desc.default_value = False + elif field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM: + field_desc.default_value = field_desc.enum_type.values[0].number + elif field_proto.type == descriptor.FieldDescriptor.TYPE_BYTES: + field_desc.default_value = b'' + elif field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE: + field_desc.default_value = None + elif field_proto.type == descriptor.FieldDescriptor.TYPE_GROUP: + field_desc.default_value = None + else: + # All other types are of the "int" type. + field_desc.default_value = 0 + + field_desc.type = field_proto.type + + def _MakeEnumValueDescriptor(self, value_proto, index): + """Creates a enum value descriptor object from a enum value proto. + + Args: + value_proto: The proto describing the enum value. + index: The index of the enum value. + + Returns: + An initialized EnumValueDescriptor object. + """ + + return descriptor.EnumValueDescriptor( + name=value_proto.name, + index=index, + number=value_proto.number, + options=_OptionsOrNone(value_proto), + type=None, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + + def _MakeServiceDescriptor(self, service_proto, service_index, scope, + package, file_desc): + """Make a protobuf ServiceDescriptor given a ServiceDescriptorProto. + + Args: + service_proto: The descriptor_pb2.ServiceDescriptorProto protobuf message. + service_index: The index of the service in the File. + scope: Dict mapping short and full symbols to message and enum types. + package: Optional package name for the new message EnumDescriptor. + file_desc: The file containing the service descriptor. + + Returns: + The added descriptor. + """ + + if package: + service_name = '.'.join((package, service_proto.name)) + else: + service_name = service_proto.name + + methods = [self._MakeMethodDescriptor(method_proto, service_name, package, + scope, index) + for index, method_proto in enumerate(service_proto.method)] + desc = descriptor.ServiceDescriptor( + name=service_proto.name, + full_name=service_name, + index=service_index, + methods=methods, + options=_OptionsOrNone(service_proto), + file=file_desc, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + self._CheckConflictRegister(desc, desc.full_name, desc.file.name) + self._service_descriptors[service_name] = desc + return desc + + def _MakeMethodDescriptor(self, method_proto, service_name, package, scope, + index): + """Creates a method descriptor from a MethodDescriptorProto. + + Args: + method_proto: The proto describing the method. + service_name: The name of the containing service. + package: Optional package name to look up for types. + scope: Scope containing available types. + index: Index of the method in the service. + + Returns: + An initialized MethodDescriptor object. + """ + full_name = '.'.join((service_name, method_proto.name)) + input_type = self._GetTypeFromScope( + package, method_proto.input_type, scope) + output_type = self._GetTypeFromScope( + package, method_proto.output_type, scope) + return descriptor.MethodDescriptor( + name=method_proto.name, + full_name=full_name, + index=index, + containing_service=None, + input_type=input_type, + output_type=output_type, + client_streaming=method_proto.client_streaming, + server_streaming=method_proto.server_streaming, + options=_OptionsOrNone(method_proto), + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + + def _ExtractSymbols(self, descriptors): + """Pulls out all the symbols from descriptor protos. + + Args: + descriptors: The messages to extract descriptors from. + Yields: + A two element tuple of the type name and descriptor object. + """ + + for desc in descriptors: + yield (_PrefixWithDot(desc.full_name), desc) + for symbol in self._ExtractSymbols(desc.nested_types): + yield symbol + for enum in desc.enum_types: + yield (_PrefixWithDot(enum.full_name), enum) + + def _GetDeps(self, dependencies, visited=None): + """Recursively finds dependencies for file protos. + + Args: + dependencies: The names of the files being depended on. + visited: The names of files already found. + + Yields: + Each direct and indirect dependency. + """ + + visited = visited or set() + for dependency in dependencies: + if dependency not in visited: + visited.add(dependency) + dep_desc = self.FindFileByName(dependency) + yield dep_desc + public_files = [d.name for d in dep_desc.public_dependencies] + yield from self._GetDeps(public_files, visited) + + def _GetTypeFromScope(self, package, type_name, scope): + """Finds a given type name in the current scope. + + Args: + package: The package the proto should be located in. + type_name: The name of the type to be found in the scope. + scope: Dict mapping short and full symbols to message and enum types. + + Returns: + The descriptor for the requested type. + """ + if type_name not in scope: + components = _PrefixWithDot(package).split('.') + while components: + possible_match = '.'.join(components + [type_name]) + if possible_match in scope: + type_name = possible_match + break + else: + components.pop(-1) + return scope[type_name] + + +def _PrefixWithDot(name): + return name if name.startswith('.') else '.%s' % name + + +if _USE_C_DESCRIPTORS: + # TODO(amauryfa): This pool could be constructed from Python code, when we + # support a flag like 'use_cpp_generated_pool=True'. + # pylint: disable=protected-access + _DEFAULT = descriptor._message.default_pool +else: + _DEFAULT = DescriptorPool() + + +def Default(): + return _DEFAULT diff --git a/scripts/protobuf3/protobuf3/duration_pb2.py b/scripts/protobuf3/protobuf3/duration_pb2.py new file mode 100644 index 0000000..a8ecc07 --- /dev/null +++ b/scripts/protobuf3/protobuf3/duration_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/duration.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1egoogle/protobuf/duration.proto\x12\x0fgoogle.protobuf\"*\n\x08\x44uration\x12\x0f\n\x07seconds\x18\x01 \x01(\x03\x12\r\n\x05nanos\x18\x02 \x01(\x05\x42\x83\x01\n\x13\x63om.google.protobufB\rDurationProtoP\x01Z1google.golang.org/protobuf/types/known/durationpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.duration_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\rDurationProtoP\001Z1google.golang.org/protobuf/types/known/durationpb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _DURATION._serialized_start=51 + _DURATION._serialized_end=93 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/empty_pb2.py b/scripts/protobuf3/protobuf3/empty_pb2.py new file mode 100644 index 0000000..0b4d554 --- /dev/null +++ b/scripts/protobuf3/protobuf3/empty_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/empty.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bgoogle/protobuf/empty.proto\x12\x0fgoogle.protobuf\"\x07\n\x05\x45mptyB}\n\x13\x63om.google.protobufB\nEmptyProtoP\x01Z.google.golang.org/protobuf/types/known/emptypb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.empty_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\nEmptyProtoP\001Z.google.golang.org/protobuf/types/known/emptypb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _EMPTY._serialized_start=48 + _EMPTY._serialized_end=55 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/field_mask_pb2.py b/scripts/protobuf3/protobuf3/field_mask_pb2.py new file mode 100644 index 0000000..80a4e96 --- /dev/null +++ b/scripts/protobuf3/protobuf3/field_mask_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/field_mask.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n google/protobuf/field_mask.proto\x12\x0fgoogle.protobuf\"\x1a\n\tFieldMask\x12\r\n\x05paths\x18\x01 \x03(\tB\x85\x01\n\x13\x63om.google.protobufB\x0e\x46ieldMaskProtoP\x01Z2google.golang.org/protobuf/types/known/fieldmaskpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.field_mask_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\016FieldMaskProtoP\001Z2google.golang.org/protobuf/types/known/fieldmaskpb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _FIELDMASK._serialized_start=53 + _FIELDMASK._serialized_end=79 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/internal/__init__.py b/scripts/protobuf3/protobuf3/internal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/protobuf3/protobuf3/internal/_parameterized.py b/scripts/protobuf3/protobuf3/internal/_parameterized.py new file mode 100644 index 0000000..afdbb78 --- /dev/null +++ b/scripts/protobuf3/protobuf3/internal/_parameterized.py @@ -0,0 +1,443 @@ +#! /usr/bin/env python +# +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Adds support for parameterized tests to Python's unittest TestCase class. + +A parameterized test is a method in a test case that is invoked with different +argument tuples. + +A simple example: + + class AdditionExample(parameterized.TestCase): + @parameterized.parameters( + (1, 2, 3), + (4, 5, 9), + (1, 1, 3)) + def testAddition(self, op1, op2, result): + self.assertEqual(result, op1 + op2) + + +Each invocation is a separate test case and properly isolated just +like a normal test method, with its own setUp/tearDown cycle. In the +example above, there are three separate testcases, one of which will +fail due to an assertion error (1 + 1 != 3). + +Parameters for individual test cases can be tuples (with positional parameters) +or dictionaries (with named parameters): + + class AdditionExample(parameterized.TestCase): + @parameterized.parameters( + {'op1': 1, 'op2': 2, 'result': 3}, + {'op1': 4, 'op2': 5, 'result': 9}, + ) + def testAddition(self, op1, op2, result): + self.assertEqual(result, op1 + op2) + +If a parameterized test fails, the error message will show the +original test name (which is modified internally) and the arguments +for the specific invocation, which are part of the string returned by +the shortDescription() method on test cases. + +The id method of the test, used internally by the unittest framework, +is also modified to show the arguments. To make sure that test names +stay the same across several invocations, object representations like + + >>> class Foo(object): + ... pass + >>> repr(Foo()) + '<__main__.Foo object at 0x23d8610>' + +are turned into '<__main__.Foo>'. For even more descriptive names, +especially in test logs, you can use the named_parameters decorator. In +this case, only tuples are supported, and the first parameters has to +be a string (or an object that returns an apt name when converted via +str()): + + class NamedExample(parameterized.TestCase): + @parameterized.named_parameters( + ('Normal', 'aa', 'aaa', True), + ('EmptyPrefix', '', 'abc', True), + ('BothEmpty', '', '', True)) + def testStartsWith(self, prefix, string, result): + self.assertEqual(result, strings.startswith(prefix)) + +Named tests also have the benefit that they can be run individually +from the command line: + + $ testmodule.py NamedExample.testStartsWithNormal + . + -------------------------------------------------------------------- + Ran 1 test in 0.000s + + OK + +Parameterized Classes +===================== +If invocation arguments are shared across test methods in a single +TestCase class, instead of decorating all test methods +individually, the class itself can be decorated: + + @parameterized.parameters( + (1, 2, 3) + (4, 5, 9)) + class ArithmeticTest(parameterized.TestCase): + def testAdd(self, arg1, arg2, result): + self.assertEqual(arg1 + arg2, result) + + def testSubtract(self, arg2, arg2, result): + self.assertEqual(result - arg1, arg2) + +Inputs from Iterables +===================== +If parameters should be shared across several test cases, or are dynamically +created from other sources, a single non-tuple iterable can be passed into +the decorator. This iterable will be used to obtain the test cases: + + class AdditionExample(parameterized.TestCase): + @parameterized.parameters( + c.op1, c.op2, c.result for c in testcases + ) + def testAddition(self, op1, op2, result): + self.assertEqual(result, op1 + op2) + + +Single-Argument Test Methods +============================ +If a test method takes only one argument, the single argument does not need to +be wrapped into a tuple: + + class NegativeNumberExample(parameterized.TestCase): + @parameterized.parameters( + -1, -3, -4, -5 + ) + def testIsNegative(self, arg): + self.assertTrue(IsNegative(arg)) +""" + +__author__ = 'tmarek@google.com (Torsten Marek)' + +import functools +import re +import types +import unittest +import uuid + +try: + # Since python 3 + import collections.abc as collections_abc +except ImportError: + # Won't work after python 3.8 + import collections as collections_abc + +ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>') +_SEPARATOR = uuid.uuid1().hex +_FIRST_ARG = object() +_ARGUMENT_REPR = object() + + +def _CleanRepr(obj): + return ADDR_RE.sub(r'<\1>', repr(obj)) + + +# Helper function formerly from the unittest module, removed from it in +# Python 2.7. +def _StrClass(cls): + return '%s.%s' % (cls.__module__, cls.__name__) + + +def _NonStringIterable(obj): + return (isinstance(obj, collections_abc.Iterable) and + not isinstance(obj, str)) + + +def _FormatParameterList(testcase_params): + if isinstance(testcase_params, collections_abc.Mapping): + return ', '.join('%s=%s' % (argname, _CleanRepr(value)) + for argname, value in testcase_params.items()) + elif _NonStringIterable(testcase_params): + return ', '.join(map(_CleanRepr, testcase_params)) + else: + return _FormatParameterList((testcase_params,)) + + +class _ParameterizedTestIter(object): + """Callable and iterable class for producing new test cases.""" + + def __init__(self, test_method, testcases, naming_type): + """Returns concrete test functions for a test and a list of parameters. + + The naming_type is used to determine the name of the concrete + functions as reported by the unittest framework. If naming_type is + _FIRST_ARG, the testcases must be tuples, and the first element must + have a string representation that is a valid Python identifier. + + Args: + test_method: The decorated test method. + testcases: (list of tuple/dict) A list of parameter + tuples/dicts for individual test invocations. + naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR. + """ + self._test_method = test_method + self.testcases = testcases + self._naming_type = naming_type + + def __call__(self, *args, **kwargs): + raise RuntimeError('You appear to be running a parameterized test case ' + 'without having inherited from parameterized.' + 'TestCase. This is bad because none of ' + 'your test cases are actually being run.') + + def __iter__(self): + test_method = self._test_method + naming_type = self._naming_type + + def MakeBoundParamTest(testcase_params): + @functools.wraps(test_method) + def BoundParamTest(self): + if isinstance(testcase_params, collections_abc.Mapping): + test_method(self, **testcase_params) + elif _NonStringIterable(testcase_params): + test_method(self, *testcase_params) + else: + test_method(self, testcase_params) + + if naming_type is _FIRST_ARG: + # Signal the metaclass that the name of the test function is unique + # and descriptive. + BoundParamTest.__x_use_name__ = True + BoundParamTest.__name__ += str(testcase_params[0]) + testcase_params = testcase_params[1:] + elif naming_type is _ARGUMENT_REPR: + # __x_extra_id__ is used to pass naming information to the __new__ + # method of TestGeneratorMetaclass. + # The metaclass will make sure to create a unique, but nondescriptive + # name for this test. + BoundParamTest.__x_extra_id__ = '(%s)' % ( + _FormatParameterList(testcase_params),) + else: + raise RuntimeError('%s is not a valid naming type.' % (naming_type,)) + + BoundParamTest.__doc__ = '%s(%s)' % ( + BoundParamTest.__name__, _FormatParameterList(testcase_params)) + if test_method.__doc__: + BoundParamTest.__doc__ += '\n%s' % (test_method.__doc__,) + return BoundParamTest + return (MakeBoundParamTest(c) for c in self.testcases) + + +def _IsSingletonList(testcases): + """True iff testcases contains only a single non-tuple element.""" + return len(testcases) == 1 and not isinstance(testcases[0], tuple) + + +def _ModifyClass(class_object, testcases, naming_type): + assert not getattr(class_object, '_id_suffix', None), ( + 'Cannot add parameters to %s,' + ' which already has parameterized methods.' % (class_object,)) + class_object._id_suffix = id_suffix = {} + # We change the size of __dict__ while we iterate over it, + # which Python 3.x will complain about, so use copy(). + for name, obj in class_object.__dict__.copy().items(): + if (name.startswith(unittest.TestLoader.testMethodPrefix) + and isinstance(obj, types.FunctionType)): + delattr(class_object, name) + methods = {} + _UpdateClassDictForParamTestCase( + methods, id_suffix, name, + _ParameterizedTestIter(obj, testcases, naming_type)) + for name, meth in methods.items(): + setattr(class_object, name, meth) + + +def _ParameterDecorator(naming_type, testcases): + """Implementation of the parameterization decorators. + + Args: + naming_type: The naming type. + testcases: Testcase parameters. + + Returns: + A function for modifying the decorated object. + """ + def _Apply(obj): + if isinstance(obj, type): + _ModifyClass( + obj, + list(testcases) if not isinstance(testcases, collections_abc.Sequence) + else testcases, + naming_type) + return obj + else: + return _ParameterizedTestIter(obj, testcases, naming_type) + + if _IsSingletonList(testcases): + assert _NonStringIterable(testcases[0]), ( + 'Single parameter argument must be a non-string iterable') + testcases = testcases[0] + + return _Apply + + +def parameters(*testcases): # pylint: disable=invalid-name + """A decorator for creating parameterized tests. + + See the module docstring for a usage example. + Args: + *testcases: Parameters for the decorated method, either a single + iterable, or a list of tuples/dicts/objects (for tests + with only one argument). + + Returns: + A test generator to be handled by TestGeneratorMetaclass. + """ + return _ParameterDecorator(_ARGUMENT_REPR, testcases) + + +def named_parameters(*testcases): # pylint: disable=invalid-name + """A decorator for creating parameterized tests. + + See the module docstring for a usage example. The first element of + each parameter tuple should be a string and will be appended to the + name of the test method. + + Args: + *testcases: Parameters for the decorated method, either a single + iterable, or a list of tuples. + + Returns: + A test generator to be handled by TestGeneratorMetaclass. + """ + return _ParameterDecorator(_FIRST_ARG, testcases) + + +class TestGeneratorMetaclass(type): + """Metaclass for test cases with test generators. + + A test generator is an iterable in a testcase that produces callables. These + callables must be single-argument methods. These methods are injected into + the class namespace and the original iterable is removed. If the name of the + iterable conforms to the test pattern, the injected methods will be picked + up as tests by the unittest framework. + + In general, it is supposed to be used in conjunction with the + parameters decorator. + """ + + def __new__(mcs, class_name, bases, dct): + dct['_id_suffix'] = id_suffix = {} + for name, obj in dct.copy().items(): + if (name.startswith(unittest.TestLoader.testMethodPrefix) and + _NonStringIterable(obj)): + iterator = iter(obj) + dct.pop(name) + _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator) + + return type.__new__(mcs, class_name, bases, dct) + + +def _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator): + """Adds individual test cases to a dictionary. + + Args: + dct: The target dictionary. + id_suffix: The dictionary for mapping names to test IDs. + name: The original name of the test case. + iterator: The iterator generating the individual test cases. + """ + for idx, func in enumerate(iterator): + assert callable(func), 'Test generators must yield callables, got %r' % ( + func,) + if getattr(func, '__x_use_name__', False): + new_name = func.__name__ + else: + new_name = '%s%s%d' % (name, _SEPARATOR, idx) + assert new_name not in dct, ( + 'Name of parameterized test case "%s" not unique' % (new_name,)) + dct[new_name] = func + id_suffix[new_name] = getattr(func, '__x_extra_id__', '') + + +class TestCase(unittest.TestCase, metaclass=TestGeneratorMetaclass): + """Base class for test cases using the parameters decorator.""" + + def _OriginalName(self): + return self._testMethodName.split(_SEPARATOR)[0] + + def __str__(self): + return '%s (%s)' % (self._OriginalName(), _StrClass(self.__class__)) + + def id(self): # pylint: disable=invalid-name + """Returns the descriptive ID of the test. + + This is used internally by the unittesting framework to get a name + for the test to be used in reports. + + Returns: + The test id. + """ + return '%s.%s%s' % (_StrClass(self.__class__), + self._OriginalName(), + self._id_suffix.get(self._testMethodName, '')) + + +def CoopTestCase(other_base_class): + """Returns a new base class with a cooperative metaclass base. + + This enables the TestCase to be used in combination + with other base classes that have custom metaclasses, such as + mox.MoxTestBase. + + Only works with metaclasses that do not override type.__new__. + + Example: + + import google3 + import mox + + from google3.testing.pybase import parameterized + + class ExampleTest(parameterized.CoopTestCase(mox.MoxTestBase)): + ... + + Args: + other_base_class: (class) A test case base class. + + Returns: + A new class object. + """ + metaclass = type( + 'CoopMetaclass', + (other_base_class.__metaclass__, + TestGeneratorMetaclass), {}) + return metaclass( + 'CoopTestCase', + (other_base_class, TestCase), {}) diff --git a/scripts/protobuf3/protobuf3/internal/api_implementation.py b/scripts/protobuf3/protobuf3/internal/api_implementation.py new file mode 100644 index 0000000..7fef237 --- /dev/null +++ b/scripts/protobuf3/protobuf3/internal/api_implementation.py @@ -0,0 +1,112 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Determine which implementation of the protobuf API is used in this process. +""" + +import os +import sys +import warnings + +try: + # pylint: disable=g-import-not-at-top + from google.protobuf.internal import _api_implementation + # The compile-time constants in the _api_implementation module can be used to + # switch to a certain implementation of the Python API at build time. + _api_version = _api_implementation.api_version +except ImportError: + _api_version = -1 # Unspecified by compiler flags. + +if _api_version == 1: + raise ValueError('api_version=1 is no longer supported.') + + +_default_implementation_type = ('cpp' if _api_version > 0 else 'python') + + +# This environment variable can be used to switch to a certain implementation +# of the Python API, overriding the compile-time constants in the +# _api_implementation module. Right now only 'python' and 'cpp' are valid +# values. Any other value will be ignored. +_implementation_type = os.getenv('PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION', + _default_implementation_type) + +if _implementation_type != 'python': + _implementation_type = 'cpp' + +if 'PyPy' in sys.version and _implementation_type == 'cpp': + warnings.warn('PyPy does not work yet with cpp protocol buffers. ' + 'Falling back to the python implementation.') + _implementation_type = 'python' + + +# Detect if serialization should be deterministic by default +try: + # The presence of this module in a build allows the proto implementation to + # be upgraded merely via build deps. + # + # NOTE: Merely importing this automatically enables deterministic proto + # serialization for C++ code, but we still need to export it as a boolean so + # that we can do the same for `_implementation_type == 'python'`. + # + # NOTE2: It is possible for C++ code to enable deterministic serialization by + # default _without_ affecting Python code, if the C++ implementation is not in + # use by this module. That is intended behavior, so we don't actually expose + # this boolean outside of this module. + # + # pylint: disable=g-import-not-at-top,unused-import + from google.protobuf import enable_deterministic_proto_serialization + _python_deterministic_proto_serialization = True +except ImportError: + _python_deterministic_proto_serialization = False + + +# Usage of this function is discouraged. Clients shouldn't care which +# implementation of the API is in use. Note that there is no guarantee +# that differences between APIs will be maintained. +# Please don't use this function if possible. +def Type(): + return _implementation_type + + +def _SetType(implementation_type): + """Never use! Only for protobuf benchmark.""" + global _implementation_type + _implementation_type = implementation_type + + +# See comment on 'Type' above. +def Version(): + return 2 + + +# For internal use only +def IsPythonDefaultSerializationDeterministic(): + return _python_deterministic_proto_serialization diff --git a/scripts/protobuf3/protobuf3/internal/builder.py b/scripts/protobuf3/protobuf3/internal/builder.py new file mode 100644 index 0000000..64353ee --- /dev/null +++ b/scripts/protobuf3/protobuf3/internal/builder.py @@ -0,0 +1,130 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Builds descriptors, message classes and services for generated _pb2.py. + +This file is only called in python generated _pb2.py files. It builds +descriptors, message classes and services that users can directly use +in generated code. +""" + +__author__ = 'jieluo@google.com (Jie Luo)' + +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +_sym_db = _symbol_database.Default() + + +def BuildMessageAndEnumDescriptors(file_des, module): + """Builds message and enum descriptors. + + Args: + file_des: FileDescriptor of the .proto file + module: Generated _pb2 module + """ + + def BuildNestedDescriptors(msg_des, prefix): + for (name, nested_msg) in msg_des.nested_types_by_name.items(): + module_name = prefix + name.upper() + module[module_name] = nested_msg + BuildNestedDescriptors(nested_msg, module_name + '_') + for enum_des in msg_des.enum_types: + module[prefix + enum_des.name.upper()] = enum_des + + for (name, msg_des) in file_des.message_types_by_name.items(): + module_name = '_' + name.upper() + module[module_name] = msg_des + BuildNestedDescriptors(msg_des, module_name + '_') + + +def BuildTopDescriptorsAndMessages(file_des, module_name, module): + """Builds top level descriptors and message classes. + + Args: + file_des: FileDescriptor of the .proto file + module_name: str, the name of generated _pb2 module + module: Generated _pb2 module + """ + + def BuildMessage(msg_des): + create_dict = {} + for (name, nested_msg) in msg_des.nested_types_by_name.items(): + create_dict[name] = BuildMessage(nested_msg) + create_dict['DESCRIPTOR'] = msg_des + create_dict['__module__'] = module_name + message_class = _reflection.GeneratedProtocolMessageType( + msg_des.name, (_message.Message,), create_dict) + _sym_db.RegisterMessage(message_class) + return message_class + + # top level enums + for (name, enum_des) in file_des.enum_types_by_name.items(): + module['_' + name.upper()] = enum_des + module[name] = enum_type_wrapper.EnumTypeWrapper(enum_des) + for enum_value in enum_des.values: + module[enum_value.name] = enum_value.number + + # top level extensions + for (name, extension_des) in file_des.extensions_by_name.items(): + module[name.upper() + '_FIELD_NUMBER'] = extension_des.number + module[name] = extension_des + + # services + for (name, service) in file_des.services_by_name.items(): + module['_' + name.upper()] = service + + # Build messages. + for (name, msg_des) in file_des.message_types_by_name.items(): + module[name] = BuildMessage(msg_des) + + +def BuildServices(file_des, module_name, module): + """Builds services classes and services stub class. + + Args: + file_des: FileDescriptor of the .proto file + module_name: str, the name of generated _pb2 module + module: Generated _pb2 module + """ + # pylint: disable=g-import-not-at-top + from google.protobuf import service as _service + from google.protobuf import service_reflection + # pylint: enable=g-import-not-at-top + for (name, service) in file_des.services_by_name.items(): + module[name] = service_reflection.GeneratedServiceType( + name, (_service.Service,), + dict(DESCRIPTOR=service, __module__=module_name)) + stub_name = name + '_Stub' + module[stub_name] = service_reflection.GeneratedServiceStubType( + stub_name, (module[name],), + dict(DESCRIPTOR=service, __module__=module_name)) diff --git a/scripts/protobuf3/protobuf3/internal/containers.py b/scripts/protobuf3/protobuf3/internal/containers.py new file mode 100644 index 0000000..29fbb53 --- /dev/null +++ b/scripts/protobuf3/protobuf3/internal/containers.py @@ -0,0 +1,710 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Contains container classes to represent different protocol buffer types. + +This file defines container classes which represent categories of protocol +buffer field types which need extra maintenance. Currently these categories +are: + +- Repeated scalar fields - These are all repeated fields which aren't + composite (e.g. they are of simple types like int32, string, etc). +- Repeated composite fields - Repeated fields which are composite. This + includes groups and nested messages. +""" + +import collections.abc +import copy +import pickle +from typing import ( + Any, + Iterable, + Iterator, + List, + MutableMapping, + MutableSequence, + NoReturn, + Optional, + Sequence, + TypeVar, + Union, + overload, +) + + +_T = TypeVar('_T') +_K = TypeVar('_K') +_V = TypeVar('_V') + + +class BaseContainer(Sequence[_T]): + """Base container class.""" + + # Minimizes memory usage and disallows assignment to other attributes. + __slots__ = ['_message_listener', '_values'] + + def __init__(self, message_listener: Any) -> None: + """ + Args: + message_listener: A MessageListener implementation. + The RepeatedScalarFieldContainer will call this object's + Modified() method when it is modified. + """ + self._message_listener = message_listener + self._values = [] + + @overload + def __getitem__(self, key: int) -> _T: + ... + + @overload + def __getitem__(self, key: slice) -> List[_T]: + ... + + def __getitem__(self, key): + """Retrieves item by the specified key.""" + return self._values[key] + + def __len__(self) -> int: + """Returns the number of elements in the container.""" + return len(self._values) + + def __ne__(self, other: Any) -> bool: + """Checks if another instance isn't equal to this one.""" + # The concrete classes should define __eq__. + return not self == other + + __hash__ = None + + def __repr__(self) -> str: + return repr(self._values) + + def sort(self, *args, **kwargs) -> None: + # Continue to support the old sort_function keyword argument. + # This is expected to be a rare occurrence, so use LBYL to avoid + # the overhead of actually catching KeyError. + if 'sort_function' in kwargs: + kwargs['cmp'] = kwargs.pop('sort_function') + self._values.sort(*args, **kwargs) + + def reverse(self) -> None: + self._values.reverse() + + +# TODO(slebedev): Remove this. BaseContainer does *not* conform to +# MutableSequence, only its subclasses do. +collections.abc.MutableSequence.register(BaseContainer) + + +class RepeatedScalarFieldContainer(BaseContainer[_T], MutableSequence[_T]): + """Simple, type-checked, list-like container for holding repeated scalars.""" + + # Disallows assignment to other attributes. + __slots__ = ['_type_checker'] + + def __init__( + self, + message_listener: Any, + type_checker: Any, + ) -> None: + """Args: + + message_listener: A MessageListener implementation. The + RepeatedScalarFieldContainer will call this object's Modified() method + when it is modified. + type_checker: A type_checkers.ValueChecker instance to run on elements + inserted into this container. + """ + super().__init__(message_listener) + self._type_checker = type_checker + + def append(self, value: _T) -> None: + """Appends an item to the list. Similar to list.append().""" + self._values.append(self._type_checker.CheckValue(value)) + if not self._message_listener.dirty: + self._message_listener.Modified() + + def insert(self, key: int, value: _T) -> None: + """Inserts the item at the specified position. Similar to list.insert().""" + self._values.insert(key, self._type_checker.CheckValue(value)) + if not self._message_listener.dirty: + self._message_listener.Modified() + + def extend(self, elem_seq: Iterable[_T]) -> None: + """Extends by appending the given iterable. Similar to list.extend().""" + if elem_seq is None: + return + try: + elem_seq_iter = iter(elem_seq) + except TypeError: + if not elem_seq: + # silently ignore falsy inputs :-/. + # TODO(ptucker): Deprecate this behavior. b/18413862 + return + raise + + new_values = [self._type_checker.CheckValue(elem) for elem in elem_seq_iter] + if new_values: + self._values.extend(new_values) + self._message_listener.Modified() + + def MergeFrom( + self, + other: Union['RepeatedScalarFieldContainer[_T]', Iterable[_T]], + ) -> None: + """Appends the contents of another repeated field of the same type to this + one. We do not check the types of the individual fields. + """ + self._values.extend(other) + self._message_listener.Modified() + + def remove(self, elem: _T): + """Removes an item from the list. Similar to list.remove().""" + self._values.remove(elem) + self._message_listener.Modified() + + def pop(self, key: Optional[int] = -1) -> _T: + """Removes and returns an item at a given index. Similar to list.pop().""" + value = self._values[key] + self.__delitem__(key) + return value + + @overload + def __setitem__(self, key: int, value: _T) -> None: + ... + + @overload + def __setitem__(self, key: slice, value: Iterable[_T]) -> None: + ... + + def __setitem__(self, key, value) -> None: + """Sets the item on the specified position.""" + if isinstance(key, slice): + if key.step is not None: + raise ValueError('Extended slices not supported') + self._values[key] = map(self._type_checker.CheckValue, value) + self._message_listener.Modified() + else: + self._values[key] = self._type_checker.CheckValue(value) + self._message_listener.Modified() + + def __delitem__(self, key: Union[int, slice]) -> None: + """Deletes the item at the specified position.""" + del self._values[key] + self._message_listener.Modified() + + def __eq__(self, other: Any) -> bool: + """Compares the current instance with another one.""" + if self is other: + return True + # Special case for the same type which should be common and fast. + if isinstance(other, self.__class__): + return other._values == self._values + # We are presumably comparing against some other sequence type. + return other == self._values + + def __deepcopy__( + self, + unused_memo: Any = None, + ) -> 'RepeatedScalarFieldContainer[_T]': + clone = RepeatedScalarFieldContainer( + copy.deepcopy(self._message_listener), self._type_checker) + clone.MergeFrom(self) + return clone + + def __reduce__(self, **kwargs) -> NoReturn: + raise pickle.PickleError( + "Can't pickle repeated scalar fields, convert to list first") + + +# TODO(slebedev): Constrain T to be a subtype of Message. +class RepeatedCompositeFieldContainer(BaseContainer[_T], MutableSequence[_T]): + """Simple, list-like container for holding repeated composite fields.""" + + # Disallows assignment to other attributes. + __slots__ = ['_message_descriptor'] + + def __init__(self, message_listener: Any, message_descriptor: Any) -> None: + """ + Note that we pass in a descriptor instead of the generated directly, + since at the time we construct a _RepeatedCompositeFieldContainer we + haven't yet necessarily initialized the type that will be contained in the + container. + + Args: + message_listener: A MessageListener implementation. + The RepeatedCompositeFieldContainer will call this object's + Modified() method when it is modified. + message_descriptor: A Descriptor instance describing the protocol type + that should be present in this container. We'll use the + _concrete_class field of this descriptor when the client calls add(). + """ + super().__init__(message_listener) + self._message_descriptor = message_descriptor + + def add(self, **kwargs: Any) -> _T: + """Adds a new element at the end of the list and returns it. Keyword + arguments may be used to initialize the element. + """ + new_element = self._message_descriptor._concrete_class(**kwargs) + new_element._SetListener(self._message_listener) + self._values.append(new_element) + if not self._message_listener.dirty: + self._message_listener.Modified() + return new_element + + def append(self, value: _T) -> None: + """Appends one element by copying the message.""" + new_element = self._message_descriptor._concrete_class() + new_element._SetListener(self._message_listener) + new_element.CopyFrom(value) + self._values.append(new_element) + if not self._message_listener.dirty: + self._message_listener.Modified() + + def insert(self, key: int, value: _T) -> None: + """Inserts the item at the specified position by copying.""" + new_element = self._message_descriptor._concrete_class() + new_element._SetListener(self._message_listener) + new_element.CopyFrom(value) + self._values.insert(key, new_element) + if not self._message_listener.dirty: + self._message_listener.Modified() + + def extend(self, elem_seq: Iterable[_T]) -> None: + """Extends by appending the given sequence of elements of the same type + + as this one, copying each individual message. + """ + message_class = self._message_descriptor._concrete_class + listener = self._message_listener + values = self._values + for message in elem_seq: + new_element = message_class() + new_element._SetListener(listener) + new_element.MergeFrom(message) + values.append(new_element) + listener.Modified() + + def MergeFrom( + self, + other: Union['RepeatedCompositeFieldContainer[_T]', Iterable[_T]], + ) -> None: + """Appends the contents of another repeated field of the same type to this + one, copying each individual message. + """ + self.extend(other) + + def remove(self, elem: _T) -> None: + """Removes an item from the list. Similar to list.remove().""" + self._values.remove(elem) + self._message_listener.Modified() + + def pop(self, key: Optional[int] = -1) -> _T: + """Removes and returns an item at a given index. Similar to list.pop().""" + value = self._values[key] + self.__delitem__(key) + return value + + @overload + def __setitem__(self, key: int, value: _T) -> None: + ... + + @overload + def __setitem__(self, key: slice, value: Iterable[_T]) -> None: + ... + + def __setitem__(self, key, value): + # This method is implemented to make RepeatedCompositeFieldContainer + # structurally compatible with typing.MutableSequence. It is + # otherwise unsupported and will always raise an error. + raise TypeError( + f'{self.__class__.__name__} object does not support item assignment') + + def __delitem__(self, key: Union[int, slice]) -> None: + """Deletes the item at the specified position.""" + del self._values[key] + self._message_listener.Modified() + + def __eq__(self, other: Any) -> bool: + """Compares the current instance with another one.""" + if self is other: + return True + if not isinstance(other, self.__class__): + raise TypeError('Can only compare repeated composite fields against ' + 'other repeated composite fields.') + return self._values == other._values + + +class ScalarMap(MutableMapping[_K, _V]): + """Simple, type-checked, dict-like container for holding repeated scalars.""" + + # Disallows assignment to other attributes. + __slots__ = ['_key_checker', '_value_checker', '_values', '_message_listener', + '_entry_descriptor'] + + def __init__( + self, + message_listener: Any, + key_checker: Any, + value_checker: Any, + entry_descriptor: Any, + ) -> None: + """ + Args: + message_listener: A MessageListener implementation. + The ScalarMap will call this object's Modified() method when it + is modified. + key_checker: A type_checkers.ValueChecker instance to run on keys + inserted into this container. + value_checker: A type_checkers.ValueChecker instance to run on values + inserted into this container. + entry_descriptor: The MessageDescriptor of a map entry: key and value. + """ + self._message_listener = message_listener + self._key_checker = key_checker + self._value_checker = value_checker + self._entry_descriptor = entry_descriptor + self._values = {} + + def __getitem__(self, key: _K) -> _V: + try: + return self._values[key] + except KeyError: + key = self._key_checker.CheckValue(key) + val = self._value_checker.DefaultValue() + self._values[key] = val + return val + + def __contains__(self, item: _K) -> bool: + # We check the key's type to match the strong-typing flavor of the API. + # Also this makes it easier to match the behavior of the C++ implementation. + self._key_checker.CheckValue(item) + return item in self._values + + @overload + def get(self, key: _K) -> Optional[_V]: + ... + + @overload + def get(self, key: _K, default: _T) -> Union[_V, _T]: + ... + + # We need to override this explicitly, because our defaultdict-like behavior + # will make the default implementation (from our base class) always insert + # the key. + def get(self, key, default=None): + if key in self: + return self[key] + else: + return default + + def __setitem__(self, key: _K, value: _V) -> _T: + checked_key = self._key_checker.CheckValue(key) + checked_value = self._value_checker.CheckValue(value) + self._values[checked_key] = checked_value + self._message_listener.Modified() + + def __delitem__(self, key: _K) -> None: + del self._values[key] + self._message_listener.Modified() + + def __len__(self) -> int: + return len(self._values) + + def __iter__(self) -> Iterator[_K]: + return iter(self._values) + + def __repr__(self) -> str: + return repr(self._values) + + def MergeFrom(self, other: 'ScalarMap[_K, _V]') -> None: + self._values.update(other._values) + self._message_listener.Modified() + + def InvalidateIterators(self) -> None: + # It appears that the only way to reliably invalidate iterators to + # self._values is to ensure that its size changes. + original = self._values + self._values = original.copy() + original[None] = None + + # This is defined in the abstract base, but we can do it much more cheaply. + def clear(self) -> None: + self._values.clear() + self._message_listener.Modified() + + def GetEntryClass(self) -> Any: + return self._entry_descriptor._concrete_class + + +class MessageMap(MutableMapping[_K, _V]): + """Simple, type-checked, dict-like container for with submessage values.""" + + # Disallows assignment to other attributes. + __slots__ = ['_key_checker', '_values', '_message_listener', + '_message_descriptor', '_entry_descriptor'] + + def __init__( + self, + message_listener: Any, + message_descriptor: Any, + key_checker: Any, + entry_descriptor: Any, + ) -> None: + """ + Args: + message_listener: A MessageListener implementation. + The ScalarMap will call this object's Modified() method when it + is modified. + key_checker: A type_checkers.ValueChecker instance to run on keys + inserted into this container. + value_checker: A type_checkers.ValueChecker instance to run on values + inserted into this container. + entry_descriptor: The MessageDescriptor of a map entry: key and value. + """ + self._message_listener = message_listener + self._message_descriptor = message_descriptor + self._key_checker = key_checker + self._entry_descriptor = entry_descriptor + self._values = {} + + def __getitem__(self, key: _K) -> _V: + key = self._key_checker.CheckValue(key) + try: + return self._values[key] + except KeyError: + new_element = self._message_descriptor._concrete_class() + new_element._SetListener(self._message_listener) + self._values[key] = new_element + self._message_listener.Modified() + return new_element + + def get_or_create(self, key: _K) -> _V: + """get_or_create() is an alias for getitem (ie. map[key]). + + Args: + key: The key to get or create in the map. + + This is useful in cases where you want to be explicit that the call is + mutating the map. This can avoid lint errors for statements like this + that otherwise would appear to be pointless statements: + + msg.my_map[key] + """ + return self[key] + + @overload + def get(self, key: _K) -> Optional[_V]: + ... + + @overload + def get(self, key: _K, default: _T) -> Union[_V, _T]: + ... + + # We need to override this explicitly, because our defaultdict-like behavior + # will make the default implementation (from our base class) always insert + # the key. + def get(self, key, default=None): + if key in self: + return self[key] + else: + return default + + def __contains__(self, item: _K) -> bool: + item = self._key_checker.CheckValue(item) + return item in self._values + + def __setitem__(self, key: _K, value: _V) -> NoReturn: + raise ValueError('May not set values directly, call my_map[key].foo = 5') + + def __delitem__(self, key: _K) -> None: + key = self._key_checker.CheckValue(key) + del self._values[key] + self._message_listener.Modified() + + def __len__(self) -> int: + return len(self._values) + + def __iter__(self) -> Iterator[_K]: + return iter(self._values) + + def __repr__(self) -> str: + return repr(self._values) + + def MergeFrom(self, other: 'MessageMap[_K, _V]') -> None: + # pylint: disable=protected-access + for key in other._values: + # According to documentation: "When parsing from the wire or when merging, + # if there are duplicate map keys the last key seen is used". + if key in self: + del self[key] + self[key].CopyFrom(other[key]) + # self._message_listener.Modified() not required here, because + # mutations to submessages already propagate. + + def InvalidateIterators(self) -> None: + # It appears that the only way to reliably invalidate iterators to + # self._values is to ensure that its size changes. + original = self._values + self._values = original.copy() + original[None] = None + + # This is defined in the abstract base, but we can do it much more cheaply. + def clear(self) -> None: + self._values.clear() + self._message_listener.Modified() + + def GetEntryClass(self) -> Any: + return self._entry_descriptor._concrete_class + + +class _UnknownField: + """A parsed unknown field.""" + + # Disallows assignment to other attributes. + __slots__ = ['_field_number', '_wire_type', '_data'] + + def __init__(self, field_number, wire_type, data): + self._field_number = field_number + self._wire_type = wire_type + self._data = data + return + + def __lt__(self, other): + # pylint: disable=protected-access + return self._field_number < other._field_number + + def __eq__(self, other): + if self is other: + return True + # pylint: disable=protected-access + return (self._field_number == other._field_number and + self._wire_type == other._wire_type and + self._data == other._data) + + +class UnknownFieldRef: # pylint: disable=missing-class-docstring + + def __init__(self, parent, index): + self._parent = parent + self._index = index + + def _check_valid(self): + if not self._parent: + raise ValueError('UnknownField does not exist. ' + 'The parent message might be cleared.') + if self._index >= len(self._parent): + raise ValueError('UnknownField does not exist. ' + 'The parent message might be cleared.') + + @property + def field_number(self): + self._check_valid() + # pylint: disable=protected-access + return self._parent._internal_get(self._index)._field_number + + @property + def wire_type(self): + self._check_valid() + # pylint: disable=protected-access + return self._parent._internal_get(self._index)._wire_type + + @property + def data(self): + self._check_valid() + # pylint: disable=protected-access + return self._parent._internal_get(self._index)._data + + +class UnknownFieldSet: + """UnknownField container""" + + # Disallows assignment to other attributes. + __slots__ = ['_values'] + + def __init__(self): + self._values = [] + + def __getitem__(self, index): + if self._values is None: + raise ValueError('UnknownFields does not exist. ' + 'The parent message might be cleared.') + size = len(self._values) + if index < 0: + index += size + if index < 0 or index >= size: + raise IndexError('index %d out of range'.index) + + return UnknownFieldRef(self, index) + + def _internal_get(self, index): + return self._values[index] + + def __len__(self): + if self._values is None: + raise ValueError('UnknownFields does not exist. ' + 'The parent message might be cleared.') + return len(self._values) + + def _add(self, field_number, wire_type, data): + unknown_field = _UnknownField(field_number, wire_type, data) + self._values.append(unknown_field) + return unknown_field + + def __iter__(self): + for i in range(len(self)): + yield UnknownFieldRef(self, i) + + def _extend(self, other): + if other is None: + return + # pylint: disable=protected-access + self._values.extend(other._values) + + def __eq__(self, other): + if self is other: + return True + # Sort unknown fields because their order shouldn't + # affect equality test. + values = list(self._values) + if other is None: + return not values + values.sort() + # pylint: disable=protected-access + other_values = sorted(other._values) + return values == other_values + + def _clear(self): + for value in self._values: + # pylint: disable=protected-access + if isinstance(value._data, UnknownFieldSet): + value._data._clear() # pylint: disable=protected-access + self._values = None diff --git a/scripts/protobuf3/protobuf3/internal/decoder.py b/scripts/protobuf3/protobuf3/internal/decoder.py new file mode 100644 index 0000000..bc1b7b7 --- /dev/null +++ b/scripts/protobuf3/protobuf3/internal/decoder.py @@ -0,0 +1,1029 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Code for decoding protocol buffer primitives. + +This code is very similar to encoder.py -- read the docs for that module first. + +A "decoder" is a function with the signature: + Decode(buffer, pos, end, message, field_dict) +The arguments are: + buffer: The string containing the encoded message. + pos: The current position in the string. + end: The position in the string where the current message ends. May be + less than len(buffer) if we're reading a sub-message. + message: The message object into which we're parsing. + field_dict: message._fields (avoids a hashtable lookup). +The decoder reads the field and stores it into field_dict, returning the new +buffer position. A decoder for a repeated field may proactively decode all of +the elements of that field, if they appear consecutively. + +Note that decoders may throw any of the following: + IndexError: Indicates a truncated message. + struct.error: Unpacking of a fixed-width field failed. + message.DecodeError: Other errors. + +Decoders are expected to raise an exception if they are called with pos > end. +This allows callers to be lax about bounds checking: it's fineto read past +"end" as long as you are sure that someone else will notice and throw an +exception later on. + +Something up the call stack is expected to catch IndexError and struct.error +and convert them to message.DecodeError. + +Decoders are constructed using decoder constructors with the signature: + MakeDecoder(field_number, is_repeated, is_packed, key, new_default) +The arguments are: + field_number: The field number of the field we want to decode. + is_repeated: Is the field a repeated field? (bool) + is_packed: Is the field a packed field? (bool) + key: The key to use when looking up the field within field_dict. + (This is actually the FieldDescriptor but nothing in this + file should depend on that.) + new_default: A function which takes a message object as a parameter and + returns a new instance of the default value for this field. + (This is called for repeated fields and sub-messages, when an + instance does not already exist.) + +As with encoders, we define a decoder constructor for every type of field. +Then, for every field of every message class we construct an actual decoder. +That decoder goes into a dict indexed by tag, so when we decode a message +we repeatedly read a tag, look up the corresponding decoder, and invoke it. +""" + +__author__ = 'kenton@google.com (Kenton Varda)' + +import math +import struct + +from google.protobuf.internal import containers +from google.protobuf.internal import encoder +from google.protobuf.internal import wire_format +from google.protobuf import message + + +# This is not for optimization, but rather to avoid conflicts with local +# variables named "message". +_DecodeError = message.DecodeError + + +def _VarintDecoder(mask, result_type): + """Return an encoder for a basic varint value (does not include tag). + + Decoded values will be bitwise-anded with the given mask before being + returned, e.g. to limit them to 32 bits. The returned decoder does not + take the usual "end" parameter -- the caller is expected to do bounds checking + after the fact (often the caller can defer such checking until later). The + decoder returns a (value, new_pos) pair. + """ + + def DecodeVarint(buffer, pos): + result = 0 + shift = 0 + while 1: + b = buffer[pos] + result |= ((b & 0x7f) << shift) + pos += 1 + if not (b & 0x80): + result &= mask + result = result_type(result) + return (result, pos) + shift += 7 + if shift >= 64: + raise _DecodeError('Too many bytes when decoding varint.') + return DecodeVarint + + +def _SignedVarintDecoder(bits, result_type): + """Like _VarintDecoder() but decodes signed values.""" + + signbit = 1 << (bits - 1) + mask = (1 << bits) - 1 + + def DecodeVarint(buffer, pos): + result = 0 + shift = 0 + while 1: + b = buffer[pos] + result |= ((b & 0x7f) << shift) + pos += 1 + if not (b & 0x80): + result &= mask + result = (result ^ signbit) - signbit + result = result_type(result) + return (result, pos) + shift += 7 + if shift >= 64: + raise _DecodeError('Too many bytes when decoding varint.') + return DecodeVarint + +# All 32-bit and 64-bit values are represented as int. +_DecodeVarint = _VarintDecoder((1 << 64) - 1, int) +_DecodeSignedVarint = _SignedVarintDecoder(64, int) + +# Use these versions for values which must be limited to 32 bits. +_DecodeVarint32 = _VarintDecoder((1 << 32) - 1, int) +_DecodeSignedVarint32 = _SignedVarintDecoder(32, int) + + +def ReadTag(buffer, pos): + """Read a tag from the memoryview, and return a (tag_bytes, new_pos) tuple. + + We return the raw bytes of the tag rather than decoding them. The raw + bytes can then be used to look up the proper decoder. This effectively allows + us to trade some work that would be done in pure-python (decoding a varint) + for work that is done in C (searching for a byte string in a hash table). + In a low-level language it would be much cheaper to decode the varint and + use that, but not in Python. + + Args: + buffer: memoryview object of the encoded bytes + pos: int of the current position to start from + + Returns: + Tuple[bytes, int] of the tag data and new position. + """ + start = pos + while buffer[pos] & 0x80: + pos += 1 + pos += 1 + + tag_bytes = buffer[start:pos].tobytes() + return tag_bytes, pos + + +# -------------------------------------------------------------------- + + +def _SimpleDecoder(wire_type, decode_value): + """Return a constructor for a decoder for fields of a particular type. + + Args: + wire_type: The field's wire type. + decode_value: A function which decodes an individual value, e.g. + _DecodeVarint() + """ + + def SpecificDecoder(field_number, is_repeated, is_packed, key, new_default, + clear_if_default=False): + if is_packed: + local_DecodeVarint = _DecodeVarint + def DecodePackedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + (endpoint, pos) = local_DecodeVarint(buffer, pos) + endpoint += pos + if endpoint > end: + raise _DecodeError('Truncated message.') + while pos < endpoint: + (element, pos) = decode_value(buffer, pos) + value.append(element) + if pos > endpoint: + del value[-1] # Discard corrupt value. + raise _DecodeError('Packed element was truncated.') + return pos + return DecodePackedField + elif is_repeated: + tag_bytes = encoder.TagBytes(field_number, wire_type) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + (element, new_pos) = decode_value(buffer, pos) + value.append(element) + # Predict that the next tag is another copy of the same repeated + # field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos >= end: + # Prediction failed. Return. + if new_pos > end: + raise _DecodeError('Truncated message.') + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + (new_value, pos) = decode_value(buffer, pos) + if pos > end: + raise _DecodeError('Truncated message.') + if clear_if_default and not new_value: + field_dict.pop(key, None) + else: + field_dict[key] = new_value + return pos + return DecodeField + + return SpecificDecoder + + +def _ModifiedDecoder(wire_type, decode_value, modify_value): + """Like SimpleDecoder but additionally invokes modify_value on every value + before storing it. Usually modify_value is ZigZagDecode. + """ + + # Reusing _SimpleDecoder is slightly slower than copying a bunch of code, but + # not enough to make a significant difference. + + def InnerDecode(buffer, pos): + (result, new_pos) = decode_value(buffer, pos) + return (modify_value(result), new_pos) + return _SimpleDecoder(wire_type, InnerDecode) + + +def _StructPackDecoder(wire_type, format): + """Return a constructor for a decoder for a fixed-width field. + + Args: + wire_type: The field's wire type. + format: The format string to pass to struct.unpack(). + """ + + value_size = struct.calcsize(format) + local_unpack = struct.unpack + + # Reusing _SimpleDecoder is slightly slower than copying a bunch of code, but + # not enough to make a significant difference. + + # Note that we expect someone up-stack to catch struct.error and convert + # it to _DecodeError -- this way we don't have to set up exception- + # handling blocks every time we parse one value. + + def InnerDecode(buffer, pos): + new_pos = pos + value_size + result = local_unpack(format, buffer[pos:new_pos])[0] + return (result, new_pos) + return _SimpleDecoder(wire_type, InnerDecode) + + +def _FloatDecoder(): + """Returns a decoder for a float field. + + This code works around a bug in struct.unpack for non-finite 32-bit + floating-point values. + """ + + local_unpack = struct.unpack + + def InnerDecode(buffer, pos): + """Decode serialized float to a float and new position. + + Args: + buffer: memoryview of the serialized bytes + pos: int, position in the memory view to start at. + + Returns: + Tuple[float, int] of the deserialized float value and new position + in the serialized data. + """ + # We expect a 32-bit value in little-endian byte order. Bit 1 is the sign + # bit, bits 2-9 represent the exponent, and bits 10-32 are the significand. + new_pos = pos + 4 + float_bytes = buffer[pos:new_pos].tobytes() + + # If this value has all its exponent bits set, then it's non-finite. + # In Python 2.4, struct.unpack will convert it to a finite 64-bit value. + # To avoid that, we parse it specially. + if (float_bytes[3:4] in b'\x7F\xFF' and float_bytes[2:3] >= b'\x80'): + # If at least one significand bit is set... + if float_bytes[0:3] != b'\x00\x00\x80': + return (math.nan, new_pos) + # If sign bit is set... + if float_bytes[3:4] == b'\xFF': + return (-math.inf, new_pos) + return (math.inf, new_pos) + + # Note that we expect someone up-stack to catch struct.error and convert + # it to _DecodeError -- this way we don't have to set up exception- + # handling blocks every time we parse one value. + result = local_unpack('<f', float_bytes)[0] + return (result, new_pos) + return _SimpleDecoder(wire_format.WIRETYPE_FIXED32, InnerDecode) + + +def _DoubleDecoder(): + """Returns a decoder for a double field. + + This code works around a bug in struct.unpack for not-a-number. + """ + + local_unpack = struct.unpack + + def InnerDecode(buffer, pos): + """Decode serialized double to a double and new position. + + Args: + buffer: memoryview of the serialized bytes. + pos: int, position in the memory view to start at. + + Returns: + Tuple[float, int] of the decoded double value and new position + in the serialized data. + """ + # We expect a 64-bit value in little-endian byte order. Bit 1 is the sign + # bit, bits 2-12 represent the exponent, and bits 13-64 are the significand. + new_pos = pos + 8 + double_bytes = buffer[pos:new_pos].tobytes() + + # If this value has all its exponent bits set and at least one significand + # bit set, it's not a number. In Python 2.4, struct.unpack will treat it + # as inf or -inf. To avoid that, we treat it specially. + if ((double_bytes[7:8] in b'\x7F\xFF') + and (double_bytes[6:7] >= b'\xF0') + and (double_bytes[0:7] != b'\x00\x00\x00\x00\x00\x00\xF0')): + return (math.nan, new_pos) + + # Note that we expect someone up-stack to catch struct.error and convert + # it to _DecodeError -- this way we don't have to set up exception- + # handling blocks every time we parse one value. + result = local_unpack('<d', double_bytes)[0] + return (result, new_pos) + return _SimpleDecoder(wire_format.WIRETYPE_FIXED64, InnerDecode) + + +def EnumDecoder(field_number, is_repeated, is_packed, key, new_default, + clear_if_default=False): + """Returns a decoder for enum field.""" + enum_type = key.enum_type + if is_packed: + local_DecodeVarint = _DecodeVarint + def DecodePackedField(buffer, pos, end, message, field_dict): + """Decode serialized packed enum to its value and a new position. + + Args: + buffer: memoryview of the serialized bytes. + pos: int, position in the memory view to start at. + end: int, end position of serialized data + message: Message object to store unknown fields in + field_dict: Map[Descriptor, Any] to store decoded values in. + + Returns: + int, new position in serialized data. + """ + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + (endpoint, pos) = local_DecodeVarint(buffer, pos) + endpoint += pos + if endpoint > end: + raise _DecodeError('Truncated message.') + while pos < endpoint: + value_start_pos = pos + (element, pos) = _DecodeSignedVarint32(buffer, pos) + # pylint: disable=protected-access + if element in enum_type.values_by_number: + value.append(element) + else: + if not message._unknown_fields: + message._unknown_fields = [] + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_VARINT) + + message._unknown_fields.append( + (tag_bytes, buffer[value_start_pos:pos].tobytes())) + if message._unknown_field_set is None: + message._unknown_field_set = containers.UnknownFieldSet() + message._unknown_field_set._add( + field_number, wire_format.WIRETYPE_VARINT, element) + # pylint: enable=protected-access + if pos > endpoint: + if element in enum_type.values_by_number: + del value[-1] # Discard corrupt value. + else: + del message._unknown_fields[-1] + # pylint: disable=protected-access + del message._unknown_field_set._values[-1] + # pylint: enable=protected-access + raise _DecodeError('Packed element was truncated.') + return pos + return DecodePackedField + elif is_repeated: + tag_bytes = encoder.TagBytes(field_number, wire_format.WIRETYPE_VARINT) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + """Decode serialized repeated enum to its value and a new position. + + Args: + buffer: memoryview of the serialized bytes. + pos: int, position in the memory view to start at. + end: int, end position of serialized data + message: Message object to store unknown fields in + field_dict: Map[Descriptor, Any] to store decoded values in. + + Returns: + int, new position in serialized data. + """ + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + (element, new_pos) = _DecodeSignedVarint32(buffer, pos) + # pylint: disable=protected-access + if element in enum_type.values_by_number: + value.append(element) + else: + if not message._unknown_fields: + message._unknown_fields = [] + message._unknown_fields.append( + (tag_bytes, buffer[pos:new_pos].tobytes())) + if message._unknown_field_set is None: + message._unknown_field_set = containers.UnknownFieldSet() + message._unknown_field_set._add( + field_number, wire_format.WIRETYPE_VARINT, element) + # pylint: enable=protected-access + # Predict that the next tag is another copy of the same repeated + # field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos >= end: + # Prediction failed. Return. + if new_pos > end: + raise _DecodeError('Truncated message.') + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + """Decode serialized repeated enum to its value and a new position. + + Args: + buffer: memoryview of the serialized bytes. + pos: int, position in the memory view to start at. + end: int, end position of serialized data + message: Message object to store unknown fields in + field_dict: Map[Descriptor, Any] to store decoded values in. + + Returns: + int, new position in serialized data. + """ + value_start_pos = pos + (enum_value, pos) = _DecodeSignedVarint32(buffer, pos) + if pos > end: + raise _DecodeError('Truncated message.') + if clear_if_default and not enum_value: + field_dict.pop(key, None) + return pos + # pylint: disable=protected-access + if enum_value in enum_type.values_by_number: + field_dict[key] = enum_value + else: + if not message._unknown_fields: + message._unknown_fields = [] + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_VARINT) + message._unknown_fields.append( + (tag_bytes, buffer[value_start_pos:pos].tobytes())) + if message._unknown_field_set is None: + message._unknown_field_set = containers.UnknownFieldSet() + message._unknown_field_set._add( + field_number, wire_format.WIRETYPE_VARINT, enum_value) + # pylint: enable=protected-access + return pos + return DecodeField + + +# -------------------------------------------------------------------- + + +Int32Decoder = _SimpleDecoder( + wire_format.WIRETYPE_VARINT, _DecodeSignedVarint32) + +Int64Decoder = _SimpleDecoder( + wire_format.WIRETYPE_VARINT, _DecodeSignedVarint) + +UInt32Decoder = _SimpleDecoder(wire_format.WIRETYPE_VARINT, _DecodeVarint32) +UInt64Decoder = _SimpleDecoder(wire_format.WIRETYPE_VARINT, _DecodeVarint) + +SInt32Decoder = _ModifiedDecoder( + wire_format.WIRETYPE_VARINT, _DecodeVarint32, wire_format.ZigZagDecode) +SInt64Decoder = _ModifiedDecoder( + wire_format.WIRETYPE_VARINT, _DecodeVarint, wire_format.ZigZagDecode) + +# Note that Python conveniently guarantees that when using the '<' prefix on +# formats, they will also have the same size across all platforms (as opposed +# to without the prefix, where their sizes depend on the C compiler's basic +# type sizes). +Fixed32Decoder = _StructPackDecoder(wire_format.WIRETYPE_FIXED32, '<I') +Fixed64Decoder = _StructPackDecoder(wire_format.WIRETYPE_FIXED64, '<Q') +SFixed32Decoder = _StructPackDecoder(wire_format.WIRETYPE_FIXED32, '<i') +SFixed64Decoder = _StructPackDecoder(wire_format.WIRETYPE_FIXED64, '<q') +FloatDecoder = _FloatDecoder() +DoubleDecoder = _DoubleDecoder() + +BoolDecoder = _ModifiedDecoder( + wire_format.WIRETYPE_VARINT, _DecodeVarint, bool) + + +def StringDecoder(field_number, is_repeated, is_packed, key, new_default, + clear_if_default=False): + """Returns a decoder for a string field.""" + + local_DecodeVarint = _DecodeVarint + + def _ConvertToUnicode(memview): + """Convert byte to unicode.""" + byte_str = memview.tobytes() + try: + value = str(byte_str, 'utf-8') + except UnicodeDecodeError as e: + # add more information to the error message and re-raise it. + e.reason = '%s in field: %s' % (e, key.full_name) + raise + + return value + + assert not is_packed + if is_repeated: + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_LENGTH_DELIMITED) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated string.') + value.append(_ConvertToUnicode(buffer[pos:new_pos])) + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated string.') + if clear_if_default and not size: + field_dict.pop(key, None) + else: + field_dict[key] = _ConvertToUnicode(buffer[pos:new_pos]) + return new_pos + return DecodeField + + +def BytesDecoder(field_number, is_repeated, is_packed, key, new_default, + clear_if_default=False): + """Returns a decoder for a bytes field.""" + + local_DecodeVarint = _DecodeVarint + + assert not is_packed + if is_repeated: + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_LENGTH_DELIMITED) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated string.') + value.append(buffer[pos:new_pos].tobytes()) + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated string.') + if clear_if_default and not size: + field_dict.pop(key, None) + else: + field_dict[key] = buffer[pos:new_pos].tobytes() + return new_pos + return DecodeField + + +def GroupDecoder(field_number, is_repeated, is_packed, key, new_default): + """Returns a decoder for a group field.""" + + end_tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_END_GROUP) + end_tag_len = len(end_tag_bytes) + + assert not is_packed + if is_repeated: + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_START_GROUP) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + # Read sub-message. + pos = value.add()._InternalParse(buffer, pos, end) + # Read end tag. + new_pos = pos+end_tag_len + if buffer[pos:new_pos] != end_tag_bytes or new_pos > end: + raise _DecodeError('Missing group end tag.') + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + # Read sub-message. + pos = value._InternalParse(buffer, pos, end) + # Read end tag. + new_pos = pos+end_tag_len + if buffer[pos:new_pos] != end_tag_bytes or new_pos > end: + raise _DecodeError('Missing group end tag.') + return new_pos + return DecodeField + + +def MessageDecoder(field_number, is_repeated, is_packed, key, new_default): + """Returns a decoder for a message field.""" + + local_DecodeVarint = _DecodeVarint + + assert not is_packed + if is_repeated: + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_LENGTH_DELIMITED) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + # Read length. + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated message.') + # Read sub-message. + if value.add()._InternalParse(buffer, pos, new_pos) != new_pos: + # The only reason _InternalParse would return early is if it + # encountered an end-group tag. + raise _DecodeError('Unexpected end-group tag.') + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + # Read length. + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated message.') + # Read sub-message. + if value._InternalParse(buffer, pos, new_pos) != new_pos: + # The only reason _InternalParse would return early is if it encountered + # an end-group tag. + raise _DecodeError('Unexpected end-group tag.') + return new_pos + return DecodeField + + +# -------------------------------------------------------------------- + +MESSAGE_SET_ITEM_TAG = encoder.TagBytes(1, wire_format.WIRETYPE_START_GROUP) + +def MessageSetItemDecoder(descriptor): + """Returns a decoder for a MessageSet item. + + The parameter is the message Descriptor. + + The message set message looks like this: + message MessageSet { + repeated group Item = 1 { + required int32 type_id = 2; + required string message = 3; + } + } + """ + + type_id_tag_bytes = encoder.TagBytes(2, wire_format.WIRETYPE_VARINT) + message_tag_bytes = encoder.TagBytes(3, wire_format.WIRETYPE_LENGTH_DELIMITED) + item_end_tag_bytes = encoder.TagBytes(1, wire_format.WIRETYPE_END_GROUP) + + local_ReadTag = ReadTag + local_DecodeVarint = _DecodeVarint + local_SkipField = SkipField + + def DecodeItem(buffer, pos, end, message, field_dict): + """Decode serialized message set to its value and new position. + + Args: + buffer: memoryview of the serialized bytes. + pos: int, position in the memory view to start at. + end: int, end position of serialized data + message: Message object to store unknown fields in + field_dict: Map[Descriptor, Any] to store decoded values in. + + Returns: + int, new position in serialized data. + """ + message_set_item_start = pos + type_id = -1 + message_start = -1 + message_end = -1 + + # Technically, type_id and message can appear in any order, so we need + # a little loop here. + while 1: + (tag_bytes, pos) = local_ReadTag(buffer, pos) + if tag_bytes == type_id_tag_bytes: + (type_id, pos) = local_DecodeVarint(buffer, pos) + elif tag_bytes == message_tag_bytes: + (size, message_start) = local_DecodeVarint(buffer, pos) + pos = message_end = message_start + size + elif tag_bytes == item_end_tag_bytes: + break + else: + pos = SkipField(buffer, pos, end, tag_bytes) + if pos == -1: + raise _DecodeError('Missing group end tag.') + + if pos > end: + raise _DecodeError('Truncated message.') + + if type_id == -1: + raise _DecodeError('MessageSet item missing type_id.') + if message_start == -1: + raise _DecodeError('MessageSet item missing message.') + + extension = message.Extensions._FindExtensionByNumber(type_id) + # pylint: disable=protected-access + if extension is not None: + value = field_dict.get(extension) + if value is None: + message_type = extension.message_type + if not hasattr(message_type, '_concrete_class'): + # pylint: disable=protected-access + message._FACTORY.GetPrototype(message_type) + value = field_dict.setdefault( + extension, message_type._concrete_class()) + if value._InternalParse(buffer, message_start,message_end) != message_end: + # The only reason _InternalParse would return early is if it encountered + # an end-group tag. + raise _DecodeError('Unexpected end-group tag.') + else: + if not message._unknown_fields: + message._unknown_fields = [] + message._unknown_fields.append( + (MESSAGE_SET_ITEM_TAG, buffer[message_set_item_start:pos].tobytes())) + if message._unknown_field_set is None: + message._unknown_field_set = containers.UnknownFieldSet() + message._unknown_field_set._add( + type_id, + wire_format.WIRETYPE_LENGTH_DELIMITED, + buffer[message_start:message_end].tobytes()) + # pylint: enable=protected-access + + return pos + + return DecodeItem + +# -------------------------------------------------------------------- + +def MapDecoder(field_descriptor, new_default, is_message_map): + """Returns a decoder for a map field.""" + + key = field_descriptor + tag_bytes = encoder.TagBytes(field_descriptor.number, + wire_format.WIRETYPE_LENGTH_DELIMITED) + tag_len = len(tag_bytes) + local_DecodeVarint = _DecodeVarint + # Can't read _concrete_class yet; might not be initialized. + message_type = field_descriptor.message_type + + def DecodeMap(buffer, pos, end, message, field_dict): + submsg = message_type._concrete_class() + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + # Read length. + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated message.') + # Read sub-message. + submsg.Clear() + if submsg._InternalParse(buffer, pos, new_pos) != new_pos: + # The only reason _InternalParse would return early is if it + # encountered an end-group tag. + raise _DecodeError('Unexpected end-group tag.') + + if is_message_map: + value[submsg.key].CopyFrom(submsg.value) + else: + value[submsg.key] = submsg.value + + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + + return DecodeMap + +# -------------------------------------------------------------------- +# Optimization is not as heavy here because calls to SkipField() are rare, +# except for handling end-group tags. + +def _SkipVarint(buffer, pos, end): + """Skip a varint value. Returns the new position.""" + # Previously ord(buffer[pos]) raised IndexError when pos is out of range. + # With this code, ord(b'') raises TypeError. Both are handled in + # python_message.py to generate a 'Truncated message' error. + while ord(buffer[pos:pos+1].tobytes()) & 0x80: + pos += 1 + pos += 1 + if pos > end: + raise _DecodeError('Truncated message.') + return pos + +def _SkipFixed64(buffer, pos, end): + """Skip a fixed64 value. Returns the new position.""" + + pos += 8 + if pos > end: + raise _DecodeError('Truncated message.') + return pos + + +def _DecodeFixed64(buffer, pos): + """Decode a fixed64.""" + new_pos = pos + 8 + return (struct.unpack('<Q', buffer[pos:new_pos])[0], new_pos) + + +def _SkipLengthDelimited(buffer, pos, end): + """Skip a length-delimited value. Returns the new position.""" + + (size, pos) = _DecodeVarint(buffer, pos) + pos += size + if pos > end: + raise _DecodeError('Truncated message.') + return pos + + +def _SkipGroup(buffer, pos, end): + """Skip sub-group. Returns the new position.""" + + while 1: + (tag_bytes, pos) = ReadTag(buffer, pos) + new_pos = SkipField(buffer, pos, end, tag_bytes) + if new_pos == -1: + return pos + pos = new_pos + + +def _DecodeUnknownFieldSet(buffer, pos, end_pos=None): + """Decode UnknownFieldSet. Returns the UnknownFieldSet and new position.""" + + unknown_field_set = containers.UnknownFieldSet() + while end_pos is None or pos < end_pos: + (tag_bytes, pos) = ReadTag(buffer, pos) + (tag, _) = _DecodeVarint(tag_bytes, 0) + field_number, wire_type = wire_format.UnpackTag(tag) + if wire_type == wire_format.WIRETYPE_END_GROUP: + break + (data, pos) = _DecodeUnknownField(buffer, pos, wire_type) + # pylint: disable=protected-access + unknown_field_set._add(field_number, wire_type, data) + + return (unknown_field_set, pos) + + +def _DecodeUnknownField(buffer, pos, wire_type): + """Decode a unknown field. Returns the UnknownField and new position.""" + + if wire_type == wire_format.WIRETYPE_VARINT: + (data, pos) = _DecodeVarint(buffer, pos) + elif wire_type == wire_format.WIRETYPE_FIXED64: + (data, pos) = _DecodeFixed64(buffer, pos) + elif wire_type == wire_format.WIRETYPE_FIXED32: + (data, pos) = _DecodeFixed32(buffer, pos) + elif wire_type == wire_format.WIRETYPE_LENGTH_DELIMITED: + (size, pos) = _DecodeVarint(buffer, pos) + data = buffer[pos:pos+size].tobytes() + pos += size + elif wire_type == wire_format.WIRETYPE_START_GROUP: + (data, pos) = _DecodeUnknownFieldSet(buffer, pos) + elif wire_type == wire_format.WIRETYPE_END_GROUP: + return (0, -1) + else: + raise _DecodeError('Wrong wire type in tag.') + + return (data, pos) + + +def _EndGroup(buffer, pos, end): + """Skipping an END_GROUP tag returns -1 to tell the parent loop to break.""" + + return -1 + + +def _SkipFixed32(buffer, pos, end): + """Skip a fixed32 value. Returns the new position.""" + + pos += 4 + if pos > end: + raise _DecodeError('Truncated message.') + return pos + + +def _DecodeFixed32(buffer, pos): + """Decode a fixed32.""" + + new_pos = pos + 4 + return (struct.unpack('<I', buffer[pos:new_pos])[0], new_pos) + + +def _RaiseInvalidWireType(buffer, pos, end): + """Skip function for unknown wire types. Raises an exception.""" + + raise _DecodeError('Tag had invalid wire type.') + +def _FieldSkipper(): + """Constructs the SkipField function.""" + + WIRETYPE_TO_SKIPPER = [ + _SkipVarint, + _SkipFixed64, + _SkipLengthDelimited, + _SkipGroup, + _EndGroup, + _SkipFixed32, + _RaiseInvalidWireType, + _RaiseInvalidWireType, + ] + + wiretype_mask = wire_format.TAG_TYPE_MASK + + def SkipField(buffer, pos, end, tag_bytes): + """Skips a field with the specified tag. + + |pos| should point to the byte immediately after the tag. + + Returns: + The new position (after the tag value), or -1 if the tag is an end-group + tag (in which case the calling loop should break). + """ + + # The wire type is always in the first byte since varints are little-endian. + wire_type = ord(tag_bytes[0:1]) & wiretype_mask + return WIRETYPE_TO_SKIPPER[wire_type](buffer, pos, end) + + return SkipField + +SkipField = _FieldSkipper() diff --git a/scripts/protobuf3/protobuf3/internal/encoder.py b/scripts/protobuf3/protobuf3/internal/encoder.py new file mode 100644 index 0000000..4b4f652 --- /dev/null +++ b/scripts/protobuf3/protobuf3/internal/encoder.py @@ -0,0 +1,829 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Code for encoding protocol message primitives. + +Contains the logic for encoding every logical protocol field type +into one of the 5 physical wire types. + +This code is designed to push the Python interpreter's performance to the +limits. + +The basic idea is that at startup time, for every field (i.e. every +FieldDescriptor) we construct two functions: a "sizer" and an "encoder". The +sizer takes a value of this field's type and computes its byte size. The +encoder takes a writer function and a value. It encodes the value into byte +strings and invokes the writer function to write those strings. Typically the +writer function is the write() method of a BytesIO. + +We try to do as much work as possible when constructing the writer and the +sizer rather than when calling them. In particular: +* We copy any needed global functions to local variables, so that we do not need + to do costly global table lookups at runtime. +* Similarly, we try to do any attribute lookups at startup time if possible. +* Every field's tag is encoded to bytes at startup, since it can't change at + runtime. +* Whatever component of the field size we can compute at startup, we do. +* We *avoid* sharing code if doing so would make the code slower and not sharing + does not burden us too much. For example, encoders for repeated fields do + not just call the encoders for singular fields in a loop because this would + add an extra function call overhead for every loop iteration; instead, we + manually inline the single-value encoder into the loop. +* If a Python function lacks a return statement, Python actually generates + instructions to pop the result of the last statement off the stack, push + None onto the stack, and then return that. If we really don't care what + value is returned, then we can save two instructions by returning the + result of the last statement. It looks funny but it helps. +* We assume that type and bounds checking has happened at a higher level. +""" + +__author__ = 'kenton@google.com (Kenton Varda)' + +import struct + +from google.protobuf.internal import wire_format + + +# This will overflow and thus become IEEE-754 "infinity". We would use +# "float('inf')" but it doesn't work on Windows pre-Python-2.6. +_POS_INF = 1e10000 +_NEG_INF = -_POS_INF + + +def _VarintSize(value): + """Compute the size of a varint value.""" + if value <= 0x7f: return 1 + if value <= 0x3fff: return 2 + if value <= 0x1fffff: return 3 + if value <= 0xfffffff: return 4 + if value <= 0x7ffffffff: return 5 + if value <= 0x3ffffffffff: return 6 + if value <= 0x1ffffffffffff: return 7 + if value <= 0xffffffffffffff: return 8 + if value <= 0x7fffffffffffffff: return 9 + return 10 + + +def _SignedVarintSize(value): + """Compute the size of a signed varint value.""" + if value < 0: return 10 + if value <= 0x7f: return 1 + if value <= 0x3fff: return 2 + if value <= 0x1fffff: return 3 + if value <= 0xfffffff: return 4 + if value <= 0x7ffffffff: return 5 + if value <= 0x3ffffffffff: return 6 + if value <= 0x1ffffffffffff: return 7 + if value <= 0xffffffffffffff: return 8 + if value <= 0x7fffffffffffffff: return 9 + return 10 + + +def _TagSize(field_number): + """Returns the number of bytes required to serialize a tag with this field + number.""" + # Just pass in type 0, since the type won't affect the tag+type size. + return _VarintSize(wire_format.PackTag(field_number, 0)) + + +# -------------------------------------------------------------------- +# In this section we define some generic sizers. Each of these functions +# takes parameters specific to a particular field type, e.g. int32 or fixed64. +# It returns another function which in turn takes parameters specific to a +# particular field, e.g. the field number and whether it is repeated or packed. +# Look at the next section to see how these are used. + + +def _SimpleSizer(compute_value_size): + """A sizer which uses the function compute_value_size to compute the size of + each value. Typically compute_value_size is _VarintSize.""" + + def SpecificSizer(field_number, is_repeated, is_packed): + tag_size = _TagSize(field_number) + if is_packed: + local_VarintSize = _VarintSize + def PackedFieldSize(value): + result = 0 + for element in value: + result += compute_value_size(element) + return result + local_VarintSize(result) + tag_size + return PackedFieldSize + elif is_repeated: + def RepeatedFieldSize(value): + result = tag_size * len(value) + for element in value: + result += compute_value_size(element) + return result + return RepeatedFieldSize + else: + def FieldSize(value): + return tag_size + compute_value_size(value) + return FieldSize + + return SpecificSizer + + +def _ModifiedSizer(compute_value_size, modify_value): + """Like SimpleSizer, but modify_value is invoked on each value before it is + passed to compute_value_size. modify_value is typically ZigZagEncode.""" + + def SpecificSizer(field_number, is_repeated, is_packed): + tag_size = _TagSize(field_number) + if is_packed: + local_VarintSize = _VarintSize + def PackedFieldSize(value): + result = 0 + for element in value: + result += compute_value_size(modify_value(element)) + return result + local_VarintSize(result) + tag_size + return PackedFieldSize + elif is_repeated: + def RepeatedFieldSize(value): + result = tag_size * len(value) + for element in value: + result += compute_value_size(modify_value(element)) + return result + return RepeatedFieldSize + else: + def FieldSize(value): + return tag_size + compute_value_size(modify_value(value)) + return FieldSize + + return SpecificSizer + + +def _FixedSizer(value_size): + """Like _SimpleSizer except for a fixed-size field. The input is the size + of one value.""" + + def SpecificSizer(field_number, is_repeated, is_packed): + tag_size = _TagSize(field_number) + if is_packed: + local_VarintSize = _VarintSize + def PackedFieldSize(value): + result = len(value) * value_size + return result + local_VarintSize(result) + tag_size + return PackedFieldSize + elif is_repeated: + element_size = value_size + tag_size + def RepeatedFieldSize(value): + return len(value) * element_size + return RepeatedFieldSize + else: + field_size = value_size + tag_size + def FieldSize(value): + return field_size + return FieldSize + + return SpecificSizer + + +# ==================================================================== +# Here we declare a sizer constructor for each field type. Each "sizer +# constructor" is a function that takes (field_number, is_repeated, is_packed) +# as parameters and returns a sizer, which in turn takes a field value as +# a parameter and returns its encoded size. + + +Int32Sizer = Int64Sizer = EnumSizer = _SimpleSizer(_SignedVarintSize) + +UInt32Sizer = UInt64Sizer = _SimpleSizer(_VarintSize) + +SInt32Sizer = SInt64Sizer = _ModifiedSizer( + _SignedVarintSize, wire_format.ZigZagEncode) + +Fixed32Sizer = SFixed32Sizer = FloatSizer = _FixedSizer(4) +Fixed64Sizer = SFixed64Sizer = DoubleSizer = _FixedSizer(8) + +BoolSizer = _FixedSizer(1) + + +def StringSizer(field_number, is_repeated, is_packed): + """Returns a sizer for a string field.""" + + tag_size = _TagSize(field_number) + local_VarintSize = _VarintSize + local_len = len + assert not is_packed + if is_repeated: + def RepeatedFieldSize(value): + result = tag_size * len(value) + for element in value: + l = local_len(element.encode('utf-8')) + result += local_VarintSize(l) + l + return result + return RepeatedFieldSize + else: + def FieldSize(value): + l = local_len(value.encode('utf-8')) + return tag_size + local_VarintSize(l) + l + return FieldSize + + +def BytesSizer(field_number, is_repeated, is_packed): + """Returns a sizer for a bytes field.""" + + tag_size = _TagSize(field_number) + local_VarintSize = _VarintSize + local_len = len + assert not is_packed + if is_repeated: + def RepeatedFieldSize(value): + result = tag_size * len(value) + for element in value: + l = local_len(element) + result += local_VarintSize(l) + l + return result + return RepeatedFieldSize + else: + def FieldSize(value): + l = local_len(value) + return tag_size + local_VarintSize(l) + l + return FieldSize + + +def GroupSizer(field_number, is_repeated, is_packed): + """Returns a sizer for a group field.""" + + tag_size = _TagSize(field_number) * 2 + assert not is_packed + if is_repeated: + def RepeatedFieldSize(value): + result = tag_size * len(value) + for element in value: + result += element.ByteSize() + return result + return RepeatedFieldSize + else: + def FieldSize(value): + return tag_size + value.ByteSize() + return FieldSize + + +def MessageSizer(field_number, is_repeated, is_packed): + """Returns a sizer for a message field.""" + + tag_size = _TagSize(field_number) + local_VarintSize = _VarintSize + assert not is_packed + if is_repeated: + def RepeatedFieldSize(value): + result = tag_size * len(value) + for element in value: + l = element.ByteSize() + result += local_VarintSize(l) + l + return result + return RepeatedFieldSize + else: + def FieldSize(value): + l = value.ByteSize() + return tag_size + local_VarintSize(l) + l + return FieldSize + + +# -------------------------------------------------------------------- +# MessageSet is special: it needs custom logic to compute its size properly. + + +def MessageSetItemSizer(field_number): + """Returns a sizer for extensions of MessageSet. + + The message set message looks like this: + message MessageSet { + repeated group Item = 1 { + required int32 type_id = 2; + required string message = 3; + } + } + """ + static_size = (_TagSize(1) * 2 + _TagSize(2) + _VarintSize(field_number) + + _TagSize(3)) + local_VarintSize = _VarintSize + + def FieldSize(value): + l = value.ByteSize() + return static_size + local_VarintSize(l) + l + + return FieldSize + + +# -------------------------------------------------------------------- +# Map is special: it needs custom logic to compute its size properly. + + +def MapSizer(field_descriptor, is_message_map): + """Returns a sizer for a map field.""" + + # Can't look at field_descriptor.message_type._concrete_class because it may + # not have been initialized yet. + message_type = field_descriptor.message_type + message_sizer = MessageSizer(field_descriptor.number, False, False) + + def FieldSize(map_value): + total = 0 + for key in map_value: + value = map_value[key] + # It's wasteful to create the messages and throw them away one second + # later since we'll do the same for the actual encode. But there's not an + # obvious way to avoid this within the current design without tons of code + # duplication. For message map, value.ByteSize() should be called to + # update the status. + entry_msg = message_type._concrete_class(key=key, value=value) + total += message_sizer(entry_msg) + if is_message_map: + value.ByteSize() + return total + + return FieldSize + +# ==================================================================== +# Encoders! + + +def _VarintEncoder(): + """Return an encoder for a basic varint value (does not include tag).""" + + local_int2byte = struct.Struct('>B').pack + + def EncodeVarint(write, value, unused_deterministic=None): + bits = value & 0x7f + value >>= 7 + while value: + write(local_int2byte(0x80|bits)) + bits = value & 0x7f + value >>= 7 + return write(local_int2byte(bits)) + + return EncodeVarint + + +def _SignedVarintEncoder(): + """Return an encoder for a basic signed varint value (does not include + tag).""" + + local_int2byte = struct.Struct('>B').pack + + def EncodeSignedVarint(write, value, unused_deterministic=None): + if value < 0: + value += (1 << 64) + bits = value & 0x7f + value >>= 7 + while value: + write(local_int2byte(0x80|bits)) + bits = value & 0x7f + value >>= 7 + return write(local_int2byte(bits)) + + return EncodeSignedVarint + + +_EncodeVarint = _VarintEncoder() +_EncodeSignedVarint = _SignedVarintEncoder() + + +def _VarintBytes(value): + """Encode the given integer as a varint and return the bytes. This is only + called at startup time so it doesn't need to be fast.""" + + pieces = [] + _EncodeVarint(pieces.append, value, True) + return b"".join(pieces) + + +def TagBytes(field_number, wire_type): + """Encode the given tag and return the bytes. Only called at startup.""" + + return bytes(_VarintBytes(wire_format.PackTag(field_number, wire_type))) + +# -------------------------------------------------------------------- +# As with sizers (see above), we have a number of common encoder +# implementations. + + +def _SimpleEncoder(wire_type, encode_value, compute_value_size): + """Return a constructor for an encoder for fields of a particular type. + + Args: + wire_type: The field's wire type, for encoding tags. + encode_value: A function which encodes an individual value, e.g. + _EncodeVarint(). + compute_value_size: A function which computes the size of an individual + value, e.g. _VarintSize(). + """ + + def SpecificEncoder(field_number, is_repeated, is_packed): + if is_packed: + tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + def EncodePackedField(write, value, deterministic): + write(tag_bytes) + size = 0 + for element in value: + size += compute_value_size(element) + local_EncodeVarint(write, size, deterministic) + for element in value: + encode_value(write, element, deterministic) + return EncodePackedField + elif is_repeated: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeRepeatedField(write, value, deterministic): + for element in value: + write(tag_bytes) + encode_value(write, element, deterministic) + return EncodeRepeatedField + else: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeField(write, value, deterministic): + write(tag_bytes) + return encode_value(write, value, deterministic) + return EncodeField + + return SpecificEncoder + + +def _ModifiedEncoder(wire_type, encode_value, compute_value_size, modify_value): + """Like SimpleEncoder but additionally invokes modify_value on every value + before passing it to encode_value. Usually modify_value is ZigZagEncode.""" + + def SpecificEncoder(field_number, is_repeated, is_packed): + if is_packed: + tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + def EncodePackedField(write, value, deterministic): + write(tag_bytes) + size = 0 + for element in value: + size += compute_value_size(modify_value(element)) + local_EncodeVarint(write, size, deterministic) + for element in value: + encode_value(write, modify_value(element), deterministic) + return EncodePackedField + elif is_repeated: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeRepeatedField(write, value, deterministic): + for element in value: + write(tag_bytes) + encode_value(write, modify_value(element), deterministic) + return EncodeRepeatedField + else: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeField(write, value, deterministic): + write(tag_bytes) + return encode_value(write, modify_value(value), deterministic) + return EncodeField + + return SpecificEncoder + + +def _StructPackEncoder(wire_type, format): + """Return a constructor for an encoder for a fixed-width field. + + Args: + wire_type: The field's wire type, for encoding tags. + format: The format string to pass to struct.pack(). + """ + + value_size = struct.calcsize(format) + + def SpecificEncoder(field_number, is_repeated, is_packed): + local_struct_pack = struct.pack + if is_packed: + tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + def EncodePackedField(write, value, deterministic): + write(tag_bytes) + local_EncodeVarint(write, len(value) * value_size, deterministic) + for element in value: + write(local_struct_pack(format, element)) + return EncodePackedField + elif is_repeated: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeRepeatedField(write, value, unused_deterministic=None): + for element in value: + write(tag_bytes) + write(local_struct_pack(format, element)) + return EncodeRepeatedField + else: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeField(write, value, unused_deterministic=None): + write(tag_bytes) + return write(local_struct_pack(format, value)) + return EncodeField + + return SpecificEncoder + + +def _FloatingPointEncoder(wire_type, format): + """Return a constructor for an encoder for float fields. + + This is like StructPackEncoder, but catches errors that may be due to + passing non-finite floating-point values to struct.pack, and makes a + second attempt to encode those values. + + Args: + wire_type: The field's wire type, for encoding tags. + format: The format string to pass to struct.pack(). + """ + + value_size = struct.calcsize(format) + if value_size == 4: + def EncodeNonFiniteOrRaise(write, value): + # Remember that the serialized form uses little-endian byte order. + if value == _POS_INF: + write(b'\x00\x00\x80\x7F') + elif value == _NEG_INF: + write(b'\x00\x00\x80\xFF') + elif value != value: # NaN + write(b'\x00\x00\xC0\x7F') + else: + raise + elif value_size == 8: + def EncodeNonFiniteOrRaise(write, value): + if value == _POS_INF: + write(b'\x00\x00\x00\x00\x00\x00\xF0\x7F') + elif value == _NEG_INF: + write(b'\x00\x00\x00\x00\x00\x00\xF0\xFF') + elif value != value: # NaN + write(b'\x00\x00\x00\x00\x00\x00\xF8\x7F') + else: + raise + else: + raise ValueError('Can\'t encode floating-point values that are ' + '%d bytes long (only 4 or 8)' % value_size) + + def SpecificEncoder(field_number, is_repeated, is_packed): + local_struct_pack = struct.pack + if is_packed: + tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + def EncodePackedField(write, value, deterministic): + write(tag_bytes) + local_EncodeVarint(write, len(value) * value_size, deterministic) + for element in value: + # This try/except block is going to be faster than any code that + # we could write to check whether element is finite. + try: + write(local_struct_pack(format, element)) + except SystemError: + EncodeNonFiniteOrRaise(write, element) + return EncodePackedField + elif is_repeated: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeRepeatedField(write, value, unused_deterministic=None): + for element in value: + write(tag_bytes) + try: + write(local_struct_pack(format, element)) + except SystemError: + EncodeNonFiniteOrRaise(write, element) + return EncodeRepeatedField + else: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeField(write, value, unused_deterministic=None): + write(tag_bytes) + try: + write(local_struct_pack(format, value)) + except SystemError: + EncodeNonFiniteOrRaise(write, value) + return EncodeField + + return SpecificEncoder + + +# ==================================================================== +# Here we declare an encoder constructor for each field type. These work +# very similarly to sizer constructors, described earlier. + + +Int32Encoder = Int64Encoder = EnumEncoder = _SimpleEncoder( + wire_format.WIRETYPE_VARINT, _EncodeSignedVarint, _SignedVarintSize) + +UInt32Encoder = UInt64Encoder = _SimpleEncoder( + wire_format.WIRETYPE_VARINT, _EncodeVarint, _VarintSize) + +SInt32Encoder = SInt64Encoder = _ModifiedEncoder( + wire_format.WIRETYPE_VARINT, _EncodeVarint, _VarintSize, + wire_format.ZigZagEncode) + +# Note that Python conveniently guarantees that when using the '<' prefix on +# formats, they will also have the same size across all platforms (as opposed +# to without the prefix, where their sizes depend on the C compiler's basic +# type sizes). +Fixed32Encoder = _StructPackEncoder(wire_format.WIRETYPE_FIXED32, '<I') +Fixed64Encoder = _StructPackEncoder(wire_format.WIRETYPE_FIXED64, '<Q') +SFixed32Encoder = _StructPackEncoder(wire_format.WIRETYPE_FIXED32, '<i') +SFixed64Encoder = _StructPackEncoder(wire_format.WIRETYPE_FIXED64, '<q') +FloatEncoder = _FloatingPointEncoder(wire_format.WIRETYPE_FIXED32, '<f') +DoubleEncoder = _FloatingPointEncoder(wire_format.WIRETYPE_FIXED64, '<d') + + +def BoolEncoder(field_number, is_repeated, is_packed): + """Returns an encoder for a boolean field.""" + + false_byte = b'\x00' + true_byte = b'\x01' + if is_packed: + tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + def EncodePackedField(write, value, deterministic): + write(tag_bytes) + local_EncodeVarint(write, len(value), deterministic) + for element in value: + if element: + write(true_byte) + else: + write(false_byte) + return EncodePackedField + elif is_repeated: + tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_VARINT) + def EncodeRepeatedField(write, value, unused_deterministic=None): + for element in value: + write(tag_bytes) + if element: + write(true_byte) + else: + write(false_byte) + return EncodeRepeatedField + else: + tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_VARINT) + def EncodeField(write, value, unused_deterministic=None): + write(tag_bytes) + if value: + return write(true_byte) + return write(false_byte) + return EncodeField + + +def StringEncoder(field_number, is_repeated, is_packed): + """Returns an encoder for a string field.""" + + tag = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + local_len = len + assert not is_packed + if is_repeated: + def EncodeRepeatedField(write, value, deterministic): + for element in value: + encoded = element.encode('utf-8') + write(tag) + local_EncodeVarint(write, local_len(encoded), deterministic) + write(encoded) + return EncodeRepeatedField + else: + def EncodeField(write, value, deterministic): + encoded = value.encode('utf-8') + write(tag) + local_EncodeVarint(write, local_len(encoded), deterministic) + return write(encoded) + return EncodeField + + +def BytesEncoder(field_number, is_repeated, is_packed): + """Returns an encoder for a bytes field.""" + + tag = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + local_len = len + assert not is_packed + if is_repeated: + def EncodeRepeatedField(write, value, deterministic): + for element in value: + write(tag) + local_EncodeVarint(write, local_len(element), deterministic) + write(element) + return EncodeRepeatedField + else: + def EncodeField(write, value, deterministic): + write(tag) + local_EncodeVarint(write, local_len(value), deterministic) + return write(value) + return EncodeField + + +def GroupEncoder(field_number, is_repeated, is_packed): + """Returns an encoder for a group field.""" + + start_tag = TagBytes(field_number, wire_format.WIRETYPE_START_GROUP) + end_tag = TagBytes(field_number, wire_format.WIRETYPE_END_GROUP) + assert not is_packed + if is_repeated: + def EncodeRepeatedField(write, value, deterministic): + for element in value: + write(start_tag) + element._InternalSerialize(write, deterministic) + write(end_tag) + return EncodeRepeatedField + else: + def EncodeField(write, value, deterministic): + write(start_tag) + value._InternalSerialize(write, deterministic) + return write(end_tag) + return EncodeField + + +def MessageEncoder(field_number, is_repeated, is_packed): + """Returns an encoder for a message field.""" + + tag = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + assert not is_packed + if is_repeated: + def EncodeRepeatedField(write, value, deterministic): + for element in value: + write(tag) + local_EncodeVarint(write, element.ByteSize(), deterministic) + element._InternalSerialize(write, deterministic) + return EncodeRepeatedField + else: + def EncodeField(write, value, deterministic): + write(tag) + local_EncodeVarint(write, value.ByteSize(), deterministic) + return value._InternalSerialize(write, deterministic) + return EncodeField + + +# -------------------------------------------------------------------- +# As before, MessageSet is special. + + +def MessageSetItemEncoder(field_number): + """Encoder for extensions of MessageSet. + + The message set message looks like this: + message MessageSet { + repeated group Item = 1 { + required int32 type_id = 2; + required string message = 3; + } + } + """ + start_bytes = b"".join([ + TagBytes(1, wire_format.WIRETYPE_START_GROUP), + TagBytes(2, wire_format.WIRETYPE_VARINT), + _VarintBytes(field_number), + TagBytes(3, wire_format.WIRETYPE_LENGTH_DELIMITED)]) + end_bytes = TagBytes(1, wire_format.WIRETYPE_END_GROUP) + local_EncodeVarint = _EncodeVarint + + def EncodeField(write, value, deterministic): + write(start_bytes) + local_EncodeVarint(write, value.ByteSize(), deterministic) + value._InternalSerialize(write, deterministic) + return write(end_bytes) + + return EncodeField + + +# -------------------------------------------------------------------- +# As before, Map is special. + + +def MapEncoder(field_descriptor): + """Encoder for extensions of MessageSet. + + Maps always have a wire format like this: + message MapEntry { + key_type key = 1; + value_type value = 2; + } + repeated MapEntry map = N; + """ + # Can't look at field_descriptor.message_type._concrete_class because it may + # not have been initialized yet. + message_type = field_descriptor.message_type + encode_message = MessageEncoder(field_descriptor.number, False, False) + + def EncodeField(write, value, deterministic): + value_keys = sorted(value.keys()) if deterministic else value + for key in value_keys: + entry_msg = message_type._concrete_class(key=key, value=value[key]) + encode_message(write, entry_msg, deterministic) + + return EncodeField diff --git a/scripts/protobuf3/protobuf3/internal/enum_type_wrapper.py b/scripts/protobuf3/protobuf3/internal/enum_type_wrapper.py new file mode 100644 index 0000000..65d2c7a --- /dev/null +++ b/scripts/protobuf3/protobuf3/internal/enum_type_wrapper.py @@ -0,0 +1,124 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""A simple wrapper around enum types to expose utility functions. + +Instances are created as properties with the same name as the enum they wrap +on proto classes. For usage, see: + reflection_test.py +""" + +__author__ = 'rabsatt@google.com (Kevin Rabsatt)' + + +class EnumTypeWrapper(object): + """A utility for finding the names of enum values.""" + + DESCRIPTOR = None + + # This is a type alias, which mypy typing stubs can type as + # a genericized parameter constrained to an int, allowing subclasses + # to be typed with more constraint in .pyi stubs + # Eg. + # def MyGeneratedEnum(Message): + # ValueType = NewType('ValueType', int) + # def Name(self, number: MyGeneratedEnum.ValueType) -> str + ValueType = int + + def __init__(self, enum_type): + """Inits EnumTypeWrapper with an EnumDescriptor.""" + self._enum_type = enum_type + self.DESCRIPTOR = enum_type # pylint: disable=invalid-name + + def Name(self, number): # pylint: disable=invalid-name + """Returns a string containing the name of an enum value.""" + try: + return self._enum_type.values_by_number[number].name + except KeyError: + pass # fall out to break exception chaining + + if not isinstance(number, int): + raise TypeError( + 'Enum value for {} must be an int, but got {} {!r}.'.format( + self._enum_type.name, type(number), number)) + else: + # repr here to handle the odd case when you pass in a boolean. + raise ValueError('Enum {} has no name defined for value {!r}'.format( + self._enum_type.name, number)) + + def Value(self, name): # pylint: disable=invalid-name + """Returns the value corresponding to the given enum name.""" + try: + return self._enum_type.values_by_name[name].number + except KeyError: + pass # fall out to break exception chaining + raise ValueError('Enum {} has no value defined for name {!r}'.format( + self._enum_type.name, name)) + + def keys(self): + """Return a list of the string names in the enum. + + Returns: + A list of strs, in the order they were defined in the .proto file. + """ + + return [value_descriptor.name + for value_descriptor in self._enum_type.values] + + def values(self): + """Return a list of the integer values in the enum. + + Returns: + A list of ints, in the order they were defined in the .proto file. + """ + + return [value_descriptor.number + for value_descriptor in self._enum_type.values] + + def items(self): + """Return a list of the (name, value) pairs of the enum. + + Returns: + A list of (str, int) pairs, in the order they were defined + in the .proto file. + """ + return [(value_descriptor.name, value_descriptor.number) + for value_descriptor in self._enum_type.values] + + def __getattr__(self, name): + """Returns the value corresponding to the given enum name.""" + try: + return super( + EnumTypeWrapper, + self).__getattribute__('_enum_type').values_by_name[name].number + except KeyError: + pass # fall out to break exception chaining + raise AttributeError('Enum {} has no value defined for name {!r}'.format( + self._enum_type.name, name)) diff --git a/scripts/protobuf3/protobuf3/internal/extension_dict.py b/scripts/protobuf3/protobuf3/internal/extension_dict.py new file mode 100644 index 0000000..b346cf2 --- /dev/null +++ b/scripts/protobuf3/protobuf3/internal/extension_dict.py @@ -0,0 +1,213 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Contains _ExtensionDict class to represent extensions. +""" + +from google.protobuf.internal import type_checkers +from google.protobuf.descriptor import FieldDescriptor + + +def _VerifyExtensionHandle(message, extension_handle): + """Verify that the given extension handle is valid.""" + + if not isinstance(extension_handle, FieldDescriptor): + raise KeyError('HasExtension() expects an extension handle, got: %s' % + extension_handle) + + if not extension_handle.is_extension: + raise KeyError('"%s" is not an extension.' % extension_handle.full_name) + + if not extension_handle.containing_type: + raise KeyError('"%s" is missing a containing_type.' + % extension_handle.full_name) + + if extension_handle.containing_type is not message.DESCRIPTOR: + raise KeyError('Extension "%s" extends message type "%s", but this ' + 'message is of type "%s".' % + (extension_handle.full_name, + extension_handle.containing_type.full_name, + message.DESCRIPTOR.full_name)) + + +# TODO(robinson): Unify error handling of "unknown extension" crap. +# TODO(robinson): Support iteritems()-style iteration over all +# extensions with the "has" bits turned on? +class _ExtensionDict(object): + + """Dict-like container for Extension fields on proto instances. + + Note that in all cases we expect extension handles to be + FieldDescriptors. + """ + + def __init__(self, extended_message): + """ + Args: + extended_message: Message instance for which we are the Extensions dict. + """ + self._extended_message = extended_message + + def __getitem__(self, extension_handle): + """Returns the current value of the given extension handle.""" + + _VerifyExtensionHandle(self._extended_message, extension_handle) + + result = self._extended_message._fields.get(extension_handle) + if result is not None: + return result + + if extension_handle.label == FieldDescriptor.LABEL_REPEATED: + result = extension_handle._default_constructor(self._extended_message) + elif extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: + message_type = extension_handle.message_type + if not hasattr(message_type, '_concrete_class'): + # pylint: disable=protected-access + self._extended_message._FACTORY.GetPrototype(message_type) + assert getattr(extension_handle.message_type, '_concrete_class', None), ( + 'Uninitialized concrete class found for field %r (message type %r)' + % (extension_handle.full_name, + extension_handle.message_type.full_name)) + result = extension_handle.message_type._concrete_class() + try: + result._SetListener(self._extended_message._listener_for_children) + except ReferenceError: + pass + else: + # Singular scalar -- just return the default without inserting into the + # dict. + return extension_handle.default_value + + # Atomically check if another thread has preempted us and, if not, swap + # in the new object we just created. If someone has preempted us, we + # take that object and discard ours. + # WARNING: We are relying on setdefault() being atomic. This is true + # in CPython but we haven't investigated others. This warning appears + # in several other locations in this file. + result = self._extended_message._fields.setdefault( + extension_handle, result) + + return result + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return False + + my_fields = self._extended_message.ListFields() + other_fields = other._extended_message.ListFields() + + # Get rid of non-extension fields. + my_fields = [field for field in my_fields if field.is_extension] + other_fields = [field for field in other_fields if field.is_extension] + + return my_fields == other_fields + + def __ne__(self, other): + return not self == other + + def __len__(self): + fields = self._extended_message.ListFields() + # Get rid of non-extension fields. + extension_fields = [field for field in fields if field[0].is_extension] + return len(extension_fields) + + def __hash__(self): + raise TypeError('unhashable object') + + # Note that this is only meaningful for non-repeated, scalar extension + # fields. Note also that we may have to call _Modified() when we do + # successfully set a field this way, to set any necessary "has" bits in the + # ancestors of the extended message. + def __setitem__(self, extension_handle, value): + """If extension_handle specifies a non-repeated, scalar extension + field, sets the value of that field. + """ + + _VerifyExtensionHandle(self._extended_message, extension_handle) + + if (extension_handle.label == FieldDescriptor.LABEL_REPEATED or + extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE): + raise TypeError( + 'Cannot assign to extension "%s" because it is a repeated or ' + 'composite type.' % extension_handle.full_name) + + # It's slightly wasteful to lookup the type checker each time, + # but we expect this to be a vanishingly uncommon case anyway. + type_checker = type_checkers.GetTypeChecker(extension_handle) + # pylint: disable=protected-access + self._extended_message._fields[extension_handle] = ( + type_checker.CheckValue(value)) + self._extended_message._Modified() + + def __delitem__(self, extension_handle): + self._extended_message.ClearExtension(extension_handle) + + def _FindExtensionByName(self, name): + """Tries to find a known extension with the specified name. + + Args: + name: Extension full name. + + Returns: + Extension field descriptor. + """ + return self._extended_message._extensions_by_name.get(name, None) + + def _FindExtensionByNumber(self, number): + """Tries to find a known extension with the field number. + + Args: + number: Extension field number. + + Returns: + Extension field descriptor. + """ + return self._extended_message._extensions_by_number.get(number, None) + + def __iter__(self): + # Return a generator over the populated extension fields + return (f[0] for f in self._extended_message.ListFields() + if f[0].is_extension) + + def __contains__(self, extension_handle): + _VerifyExtensionHandle(self._extended_message, extension_handle) + + if extension_handle not in self._extended_message._fields: + return False + + if extension_handle.label == FieldDescriptor.LABEL_REPEATED: + return bool(self._extended_message._fields.get(extension_handle)) + + if extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: + value = self._extended_message._fields.get(extension_handle) + # pylint: disable=protected-access + return value is not None and value._is_present_in_parent + + return True diff --git a/scripts/protobuf3/protobuf3/internal/message_listener.py b/scripts/protobuf3/protobuf3/internal/message_listener.py new file mode 100644 index 0000000..0fc255a --- /dev/null +++ b/scripts/protobuf3/protobuf3/internal/message_listener.py @@ -0,0 +1,78 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Defines a listener interface for observing certain +state transitions on Message objects. + +Also defines a null implementation of this interface. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + + +class MessageListener(object): + + """Listens for modifications made to a message. Meant to be registered via + Message._SetListener(). + + Attributes: + dirty: If True, then calling Modified() would be a no-op. This can be + used to avoid these calls entirely in the common case. + """ + + def Modified(self): + """Called every time the message is modified in such a way that the parent + message may need to be updated. This currently means either: + (a) The message was modified for the first time, so the parent message + should henceforth mark the message as present. + (b) The message's cached byte size became dirty -- i.e. the message was + modified for the first time after a previous call to ByteSize(). + Therefore the parent should also mark its byte size as dirty. + Note that (a) implies (b), since new objects start out with a client cached + size (zero). However, we document (a) explicitly because it is important. + + Modified() will *only* be called in response to one of these two events -- + not every time the sub-message is modified. + + Note that if the listener's |dirty| attribute is true, then calling + Modified at the moment would be a no-op, so it can be skipped. Performance- + sensitive callers should check this attribute directly before calling since + it will be true most of the time. + """ + + raise NotImplementedError + + +class NullMessageListener(object): + + """No-op MessageListener implementation.""" + + def Modified(self): + pass diff --git a/scripts/protobuf3/protobuf3/internal/message_set_extensions_pb2.py b/scripts/protobuf3/protobuf3/internal/message_set_extensions_pb2.py new file mode 100644 index 0000000..63651a3 --- /dev/null +++ b/scripts/protobuf3/protobuf3/internal/message_set_extensions_pb2.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/internal/message_set_extensions.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n5google/protobuf/internal/message_set_extensions.proto\x12\x18google.protobuf.internal\"\x1e\n\x0eTestMessageSet*\x08\x08\x04\x10\xff\xff\xff\xff\x07:\x02\x08\x01\"\xa5\x01\n\x18TestMessageSetExtension1\x12\t\n\x01i\x18\x0f \x01(\x05\x32~\n\x15message_set_extension\x12(.google.protobuf.internal.TestMessageSet\x18\xab\xff\xf6. \x01(\x0b\x32\x32.google.protobuf.internal.TestMessageSetExtension1\"\xa7\x01\n\x18TestMessageSetExtension2\x12\x0b\n\x03str\x18\x19 \x01(\t2~\n\x15message_set_extension\x12(.google.protobuf.internal.TestMessageSet\x18\xca\xff\xf6. \x01(\x0b\x32\x32.google.protobuf.internal.TestMessageSetExtension2\"(\n\x18TestMessageSetExtension3\x12\x0c\n\x04text\x18# \x01(\t:\x7f\n\x16message_set_extension3\x12(.google.protobuf.internal.TestMessageSet\x18\xdf\xff\xf6. \x01(\x0b\x32\x32.google.protobuf.internal.TestMessageSetExtension3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.message_set_extensions_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + TestMessageSet.RegisterExtension(message_set_extension3) + TestMessageSet.RegisterExtension(_TESTMESSAGESETEXTENSION1.extensions_by_name['message_set_extension']) + TestMessageSet.RegisterExtension(_TESTMESSAGESETEXTENSION2.extensions_by_name['message_set_extension']) + + DESCRIPTOR._options = None + _TESTMESSAGESET._options = None + _TESTMESSAGESET._serialized_options = b'\010\001' + _TESTMESSAGESET._serialized_start=83 + _TESTMESSAGESET._serialized_end=113 + _TESTMESSAGESETEXTENSION1._serialized_start=116 + _TESTMESSAGESETEXTENSION1._serialized_end=281 + _TESTMESSAGESETEXTENSION2._serialized_start=284 + _TESTMESSAGESETEXTENSION2._serialized_end=451 + _TESTMESSAGESETEXTENSION3._serialized_start=453 + _TESTMESSAGESETEXTENSION3._serialized_end=493 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/internal/missing_enum_values_pb2.py b/scripts/protobuf3/protobuf3/internal/missing_enum_values_pb2.py new file mode 100644 index 0000000..5497083 --- /dev/null +++ b/scripts/protobuf3/protobuf3/internal/missing_enum_values_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/internal/missing_enum_values.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n2google/protobuf/internal/missing_enum_values.proto\x12\x1fgoogle.protobuf.python.internal\"\xc1\x02\n\x0eTestEnumValues\x12X\n\x14optional_nested_enum\x18\x01 \x01(\x0e\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnum\x12X\n\x14repeated_nested_enum\x18\x02 \x03(\x0e\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnum\x12Z\n\x12packed_nested_enum\x18\x03 \x03(\x0e\x32:.google.protobuf.python.internal.TestEnumValues.NestedEnumB\x02\x10\x01\"\x1f\n\nNestedEnum\x12\x08\n\x04ZERO\x10\x00\x12\x07\n\x03ONE\x10\x01\"\xd3\x02\n\x15TestMissingEnumValues\x12_\n\x14optional_nested_enum\x18\x01 \x01(\x0e\x32\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnum\x12_\n\x14repeated_nested_enum\x18\x02 \x03(\x0e\x32\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnum\x12\x61\n\x12packed_nested_enum\x18\x03 \x03(\x0e\x32\x41.google.protobuf.python.internal.TestMissingEnumValues.NestedEnumB\x02\x10\x01\"\x15\n\nNestedEnum\x12\x07\n\x03TWO\x10\x02\"\x1b\n\nJustString\x12\r\n\x05\x64ummy\x18\x01 \x02(\t') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.missing_enum_values_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _TESTENUMVALUES.fields_by_name['packed_nested_enum']._options = None + _TESTENUMVALUES.fields_by_name['packed_nested_enum']._serialized_options = b'\020\001' + _TESTMISSINGENUMVALUES.fields_by_name['packed_nested_enum']._options = None + _TESTMISSINGENUMVALUES.fields_by_name['packed_nested_enum']._serialized_options = b'\020\001' + _TESTENUMVALUES._serialized_start=88 + _TESTENUMVALUES._serialized_end=409 + _TESTENUMVALUES_NESTEDENUM._serialized_start=378 + _TESTENUMVALUES_NESTEDENUM._serialized_end=409 + _TESTMISSINGENUMVALUES._serialized_start=412 + _TESTMISSINGENUMVALUES._serialized_end=751 + _TESTMISSINGENUMVALUES_NESTEDENUM._serialized_start=730 + _TESTMISSINGENUMVALUES_NESTEDENUM._serialized_end=751 + _JUSTSTRING._serialized_start=753 + _JUSTSTRING._serialized_end=780 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/internal/more_extensions_dynamic_pb2.py b/scripts/protobuf3/protobuf3/internal/more_extensions_dynamic_pb2.py new file mode 100644 index 0000000..0953706 --- /dev/null +++ b/scripts/protobuf3/protobuf3/internal/more_extensions_dynamic_pb2.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/internal/more_extensions_dynamic.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf.internal import more_extensions_pb2 as google_dot_protobuf_dot_internal_dot_more__extensions__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n6google/protobuf/internal/more_extensions_dynamic.proto\x12\x18google.protobuf.internal\x1a.google/protobuf/internal/more_extensions.proto\"\x1f\n\x12\x44ynamicMessageType\x12\t\n\x01\x61\x18\x01 \x01(\x05:J\n\x17\x64ynamic_int32_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x64 \x01(\x05:z\n\x19\x64ynamic_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x65 \x01(\x0b\x32,.google.protobuf.internal.DynamicMessageType:\x83\x01\n\"repeated_dynamic_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x66 \x03(\x0b\x32,.google.protobuf.internal.DynamicMessageType') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_extensions_dynamic_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(dynamic_int32_extension) + google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(dynamic_message_extension) + google_dot_protobuf_dot_internal_dot_more__extensions__pb2.ExtendedMessage.RegisterExtension(repeated_dynamic_message_extension) + + DESCRIPTOR._options = None + _DYNAMICMESSAGETYPE._serialized_start=132 + _DYNAMICMESSAGETYPE._serialized_end=163 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/internal/more_extensions_pb2.py b/scripts/protobuf3/protobuf3/internal/more_extensions_pb2.py new file mode 100644 index 0000000..1cfa1b7 --- /dev/null +++ b/scripts/protobuf3/protobuf3/internal/more_extensions_pb2.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/internal/more_extensions.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n.google/protobuf/internal/more_extensions.proto\x12\x18google.protobuf.internal\"\x99\x01\n\x0fTopLevelMessage\x12\x41\n\nsubmessage\x18\x01 \x01(\x0b\x32).google.protobuf.internal.ExtendedMessageB\x02(\x01\x12\x43\n\x0enested_message\x18\x02 \x01(\x0b\x32\'.google.protobuf.internal.NestedMessageB\x02(\x01\"R\n\rNestedMessage\x12\x41\n\nsubmessage\x18\x01 \x01(\x0b\x32).google.protobuf.internal.ExtendedMessageB\x02(\x01\"K\n\x0f\x45xtendedMessage\x12\x17\n\x0eoptional_int32\x18\xe9\x07 \x01(\x05\x12\x18\n\x0frepeated_string\x18\xea\x07 \x03(\t*\x05\x08\x01\x10\xe8\x07\"-\n\x0e\x46oreignMessage\x12\x1b\n\x13\x66oreign_message_int\x18\x01 \x01(\x05:I\n\x16optional_int_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x01 \x01(\x05:w\n\x1aoptional_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x02 \x01(\x0b\x32(.google.protobuf.internal.ForeignMessage:I\n\x16repeated_int_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x03 \x03(\x05:w\n\x1arepeated_message_extension\x12).google.protobuf.internal.ExtendedMessage\x18\x04 \x03(\x0b\x32(.google.protobuf.internal.ForeignMessage') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_extensions_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + ExtendedMessage.RegisterExtension(optional_int_extension) + ExtendedMessage.RegisterExtension(optional_message_extension) + ExtendedMessage.RegisterExtension(repeated_int_extension) + ExtendedMessage.RegisterExtension(repeated_message_extension) + + DESCRIPTOR._options = None + _TOPLEVELMESSAGE.fields_by_name['submessage']._options = None + _TOPLEVELMESSAGE.fields_by_name['submessage']._serialized_options = b'(\001' + _TOPLEVELMESSAGE.fields_by_name['nested_message']._options = None + _TOPLEVELMESSAGE.fields_by_name['nested_message']._serialized_options = b'(\001' + _NESTEDMESSAGE.fields_by_name['submessage']._options = None + _NESTEDMESSAGE.fields_by_name['submessage']._serialized_options = b'(\001' + _TOPLEVELMESSAGE._serialized_start=77 + _TOPLEVELMESSAGE._serialized_end=230 + _NESTEDMESSAGE._serialized_start=232 + _NESTEDMESSAGE._serialized_end=314 + _EXTENDEDMESSAGE._serialized_start=316 + _EXTENDEDMESSAGE._serialized_end=391 + _FOREIGNMESSAGE._serialized_start=393 + _FOREIGNMESSAGE._serialized_end=438 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/internal/more_messages_pb2.py b/scripts/protobuf3/protobuf3/internal/more_messages_pb2.py new file mode 100644 index 0000000..d7f7115 --- /dev/null +++ b/scripts/protobuf3/protobuf3/internal/more_messages_pb2.py @@ -0,0 +1,556 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/internal/more_messages.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,google/protobuf/internal/more_messages.proto\x12\x18google.protobuf.internal\"h\n\x10OutOfOrderFields\x12\x17\n\x0foptional_sint32\x18\x05 \x01(\x11\x12\x17\n\x0foptional_uint32\x18\x03 \x01(\r\x12\x16\n\x0eoptional_int32\x18\x01 \x01(\x05*\x04\x08\x04\x10\x05*\x04\x08\x02\x10\x03\"\xcd\x02\n\x05\x63lass\x12\x1b\n\tint_field\x18\x01 \x01(\x05R\x08json_int\x12\n\n\x02if\x18\x02 \x01(\x05\x12(\n\x02\x61s\x18\x03 \x01(\x0e\x32\x1c.google.protobuf.internal.is\x12\x30\n\nenum_field\x18\x04 \x01(\x0e\x32\x1c.google.protobuf.internal.is\x12>\n\x11nested_enum_field\x18\x05 \x01(\x0e\x32#.google.protobuf.internal.class.for\x12;\n\x0enested_message\x18\x06 \x01(\x0b\x32#.google.protobuf.internal.class.try\x1a\x1c\n\x03try\x12\r\n\x05\x66ield\x18\x01 \x01(\x05*\x06\x08\xe7\x07\x10\x90N\"\x1c\n\x03\x66or\x12\x0b\n\x07\x64\x65\x66\x61ult\x10\x00\x12\x08\n\x04True\x10\x01*\x06\x08\xe7\x07\x10\x90N\"?\n\x0b\x45xtendClass20\n\x06return\x12\x1f.google.protobuf.internal.class\x18\xea\x07 \x01(\x05\"~\n\x0fTestFullKeyword\x12:\n\x06\x66ield1\x18\x01 \x01(\x0b\x32*.google.protobuf.internal.OutOfOrderFields\x12/\n\x06\x66ield2\x18\x02 \x01(\x0b\x32\x1f.google.protobuf.internal.class\"\xa5\x0f\n\x11LotsNestedMessage\x1a\x04\n\x02\x42\x30\x1a\x04\n\x02\x42\x31\x1a\x04\n\x02\x42\x32\x1a\x04\n\x02\x42\x33\x1a\x04\n\x02\x42\x34\x1a\x04\n\x02\x42\x35\x1a\x04\n\x02\x42\x36\x1a\x04\n\x02\x42\x37\x1a\x04\n\x02\x42\x38\x1a\x04\n\x02\x42\x39\x1a\x05\n\x03\x42\x31\x30\x1a\x05\n\x03\x42\x31\x31\x1a\x05\n\x03\x42\x31\x32\x1a\x05\n\x03\x42\x31\x33\x1a\x05\n\x03\x42\x31\x34\x1a\x05\n\x03\x42\x31\x35\x1a\x05\n\x03\x42\x31\x36\x1a\x05\n\x03\x42\x31\x37\x1a\x05\n\x03\x42\x31\x38\x1a\x05\n\x03\x42\x31\x39\x1a\x05\n\x03\x42\x32\x30\x1a\x05\n\x03\x42\x32\x31\x1a\x05\n\x03\x42\x32\x32\x1a\x05\n\x03\x42\x32\x33\x1a\x05\n\x03\x42\x32\x34\x1a\x05\n\x03\x42\x32\x35\x1a\x05\n\x03\x42\x32\x36\x1a\x05\n\x03\x42\x32\x37\x1a\x05\n\x03\x42\x32\x38\x1a\x05\n\x03\x42\x32\x39\x1a\x05\n\x03\x42\x33\x30\x1a\x05\n\x03\x42\x33\x31\x1a\x05\n\x03\x42\x33\x32\x1a\x05\n\x03\x42\x33\x33\x1a\x05\n\x03\x42\x33\x34\x1a\x05\n\x03\x42\x33\x35\x1a\x05\n\x03\x42\x33\x36\x1a\x05\n\x03\x42\x33\x37\x1a\x05\n\x03\x42\x33\x38\x1a\x05\n\x03\x42\x33\x39\x1a\x05\n\x03\x42\x34\x30\x1a\x05\n\x03\x42\x34\x31\x1a\x05\n\x03\x42\x34\x32\x1a\x05\n\x03\x42\x34\x33\x1a\x05\n\x03\x42\x34\x34\x1a\x05\n\x03\x42\x34\x35\x1a\x05\n\x03\x42\x34\x36\x1a\x05\n\x03\x42\x34\x37\x1a\x05\n\x03\x42\x34\x38\x1a\x05\n\x03\x42\x34\x39\x1a\x05\n\x03\x42\x35\x30\x1a\x05\n\x03\x42\x35\x31\x1a\x05\n\x03\x42\x35\x32\x1a\x05\n\x03\x42\x35\x33\x1a\x05\n\x03\x42\x35\x34\x1a\x05\n\x03\x42\x35\x35\x1a\x05\n\x03\x42\x35\x36\x1a\x05\n\x03\x42\x35\x37\x1a\x05\n\x03\x42\x35\x38\x1a\x05\n\x03\x42\x35\x39\x1a\x05\n\x03\x42\x36\x30\x1a\x05\n\x03\x42\x36\x31\x1a\x05\n\x03\x42\x36\x32\x1a\x05\n\x03\x42\x36\x33\x1a\x05\n\x03\x42\x36\x34\x1a\x05\n\x03\x42\x36\x35\x1a\x05\n\x03\x42\x36\x36\x1a\x05\n\x03\x42\x36\x37\x1a\x05\n\x03\x42\x36\x38\x1a\x05\n\x03\x42\x36\x39\x1a\x05\n\x03\x42\x37\x30\x1a\x05\n\x03\x42\x37\x31\x1a\x05\n\x03\x42\x37\x32\x1a\x05\n\x03\x42\x37\x33\x1a\x05\n\x03\x42\x37\x34\x1a\x05\n\x03\x42\x37\x35\x1a\x05\n\x03\x42\x37\x36\x1a\x05\n\x03\x42\x37\x37\x1a\x05\n\x03\x42\x37\x38\x1a\x05\n\x03\x42\x37\x39\x1a\x05\n\x03\x42\x38\x30\x1a\x05\n\x03\x42\x38\x31\x1a\x05\n\x03\x42\x38\x32\x1a\x05\n\x03\x42\x38\x33\x1a\x05\n\x03\x42\x38\x34\x1a\x05\n\x03\x42\x38\x35\x1a\x05\n\x03\x42\x38\x36\x1a\x05\n\x03\x42\x38\x37\x1a\x05\n\x03\x42\x38\x38\x1a\x05\n\x03\x42\x38\x39\x1a\x05\n\x03\x42\x39\x30\x1a\x05\n\x03\x42\x39\x31\x1a\x05\n\x03\x42\x39\x32\x1a\x05\n\x03\x42\x39\x33\x1a\x05\n\x03\x42\x39\x34\x1a\x05\n\x03\x42\x39\x35\x1a\x05\n\x03\x42\x39\x36\x1a\x05\n\x03\x42\x39\x37\x1a\x05\n\x03\x42\x39\x38\x1a\x05\n\x03\x42\x39\x39\x1a\x06\n\x04\x42\x31\x30\x30\x1a\x06\n\x04\x42\x31\x30\x31\x1a\x06\n\x04\x42\x31\x30\x32\x1a\x06\n\x04\x42\x31\x30\x33\x1a\x06\n\x04\x42\x31\x30\x34\x1a\x06\n\x04\x42\x31\x30\x35\x1a\x06\n\x04\x42\x31\x30\x36\x1a\x06\n\x04\x42\x31\x30\x37\x1a\x06\n\x04\x42\x31\x30\x38\x1a\x06\n\x04\x42\x31\x30\x39\x1a\x06\n\x04\x42\x31\x31\x30\x1a\x06\n\x04\x42\x31\x31\x31\x1a\x06\n\x04\x42\x31\x31\x32\x1a\x06\n\x04\x42\x31\x31\x33\x1a\x06\n\x04\x42\x31\x31\x34\x1a\x06\n\x04\x42\x31\x31\x35\x1a\x06\n\x04\x42\x31\x31\x36\x1a\x06\n\x04\x42\x31\x31\x37\x1a\x06\n\x04\x42\x31\x31\x38\x1a\x06\n\x04\x42\x31\x31\x39\x1a\x06\n\x04\x42\x31\x32\x30\x1a\x06\n\x04\x42\x31\x32\x31\x1a\x06\n\x04\x42\x31\x32\x32\x1a\x06\n\x04\x42\x31\x32\x33\x1a\x06\n\x04\x42\x31\x32\x34\x1a\x06\n\x04\x42\x31\x32\x35\x1a\x06\n\x04\x42\x31\x32\x36\x1a\x06\n\x04\x42\x31\x32\x37\x1a\x06\n\x04\x42\x31\x32\x38\x1a\x06\n\x04\x42\x31\x32\x39\x1a\x06\n\x04\x42\x31\x33\x30\x1a\x06\n\x04\x42\x31\x33\x31\x1a\x06\n\x04\x42\x31\x33\x32\x1a\x06\n\x04\x42\x31\x33\x33\x1a\x06\n\x04\x42\x31\x33\x34\x1a\x06\n\x04\x42\x31\x33\x35\x1a\x06\n\x04\x42\x31\x33\x36\x1a\x06\n\x04\x42\x31\x33\x37\x1a\x06\n\x04\x42\x31\x33\x38\x1a\x06\n\x04\x42\x31\x33\x39\x1a\x06\n\x04\x42\x31\x34\x30\x1a\x06\n\x04\x42\x31\x34\x31\x1a\x06\n\x04\x42\x31\x34\x32\x1a\x06\n\x04\x42\x31\x34\x33\x1a\x06\n\x04\x42\x31\x34\x34\x1a\x06\n\x04\x42\x31\x34\x35\x1a\x06\n\x04\x42\x31\x34\x36\x1a\x06\n\x04\x42\x31\x34\x37\x1a\x06\n\x04\x42\x31\x34\x38\x1a\x06\n\x04\x42\x31\x34\x39\x1a\x06\n\x04\x42\x31\x35\x30\x1a\x06\n\x04\x42\x31\x35\x31\x1a\x06\n\x04\x42\x31\x35\x32\x1a\x06\n\x04\x42\x31\x35\x33\x1a\x06\n\x04\x42\x31\x35\x34\x1a\x06\n\x04\x42\x31\x35\x35\x1a\x06\n\x04\x42\x31\x35\x36\x1a\x06\n\x04\x42\x31\x35\x37\x1a\x06\n\x04\x42\x31\x35\x38\x1a\x06\n\x04\x42\x31\x35\x39\x1a\x06\n\x04\x42\x31\x36\x30\x1a\x06\n\x04\x42\x31\x36\x31\x1a\x06\n\x04\x42\x31\x36\x32\x1a\x06\n\x04\x42\x31\x36\x33\x1a\x06\n\x04\x42\x31\x36\x34\x1a\x06\n\x04\x42\x31\x36\x35\x1a\x06\n\x04\x42\x31\x36\x36\x1a\x06\n\x04\x42\x31\x36\x37\x1a\x06\n\x04\x42\x31\x36\x38\x1a\x06\n\x04\x42\x31\x36\x39\x1a\x06\n\x04\x42\x31\x37\x30\x1a\x06\n\x04\x42\x31\x37\x31\x1a\x06\n\x04\x42\x31\x37\x32\x1a\x06\n\x04\x42\x31\x37\x33\x1a\x06\n\x04\x42\x31\x37\x34\x1a\x06\n\x04\x42\x31\x37\x35\x1a\x06\n\x04\x42\x31\x37\x36\x1a\x06\n\x04\x42\x31\x37\x37\x1a\x06\n\x04\x42\x31\x37\x38\x1a\x06\n\x04\x42\x31\x37\x39\x1a\x06\n\x04\x42\x31\x38\x30\x1a\x06\n\x04\x42\x31\x38\x31\x1a\x06\n\x04\x42\x31\x38\x32\x1a\x06\n\x04\x42\x31\x38\x33\x1a\x06\n\x04\x42\x31\x38\x34\x1a\x06\n\x04\x42\x31\x38\x35\x1a\x06\n\x04\x42\x31\x38\x36\x1a\x06\n\x04\x42\x31\x38\x37\x1a\x06\n\x04\x42\x31\x38\x38\x1a\x06\n\x04\x42\x31\x38\x39\x1a\x06\n\x04\x42\x31\x39\x30\x1a\x06\n\x04\x42\x31\x39\x31\x1a\x06\n\x04\x42\x31\x39\x32\x1a\x06\n\x04\x42\x31\x39\x33\x1a\x06\n\x04\x42\x31\x39\x34\x1a\x06\n\x04\x42\x31\x39\x35\x1a\x06\n\x04\x42\x31\x39\x36\x1a\x06\n\x04\x42\x31\x39\x37\x1a\x06\n\x04\x42\x31\x39\x38\x1a\x06\n\x04\x42\x31\x39\x39\x1a\x06\n\x04\x42\x32\x30\x30\x1a\x06\n\x04\x42\x32\x30\x31\x1a\x06\n\x04\x42\x32\x30\x32\x1a\x06\n\x04\x42\x32\x30\x33\x1a\x06\n\x04\x42\x32\x30\x34\x1a\x06\n\x04\x42\x32\x30\x35\x1a\x06\n\x04\x42\x32\x30\x36\x1a\x06\n\x04\x42\x32\x30\x37\x1a\x06\n\x04\x42\x32\x30\x38\x1a\x06\n\x04\x42\x32\x30\x39\x1a\x06\n\x04\x42\x32\x31\x30\x1a\x06\n\x04\x42\x32\x31\x31\x1a\x06\n\x04\x42\x32\x31\x32\x1a\x06\n\x04\x42\x32\x31\x33\x1a\x06\n\x04\x42\x32\x31\x34\x1a\x06\n\x04\x42\x32\x31\x35\x1a\x06\n\x04\x42\x32\x31\x36\x1a\x06\n\x04\x42\x32\x31\x37\x1a\x06\n\x04\x42\x32\x31\x38\x1a\x06\n\x04\x42\x32\x31\x39\x1a\x06\n\x04\x42\x32\x32\x30\x1a\x06\n\x04\x42\x32\x32\x31\x1a\x06\n\x04\x42\x32\x32\x32\x1a\x06\n\x04\x42\x32\x32\x33\x1a\x06\n\x04\x42\x32\x32\x34\x1a\x06\n\x04\x42\x32\x32\x35\x1a\x06\n\x04\x42\x32\x32\x36\x1a\x06\n\x04\x42\x32\x32\x37\x1a\x06\n\x04\x42\x32\x32\x38\x1a\x06\n\x04\x42\x32\x32\x39\x1a\x06\n\x04\x42\x32\x33\x30\x1a\x06\n\x04\x42\x32\x33\x31\x1a\x06\n\x04\x42\x32\x33\x32\x1a\x06\n\x04\x42\x32\x33\x33\x1a\x06\n\x04\x42\x32\x33\x34\x1a\x06\n\x04\x42\x32\x33\x35\x1a\x06\n\x04\x42\x32\x33\x36\x1a\x06\n\x04\x42\x32\x33\x37\x1a\x06\n\x04\x42\x32\x33\x38\x1a\x06\n\x04\x42\x32\x33\x39\x1a\x06\n\x04\x42\x32\x34\x30\x1a\x06\n\x04\x42\x32\x34\x31\x1a\x06\n\x04\x42\x32\x34\x32\x1a\x06\n\x04\x42\x32\x34\x33\x1a\x06\n\x04\x42\x32\x34\x34\x1a\x06\n\x04\x42\x32\x34\x35\x1a\x06\n\x04\x42\x32\x34\x36\x1a\x06\n\x04\x42\x32\x34\x37\x1a\x06\n\x04\x42\x32\x34\x38\x1a\x06\n\x04\x42\x32\x34\x39\x1a\x06\n\x04\x42\x32\x35\x30\x1a\x06\n\x04\x42\x32\x35\x31\x1a\x06\n\x04\x42\x32\x35\x32\x1a\x06\n\x04\x42\x32\x35\x33\x1a\x06\n\x04\x42\x32\x35\x34\x1a\x06\n\x04\x42\x32\x35\x35*\x1b\n\x02is\x12\x0b\n\x07\x64\x65\x66\x61ult\x10\x00\x12\x08\n\x04\x65lse\x10\x01:C\n\x0foptional_uint64\x12*.google.protobuf.internal.OutOfOrderFields\x18\x04 \x01(\x04:B\n\x0eoptional_int64\x12*.google.protobuf.internal.OutOfOrderFields\x18\x02 \x01(\x03:2\n\x08\x63ontinue\x12\x1f.google.protobuf.internal.class\x18\xe9\x07 \x01(\x05:2\n\x04with\x12#.google.protobuf.internal.class.try\x18\xe9\x07 \x01(\x05') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.more_messages_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + OutOfOrderFields.RegisterExtension(optional_uint64) + OutOfOrderFields.RegisterExtension(optional_int64) + globals()['class'].RegisterExtension(globals()['continue']) + getattr(globals()['class'], 'try').RegisterExtension(globals()['with']) + globals()['class'].RegisterExtension(_EXTENDCLASS.extensions_by_name['return']) + + DESCRIPTOR._options = None + _IS._serialized_start=2669 + _IS._serialized_end=2696 + _OUTOFORDERFIELDS._serialized_start=74 + _OUTOFORDERFIELDS._serialized_end=178 + _CLASS._serialized_start=181 + _CLASS._serialized_end=514 + _CLASS_TRY._serialized_start=448 + _CLASS_TRY._serialized_end=476 + _CLASS_FOR._serialized_start=478 + _CLASS_FOR._serialized_end=506 + _EXTENDCLASS._serialized_start=516 + _EXTENDCLASS._serialized_end=579 + _TESTFULLKEYWORD._serialized_start=581 + _TESTFULLKEYWORD._serialized_end=707 + _LOTSNESTEDMESSAGE._serialized_start=710 + _LOTSNESTEDMESSAGE._serialized_end=2667 + _LOTSNESTEDMESSAGE_B0._serialized_start=731 + _LOTSNESTEDMESSAGE_B0._serialized_end=735 + _LOTSNESTEDMESSAGE_B1._serialized_start=737 + _LOTSNESTEDMESSAGE_B1._serialized_end=741 + _LOTSNESTEDMESSAGE_B2._serialized_start=743 + _LOTSNESTEDMESSAGE_B2._serialized_end=747 + _LOTSNESTEDMESSAGE_B3._serialized_start=749 + _LOTSNESTEDMESSAGE_B3._serialized_end=753 + _LOTSNESTEDMESSAGE_B4._serialized_start=755 + _LOTSNESTEDMESSAGE_B4._serialized_end=759 + _LOTSNESTEDMESSAGE_B5._serialized_start=761 + _LOTSNESTEDMESSAGE_B5._serialized_end=765 + _LOTSNESTEDMESSAGE_B6._serialized_start=767 + _LOTSNESTEDMESSAGE_B6._serialized_end=771 + _LOTSNESTEDMESSAGE_B7._serialized_start=773 + _LOTSNESTEDMESSAGE_B7._serialized_end=777 + _LOTSNESTEDMESSAGE_B8._serialized_start=779 + _LOTSNESTEDMESSAGE_B8._serialized_end=783 + _LOTSNESTEDMESSAGE_B9._serialized_start=785 + _LOTSNESTEDMESSAGE_B9._serialized_end=789 + _LOTSNESTEDMESSAGE_B10._serialized_start=791 + _LOTSNESTEDMESSAGE_B10._serialized_end=796 + _LOTSNESTEDMESSAGE_B11._serialized_start=798 + _LOTSNESTEDMESSAGE_B11._serialized_end=803 + _LOTSNESTEDMESSAGE_B12._serialized_start=805 + _LOTSNESTEDMESSAGE_B12._serialized_end=810 + _LOTSNESTEDMESSAGE_B13._serialized_start=812 + _LOTSNESTEDMESSAGE_B13._serialized_end=817 + _LOTSNESTEDMESSAGE_B14._serialized_start=819 + _LOTSNESTEDMESSAGE_B14._serialized_end=824 + _LOTSNESTEDMESSAGE_B15._serialized_start=826 + _LOTSNESTEDMESSAGE_B15._serialized_end=831 + _LOTSNESTEDMESSAGE_B16._serialized_start=833 + _LOTSNESTEDMESSAGE_B16._serialized_end=838 + _LOTSNESTEDMESSAGE_B17._serialized_start=840 + _LOTSNESTEDMESSAGE_B17._serialized_end=845 + _LOTSNESTEDMESSAGE_B18._serialized_start=847 + _LOTSNESTEDMESSAGE_B18._serialized_end=852 + _LOTSNESTEDMESSAGE_B19._serialized_start=854 + _LOTSNESTEDMESSAGE_B19._serialized_end=859 + _LOTSNESTEDMESSAGE_B20._serialized_start=861 + _LOTSNESTEDMESSAGE_B20._serialized_end=866 + _LOTSNESTEDMESSAGE_B21._serialized_start=868 + _LOTSNESTEDMESSAGE_B21._serialized_end=873 + _LOTSNESTEDMESSAGE_B22._serialized_start=875 + _LOTSNESTEDMESSAGE_B22._serialized_end=880 + _LOTSNESTEDMESSAGE_B23._serialized_start=882 + _LOTSNESTEDMESSAGE_B23._serialized_end=887 + _LOTSNESTEDMESSAGE_B24._serialized_start=889 + _LOTSNESTEDMESSAGE_B24._serialized_end=894 + _LOTSNESTEDMESSAGE_B25._serialized_start=896 + _LOTSNESTEDMESSAGE_B25._serialized_end=901 + _LOTSNESTEDMESSAGE_B26._serialized_start=903 + _LOTSNESTEDMESSAGE_B26._serialized_end=908 + _LOTSNESTEDMESSAGE_B27._serialized_start=910 + _LOTSNESTEDMESSAGE_B27._serialized_end=915 + _LOTSNESTEDMESSAGE_B28._serialized_start=917 + _LOTSNESTEDMESSAGE_B28._serialized_end=922 + _LOTSNESTEDMESSAGE_B29._serialized_start=924 + _LOTSNESTEDMESSAGE_B29._serialized_end=929 + _LOTSNESTEDMESSAGE_B30._serialized_start=931 + _LOTSNESTEDMESSAGE_B30._serialized_end=936 + _LOTSNESTEDMESSAGE_B31._serialized_start=938 + _LOTSNESTEDMESSAGE_B31._serialized_end=943 + _LOTSNESTEDMESSAGE_B32._serialized_start=945 + _LOTSNESTEDMESSAGE_B32._serialized_end=950 + _LOTSNESTEDMESSAGE_B33._serialized_start=952 + _LOTSNESTEDMESSAGE_B33._serialized_end=957 + _LOTSNESTEDMESSAGE_B34._serialized_start=959 + _LOTSNESTEDMESSAGE_B34._serialized_end=964 + _LOTSNESTEDMESSAGE_B35._serialized_start=966 + _LOTSNESTEDMESSAGE_B35._serialized_end=971 + _LOTSNESTEDMESSAGE_B36._serialized_start=973 + _LOTSNESTEDMESSAGE_B36._serialized_end=978 + _LOTSNESTEDMESSAGE_B37._serialized_start=980 + _LOTSNESTEDMESSAGE_B37._serialized_end=985 + _LOTSNESTEDMESSAGE_B38._serialized_start=987 + _LOTSNESTEDMESSAGE_B38._serialized_end=992 + _LOTSNESTEDMESSAGE_B39._serialized_start=994 + _LOTSNESTEDMESSAGE_B39._serialized_end=999 + _LOTSNESTEDMESSAGE_B40._serialized_start=1001 + _LOTSNESTEDMESSAGE_B40._serialized_end=1006 + _LOTSNESTEDMESSAGE_B41._serialized_start=1008 + _LOTSNESTEDMESSAGE_B41._serialized_end=1013 + _LOTSNESTEDMESSAGE_B42._serialized_start=1015 + _LOTSNESTEDMESSAGE_B42._serialized_end=1020 + _LOTSNESTEDMESSAGE_B43._serialized_start=1022 + _LOTSNESTEDMESSAGE_B43._serialized_end=1027 + _LOTSNESTEDMESSAGE_B44._serialized_start=1029 + _LOTSNESTEDMESSAGE_B44._serialized_end=1034 + _LOTSNESTEDMESSAGE_B45._serialized_start=1036 + _LOTSNESTEDMESSAGE_B45._serialized_end=1041 + _LOTSNESTEDMESSAGE_B46._serialized_start=1043 + _LOTSNESTEDMESSAGE_B46._serialized_end=1048 + _LOTSNESTEDMESSAGE_B47._serialized_start=1050 + _LOTSNESTEDMESSAGE_B47._serialized_end=1055 + _LOTSNESTEDMESSAGE_B48._serialized_start=1057 + _LOTSNESTEDMESSAGE_B48._serialized_end=1062 + _LOTSNESTEDMESSAGE_B49._serialized_start=1064 + _LOTSNESTEDMESSAGE_B49._serialized_end=1069 + _LOTSNESTEDMESSAGE_B50._serialized_start=1071 + _LOTSNESTEDMESSAGE_B50._serialized_end=1076 + _LOTSNESTEDMESSAGE_B51._serialized_start=1078 + _LOTSNESTEDMESSAGE_B51._serialized_end=1083 + _LOTSNESTEDMESSAGE_B52._serialized_start=1085 + _LOTSNESTEDMESSAGE_B52._serialized_end=1090 + _LOTSNESTEDMESSAGE_B53._serialized_start=1092 + _LOTSNESTEDMESSAGE_B53._serialized_end=1097 + _LOTSNESTEDMESSAGE_B54._serialized_start=1099 + _LOTSNESTEDMESSAGE_B54._serialized_end=1104 + _LOTSNESTEDMESSAGE_B55._serialized_start=1106 + _LOTSNESTEDMESSAGE_B55._serialized_end=1111 + _LOTSNESTEDMESSAGE_B56._serialized_start=1113 + _LOTSNESTEDMESSAGE_B56._serialized_end=1118 + _LOTSNESTEDMESSAGE_B57._serialized_start=1120 + _LOTSNESTEDMESSAGE_B57._serialized_end=1125 + _LOTSNESTEDMESSAGE_B58._serialized_start=1127 + _LOTSNESTEDMESSAGE_B58._serialized_end=1132 + _LOTSNESTEDMESSAGE_B59._serialized_start=1134 + _LOTSNESTEDMESSAGE_B59._serialized_end=1139 + _LOTSNESTEDMESSAGE_B60._serialized_start=1141 + _LOTSNESTEDMESSAGE_B60._serialized_end=1146 + _LOTSNESTEDMESSAGE_B61._serialized_start=1148 + _LOTSNESTEDMESSAGE_B61._serialized_end=1153 + _LOTSNESTEDMESSAGE_B62._serialized_start=1155 + _LOTSNESTEDMESSAGE_B62._serialized_end=1160 + _LOTSNESTEDMESSAGE_B63._serialized_start=1162 + _LOTSNESTEDMESSAGE_B63._serialized_end=1167 + _LOTSNESTEDMESSAGE_B64._serialized_start=1169 + _LOTSNESTEDMESSAGE_B64._serialized_end=1174 + _LOTSNESTEDMESSAGE_B65._serialized_start=1176 + _LOTSNESTEDMESSAGE_B65._serialized_end=1181 + _LOTSNESTEDMESSAGE_B66._serialized_start=1183 + _LOTSNESTEDMESSAGE_B66._serialized_end=1188 + _LOTSNESTEDMESSAGE_B67._serialized_start=1190 + _LOTSNESTEDMESSAGE_B67._serialized_end=1195 + _LOTSNESTEDMESSAGE_B68._serialized_start=1197 + _LOTSNESTEDMESSAGE_B68._serialized_end=1202 + _LOTSNESTEDMESSAGE_B69._serialized_start=1204 + _LOTSNESTEDMESSAGE_B69._serialized_end=1209 + _LOTSNESTEDMESSAGE_B70._serialized_start=1211 + _LOTSNESTEDMESSAGE_B70._serialized_end=1216 + _LOTSNESTEDMESSAGE_B71._serialized_start=1218 + _LOTSNESTEDMESSAGE_B71._serialized_end=1223 + _LOTSNESTEDMESSAGE_B72._serialized_start=1225 + _LOTSNESTEDMESSAGE_B72._serialized_end=1230 + _LOTSNESTEDMESSAGE_B73._serialized_start=1232 + _LOTSNESTEDMESSAGE_B73._serialized_end=1237 + _LOTSNESTEDMESSAGE_B74._serialized_start=1239 + _LOTSNESTEDMESSAGE_B74._serialized_end=1244 + _LOTSNESTEDMESSAGE_B75._serialized_start=1246 + _LOTSNESTEDMESSAGE_B75._serialized_end=1251 + _LOTSNESTEDMESSAGE_B76._serialized_start=1253 + _LOTSNESTEDMESSAGE_B76._serialized_end=1258 + _LOTSNESTEDMESSAGE_B77._serialized_start=1260 + _LOTSNESTEDMESSAGE_B77._serialized_end=1265 + _LOTSNESTEDMESSAGE_B78._serialized_start=1267 + _LOTSNESTEDMESSAGE_B78._serialized_end=1272 + _LOTSNESTEDMESSAGE_B79._serialized_start=1274 + _LOTSNESTEDMESSAGE_B79._serialized_end=1279 + _LOTSNESTEDMESSAGE_B80._serialized_start=1281 + _LOTSNESTEDMESSAGE_B80._serialized_end=1286 + _LOTSNESTEDMESSAGE_B81._serialized_start=1288 + _LOTSNESTEDMESSAGE_B81._serialized_end=1293 + _LOTSNESTEDMESSAGE_B82._serialized_start=1295 + _LOTSNESTEDMESSAGE_B82._serialized_end=1300 + _LOTSNESTEDMESSAGE_B83._serialized_start=1302 + _LOTSNESTEDMESSAGE_B83._serialized_end=1307 + _LOTSNESTEDMESSAGE_B84._serialized_start=1309 + _LOTSNESTEDMESSAGE_B84._serialized_end=1314 + _LOTSNESTEDMESSAGE_B85._serialized_start=1316 + _LOTSNESTEDMESSAGE_B85._serialized_end=1321 + _LOTSNESTEDMESSAGE_B86._serialized_start=1323 + _LOTSNESTEDMESSAGE_B86._serialized_end=1328 + _LOTSNESTEDMESSAGE_B87._serialized_start=1330 + _LOTSNESTEDMESSAGE_B87._serialized_end=1335 + _LOTSNESTEDMESSAGE_B88._serialized_start=1337 + _LOTSNESTEDMESSAGE_B88._serialized_end=1342 + _LOTSNESTEDMESSAGE_B89._serialized_start=1344 + _LOTSNESTEDMESSAGE_B89._serialized_end=1349 + _LOTSNESTEDMESSAGE_B90._serialized_start=1351 + _LOTSNESTEDMESSAGE_B90._serialized_end=1356 + _LOTSNESTEDMESSAGE_B91._serialized_start=1358 + _LOTSNESTEDMESSAGE_B91._serialized_end=1363 + _LOTSNESTEDMESSAGE_B92._serialized_start=1365 + _LOTSNESTEDMESSAGE_B92._serialized_end=1370 + _LOTSNESTEDMESSAGE_B93._serialized_start=1372 + _LOTSNESTEDMESSAGE_B93._serialized_end=1377 + _LOTSNESTEDMESSAGE_B94._serialized_start=1379 + _LOTSNESTEDMESSAGE_B94._serialized_end=1384 + _LOTSNESTEDMESSAGE_B95._serialized_start=1386 + _LOTSNESTEDMESSAGE_B95._serialized_end=1391 + _LOTSNESTEDMESSAGE_B96._serialized_start=1393 + _LOTSNESTEDMESSAGE_B96._serialized_end=1398 + _LOTSNESTEDMESSAGE_B97._serialized_start=1400 + _LOTSNESTEDMESSAGE_B97._serialized_end=1405 + _LOTSNESTEDMESSAGE_B98._serialized_start=1407 + _LOTSNESTEDMESSAGE_B98._serialized_end=1412 + _LOTSNESTEDMESSAGE_B99._serialized_start=1414 + _LOTSNESTEDMESSAGE_B99._serialized_end=1419 + _LOTSNESTEDMESSAGE_B100._serialized_start=1421 + _LOTSNESTEDMESSAGE_B100._serialized_end=1427 + _LOTSNESTEDMESSAGE_B101._serialized_start=1429 + _LOTSNESTEDMESSAGE_B101._serialized_end=1435 + _LOTSNESTEDMESSAGE_B102._serialized_start=1437 + _LOTSNESTEDMESSAGE_B102._serialized_end=1443 + _LOTSNESTEDMESSAGE_B103._serialized_start=1445 + _LOTSNESTEDMESSAGE_B103._serialized_end=1451 + _LOTSNESTEDMESSAGE_B104._serialized_start=1453 + _LOTSNESTEDMESSAGE_B104._serialized_end=1459 + _LOTSNESTEDMESSAGE_B105._serialized_start=1461 + _LOTSNESTEDMESSAGE_B105._serialized_end=1467 + _LOTSNESTEDMESSAGE_B106._serialized_start=1469 + _LOTSNESTEDMESSAGE_B106._serialized_end=1475 + _LOTSNESTEDMESSAGE_B107._serialized_start=1477 + _LOTSNESTEDMESSAGE_B107._serialized_end=1483 + _LOTSNESTEDMESSAGE_B108._serialized_start=1485 + _LOTSNESTEDMESSAGE_B108._serialized_end=1491 + _LOTSNESTEDMESSAGE_B109._serialized_start=1493 + _LOTSNESTEDMESSAGE_B109._serialized_end=1499 + _LOTSNESTEDMESSAGE_B110._serialized_start=1501 + _LOTSNESTEDMESSAGE_B110._serialized_end=1507 + _LOTSNESTEDMESSAGE_B111._serialized_start=1509 + _LOTSNESTEDMESSAGE_B111._serialized_end=1515 + _LOTSNESTEDMESSAGE_B112._serialized_start=1517 + _LOTSNESTEDMESSAGE_B112._serialized_end=1523 + _LOTSNESTEDMESSAGE_B113._serialized_start=1525 + _LOTSNESTEDMESSAGE_B113._serialized_end=1531 + _LOTSNESTEDMESSAGE_B114._serialized_start=1533 + _LOTSNESTEDMESSAGE_B114._serialized_end=1539 + _LOTSNESTEDMESSAGE_B115._serialized_start=1541 + _LOTSNESTEDMESSAGE_B115._serialized_end=1547 + _LOTSNESTEDMESSAGE_B116._serialized_start=1549 + _LOTSNESTEDMESSAGE_B116._serialized_end=1555 + _LOTSNESTEDMESSAGE_B117._serialized_start=1557 + _LOTSNESTEDMESSAGE_B117._serialized_end=1563 + _LOTSNESTEDMESSAGE_B118._serialized_start=1565 + _LOTSNESTEDMESSAGE_B118._serialized_end=1571 + _LOTSNESTEDMESSAGE_B119._serialized_start=1573 + _LOTSNESTEDMESSAGE_B119._serialized_end=1579 + _LOTSNESTEDMESSAGE_B120._serialized_start=1581 + _LOTSNESTEDMESSAGE_B120._serialized_end=1587 + _LOTSNESTEDMESSAGE_B121._serialized_start=1589 + _LOTSNESTEDMESSAGE_B121._serialized_end=1595 + _LOTSNESTEDMESSAGE_B122._serialized_start=1597 + _LOTSNESTEDMESSAGE_B122._serialized_end=1603 + _LOTSNESTEDMESSAGE_B123._serialized_start=1605 + _LOTSNESTEDMESSAGE_B123._serialized_end=1611 + _LOTSNESTEDMESSAGE_B124._serialized_start=1613 + _LOTSNESTEDMESSAGE_B124._serialized_end=1619 + _LOTSNESTEDMESSAGE_B125._serialized_start=1621 + _LOTSNESTEDMESSAGE_B125._serialized_end=1627 + _LOTSNESTEDMESSAGE_B126._serialized_start=1629 + _LOTSNESTEDMESSAGE_B126._serialized_end=1635 + _LOTSNESTEDMESSAGE_B127._serialized_start=1637 + _LOTSNESTEDMESSAGE_B127._serialized_end=1643 + _LOTSNESTEDMESSAGE_B128._serialized_start=1645 + _LOTSNESTEDMESSAGE_B128._serialized_end=1651 + _LOTSNESTEDMESSAGE_B129._serialized_start=1653 + _LOTSNESTEDMESSAGE_B129._serialized_end=1659 + _LOTSNESTEDMESSAGE_B130._serialized_start=1661 + _LOTSNESTEDMESSAGE_B130._serialized_end=1667 + _LOTSNESTEDMESSAGE_B131._serialized_start=1669 + _LOTSNESTEDMESSAGE_B131._serialized_end=1675 + _LOTSNESTEDMESSAGE_B132._serialized_start=1677 + _LOTSNESTEDMESSAGE_B132._serialized_end=1683 + _LOTSNESTEDMESSAGE_B133._serialized_start=1685 + _LOTSNESTEDMESSAGE_B133._serialized_end=1691 + _LOTSNESTEDMESSAGE_B134._serialized_start=1693 + _LOTSNESTEDMESSAGE_B134._serialized_end=1699 + _LOTSNESTEDMESSAGE_B135._serialized_start=1701 + _LOTSNESTEDMESSAGE_B135._serialized_end=1707 + _LOTSNESTEDMESSAGE_B136._serialized_start=1709 + _LOTSNESTEDMESSAGE_B136._serialized_end=1715 + _LOTSNESTEDMESSAGE_B137._serialized_start=1717 + _LOTSNESTEDMESSAGE_B137._serialized_end=1723 + _LOTSNESTEDMESSAGE_B138._serialized_start=1725 + _LOTSNESTEDMESSAGE_B138._serialized_end=1731 + _LOTSNESTEDMESSAGE_B139._serialized_start=1733 + _LOTSNESTEDMESSAGE_B139._serialized_end=1739 + _LOTSNESTEDMESSAGE_B140._serialized_start=1741 + _LOTSNESTEDMESSAGE_B140._serialized_end=1747 + _LOTSNESTEDMESSAGE_B141._serialized_start=1749 + _LOTSNESTEDMESSAGE_B141._serialized_end=1755 + _LOTSNESTEDMESSAGE_B142._serialized_start=1757 + _LOTSNESTEDMESSAGE_B142._serialized_end=1763 + _LOTSNESTEDMESSAGE_B143._serialized_start=1765 + _LOTSNESTEDMESSAGE_B143._serialized_end=1771 + _LOTSNESTEDMESSAGE_B144._serialized_start=1773 + _LOTSNESTEDMESSAGE_B144._serialized_end=1779 + _LOTSNESTEDMESSAGE_B145._serialized_start=1781 + _LOTSNESTEDMESSAGE_B145._serialized_end=1787 + _LOTSNESTEDMESSAGE_B146._serialized_start=1789 + _LOTSNESTEDMESSAGE_B146._serialized_end=1795 + _LOTSNESTEDMESSAGE_B147._serialized_start=1797 + _LOTSNESTEDMESSAGE_B147._serialized_end=1803 + _LOTSNESTEDMESSAGE_B148._serialized_start=1805 + _LOTSNESTEDMESSAGE_B148._serialized_end=1811 + _LOTSNESTEDMESSAGE_B149._serialized_start=1813 + _LOTSNESTEDMESSAGE_B149._serialized_end=1819 + _LOTSNESTEDMESSAGE_B150._serialized_start=1821 + _LOTSNESTEDMESSAGE_B150._serialized_end=1827 + _LOTSNESTEDMESSAGE_B151._serialized_start=1829 + _LOTSNESTEDMESSAGE_B151._serialized_end=1835 + _LOTSNESTEDMESSAGE_B152._serialized_start=1837 + _LOTSNESTEDMESSAGE_B152._serialized_end=1843 + _LOTSNESTEDMESSAGE_B153._serialized_start=1845 + _LOTSNESTEDMESSAGE_B153._serialized_end=1851 + _LOTSNESTEDMESSAGE_B154._serialized_start=1853 + _LOTSNESTEDMESSAGE_B154._serialized_end=1859 + _LOTSNESTEDMESSAGE_B155._serialized_start=1861 + _LOTSNESTEDMESSAGE_B155._serialized_end=1867 + _LOTSNESTEDMESSAGE_B156._serialized_start=1869 + _LOTSNESTEDMESSAGE_B156._serialized_end=1875 + _LOTSNESTEDMESSAGE_B157._serialized_start=1877 + _LOTSNESTEDMESSAGE_B157._serialized_end=1883 + _LOTSNESTEDMESSAGE_B158._serialized_start=1885 + _LOTSNESTEDMESSAGE_B158._serialized_end=1891 + _LOTSNESTEDMESSAGE_B159._serialized_start=1893 + _LOTSNESTEDMESSAGE_B159._serialized_end=1899 + _LOTSNESTEDMESSAGE_B160._serialized_start=1901 + _LOTSNESTEDMESSAGE_B160._serialized_end=1907 + _LOTSNESTEDMESSAGE_B161._serialized_start=1909 + _LOTSNESTEDMESSAGE_B161._serialized_end=1915 + _LOTSNESTEDMESSAGE_B162._serialized_start=1917 + _LOTSNESTEDMESSAGE_B162._serialized_end=1923 + _LOTSNESTEDMESSAGE_B163._serialized_start=1925 + _LOTSNESTEDMESSAGE_B163._serialized_end=1931 + _LOTSNESTEDMESSAGE_B164._serialized_start=1933 + _LOTSNESTEDMESSAGE_B164._serialized_end=1939 + _LOTSNESTEDMESSAGE_B165._serialized_start=1941 + _LOTSNESTEDMESSAGE_B165._serialized_end=1947 + _LOTSNESTEDMESSAGE_B166._serialized_start=1949 + _LOTSNESTEDMESSAGE_B166._serialized_end=1955 + _LOTSNESTEDMESSAGE_B167._serialized_start=1957 + _LOTSNESTEDMESSAGE_B167._serialized_end=1963 + _LOTSNESTEDMESSAGE_B168._serialized_start=1965 + _LOTSNESTEDMESSAGE_B168._serialized_end=1971 + _LOTSNESTEDMESSAGE_B169._serialized_start=1973 + _LOTSNESTEDMESSAGE_B169._serialized_end=1979 + _LOTSNESTEDMESSAGE_B170._serialized_start=1981 + _LOTSNESTEDMESSAGE_B170._serialized_end=1987 + _LOTSNESTEDMESSAGE_B171._serialized_start=1989 + _LOTSNESTEDMESSAGE_B171._serialized_end=1995 + _LOTSNESTEDMESSAGE_B172._serialized_start=1997 + _LOTSNESTEDMESSAGE_B172._serialized_end=2003 + _LOTSNESTEDMESSAGE_B173._serialized_start=2005 + _LOTSNESTEDMESSAGE_B173._serialized_end=2011 + _LOTSNESTEDMESSAGE_B174._serialized_start=2013 + _LOTSNESTEDMESSAGE_B174._serialized_end=2019 + _LOTSNESTEDMESSAGE_B175._serialized_start=2021 + _LOTSNESTEDMESSAGE_B175._serialized_end=2027 + _LOTSNESTEDMESSAGE_B176._serialized_start=2029 + _LOTSNESTEDMESSAGE_B176._serialized_end=2035 + _LOTSNESTEDMESSAGE_B177._serialized_start=2037 + _LOTSNESTEDMESSAGE_B177._serialized_end=2043 + _LOTSNESTEDMESSAGE_B178._serialized_start=2045 + _LOTSNESTEDMESSAGE_B178._serialized_end=2051 + _LOTSNESTEDMESSAGE_B179._serialized_start=2053 + _LOTSNESTEDMESSAGE_B179._serialized_end=2059 + _LOTSNESTEDMESSAGE_B180._serialized_start=2061 + _LOTSNESTEDMESSAGE_B180._serialized_end=2067 + _LOTSNESTEDMESSAGE_B181._serialized_start=2069 + _LOTSNESTEDMESSAGE_B181._serialized_end=2075 + _LOTSNESTEDMESSAGE_B182._serialized_start=2077 + _LOTSNESTEDMESSAGE_B182._serialized_end=2083 + _LOTSNESTEDMESSAGE_B183._serialized_start=2085 + _LOTSNESTEDMESSAGE_B183._serialized_end=2091 + _LOTSNESTEDMESSAGE_B184._serialized_start=2093 + _LOTSNESTEDMESSAGE_B184._serialized_end=2099 + _LOTSNESTEDMESSAGE_B185._serialized_start=2101 + _LOTSNESTEDMESSAGE_B185._serialized_end=2107 + _LOTSNESTEDMESSAGE_B186._serialized_start=2109 + _LOTSNESTEDMESSAGE_B186._serialized_end=2115 + _LOTSNESTEDMESSAGE_B187._serialized_start=2117 + _LOTSNESTEDMESSAGE_B187._serialized_end=2123 + _LOTSNESTEDMESSAGE_B188._serialized_start=2125 + _LOTSNESTEDMESSAGE_B188._serialized_end=2131 + _LOTSNESTEDMESSAGE_B189._serialized_start=2133 + _LOTSNESTEDMESSAGE_B189._serialized_end=2139 + _LOTSNESTEDMESSAGE_B190._serialized_start=2141 + _LOTSNESTEDMESSAGE_B190._serialized_end=2147 + _LOTSNESTEDMESSAGE_B191._serialized_start=2149 + _LOTSNESTEDMESSAGE_B191._serialized_end=2155 + _LOTSNESTEDMESSAGE_B192._serialized_start=2157 + _LOTSNESTEDMESSAGE_B192._serialized_end=2163 + _LOTSNESTEDMESSAGE_B193._serialized_start=2165 + _LOTSNESTEDMESSAGE_B193._serialized_end=2171 + _LOTSNESTEDMESSAGE_B194._serialized_start=2173 + _LOTSNESTEDMESSAGE_B194._serialized_end=2179 + _LOTSNESTEDMESSAGE_B195._serialized_start=2181 + _LOTSNESTEDMESSAGE_B195._serialized_end=2187 + _LOTSNESTEDMESSAGE_B196._serialized_start=2189 + _LOTSNESTEDMESSAGE_B196._serialized_end=2195 + _LOTSNESTEDMESSAGE_B197._serialized_start=2197 + _LOTSNESTEDMESSAGE_B197._serialized_end=2203 + _LOTSNESTEDMESSAGE_B198._serialized_start=2205 + _LOTSNESTEDMESSAGE_B198._serialized_end=2211 + _LOTSNESTEDMESSAGE_B199._serialized_start=2213 + _LOTSNESTEDMESSAGE_B199._serialized_end=2219 + _LOTSNESTEDMESSAGE_B200._serialized_start=2221 + _LOTSNESTEDMESSAGE_B200._serialized_end=2227 + _LOTSNESTEDMESSAGE_B201._serialized_start=2229 + _LOTSNESTEDMESSAGE_B201._serialized_end=2235 + _LOTSNESTEDMESSAGE_B202._serialized_start=2237 + _LOTSNESTEDMESSAGE_B202._serialized_end=2243 + _LOTSNESTEDMESSAGE_B203._serialized_start=2245 + _LOTSNESTEDMESSAGE_B203._serialized_end=2251 + _LOTSNESTEDMESSAGE_B204._serialized_start=2253 + _LOTSNESTEDMESSAGE_B204._serialized_end=2259 + _LOTSNESTEDMESSAGE_B205._serialized_start=2261 + _LOTSNESTEDMESSAGE_B205._serialized_end=2267 + _LOTSNESTEDMESSAGE_B206._serialized_start=2269 + _LOTSNESTEDMESSAGE_B206._serialized_end=2275 + _LOTSNESTEDMESSAGE_B207._serialized_start=2277 + _LOTSNESTEDMESSAGE_B207._serialized_end=2283 + _LOTSNESTEDMESSAGE_B208._serialized_start=2285 + _LOTSNESTEDMESSAGE_B208._serialized_end=2291 + _LOTSNESTEDMESSAGE_B209._serialized_start=2293 + _LOTSNESTEDMESSAGE_B209._serialized_end=2299 + _LOTSNESTEDMESSAGE_B210._serialized_start=2301 + _LOTSNESTEDMESSAGE_B210._serialized_end=2307 + _LOTSNESTEDMESSAGE_B211._serialized_start=2309 + _LOTSNESTEDMESSAGE_B211._serialized_end=2315 + _LOTSNESTEDMESSAGE_B212._serialized_start=2317 + _LOTSNESTEDMESSAGE_B212._serialized_end=2323 + _LOTSNESTEDMESSAGE_B213._serialized_start=2325 + _LOTSNESTEDMESSAGE_B213._serialized_end=2331 + _LOTSNESTEDMESSAGE_B214._serialized_start=2333 + _LOTSNESTEDMESSAGE_B214._serialized_end=2339 + _LOTSNESTEDMESSAGE_B215._serialized_start=2341 + _LOTSNESTEDMESSAGE_B215._serialized_end=2347 + _LOTSNESTEDMESSAGE_B216._serialized_start=2349 + _LOTSNESTEDMESSAGE_B216._serialized_end=2355 + _LOTSNESTEDMESSAGE_B217._serialized_start=2357 + _LOTSNESTEDMESSAGE_B217._serialized_end=2363 + _LOTSNESTEDMESSAGE_B218._serialized_start=2365 + _LOTSNESTEDMESSAGE_B218._serialized_end=2371 + _LOTSNESTEDMESSAGE_B219._serialized_start=2373 + _LOTSNESTEDMESSAGE_B219._serialized_end=2379 + _LOTSNESTEDMESSAGE_B220._serialized_start=2381 + _LOTSNESTEDMESSAGE_B220._serialized_end=2387 + _LOTSNESTEDMESSAGE_B221._serialized_start=2389 + _LOTSNESTEDMESSAGE_B221._serialized_end=2395 + _LOTSNESTEDMESSAGE_B222._serialized_start=2397 + _LOTSNESTEDMESSAGE_B222._serialized_end=2403 + _LOTSNESTEDMESSAGE_B223._serialized_start=2405 + _LOTSNESTEDMESSAGE_B223._serialized_end=2411 + _LOTSNESTEDMESSAGE_B224._serialized_start=2413 + _LOTSNESTEDMESSAGE_B224._serialized_end=2419 + _LOTSNESTEDMESSAGE_B225._serialized_start=2421 + _LOTSNESTEDMESSAGE_B225._serialized_end=2427 + _LOTSNESTEDMESSAGE_B226._serialized_start=2429 + _LOTSNESTEDMESSAGE_B226._serialized_end=2435 + _LOTSNESTEDMESSAGE_B227._serialized_start=2437 + _LOTSNESTEDMESSAGE_B227._serialized_end=2443 + _LOTSNESTEDMESSAGE_B228._serialized_start=2445 + _LOTSNESTEDMESSAGE_B228._serialized_end=2451 + _LOTSNESTEDMESSAGE_B229._serialized_start=2453 + _LOTSNESTEDMESSAGE_B229._serialized_end=2459 + _LOTSNESTEDMESSAGE_B230._serialized_start=2461 + _LOTSNESTEDMESSAGE_B230._serialized_end=2467 + _LOTSNESTEDMESSAGE_B231._serialized_start=2469 + _LOTSNESTEDMESSAGE_B231._serialized_end=2475 + _LOTSNESTEDMESSAGE_B232._serialized_start=2477 + _LOTSNESTEDMESSAGE_B232._serialized_end=2483 + _LOTSNESTEDMESSAGE_B233._serialized_start=2485 + _LOTSNESTEDMESSAGE_B233._serialized_end=2491 + _LOTSNESTEDMESSAGE_B234._serialized_start=2493 + _LOTSNESTEDMESSAGE_B234._serialized_end=2499 + _LOTSNESTEDMESSAGE_B235._serialized_start=2501 + _LOTSNESTEDMESSAGE_B235._serialized_end=2507 + _LOTSNESTEDMESSAGE_B236._serialized_start=2509 + _LOTSNESTEDMESSAGE_B236._serialized_end=2515 + _LOTSNESTEDMESSAGE_B237._serialized_start=2517 + _LOTSNESTEDMESSAGE_B237._serialized_end=2523 + _LOTSNESTEDMESSAGE_B238._serialized_start=2525 + _LOTSNESTEDMESSAGE_B238._serialized_end=2531 + _LOTSNESTEDMESSAGE_B239._serialized_start=2533 + _LOTSNESTEDMESSAGE_B239._serialized_end=2539 + _LOTSNESTEDMESSAGE_B240._serialized_start=2541 + _LOTSNESTEDMESSAGE_B240._serialized_end=2547 + _LOTSNESTEDMESSAGE_B241._serialized_start=2549 + _LOTSNESTEDMESSAGE_B241._serialized_end=2555 + _LOTSNESTEDMESSAGE_B242._serialized_start=2557 + _LOTSNESTEDMESSAGE_B242._serialized_end=2563 + _LOTSNESTEDMESSAGE_B243._serialized_start=2565 + _LOTSNESTEDMESSAGE_B243._serialized_end=2571 + _LOTSNESTEDMESSAGE_B244._serialized_start=2573 + _LOTSNESTEDMESSAGE_B244._serialized_end=2579 + _LOTSNESTEDMESSAGE_B245._serialized_start=2581 + _LOTSNESTEDMESSAGE_B245._serialized_end=2587 + _LOTSNESTEDMESSAGE_B246._serialized_start=2589 + _LOTSNESTEDMESSAGE_B246._serialized_end=2595 + _LOTSNESTEDMESSAGE_B247._serialized_start=2597 + _LOTSNESTEDMESSAGE_B247._serialized_end=2603 + _LOTSNESTEDMESSAGE_B248._serialized_start=2605 + _LOTSNESTEDMESSAGE_B248._serialized_end=2611 + _LOTSNESTEDMESSAGE_B249._serialized_start=2613 + _LOTSNESTEDMESSAGE_B249._serialized_end=2619 + _LOTSNESTEDMESSAGE_B250._serialized_start=2621 + _LOTSNESTEDMESSAGE_B250._serialized_end=2627 + _LOTSNESTEDMESSAGE_B251._serialized_start=2629 + _LOTSNESTEDMESSAGE_B251._serialized_end=2635 + _LOTSNESTEDMESSAGE_B252._serialized_start=2637 + _LOTSNESTEDMESSAGE_B252._serialized_end=2643 + _LOTSNESTEDMESSAGE_B253._serialized_start=2645 + _LOTSNESTEDMESSAGE_B253._serialized_end=2651 + _LOTSNESTEDMESSAGE_B254._serialized_start=2653 + _LOTSNESTEDMESSAGE_B254._serialized_end=2659 + _LOTSNESTEDMESSAGE_B255._serialized_start=2661 + _LOTSNESTEDMESSAGE_B255._serialized_end=2667 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/internal/no_package_pb2.py b/scripts/protobuf3/protobuf3/internal/no_package_pb2.py new file mode 100644 index 0000000..d46dee0 --- /dev/null +++ b/scripts/protobuf3/protobuf3/internal/no_package_pb2.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/internal/no_package.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n)google/protobuf/internal/no_package.proto\";\n\x10NoPackageMessage\x12\'\n\x0fno_package_enum\x18\x01 \x01(\x0e\x32\x0e.NoPackageEnum*?\n\rNoPackageEnum\x12\x16\n\x12NO_PACKAGE_VALUE_0\x10\x00\x12\x16\n\x12NO_PACKAGE_VALUE_1\x10\x01') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.internal.no_package_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _NOPACKAGEENUM._serialized_start=106 + _NOPACKAGEENUM._serialized_end=169 + _NOPACKAGEMESSAGE._serialized_start=45 + _NOPACKAGEMESSAGE._serialized_end=104 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/internal/python_message.py b/scripts/protobuf3/protobuf3/internal/python_message.py new file mode 100644 index 0000000..2921d5c --- /dev/null +++ b/scripts/protobuf3/protobuf3/internal/python_message.py @@ -0,0 +1,1539 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# This code is meant to work on Python 2.4 and above only. +# +# TODO(robinson): Helpers for verbose, common checks like seeing if a +# descriptor's cpp_type is CPPTYPE_MESSAGE. + +"""Contains a metaclass and helper functions used to create +protocol message classes from Descriptor objects at runtime. + +Recall that a metaclass is the "type" of a class. +(A class is to a metaclass what an instance is to a class.) + +In this case, we use the GeneratedProtocolMessageType metaclass +to inject all the useful functionality into the classes +output by the protocol compiler at compile-time. + +The upshot of all this is that the real implementation +details for ALL pure-Python protocol buffers are *here in +this file*. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + +from io import BytesIO +import struct +import sys +import weakref + +# We use "as" to avoid name collisions with variables. +from google.protobuf.internal import api_implementation +from google.protobuf.internal import containers +from google.protobuf.internal import decoder +from google.protobuf.internal import encoder +from google.protobuf.internal import enum_type_wrapper +from google.protobuf.internal import extension_dict +from google.protobuf.internal import message_listener as message_listener_mod +from google.protobuf.internal import type_checkers +from google.protobuf.internal import well_known_types +from google.protobuf.internal import wire_format +from google.protobuf import descriptor as descriptor_mod +from google.protobuf import message as message_mod +from google.protobuf import text_format + +_FieldDescriptor = descriptor_mod.FieldDescriptor +_AnyFullTypeName = 'google.protobuf.Any' +_ExtensionDict = extension_dict._ExtensionDict + +class GeneratedProtocolMessageType(type): + + """Metaclass for protocol message classes created at runtime from Descriptors. + + We add implementations for all methods described in the Message class. We + also create properties to allow getting/setting all fields in the protocol + message. Finally, we create slots to prevent users from accidentally + "setting" nonexistent fields in the protocol message, which then wouldn't get + serialized / deserialized properly. + + The protocol compiler currently uses this metaclass to create protocol + message classes at runtime. Clients can also manually create their own + classes at runtime, as in this example: + + mydescriptor = Descriptor(.....) + factory = symbol_database.Default() + factory.pool.AddDescriptor(mydescriptor) + MyProtoClass = factory.GetPrototype(mydescriptor) + myproto_instance = MyProtoClass() + myproto.foo_field = 23 + ... + """ + + # Must be consistent with the protocol-compiler code in + # proto2/compiler/internal/generator.*. + _DESCRIPTOR_KEY = 'DESCRIPTOR' + + def __new__(cls, name, bases, dictionary): + """Custom allocation for runtime-generated class types. + + We override __new__ because this is apparently the only place + where we can meaningfully set __slots__ on the class we're creating(?). + (The interplay between metaclasses and slots is not very well-documented). + + Args: + name: Name of the class (ignored, but required by the + metaclass protocol). + bases: Base classes of the class we're constructing. + (Should be message.Message). We ignore this field, but + it's required by the metaclass protocol + dictionary: The class dictionary of the class we're + constructing. dictionary[_DESCRIPTOR_KEY] must contain + a Descriptor object describing this protocol message + type. + + Returns: + Newly-allocated class. + + Raises: + RuntimeError: Generated code only work with python cpp extension. + """ + descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY] + + if isinstance(descriptor, str): + raise RuntimeError('The generated code only work with python cpp ' + 'extension, but it is using pure python runtime.') + + # If a concrete class already exists for this descriptor, don't try to + # create another. Doing so will break any messages that already exist with + # the existing class. + # + # The C++ implementation appears to have its own internal `PyMessageFactory` + # to achieve similar results. + # + # This most commonly happens in `text_format.py` when using descriptors from + # a custom pool; it calls symbol_database.Global().getPrototype() on a + # descriptor which already has an existing concrete class. + new_class = getattr(descriptor, '_concrete_class', None) + if new_class: + return new_class + + if descriptor.full_name in well_known_types.WKTBASES: + bases += (well_known_types.WKTBASES[descriptor.full_name],) + _AddClassAttributesForNestedExtensions(descriptor, dictionary) + _AddSlots(descriptor, dictionary) + + superclass = super(GeneratedProtocolMessageType, cls) + new_class = superclass.__new__(cls, name, bases, dictionary) + return new_class + + def __init__(cls, name, bases, dictionary): + """Here we perform the majority of our work on the class. + We add enum getters, an __init__ method, implementations + of all Message methods, and properties for all fields + in the protocol type. + + Args: + name: Name of the class (ignored, but required by the + metaclass protocol). + bases: Base classes of the class we're constructing. + (Should be message.Message). We ignore this field, but + it's required by the metaclass protocol + dictionary: The class dictionary of the class we're + constructing. dictionary[_DESCRIPTOR_KEY] must contain + a Descriptor object describing this protocol message + type. + """ + descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY] + + # If this is an _existing_ class looked up via `_concrete_class` in the + # __new__ method above, then we don't need to re-initialize anything. + existing_class = getattr(descriptor, '_concrete_class', None) + if existing_class: + assert existing_class is cls, ( + 'Duplicate `GeneratedProtocolMessageType` created for descriptor %r' + % (descriptor.full_name)) + return + + cls._decoders_by_tag = {} + if (descriptor.has_options and + descriptor.GetOptions().message_set_wire_format): + cls._decoders_by_tag[decoder.MESSAGE_SET_ITEM_TAG] = ( + decoder.MessageSetItemDecoder(descriptor), None) + + # Attach stuff to each FieldDescriptor for quick lookup later on. + for field in descriptor.fields: + _AttachFieldHelpers(cls, field) + + descriptor._concrete_class = cls # pylint: disable=protected-access + _AddEnumValues(descriptor, cls) + _AddInitMethod(descriptor, cls) + _AddPropertiesForFields(descriptor, cls) + _AddPropertiesForExtensions(descriptor, cls) + _AddStaticMethods(cls) + _AddMessageMethods(descriptor, cls) + _AddPrivateHelperMethods(descriptor, cls) + + superclass = super(GeneratedProtocolMessageType, cls) + superclass.__init__(name, bases, dictionary) + + +# Stateless helpers for GeneratedProtocolMessageType below. +# Outside clients should not access these directly. +# +# I opted not to make any of these methods on the metaclass, to make it more +# clear that I'm not really using any state there and to keep clients from +# thinking that they have direct access to these construction helpers. + + +def _PropertyName(proto_field_name): + """Returns the name of the public property attribute which + clients can use to get and (in some cases) set the value + of a protocol message field. + + Args: + proto_field_name: The protocol message field name, exactly + as it appears (or would appear) in a .proto file. + """ + # TODO(robinson): Escape Python keywords (e.g., yield), and test this support. + # nnorwitz makes my day by writing: + # """ + # FYI. See the keyword module in the stdlib. This could be as simple as: + # + # if keyword.iskeyword(proto_field_name): + # return proto_field_name + "_" + # return proto_field_name + # """ + # Kenton says: The above is a BAD IDEA. People rely on being able to use + # getattr() and setattr() to reflectively manipulate field values. If we + # rename the properties, then every such user has to also make sure to apply + # the same transformation. Note that currently if you name a field "yield", + # you can still access it just fine using getattr/setattr -- it's not even + # that cumbersome to do so. + # TODO(kenton): Remove this method entirely if/when everyone agrees with my + # position. + return proto_field_name + + +def _AddSlots(message_descriptor, dictionary): + """Adds a __slots__ entry to dictionary, containing the names of all valid + attributes for this message type. + + Args: + message_descriptor: A Descriptor instance describing this message type. + dictionary: Class dictionary to which we'll add a '__slots__' entry. + """ + dictionary['__slots__'] = ['_cached_byte_size', + '_cached_byte_size_dirty', + '_fields', + '_unknown_fields', + '_unknown_field_set', + '_is_present_in_parent', + '_listener', + '_listener_for_children', + '__weakref__', + '_oneofs'] + + +def _IsMessageSetExtension(field): + return (field.is_extension and + field.containing_type.has_options and + field.containing_type.GetOptions().message_set_wire_format and + field.type == _FieldDescriptor.TYPE_MESSAGE and + field.label == _FieldDescriptor.LABEL_OPTIONAL) + + +def _IsMapField(field): + return (field.type == _FieldDescriptor.TYPE_MESSAGE and + field.message_type.has_options and + field.message_type.GetOptions().map_entry) + + +def _IsMessageMapField(field): + value_type = field.message_type.fields_by_name['value'] + return value_type.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE + + +def _AttachFieldHelpers(cls, field_descriptor): + is_repeated = (field_descriptor.label == _FieldDescriptor.LABEL_REPEATED) + is_packable = (is_repeated and + wire_format.IsTypePackable(field_descriptor.type)) + is_proto3 = field_descriptor.containing_type.syntax == 'proto3' + if not is_packable: + is_packed = False + elif field_descriptor.containing_type.syntax == 'proto2': + is_packed = (field_descriptor.has_options and + field_descriptor.GetOptions().packed) + else: + has_packed_false = (field_descriptor.has_options and + field_descriptor.GetOptions().HasField('packed') and + field_descriptor.GetOptions().packed == False) + is_packed = not has_packed_false + is_map_entry = _IsMapField(field_descriptor) + + if is_map_entry: + field_encoder = encoder.MapEncoder(field_descriptor) + sizer = encoder.MapSizer(field_descriptor, + _IsMessageMapField(field_descriptor)) + elif _IsMessageSetExtension(field_descriptor): + field_encoder = encoder.MessageSetItemEncoder(field_descriptor.number) + sizer = encoder.MessageSetItemSizer(field_descriptor.number) + else: + field_encoder = type_checkers.TYPE_TO_ENCODER[field_descriptor.type]( + field_descriptor.number, is_repeated, is_packed) + sizer = type_checkers.TYPE_TO_SIZER[field_descriptor.type]( + field_descriptor.number, is_repeated, is_packed) + + field_descriptor._encoder = field_encoder + field_descriptor._sizer = sizer + field_descriptor._default_constructor = _DefaultValueConstructorForField( + field_descriptor) + + def AddDecoder(wiretype, is_packed): + tag_bytes = encoder.TagBytes(field_descriptor.number, wiretype) + decode_type = field_descriptor.type + if (decode_type == _FieldDescriptor.TYPE_ENUM and + type_checkers.SupportsOpenEnums(field_descriptor)): + decode_type = _FieldDescriptor.TYPE_INT32 + + oneof_descriptor = None + clear_if_default = False + if field_descriptor.containing_oneof is not None: + oneof_descriptor = field_descriptor + elif (is_proto3 and not is_repeated and + field_descriptor.cpp_type != _FieldDescriptor.CPPTYPE_MESSAGE): + clear_if_default = True + + if is_map_entry: + is_message_map = _IsMessageMapField(field_descriptor) + + field_decoder = decoder.MapDecoder( + field_descriptor, _GetInitializeDefaultForMap(field_descriptor), + is_message_map) + elif decode_type == _FieldDescriptor.TYPE_STRING: + field_decoder = decoder.StringDecoder( + field_descriptor.number, is_repeated, is_packed, + field_descriptor, field_descriptor._default_constructor, + clear_if_default) + elif field_descriptor.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + field_decoder = type_checkers.TYPE_TO_DECODER[decode_type]( + field_descriptor.number, is_repeated, is_packed, + field_descriptor, field_descriptor._default_constructor) + else: + field_decoder = type_checkers.TYPE_TO_DECODER[decode_type]( + field_descriptor.number, is_repeated, is_packed, + # pylint: disable=protected-access + field_descriptor, field_descriptor._default_constructor, + clear_if_default) + + cls._decoders_by_tag[tag_bytes] = (field_decoder, oneof_descriptor) + + AddDecoder(type_checkers.FIELD_TYPE_TO_WIRE_TYPE[field_descriptor.type], + False) + + if is_repeated and wire_format.IsTypePackable(field_descriptor.type): + # To support wire compatibility of adding packed = true, add a decoder for + # packed values regardless of the field's options. + AddDecoder(wire_format.WIRETYPE_LENGTH_DELIMITED, True) + + +def _AddClassAttributesForNestedExtensions(descriptor, dictionary): + extensions = descriptor.extensions_by_name + for extension_name, extension_field in extensions.items(): + assert extension_name not in dictionary + dictionary[extension_name] = extension_field + + +def _AddEnumValues(descriptor, cls): + """Sets class-level attributes for all enum fields defined in this message. + + Also exporting a class-level object that can name enum values. + + Args: + descriptor: Descriptor object for this message type. + cls: Class we're constructing for this message type. + """ + for enum_type in descriptor.enum_types: + setattr(cls, enum_type.name, enum_type_wrapper.EnumTypeWrapper(enum_type)) + for enum_value in enum_type.values: + setattr(cls, enum_value.name, enum_value.number) + + +def _GetInitializeDefaultForMap(field): + if field.label != _FieldDescriptor.LABEL_REPEATED: + raise ValueError('map_entry set on non-repeated field %s' % ( + field.name)) + fields_by_name = field.message_type.fields_by_name + key_checker = type_checkers.GetTypeChecker(fields_by_name['key']) + + value_field = fields_by_name['value'] + if _IsMessageMapField(field): + def MakeMessageMapDefault(message): + return containers.MessageMap( + message._listener_for_children, value_field.message_type, key_checker, + field.message_type) + return MakeMessageMapDefault + else: + value_checker = type_checkers.GetTypeChecker(value_field) + def MakePrimitiveMapDefault(message): + return containers.ScalarMap( + message._listener_for_children, key_checker, value_checker, + field.message_type) + return MakePrimitiveMapDefault + +def _DefaultValueConstructorForField(field): + """Returns a function which returns a default value for a field. + + Args: + field: FieldDescriptor object for this field. + + The returned function has one argument: + message: Message instance containing this field, or a weakref proxy + of same. + + That function in turn returns a default value for this field. The default + value may refer back to |message| via a weak reference. + """ + + if _IsMapField(field): + return _GetInitializeDefaultForMap(field) + + if field.label == _FieldDescriptor.LABEL_REPEATED: + if field.has_default_value and field.default_value != []: + raise ValueError('Repeated field default value not empty list: %s' % ( + field.default_value)) + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + # We can't look at _concrete_class yet since it might not have + # been set. (Depends on order in which we initialize the classes). + message_type = field.message_type + def MakeRepeatedMessageDefault(message): + return containers.RepeatedCompositeFieldContainer( + message._listener_for_children, field.message_type) + return MakeRepeatedMessageDefault + else: + type_checker = type_checkers.GetTypeChecker(field) + def MakeRepeatedScalarDefault(message): + return containers.RepeatedScalarFieldContainer( + message._listener_for_children, type_checker) + return MakeRepeatedScalarDefault + + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + # _concrete_class may not yet be initialized. + message_type = field.message_type + def MakeSubMessageDefault(message): + assert getattr(message_type, '_concrete_class', None), ( + 'Uninitialized concrete class found for field %r (message type %r)' + % (field.full_name, message_type.full_name)) + result = message_type._concrete_class() + result._SetListener( + _OneofListener(message, field) + if field.containing_oneof is not None + else message._listener_for_children) + return result + return MakeSubMessageDefault + + def MakeScalarDefault(message): + # TODO(protobuf-team): This may be broken since there may not be + # default_value. Combine with has_default_value somehow. + return field.default_value + return MakeScalarDefault + + +def _ReraiseTypeErrorWithFieldName(message_name, field_name): + """Re-raise the currently-handled TypeError with the field name added.""" + exc = sys.exc_info()[1] + if len(exc.args) == 1 and type(exc) is TypeError: + # simple TypeError; add field name to exception message + exc = TypeError('%s for field %s.%s' % (str(exc), message_name, field_name)) + + # re-raise possibly-amended exception with original traceback: + raise exc.with_traceback(sys.exc_info()[2]) + + +def _AddInitMethod(message_descriptor, cls): + """Adds an __init__ method to cls.""" + + def _GetIntegerEnumValue(enum_type, value): + """Convert a string or integer enum value to an integer. + + If the value is a string, it is converted to the enum value in + enum_type with the same name. If the value is not a string, it's + returned as-is. (No conversion or bounds-checking is done.) + """ + if isinstance(value, str): + try: + return enum_type.values_by_name[value].number + except KeyError: + raise ValueError('Enum type %s: unknown label "%s"' % ( + enum_type.full_name, value)) + return value + + def init(self, **kwargs): + self._cached_byte_size = 0 + self._cached_byte_size_dirty = len(kwargs) > 0 + self._fields = {} + # Contains a mapping from oneof field descriptors to the descriptor + # of the currently set field in that oneof field. + self._oneofs = {} + + # _unknown_fields is () when empty for efficiency, and will be turned into + # a list if fields are added. + self._unknown_fields = () + # _unknown_field_set is None when empty for efficiency, and will be + # turned into UnknownFieldSet struct if fields are added. + self._unknown_field_set = None # pylint: disable=protected-access + self._is_present_in_parent = False + self._listener = message_listener_mod.NullMessageListener() + self._listener_for_children = _Listener(self) + for field_name, field_value in kwargs.items(): + field = _GetFieldByName(message_descriptor, field_name) + if field is None: + raise TypeError('%s() got an unexpected keyword argument "%s"' % + (message_descriptor.name, field_name)) + if field_value is None: + # field=None is the same as no field at all. + continue + if field.label == _FieldDescriptor.LABEL_REPEATED: + copy = field._default_constructor(self) + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: # Composite + if _IsMapField(field): + if _IsMessageMapField(field): + for key in field_value: + copy[key].MergeFrom(field_value[key]) + else: + copy.update(field_value) + else: + for val in field_value: + if isinstance(val, dict): + copy.add(**val) + else: + copy.add().MergeFrom(val) + else: # Scalar + if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: + field_value = [_GetIntegerEnumValue(field.enum_type, val) + for val in field_value] + copy.extend(field_value) + self._fields[field] = copy + elif field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + copy = field._default_constructor(self) + new_val = field_value + if isinstance(field_value, dict): + new_val = field.message_type._concrete_class(**field_value) + try: + copy.MergeFrom(new_val) + except TypeError: + _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name) + self._fields[field] = copy + else: + if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: + field_value = _GetIntegerEnumValue(field.enum_type, field_value) + try: + setattr(self, field_name, field_value) + except TypeError: + _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name) + + init.__module__ = None + init.__doc__ = None + cls.__init__ = init + + +def _GetFieldByName(message_descriptor, field_name): + """Returns a field descriptor by field name. + + Args: + message_descriptor: A Descriptor describing all fields in message. + field_name: The name of the field to retrieve. + Returns: + The field descriptor associated with the field name. + """ + try: + return message_descriptor.fields_by_name[field_name] + except KeyError: + raise ValueError('Protocol message %s has no "%s" field.' % + (message_descriptor.name, field_name)) + + +def _AddPropertiesForFields(descriptor, cls): + """Adds properties for all fields in this protocol message type.""" + for field in descriptor.fields: + _AddPropertiesForField(field, cls) + + if descriptor.is_extendable: + # _ExtensionDict is just an adaptor with no state so we allocate a new one + # every time it is accessed. + cls.Extensions = property(lambda self: _ExtensionDict(self)) + + +def _AddPropertiesForField(field, cls): + """Adds a public property for a protocol message field. + Clients can use this property to get and (in the case + of non-repeated scalar fields) directly set the value + of a protocol message field. + + Args: + field: A FieldDescriptor for this field. + cls: The class we're constructing. + """ + # Catch it if we add other types that we should + # handle specially here. + assert _FieldDescriptor.MAX_CPPTYPE == 10 + + constant_name = field.name.upper() + '_FIELD_NUMBER' + setattr(cls, constant_name, field.number) + + if field.label == _FieldDescriptor.LABEL_REPEATED: + _AddPropertiesForRepeatedField(field, cls) + elif field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + _AddPropertiesForNonRepeatedCompositeField(field, cls) + else: + _AddPropertiesForNonRepeatedScalarField(field, cls) + + +class _FieldProperty(property): + __slots__ = ('DESCRIPTOR',) + + def __init__(self, descriptor, getter, setter, doc): + property.__init__(self, getter, setter, doc=doc) + self.DESCRIPTOR = descriptor + + +def _AddPropertiesForRepeatedField(field, cls): + """Adds a public property for a "repeated" protocol message field. Clients + can use this property to get the value of the field, which will be either a + RepeatedScalarFieldContainer or RepeatedCompositeFieldContainer (see + below). + + Note that when clients add values to these containers, we perform + type-checking in the case of repeated scalar fields, and we also set any + necessary "has" bits as a side-effect. + + Args: + field: A FieldDescriptor for this field. + cls: The class we're constructing. + """ + proto_field_name = field.name + property_name = _PropertyName(proto_field_name) + + def getter(self): + field_value = self._fields.get(field) + if field_value is None: + # Construct a new object to represent this field. + field_value = field._default_constructor(self) + + # Atomically check if another thread has preempted us and, if not, swap + # in the new object we just created. If someone has preempted us, we + # take that object and discard ours. + # WARNING: We are relying on setdefault() being atomic. This is true + # in CPython but we haven't investigated others. This warning appears + # in several other locations in this file. + field_value = self._fields.setdefault(field, field_value) + return field_value + getter.__module__ = None + getter.__doc__ = 'Getter for %s.' % proto_field_name + + # We define a setter just so we can throw an exception with a more + # helpful error message. + def setter(self, new_value): + raise AttributeError('Assignment not allowed to repeated field ' + '"%s" in protocol message object.' % proto_field_name) + + doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name + setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) + + +def _AddPropertiesForNonRepeatedScalarField(field, cls): + """Adds a public property for a nonrepeated, scalar protocol message field. + Clients can use this property to get and directly set the value of the field. + Note that when the client sets the value of a field by using this property, + all necessary "has" bits are set as a side-effect, and we also perform + type-checking. + + Args: + field: A FieldDescriptor for this field. + cls: The class we're constructing. + """ + proto_field_name = field.name + property_name = _PropertyName(proto_field_name) + type_checker = type_checkers.GetTypeChecker(field) + default_value = field.default_value + is_proto3 = field.containing_type.syntax == 'proto3' + + def getter(self): + # TODO(protobuf-team): This may be broken since there may not be + # default_value. Combine with has_default_value somehow. + return self._fields.get(field, default_value) + getter.__module__ = None + getter.__doc__ = 'Getter for %s.' % proto_field_name + + clear_when_set_to_default = is_proto3 and not field.containing_oneof + + def field_setter(self, new_value): + # pylint: disable=protected-access + # Testing the value for truthiness captures all of the proto3 defaults + # (0, 0.0, enum 0, and False). + try: + new_value = type_checker.CheckValue(new_value) + except TypeError as e: + raise TypeError( + 'Cannot set %s to %.1024r: %s' % (field.full_name, new_value, e)) + if clear_when_set_to_default and not new_value: + self._fields.pop(field, None) + else: + self._fields[field] = new_value + # Check _cached_byte_size_dirty inline to improve performance, since scalar + # setters are called frequently. + if not self._cached_byte_size_dirty: + self._Modified() + + if field.containing_oneof: + def setter(self, new_value): + field_setter(self, new_value) + self._UpdateOneofState(field) + else: + setter = field_setter + + setter.__module__ = None + setter.__doc__ = 'Setter for %s.' % proto_field_name + + # Add a property to encapsulate the getter/setter. + doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name + setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) + + +def _AddPropertiesForNonRepeatedCompositeField(field, cls): + """Adds a public property for a nonrepeated, composite protocol message field. + A composite field is a "group" or "message" field. + + Clients can use this property to get the value of the field, but cannot + assign to the property directly. + + Args: + field: A FieldDescriptor for this field. + cls: The class we're constructing. + """ + # TODO(robinson): Remove duplication with similar method + # for non-repeated scalars. + proto_field_name = field.name + property_name = _PropertyName(proto_field_name) + + def getter(self): + field_value = self._fields.get(field) + if field_value is None: + # Construct a new object to represent this field. + field_value = field._default_constructor(self) + + # Atomically check if another thread has preempted us and, if not, swap + # in the new object we just created. If someone has preempted us, we + # take that object and discard ours. + # WARNING: We are relying on setdefault() being atomic. This is true + # in CPython but we haven't investigated others. This warning appears + # in several other locations in this file. + field_value = self._fields.setdefault(field, field_value) + return field_value + getter.__module__ = None + getter.__doc__ = 'Getter for %s.' % proto_field_name + + # We define a setter just so we can throw an exception with a more + # helpful error message. + def setter(self, new_value): + raise AttributeError('Assignment not allowed to composite field ' + '"%s" in protocol message object.' % proto_field_name) + + # Add a property to encapsulate the getter. + doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name + setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) + + +def _AddPropertiesForExtensions(descriptor, cls): + """Adds properties for all fields in this protocol message type.""" + extensions = descriptor.extensions_by_name + for extension_name, extension_field in extensions.items(): + constant_name = extension_name.upper() + '_FIELD_NUMBER' + setattr(cls, constant_name, extension_field.number) + + # TODO(amauryfa): Migrate all users of these attributes to functions like + # pool.FindExtensionByNumber(descriptor). + if descriptor.file is not None: + # TODO(amauryfa): Use cls.MESSAGE_FACTORY.pool when available. + pool = descriptor.file.pool + cls._extensions_by_number = pool._extensions_by_number[descriptor] + cls._extensions_by_name = pool._extensions_by_name[descriptor] + +def _AddStaticMethods(cls): + # TODO(robinson): This probably needs to be thread-safe(?) + def RegisterExtension(extension_handle): + extension_handle.containing_type = cls.DESCRIPTOR + # TODO(amauryfa): Use cls.MESSAGE_FACTORY.pool when available. + # pylint: disable=protected-access + cls.DESCRIPTOR.file.pool._AddExtensionDescriptor(extension_handle) + _AttachFieldHelpers(cls, extension_handle) + cls.RegisterExtension = staticmethod(RegisterExtension) + + def FromString(s): + message = cls() + message.MergeFromString(s) + return message + cls.FromString = staticmethod(FromString) + + +def _IsPresent(item): + """Given a (FieldDescriptor, value) tuple from _fields, return true if the + value should be included in the list returned by ListFields().""" + + if item[0].label == _FieldDescriptor.LABEL_REPEATED: + return bool(item[1]) + elif item[0].cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + return item[1]._is_present_in_parent + else: + return True + + +def _AddListFieldsMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def ListFields(self): + all_fields = [item for item in self._fields.items() if _IsPresent(item)] + all_fields.sort(key = lambda item: item[0].number) + return all_fields + + cls.ListFields = ListFields + +_PROTO3_ERROR_TEMPLATE = \ + ('Protocol message %s has no non-repeated submessage field "%s" ' + 'nor marked as optional') +_PROTO2_ERROR_TEMPLATE = 'Protocol message %s has no non-repeated field "%s"' + +def _AddHasFieldMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + is_proto3 = (message_descriptor.syntax == "proto3") + error_msg = _PROTO3_ERROR_TEMPLATE if is_proto3 else _PROTO2_ERROR_TEMPLATE + + hassable_fields = {} + for field in message_descriptor.fields: + if field.label == _FieldDescriptor.LABEL_REPEATED: + continue + # For proto3, only submessages and fields inside a oneof have presence. + if (is_proto3 and field.cpp_type != _FieldDescriptor.CPPTYPE_MESSAGE and + not field.containing_oneof): + continue + hassable_fields[field.name] = field + + # Has methods are supported for oneof descriptors. + for oneof in message_descriptor.oneofs: + hassable_fields[oneof.name] = oneof + + def HasField(self, field_name): + try: + field = hassable_fields[field_name] + except KeyError: + raise ValueError(error_msg % (message_descriptor.full_name, field_name)) + + if isinstance(field, descriptor_mod.OneofDescriptor): + try: + return HasField(self, self._oneofs[field].name) + except KeyError: + return False + else: + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + value = self._fields.get(field) + return value is not None and value._is_present_in_parent + else: + return field in self._fields + + cls.HasField = HasField + + +def _AddClearFieldMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def ClearField(self, field_name): + try: + field = message_descriptor.fields_by_name[field_name] + except KeyError: + try: + field = message_descriptor.oneofs_by_name[field_name] + if field in self._oneofs: + field = self._oneofs[field] + else: + return + except KeyError: + raise ValueError('Protocol message %s has no "%s" field.' % + (message_descriptor.name, field_name)) + + if field in self._fields: + # To match the C++ implementation, we need to invalidate iterators + # for map fields when ClearField() happens. + if hasattr(self._fields[field], 'InvalidateIterators'): + self._fields[field].InvalidateIterators() + + # Note: If the field is a sub-message, its listener will still point + # at us. That's fine, because the worst than can happen is that it + # will call _Modified() and invalidate our byte size. Big deal. + del self._fields[field] + + if self._oneofs.get(field.containing_oneof, None) is field: + del self._oneofs[field.containing_oneof] + + # Always call _Modified() -- even if nothing was changed, this is + # a mutating method, and thus calling it should cause the field to become + # present in the parent message. + self._Modified() + + cls.ClearField = ClearField + + +def _AddClearExtensionMethod(cls): + """Helper for _AddMessageMethods().""" + def ClearExtension(self, extension_handle): + extension_dict._VerifyExtensionHandle(self, extension_handle) + + # Similar to ClearField(), above. + if extension_handle in self._fields: + del self._fields[extension_handle] + self._Modified() + cls.ClearExtension = ClearExtension + + +def _AddHasExtensionMethod(cls): + """Helper for _AddMessageMethods().""" + def HasExtension(self, extension_handle): + extension_dict._VerifyExtensionHandle(self, extension_handle) + if extension_handle.label == _FieldDescriptor.LABEL_REPEATED: + raise KeyError('"%s" is repeated.' % extension_handle.full_name) + + if extension_handle.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + value = self._fields.get(extension_handle) + return value is not None and value._is_present_in_parent + else: + return extension_handle in self._fields + cls.HasExtension = HasExtension + +def _InternalUnpackAny(msg): + """Unpacks Any message and returns the unpacked message. + + This internal method is different from public Any Unpack method which takes + the target message as argument. _InternalUnpackAny method does not have + target message type and need to find the message type in descriptor pool. + + Args: + msg: An Any message to be unpacked. + + Returns: + The unpacked message. + """ + # TODO(amauryfa): Don't use the factory of generated messages. + # To make Any work with custom factories, use the message factory of the + # parent message. + # pylint: disable=g-import-not-at-top + from google.protobuf import symbol_database + factory = symbol_database.Default() + + type_url = msg.type_url + + if not type_url: + return None + + # TODO(haberman): For now we just strip the hostname. Better logic will be + # required. + type_name = type_url.split('/')[-1] + descriptor = factory.pool.FindMessageTypeByName(type_name) + + if descriptor is None: + return None + + message_class = factory.GetPrototype(descriptor) + message = message_class() + + message.ParseFromString(msg.value) + return message + + +def _AddEqualsMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def __eq__(self, other): + if (not isinstance(other, message_mod.Message) or + other.DESCRIPTOR != self.DESCRIPTOR): + return False + + if self is other: + return True + + if self.DESCRIPTOR.full_name == _AnyFullTypeName: + any_a = _InternalUnpackAny(self) + any_b = _InternalUnpackAny(other) + if any_a and any_b: + return any_a == any_b + + if not self.ListFields() == other.ListFields(): + return False + + # TODO(jieluo): Fix UnknownFieldSet to consider MessageSet extensions, + # then use it for the comparison. + unknown_fields = list(self._unknown_fields) + unknown_fields.sort() + other_unknown_fields = list(other._unknown_fields) + other_unknown_fields.sort() + return unknown_fields == other_unknown_fields + + cls.__eq__ = __eq__ + + +def _AddStrMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def __str__(self): + return text_format.MessageToString(self) + cls.__str__ = __str__ + + +def _AddReprMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def __repr__(self): + return text_format.MessageToString(self) + cls.__repr__ = __repr__ + + +def _AddUnicodeMethod(unused_message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def __unicode__(self): + return text_format.MessageToString(self, as_utf8=True).decode('utf-8') + cls.__unicode__ = __unicode__ + + +def _BytesForNonRepeatedElement(value, field_number, field_type): + """Returns the number of bytes needed to serialize a non-repeated element. + The returned byte count includes space for tag information and any + other additional space associated with serializing value. + + Args: + value: Value we're serializing. + field_number: Field number of this value. (Since the field number + is stored as part of a varint-encoded tag, this has an impact + on the total bytes required to serialize the value). + field_type: The type of the field. One of the TYPE_* constants + within FieldDescriptor. + """ + try: + fn = type_checkers.TYPE_TO_BYTE_SIZE_FN[field_type] + return fn(field_number, value) + except KeyError: + raise message_mod.EncodeError('Unrecognized field type: %d' % field_type) + + +def _AddByteSizeMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def ByteSize(self): + if not self._cached_byte_size_dirty: + return self._cached_byte_size + + size = 0 + descriptor = self.DESCRIPTOR + if descriptor.GetOptions().map_entry: + # Fields of map entry should always be serialized. + size = descriptor.fields_by_name['key']._sizer(self.key) + size += descriptor.fields_by_name['value']._sizer(self.value) + else: + for field_descriptor, field_value in self.ListFields(): + size += field_descriptor._sizer(field_value) + for tag_bytes, value_bytes in self._unknown_fields: + size += len(tag_bytes) + len(value_bytes) + + self._cached_byte_size = size + self._cached_byte_size_dirty = False + self._listener_for_children.dirty = False + return size + + cls.ByteSize = ByteSize + + +def _AddSerializeToStringMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def SerializeToString(self, **kwargs): + # Check if the message has all of its required fields set. + if not self.IsInitialized(): + raise message_mod.EncodeError( + 'Message %s is missing required fields: %s' % ( + self.DESCRIPTOR.full_name, ','.join(self.FindInitializationErrors()))) + return self.SerializePartialToString(**kwargs) + cls.SerializeToString = SerializeToString + + +def _AddSerializePartialToStringMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def SerializePartialToString(self, **kwargs): + out = BytesIO() + self._InternalSerialize(out.write, **kwargs) + return out.getvalue() + cls.SerializePartialToString = SerializePartialToString + + def InternalSerialize(self, write_bytes, deterministic=None): + if deterministic is None: + deterministic = ( + api_implementation.IsPythonDefaultSerializationDeterministic()) + else: + deterministic = bool(deterministic) + + descriptor = self.DESCRIPTOR + if descriptor.GetOptions().map_entry: + # Fields of map entry should always be serialized. + descriptor.fields_by_name['key']._encoder( + write_bytes, self.key, deterministic) + descriptor.fields_by_name['value']._encoder( + write_bytes, self.value, deterministic) + else: + for field_descriptor, field_value in self.ListFields(): + field_descriptor._encoder(write_bytes, field_value, deterministic) + for tag_bytes, value_bytes in self._unknown_fields: + write_bytes(tag_bytes) + write_bytes(value_bytes) + cls._InternalSerialize = InternalSerialize + + +def _AddMergeFromStringMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def MergeFromString(self, serialized): + serialized = memoryview(serialized) + length = len(serialized) + try: + if self._InternalParse(serialized, 0, length) != length: + # The only reason _InternalParse would return early is if it + # encountered an end-group tag. + raise message_mod.DecodeError('Unexpected end-group tag.') + except (IndexError, TypeError): + # Now ord(buf[p:p+1]) == ord('') gets TypeError. + raise message_mod.DecodeError('Truncated message.') + except struct.error as e: + raise message_mod.DecodeError(e) + return length # Return this for legacy reasons. + cls.MergeFromString = MergeFromString + + local_ReadTag = decoder.ReadTag + local_SkipField = decoder.SkipField + decoders_by_tag = cls._decoders_by_tag + + def InternalParse(self, buffer, pos, end): + """Create a message from serialized bytes. + + Args: + self: Message, instance of the proto message object. + buffer: memoryview of the serialized data. + pos: int, position to start in the serialized data. + end: int, end position of the serialized data. + + Returns: + Message object. + """ + # Guard against internal misuse, since this function is called internally + # quite extensively, and its easy to accidentally pass bytes. + assert isinstance(buffer, memoryview) + self._Modified() + field_dict = self._fields + # pylint: disable=protected-access + unknown_field_set = self._unknown_field_set + while pos != end: + (tag_bytes, new_pos) = local_ReadTag(buffer, pos) + field_decoder, field_desc = decoders_by_tag.get(tag_bytes, (None, None)) + if field_decoder is None: + if not self._unknown_fields: # pylint: disable=protected-access + self._unknown_fields = [] # pylint: disable=protected-access + if unknown_field_set is None: + # pylint: disable=protected-access + self._unknown_field_set = containers.UnknownFieldSet() + # pylint: disable=protected-access + unknown_field_set = self._unknown_field_set + # pylint: disable=protected-access + (tag, _) = decoder._DecodeVarint(tag_bytes, 0) + field_number, wire_type = wire_format.UnpackTag(tag) + if field_number == 0: + raise message_mod.DecodeError('Field number 0 is illegal.') + # TODO(jieluo): remove old_pos. + old_pos = new_pos + (data, new_pos) = decoder._DecodeUnknownField( + buffer, new_pos, wire_type) # pylint: disable=protected-access + if new_pos == -1: + return pos + # pylint: disable=protected-access + unknown_field_set._add(field_number, wire_type, data) + # TODO(jieluo): remove _unknown_fields. + new_pos = local_SkipField(buffer, old_pos, end, tag_bytes) + if new_pos == -1: + return pos + self._unknown_fields.append( + (tag_bytes, buffer[old_pos:new_pos].tobytes())) + pos = new_pos + else: + pos = field_decoder(buffer, new_pos, end, self, field_dict) + if field_desc: + self._UpdateOneofState(field_desc) + return pos + cls._InternalParse = InternalParse + + +def _AddIsInitializedMethod(message_descriptor, cls): + """Adds the IsInitialized and FindInitializationError methods to the + protocol message class.""" + + required_fields = [field for field in message_descriptor.fields + if field.label == _FieldDescriptor.LABEL_REQUIRED] + + def IsInitialized(self, errors=None): + """Checks if all required fields of a message are set. + + Args: + errors: A list which, if provided, will be populated with the field + paths of all missing required fields. + + Returns: + True iff the specified message has all required fields set. + """ + + # Performance is critical so we avoid HasField() and ListFields(). + + for field in required_fields: + if (field not in self._fields or + (field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE and + not self._fields[field]._is_present_in_parent)): + if errors is not None: + errors.extend(self.FindInitializationErrors()) + return False + + for field, value in list(self._fields.items()): # dict can change size! + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + if field.label == _FieldDescriptor.LABEL_REPEATED: + if (field.message_type.has_options and + field.message_type.GetOptions().map_entry): + continue + for element in value: + if not element.IsInitialized(): + if errors is not None: + errors.extend(self.FindInitializationErrors()) + return False + elif value._is_present_in_parent and not value.IsInitialized(): + if errors is not None: + errors.extend(self.FindInitializationErrors()) + return False + + return True + + cls.IsInitialized = IsInitialized + + def FindInitializationErrors(self): + """Finds required fields which are not initialized. + + Returns: + A list of strings. Each string is a path to an uninitialized field from + the top-level message, e.g. "foo.bar[5].baz". + """ + + errors = [] # simplify things + + for field in required_fields: + if not self.HasField(field.name): + errors.append(field.name) + + for field, value in self.ListFields(): + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + if field.is_extension: + name = '(%s)' % field.full_name + else: + name = field.name + + if _IsMapField(field): + if _IsMessageMapField(field): + for key in value: + element = value[key] + prefix = '%s[%s].' % (name, key) + sub_errors = element.FindInitializationErrors() + errors += [prefix + error for error in sub_errors] + else: + # ScalarMaps can't have any initialization errors. + pass + elif field.label == _FieldDescriptor.LABEL_REPEATED: + for i in range(len(value)): + element = value[i] + prefix = '%s[%d].' % (name, i) + sub_errors = element.FindInitializationErrors() + errors += [prefix + error for error in sub_errors] + else: + prefix = name + '.' + sub_errors = value.FindInitializationErrors() + errors += [prefix + error for error in sub_errors] + + return errors + + cls.FindInitializationErrors = FindInitializationErrors + + +def _FullyQualifiedClassName(klass): + module = klass.__module__ + name = getattr(klass, '__qualname__', klass.__name__) + if module in (None, 'builtins', '__builtin__'): + return name + return module + '.' + name + + +def _AddMergeFromMethod(cls): + LABEL_REPEATED = _FieldDescriptor.LABEL_REPEATED + CPPTYPE_MESSAGE = _FieldDescriptor.CPPTYPE_MESSAGE + + def MergeFrom(self, msg): + if not isinstance(msg, cls): + raise TypeError( + 'Parameter to MergeFrom() must be instance of same class: ' + 'expected %s got %s.' % (_FullyQualifiedClassName(cls), + _FullyQualifiedClassName(msg.__class__))) + + assert msg is not self + self._Modified() + + fields = self._fields + + for field, value in msg._fields.items(): + if field.label == LABEL_REPEATED: + field_value = fields.get(field) + if field_value is None: + # Construct a new object to represent this field. + field_value = field._default_constructor(self) + fields[field] = field_value + field_value.MergeFrom(value) + elif field.cpp_type == CPPTYPE_MESSAGE: + if value._is_present_in_parent: + field_value = fields.get(field) + if field_value is None: + # Construct a new object to represent this field. + field_value = field._default_constructor(self) + fields[field] = field_value + field_value.MergeFrom(value) + else: + self._fields[field] = value + if field.containing_oneof: + self._UpdateOneofState(field) + + if msg._unknown_fields: + if not self._unknown_fields: + self._unknown_fields = [] + self._unknown_fields.extend(msg._unknown_fields) + # pylint: disable=protected-access + if self._unknown_field_set is None: + self._unknown_field_set = containers.UnknownFieldSet() + self._unknown_field_set._extend(msg._unknown_field_set) + + cls.MergeFrom = MergeFrom + + +def _AddWhichOneofMethod(message_descriptor, cls): + def WhichOneof(self, oneof_name): + """Returns the name of the currently set field inside a oneof, or None.""" + try: + field = message_descriptor.oneofs_by_name[oneof_name] + except KeyError: + raise ValueError( + 'Protocol message has no oneof "%s" field.' % oneof_name) + + nested_field = self._oneofs.get(field, None) + if nested_field is not None and self.HasField(nested_field.name): + return nested_field.name + else: + return None + + cls.WhichOneof = WhichOneof + + +def _Clear(self): + # Clear fields. + self._fields = {} + self._unknown_fields = () + # pylint: disable=protected-access + if self._unknown_field_set is not None: + self._unknown_field_set._clear() + self._unknown_field_set = None + + self._oneofs = {} + self._Modified() + + +def _UnknownFields(self): + if self._unknown_field_set is None: # pylint: disable=protected-access + # pylint: disable=protected-access + self._unknown_field_set = containers.UnknownFieldSet() + return self._unknown_field_set # pylint: disable=protected-access + + +def _DiscardUnknownFields(self): + self._unknown_fields = [] + self._unknown_field_set = None # pylint: disable=protected-access + for field, value in self.ListFields(): + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + if _IsMapField(field): + if _IsMessageMapField(field): + for key in value: + value[key].DiscardUnknownFields() + elif field.label == _FieldDescriptor.LABEL_REPEATED: + for sub_message in value: + sub_message.DiscardUnknownFields() + else: + value.DiscardUnknownFields() + + +def _SetListener(self, listener): + if listener is None: + self._listener = message_listener_mod.NullMessageListener() + else: + self._listener = listener + + +def _AddMessageMethods(message_descriptor, cls): + """Adds implementations of all Message methods to cls.""" + _AddListFieldsMethod(message_descriptor, cls) + _AddHasFieldMethod(message_descriptor, cls) + _AddClearFieldMethod(message_descriptor, cls) + if message_descriptor.is_extendable: + _AddClearExtensionMethod(cls) + _AddHasExtensionMethod(cls) + _AddEqualsMethod(message_descriptor, cls) + _AddStrMethod(message_descriptor, cls) + _AddReprMethod(message_descriptor, cls) + _AddUnicodeMethod(message_descriptor, cls) + _AddByteSizeMethod(message_descriptor, cls) + _AddSerializeToStringMethod(message_descriptor, cls) + _AddSerializePartialToStringMethod(message_descriptor, cls) + _AddMergeFromStringMethod(message_descriptor, cls) + _AddIsInitializedMethod(message_descriptor, cls) + _AddMergeFromMethod(cls) + _AddWhichOneofMethod(message_descriptor, cls) + # Adds methods which do not depend on cls. + cls.Clear = _Clear + cls.UnknownFields = _UnknownFields + cls.DiscardUnknownFields = _DiscardUnknownFields + cls._SetListener = _SetListener + + +def _AddPrivateHelperMethods(message_descriptor, cls): + """Adds implementation of private helper methods to cls.""" + + def Modified(self): + """Sets the _cached_byte_size_dirty bit to true, + and propagates this to our listener iff this was a state change. + """ + + # Note: Some callers check _cached_byte_size_dirty before calling + # _Modified() as an extra optimization. So, if this method is ever + # changed such that it does stuff even when _cached_byte_size_dirty is + # already true, the callers need to be updated. + if not self._cached_byte_size_dirty: + self._cached_byte_size_dirty = True + self._listener_for_children.dirty = True + self._is_present_in_parent = True + self._listener.Modified() + + def _UpdateOneofState(self, field): + """Sets field as the active field in its containing oneof. + + Will also delete currently active field in the oneof, if it is different + from the argument. Does not mark the message as modified. + """ + other_field = self._oneofs.setdefault(field.containing_oneof, field) + if other_field is not field: + del self._fields[other_field] + self._oneofs[field.containing_oneof] = field + + cls._Modified = Modified + cls.SetInParent = Modified + cls._UpdateOneofState = _UpdateOneofState + + +class _Listener(object): + + """MessageListener implementation that a parent message registers with its + child message. + + In order to support semantics like: + + foo.bar.baz.qux = 23 + assert foo.HasField('bar') + + ...child objects must have back references to their parents. + This helper class is at the heart of this support. + """ + + def __init__(self, parent_message): + """Args: + parent_message: The message whose _Modified() method we should call when + we receive Modified() messages. + """ + # This listener establishes a back reference from a child (contained) object + # to its parent (containing) object. We make this a weak reference to avoid + # creating cyclic garbage when the client finishes with the 'parent' object + # in the tree. + if isinstance(parent_message, weakref.ProxyType): + self._parent_message_weakref = parent_message + else: + self._parent_message_weakref = weakref.proxy(parent_message) + + # As an optimization, we also indicate directly on the listener whether + # or not the parent message is dirty. This way we can avoid traversing + # up the tree in the common case. + self.dirty = False + + def Modified(self): + if self.dirty: + return + try: + # Propagate the signal to our parents iff this is the first field set. + self._parent_message_weakref._Modified() + except ReferenceError: + # We can get here if a client has kept a reference to a child object, + # and is now setting a field on it, but the child's parent has been + # garbage-collected. This is not an error. + pass + + +class _OneofListener(_Listener): + """Special listener implementation for setting composite oneof fields.""" + + def __init__(self, parent_message, field): + """Args: + parent_message: The message whose _Modified() method we should call when + we receive Modified() messages. + field: The descriptor of the field being set in the parent message. + """ + super(_OneofListener, self).__init__(parent_message) + self._field = field + + def Modified(self): + """Also updates the state of the containing oneof in the parent message.""" + try: + self._parent_message_weakref._UpdateOneofState(self._field) + super(_OneofListener, self).Modified() + except ReferenceError: + pass diff --git a/scripts/protobuf3/protobuf3/internal/type_checkers.py b/scripts/protobuf3/protobuf3/internal/type_checkers.py new file mode 100644 index 0000000..a53e71f --- /dev/null +++ b/scripts/protobuf3/protobuf3/internal/type_checkers.py @@ -0,0 +1,435 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Provides type checking routines. + +This module defines type checking utilities in the forms of dictionaries: + +VALUE_CHECKERS: A dictionary of field types and a value validation object. +TYPE_TO_BYTE_SIZE_FN: A dictionary with field types and a size computing + function. +TYPE_TO_SERIALIZE_METHOD: A dictionary with field types and serialization + function. +FIELD_TYPE_TO_WIRE_TYPE: A dictionary with field typed and their + corresponding wire types. +TYPE_TO_DESERIALIZE_METHOD: A dictionary with field types and deserialization + function. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + +import ctypes +import numbers + +from google.protobuf.internal import decoder +from google.protobuf.internal import encoder +from google.protobuf.internal import wire_format +from google.protobuf import descriptor + +_FieldDescriptor = descriptor.FieldDescriptor + + +def TruncateToFourByteFloat(original): + return ctypes.c_float(original).value + + +def ToShortestFloat(original): + """Returns the shortest float that has same value in wire.""" + # All 4 byte floats have between 6 and 9 significant digits, so we + # start with 6 as the lower bound. + # It has to be iterative because use '.9g' directly can not get rid + # of the noises for most values. For example if set a float_field=0.9 + # use '.9g' will print 0.899999976. + precision = 6 + rounded = float('{0:.{1}g}'.format(original, precision)) + while TruncateToFourByteFloat(rounded) != original: + precision += 1 + rounded = float('{0:.{1}g}'.format(original, precision)) + return rounded + + +def SupportsOpenEnums(field_descriptor): + return field_descriptor.containing_type.syntax == 'proto3' + + +def GetTypeChecker(field): + """Returns a type checker for a message field of the specified types. + + Args: + field: FieldDescriptor object for this field. + + Returns: + An instance of TypeChecker which can be used to verify the types + of values assigned to a field of the specified type. + """ + if (field.cpp_type == _FieldDescriptor.CPPTYPE_STRING and + field.type == _FieldDescriptor.TYPE_STRING): + return UnicodeValueChecker() + if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: + if SupportsOpenEnums(field): + # When open enums are supported, any int32 can be assigned. + return _VALUE_CHECKERS[_FieldDescriptor.CPPTYPE_INT32] + else: + return EnumValueChecker(field.enum_type) + return _VALUE_CHECKERS[field.cpp_type] + + +# None of the typecheckers below make any attempt to guard against people +# subclassing builtin types and doing weird things. We're not trying to +# protect against malicious clients here, just people accidentally shooting +# themselves in the foot in obvious ways. +class TypeChecker(object): + + """Type checker used to catch type errors as early as possible + when the client is setting scalar fields in protocol messages. + """ + + def __init__(self, *acceptable_types): + self._acceptable_types = acceptable_types + + def CheckValue(self, proposed_value): + """Type check the provided value and return it. + + The returned value might have been normalized to another type. + """ + if not isinstance(proposed_value, self._acceptable_types): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), self._acceptable_types)) + raise TypeError(message) + return proposed_value + + +class TypeCheckerWithDefault(TypeChecker): + + def __init__(self, default_value, *acceptable_types): + TypeChecker.__init__(self, *acceptable_types) + self._default_value = default_value + + def DefaultValue(self): + return self._default_value + + +class BoolValueChecker(object): + """Type checker used for bool fields.""" + + def CheckValue(self, proposed_value): + if not hasattr(proposed_value, '__index__') or ( + type(proposed_value).__module__ == 'numpy' and + type(proposed_value).__name__ == 'ndarray'): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), (bool, int))) + raise TypeError(message) + return bool(proposed_value) + + def DefaultValue(self): + return False + + +# IntValueChecker and its subclasses perform integer type-checks +# and bounds-checks. +class IntValueChecker(object): + + """Checker used for integer fields. Performs type-check and range check.""" + + def CheckValue(self, proposed_value): + if not hasattr(proposed_value, '__index__') or ( + type(proposed_value).__module__ == 'numpy' and + type(proposed_value).__name__ == 'ndarray'): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), (int,))) + raise TypeError(message) + + if not self._MIN <= int(proposed_value) <= self._MAX: + raise ValueError('Value out of range: %d' % proposed_value) + # We force all values to int to make alternate implementations where the + # distinction is more significant (e.g. the C++ implementation) simpler. + proposed_value = int(proposed_value) + return proposed_value + + def DefaultValue(self): + return 0 + + +class EnumValueChecker(object): + + """Checker used for enum fields. Performs type-check and range check.""" + + def __init__(self, enum_type): + self._enum_type = enum_type + + def CheckValue(self, proposed_value): + if not isinstance(proposed_value, numbers.Integral): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), (int,))) + raise TypeError(message) + if int(proposed_value) not in self._enum_type.values_by_number: + raise ValueError('Unknown enum value: %d' % proposed_value) + return proposed_value + + def DefaultValue(self): + return self._enum_type.values[0].number + + +class UnicodeValueChecker(object): + + """Checker used for string fields. + + Always returns a unicode value, even if the input is of type str. + """ + + def CheckValue(self, proposed_value): + if not isinstance(proposed_value, (bytes, str)): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), (bytes, str))) + raise TypeError(message) + + # If the value is of type 'bytes' make sure that it is valid UTF-8 data. + if isinstance(proposed_value, bytes): + try: + proposed_value = proposed_value.decode('utf-8') + except UnicodeDecodeError: + raise ValueError('%.1024r has type bytes, but isn\'t valid UTF-8 ' + 'encoding. Non-UTF-8 strings must be converted to ' + 'unicode objects before being added.' % + (proposed_value)) + else: + try: + proposed_value.encode('utf8') + except UnicodeEncodeError: + raise ValueError('%.1024r isn\'t a valid unicode string and ' + 'can\'t be encoded in UTF-8.'% + (proposed_value)) + + return proposed_value + + def DefaultValue(self): + return u"" + + +class Int32ValueChecker(IntValueChecker): + # We're sure to use ints instead of longs here since comparison may be more + # efficient. + _MIN = -2147483648 + _MAX = 2147483647 + + +class Uint32ValueChecker(IntValueChecker): + _MIN = 0 + _MAX = (1 << 32) - 1 + + +class Int64ValueChecker(IntValueChecker): + _MIN = -(1 << 63) + _MAX = (1 << 63) - 1 + + +class Uint64ValueChecker(IntValueChecker): + _MIN = 0 + _MAX = (1 << 64) - 1 + + +# The max 4 bytes float is about 3.4028234663852886e+38 +_FLOAT_MAX = float.fromhex('0x1.fffffep+127') +_FLOAT_MIN = -_FLOAT_MAX +_INF = float('inf') +_NEG_INF = float('-inf') + + +class DoubleValueChecker(object): + """Checker used for double fields. + + Performs type-check and range check. + """ + + def CheckValue(self, proposed_value): + """Check and convert proposed_value to float.""" + if (not hasattr(proposed_value, '__float__') and + not hasattr(proposed_value, '__index__')) or ( + type(proposed_value).__module__ == 'numpy' and + type(proposed_value).__name__ == 'ndarray'): + message = ('%.1024r has type %s, but expected one of: int, float' % + (proposed_value, type(proposed_value))) + raise TypeError(message) + return float(proposed_value) + + def DefaultValue(self): + return 0.0 + + +class FloatValueChecker(DoubleValueChecker): + """Checker used for float fields. + + Performs type-check and range check. + + Values exceeding a 32-bit float will be converted to inf/-inf. + """ + + def CheckValue(self, proposed_value): + """Check and convert proposed_value to float.""" + converted_value = super().CheckValue(proposed_value) + # This inf rounding matches the C++ proto SafeDoubleToFloat logic. + if converted_value > _FLOAT_MAX: + return _INF + if converted_value < _FLOAT_MIN: + return _NEG_INF + + return TruncateToFourByteFloat(converted_value) + +# Type-checkers for all scalar CPPTYPEs. +_VALUE_CHECKERS = { + _FieldDescriptor.CPPTYPE_INT32: Int32ValueChecker(), + _FieldDescriptor.CPPTYPE_INT64: Int64ValueChecker(), + _FieldDescriptor.CPPTYPE_UINT32: Uint32ValueChecker(), + _FieldDescriptor.CPPTYPE_UINT64: Uint64ValueChecker(), + _FieldDescriptor.CPPTYPE_DOUBLE: DoubleValueChecker(), + _FieldDescriptor.CPPTYPE_FLOAT: FloatValueChecker(), + _FieldDescriptor.CPPTYPE_BOOL: BoolValueChecker(), + _FieldDescriptor.CPPTYPE_STRING: TypeCheckerWithDefault(b'', bytes), +} + + +# Map from field type to a function F, such that F(field_num, value) +# gives the total byte size for a value of the given type. This +# byte size includes tag information and any other additional space +# associated with serializing "value". +TYPE_TO_BYTE_SIZE_FN = { + _FieldDescriptor.TYPE_DOUBLE: wire_format.DoubleByteSize, + _FieldDescriptor.TYPE_FLOAT: wire_format.FloatByteSize, + _FieldDescriptor.TYPE_INT64: wire_format.Int64ByteSize, + _FieldDescriptor.TYPE_UINT64: wire_format.UInt64ByteSize, + _FieldDescriptor.TYPE_INT32: wire_format.Int32ByteSize, + _FieldDescriptor.TYPE_FIXED64: wire_format.Fixed64ByteSize, + _FieldDescriptor.TYPE_FIXED32: wire_format.Fixed32ByteSize, + _FieldDescriptor.TYPE_BOOL: wire_format.BoolByteSize, + _FieldDescriptor.TYPE_STRING: wire_format.StringByteSize, + _FieldDescriptor.TYPE_GROUP: wire_format.GroupByteSize, + _FieldDescriptor.TYPE_MESSAGE: wire_format.MessageByteSize, + _FieldDescriptor.TYPE_BYTES: wire_format.BytesByteSize, + _FieldDescriptor.TYPE_UINT32: wire_format.UInt32ByteSize, + _FieldDescriptor.TYPE_ENUM: wire_format.EnumByteSize, + _FieldDescriptor.TYPE_SFIXED32: wire_format.SFixed32ByteSize, + _FieldDescriptor.TYPE_SFIXED64: wire_format.SFixed64ByteSize, + _FieldDescriptor.TYPE_SINT32: wire_format.SInt32ByteSize, + _FieldDescriptor.TYPE_SINT64: wire_format.SInt64ByteSize + } + + +# Maps from field types to encoder constructors. +TYPE_TO_ENCODER = { + _FieldDescriptor.TYPE_DOUBLE: encoder.DoubleEncoder, + _FieldDescriptor.TYPE_FLOAT: encoder.FloatEncoder, + _FieldDescriptor.TYPE_INT64: encoder.Int64Encoder, + _FieldDescriptor.TYPE_UINT64: encoder.UInt64Encoder, + _FieldDescriptor.TYPE_INT32: encoder.Int32Encoder, + _FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Encoder, + _FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Encoder, + _FieldDescriptor.TYPE_BOOL: encoder.BoolEncoder, + _FieldDescriptor.TYPE_STRING: encoder.StringEncoder, + _FieldDescriptor.TYPE_GROUP: encoder.GroupEncoder, + _FieldDescriptor.TYPE_MESSAGE: encoder.MessageEncoder, + _FieldDescriptor.TYPE_BYTES: encoder.BytesEncoder, + _FieldDescriptor.TYPE_UINT32: encoder.UInt32Encoder, + _FieldDescriptor.TYPE_ENUM: encoder.EnumEncoder, + _FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Encoder, + _FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Encoder, + _FieldDescriptor.TYPE_SINT32: encoder.SInt32Encoder, + _FieldDescriptor.TYPE_SINT64: encoder.SInt64Encoder, + } + + +# Maps from field types to sizer constructors. +TYPE_TO_SIZER = { + _FieldDescriptor.TYPE_DOUBLE: encoder.DoubleSizer, + _FieldDescriptor.TYPE_FLOAT: encoder.FloatSizer, + _FieldDescriptor.TYPE_INT64: encoder.Int64Sizer, + _FieldDescriptor.TYPE_UINT64: encoder.UInt64Sizer, + _FieldDescriptor.TYPE_INT32: encoder.Int32Sizer, + _FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Sizer, + _FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Sizer, + _FieldDescriptor.TYPE_BOOL: encoder.BoolSizer, + _FieldDescriptor.TYPE_STRING: encoder.StringSizer, + _FieldDescriptor.TYPE_GROUP: encoder.GroupSizer, + _FieldDescriptor.TYPE_MESSAGE: encoder.MessageSizer, + _FieldDescriptor.TYPE_BYTES: encoder.BytesSizer, + _FieldDescriptor.TYPE_UINT32: encoder.UInt32Sizer, + _FieldDescriptor.TYPE_ENUM: encoder.EnumSizer, + _FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Sizer, + _FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Sizer, + _FieldDescriptor.TYPE_SINT32: encoder.SInt32Sizer, + _FieldDescriptor.TYPE_SINT64: encoder.SInt64Sizer, + } + + +# Maps from field type to a decoder constructor. +TYPE_TO_DECODER = { + _FieldDescriptor.TYPE_DOUBLE: decoder.DoubleDecoder, + _FieldDescriptor.TYPE_FLOAT: decoder.FloatDecoder, + _FieldDescriptor.TYPE_INT64: decoder.Int64Decoder, + _FieldDescriptor.TYPE_UINT64: decoder.UInt64Decoder, + _FieldDescriptor.TYPE_INT32: decoder.Int32Decoder, + _FieldDescriptor.TYPE_FIXED64: decoder.Fixed64Decoder, + _FieldDescriptor.TYPE_FIXED32: decoder.Fixed32Decoder, + _FieldDescriptor.TYPE_BOOL: decoder.BoolDecoder, + _FieldDescriptor.TYPE_STRING: decoder.StringDecoder, + _FieldDescriptor.TYPE_GROUP: decoder.GroupDecoder, + _FieldDescriptor.TYPE_MESSAGE: decoder.MessageDecoder, + _FieldDescriptor.TYPE_BYTES: decoder.BytesDecoder, + _FieldDescriptor.TYPE_UINT32: decoder.UInt32Decoder, + _FieldDescriptor.TYPE_ENUM: decoder.EnumDecoder, + _FieldDescriptor.TYPE_SFIXED32: decoder.SFixed32Decoder, + _FieldDescriptor.TYPE_SFIXED64: decoder.SFixed64Decoder, + _FieldDescriptor.TYPE_SINT32: decoder.SInt32Decoder, + _FieldDescriptor.TYPE_SINT64: decoder.SInt64Decoder, + } + +# Maps from field type to expected wiretype. +FIELD_TYPE_TO_WIRE_TYPE = { + _FieldDescriptor.TYPE_DOUBLE: wire_format.WIRETYPE_FIXED64, + _FieldDescriptor.TYPE_FLOAT: wire_format.WIRETYPE_FIXED32, + _FieldDescriptor.TYPE_INT64: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_UINT64: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_INT32: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_FIXED64: wire_format.WIRETYPE_FIXED64, + _FieldDescriptor.TYPE_FIXED32: wire_format.WIRETYPE_FIXED32, + _FieldDescriptor.TYPE_BOOL: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_STRING: + wire_format.WIRETYPE_LENGTH_DELIMITED, + _FieldDescriptor.TYPE_GROUP: wire_format.WIRETYPE_START_GROUP, + _FieldDescriptor.TYPE_MESSAGE: + wire_format.WIRETYPE_LENGTH_DELIMITED, + _FieldDescriptor.TYPE_BYTES: + wire_format.WIRETYPE_LENGTH_DELIMITED, + _FieldDescriptor.TYPE_UINT32: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_ENUM: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_SFIXED32: wire_format.WIRETYPE_FIXED32, + _FieldDescriptor.TYPE_SFIXED64: wire_format.WIRETYPE_FIXED64, + _FieldDescriptor.TYPE_SINT32: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_SINT64: wire_format.WIRETYPE_VARINT, + } diff --git a/scripts/protobuf3/protobuf3/internal/well_known_types.py b/scripts/protobuf3/protobuf3/internal/well_known_types.py new file mode 100644 index 0000000..b581ab7 --- /dev/null +++ b/scripts/protobuf3/protobuf3/internal/well_known_types.py @@ -0,0 +1,878 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Contains well known classes. + +This files defines well known classes which need extra maintenance including: + - Any + - Duration + - FieldMask + - Struct + - Timestamp +""" + +__author__ = 'jieluo@google.com (Jie Luo)' + +import calendar +import collections.abc +import datetime + +from google.protobuf.descriptor import FieldDescriptor + +_TIMESTAMPFOMAT = '%Y-%m-%dT%H:%M:%S' +_NANOS_PER_SECOND = 1000000000 +_NANOS_PER_MILLISECOND = 1000000 +_NANOS_PER_MICROSECOND = 1000 +_MILLIS_PER_SECOND = 1000 +_MICROS_PER_SECOND = 1000000 +_SECONDS_PER_DAY = 24 * 3600 +_DURATION_SECONDS_MAX = 315576000000 + + +class Any(object): + """Class for Any Message type.""" + + __slots__ = () + + def Pack(self, msg, type_url_prefix='type.googleapis.com/', + deterministic=None): + """Packs the specified message into current Any message.""" + if len(type_url_prefix) < 1 or type_url_prefix[-1] != '/': + self.type_url = '%s/%s' % (type_url_prefix, msg.DESCRIPTOR.full_name) + else: + self.type_url = '%s%s' % (type_url_prefix, msg.DESCRIPTOR.full_name) + self.value = msg.SerializeToString(deterministic=deterministic) + + def Unpack(self, msg): + """Unpacks the current Any message into specified message.""" + descriptor = msg.DESCRIPTOR + if not self.Is(descriptor): + return False + msg.ParseFromString(self.value) + return True + + def TypeName(self): + """Returns the protobuf type name of the inner message.""" + # Only last part is to be used: b/25630112 + return self.type_url.split('/')[-1] + + def Is(self, descriptor): + """Checks if this Any represents the given protobuf type.""" + return '/' in self.type_url and self.TypeName() == descriptor.full_name + + +_EPOCH_DATETIME_NAIVE = datetime.datetime.utcfromtimestamp(0) +_EPOCH_DATETIME_AWARE = datetime.datetime.fromtimestamp( + 0, tz=datetime.timezone.utc) + + +class Timestamp(object): + """Class for Timestamp message type.""" + + __slots__ = () + + def ToJsonString(self): + """Converts Timestamp to RFC 3339 date string format. + + Returns: + A string converted from timestamp. The string is always Z-normalized + and uses 3, 6 or 9 fractional digits as required to represent the + exact time. Example of the return format: '1972-01-01T10:00:20.021Z' + """ + nanos = self.nanos % _NANOS_PER_SECOND + total_sec = self.seconds + (self.nanos - nanos) // _NANOS_PER_SECOND + seconds = total_sec % _SECONDS_PER_DAY + days = (total_sec - seconds) // _SECONDS_PER_DAY + dt = datetime.datetime(1970, 1, 1) + datetime.timedelta(days, seconds) + + result = dt.isoformat() + if (nanos % 1e9) == 0: + # If there are 0 fractional digits, the fractional + # point '.' should be omitted when serializing. + return result + 'Z' + if (nanos % 1e6) == 0: + # Serialize 3 fractional digits. + return result + '.%03dZ' % (nanos / 1e6) + if (nanos % 1e3) == 0: + # Serialize 6 fractional digits. + return result + '.%06dZ' % (nanos / 1e3) + # Serialize 9 fractional digits. + return result + '.%09dZ' % nanos + + def FromJsonString(self, value): + """Parse a RFC 3339 date string format to Timestamp. + + Args: + value: A date string. Any fractional digits (or none) and any offset are + accepted as long as they fit into nano-seconds precision. + Example of accepted format: '1972-01-01T10:00:20.021-05:00' + + Raises: + ValueError: On parsing problems. + """ + if not isinstance(value, str): + raise ValueError('Timestamp JSON value not a string: {!r}'.format(value)) + timezone_offset = value.find('Z') + if timezone_offset == -1: + timezone_offset = value.find('+') + if timezone_offset == -1: + timezone_offset = value.rfind('-') + if timezone_offset == -1: + raise ValueError( + 'Failed to parse timestamp: missing valid timezone offset.') + time_value = value[0:timezone_offset] + # Parse datetime and nanos. + point_position = time_value.find('.') + if point_position == -1: + second_value = time_value + nano_value = '' + else: + second_value = time_value[:point_position] + nano_value = time_value[point_position + 1:] + if 't' in second_value: + raise ValueError( + 'time data \'{0}\' does not match format \'%Y-%m-%dT%H:%M:%S\', ' + 'lowercase \'t\' is not accepted'.format(second_value)) + date_object = datetime.datetime.strptime(second_value, _TIMESTAMPFOMAT) + td = date_object - datetime.datetime(1970, 1, 1) + seconds = td.seconds + td.days * _SECONDS_PER_DAY + if len(nano_value) > 9: + raise ValueError( + 'Failed to parse Timestamp: nanos {0} more than ' + '9 fractional digits.'.format(nano_value)) + if nano_value: + nanos = round(float('0.' + nano_value) * 1e9) + else: + nanos = 0 + # Parse timezone offsets. + if value[timezone_offset] == 'Z': + if len(value) != timezone_offset + 1: + raise ValueError('Failed to parse timestamp: invalid trailing' + ' data {0}.'.format(value)) + else: + timezone = value[timezone_offset:] + pos = timezone.find(':') + if pos == -1: + raise ValueError( + 'Invalid timezone offset value: {0}.'.format(timezone)) + if timezone[0] == '+': + seconds -= (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60 + else: + seconds += (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60 + # Set seconds and nanos + self.seconds = int(seconds) + self.nanos = int(nanos) + + def GetCurrentTime(self): + """Get the current UTC into Timestamp.""" + self.FromDatetime(datetime.datetime.utcnow()) + + def ToNanoseconds(self): + """Converts Timestamp to nanoseconds since epoch.""" + return self.seconds * _NANOS_PER_SECOND + self.nanos + + def ToMicroseconds(self): + """Converts Timestamp to microseconds since epoch.""" + return (self.seconds * _MICROS_PER_SECOND + + self.nanos // _NANOS_PER_MICROSECOND) + + def ToMilliseconds(self): + """Converts Timestamp to milliseconds since epoch.""" + return (self.seconds * _MILLIS_PER_SECOND + + self.nanos // _NANOS_PER_MILLISECOND) + + def ToSeconds(self): + """Converts Timestamp to seconds since epoch.""" + return self.seconds + + def FromNanoseconds(self, nanos): + """Converts nanoseconds since epoch to Timestamp.""" + self.seconds = nanos // _NANOS_PER_SECOND + self.nanos = nanos % _NANOS_PER_SECOND + + def FromMicroseconds(self, micros): + """Converts microseconds since epoch to Timestamp.""" + self.seconds = micros // _MICROS_PER_SECOND + self.nanos = (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND + + def FromMilliseconds(self, millis): + """Converts milliseconds since epoch to Timestamp.""" + self.seconds = millis // _MILLIS_PER_SECOND + self.nanos = (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND + + def FromSeconds(self, seconds): + """Converts seconds since epoch to Timestamp.""" + self.seconds = seconds + self.nanos = 0 + + def ToDatetime(self, tzinfo=None): + """Converts Timestamp to a datetime. + + Args: + tzinfo: A datetime.tzinfo subclass; defaults to None. + + Returns: + If tzinfo is None, returns a timezone-naive UTC datetime (with no timezone + information, i.e. not aware that it's UTC). + + Otherwise, returns a timezone-aware datetime in the input timezone. + """ + delta = datetime.timedelta( + seconds=self.seconds, + microseconds=_RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND)) + if tzinfo is None: + return _EPOCH_DATETIME_NAIVE + delta + else: + return _EPOCH_DATETIME_AWARE.astimezone(tzinfo) + delta + + def FromDatetime(self, dt): + """Converts datetime to Timestamp. + + Args: + dt: A datetime. If it's timezone-naive, it's assumed to be in UTC. + """ + # Using this guide: http://wiki.python.org/moin/WorkingWithTime + # And this conversion guide: http://docs.python.org/library/time.html + + # Turn the date parameter into a tuple (struct_time) that can then be + # manipulated into a long value of seconds. During the conversion from + # struct_time to long, the source date in UTC, and so it follows that the + # correct transformation is calendar.timegm() + self.seconds = calendar.timegm(dt.utctimetuple()) + self.nanos = dt.microsecond * _NANOS_PER_MICROSECOND + + +class Duration(object): + """Class for Duration message type.""" + + __slots__ = () + + def ToJsonString(self): + """Converts Duration to string format. + + Returns: + A string converted from self. The string format will contains + 3, 6, or 9 fractional digits depending on the precision required to + represent the exact Duration value. For example: "1s", "1.010s", + "1.000000100s", "-3.100s" + """ + _CheckDurationValid(self.seconds, self.nanos) + if self.seconds < 0 or self.nanos < 0: + result = '-' + seconds = - self.seconds + int((0 - self.nanos) // 1e9) + nanos = (0 - self.nanos) % 1e9 + else: + result = '' + seconds = self.seconds + int(self.nanos // 1e9) + nanos = self.nanos % 1e9 + result += '%d' % seconds + if (nanos % 1e9) == 0: + # If there are 0 fractional digits, the fractional + # point '.' should be omitted when serializing. + return result + 's' + if (nanos % 1e6) == 0: + # Serialize 3 fractional digits. + return result + '.%03ds' % (nanos / 1e6) + if (nanos % 1e3) == 0: + # Serialize 6 fractional digits. + return result + '.%06ds' % (nanos / 1e3) + # Serialize 9 fractional digits. + return result + '.%09ds' % nanos + + def FromJsonString(self, value): + """Converts a string to Duration. + + Args: + value: A string to be converted. The string must end with 's'. Any + fractional digits (or none) are accepted as long as they fit into + precision. For example: "1s", "1.01s", "1.0000001s", "-3.100s + + Raises: + ValueError: On parsing problems. + """ + if not isinstance(value, str): + raise ValueError('Duration JSON value not a string: {!r}'.format(value)) + if len(value) < 1 or value[-1] != 's': + raise ValueError( + 'Duration must end with letter "s": {0}.'.format(value)) + try: + pos = value.find('.') + if pos == -1: + seconds = int(value[:-1]) + nanos = 0 + else: + seconds = int(value[:pos]) + if value[0] == '-': + nanos = int(round(float('-0{0}'.format(value[pos: -1])) *1e9)) + else: + nanos = int(round(float('0{0}'.format(value[pos: -1])) *1e9)) + _CheckDurationValid(seconds, nanos) + self.seconds = seconds + self.nanos = nanos + except ValueError as e: + raise ValueError( + 'Couldn\'t parse duration: {0} : {1}.'.format(value, e)) + + def ToNanoseconds(self): + """Converts a Duration to nanoseconds.""" + return self.seconds * _NANOS_PER_SECOND + self.nanos + + def ToMicroseconds(self): + """Converts a Duration to microseconds.""" + micros = _RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND) + return self.seconds * _MICROS_PER_SECOND + micros + + def ToMilliseconds(self): + """Converts a Duration to milliseconds.""" + millis = _RoundTowardZero(self.nanos, _NANOS_PER_MILLISECOND) + return self.seconds * _MILLIS_PER_SECOND + millis + + def ToSeconds(self): + """Converts a Duration to seconds.""" + return self.seconds + + def FromNanoseconds(self, nanos): + """Converts nanoseconds to Duration.""" + self._NormalizeDuration(nanos // _NANOS_PER_SECOND, + nanos % _NANOS_PER_SECOND) + + def FromMicroseconds(self, micros): + """Converts microseconds to Duration.""" + self._NormalizeDuration( + micros // _MICROS_PER_SECOND, + (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND) + + def FromMilliseconds(self, millis): + """Converts milliseconds to Duration.""" + self._NormalizeDuration( + millis // _MILLIS_PER_SECOND, + (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND) + + def FromSeconds(self, seconds): + """Converts seconds to Duration.""" + self.seconds = seconds + self.nanos = 0 + + def ToTimedelta(self): + """Converts Duration to timedelta.""" + return datetime.timedelta( + seconds=self.seconds, microseconds=_RoundTowardZero( + self.nanos, _NANOS_PER_MICROSECOND)) + + def FromTimedelta(self, td): + """Converts timedelta to Duration.""" + self._NormalizeDuration(td.seconds + td.days * _SECONDS_PER_DAY, + td.microseconds * _NANOS_PER_MICROSECOND) + + def _NormalizeDuration(self, seconds, nanos): + """Set Duration by seconds and nanos.""" + # Force nanos to be negative if the duration is negative. + if seconds < 0 and nanos > 0: + seconds += 1 + nanos -= _NANOS_PER_SECOND + self.seconds = seconds + self.nanos = nanos + + +def _CheckDurationValid(seconds, nanos): + if seconds < -_DURATION_SECONDS_MAX or seconds > _DURATION_SECONDS_MAX: + raise ValueError( + 'Duration is not valid: Seconds {0} must be in range ' + '[-315576000000, 315576000000].'.format(seconds)) + if nanos <= -_NANOS_PER_SECOND or nanos >= _NANOS_PER_SECOND: + raise ValueError( + 'Duration is not valid: Nanos {0} must be in range ' + '[-999999999, 999999999].'.format(nanos)) + if (nanos < 0 and seconds > 0) or (nanos > 0 and seconds < 0): + raise ValueError( + 'Duration is not valid: Sign mismatch.') + + +def _RoundTowardZero(value, divider): + """Truncates the remainder part after division.""" + # For some languages, the sign of the remainder is implementation + # dependent if any of the operands is negative. Here we enforce + # "rounded toward zero" semantics. For example, for (-5) / 2 an + # implementation may give -3 as the result with the remainder being + # 1. This function ensures we always return -2 (closer to zero). + result = value // divider + remainder = value % divider + if result < 0 and remainder > 0: + return result + 1 + else: + return result + + +class FieldMask(object): + """Class for FieldMask message type.""" + + __slots__ = () + + def ToJsonString(self): + """Converts FieldMask to string according to proto3 JSON spec.""" + camelcase_paths = [] + for path in self.paths: + camelcase_paths.append(_SnakeCaseToCamelCase(path)) + return ','.join(camelcase_paths) + + def FromJsonString(self, value): + """Converts string to FieldMask according to proto3 JSON spec.""" + if not isinstance(value, str): + raise ValueError('FieldMask JSON value not a string: {!r}'.format(value)) + self.Clear() + if value: + for path in value.split(','): + self.paths.append(_CamelCaseToSnakeCase(path)) + + def IsValidForDescriptor(self, message_descriptor): + """Checks whether the FieldMask is valid for Message Descriptor.""" + for path in self.paths: + if not _IsValidPath(message_descriptor, path): + return False + return True + + def AllFieldsFromDescriptor(self, message_descriptor): + """Gets all direct fields of Message Descriptor to FieldMask.""" + self.Clear() + for field in message_descriptor.fields: + self.paths.append(field.name) + + def CanonicalFormFromMask(self, mask): + """Converts a FieldMask to the canonical form. + + Removes paths that are covered by another path. For example, + "foo.bar" is covered by "foo" and will be removed if "foo" + is also in the FieldMask. Then sorts all paths in alphabetical order. + + Args: + mask: The original FieldMask to be converted. + """ + tree = _FieldMaskTree(mask) + tree.ToFieldMask(self) + + def Union(self, mask1, mask2): + """Merges mask1 and mask2 into this FieldMask.""" + _CheckFieldMaskMessage(mask1) + _CheckFieldMaskMessage(mask2) + tree = _FieldMaskTree(mask1) + tree.MergeFromFieldMask(mask2) + tree.ToFieldMask(self) + + def Intersect(self, mask1, mask2): + """Intersects mask1 and mask2 into this FieldMask.""" + _CheckFieldMaskMessage(mask1) + _CheckFieldMaskMessage(mask2) + tree = _FieldMaskTree(mask1) + intersection = _FieldMaskTree() + for path in mask2.paths: + tree.IntersectPath(path, intersection) + intersection.ToFieldMask(self) + + def MergeMessage( + self, source, destination, + replace_message_field=False, replace_repeated_field=False): + """Merges fields specified in FieldMask from source to destination. + + Args: + source: Source message. + destination: The destination message to be merged into. + replace_message_field: Replace message field if True. Merge message + field if False. + replace_repeated_field: Replace repeated field if True. Append + elements of repeated field if False. + """ + tree = _FieldMaskTree(self) + tree.MergeMessage( + source, destination, replace_message_field, replace_repeated_field) + + +def _IsValidPath(message_descriptor, path): + """Checks whether the path is valid for Message Descriptor.""" + parts = path.split('.') + last = parts.pop() + for name in parts: + field = message_descriptor.fields_by_name.get(name) + if (field is None or + field.label == FieldDescriptor.LABEL_REPEATED or + field.type != FieldDescriptor.TYPE_MESSAGE): + return False + message_descriptor = field.message_type + return last in message_descriptor.fields_by_name + + +def _CheckFieldMaskMessage(message): + """Raises ValueError if message is not a FieldMask.""" + message_descriptor = message.DESCRIPTOR + if (message_descriptor.name != 'FieldMask' or + message_descriptor.file.name != 'google/protobuf/field_mask.proto'): + raise ValueError('Message {0} is not a FieldMask.'.format( + message_descriptor.full_name)) + + +def _SnakeCaseToCamelCase(path_name): + """Converts a path name from snake_case to camelCase.""" + result = [] + after_underscore = False + for c in path_name: + if c.isupper(): + raise ValueError( + 'Fail to print FieldMask to Json string: Path name ' + '{0} must not contain uppercase letters.'.format(path_name)) + if after_underscore: + if c.islower(): + result.append(c.upper()) + after_underscore = False + else: + raise ValueError( + 'Fail to print FieldMask to Json string: The ' + 'character after a "_" must be a lowercase letter ' + 'in path name {0}.'.format(path_name)) + elif c == '_': + after_underscore = True + else: + result += c + + if after_underscore: + raise ValueError('Fail to print FieldMask to Json string: Trailing "_" ' + 'in path name {0}.'.format(path_name)) + return ''.join(result) + + +def _CamelCaseToSnakeCase(path_name): + """Converts a field name from camelCase to snake_case.""" + result = [] + for c in path_name: + if c == '_': + raise ValueError('Fail to parse FieldMask: Path name ' + '{0} must not contain "_"s.'.format(path_name)) + if c.isupper(): + result += '_' + result += c.lower() + else: + result += c + return ''.join(result) + + +class _FieldMaskTree(object): + """Represents a FieldMask in a tree structure. + + For example, given a FieldMask "foo.bar,foo.baz,bar.baz", + the FieldMaskTree will be: + [_root] -+- foo -+- bar + | | + | +- baz + | + +- bar --- baz + In the tree, each leaf node represents a field path. + """ + + __slots__ = ('_root',) + + def __init__(self, field_mask=None): + """Initializes the tree by FieldMask.""" + self._root = {} + if field_mask: + self.MergeFromFieldMask(field_mask) + + def MergeFromFieldMask(self, field_mask): + """Merges a FieldMask to the tree.""" + for path in field_mask.paths: + self.AddPath(path) + + def AddPath(self, path): + """Adds a field path into the tree. + + If the field path to add is a sub-path of an existing field path + in the tree (i.e., a leaf node), it means the tree already matches + the given path so nothing will be added to the tree. If the path + matches an existing non-leaf node in the tree, that non-leaf node + will be turned into a leaf node with all its children removed because + the path matches all the node's children. Otherwise, a new path will + be added. + + Args: + path: The field path to add. + """ + node = self._root + for name in path.split('.'): + if name not in node: + node[name] = {} + elif not node[name]: + # Pre-existing empty node implies we already have this entire tree. + return + node = node[name] + # Remove any sub-trees we might have had. + node.clear() + + def ToFieldMask(self, field_mask): + """Converts the tree to a FieldMask.""" + field_mask.Clear() + _AddFieldPaths(self._root, '', field_mask) + + def IntersectPath(self, path, intersection): + """Calculates the intersection part of a field path with this tree. + + Args: + path: The field path to calculates. + intersection: The out tree to record the intersection part. + """ + node = self._root + for name in path.split('.'): + if name not in node: + return + elif not node[name]: + intersection.AddPath(path) + return + node = node[name] + intersection.AddLeafNodes(path, node) + + def AddLeafNodes(self, prefix, node): + """Adds leaf nodes begin with prefix to this tree.""" + if not node: + self.AddPath(prefix) + for name in node: + child_path = prefix + '.' + name + self.AddLeafNodes(child_path, node[name]) + + def MergeMessage( + self, source, destination, + replace_message, replace_repeated): + """Merge all fields specified by this tree from source to destination.""" + _MergeMessage( + self._root, source, destination, replace_message, replace_repeated) + + +def _StrConvert(value): + """Converts value to str if it is not.""" + # This file is imported by c extension and some methods like ClearField + # requires string for the field name. py2/py3 has different text + # type and may use unicode. + if not isinstance(value, str): + return value.encode('utf-8') + return value + + +def _MergeMessage( + node, source, destination, replace_message, replace_repeated): + """Merge all fields specified by a sub-tree from source to destination.""" + source_descriptor = source.DESCRIPTOR + for name in node: + child = node[name] + field = source_descriptor.fields_by_name[name] + if field is None: + raise ValueError('Error: Can\'t find field {0} in message {1}.'.format( + name, source_descriptor.full_name)) + if child: + # Sub-paths are only allowed for singular message fields. + if (field.label == FieldDescriptor.LABEL_REPEATED or + field.cpp_type != FieldDescriptor.CPPTYPE_MESSAGE): + raise ValueError('Error: Field {0} in message {1} is not a singular ' + 'message field and cannot have sub-fields.'.format( + name, source_descriptor.full_name)) + if source.HasField(name): + _MergeMessage( + child, getattr(source, name), getattr(destination, name), + replace_message, replace_repeated) + continue + if field.label == FieldDescriptor.LABEL_REPEATED: + if replace_repeated: + destination.ClearField(_StrConvert(name)) + repeated_source = getattr(source, name) + repeated_destination = getattr(destination, name) + repeated_destination.MergeFrom(repeated_source) + else: + if field.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: + if replace_message: + destination.ClearField(_StrConvert(name)) + if source.HasField(name): + getattr(destination, name).MergeFrom(getattr(source, name)) + else: + setattr(destination, name, getattr(source, name)) + + +def _AddFieldPaths(node, prefix, field_mask): + """Adds the field paths descended from node to field_mask.""" + if not node and prefix: + field_mask.paths.append(prefix) + return + for name in sorted(node): + if prefix: + child_path = prefix + '.' + name + else: + child_path = name + _AddFieldPaths(node[name], child_path, field_mask) + + +def _SetStructValue(struct_value, value): + if value is None: + struct_value.null_value = 0 + elif isinstance(value, bool): + # Note: this check must come before the number check because in Python + # True and False are also considered numbers. + struct_value.bool_value = value + elif isinstance(value, str): + struct_value.string_value = value + elif isinstance(value, (int, float)): + struct_value.number_value = value + elif isinstance(value, (dict, Struct)): + struct_value.struct_value.Clear() + struct_value.struct_value.update(value) + elif isinstance(value, (list, ListValue)): + struct_value.list_value.Clear() + struct_value.list_value.extend(value) + else: + raise ValueError('Unexpected type') + + +def _GetStructValue(struct_value): + which = struct_value.WhichOneof('kind') + if which == 'struct_value': + return struct_value.struct_value + elif which == 'null_value': + return None + elif which == 'number_value': + return struct_value.number_value + elif which == 'string_value': + return struct_value.string_value + elif which == 'bool_value': + return struct_value.bool_value + elif which == 'list_value': + return struct_value.list_value + elif which is None: + raise ValueError('Value not set') + + +class Struct(object): + """Class for Struct message type.""" + + __slots__ = () + + def __getitem__(self, key): + return _GetStructValue(self.fields[key]) + + def __contains__(self, item): + return item in self.fields + + def __setitem__(self, key, value): + _SetStructValue(self.fields[key], value) + + def __delitem__(self, key): + del self.fields[key] + + def __len__(self): + return len(self.fields) + + def __iter__(self): + return iter(self.fields) + + def keys(self): # pylint: disable=invalid-name + return self.fields.keys() + + def values(self): # pylint: disable=invalid-name + return [self[key] for key in self] + + def items(self): # pylint: disable=invalid-name + return [(key, self[key]) for key in self] + + def get_or_create_list(self, key): + """Returns a list for this key, creating if it didn't exist already.""" + if not self.fields[key].HasField('list_value'): + # Clear will mark list_value modified which will indeed create a list. + self.fields[key].list_value.Clear() + return self.fields[key].list_value + + def get_or_create_struct(self, key): + """Returns a struct for this key, creating if it didn't exist already.""" + if not self.fields[key].HasField('struct_value'): + # Clear will mark struct_value modified which will indeed create a struct. + self.fields[key].struct_value.Clear() + return self.fields[key].struct_value + + def update(self, dictionary): # pylint: disable=invalid-name + for key, value in dictionary.items(): + _SetStructValue(self.fields[key], value) + +collections.abc.MutableMapping.register(Struct) + + +class ListValue(object): + """Class for ListValue message type.""" + + __slots__ = () + + def __len__(self): + return len(self.values) + + def append(self, value): + _SetStructValue(self.values.add(), value) + + def extend(self, elem_seq): + for value in elem_seq: + self.append(value) + + def __getitem__(self, index): + """Retrieves item by the specified index.""" + return _GetStructValue(self.values.__getitem__(index)) + + def __setitem__(self, index, value): + _SetStructValue(self.values.__getitem__(index), value) + + def __delitem__(self, key): + del self.values[key] + + def items(self): + for i in range(len(self)): + yield self[i] + + def add_struct(self): + """Appends and returns a struct value as the next value in the list.""" + struct_value = self.values.add().struct_value + # Clear will mark struct_value modified which will indeed create a struct. + struct_value.Clear() + return struct_value + + def add_list(self): + """Appends and returns a list value as the next value in the list.""" + list_value = self.values.add().list_value + # Clear will mark list_value modified which will indeed create a list. + list_value.Clear() + return list_value + +collections.abc.MutableSequence.register(ListValue) + + +WKTBASES = { + 'google.protobuf.Any': Any, + 'google.protobuf.Duration': Duration, + 'google.protobuf.FieldMask': FieldMask, + 'google.protobuf.ListValue': ListValue, + 'google.protobuf.Struct': Struct, + 'google.protobuf.Timestamp': Timestamp, +} diff --git a/scripts/protobuf3/protobuf3/internal/wire_format.py b/scripts/protobuf3/protobuf3/internal/wire_format.py new file mode 100644 index 0000000..883f525 --- /dev/null +++ b/scripts/protobuf3/protobuf3/internal/wire_format.py @@ -0,0 +1,268 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Constants and static functions to support protocol buffer wire format.""" + +__author__ = 'robinson@google.com (Will Robinson)' + +import struct +from google.protobuf import descriptor +from google.protobuf import message + + +TAG_TYPE_BITS = 3 # Number of bits used to hold type info in a proto tag. +TAG_TYPE_MASK = (1 << TAG_TYPE_BITS) - 1 # 0x7 + +# These numbers identify the wire type of a protocol buffer value. +# We use the least-significant TAG_TYPE_BITS bits of the varint-encoded +# tag-and-type to store one of these WIRETYPE_* constants. +# These values must match WireType enum in google/protobuf/wire_format.h. +WIRETYPE_VARINT = 0 +WIRETYPE_FIXED64 = 1 +WIRETYPE_LENGTH_DELIMITED = 2 +WIRETYPE_START_GROUP = 3 +WIRETYPE_END_GROUP = 4 +WIRETYPE_FIXED32 = 5 +_WIRETYPE_MAX = 5 + + +# Bounds for various integer types. +INT32_MAX = int((1 << 31) - 1) +INT32_MIN = int(-(1 << 31)) +UINT32_MAX = (1 << 32) - 1 + +INT64_MAX = (1 << 63) - 1 +INT64_MIN = -(1 << 63) +UINT64_MAX = (1 << 64) - 1 + +# "struct" format strings that will encode/decode the specified formats. +FORMAT_UINT32_LITTLE_ENDIAN = '<I' +FORMAT_UINT64_LITTLE_ENDIAN = '<Q' +FORMAT_FLOAT_LITTLE_ENDIAN = '<f' +FORMAT_DOUBLE_LITTLE_ENDIAN = '<d' + + +# We'll have to provide alternate implementations of AppendLittleEndian*() on +# any architectures where these checks fail. +if struct.calcsize(FORMAT_UINT32_LITTLE_ENDIAN) != 4: + raise AssertionError('Format "I" is not a 32-bit number.') +if struct.calcsize(FORMAT_UINT64_LITTLE_ENDIAN) != 8: + raise AssertionError('Format "Q" is not a 64-bit number.') + + +def PackTag(field_number, wire_type): + """Returns an unsigned 32-bit integer that encodes the field number and + wire type information in standard protocol message wire format. + + Args: + field_number: Expected to be an integer in the range [1, 1 << 29) + wire_type: One of the WIRETYPE_* constants. + """ + if not 0 <= wire_type <= _WIRETYPE_MAX: + raise message.EncodeError('Unknown wire type: %d' % wire_type) + return (field_number << TAG_TYPE_BITS) | wire_type + + +def UnpackTag(tag): + """The inverse of PackTag(). Given an unsigned 32-bit number, + returns a (field_number, wire_type) tuple. + """ + return (tag >> TAG_TYPE_BITS), (tag & TAG_TYPE_MASK) + + +def ZigZagEncode(value): + """ZigZag Transform: Encodes signed integers so that they can be + effectively used with varint encoding. See wire_format.h for + more details. + """ + if value >= 0: + return value << 1 + return (value << 1) ^ (~0) + + +def ZigZagDecode(value): + """Inverse of ZigZagEncode().""" + if not value & 0x1: + return value >> 1 + return (value >> 1) ^ (~0) + + + +# The *ByteSize() functions below return the number of bytes required to +# serialize "field number + type" information and then serialize the value. + + +def Int32ByteSize(field_number, int32): + return Int64ByteSize(field_number, int32) + + +def Int32ByteSizeNoTag(int32): + return _VarUInt64ByteSizeNoTag(0xffffffffffffffff & int32) + + +def Int64ByteSize(field_number, int64): + # Have to convert to uint before calling UInt64ByteSize(). + return UInt64ByteSize(field_number, 0xffffffffffffffff & int64) + + +def UInt32ByteSize(field_number, uint32): + return UInt64ByteSize(field_number, uint32) + + +def UInt64ByteSize(field_number, uint64): + return TagByteSize(field_number) + _VarUInt64ByteSizeNoTag(uint64) + + +def SInt32ByteSize(field_number, int32): + return UInt32ByteSize(field_number, ZigZagEncode(int32)) + + +def SInt64ByteSize(field_number, int64): + return UInt64ByteSize(field_number, ZigZagEncode(int64)) + + +def Fixed32ByteSize(field_number, fixed32): + return TagByteSize(field_number) + 4 + + +def Fixed64ByteSize(field_number, fixed64): + return TagByteSize(field_number) + 8 + + +def SFixed32ByteSize(field_number, sfixed32): + return TagByteSize(field_number) + 4 + + +def SFixed64ByteSize(field_number, sfixed64): + return TagByteSize(field_number) + 8 + + +def FloatByteSize(field_number, flt): + return TagByteSize(field_number) + 4 + + +def DoubleByteSize(field_number, double): + return TagByteSize(field_number) + 8 + + +def BoolByteSize(field_number, b): + return TagByteSize(field_number) + 1 + + +def EnumByteSize(field_number, enum): + return UInt32ByteSize(field_number, enum) + + +def StringByteSize(field_number, string): + return BytesByteSize(field_number, string.encode('utf-8')) + + +def BytesByteSize(field_number, b): + return (TagByteSize(field_number) + + _VarUInt64ByteSizeNoTag(len(b)) + + len(b)) + + +def GroupByteSize(field_number, message): + return (2 * TagByteSize(field_number) # START and END group. + + message.ByteSize()) + + +def MessageByteSize(field_number, message): + return (TagByteSize(field_number) + + _VarUInt64ByteSizeNoTag(message.ByteSize()) + + message.ByteSize()) + + +def MessageSetItemByteSize(field_number, msg): + # First compute the sizes of the tags. + # There are 2 tags for the beginning and ending of the repeated group, that + # is field number 1, one with field number 2 (type_id) and one with field + # number 3 (message). + total_size = (2 * TagByteSize(1) + TagByteSize(2) + TagByteSize(3)) + + # Add the number of bytes for type_id. + total_size += _VarUInt64ByteSizeNoTag(field_number) + + message_size = msg.ByteSize() + + # The number of bytes for encoding the length of the message. + total_size += _VarUInt64ByteSizeNoTag(message_size) + + # The size of the message. + total_size += message_size + return total_size + + +def TagByteSize(field_number): + """Returns the bytes required to serialize a tag with this field number.""" + # Just pass in type 0, since the type won't affect the tag+type size. + return _VarUInt64ByteSizeNoTag(PackTag(field_number, 0)) + + +# Private helper function for the *ByteSize() functions above. + +def _VarUInt64ByteSizeNoTag(uint64): + """Returns the number of bytes required to serialize a single varint + using boundary value comparisons. (unrolled loop optimization -WPierce) + uint64 must be unsigned. + """ + if uint64 <= 0x7f: return 1 + if uint64 <= 0x3fff: return 2 + if uint64 <= 0x1fffff: return 3 + if uint64 <= 0xfffffff: return 4 + if uint64 <= 0x7ffffffff: return 5 + if uint64 <= 0x3ffffffffff: return 6 + if uint64 <= 0x1ffffffffffff: return 7 + if uint64 <= 0xffffffffffffff: return 8 + if uint64 <= 0x7fffffffffffffff: return 9 + if uint64 > UINT64_MAX: + raise message.EncodeError('Value out of range: %d' % uint64) + return 10 + + +NON_PACKABLE_TYPES = ( + descriptor.FieldDescriptor.TYPE_STRING, + descriptor.FieldDescriptor.TYPE_GROUP, + descriptor.FieldDescriptor.TYPE_MESSAGE, + descriptor.FieldDescriptor.TYPE_BYTES +) + + +def IsTypePackable(field_type): + """Return true iff packable = true is valid for fields of this type. + + Args: + field_type: a FieldDescriptor::Type value. + + Returns: + True iff fields of this type are packable. + """ + return field_type not in NON_PACKABLE_TYPES diff --git a/scripts/protobuf3/protobuf3/json_format.py b/scripts/protobuf3/protobuf3/json_format.py new file mode 100644 index 0000000..5024ed8 --- /dev/null +++ b/scripts/protobuf3/protobuf3/json_format.py @@ -0,0 +1,912 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Contains routines for printing protocol messages in JSON format. + +Simple usage example: + + # Create a proto object and serialize it to a json format string. + message = my_proto_pb2.MyMessage(foo='bar') + json_string = json_format.MessageToJson(message) + + # Parse a json format string to proto object. + message = json_format.Parse(json_string, my_proto_pb2.MyMessage()) +""" + +__author__ = 'jieluo@google.com (Jie Luo)' + + +import base64 +from collections import OrderedDict +import json +import math +from operator import methodcaller +import re +import sys + +from google.protobuf.internal import type_checkers +from google.protobuf import descriptor +from google.protobuf import symbol_database + + +_TIMESTAMPFOMAT = '%Y-%m-%dT%H:%M:%S' +_INT_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_INT32, + descriptor.FieldDescriptor.CPPTYPE_UINT32, + descriptor.FieldDescriptor.CPPTYPE_INT64, + descriptor.FieldDescriptor.CPPTYPE_UINT64]) +_INT64_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_INT64, + descriptor.FieldDescriptor.CPPTYPE_UINT64]) +_FLOAT_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_FLOAT, + descriptor.FieldDescriptor.CPPTYPE_DOUBLE]) +_INFINITY = 'Infinity' +_NEG_INFINITY = '-Infinity' +_NAN = 'NaN' + +_UNPAIRED_SURROGATE_PATTERN = re.compile( + u'[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]') + +_VALID_EXTENSION_NAME = re.compile(r'\[[a-zA-Z0-9\._]*\]$') + + +class Error(Exception): + """Top-level module error for json_format.""" + + +class SerializeToJsonError(Error): + """Thrown if serialization to JSON fails.""" + + +class ParseError(Error): + """Thrown in case of parsing error.""" + + +def MessageToJson( + message, + including_default_value_fields=False, + preserving_proto_field_name=False, + indent=2, + sort_keys=False, + use_integers_for_enums=False, + descriptor_pool=None, + float_precision=None, + ensure_ascii=True): + """Converts protobuf message to JSON format. + + Args: + message: The protocol buffers message instance to serialize. + including_default_value_fields: If True, singular primitive fields, + repeated fields, and map fields will always be serialized. If + False, only serialize non-empty fields. Singular message fields + and oneof fields are not affected by this option. + preserving_proto_field_name: If True, use the original proto field + names as defined in the .proto file. If False, convert the field + names to lowerCamelCase. + indent: The JSON object will be pretty-printed with this indent level. + An indent level of 0 or negative will only insert newlines. + sort_keys: If True, then the output will be sorted by field names. + use_integers_for_enums: If true, print integers instead of enum names. + descriptor_pool: A Descriptor Pool for resolving types. If None use the + default. + float_precision: If set, use this to specify float field valid digits. + ensure_ascii: If True, strings with non-ASCII characters are escaped. + If False, Unicode strings are returned unchanged. + + Returns: + A string containing the JSON formatted protocol buffer message. + """ + printer = _Printer( + including_default_value_fields, + preserving_proto_field_name, + use_integers_for_enums, + descriptor_pool, + float_precision=float_precision) + return printer.ToJsonString(message, indent, sort_keys, ensure_ascii) + + +def MessageToDict( + message, + including_default_value_fields=False, + preserving_proto_field_name=False, + use_integers_for_enums=False, + descriptor_pool=None, + float_precision=None): + """Converts protobuf message to a dictionary. + + When the dictionary is encoded to JSON, it conforms to proto3 JSON spec. + + Args: + message: The protocol buffers message instance to serialize. + including_default_value_fields: If True, singular primitive fields, + repeated fields, and map fields will always be serialized. If + False, only serialize non-empty fields. Singular message fields + and oneof fields are not affected by this option. + preserving_proto_field_name: If True, use the original proto field + names as defined in the .proto file. If False, convert the field + names to lowerCamelCase. + use_integers_for_enums: If true, print integers instead of enum names. + descriptor_pool: A Descriptor Pool for resolving types. If None use the + default. + float_precision: If set, use this to specify float field valid digits. + + Returns: + A dict representation of the protocol buffer message. + """ + printer = _Printer( + including_default_value_fields, + preserving_proto_field_name, + use_integers_for_enums, + descriptor_pool, + float_precision=float_precision) + # pylint: disable=protected-access + return printer._MessageToJsonObject(message) + + +def _IsMapEntry(field): + return (field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and + field.message_type.has_options and + field.message_type.GetOptions().map_entry) + + +class _Printer(object): + """JSON format printer for protocol message.""" + + def __init__( + self, + including_default_value_fields=False, + preserving_proto_field_name=False, + use_integers_for_enums=False, + descriptor_pool=None, + float_precision=None): + self.including_default_value_fields = including_default_value_fields + self.preserving_proto_field_name = preserving_proto_field_name + self.use_integers_for_enums = use_integers_for_enums + self.descriptor_pool = descriptor_pool + if float_precision: + self.float_format = '.{}g'.format(float_precision) + else: + self.float_format = None + + def ToJsonString(self, message, indent, sort_keys, ensure_ascii): + js = self._MessageToJsonObject(message) + return json.dumps( + js, indent=indent, sort_keys=sort_keys, ensure_ascii=ensure_ascii) + + def _MessageToJsonObject(self, message): + """Converts message to an object according to Proto3 JSON Specification.""" + message_descriptor = message.DESCRIPTOR + full_name = message_descriptor.full_name + if _IsWrapperMessage(message_descriptor): + return self._WrapperMessageToJsonObject(message) + if full_name in _WKTJSONMETHODS: + return methodcaller(_WKTJSONMETHODS[full_name][0], message)(self) + js = {} + return self._RegularMessageToJsonObject(message, js) + + def _RegularMessageToJsonObject(self, message, js): + """Converts normal message according to Proto3 JSON Specification.""" + fields = message.ListFields() + + try: + for field, value in fields: + if self.preserving_proto_field_name: + name = field.name + else: + name = field.json_name + if _IsMapEntry(field): + # Convert a map field. + v_field = field.message_type.fields_by_name['value'] + js_map = {} + for key in value: + if isinstance(key, bool): + if key: + recorded_key = 'true' + else: + recorded_key = 'false' + else: + recorded_key = str(key) + js_map[recorded_key] = self._FieldToJsonObject( + v_field, value[key]) + js[name] = js_map + elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + # Convert a repeated field. + js[name] = [self._FieldToJsonObject(field, k) + for k in value] + elif field.is_extension: + name = '[%s]' % field.full_name + js[name] = self._FieldToJsonObject(field, value) + else: + js[name] = self._FieldToJsonObject(field, value) + + # Serialize default value if including_default_value_fields is True. + if self.including_default_value_fields: + message_descriptor = message.DESCRIPTOR + for field in message_descriptor.fields: + # Singular message fields and oneof fields will not be affected. + if ((field.label != descriptor.FieldDescriptor.LABEL_REPEATED and + field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE) or + field.containing_oneof): + continue + if self.preserving_proto_field_name: + name = field.name + else: + name = field.json_name + if name in js: + # Skip the field which has been serialized already. + continue + if _IsMapEntry(field): + js[name] = {} + elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + js[name] = [] + else: + js[name] = self._FieldToJsonObject(field, field.default_value) + + except ValueError as e: + raise SerializeToJsonError( + 'Failed to serialize {0} field: {1}.'.format(field.name, e)) + + return js + + def _FieldToJsonObject(self, field, value): + """Converts field value according to Proto3 JSON Specification.""" + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + return self._MessageToJsonObject(value) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM: + if self.use_integers_for_enums: + return value + if field.enum_type.full_name == 'google.protobuf.NullValue': + return None + enum_value = field.enum_type.values_by_number.get(value, None) + if enum_value is not None: + return enum_value.name + else: + if field.file.syntax == 'proto3': + return value + raise SerializeToJsonError('Enum field contains an integer value ' + 'which can not mapped to an enum value.') + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING: + if field.type == descriptor.FieldDescriptor.TYPE_BYTES: + # Use base64 Data encoding for bytes + return base64.b64encode(value).decode('utf-8') + else: + return value + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL: + return bool(value) + elif field.cpp_type in _INT64_TYPES: + return str(value) + elif field.cpp_type in _FLOAT_TYPES: + if math.isinf(value): + if value < 0.0: + return _NEG_INFINITY + else: + return _INFINITY + if math.isnan(value): + return _NAN + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT: + if self.float_format: + return float(format(value, self.float_format)) + else: + return type_checkers.ToShortestFloat(value) + + return value + + def _AnyMessageToJsonObject(self, message): + """Converts Any message according to Proto3 JSON Specification.""" + if not message.ListFields(): + return {} + # Must print @type first, use OrderedDict instead of {} + js = OrderedDict() + type_url = message.type_url + js['@type'] = type_url + sub_message = _CreateMessageFromTypeUrl(type_url, self.descriptor_pool) + sub_message.ParseFromString(message.value) + message_descriptor = sub_message.DESCRIPTOR + full_name = message_descriptor.full_name + if _IsWrapperMessage(message_descriptor): + js['value'] = self._WrapperMessageToJsonObject(sub_message) + return js + if full_name in _WKTJSONMETHODS: + js['value'] = methodcaller(_WKTJSONMETHODS[full_name][0], + sub_message)(self) + return js + return self._RegularMessageToJsonObject(sub_message, js) + + def _GenericMessageToJsonObject(self, message): + """Converts message according to Proto3 JSON Specification.""" + # Duration, Timestamp and FieldMask have ToJsonString method to do the + # convert. Users can also call the method directly. + return message.ToJsonString() + + def _ValueMessageToJsonObject(self, message): + """Converts Value message according to Proto3 JSON Specification.""" + which = message.WhichOneof('kind') + # If the Value message is not set treat as null_value when serialize + # to JSON. The parse back result will be different from original message. + if which is None or which == 'null_value': + return None + if which == 'list_value': + return self._ListValueMessageToJsonObject(message.list_value) + if which == 'struct_value': + value = message.struct_value + else: + value = getattr(message, which) + oneof_descriptor = message.DESCRIPTOR.fields_by_name[which] + return self._FieldToJsonObject(oneof_descriptor, value) + + def _ListValueMessageToJsonObject(self, message): + """Converts ListValue message according to Proto3 JSON Specification.""" + return [self._ValueMessageToJsonObject(value) + for value in message.values] + + def _StructMessageToJsonObject(self, message): + """Converts Struct message according to Proto3 JSON Specification.""" + fields = message.fields + ret = {} + for key in fields: + ret[key] = self._ValueMessageToJsonObject(fields[key]) + return ret + + def _WrapperMessageToJsonObject(self, message): + return self._FieldToJsonObject( + message.DESCRIPTOR.fields_by_name['value'], message.value) + + +def _IsWrapperMessage(message_descriptor): + return message_descriptor.file.name == 'google/protobuf/wrappers.proto' + + +def _DuplicateChecker(js): + result = {} + for name, value in js: + if name in result: + raise ParseError('Failed to load JSON: duplicate key {0}.'.format(name)) + result[name] = value + return result + + +def _CreateMessageFromTypeUrl(type_url, descriptor_pool): + """Creates a message from a type URL.""" + db = symbol_database.Default() + pool = db.pool if descriptor_pool is None else descriptor_pool + type_name = type_url.split('/')[-1] + try: + message_descriptor = pool.FindMessageTypeByName(type_name) + except KeyError: + raise TypeError( + 'Can not find message descriptor by type_url: {0}'.format(type_url)) + message_class = db.GetPrototype(message_descriptor) + return message_class() + + +def Parse(text, + message, + ignore_unknown_fields=False, + descriptor_pool=None, + max_recursion_depth=100): + """Parses a JSON representation of a protocol message into a message. + + Args: + text: Message JSON representation. + message: A protocol buffer message to merge into. + ignore_unknown_fields: If True, do not raise errors for unknown fields. + descriptor_pool: A Descriptor Pool for resolving types. If None use the + default. + max_recursion_depth: max recursion depth of JSON message to be + deserialized. JSON messages over this depth will fail to be + deserialized. Default value is 100. + + Returns: + The same message passed as argument. + + Raises:: + ParseError: On JSON parsing problems. + """ + if not isinstance(text, str): + text = text.decode('utf-8') + try: + js = json.loads(text, object_pairs_hook=_DuplicateChecker) + except ValueError as e: + raise ParseError('Failed to load JSON: {0}.'.format(str(e))) + return ParseDict(js, message, ignore_unknown_fields, descriptor_pool, + max_recursion_depth) + + +def ParseDict(js_dict, + message, + ignore_unknown_fields=False, + descriptor_pool=None, + max_recursion_depth=100): + """Parses a JSON dictionary representation into a message. + + Args: + js_dict: Dict representation of a JSON message. + message: A protocol buffer message to merge into. + ignore_unknown_fields: If True, do not raise errors for unknown fields. + descriptor_pool: A Descriptor Pool for resolving types. If None use the + default. + max_recursion_depth: max recursion depth of JSON message to be + deserialized. JSON messages over this depth will fail to be + deserialized. Default value is 100. + + Returns: + The same message passed as argument. + """ + parser = _Parser(ignore_unknown_fields, descriptor_pool, max_recursion_depth) + parser.ConvertMessage(js_dict, message, '') + return message + + +_INT_OR_FLOAT = (int, float) + + +class _Parser(object): + """JSON format parser for protocol message.""" + + def __init__(self, ignore_unknown_fields, descriptor_pool, + max_recursion_depth): + self.ignore_unknown_fields = ignore_unknown_fields + self.descriptor_pool = descriptor_pool + self.max_recursion_depth = max_recursion_depth + self.recursion_depth = 0 + + def ConvertMessage(self, value, message, path): + """Convert a JSON object into a message. + + Args: + value: A JSON object. + message: A WKT or regular protocol message to record the data. + path: parent path to log parse error info. + + Raises: + ParseError: In case of convert problems. + """ + self.recursion_depth += 1 + if self.recursion_depth > self.max_recursion_depth: + raise ParseError('Message too deep. Max recursion depth is {0}'.format( + self.max_recursion_depth)) + message_descriptor = message.DESCRIPTOR + full_name = message_descriptor.full_name + if not path: + path = message_descriptor.name + if _IsWrapperMessage(message_descriptor): + self._ConvertWrapperMessage(value, message, path) + elif full_name in _WKTJSONMETHODS: + methodcaller(_WKTJSONMETHODS[full_name][1], value, message, path)(self) + else: + self._ConvertFieldValuePair(value, message, path) + self.recursion_depth -= 1 + + def _ConvertFieldValuePair(self, js, message, path): + """Convert field value pairs into regular message. + + Args: + js: A JSON object to convert the field value pairs. + message: A regular protocol message to record the data. + path: parent path to log parse error info. + + Raises: + ParseError: In case of problems converting. + """ + names = [] + message_descriptor = message.DESCRIPTOR + fields_by_json_name = dict((f.json_name, f) + for f in message_descriptor.fields) + for name in js: + try: + field = fields_by_json_name.get(name, None) + if not field: + field = message_descriptor.fields_by_name.get(name, None) + if not field and _VALID_EXTENSION_NAME.match(name): + if not message_descriptor.is_extendable: + raise ParseError( + 'Message type {0} does not have extensions at {1}'.format( + message_descriptor.full_name, path)) + identifier = name[1:-1] # strip [] brackets + # pylint: disable=protected-access + field = message.Extensions._FindExtensionByName(identifier) + # pylint: enable=protected-access + if not field: + # Try looking for extension by the message type name, dropping the + # field name following the final . separator in full_name. + identifier = '.'.join(identifier.split('.')[:-1]) + # pylint: disable=protected-access + field = message.Extensions._FindExtensionByName(identifier) + # pylint: enable=protected-access + if not field: + if self.ignore_unknown_fields: + continue + raise ParseError( + ('Message type "{0}" has no field named "{1}" at "{2}".\n' + ' Available Fields(except extensions): "{3}"').format( + message_descriptor.full_name, name, path, + [f.json_name for f in message_descriptor.fields])) + if name in names: + raise ParseError('Message type "{0}" should not have multiple ' + '"{1}" fields at "{2}".'.format( + message.DESCRIPTOR.full_name, name, path)) + names.append(name) + value = js[name] + # Check no other oneof field is parsed. + if field.containing_oneof is not None and value is not None: + oneof_name = field.containing_oneof.name + if oneof_name in names: + raise ParseError('Message type "{0}" should not have multiple ' + '"{1}" oneof fields at "{2}".'.format( + message.DESCRIPTOR.full_name, oneof_name, + path)) + names.append(oneof_name) + + if value is None: + if (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE + and field.message_type.full_name == 'google.protobuf.Value'): + sub_message = getattr(message, field.name) + sub_message.null_value = 0 + elif (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM + and field.enum_type.full_name == 'google.protobuf.NullValue'): + setattr(message, field.name, 0) + else: + message.ClearField(field.name) + continue + + # Parse field value. + if _IsMapEntry(field): + message.ClearField(field.name) + self._ConvertMapFieldValue(value, message, field, + '{0}.{1}'.format(path, name)) + elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + message.ClearField(field.name) + if not isinstance(value, list): + raise ParseError('repeated field {0} must be in [] which is ' + '{1} at {2}'.format(name, value, path)) + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + # Repeated message field. + for index, item in enumerate(value): + sub_message = getattr(message, field.name).add() + # None is a null_value in Value. + if (item is None and + sub_message.DESCRIPTOR.full_name != 'google.protobuf.Value'): + raise ParseError('null is not allowed to be used as an element' + ' in a repeated field at {0}.{1}[{2}]'.format( + path, name, index)) + self.ConvertMessage(item, sub_message, + '{0}.{1}[{2}]'.format(path, name, index)) + else: + # Repeated scalar field. + for index, item in enumerate(value): + if item is None: + raise ParseError('null is not allowed to be used as an element' + ' in a repeated field at {0}.{1}[{2}]'.format( + path, name, index)) + getattr(message, field.name).append( + _ConvertScalarFieldValue( + item, field, '{0}.{1}[{2}]'.format(path, name, index))) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + if field.is_extension: + sub_message = message.Extensions[field] + else: + sub_message = getattr(message, field.name) + sub_message.SetInParent() + self.ConvertMessage(value, sub_message, '{0}.{1}'.format(path, name)) + else: + if field.is_extension: + message.Extensions[field] = _ConvertScalarFieldValue( + value, field, '{0}.{1}'.format(path, name)) + else: + setattr( + message, field.name, + _ConvertScalarFieldValue(value, field, + '{0}.{1}'.format(path, name))) + except ParseError as e: + if field and field.containing_oneof is None: + raise ParseError('Failed to parse {0} field: {1}.'.format(name, e)) + else: + raise ParseError(str(e)) + except ValueError as e: + raise ParseError('Failed to parse {0} field: {1}.'.format(name, e)) + except TypeError as e: + raise ParseError('Failed to parse {0} field: {1}.'.format(name, e)) + + def _ConvertAnyMessage(self, value, message, path): + """Convert a JSON representation into Any message.""" + if isinstance(value, dict) and not value: + return + try: + type_url = value['@type'] + except KeyError: + raise ParseError( + '@type is missing when parsing any message at {0}'.format(path)) + + try: + sub_message = _CreateMessageFromTypeUrl(type_url, self.descriptor_pool) + except TypeError as e: + raise ParseError('{0} at {1}'.format(e, path)) + message_descriptor = sub_message.DESCRIPTOR + full_name = message_descriptor.full_name + if _IsWrapperMessage(message_descriptor): + self._ConvertWrapperMessage(value['value'], sub_message, + '{0}.value'.format(path)) + elif full_name in _WKTJSONMETHODS: + methodcaller(_WKTJSONMETHODS[full_name][1], value['value'], sub_message, + '{0}.value'.format(path))( + self) + else: + del value['@type'] + self._ConvertFieldValuePair(value, sub_message, path) + value['@type'] = type_url + # Sets Any message + message.value = sub_message.SerializeToString() + message.type_url = type_url + + def _ConvertGenericMessage(self, value, message, path): + """Convert a JSON representation into message with FromJsonString.""" + # Duration, Timestamp, FieldMask have a FromJsonString method to do the + # conversion. Users can also call the method directly. + try: + message.FromJsonString(value) + except ValueError as e: + raise ParseError('{0} at {1}'.format(e, path)) + + def _ConvertValueMessage(self, value, message, path): + """Convert a JSON representation into Value message.""" + if isinstance(value, dict): + self._ConvertStructMessage(value, message.struct_value, path) + elif isinstance(value, list): + self._ConvertListValueMessage(value, message.list_value, path) + elif value is None: + message.null_value = 0 + elif isinstance(value, bool): + message.bool_value = value + elif isinstance(value, str): + message.string_value = value + elif isinstance(value, _INT_OR_FLOAT): + message.number_value = value + else: + raise ParseError('Value {0} has unexpected type {1} at {2}'.format( + value, type(value), path)) + + def _ConvertListValueMessage(self, value, message, path): + """Convert a JSON representation into ListValue message.""" + if not isinstance(value, list): + raise ParseError('ListValue must be in [] which is {0} at {1}'.format( + value, path)) + message.ClearField('values') + for index, item in enumerate(value): + self._ConvertValueMessage(item, message.values.add(), + '{0}[{1}]'.format(path, index)) + + def _ConvertStructMessage(self, value, message, path): + """Convert a JSON representation into Struct message.""" + if not isinstance(value, dict): + raise ParseError('Struct must be in a dict which is {0} at {1}'.format( + value, path)) + # Clear will mark the struct as modified so it will be created even if + # there are no values. + message.Clear() + for key in value: + self._ConvertValueMessage(value[key], message.fields[key], + '{0}.{1}'.format(path, key)) + return + + def _ConvertWrapperMessage(self, value, message, path): + """Convert a JSON representation into Wrapper message.""" + field = message.DESCRIPTOR.fields_by_name['value'] + setattr( + message, 'value', + _ConvertScalarFieldValue(value, field, path='{0}.value'.format(path))) + + def _ConvertMapFieldValue(self, value, message, field, path): + """Convert map field value for a message map field. + + Args: + value: A JSON object to convert the map field value. + message: A protocol message to record the converted data. + field: The descriptor of the map field to be converted. + path: parent path to log parse error info. + + Raises: + ParseError: In case of convert problems. + """ + if not isinstance(value, dict): + raise ParseError( + 'Map field {0} must be in a dict which is {1} at {2}'.format( + field.name, value, path)) + key_field = field.message_type.fields_by_name['key'] + value_field = field.message_type.fields_by_name['value'] + for key in value: + key_value = _ConvertScalarFieldValue(key, key_field, + '{0}.key'.format(path), True) + if value_field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + self.ConvertMessage(value[key], + getattr(message, field.name)[key_value], + '{0}[{1}]'.format(path, key_value)) + else: + getattr(message, field.name)[key_value] = _ConvertScalarFieldValue( + value[key], value_field, path='{0}[{1}]'.format(path, key_value)) + + +def _ConvertScalarFieldValue(value, field, path, require_str=False): + """Convert a single scalar field value. + + Args: + value: A scalar value to convert the scalar field value. + field: The descriptor of the field to convert. + path: parent path to log parse error info. + require_str: If True, the field value must be a str. + + Returns: + The converted scalar field value + + Raises: + ParseError: In case of convert problems. + """ + try: + if field.cpp_type in _INT_TYPES: + return _ConvertInteger(value) + elif field.cpp_type in _FLOAT_TYPES: + return _ConvertFloat(value, field) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL: + return _ConvertBool(value, require_str) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING: + if field.type == descriptor.FieldDescriptor.TYPE_BYTES: + if isinstance(value, str): + encoded = value.encode('utf-8') + else: + encoded = value + # Add extra padding '=' + padded_value = encoded + b'=' * (4 - len(encoded) % 4) + return base64.urlsafe_b64decode(padded_value) + else: + # Checking for unpaired surrogates appears to be unreliable, + # depending on the specific Python version, so we check manually. + if _UNPAIRED_SURROGATE_PATTERN.search(value): + raise ParseError('Unpaired surrogate') + return value + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM: + # Convert an enum value. + enum_value = field.enum_type.values_by_name.get(value, None) + if enum_value is None: + try: + number = int(value) + enum_value = field.enum_type.values_by_number.get(number, None) + except ValueError: + raise ParseError('Invalid enum value {0} for enum type {1}'.format( + value, field.enum_type.full_name)) + if enum_value is None: + if field.file.syntax == 'proto3': + # Proto3 accepts unknown enums. + return number + raise ParseError('Invalid enum value {0} for enum type {1}'.format( + value, field.enum_type.full_name)) + return enum_value.number + except ParseError as e: + raise ParseError('{0} at {1}'.format(e, path)) + + +def _ConvertInteger(value): + """Convert an integer. + + Args: + value: A scalar value to convert. + + Returns: + The integer value. + + Raises: + ParseError: If an integer couldn't be consumed. + """ + if isinstance(value, float) and not value.is_integer(): + raise ParseError('Couldn\'t parse integer: {0}'.format(value)) + + if isinstance(value, str) and value.find(' ') != -1: + raise ParseError('Couldn\'t parse integer: "{0}"'.format(value)) + + if isinstance(value, bool): + raise ParseError('Bool value {0} is not acceptable for ' + 'integer field'.format(value)) + + return int(value) + + +def _ConvertFloat(value, field): + """Convert an floating point number.""" + if isinstance(value, float): + if math.isnan(value): + raise ParseError('Couldn\'t parse NaN, use quoted "NaN" instead') + if math.isinf(value): + if value > 0: + raise ParseError('Couldn\'t parse Infinity or value too large, ' + 'use quoted "Infinity" instead') + else: + raise ParseError('Couldn\'t parse -Infinity or value too small, ' + 'use quoted "-Infinity" instead') + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT: + # pylint: disable=protected-access + if value > type_checkers._FLOAT_MAX: + raise ParseError('Float value too large') + # pylint: disable=protected-access + if value < type_checkers._FLOAT_MIN: + raise ParseError('Float value too small') + if value == 'nan': + raise ParseError('Couldn\'t parse float "nan", use "NaN" instead') + try: + # Assume Python compatible syntax. + return float(value) + except ValueError: + # Check alternative spellings. + if value == _NEG_INFINITY: + return float('-inf') + elif value == _INFINITY: + return float('inf') + elif value == _NAN: + return float('nan') + else: + raise ParseError('Couldn\'t parse float: {0}'.format(value)) + + +def _ConvertBool(value, require_str): + """Convert a boolean value. + + Args: + value: A scalar value to convert. + require_str: If True, value must be a str. + + Returns: + The bool parsed. + + Raises: + ParseError: If a boolean value couldn't be consumed. + """ + if require_str: + if value == 'true': + return True + elif value == 'false': + return False + else: + raise ParseError('Expected "true" or "false", not {0}'.format(value)) + + if not isinstance(value, bool): + raise ParseError('Expected true or false without quotes') + return value + +_WKTJSONMETHODS = { + 'google.protobuf.Any': ['_AnyMessageToJsonObject', + '_ConvertAnyMessage'], + 'google.protobuf.Duration': ['_GenericMessageToJsonObject', + '_ConvertGenericMessage'], + 'google.protobuf.FieldMask': ['_GenericMessageToJsonObject', + '_ConvertGenericMessage'], + 'google.protobuf.ListValue': ['_ListValueMessageToJsonObject', + '_ConvertListValueMessage'], + 'google.protobuf.Struct': ['_StructMessageToJsonObject', + '_ConvertStructMessage'], + 'google.protobuf.Timestamp': ['_GenericMessageToJsonObject', + '_ConvertGenericMessage'], + 'google.protobuf.Value': ['_ValueMessageToJsonObject', + '_ConvertValueMessage'] +} diff --git a/scripts/protobuf3/protobuf3/message.py b/scripts/protobuf3/protobuf3/message.py new file mode 100644 index 0000000..76c6802 --- /dev/null +++ b/scripts/protobuf3/protobuf3/message.py @@ -0,0 +1,424 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# TODO(robinson): We should just make these methods all "pure-virtual" and move +# all implementation out, into reflection.py for now. + + +"""Contains an abstract base class for protocol messages.""" + +__author__ = 'robinson@google.com (Will Robinson)' + +class Error(Exception): + """Base error type for this module.""" + pass + + +class DecodeError(Error): + """Exception raised when deserializing messages.""" + pass + + +class EncodeError(Error): + """Exception raised when serializing messages.""" + pass + + +class Message(object): + + """Abstract base class for protocol messages. + + Protocol message classes are almost always generated by the protocol + compiler. These generated types subclass Message and implement the methods + shown below. + """ + + # TODO(robinson): Link to an HTML document here. + + # TODO(robinson): Document that instances of this class will also + # have an Extensions attribute with __getitem__ and __setitem__. + # Again, not sure how to best convey this. + + # TODO(robinson): Document that the class must also have a static + # RegisterExtension(extension_field) method. + # Not sure how to best express at this point. + + # TODO(robinson): Document these fields and methods. + + __slots__ = [] + + #: The :class:`google.protobuf.descriptor.Descriptor` for this message type. + DESCRIPTOR = None + + def __deepcopy__(self, memo=None): + clone = type(self)() + clone.MergeFrom(self) + return clone + + def __eq__(self, other_msg): + """Recursively compares two messages by value and structure.""" + raise NotImplementedError + + def __ne__(self, other_msg): + # Can't just say self != other_msg, since that would infinitely recurse. :) + return not self == other_msg + + def __hash__(self): + raise TypeError('unhashable object') + + def __str__(self): + """Outputs a human-readable representation of the message.""" + raise NotImplementedError + + def __unicode__(self): + """Outputs a human-readable representation of the message.""" + raise NotImplementedError + + def MergeFrom(self, other_msg): + """Merges the contents of the specified message into current message. + + This method merges the contents of the specified message into the current + message. Singular fields that are set in the specified message overwrite + the corresponding fields in the current message. Repeated fields are + appended. Singular sub-messages and groups are recursively merged. + + Args: + other_msg (Message): A message to merge into the current message. + """ + raise NotImplementedError + + def CopyFrom(self, other_msg): + """Copies the content of the specified message into the current message. + + The method clears the current message and then merges the specified + message using MergeFrom. + + Args: + other_msg (Message): A message to copy into the current one. + """ + if self is other_msg: + return + self.Clear() + self.MergeFrom(other_msg) + + def Clear(self): + """Clears all data that was set in the message.""" + raise NotImplementedError + + def SetInParent(self): + """Mark this as present in the parent. + + This normally happens automatically when you assign a field of a + sub-message, but sometimes you want to make the sub-message + present while keeping it empty. If you find yourself using this, + you may want to reconsider your design. + """ + raise NotImplementedError + + def IsInitialized(self): + """Checks if the message is initialized. + + Returns: + bool: The method returns True if the message is initialized (i.e. all of + its required fields are set). + """ + raise NotImplementedError + + # TODO(robinson): MergeFromString() should probably return None and be + # implemented in terms of a helper that returns the # of bytes read. Our + # deserialization routines would use the helper when recursively + # deserializing, but the end user would almost always just want the no-return + # MergeFromString(). + + def MergeFromString(self, serialized): + """Merges serialized protocol buffer data into this message. + + When we find a field in `serialized` that is already present + in this message: + + - If it's a "repeated" field, we append to the end of our list. + - Else, if it's a scalar, we overwrite our field. + - Else, (it's a nonrepeated composite), we recursively merge + into the existing composite. + + Args: + serialized (bytes): Any object that allows us to call + ``memoryview(serialized)`` to access a string of bytes using the + buffer interface. + + Returns: + int: The number of bytes read from `serialized`. + For non-group messages, this will always be `len(serialized)`, + but for messages which are actually groups, this will + generally be less than `len(serialized)`, since we must + stop when we reach an ``END_GROUP`` tag. Note that if + we *do* stop because of an ``END_GROUP`` tag, the number + of bytes returned does not include the bytes + for the ``END_GROUP`` tag information. + + Raises: + DecodeError: if the input cannot be parsed. + """ + # TODO(robinson): Document handling of unknown fields. + # TODO(robinson): When we switch to a helper, this will return None. + raise NotImplementedError + + def ParseFromString(self, serialized): + """Parse serialized protocol buffer data into this message. + + Like :func:`MergeFromString()`, except we clear the object first. + + Raises: + message.DecodeError if the input cannot be parsed. + """ + self.Clear() + return self.MergeFromString(serialized) + + def SerializeToString(self, **kwargs): + """Serializes the protocol message to a binary string. + + Keyword Args: + deterministic (bool): If true, requests deterministic serialization + of the protobuf, with predictable ordering of map keys. + + Returns: + A binary string representation of the message if all of the required + fields in the message are set (i.e. the message is initialized). + + Raises: + EncodeError: if the message isn't initialized (see :func:`IsInitialized`). + """ + raise NotImplementedError + + def SerializePartialToString(self, **kwargs): + """Serializes the protocol message to a binary string. + + This method is similar to SerializeToString but doesn't check if the + message is initialized. + + Keyword Args: + deterministic (bool): If true, requests deterministic serialization + of the protobuf, with predictable ordering of map keys. + + Returns: + bytes: A serialized representation of the partial message. + """ + raise NotImplementedError + + # TODO(robinson): Decide whether we like these better + # than auto-generated has_foo() and clear_foo() methods + # on the instances themselves. This way is less consistent + # with C++, but it makes reflection-type access easier and + # reduces the number of magically autogenerated things. + # + # TODO(robinson): Be sure to document (and test) exactly + # which field names are accepted here. Are we case-sensitive? + # What do we do with fields that share names with Python keywords + # like 'lambda' and 'yield'? + # + # nnorwitz says: + # """ + # Typically (in python), an underscore is appended to names that are + # keywords. So they would become lambda_ or yield_. + # """ + def ListFields(self): + """Returns a list of (FieldDescriptor, value) tuples for present fields. + + A message field is non-empty if HasField() would return true. A singular + primitive field is non-empty if HasField() would return true in proto2 or it + is non zero in proto3. A repeated field is non-empty if it contains at least + one element. The fields are ordered by field number. + + Returns: + list[tuple(FieldDescriptor, value)]: field descriptors and values + for all fields in the message which are not empty. The values vary by + field type. + """ + raise NotImplementedError + + def HasField(self, field_name): + """Checks if a certain field is set for the message. + + For a oneof group, checks if any field inside is set. Note that if the + field_name is not defined in the message descriptor, :exc:`ValueError` will + be raised. + + Args: + field_name (str): The name of the field to check for presence. + + Returns: + bool: Whether a value has been set for the named field. + + Raises: + ValueError: if the `field_name` is not a member of this message. + """ + raise NotImplementedError + + def ClearField(self, field_name): + """Clears the contents of a given field. + + Inside a oneof group, clears the field set. If the name neither refers to a + defined field or oneof group, :exc:`ValueError` is raised. + + Args: + field_name (str): The name of the field to check for presence. + + Raises: + ValueError: if the `field_name` is not a member of this message. + """ + raise NotImplementedError + + def WhichOneof(self, oneof_group): + """Returns the name of the field that is set inside a oneof group. + + If no field is set, returns None. + + Args: + oneof_group (str): the name of the oneof group to check. + + Returns: + str or None: The name of the group that is set, or None. + + Raises: + ValueError: no group with the given name exists + """ + raise NotImplementedError + + def HasExtension(self, extension_handle): + """Checks if a certain extension is present for this message. + + Extensions are retrieved using the :attr:`Extensions` mapping (if present). + + Args: + extension_handle: The handle for the extension to check. + + Returns: + bool: Whether the extension is present for this message. + + Raises: + KeyError: if the extension is repeated. Similar to repeated fields, + there is no separate notion of presence: a "not present" repeated + extension is an empty list. + """ + raise NotImplementedError + + def ClearExtension(self, extension_handle): + """Clears the contents of a given extension. + + Args: + extension_handle: The handle for the extension to clear. + """ + raise NotImplementedError + + def UnknownFields(self): + """Returns the UnknownFieldSet. + + Returns: + UnknownFieldSet: The unknown fields stored in this message. + """ + raise NotImplementedError + + def DiscardUnknownFields(self): + """Clears all fields in the :class:`UnknownFieldSet`. + + This operation is recursive for nested message. + """ + raise NotImplementedError + + def ByteSize(self): + """Returns the serialized size of this message. + + Recursively calls ByteSize() on all contained messages. + + Returns: + int: The number of bytes required to serialize this message. + """ + raise NotImplementedError + + @classmethod + def FromString(cls, s): + raise NotImplementedError + + @staticmethod + def RegisterExtension(extension_handle): + raise NotImplementedError + + def _SetListener(self, message_listener): + """Internal method used by the protocol message implementation. + Clients should not call this directly. + + Sets a listener that this message will call on certain state transitions. + + The purpose of this method is to register back-edges from children to + parents at runtime, for the purpose of setting "has" bits and + byte-size-dirty bits in the parent and ancestor objects whenever a child or + descendant object is modified. + + If the client wants to disconnect this Message from the object tree, she + explicitly sets callback to None. + + If message_listener is None, unregisters any existing listener. Otherwise, + message_listener must implement the MessageListener interface in + internal/message_listener.py, and we discard any listener registered + via a previous _SetListener() call. + """ + raise NotImplementedError + + def __getstate__(self): + """Support the pickle protocol.""" + return dict(serialized=self.SerializePartialToString()) + + def __setstate__(self, state): + """Support the pickle protocol.""" + self.__init__() + serialized = state['serialized'] + # On Python 3, using encoding='latin1' is required for unpickling + # protos pickled by Python 2. + if not isinstance(serialized, bytes): + serialized = serialized.encode('latin1') + self.ParseFromString(serialized) + + def __reduce__(self): + message_descriptor = self.DESCRIPTOR + if message_descriptor.containing_type is None: + return type(self), (), self.__getstate__() + # the message type must be nested. + # Python does not pickle nested classes; use the symbol_database on the + # receiving end. + container = message_descriptor + return (_InternalConstructMessage, (container.full_name,), + self.__getstate__()) + + +def _InternalConstructMessage(full_name): + """Constructs a nested message.""" + from google.protobuf import symbol_database # pylint:disable=g-import-not-at-top + + return symbol_database.Default().GetSymbol(full_name)() diff --git a/scripts/protobuf3/protobuf3/message_factory.py b/scripts/protobuf3/protobuf3/message_factory.py new file mode 100644 index 0000000..3656fa6 --- /dev/null +++ b/scripts/protobuf3/protobuf3/message_factory.py @@ -0,0 +1,185 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Provides a factory class for generating dynamic messages. + +The easiest way to use this class is if you have access to the FileDescriptor +protos containing the messages you want to create you can just do the following: + +message_classes = message_factory.GetMessages(iterable_of_file_descriptors) +my_proto_instance = message_classes['some.proto.package.MessageName']() +""" + +__author__ = 'matthewtoia@google.com (Matt Toia)' + +from google.protobuf.internal import api_implementation +from google.protobuf import descriptor_pool +from google.protobuf import message + +if api_implementation.Type() == 'cpp': + from google.protobuf.pyext import cpp_message as message_impl +else: + from google.protobuf.internal import python_message as message_impl + + +# The type of all Message classes. +_GENERATED_PROTOCOL_MESSAGE_TYPE = message_impl.GeneratedProtocolMessageType + + +class MessageFactory(object): + """Factory for creating Proto2 messages from descriptors in a pool.""" + + def __init__(self, pool=None): + """Initializes a new factory.""" + self.pool = pool or descriptor_pool.DescriptorPool() + + # local cache of all classes built from protobuf descriptors + self._classes = {} + + def GetPrototype(self, descriptor): + """Obtains a proto2 message class based on the passed in descriptor. + + Passing a descriptor with a fully qualified name matching a previous + invocation will cause the same class to be returned. + + Args: + descriptor: The descriptor to build from. + + Returns: + A class describing the passed in descriptor. + """ + if descriptor not in self._classes: + result_class = self.CreatePrototype(descriptor) + # The assignment to _classes is redundant for the base implementation, but + # might avoid confusion in cases where CreatePrototype gets overridden and + # does not call the base implementation. + self._classes[descriptor] = result_class + return result_class + return self._classes[descriptor] + + def CreatePrototype(self, descriptor): + """Builds a proto2 message class based on the passed in descriptor. + + Don't call this function directly, it always creates a new class. Call + GetPrototype() instead. This method is meant to be overridden in subblasses + to perform additional operations on the newly constructed class. + + Args: + descriptor: The descriptor to build from. + + Returns: + A class describing the passed in descriptor. + """ + descriptor_name = descriptor.name + result_class = _GENERATED_PROTOCOL_MESSAGE_TYPE( + descriptor_name, + (message.Message,), + { + 'DESCRIPTOR': descriptor, + # If module not set, it wrongly points to message_factory module. + '__module__': None, + }) + result_class._FACTORY = self # pylint: disable=protected-access + # Assign in _classes before doing recursive calls to avoid infinite + # recursion. + self._classes[descriptor] = result_class + for field in descriptor.fields: + if field.message_type: + self.GetPrototype(field.message_type) + for extension in result_class.DESCRIPTOR.extensions: + if extension.containing_type not in self._classes: + self.GetPrototype(extension.containing_type) + extended_class = self._classes[extension.containing_type] + extended_class.RegisterExtension(extension) + return result_class + + def GetMessages(self, files): + """Gets all the messages from a specified file. + + This will find and resolve dependencies, failing if the descriptor + pool cannot satisfy them. + + Args: + files: The file names to extract messages from. + + Returns: + A dictionary mapping proto names to the message classes. This will include + any dependent messages as well as any messages defined in the same file as + a specified message. + """ + result = {} + for file_name in files: + file_desc = self.pool.FindFileByName(file_name) + for desc in file_desc.message_types_by_name.values(): + result[desc.full_name] = self.GetPrototype(desc) + + # While the extension FieldDescriptors are created by the descriptor pool, + # the python classes created in the factory need them to be registered + # explicitly, which is done below. + # + # The call to RegisterExtension will specifically check if the + # extension was already registered on the object and either + # ignore the registration if the original was the same, or raise + # an error if they were different. + + for extension in file_desc.extensions_by_name.values(): + if extension.containing_type not in self._classes: + self.GetPrototype(extension.containing_type) + extended_class = self._classes[extension.containing_type] + extended_class.RegisterExtension(extension) + return result + + +_FACTORY = MessageFactory() + + +def GetMessages(file_protos): + """Builds a dictionary of all the messages available in a set of files. + + Args: + file_protos: Iterable of FileDescriptorProto to build messages out of. + + Returns: + A dictionary mapping proto names to the message classes. This will include + any dependent messages as well as any messages defined in the same file as + a specified message. + """ + # The cpp implementation of the protocol buffer library requires to add the + # message in topological order of the dependency graph. + file_by_name = {file_proto.name: file_proto for file_proto in file_protos} + def _AddFile(file_proto): + for dependency in file_proto.dependency: + if dependency in file_by_name: + # Remove from elements to be visited, in order to cut cycles. + _AddFile(file_by_name.pop(dependency)) + _FACTORY.pool.Add(file_proto) + while file_by_name: + _AddFile(file_by_name.popitem()[1]) + return _FACTORY.GetMessages([file_proto.name for file_proto in file_protos]) diff --git a/scripts/protobuf3/protobuf3/proto_builder.py b/scripts/protobuf3/protobuf3/proto_builder.py new file mode 100644 index 0000000..a4667ce --- /dev/null +++ b/scripts/protobuf3/protobuf3/proto_builder.py @@ -0,0 +1,134 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Dynamic Protobuf class creator.""" + +from collections import OrderedDict +import hashlib +import os + +from google.protobuf import descriptor_pb2 +from google.protobuf import descriptor +from google.protobuf import message_factory + + +def _GetMessageFromFactory(factory, full_name): + """Get a proto class from the MessageFactory by name. + + Args: + factory: a MessageFactory instance. + full_name: str, the fully qualified name of the proto type. + Returns: + A class, for the type identified by full_name. + Raises: + KeyError, if the proto is not found in the factory's descriptor pool. + """ + proto_descriptor = factory.pool.FindMessageTypeByName(full_name) + proto_cls = factory.GetPrototype(proto_descriptor) + return proto_cls + + +def MakeSimpleProtoClass(fields, full_name=None, pool=None): + """Create a Protobuf class whose fields are basic types. + + Note: this doesn't validate field names! + + Args: + fields: dict of {name: field_type} mappings for each field in the proto. If + this is an OrderedDict the order will be maintained, otherwise the + fields will be sorted by name. + full_name: optional str, the fully-qualified name of the proto type. + pool: optional DescriptorPool instance. + Returns: + a class, the new protobuf class with a FileDescriptor. + """ + factory = message_factory.MessageFactory(pool=pool) + + if full_name is not None: + try: + proto_cls = _GetMessageFromFactory(factory, full_name) + return proto_cls + except KeyError: + # The factory's DescriptorPool doesn't know about this class yet. + pass + + # Get a list of (name, field_type) tuples from the fields dict. If fields was + # an OrderedDict we keep the order, but otherwise we sort the field to ensure + # consistent ordering. + field_items = fields.items() + if not isinstance(fields, OrderedDict): + field_items = sorted(field_items) + + # Use a consistent file name that is unlikely to conflict with any imported + # proto files. + fields_hash = hashlib.sha1() + for f_name, f_type in field_items: + fields_hash.update(f_name.encode('utf-8')) + fields_hash.update(str(f_type).encode('utf-8')) + proto_file_name = fields_hash.hexdigest() + '.proto' + + # If the proto is anonymous, use the same hash to name it. + if full_name is None: + full_name = ('net.proto2.python.public.proto_builder.AnonymousProto_' + + fields_hash.hexdigest()) + try: + proto_cls = _GetMessageFromFactory(factory, full_name) + return proto_cls + except KeyError: + # The factory's DescriptorPool doesn't know about this class yet. + pass + + # This is the first time we see this proto: add a new descriptor to the pool. + factory.pool.Add( + _MakeFileDescriptorProto(proto_file_name, full_name, field_items)) + return _GetMessageFromFactory(factory, full_name) + + +def _MakeFileDescriptorProto(proto_file_name, full_name, field_items): + """Populate FileDescriptorProto for MessageFactory's DescriptorPool.""" + package, name = full_name.rsplit('.', 1) + file_proto = descriptor_pb2.FileDescriptorProto() + file_proto.name = os.path.join(package.replace('.', '/'), proto_file_name) + file_proto.package = package + desc_proto = file_proto.message_type.add() + desc_proto.name = name + for f_number, (f_name, f_type) in enumerate(field_items, 1): + field_proto = desc_proto.field.add() + field_proto.name = f_name + # # If the number falls in the reserved range, reassign it to the correct + # # number after the range. + if f_number >= descriptor.FieldDescriptor.FIRST_RESERVED_FIELD_NUMBER: + f_number += ( + descriptor.FieldDescriptor.LAST_RESERVED_FIELD_NUMBER - + descriptor.FieldDescriptor.FIRST_RESERVED_FIELD_NUMBER + 1) + field_proto.number = f_number + field_proto.label = descriptor_pb2.FieldDescriptorProto.LABEL_OPTIONAL + field_proto.type = f_type + return file_proto diff --git a/scripts/protobuf3/protobuf3/pyext/__init__.py b/scripts/protobuf3/protobuf3/pyext/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/protobuf3/protobuf3/pyext/cpp_message.py b/scripts/protobuf3/protobuf3/pyext/cpp_message.py new file mode 100644 index 0000000..fc8eb32 --- /dev/null +++ b/scripts/protobuf3/protobuf3/pyext/cpp_message.py @@ -0,0 +1,65 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Protocol message implementation hooks for C++ implementation. + +Contains helper functions used to create protocol message classes from +Descriptor objects at runtime backed by the protocol buffer C++ API. +""" + +__author__ = 'tibell@google.com (Johan Tibell)' + +from google.protobuf.pyext import _message + + +class GeneratedProtocolMessageType(_message.MessageMeta): + + """Metaclass for protocol message classes created at runtime from Descriptors. + + The protocol compiler currently uses this metaclass to create protocol + message classes at runtime. Clients can also manually create their own + classes at runtime, as in this example: + + mydescriptor = Descriptor(.....) + factory = symbol_database.Default() + factory.pool.AddDescriptor(mydescriptor) + MyProtoClass = factory.GetPrototype(mydescriptor) + myproto_instance = MyProtoClass() + myproto.foo_field = 23 + ... + + The above example will not work for nested types. If you wish to include them, + use reflection.MakeClass() instead of manually instantiating the class in + order to create the appropriate class structure. + """ + + # Must be consistent with the protocol-compiler code in + # proto2/compiler/internal/generator.*. + _DESCRIPTOR_KEY = 'DESCRIPTOR' diff --git a/scripts/protobuf3/protobuf3/pyext/python_pb2.py b/scripts/protobuf3/protobuf3/pyext/python_pb2.py new file mode 100644 index 0000000..2c6ecf4 --- /dev/null +++ b/scripts/protobuf3/protobuf3/pyext/python_pb2.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/pyext/python.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"google/protobuf/pyext/python.proto\x12\x1fgoogle.protobuf.python.internal\"\xbc\x02\n\x0cTestAllTypes\x12\\\n\x17repeated_nested_message\x18\x01 \x03(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage\x12\\\n\x17optional_nested_message\x18\x02 \x01(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage\x12\x16\n\x0eoptional_int32\x18\x03 \x01(\x05\x1aX\n\rNestedMessage\x12\n\n\x02\x62\x62\x18\x01 \x01(\x05\x12;\n\x02\x63\x63\x18\x02 \x01(\x0b\x32/.google.protobuf.python.internal.ForeignMessage\"&\n\x0e\x46oreignMessage\x12\t\n\x01\x63\x18\x01 \x01(\x05\x12\t\n\x01\x64\x18\x02 \x03(\x05\"\x1d\n\x11TestAllExtensions*\x08\x08\x01\x10\x80\x80\x80\x80\x02:\x9a\x01\n!optional_nested_message_extension\x12\x32.google.protobuf.python.internal.TestAllExtensions\x18\x01 \x01(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessage:\x9a\x01\n!repeated_nested_message_extension\x12\x32.google.protobuf.python.internal.TestAllExtensions\x18\x02 \x03(\x0b\x32;.google.protobuf.python.internal.TestAllTypes.NestedMessageB\x02H\x01') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.pyext.python_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + TestAllExtensions.RegisterExtension(optional_nested_message_extension) + TestAllExtensions.RegisterExtension(repeated_nested_message_extension) + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'H\001' + _TESTALLTYPES._serialized_start=72 + _TESTALLTYPES._serialized_end=388 + _TESTALLTYPES_NESTEDMESSAGE._serialized_start=300 + _TESTALLTYPES_NESTEDMESSAGE._serialized_end=388 + _FOREIGNMESSAGE._serialized_start=390 + _FOREIGNMESSAGE._serialized_end=428 + _TESTALLEXTENSIONS._serialized_start=430 + _TESTALLEXTENSIONS._serialized_end=459 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/reflection.py b/scripts/protobuf3/protobuf3/reflection.py new file mode 100644 index 0000000..81e1885 --- /dev/null +++ b/scripts/protobuf3/protobuf3/reflection.py @@ -0,0 +1,95 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# This code is meant to work on Python 2.4 and above only. + +"""Contains a metaclass and helper functions used to create +protocol message classes from Descriptor objects at runtime. + +Recall that a metaclass is the "type" of a class. +(A class is to a metaclass what an instance is to a class.) + +In this case, we use the GeneratedProtocolMessageType metaclass +to inject all the useful functionality into the classes +output by the protocol compiler at compile-time. + +The upshot of all this is that the real implementation +details for ALL pure-Python protocol buffers are *here in +this file*. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + + +from google.protobuf import message_factory +from google.protobuf import symbol_database + +# The type of all Message classes. +# Part of the public interface, but normally only used by message factories. +GeneratedProtocolMessageType = message_factory._GENERATED_PROTOCOL_MESSAGE_TYPE + +MESSAGE_CLASS_CACHE = {} + + +# Deprecated. Please NEVER use reflection.ParseMessage(). +def ParseMessage(descriptor, byte_str): + """Generate a new Message instance from this Descriptor and a byte string. + + DEPRECATED: ParseMessage is deprecated because it is using MakeClass(). + Please use MessageFactory.GetPrototype() instead. + + Args: + descriptor: Protobuf Descriptor object + byte_str: Serialized protocol buffer byte string + + Returns: + Newly created protobuf Message object. + """ + result_class = MakeClass(descriptor) + new_msg = result_class() + new_msg.ParseFromString(byte_str) + return new_msg + + +# Deprecated. Please NEVER use reflection.MakeClass(). +def MakeClass(descriptor): + """Construct a class object for a protobuf described by descriptor. + + DEPRECATED: use MessageFactory.GetPrototype() instead. + + Args: + descriptor: A descriptor.Descriptor object describing the protobuf. + Returns: + The Message class object described by the descriptor. + """ + # Original implementation leads to duplicate message classes, which won't play + # well with extensions. Message factory info is also missing. + # Redirect to message_factory. + return symbol_database.Default().GetPrototype(descriptor) diff --git a/scripts/protobuf3/protobuf3/service.py b/scripts/protobuf3/protobuf3/service.py new file mode 100644 index 0000000..5625246 --- /dev/null +++ b/scripts/protobuf3/protobuf3/service.py @@ -0,0 +1,228 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""DEPRECATED: Declares the RPC service interfaces. + +This module declares the abstract interfaces underlying proto2 RPC +services. These are intended to be independent of any particular RPC +implementation, so that proto2 services can be used on top of a variety +of implementations. Starting with version 2.3.0, RPC implementations should +not try to build on these, but should instead provide code generator plugins +which generate code specific to the particular RPC implementation. This way +the generated code can be more appropriate for the implementation in use +and can avoid unnecessary layers of indirection. +""" + +__author__ = 'petar@google.com (Petar Petrov)' + + +class RpcException(Exception): + """Exception raised on failed blocking RPC method call.""" + pass + + +class Service(object): + + """Abstract base interface for protocol-buffer-based RPC services. + + Services themselves are abstract classes (implemented either by servers or as + stubs), but they subclass this base interface. The methods of this + interface can be used to call the methods of the service without knowing + its exact type at compile time (analogous to the Message interface). + """ + + def GetDescriptor(): + """Retrieves this service's descriptor.""" + raise NotImplementedError + + def CallMethod(self, method_descriptor, rpc_controller, + request, done): + """Calls a method of the service specified by method_descriptor. + + If "done" is None then the call is blocking and the response + message will be returned directly. Otherwise the call is asynchronous + and "done" will later be called with the response value. + + In the blocking case, RpcException will be raised on error. + + Preconditions: + + * method_descriptor.service == GetDescriptor + * request is of the exact same classes as returned by + GetRequestClass(method). + * After the call has started, the request must not be modified. + * "rpc_controller" is of the correct type for the RPC implementation being + used by this Service. For stubs, the "correct type" depends on the + RpcChannel which the stub is using. + + Postconditions: + + * "done" will be called when the method is complete. This may be + before CallMethod() returns or it may be at some point in the future. + * If the RPC failed, the response value passed to "done" will be None. + Further details about the failure can be found by querying the + RpcController. + """ + raise NotImplementedError + + def GetRequestClass(self, method_descriptor): + """Returns the class of the request message for the specified method. + + CallMethod() requires that the request is of a particular subclass of + Message. GetRequestClass() gets the default instance of this required + type. + + Example: + method = service.GetDescriptor().FindMethodByName("Foo") + request = stub.GetRequestClass(method)() + request.ParseFromString(input) + service.CallMethod(method, request, callback) + """ + raise NotImplementedError + + def GetResponseClass(self, method_descriptor): + """Returns the class of the response message for the specified method. + + This method isn't really needed, as the RpcChannel's CallMethod constructs + the response protocol message. It's provided anyway in case it is useful + for the caller to know the response type in advance. + """ + raise NotImplementedError + + +class RpcController(object): + + """An RpcController mediates a single method call. + + The primary purpose of the controller is to provide a way to manipulate + settings specific to the RPC implementation and to find out about RPC-level + errors. The methods provided by the RpcController interface are intended + to be a "least common denominator" set of features which we expect all + implementations to support. Specific implementations may provide more + advanced features (e.g. deadline propagation). + """ + + # Client-side methods below + + def Reset(self): + """Resets the RpcController to its initial state. + + After the RpcController has been reset, it may be reused in + a new call. Must not be called while an RPC is in progress. + """ + raise NotImplementedError + + def Failed(self): + """Returns true if the call failed. + + After a call has finished, returns true if the call failed. The possible + reasons for failure depend on the RPC implementation. Failed() must not + be called before a call has finished. If Failed() returns true, the + contents of the response message are undefined. + """ + raise NotImplementedError + + def ErrorText(self): + """If Failed is true, returns a human-readable description of the error.""" + raise NotImplementedError + + def StartCancel(self): + """Initiate cancellation. + + Advises the RPC system that the caller desires that the RPC call be + canceled. The RPC system may cancel it immediately, may wait awhile and + then cancel it, or may not even cancel the call at all. If the call is + canceled, the "done" callback will still be called and the RpcController + will indicate that the call failed at that time. + """ + raise NotImplementedError + + # Server-side methods below + + def SetFailed(self, reason): + """Sets a failure reason. + + Causes Failed() to return true on the client side. "reason" will be + incorporated into the message returned by ErrorText(). If you find + you need to return machine-readable information about failures, you + should incorporate it into your response protocol buffer and should + NOT call SetFailed(). + """ + raise NotImplementedError + + def IsCanceled(self): + """Checks if the client cancelled the RPC. + + If true, indicates that the client canceled the RPC, so the server may + as well give up on replying to it. The server should still call the + final "done" callback. + """ + raise NotImplementedError + + def NotifyOnCancel(self, callback): + """Sets a callback to invoke on cancel. + + Asks that the given callback be called when the RPC is canceled. The + callback will always be called exactly once. If the RPC completes without + being canceled, the callback will be called after completion. If the RPC + has already been canceled when NotifyOnCancel() is called, the callback + will be called immediately. + + NotifyOnCancel() must be called no more than once per request. + """ + raise NotImplementedError + + +class RpcChannel(object): + + """Abstract interface for an RPC channel. + + An RpcChannel represents a communication line to a service which can be used + to call that service's methods. The service may be running on another + machine. Normally, you should not use an RpcChannel directly, but instead + construct a stub {@link Service} wrapping it. Example: + + Example: + RpcChannel channel = rpcImpl.Channel("remotehost.example.com:1234") + RpcController controller = rpcImpl.Controller() + MyService service = MyService_Stub(channel) + service.MyMethod(controller, request, callback) + """ + + def CallMethod(self, method_descriptor, rpc_controller, + request, response_class, done): + """Calls the method identified by the descriptor. + + Call the given method of the remote service. The signature of this + procedure looks the same as Service.CallMethod(), but the requirements + are less strict in one important way: the request object doesn't have to + be of any specific class as long as its descriptor is method.input_type. + """ + raise NotImplementedError diff --git a/scripts/protobuf3/protobuf3/service_reflection.py b/scripts/protobuf3/protobuf3/service_reflection.py new file mode 100644 index 0000000..f82ab71 --- /dev/null +++ b/scripts/protobuf3/protobuf3/service_reflection.py @@ -0,0 +1,295 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Contains metaclasses used to create protocol service and service stub +classes from ServiceDescriptor objects at runtime. + +The GeneratedServiceType and GeneratedServiceStubType metaclasses are used to +inject all useful functionality into the classes output by the protocol +compiler at compile-time. +""" + +__author__ = 'petar@google.com (Petar Petrov)' + + +class GeneratedServiceType(type): + + """Metaclass for service classes created at runtime from ServiceDescriptors. + + Implementations for all methods described in the Service class are added here + by this class. We also create properties to allow getting/setting all fields + in the protocol message. + + The protocol compiler currently uses this metaclass to create protocol service + classes at runtime. Clients can also manually create their own classes at + runtime, as in this example:: + + mydescriptor = ServiceDescriptor(.....) + class MyProtoService(service.Service): + __metaclass__ = GeneratedServiceType + DESCRIPTOR = mydescriptor + myservice_instance = MyProtoService() + # ... + """ + + _DESCRIPTOR_KEY = 'DESCRIPTOR' + + def __init__(cls, name, bases, dictionary): + """Creates a message service class. + + Args: + name: Name of the class (ignored, but required by the metaclass + protocol). + bases: Base classes of the class being constructed. + dictionary: The class dictionary of the class being constructed. + dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object + describing this protocol service type. + """ + # Don't do anything if this class doesn't have a descriptor. This happens + # when a service class is subclassed. + if GeneratedServiceType._DESCRIPTOR_KEY not in dictionary: + return + + descriptor = dictionary[GeneratedServiceType._DESCRIPTOR_KEY] + service_builder = _ServiceBuilder(descriptor) + service_builder.BuildService(cls) + cls.DESCRIPTOR = descriptor + + +class GeneratedServiceStubType(GeneratedServiceType): + + """Metaclass for service stubs created at runtime from ServiceDescriptors. + + This class has similar responsibilities as GeneratedServiceType, except that + it creates the service stub classes. + """ + + _DESCRIPTOR_KEY = 'DESCRIPTOR' + + def __init__(cls, name, bases, dictionary): + """Creates a message service stub class. + + Args: + name: Name of the class (ignored, here). + bases: Base classes of the class being constructed. + dictionary: The class dictionary of the class being constructed. + dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object + describing this protocol service type. + """ + super(GeneratedServiceStubType, cls).__init__(name, bases, dictionary) + # Don't do anything if this class doesn't have a descriptor. This happens + # when a service stub is subclassed. + if GeneratedServiceStubType._DESCRIPTOR_KEY not in dictionary: + return + + descriptor = dictionary[GeneratedServiceStubType._DESCRIPTOR_KEY] + service_stub_builder = _ServiceStubBuilder(descriptor) + service_stub_builder.BuildServiceStub(cls) + + +class _ServiceBuilder(object): + + """This class constructs a protocol service class using a service descriptor. + + Given a service descriptor, this class constructs a class that represents + the specified service descriptor. One service builder instance constructs + exactly one service class. That means all instances of that class share the + same builder. + """ + + def __init__(self, service_descriptor): + """Initializes an instance of the service class builder. + + Args: + service_descriptor: ServiceDescriptor to use when constructing the + service class. + """ + self.descriptor = service_descriptor + + def BuildService(builder, cls): + """Constructs the service class. + + Args: + cls: The class that will be constructed. + """ + + # CallMethod needs to operate with an instance of the Service class. This + # internal wrapper function exists only to be able to pass the service + # instance to the method that does the real CallMethod work. + # Making sure to use exact argument names from the abstract interface in + # service.py to match the type signature + def _WrapCallMethod(self, method_descriptor, rpc_controller, request, done): + return builder._CallMethod(self, method_descriptor, rpc_controller, + request, done) + + def _WrapGetRequestClass(self, method_descriptor): + return builder._GetRequestClass(method_descriptor) + + def _WrapGetResponseClass(self, method_descriptor): + return builder._GetResponseClass(method_descriptor) + + builder.cls = cls + cls.CallMethod = _WrapCallMethod + cls.GetDescriptor = staticmethod(lambda: builder.descriptor) + cls.GetDescriptor.__doc__ = 'Returns the service descriptor.' + cls.GetRequestClass = _WrapGetRequestClass + cls.GetResponseClass = _WrapGetResponseClass + for method in builder.descriptor.methods: + setattr(cls, method.name, builder._GenerateNonImplementedMethod(method)) + + def _CallMethod(self, srvc, method_descriptor, + rpc_controller, request, callback): + """Calls the method described by a given method descriptor. + + Args: + srvc: Instance of the service for which this method is called. + method_descriptor: Descriptor that represent the method to call. + rpc_controller: RPC controller to use for this method's execution. + request: Request protocol message. + callback: A callback to invoke after the method has completed. + """ + if method_descriptor.containing_service != self.descriptor: + raise RuntimeError( + 'CallMethod() given method descriptor for wrong service type.') + method = getattr(srvc, method_descriptor.name) + return method(rpc_controller, request, callback) + + def _GetRequestClass(self, method_descriptor): + """Returns the class of the request protocol message. + + Args: + method_descriptor: Descriptor of the method for which to return the + request protocol message class. + + Returns: + A class that represents the input protocol message of the specified + method. + """ + if method_descriptor.containing_service != self.descriptor: + raise RuntimeError( + 'GetRequestClass() given method descriptor for wrong service type.') + return method_descriptor.input_type._concrete_class + + def _GetResponseClass(self, method_descriptor): + """Returns the class of the response protocol message. + + Args: + method_descriptor: Descriptor of the method for which to return the + response protocol message class. + + Returns: + A class that represents the output protocol message of the specified + method. + """ + if method_descriptor.containing_service != self.descriptor: + raise RuntimeError( + 'GetResponseClass() given method descriptor for wrong service type.') + return method_descriptor.output_type._concrete_class + + def _GenerateNonImplementedMethod(self, method): + """Generates and returns a method that can be set for a service methods. + + Args: + method: Descriptor of the service method for which a method is to be + generated. + + Returns: + A method that can be added to the service class. + """ + return lambda inst, rpc_controller, request, callback: ( + self._NonImplementedMethod(method.name, rpc_controller, callback)) + + def _NonImplementedMethod(self, method_name, rpc_controller, callback): + """The body of all methods in the generated service class. + + Args: + method_name: Name of the method being executed. + rpc_controller: RPC controller used to execute this method. + callback: A callback which will be invoked when the method finishes. + """ + rpc_controller.SetFailed('Method %s not implemented.' % method_name) + callback(None) + + +class _ServiceStubBuilder(object): + + """Constructs a protocol service stub class using a service descriptor. + + Given a service descriptor, this class constructs a suitable stub class. + A stub is just a type-safe wrapper around an RpcChannel which emulates a + local implementation of the service. + + One service stub builder instance constructs exactly one class. It means all + instances of that class share the same service stub builder. + """ + + def __init__(self, service_descriptor): + """Initializes an instance of the service stub class builder. + + Args: + service_descriptor: ServiceDescriptor to use when constructing the + stub class. + """ + self.descriptor = service_descriptor + + def BuildServiceStub(self, cls): + """Constructs the stub class. + + Args: + cls: The class that will be constructed. + """ + + def _ServiceStubInit(stub, rpc_channel): + stub.rpc_channel = rpc_channel + self.cls = cls + cls.__init__ = _ServiceStubInit + for method in self.descriptor.methods: + setattr(cls, method.name, self._GenerateStubMethod(method)) + + def _GenerateStubMethod(self, method): + return (lambda inst, rpc_controller, request, callback=None: + self._StubMethod(inst, method, rpc_controller, request, callback)) + + def _StubMethod(self, stub, method_descriptor, + rpc_controller, request, callback): + """The body of all service methods in the generated stub class. + + Args: + stub: Stub instance. + method_descriptor: Descriptor of the invoked method. + rpc_controller: Rpc controller to execute the method. + request: Request protocol message. + callback: A callback to execute when the method finishes. + Returns: + Response message (in case of blocking call). + """ + return stub.rpc_channel.CallMethod( + method_descriptor, rpc_controller, request, + method_descriptor.output_type._concrete_class, callback) diff --git a/scripts/protobuf3/protobuf3/source_context_pb2.py b/scripts/protobuf3/protobuf3/source_context_pb2.py new file mode 100644 index 0000000..30cca2e --- /dev/null +++ b/scripts/protobuf3/protobuf3/source_context_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/source_context.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$google/protobuf/source_context.proto\x12\x0fgoogle.protobuf\"\"\n\rSourceContext\x12\x11\n\tfile_name\x18\x01 \x01(\tB\x8a\x01\n\x13\x63om.google.protobufB\x12SourceContextProtoP\x01Z6google.golang.org/protobuf/types/known/sourcecontextpb\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.source_context_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\022SourceContextProtoP\001Z6google.golang.org/protobuf/types/known/sourcecontextpb\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _SOURCECONTEXT._serialized_start=57 + _SOURCECONTEXT._serialized_end=91 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/struct_pb2.py b/scripts/protobuf3/protobuf3/struct_pb2.py new file mode 100644 index 0000000..149728c --- /dev/null +++ b/scripts/protobuf3/protobuf3/struct_pb2.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/struct.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cgoogle/protobuf/struct.proto\x12\x0fgoogle.protobuf\"\x84\x01\n\x06Struct\x12\x33\n\x06\x66ields\x18\x01 \x03(\x0b\x32#.google.protobuf.Struct.FieldsEntry\x1a\x45\n\x0b\x46ieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value:\x02\x38\x01\"\xea\x01\n\x05Value\x12\x30\n\nnull_value\x18\x01 \x01(\x0e\x32\x1a.google.protobuf.NullValueH\x00\x12\x16\n\x0cnumber_value\x18\x02 \x01(\x01H\x00\x12\x16\n\x0cstring_value\x18\x03 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x04 \x01(\x08H\x00\x12/\n\x0cstruct_value\x18\x05 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x12\x30\n\nlist_value\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.ListValueH\x00\x42\x06\n\x04kind\"3\n\tListValue\x12&\n\x06values\x18\x01 \x03(\x0b\x32\x16.google.protobuf.Value*\x1b\n\tNullValue\x12\x0e\n\nNULL_VALUE\x10\x00\x42\x7f\n\x13\x63om.google.protobufB\x0bStructProtoP\x01Z/google.golang.org/protobuf/types/known/structpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.struct_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\013StructProtoP\001Z/google.golang.org/protobuf/types/known/structpb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _STRUCT_FIELDSENTRY._options = None + _STRUCT_FIELDSENTRY._serialized_options = b'8\001' + _NULLVALUE._serialized_start=474 + _NULLVALUE._serialized_end=501 + _STRUCT._serialized_start=50 + _STRUCT._serialized_end=182 + _STRUCT_FIELDSENTRY._serialized_start=113 + _STRUCT_FIELDSENTRY._serialized_end=182 + _VALUE._serialized_start=185 + _VALUE._serialized_end=419 + _LISTVALUE._serialized_start=421 + _LISTVALUE._serialized_end=472 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/symbol_database.py b/scripts/protobuf3/protobuf3/symbol_database.py new file mode 100644 index 0000000..fdcf8cf --- /dev/null +++ b/scripts/protobuf3/protobuf3/symbol_database.py @@ -0,0 +1,194 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""A database of Python protocol buffer generated symbols. + +SymbolDatabase is the MessageFactory for messages generated at compile time, +and makes it easy to create new instances of a registered type, given only the +type's protocol buffer symbol name. + +Example usage:: + + db = symbol_database.SymbolDatabase() + + # Register symbols of interest, from one or multiple files. + db.RegisterFileDescriptor(my_proto_pb2.DESCRIPTOR) + db.RegisterMessage(my_proto_pb2.MyMessage) + db.RegisterEnumDescriptor(my_proto_pb2.MyEnum.DESCRIPTOR) + + # The database can be used as a MessageFactory, to generate types based on + # their name: + types = db.GetMessages(['my_proto.proto']) + my_message_instance = types['MyMessage']() + + # The database's underlying descriptor pool can be queried, so it's not + # necessary to know a type's filename to be able to generate it: + filename = db.pool.FindFileContainingSymbol('MyMessage') + my_message_instance = db.GetMessages([filename])['MyMessage']() + + # This functionality is also provided directly via a convenience method: + my_message_instance = db.GetSymbol('MyMessage')() +""" + + +from google.protobuf.internal import api_implementation +from google.protobuf import descriptor_pool +from google.protobuf import message_factory + + +class SymbolDatabase(message_factory.MessageFactory): + """A database of Python generated symbols.""" + + def RegisterMessage(self, message): + """Registers the given message type in the local database. + + Calls to GetSymbol() and GetMessages() will return messages registered here. + + Args: + message: A :class:`google.protobuf.message.Message` subclass (or + instance); its descriptor will be registered. + + Returns: + The provided message. + """ + + desc = message.DESCRIPTOR + self._classes[desc] = message + self.RegisterMessageDescriptor(desc) + return message + + def RegisterMessageDescriptor(self, message_descriptor): + """Registers the given message descriptor in the local database. + + Args: + message_descriptor (Descriptor): the message descriptor to add. + """ + if api_implementation.Type() == 'python': + # pylint: disable=protected-access + self.pool._AddDescriptor(message_descriptor) + + def RegisterEnumDescriptor(self, enum_descriptor): + """Registers the given enum descriptor in the local database. + + Args: + enum_descriptor (EnumDescriptor): The enum descriptor to register. + + Returns: + EnumDescriptor: The provided descriptor. + """ + if api_implementation.Type() == 'python': + # pylint: disable=protected-access + self.pool._AddEnumDescriptor(enum_descriptor) + return enum_descriptor + + def RegisterServiceDescriptor(self, service_descriptor): + """Registers the given service descriptor in the local database. + + Args: + service_descriptor (ServiceDescriptor): the service descriptor to + register. + """ + if api_implementation.Type() == 'python': + # pylint: disable=protected-access + self.pool._AddServiceDescriptor(service_descriptor) + + def RegisterFileDescriptor(self, file_descriptor): + """Registers the given file descriptor in the local database. + + Args: + file_descriptor (FileDescriptor): The file descriptor to register. + """ + if api_implementation.Type() == 'python': + # pylint: disable=protected-access + self.pool._InternalAddFileDescriptor(file_descriptor) + + def GetSymbol(self, symbol): + """Tries to find a symbol in the local database. + + Currently, this method only returns message.Message instances, however, if + may be extended in future to support other symbol types. + + Args: + symbol (str): a protocol buffer symbol. + + Returns: + A Python class corresponding to the symbol. + + Raises: + KeyError: if the symbol could not be found. + """ + + return self._classes[self.pool.FindMessageTypeByName(symbol)] + + def GetMessages(self, files): + # TODO(amauryfa): Fix the differences with MessageFactory. + """Gets all registered messages from a specified file. + + Only messages already created and registered will be returned; (this is the + case for imported _pb2 modules) + But unlike MessageFactory, this version also returns already defined nested + messages, but does not register any message extensions. + + Args: + files (list[str]): The file names to extract messages from. + + Returns: + A dictionary mapping proto names to the message classes. + + Raises: + KeyError: if a file could not be found. + """ + + def _GetAllMessages(desc): + """Walk a message Descriptor and recursively yields all message names.""" + yield desc + for msg_desc in desc.nested_types: + for nested_desc in _GetAllMessages(msg_desc): + yield nested_desc + + result = {} + for file_name in files: + file_desc = self.pool.FindFileByName(file_name) + for msg_desc in file_desc.message_types_by_name.values(): + for desc in _GetAllMessages(msg_desc): + try: + result[desc.full_name] = self._classes[desc] + except KeyError: + # This descriptor has no registered class, skip it. + pass + return result + + +_DEFAULT = SymbolDatabase(pool=descriptor_pool.Default()) + + +def Default(): + """Returns the default SymbolDatabase.""" + return _DEFAULT diff --git a/scripts/protobuf3/protobuf3/text_encoding.py b/scripts/protobuf3/protobuf3/text_encoding.py new file mode 100644 index 0000000..759cf11 --- /dev/null +++ b/scripts/protobuf3/protobuf3/text_encoding.py @@ -0,0 +1,110 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Encoding related utilities.""" +import re + +_cescape_chr_to_symbol_map = {} +_cescape_chr_to_symbol_map[9] = r'\t' # optional escape +_cescape_chr_to_symbol_map[10] = r'\n' # optional escape +_cescape_chr_to_symbol_map[13] = r'\r' # optional escape +_cescape_chr_to_symbol_map[34] = r'\"' # necessary escape +_cescape_chr_to_symbol_map[39] = r"\'" # optional escape +_cescape_chr_to_symbol_map[92] = r'\\' # necessary escape + +# Lookup table for unicode +_cescape_unicode_to_str = [chr(i) for i in range(0, 256)] +for byte, string in _cescape_chr_to_symbol_map.items(): + _cescape_unicode_to_str[byte] = string + +# Lookup table for non-utf8, with necessary escapes at (o >= 127 or o < 32) +_cescape_byte_to_str = ([r'\%03o' % i for i in range(0, 32)] + + [chr(i) for i in range(32, 127)] + + [r'\%03o' % i for i in range(127, 256)]) +for byte, string in _cescape_chr_to_symbol_map.items(): + _cescape_byte_to_str[byte] = string +del byte, string + + +def CEscape(text, as_utf8): + # type: (...) -> str + """Escape a bytes string for use in an text protocol buffer. + + Args: + text: A byte string to be escaped. + as_utf8: Specifies if result may contain non-ASCII characters. + In Python 3 this allows unescaped non-ASCII Unicode characters. + In Python 2 the return value will be valid UTF-8 rather than only ASCII. + Returns: + Escaped string (str). + """ + # Python's text.encode() 'string_escape' or 'unicode_escape' codecs do not + # satisfy our needs; they encodes unprintable characters using two-digit hex + # escapes whereas our C++ unescaping function allows hex escapes to be any + # length. So, "\0011".encode('string_escape') ends up being "\\x011", which + # will be decoded in C++ as a single-character string with char code 0x11. + text_is_unicode = isinstance(text, str) + if as_utf8 and text_is_unicode: + # We're already unicode, no processing beyond control char escapes. + return text.translate(_cescape_chr_to_symbol_map) + ord_ = ord if text_is_unicode else lambda x: x # bytes iterate as ints. + if as_utf8: + return ''.join(_cescape_unicode_to_str[ord_(c)] for c in text) + return ''.join(_cescape_byte_to_str[ord_(c)] for c in text) + + +_CUNESCAPE_HEX = re.compile(r'(\\+)x([0-9a-fA-F])(?![0-9a-fA-F])') + + +def CUnescape(text): + # type: (str) -> bytes + """Unescape a text string with C-style escape sequences to UTF-8 bytes. + + Args: + text: The data to parse in a str. + Returns: + A byte string. + """ + + def ReplaceHex(m): + # Only replace the match if the number of leading back slashes is odd. i.e. + # the slash itself is not escaped. + if len(m.group(1)) & 1: + return m.group(1) + 'x0' + m.group(2) + return m.group(0) + + # This is required because the 'string_escape' encoding doesn't + # allow single-digit hex escapes (like '\xf'). + result = _CUNESCAPE_HEX.sub(ReplaceHex, text) + + return (result.encode('utf-8') # Make it bytes to allow decode. + .decode('unicode_escape') + # Make it bytes again to return the proper type. + .encode('raw_unicode_escape')) diff --git a/scripts/protobuf3/protobuf3/text_format.py b/scripts/protobuf3/protobuf3/text_format.py new file mode 100644 index 0000000..412385c --- /dev/null +++ b/scripts/protobuf3/protobuf3/text_format.py @@ -0,0 +1,1795 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Contains routines for printing protocol messages in text format. + +Simple usage example:: + + # Create a proto object and serialize it to a text proto string. + message = my_proto_pb2.MyMessage(foo='bar') + text_proto = text_format.MessageToString(message) + + # Parse a text proto string. + message = text_format.Parse(text_proto, my_proto_pb2.MyMessage()) +""" + +__author__ = 'kenton@google.com (Kenton Varda)' + +# TODO(b/129989314) Import thread contention leads to test failures. +import encodings.raw_unicode_escape # pylint: disable=unused-import +import encodings.unicode_escape # pylint: disable=unused-import +import io +import math +import re + +from google.protobuf.internal import decoder +from google.protobuf.internal import type_checkers +from google.protobuf import descriptor +from google.protobuf import text_encoding + +# pylint: disable=g-import-not-at-top +__all__ = ['MessageToString', 'Parse', 'PrintMessage', 'PrintField', + 'PrintFieldValue', 'Merge', 'MessageToBytes'] + +_INTEGER_CHECKERS = (type_checkers.Uint32ValueChecker(), + type_checkers.Int32ValueChecker(), + type_checkers.Uint64ValueChecker(), + type_checkers.Int64ValueChecker()) +_FLOAT_INFINITY = re.compile('-?inf(?:inity)?f?$', re.IGNORECASE) +_FLOAT_NAN = re.compile('nanf?$', re.IGNORECASE) +_QUOTES = frozenset(("'", '"')) +_ANY_FULL_TYPE_NAME = 'google.protobuf.Any' + + +class Error(Exception): + """Top-level module error for text_format.""" + + +class ParseError(Error): + """Thrown in case of text parsing or tokenizing error.""" + + def __init__(self, message=None, line=None, column=None): + if message is not None and line is not None: + loc = str(line) + if column is not None: + loc += ':{0}'.format(column) + message = '{0} : {1}'.format(loc, message) + if message is not None: + super(ParseError, self).__init__(message) + else: + super(ParseError, self).__init__() + self._line = line + self._column = column + + def GetLine(self): + return self._line + + def GetColumn(self): + return self._column + + +class TextWriter(object): + + def __init__(self, as_utf8): + self._writer = io.StringIO() + + def write(self, val): + return self._writer.write(val) + + def close(self): + return self._writer.close() + + def getvalue(self): + return self._writer.getvalue() + + +def MessageToString( + message, + as_utf8=False, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + use_field_number=False, + descriptor_pool=None, + indent=0, + message_formatter=None, + print_unknown_fields=False, + force_colon=False): + # type: (...) -> str + """Convert protobuf message to text format. + + Double values can be formatted compactly with 15 digits of + precision (which is the most that IEEE 754 "double" can guarantee) + using double_format='.15g'. To ensure that converting to text and back to a + proto will result in an identical value, double_format='.17g' should be used. + + Args: + message: The protocol buffers message. + as_utf8: Return unescaped Unicode for non-ASCII characters. + In Python 3 actual Unicode characters may appear as is in strings. + In Python 2 the return value will be valid UTF-8 rather than only ASCII. + as_one_line: Don't introduce newlines between fields. + use_short_repeated_primitives: Use short repeated format for primitives. + pointy_brackets: If True, use angle brackets instead of curly braces for + nesting. + use_index_order: If True, fields of a proto message will be printed using + the order defined in source code instead of the field number, extensions + will be printed at the end of the message and their relative order is + determined by the extension number. By default, use the field number + order. + float_format (str): If set, use this to specify float field formatting + (per the "Format Specification Mini-Language"); otherwise, shortest float + that has same value in wire will be printed. Also affect double field + if double_format is not set but float_format is set. + double_format (str): If set, use this to specify double field formatting + (per the "Format Specification Mini-Language"); if it is not set but + float_format is set, use float_format. Otherwise, use ``str()`` + use_field_number: If True, print field numbers instead of names. + descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. + indent (int): The initial indent level, in terms of spaces, for pretty + print. + message_formatter (function(message, indent, as_one_line) -> unicode|None): + Custom formatter for selected sub-messages (usually based on message + type). Use to pretty print parts of the protobuf for easier diffing. + print_unknown_fields: If True, unknown fields will be printed. + force_colon: If set, a colon will be added after the field name even if the + field is a proto message. + + Returns: + str: A string of the text formatted protocol buffer message. + """ + out = TextWriter(as_utf8) + printer = _Printer( + out, + indent, + as_utf8, + as_one_line, + use_short_repeated_primitives, + pointy_brackets, + use_index_order, + float_format, + double_format, + use_field_number, + descriptor_pool, + message_formatter, + print_unknown_fields=print_unknown_fields, + force_colon=force_colon) + printer.PrintMessage(message) + result = out.getvalue() + out.close() + if as_one_line: + return result.rstrip() + return result + + +def MessageToBytes(message, **kwargs): + # type: (...) -> bytes + """Convert protobuf message to encoded text format. See MessageToString.""" + text = MessageToString(message, **kwargs) + if isinstance(text, bytes): + return text + codec = 'utf-8' if kwargs.get('as_utf8') else 'ascii' + return text.encode(codec) + + +def _IsMapEntry(field): + return (field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and + field.message_type.has_options and + field.message_type.GetOptions().map_entry) + + +def PrintMessage(message, + out, + indent=0, + as_utf8=False, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + use_field_number=False, + descriptor_pool=None, + message_formatter=None, + print_unknown_fields=False, + force_colon=False): + printer = _Printer( + out=out, indent=indent, as_utf8=as_utf8, + as_one_line=as_one_line, + use_short_repeated_primitives=use_short_repeated_primitives, + pointy_brackets=pointy_brackets, + use_index_order=use_index_order, + float_format=float_format, + double_format=double_format, + use_field_number=use_field_number, + descriptor_pool=descriptor_pool, + message_formatter=message_formatter, + print_unknown_fields=print_unknown_fields, + force_colon=force_colon) + printer.PrintMessage(message) + + +def PrintField(field, + value, + out, + indent=0, + as_utf8=False, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + message_formatter=None, + print_unknown_fields=False, + force_colon=False): + """Print a single field name/value pair.""" + printer = _Printer(out, indent, as_utf8, as_one_line, + use_short_repeated_primitives, pointy_brackets, + use_index_order, float_format, double_format, + message_formatter=message_formatter, + print_unknown_fields=print_unknown_fields, + force_colon=force_colon) + printer.PrintField(field, value) + + +def PrintFieldValue(field, + value, + out, + indent=0, + as_utf8=False, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + message_formatter=None, + print_unknown_fields=False, + force_colon=False): + """Print a single field value (not including name).""" + printer = _Printer(out, indent, as_utf8, as_one_line, + use_short_repeated_primitives, pointy_brackets, + use_index_order, float_format, double_format, + message_formatter=message_formatter, + print_unknown_fields=print_unknown_fields, + force_colon=force_colon) + printer.PrintFieldValue(field, value) + + +def _BuildMessageFromTypeName(type_name, descriptor_pool): + """Returns a protobuf message instance. + + Args: + type_name: Fully-qualified protobuf message type name string. + descriptor_pool: DescriptorPool instance. + + Returns: + A Message instance of type matching type_name, or None if the a Descriptor + wasn't found matching type_name. + """ + # pylint: disable=g-import-not-at-top + if descriptor_pool is None: + from google.protobuf import descriptor_pool as pool_mod + descriptor_pool = pool_mod.Default() + from google.protobuf import symbol_database + database = symbol_database.Default() + try: + message_descriptor = descriptor_pool.FindMessageTypeByName(type_name) + except KeyError: + return None + message_type = database.GetPrototype(message_descriptor) + return message_type() + + +# These values must match WireType enum in google/protobuf/wire_format.h. +WIRETYPE_LENGTH_DELIMITED = 2 +WIRETYPE_START_GROUP = 3 + + +class _Printer(object): + """Text format printer for protocol message.""" + + def __init__( + self, + out, + indent=0, + as_utf8=False, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + use_field_number=False, + descriptor_pool=None, + message_formatter=None, + print_unknown_fields=False, + force_colon=False): + """Initialize the Printer. + + Double values can be formatted compactly with 15 digits of precision + (which is the most that IEEE 754 "double" can guarantee) using + double_format='.15g'. To ensure that converting to text and back to a proto + will result in an identical value, double_format='.17g' should be used. + + Args: + out: To record the text format result. + indent: The initial indent level for pretty print. + as_utf8: Return unescaped Unicode for non-ASCII characters. + In Python 3 actual Unicode characters may appear as is in strings. + In Python 2 the return value will be valid UTF-8 rather than ASCII. + as_one_line: Don't introduce newlines between fields. + use_short_repeated_primitives: Use short repeated format for primitives. + pointy_brackets: If True, use angle brackets instead of curly braces for + nesting. + use_index_order: If True, print fields of a proto message using the order + defined in source code instead of the field number. By default, use the + field number order. + float_format: If set, use this to specify float field formatting + (per the "Format Specification Mini-Language"); otherwise, shortest + float that has same value in wire will be printed. Also affect double + field if double_format is not set but float_format is set. + double_format: If set, use this to specify double field formatting + (per the "Format Specification Mini-Language"); if it is not set but + float_format is set, use float_format. Otherwise, str() is used. + use_field_number: If True, print field numbers instead of names. + descriptor_pool: A DescriptorPool used to resolve Any types. + message_formatter: A function(message, indent, as_one_line): unicode|None + to custom format selected sub-messages (usually based on message type). + Use to pretty print parts of the protobuf for easier diffing. + print_unknown_fields: If True, unknown fields will be printed. + force_colon: If set, a colon will be added after the field name even if + the field is a proto message. + """ + self.out = out + self.indent = indent + self.as_utf8 = as_utf8 + self.as_one_line = as_one_line + self.use_short_repeated_primitives = use_short_repeated_primitives + self.pointy_brackets = pointy_brackets + self.use_index_order = use_index_order + self.float_format = float_format + if double_format is not None: + self.double_format = double_format + else: + self.double_format = float_format + self.use_field_number = use_field_number + self.descriptor_pool = descriptor_pool + self.message_formatter = message_formatter + self.print_unknown_fields = print_unknown_fields + self.force_colon = force_colon + + def _TryPrintAsAnyMessage(self, message): + """Serializes if message is a google.protobuf.Any field.""" + if '/' not in message.type_url: + return False + packed_message = _BuildMessageFromTypeName(message.TypeName(), + self.descriptor_pool) + if packed_message: + packed_message.MergeFromString(message.value) + colon = ':' if self.force_colon else '' + self.out.write('%s[%s]%s ' % (self.indent * ' ', message.type_url, colon)) + self._PrintMessageFieldValue(packed_message) + self.out.write(' ' if self.as_one_line else '\n') + return True + else: + return False + + def _TryCustomFormatMessage(self, message): + formatted = self.message_formatter(message, self.indent, self.as_one_line) + if formatted is None: + return False + + out = self.out + out.write(' ' * self.indent) + out.write(formatted) + out.write(' ' if self.as_one_line else '\n') + return True + + def PrintMessage(self, message): + """Convert protobuf message to text format. + + Args: + message: The protocol buffers message. + """ + if self.message_formatter and self._TryCustomFormatMessage(message): + return + if (message.DESCRIPTOR.full_name == _ANY_FULL_TYPE_NAME and + self._TryPrintAsAnyMessage(message)): + return + fields = message.ListFields() + if self.use_index_order: + fields.sort( + key=lambda x: x[0].number if x[0].is_extension else x[0].index) + for field, value in fields: + if _IsMapEntry(field): + for key in sorted(value): + # This is slow for maps with submessage entries because it copies the + # entire tree. Unfortunately this would take significant refactoring + # of this file to work around. + # + # TODO(haberman): refactor and optimize if this becomes an issue. + entry_submsg = value.GetEntryClass()(key=key, value=value[key]) + self.PrintField(field, entry_submsg) + elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + if (self.use_short_repeated_primitives + and field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_MESSAGE + and field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_STRING): + self._PrintShortRepeatedPrimitivesValue(field, value) + else: + for element in value: + self.PrintField(field, element) + else: + self.PrintField(field, value) + + if self.print_unknown_fields: + self._PrintUnknownFields(message.UnknownFields()) + + def _PrintUnknownFields(self, unknown_fields): + """Print unknown fields.""" + out = self.out + for field in unknown_fields: + out.write(' ' * self.indent) + out.write(str(field.field_number)) + if field.wire_type == WIRETYPE_START_GROUP: + if self.as_one_line: + out.write(' { ') + else: + out.write(' {\n') + self.indent += 2 + + self._PrintUnknownFields(field.data) + + if self.as_one_line: + out.write('} ') + else: + self.indent -= 2 + out.write(' ' * self.indent + '}\n') + elif field.wire_type == WIRETYPE_LENGTH_DELIMITED: + try: + # If this field is parseable as a Message, it is probably + # an embedded message. + # pylint: disable=protected-access + (embedded_unknown_message, pos) = decoder._DecodeUnknownFieldSet( + memoryview(field.data), 0, len(field.data)) + except Exception: # pylint: disable=broad-except + pos = 0 + + if pos == len(field.data): + if self.as_one_line: + out.write(' { ') + else: + out.write(' {\n') + self.indent += 2 + + self._PrintUnknownFields(embedded_unknown_message) + + if self.as_one_line: + out.write('} ') + else: + self.indent -= 2 + out.write(' ' * self.indent + '}\n') + else: + # A string or bytes field. self.as_utf8 may not work. + out.write(': \"') + out.write(text_encoding.CEscape(field.data, False)) + out.write('\" ' if self.as_one_line else '\"\n') + else: + # varint, fixed32, fixed64 + out.write(': ') + out.write(str(field.data)) + out.write(' ' if self.as_one_line else '\n') + + def _PrintFieldName(self, field): + """Print field name.""" + out = self.out + out.write(' ' * self.indent) + if self.use_field_number: + out.write(str(field.number)) + else: + if field.is_extension: + out.write('[') + if (field.containing_type.GetOptions().message_set_wire_format and + field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and + field.label == descriptor.FieldDescriptor.LABEL_OPTIONAL): + out.write(field.message_type.full_name) + else: + out.write(field.full_name) + out.write(']') + elif field.type == descriptor.FieldDescriptor.TYPE_GROUP: + # For groups, use the capitalized name. + out.write(field.message_type.name) + else: + out.write(field.name) + + if (self.force_colon or + field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_MESSAGE): + # The colon is optional in this case, but our cross-language golden files + # don't include it. Here, the colon is only included if force_colon is + # set to True + out.write(':') + + def PrintField(self, field, value): + """Print a single field name/value pair.""" + self._PrintFieldName(field) + self.out.write(' ') + self.PrintFieldValue(field, value) + self.out.write(' ' if self.as_one_line else '\n') + + def _PrintShortRepeatedPrimitivesValue(self, field, value): + """"Prints short repeated primitives value.""" + # Note: this is called only when value has at least one element. + self._PrintFieldName(field) + self.out.write(' [') + for i in range(len(value) - 1): + self.PrintFieldValue(field, value[i]) + self.out.write(', ') + self.PrintFieldValue(field, value[-1]) + self.out.write(']') + self.out.write(' ' if self.as_one_line else '\n') + + def _PrintMessageFieldValue(self, value): + if self.pointy_brackets: + openb = '<' + closeb = '>' + else: + openb = '{' + closeb = '}' + + if self.as_one_line: + self.out.write('%s ' % openb) + self.PrintMessage(value) + self.out.write(closeb) + else: + self.out.write('%s\n' % openb) + self.indent += 2 + self.PrintMessage(value) + self.indent -= 2 + self.out.write(' ' * self.indent + closeb) + + def PrintFieldValue(self, field, value): + """Print a single field value (not including name). + + For repeated fields, the value should be a single element. + + Args: + field: The descriptor of the field to be printed. + value: The value of the field. + """ + out = self.out + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + self._PrintMessageFieldValue(value) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM: + enum_value = field.enum_type.values_by_number.get(value, None) + if enum_value is not None: + out.write(enum_value.name) + else: + out.write(str(value)) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING: + out.write('\"') + if isinstance(value, str) and not self.as_utf8: + out_value = value.encode('utf-8') + else: + out_value = value + if field.type == descriptor.FieldDescriptor.TYPE_BYTES: + # We always need to escape all binary data in TYPE_BYTES fields. + out_as_utf8 = False + else: + out_as_utf8 = self.as_utf8 + out.write(text_encoding.CEscape(out_value, out_as_utf8)) + out.write('\"') + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL: + if value: + out.write('true') + else: + out.write('false') + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT: + if self.float_format is not None: + out.write('{1:{0}}'.format(self.float_format, value)) + else: + if math.isnan(value): + out.write(str(value)) + else: + out.write(str(type_checkers.ToShortestFloat(value))) + elif (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_DOUBLE and + self.double_format is not None): + out.write('{1:{0}}'.format(self.double_format, value)) + else: + out.write(str(value)) + + +def Parse(text, + message, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + """Parses a text representation of a protocol message into a message. + + NOTE: for historical reasons this function does not clear the input + message. This is different from what the binary msg.ParseFrom(...) does. + If text contains a field already set in message, the value is appended if the + field is repeated. Otherwise, an error is raised. + + Example:: + + a = MyProto() + a.repeated_field.append('test') + b = MyProto() + + # Repeated fields are combined + text_format.Parse(repr(a), b) + text_format.Parse(repr(a), b) # repeated_field contains ["test", "test"] + + # Non-repeated fields cannot be overwritten + a.singular_field = 1 + b.singular_field = 2 + text_format.Parse(repr(a), b) # ParseError + + # Binary version: + b.ParseFromString(a.SerializeToString()) # repeated_field is now "test" + + Caller is responsible for clearing the message as needed. + + Args: + text (str): Message text representation. + message (Message): A protocol buffer message to merge into. + allow_unknown_extension: if True, skip over missing extensions and keep + parsing + allow_field_number: if True, both field number and field name are allowed. + descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. + allow_unknown_field: if True, skip over unknown field and keep + parsing. Avoid to use this option if possible. It may hide some + errors (e.g. spelling error on field name) + + Returns: + Message: The same message passed as argument. + + Raises: + ParseError: On text parsing problems. + """ + return ParseLines(text.split(b'\n' if isinstance(text, bytes) else u'\n'), + message, + allow_unknown_extension, + allow_field_number, + descriptor_pool=descriptor_pool, + allow_unknown_field=allow_unknown_field) + + +def Merge(text, + message, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + """Parses a text representation of a protocol message into a message. + + Like Parse(), but allows repeated values for a non-repeated field, and uses + the last one. This means any non-repeated, top-level fields specified in text + replace those in the message. + + Args: + text (str): Message text representation. + message (Message): A protocol buffer message to merge into. + allow_unknown_extension: if True, skip over missing extensions and keep + parsing + allow_field_number: if True, both field number and field name are allowed. + descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. + allow_unknown_field: if True, skip over unknown field and keep + parsing. Avoid to use this option if possible. It may hide some + errors (e.g. spelling error on field name) + + Returns: + Message: The same message passed as argument. + + Raises: + ParseError: On text parsing problems. + """ + return MergeLines( + text.split(b'\n' if isinstance(text, bytes) else u'\n'), + message, + allow_unknown_extension, + allow_field_number, + descriptor_pool=descriptor_pool, + allow_unknown_field=allow_unknown_field) + + +def ParseLines(lines, + message, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + """Parses a text representation of a protocol message into a message. + + See Parse() for caveats. + + Args: + lines: An iterable of lines of a message's text representation. + message: A protocol buffer message to merge into. + allow_unknown_extension: if True, skip over missing extensions and keep + parsing + allow_field_number: if True, both field number and field name are allowed. + descriptor_pool: A DescriptorPool used to resolve Any types. + allow_unknown_field: if True, skip over unknown field and keep + parsing. Avoid to use this option if possible. It may hide some + errors (e.g. spelling error on field name) + + Returns: + The same message passed as argument. + + Raises: + ParseError: On text parsing problems. + """ + parser = _Parser(allow_unknown_extension, + allow_field_number, + descriptor_pool=descriptor_pool, + allow_unknown_field=allow_unknown_field) + return parser.ParseLines(lines, message) + + +def MergeLines(lines, + message, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + """Parses a text representation of a protocol message into a message. + + See Merge() for more details. + + Args: + lines: An iterable of lines of a message's text representation. + message: A protocol buffer message to merge into. + allow_unknown_extension: if True, skip over missing extensions and keep + parsing + allow_field_number: if True, both field number and field name are allowed. + descriptor_pool: A DescriptorPool used to resolve Any types. + allow_unknown_field: if True, skip over unknown field and keep + parsing. Avoid to use this option if possible. It may hide some + errors (e.g. spelling error on field name) + + Returns: + The same message passed as argument. + + Raises: + ParseError: On text parsing problems. + """ + parser = _Parser(allow_unknown_extension, + allow_field_number, + descriptor_pool=descriptor_pool, + allow_unknown_field=allow_unknown_field) + return parser.MergeLines(lines, message) + + +class _Parser(object): + """Text format parser for protocol message.""" + + def __init__(self, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + self.allow_unknown_extension = allow_unknown_extension + self.allow_field_number = allow_field_number + self.descriptor_pool = descriptor_pool + self.allow_unknown_field = allow_unknown_field + + def ParseLines(self, lines, message): + """Parses a text representation of a protocol message into a message.""" + self._allow_multiple_scalars = False + self._ParseOrMerge(lines, message) + return message + + def MergeLines(self, lines, message): + """Merges a text representation of a protocol message into a message.""" + self._allow_multiple_scalars = True + self._ParseOrMerge(lines, message) + return message + + def _ParseOrMerge(self, lines, message): + """Converts a text representation of a protocol message into a message. + + Args: + lines: Lines of a message's text representation. + message: A protocol buffer message to merge into. + + Raises: + ParseError: On text parsing problems. + """ + # Tokenize expects native str lines. + str_lines = ( + line if isinstance(line, str) else line.decode('utf-8') + for line in lines) + tokenizer = Tokenizer(str_lines) + while not tokenizer.AtEnd(): + self._MergeField(tokenizer, message) + + def _MergeField(self, tokenizer, message): + """Merges a single protocol message field into a message. + + Args: + tokenizer: A tokenizer to parse the field name and values. + message: A protocol message to record the data. + + Raises: + ParseError: In case of text parsing problems. + """ + message_descriptor = message.DESCRIPTOR + if (message_descriptor.full_name == _ANY_FULL_TYPE_NAME and + tokenizer.TryConsume('[')): + type_url_prefix, packed_type_name = self._ConsumeAnyTypeUrl(tokenizer) + tokenizer.Consume(']') + tokenizer.TryConsume(':') + if tokenizer.TryConsume('<'): + expanded_any_end_token = '>' + else: + tokenizer.Consume('{') + expanded_any_end_token = '}' + expanded_any_sub_message = _BuildMessageFromTypeName(packed_type_name, + self.descriptor_pool) + if not expanded_any_sub_message: + raise ParseError('Type %s not found in descriptor pool' % + packed_type_name) + while not tokenizer.TryConsume(expanded_any_end_token): + if tokenizer.AtEnd(): + raise tokenizer.ParseErrorPreviousToken('Expected "%s".' % + (expanded_any_end_token,)) + self._MergeField(tokenizer, expanded_any_sub_message) + deterministic = False + + message.Pack(expanded_any_sub_message, + type_url_prefix=type_url_prefix, + deterministic=deterministic) + return + + if tokenizer.TryConsume('['): + name = [tokenizer.ConsumeIdentifier()] + while tokenizer.TryConsume('.'): + name.append(tokenizer.ConsumeIdentifier()) + name = '.'.join(name) + + if not message_descriptor.is_extendable: + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" does not have extensions.' % + message_descriptor.full_name) + # pylint: disable=protected-access + field = message.Extensions._FindExtensionByName(name) + # pylint: enable=protected-access + + + if not field: + if self.allow_unknown_extension: + field = None + else: + raise tokenizer.ParseErrorPreviousToken( + 'Extension "%s" not registered. ' + 'Did you import the _pb2 module which defines it? ' + 'If you are trying to place the extension in the MessageSet ' + 'field of another message that is in an Any or MessageSet field, ' + 'that message\'s _pb2 module must be imported as well' % name) + elif message_descriptor != field.containing_type: + raise tokenizer.ParseErrorPreviousToken( + 'Extension "%s" does not extend message type "%s".' % + (name, message_descriptor.full_name)) + + tokenizer.Consume(']') + + else: + name = tokenizer.ConsumeIdentifierOrNumber() + if self.allow_field_number and name.isdigit(): + number = ParseInteger(name, True, True) + field = message_descriptor.fields_by_number.get(number, None) + if not field and message_descriptor.is_extendable: + field = message.Extensions._FindExtensionByNumber(number) + else: + field = message_descriptor.fields_by_name.get(name, None) + + # Group names are expected to be capitalized as they appear in the + # .proto file, which actually matches their type names, not their field + # names. + if not field: + field = message_descriptor.fields_by_name.get(name.lower(), None) + if field and field.type != descriptor.FieldDescriptor.TYPE_GROUP: + field = None + + if (field and field.type == descriptor.FieldDescriptor.TYPE_GROUP and + field.message_type.name != name): + field = None + + if not field and not self.allow_unknown_field: + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" has no field named "%s".' % + (message_descriptor.full_name, name)) + + if field: + if not self._allow_multiple_scalars and field.containing_oneof: + # Check if there's a different field set in this oneof. + # Note that we ignore the case if the same field was set before, and we + # apply _allow_multiple_scalars to non-scalar fields as well. + which_oneof = message.WhichOneof(field.containing_oneof.name) + if which_oneof is not None and which_oneof != field.name: + raise tokenizer.ParseErrorPreviousToken( + 'Field "%s" is specified along with field "%s", another member ' + 'of oneof "%s" for message type "%s".' % + (field.name, which_oneof, field.containing_oneof.name, + message_descriptor.full_name)) + + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + tokenizer.TryConsume(':') + merger = self._MergeMessageField + else: + tokenizer.Consume(':') + merger = self._MergeScalarField + + if (field.label == descriptor.FieldDescriptor.LABEL_REPEATED and + tokenizer.TryConsume('[')): + # Short repeated format, e.g. "foo: [1, 2, 3]" + if not tokenizer.TryConsume(']'): + while True: + merger(tokenizer, message, field) + if tokenizer.TryConsume(']'): + break + tokenizer.Consume(',') + + else: + merger(tokenizer, message, field) + + else: # Proto field is unknown. + assert (self.allow_unknown_extension or self.allow_unknown_field) + _SkipFieldContents(tokenizer) + + # For historical reasons, fields may optionally be separated by commas or + # semicolons. + if not tokenizer.TryConsume(','): + tokenizer.TryConsume(';') + + + def _ConsumeAnyTypeUrl(self, tokenizer): + """Consumes a google.protobuf.Any type URL and returns the type name.""" + # Consume "type.googleapis.com/". + prefix = [tokenizer.ConsumeIdentifier()] + tokenizer.Consume('.') + prefix.append(tokenizer.ConsumeIdentifier()) + tokenizer.Consume('.') + prefix.append(tokenizer.ConsumeIdentifier()) + tokenizer.Consume('/') + # Consume the fully-qualified type name. + name = [tokenizer.ConsumeIdentifier()] + while tokenizer.TryConsume('.'): + name.append(tokenizer.ConsumeIdentifier()) + return '.'.join(prefix), '.'.join(name) + + def _MergeMessageField(self, tokenizer, message, field): + """Merges a single scalar field into a message. + + Args: + tokenizer: A tokenizer to parse the field value. + message: The message of which field is a member. + field: The descriptor of the field to be merged. + + Raises: + ParseError: In case of text parsing problems. + """ + is_map_entry = _IsMapEntry(field) + + if tokenizer.TryConsume('<'): + end_token = '>' + else: + tokenizer.Consume('{') + end_token = '}' + + if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + if field.is_extension: + sub_message = message.Extensions[field].add() + elif is_map_entry: + sub_message = getattr(message, field.name).GetEntryClass()() + else: + sub_message = getattr(message, field.name).add() + else: + if field.is_extension: + if (not self._allow_multiple_scalars and + message.HasExtension(field)): + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" should not have multiple "%s" extensions.' % + (message.DESCRIPTOR.full_name, field.full_name)) + sub_message = message.Extensions[field] + else: + # Also apply _allow_multiple_scalars to message field. + # TODO(jieluo): Change to _allow_singular_overwrites. + if (not self._allow_multiple_scalars and + message.HasField(field.name)): + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" should not have multiple "%s" fields.' % + (message.DESCRIPTOR.full_name, field.name)) + sub_message = getattr(message, field.name) + sub_message.SetInParent() + + while not tokenizer.TryConsume(end_token): + if tokenizer.AtEnd(): + raise tokenizer.ParseErrorPreviousToken('Expected "%s".' % (end_token,)) + self._MergeField(tokenizer, sub_message) + + if is_map_entry: + value_cpptype = field.message_type.fields_by_name['value'].cpp_type + if value_cpptype == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + value = getattr(message, field.name)[sub_message.key] + value.CopyFrom(sub_message.value) + else: + getattr(message, field.name)[sub_message.key] = sub_message.value + + @staticmethod + def _IsProto3Syntax(message): + message_descriptor = message.DESCRIPTOR + return (hasattr(message_descriptor, 'syntax') and + message_descriptor.syntax == 'proto3') + + def _MergeScalarField(self, tokenizer, message, field): + """Merges a single scalar field into a message. + + Args: + tokenizer: A tokenizer to parse the field value. + message: A protocol message to record the data. + field: The descriptor of the field to be merged. + + Raises: + ParseError: In case of text parsing problems. + RuntimeError: On runtime errors. + """ + _ = self.allow_unknown_extension + value = None + + if field.type in (descriptor.FieldDescriptor.TYPE_INT32, + descriptor.FieldDescriptor.TYPE_SINT32, + descriptor.FieldDescriptor.TYPE_SFIXED32): + value = _ConsumeInt32(tokenizer) + elif field.type in (descriptor.FieldDescriptor.TYPE_INT64, + descriptor.FieldDescriptor.TYPE_SINT64, + descriptor.FieldDescriptor.TYPE_SFIXED64): + value = _ConsumeInt64(tokenizer) + elif field.type in (descriptor.FieldDescriptor.TYPE_UINT32, + descriptor.FieldDescriptor.TYPE_FIXED32): + value = _ConsumeUint32(tokenizer) + elif field.type in (descriptor.FieldDescriptor.TYPE_UINT64, + descriptor.FieldDescriptor.TYPE_FIXED64): + value = _ConsumeUint64(tokenizer) + elif field.type in (descriptor.FieldDescriptor.TYPE_FLOAT, + descriptor.FieldDescriptor.TYPE_DOUBLE): + value = tokenizer.ConsumeFloat() + elif field.type == descriptor.FieldDescriptor.TYPE_BOOL: + value = tokenizer.ConsumeBool() + elif field.type == descriptor.FieldDescriptor.TYPE_STRING: + value = tokenizer.ConsumeString() + elif field.type == descriptor.FieldDescriptor.TYPE_BYTES: + value = tokenizer.ConsumeByteString() + elif field.type == descriptor.FieldDescriptor.TYPE_ENUM: + value = tokenizer.ConsumeEnum(field) + else: + raise RuntimeError('Unknown field type %d' % field.type) + + if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + if field.is_extension: + message.Extensions[field].append(value) + else: + getattr(message, field.name).append(value) + else: + if field.is_extension: + if (not self._allow_multiple_scalars and + not self._IsProto3Syntax(message) and + message.HasExtension(field)): + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" should not have multiple "%s" extensions.' % + (message.DESCRIPTOR.full_name, field.full_name)) + else: + message.Extensions[field] = value + else: + duplicate_error = False + if not self._allow_multiple_scalars: + if self._IsProto3Syntax(message): + # Proto3 doesn't represent presence so we try best effort to check + # multiple scalars by compare to default values. + duplicate_error = bool(getattr(message, field.name)) + else: + duplicate_error = message.HasField(field.name) + + if duplicate_error: + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" should not have multiple "%s" fields.' % + (message.DESCRIPTOR.full_name, field.name)) + else: + setattr(message, field.name, value) + + +def _SkipFieldContents(tokenizer): + """Skips over contents (value or message) of a field. + + Args: + tokenizer: A tokenizer to parse the field name and values. + """ + # Try to guess the type of this field. + # If this field is not a message, there should be a ":" between the + # field name and the field value and also the field value should not + # start with "{" or "<" which indicates the beginning of a message body. + # If there is no ":" or there is a "{" or "<" after ":", this field has + # to be a message or the input is ill-formed. + if tokenizer.TryConsume(':') and not tokenizer.LookingAt( + '{') and not tokenizer.LookingAt('<'): + _SkipFieldValue(tokenizer) + else: + _SkipFieldMessage(tokenizer) + + +def _SkipField(tokenizer): + """Skips over a complete field (name and value/message). + + Args: + tokenizer: A tokenizer to parse the field name and values. + """ + if tokenizer.TryConsume('['): + # Consume extension name. + tokenizer.ConsumeIdentifier() + while tokenizer.TryConsume('.'): + tokenizer.ConsumeIdentifier() + tokenizer.Consume(']') + else: + tokenizer.ConsumeIdentifierOrNumber() + + _SkipFieldContents(tokenizer) + + # For historical reasons, fields may optionally be separated by commas or + # semicolons. + if not tokenizer.TryConsume(','): + tokenizer.TryConsume(';') + + +def _SkipFieldMessage(tokenizer): + """Skips over a field message. + + Args: + tokenizer: A tokenizer to parse the field name and values. + """ + + if tokenizer.TryConsume('<'): + delimiter = '>' + else: + tokenizer.Consume('{') + delimiter = '}' + + while not tokenizer.LookingAt('>') and not tokenizer.LookingAt('}'): + _SkipField(tokenizer) + + tokenizer.Consume(delimiter) + + +def _SkipFieldValue(tokenizer): + """Skips over a field value. + + Args: + tokenizer: A tokenizer to parse the field name and values. + + Raises: + ParseError: In case an invalid field value is found. + """ + # String/bytes tokens can come in multiple adjacent string literals. + # If we can consume one, consume as many as we can. + if tokenizer.TryConsumeByteString(): + while tokenizer.TryConsumeByteString(): + pass + return + + if (not tokenizer.TryConsumeIdentifier() and + not _TryConsumeInt64(tokenizer) and not _TryConsumeUint64(tokenizer) and + not tokenizer.TryConsumeFloat()): + raise ParseError('Invalid field value: ' + tokenizer.token) + + +class Tokenizer(object): + """Protocol buffer text representation tokenizer. + + This class handles the lower level string parsing by splitting it into + meaningful tokens. + + It was directly ported from the Java protocol buffer API. + """ + + _WHITESPACE = re.compile(r'\s+') + _COMMENT = re.compile(r'(\s*#.*$)', re.MULTILINE) + _WHITESPACE_OR_COMMENT = re.compile(r'(\s|(#.*$))+', re.MULTILINE) + _TOKEN = re.compile('|'.join([ + r'[a-zA-Z_][0-9a-zA-Z_+-]*', # an identifier + r'([0-9+-]|(\.[0-9]))[0-9a-zA-Z_.+-]*', # a number + ] + [ # quoted str for each quote mark + # Avoid backtracking! https://stackoverflow.com/a/844267 + r'{qt}[^{qt}\n\\]*((\\.)+[^{qt}\n\\]*)*({qt}|\\?$)'.format(qt=mark) + for mark in _QUOTES + ])) + + _IDENTIFIER = re.compile(r'[^\d\W]\w*') + _IDENTIFIER_OR_NUMBER = re.compile(r'\w+') + + def __init__(self, lines, skip_comments=True): + self._position = 0 + self._line = -1 + self._column = 0 + self._token_start = None + self.token = '' + self._lines = iter(lines) + self._current_line = '' + self._previous_line = 0 + self._previous_column = 0 + self._more_lines = True + self._skip_comments = skip_comments + self._whitespace_pattern = (skip_comments and self._WHITESPACE_OR_COMMENT + or self._WHITESPACE) + self._SkipWhitespace() + self.NextToken() + + def LookingAt(self, token): + return self.token == token + + def AtEnd(self): + """Checks the end of the text was reached. + + Returns: + True iff the end was reached. + """ + return not self.token + + def _PopLine(self): + while len(self._current_line) <= self._column: + try: + self._current_line = next(self._lines) + except StopIteration: + self._current_line = '' + self._more_lines = False + return + else: + self._line += 1 + self._column = 0 + + def _SkipWhitespace(self): + while True: + self._PopLine() + match = self._whitespace_pattern.match(self._current_line, self._column) + if not match: + break + length = len(match.group(0)) + self._column += length + + def TryConsume(self, token): + """Tries to consume a given piece of text. + + Args: + token: Text to consume. + + Returns: + True iff the text was consumed. + """ + if self.token == token: + self.NextToken() + return True + return False + + def Consume(self, token): + """Consumes a piece of text. + + Args: + token: Text to consume. + + Raises: + ParseError: If the text couldn't be consumed. + """ + if not self.TryConsume(token): + raise self.ParseError('Expected "%s".' % token) + + def ConsumeComment(self): + result = self.token + if not self._COMMENT.match(result): + raise self.ParseError('Expected comment.') + self.NextToken() + return result + + def ConsumeCommentOrTrailingComment(self): + """Consumes a comment, returns a 2-tuple (trailing bool, comment str).""" + + # Tokenizer initializes _previous_line and _previous_column to 0. As the + # tokenizer starts, it looks like there is a previous token on the line. + just_started = self._line == 0 and self._column == 0 + + before_parsing = self._previous_line + comment = self.ConsumeComment() + + # A trailing comment is a comment on the same line than the previous token. + trailing = (self._previous_line == before_parsing + and not just_started) + + return trailing, comment + + def TryConsumeIdentifier(self): + try: + self.ConsumeIdentifier() + return True + except ParseError: + return False + + def ConsumeIdentifier(self): + """Consumes protocol message field identifier. + + Returns: + Identifier string. + + Raises: + ParseError: If an identifier couldn't be consumed. + """ + result = self.token + if not self._IDENTIFIER.match(result): + raise self.ParseError('Expected identifier.') + self.NextToken() + return result + + def TryConsumeIdentifierOrNumber(self): + try: + self.ConsumeIdentifierOrNumber() + return True + except ParseError: + return False + + def ConsumeIdentifierOrNumber(self): + """Consumes protocol message field identifier. + + Returns: + Identifier string. + + Raises: + ParseError: If an identifier couldn't be consumed. + """ + result = self.token + if not self._IDENTIFIER_OR_NUMBER.match(result): + raise self.ParseError('Expected identifier or number, got %s.' % result) + self.NextToken() + return result + + def TryConsumeInteger(self): + try: + self.ConsumeInteger() + return True + except ParseError: + return False + + def ConsumeInteger(self): + """Consumes an integer number. + + Returns: + The integer parsed. + + Raises: + ParseError: If an integer couldn't be consumed. + """ + try: + result = _ParseAbstractInteger(self.token) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def TryConsumeFloat(self): + try: + self.ConsumeFloat() + return True + except ParseError: + return False + + def ConsumeFloat(self): + """Consumes an floating point number. + + Returns: + The number parsed. + + Raises: + ParseError: If a floating point number couldn't be consumed. + """ + try: + result = ParseFloat(self.token) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def ConsumeBool(self): + """Consumes a boolean value. + + Returns: + The bool parsed. + + Raises: + ParseError: If a boolean value couldn't be consumed. + """ + try: + result = ParseBool(self.token) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def TryConsumeByteString(self): + try: + self.ConsumeByteString() + return True + except ParseError: + return False + + def ConsumeString(self): + """Consumes a string value. + + Returns: + The string parsed. + + Raises: + ParseError: If a string value couldn't be consumed. + """ + the_bytes = self.ConsumeByteString() + try: + return str(the_bytes, 'utf-8') + except UnicodeDecodeError as e: + raise self._StringParseError(e) + + def ConsumeByteString(self): + """Consumes a byte array value. + + Returns: + The array parsed (as a string). + + Raises: + ParseError: If a byte array value couldn't be consumed. + """ + the_list = [self._ConsumeSingleByteString()] + while self.token and self.token[0] in _QUOTES: + the_list.append(self._ConsumeSingleByteString()) + return b''.join(the_list) + + def _ConsumeSingleByteString(self): + """Consume one token of a string literal. + + String literals (whether bytes or text) can come in multiple adjacent + tokens which are automatically concatenated, like in C or Python. This + method only consumes one token. + + Returns: + The token parsed. + Raises: + ParseError: When the wrong format data is found. + """ + text = self.token + if len(text) < 1 or text[0] not in _QUOTES: + raise self.ParseError('Expected string but found: %r' % (text,)) + + if len(text) < 2 or text[-1] != text[0]: + raise self.ParseError('String missing ending quote: %r' % (text,)) + + try: + result = text_encoding.CUnescape(text[1:-1]) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def ConsumeEnum(self, field): + try: + result = ParseEnum(field, self.token) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def ParseErrorPreviousToken(self, message): + """Creates and *returns* a ParseError for the previously read token. + + Args: + message: A message to set for the exception. + + Returns: + A ParseError instance. + """ + return ParseError(message, self._previous_line + 1, + self._previous_column + 1) + + def ParseError(self, message): + """Creates and *returns* a ParseError for the current token.""" + return ParseError('\'' + self._current_line + '\': ' + message, + self._line + 1, self._column + 1) + + def _StringParseError(self, e): + return self.ParseError('Couldn\'t parse string: ' + str(e)) + + def NextToken(self): + """Reads the next meaningful token.""" + self._previous_line = self._line + self._previous_column = self._column + + self._column += len(self.token) + self._SkipWhitespace() + + if not self._more_lines: + self.token = '' + return + + match = self._TOKEN.match(self._current_line, self._column) + if not match and not self._skip_comments: + match = self._COMMENT.match(self._current_line, self._column) + if match: + token = match.group(0) + self.token = token + else: + self.token = self._current_line[self._column] + +# Aliased so it can still be accessed by current visibility violators. +# TODO(dbarnett): Migrate violators to textformat_tokenizer. +_Tokenizer = Tokenizer # pylint: disable=invalid-name + + +def _ConsumeInt32(tokenizer): + """Consumes a signed 32bit integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + + Returns: + The integer parsed. + + Raises: + ParseError: If a signed 32bit integer couldn't be consumed. + """ + return _ConsumeInteger(tokenizer, is_signed=True, is_long=False) + + +def _ConsumeUint32(tokenizer): + """Consumes an unsigned 32bit integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + + Returns: + The integer parsed. + + Raises: + ParseError: If an unsigned 32bit integer couldn't be consumed. + """ + return _ConsumeInteger(tokenizer, is_signed=False, is_long=False) + + +def _TryConsumeInt64(tokenizer): + try: + _ConsumeInt64(tokenizer) + return True + except ParseError: + return False + + +def _ConsumeInt64(tokenizer): + """Consumes a signed 32bit integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + + Returns: + The integer parsed. + + Raises: + ParseError: If a signed 32bit integer couldn't be consumed. + """ + return _ConsumeInteger(tokenizer, is_signed=True, is_long=True) + + +def _TryConsumeUint64(tokenizer): + try: + _ConsumeUint64(tokenizer) + return True + except ParseError: + return False + + +def _ConsumeUint64(tokenizer): + """Consumes an unsigned 64bit integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + + Returns: + The integer parsed. + + Raises: + ParseError: If an unsigned 64bit integer couldn't be consumed. + """ + return _ConsumeInteger(tokenizer, is_signed=False, is_long=True) + + +def _ConsumeInteger(tokenizer, is_signed=False, is_long=False): + """Consumes an integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + is_signed: True if a signed integer must be parsed. + is_long: True if a long integer must be parsed. + + Returns: + The integer parsed. + + Raises: + ParseError: If an integer with given characteristics couldn't be consumed. + """ + try: + result = ParseInteger(tokenizer.token, is_signed=is_signed, is_long=is_long) + except ValueError as e: + raise tokenizer.ParseError(str(e)) + tokenizer.NextToken() + return result + + +def ParseInteger(text, is_signed=False, is_long=False): + """Parses an integer. + + Args: + text: The text to parse. + is_signed: True if a signed integer must be parsed. + is_long: True if a long integer must be parsed. + + Returns: + The integer value. + + Raises: + ValueError: Thrown Iff the text is not a valid integer. + """ + # Do the actual parsing. Exception handling is propagated to caller. + result = _ParseAbstractInteger(text) + + # Check if the integer is sane. Exceptions handled by callers. + checker = _INTEGER_CHECKERS[2 * int(is_long) + int(is_signed)] + checker.CheckValue(result) + return result + + +def _ParseAbstractInteger(text): + """Parses an integer without checking size/signedness. + + Args: + text: The text to parse. + + Returns: + The integer value. + + Raises: + ValueError: Thrown Iff the text is not a valid integer. + """ + # Do the actual parsing. Exception handling is propagated to caller. + orig_text = text + c_octal_match = re.match(r'(-?)0(\d+)$', text) + if c_octal_match: + # Python 3 no longer supports 0755 octal syntax without the 'o', so + # we always use the '0o' prefix for multi-digit numbers starting with 0. + text = c_octal_match.group(1) + '0o' + c_octal_match.group(2) + try: + return int(text, 0) + except ValueError: + raise ValueError('Couldn\'t parse integer: %s' % orig_text) + + +def ParseFloat(text): + """Parse a floating point number. + + Args: + text: Text to parse. + + Returns: + The number parsed. + + Raises: + ValueError: If a floating point number couldn't be parsed. + """ + try: + # Assume Python compatible syntax. + return float(text) + except ValueError: + # Check alternative spellings. + if _FLOAT_INFINITY.match(text): + if text[0] == '-': + return float('-inf') + else: + return float('inf') + elif _FLOAT_NAN.match(text): + return float('nan') + else: + # assume '1.0f' format + try: + return float(text.rstrip('f')) + except ValueError: + raise ValueError('Couldn\'t parse float: %s' % text) + + +def ParseBool(text): + """Parse a boolean value. + + Args: + text: Text to parse. + + Returns: + Boolean values parsed + + Raises: + ValueError: If text is not a valid boolean. + """ + if text in ('true', 't', '1', 'True'): + return True + elif text in ('false', 'f', '0', 'False'): + return False + else: + raise ValueError('Expected "true" or "false".') + + +def ParseEnum(field, value): + """Parse an enum value. + + The value can be specified by a number (the enum value), or by + a string literal (the enum name). + + Args: + field: Enum field descriptor. + value: String value. + + Returns: + Enum value number. + + Raises: + ValueError: If the enum value could not be parsed. + """ + enum_descriptor = field.enum_type + try: + number = int(value, 0) + except ValueError: + # Identifier. + enum_value = enum_descriptor.values_by_name.get(value, None) + if enum_value is None: + raise ValueError('Enum type "%s" has no value named %s.' % + (enum_descriptor.full_name, value)) + else: + # Numeric value. + if hasattr(field.file, 'syntax'): + # Attribute is checked for compatibility. + if field.file.syntax == 'proto3': + # Proto3 accept numeric unknown enums. + return number + enum_value = enum_descriptor.values_by_number.get(number, None) + if enum_value is None: + raise ValueError('Enum type "%s" has no value with number %d.' % + (enum_descriptor.full_name, number)) + return enum_value.number diff --git a/scripts/protobuf3/protobuf3/timestamp_pb2.py b/scripts/protobuf3/protobuf3/timestamp_pb2.py new file mode 100644 index 0000000..558d496 --- /dev/null +++ b/scripts/protobuf3/protobuf3/timestamp_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/timestamp.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fgoogle/protobuf/timestamp.proto\x12\x0fgoogle.protobuf\"+\n\tTimestamp\x12\x0f\n\x07seconds\x18\x01 \x01(\x03\x12\r\n\x05nanos\x18\x02 \x01(\x05\x42\x85\x01\n\x13\x63om.google.protobufB\x0eTimestampProtoP\x01Z2google.golang.org/protobuf/types/known/timestamppb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.timestamp_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\016TimestampProtoP\001Z2google.golang.org/protobuf/types/known/timestamppb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _TIMESTAMP._serialized_start=52 + _TIMESTAMP._serialized_end=95 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/type_pb2.py b/scripts/protobuf3/protobuf3/type_pb2.py new file mode 100644 index 0000000..19903fb --- /dev/null +++ b/scripts/protobuf3/protobuf3/type_pb2.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/type.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 +from google.protobuf import source_context_pb2 as google_dot_protobuf_dot_source__context__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1agoogle/protobuf/type.proto\x12\x0fgoogle.protobuf\x1a\x19google/protobuf/any.proto\x1a$google/protobuf/source_context.proto\"\xd7\x01\n\x04Type\x12\x0c\n\x04name\x18\x01 \x01(\t\x12&\n\x06\x66ields\x18\x02 \x03(\x0b\x32\x16.google.protobuf.Field\x12\x0e\n\x06oneofs\x18\x03 \x03(\t\x12(\n\x07options\x18\x04 \x03(\x0b\x32\x17.google.protobuf.Option\x12\x36\n\x0esource_context\x18\x05 \x01(\x0b\x32\x1e.google.protobuf.SourceContext\x12\'\n\x06syntax\x18\x06 \x01(\x0e\x32\x17.google.protobuf.Syntax\"\xd5\x05\n\x05\x46ield\x12)\n\x04kind\x18\x01 \x01(\x0e\x32\x1b.google.protobuf.Field.Kind\x12\x37\n\x0b\x63\x61rdinality\x18\x02 \x01(\x0e\x32\".google.protobuf.Field.Cardinality\x12\x0e\n\x06number\x18\x03 \x01(\x05\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x10\n\x08type_url\x18\x06 \x01(\t\x12\x13\n\x0boneof_index\x18\x07 \x01(\x05\x12\x0e\n\x06packed\x18\x08 \x01(\x08\x12(\n\x07options\x18\t \x03(\x0b\x32\x17.google.protobuf.Option\x12\x11\n\tjson_name\x18\n \x01(\t\x12\x15\n\rdefault_value\x18\x0b \x01(\t\"\xc8\x02\n\x04Kind\x12\x10\n\x0cTYPE_UNKNOWN\x10\x00\x12\x0f\n\x0bTYPE_DOUBLE\x10\x01\x12\x0e\n\nTYPE_FLOAT\x10\x02\x12\x0e\n\nTYPE_INT64\x10\x03\x12\x0f\n\x0bTYPE_UINT64\x10\x04\x12\x0e\n\nTYPE_INT32\x10\x05\x12\x10\n\x0cTYPE_FIXED64\x10\x06\x12\x10\n\x0cTYPE_FIXED32\x10\x07\x12\r\n\tTYPE_BOOL\x10\x08\x12\x0f\n\x0bTYPE_STRING\x10\t\x12\x0e\n\nTYPE_GROUP\x10\n\x12\x10\n\x0cTYPE_MESSAGE\x10\x0b\x12\x0e\n\nTYPE_BYTES\x10\x0c\x12\x0f\n\x0bTYPE_UINT32\x10\r\x12\r\n\tTYPE_ENUM\x10\x0e\x12\x11\n\rTYPE_SFIXED32\x10\x0f\x12\x11\n\rTYPE_SFIXED64\x10\x10\x12\x0f\n\x0bTYPE_SINT32\x10\x11\x12\x0f\n\x0bTYPE_SINT64\x10\x12\"t\n\x0b\x43\x61rdinality\x12\x17\n\x13\x43\x41RDINALITY_UNKNOWN\x10\x00\x12\x18\n\x14\x43\x41RDINALITY_OPTIONAL\x10\x01\x12\x18\n\x14\x43\x41RDINALITY_REQUIRED\x10\x02\x12\x18\n\x14\x43\x41RDINALITY_REPEATED\x10\x03\"\xce\x01\n\x04\x45num\x12\x0c\n\x04name\x18\x01 \x01(\t\x12-\n\tenumvalue\x18\x02 \x03(\x0b\x32\x1a.google.protobuf.EnumValue\x12(\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.Option\x12\x36\n\x0esource_context\x18\x04 \x01(\x0b\x32\x1e.google.protobuf.SourceContext\x12\'\n\x06syntax\x18\x05 \x01(\x0e\x32\x17.google.protobuf.Syntax\"S\n\tEnumValue\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06number\x18\x02 \x01(\x05\x12(\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.Option\";\n\x06Option\x12\x0c\n\x04name\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any*.\n\x06Syntax\x12\x11\n\rSYNTAX_PROTO2\x10\x00\x12\x11\n\rSYNTAX_PROTO3\x10\x01\x42{\n\x13\x63om.google.protobufB\tTypeProtoP\x01Z-google.golang.org/protobuf/types/known/typepb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.type_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\tTypeProtoP\001Z-google.golang.org/protobuf/types/known/typepb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _SYNTAX._serialized_start=1413 + _SYNTAX._serialized_end=1459 + _TYPE._serialized_start=113 + _TYPE._serialized_end=328 + _FIELD._serialized_start=331 + _FIELD._serialized_end=1056 + _FIELD_KIND._serialized_start=610 + _FIELD_KIND._serialized_end=938 + _FIELD_CARDINALITY._serialized_start=940 + _FIELD_CARDINALITY._serialized_end=1056 + _ENUM._serialized_start=1059 + _ENUM._serialized_end=1265 + _ENUMVALUE._serialized_start=1267 + _ENUMVALUE._serialized_end=1350 + _OPTION._serialized_start=1352 + _OPTION._serialized_end=1411 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/util/__init__.py b/scripts/protobuf3/protobuf3/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/protobuf3/protobuf3/util/json_format_pb2.py b/scripts/protobuf3/protobuf3/util/json_format_pb2.py new file mode 100644 index 0000000..66a5836 --- /dev/null +++ b/scripts/protobuf3/protobuf3/util/json_format_pb2.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/util/json_format.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&google/protobuf/util/json_format.proto\x12\x11protobuf_unittest\"\x89\x01\n\x13TestFlagsAndStrings\x12\t\n\x01\x41\x18\x01 \x02(\x05\x12K\n\rrepeatedgroup\x18\x02 \x03(\n24.protobuf_unittest.TestFlagsAndStrings.RepeatedGroup\x1a\x1a\n\rRepeatedGroup\x12\t\n\x01\x66\x18\x03 \x02(\t\"!\n\x14TestBase64ByteArrays\x12\t\n\x01\x61\x18\x01 \x02(\x0c\"G\n\x12TestJavaScriptJSON\x12\t\n\x01\x61\x18\x01 \x01(\x05\x12\r\n\x05\x66inal\x18\x02 \x01(\x02\x12\n\n\x02in\x18\x03 \x01(\t\x12\x0b\n\x03Var\x18\x04 \x01(\t\"Q\n\x18TestJavaScriptOrderJSON1\x12\t\n\x01\x64\x18\x01 \x01(\x05\x12\t\n\x01\x63\x18\x02 \x01(\x05\x12\t\n\x01x\x18\x03 \x01(\x08\x12\t\n\x01\x62\x18\x04 \x01(\x05\x12\t\n\x01\x61\x18\x05 \x01(\x05\"\x89\x01\n\x18TestJavaScriptOrderJSON2\x12\t\n\x01\x64\x18\x01 \x01(\x05\x12\t\n\x01\x63\x18\x02 \x01(\x05\x12\t\n\x01x\x18\x03 \x01(\x08\x12\t\n\x01\x62\x18\x04 \x01(\x05\x12\t\n\x01\x61\x18\x05 \x01(\x05\x12\x36\n\x01z\x18\x06 \x03(\x0b\x32+.protobuf_unittest.TestJavaScriptOrderJSON1\"$\n\x0cTestLargeInt\x12\t\n\x01\x61\x18\x01 \x02(\x03\x12\t\n\x01\x62\x18\x02 \x02(\x04\"\xa0\x01\n\x0bTestNumbers\x12\x30\n\x01\x61\x18\x01 \x01(\x0e\x32%.protobuf_unittest.TestNumbers.MyType\x12\t\n\x01\x62\x18\x02 \x01(\x05\x12\t\n\x01\x63\x18\x03 \x01(\x02\x12\t\n\x01\x64\x18\x04 \x01(\x08\x12\t\n\x01\x65\x18\x05 \x01(\x01\x12\t\n\x01\x66\x18\x06 \x01(\r\"(\n\x06MyType\x12\x06\n\x02OK\x10\x00\x12\x0b\n\x07WARNING\x10\x01\x12\t\n\x05\x45RROR\x10\x02\"T\n\rTestCamelCase\x12\x14\n\x0cnormal_field\x18\x01 \x01(\t\x12\x15\n\rCAPITAL_FIELD\x18\x02 \x01(\x05\x12\x16\n\x0e\x43\x61melCaseField\x18\x03 \x01(\x05\"|\n\x0bTestBoolMap\x12=\n\x08\x62ool_map\x18\x01 \x03(\x0b\x32+.protobuf_unittest.TestBoolMap.BoolMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"O\n\rTestRecursion\x12\r\n\x05value\x18\x01 \x01(\x05\x12/\n\x05\x63hild\x18\x02 \x01(\x0b\x32 .protobuf_unittest.TestRecursion\"\x86\x01\n\rTestStringMap\x12\x43\n\nstring_map\x18\x01 \x03(\x0b\x32/.protobuf_unittest.TestStringMap.StringMapEntry\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc4\x01\n\x14TestStringSerializer\x12\x15\n\rscalar_string\x18\x01 \x01(\t\x12\x17\n\x0frepeated_string\x18\x02 \x03(\t\x12J\n\nstring_map\x18\x03 \x03(\x0b\x32\x36.protobuf_unittest.TestStringSerializer.StringMapEntry\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"$\n\x18TestMessageWithExtension*\x08\x08\x64\x10\x80\x80\x80\x80\x02\"z\n\rTestExtension\x12\r\n\x05value\x18\x01 \x01(\t2Z\n\x03\x65xt\x12+.protobuf_unittest.TestMessageWithExtension\x18\x64 \x01(\x0b\x32 .protobuf_unittest.TestExtension\"Q\n\x14TestDefaultEnumValue\x12\x39\n\nenum_value\x18\x01 \x01(\x0e\x32\x1c.protobuf_unittest.EnumValue:\x07\x44\x45\x46\x41ULT*2\n\tEnumValue\x12\x0c\n\x08PROTOCOL\x10\x00\x12\n\n\x06\x42UFFER\x10\x01\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x02') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.util.json_format_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + TestMessageWithExtension.RegisterExtension(_TESTEXTENSION.extensions_by_name['ext']) + + DESCRIPTOR._options = None + _TESTBOOLMAP_BOOLMAPENTRY._options = None + _TESTBOOLMAP_BOOLMAPENTRY._serialized_options = b'8\001' + _TESTSTRINGMAP_STRINGMAPENTRY._options = None + _TESTSTRINGMAP_STRINGMAPENTRY._serialized_options = b'8\001' + _TESTSTRINGSERIALIZER_STRINGMAPENTRY._options = None + _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_options = b'8\001' + _ENUMVALUE._serialized_start=1607 + _ENUMVALUE._serialized_end=1657 + _TESTFLAGSANDSTRINGS._serialized_start=62 + _TESTFLAGSANDSTRINGS._serialized_end=199 + _TESTFLAGSANDSTRINGS_REPEATEDGROUP._serialized_start=173 + _TESTFLAGSANDSTRINGS_REPEATEDGROUP._serialized_end=199 + _TESTBASE64BYTEARRAYS._serialized_start=201 + _TESTBASE64BYTEARRAYS._serialized_end=234 + _TESTJAVASCRIPTJSON._serialized_start=236 + _TESTJAVASCRIPTJSON._serialized_end=307 + _TESTJAVASCRIPTORDERJSON1._serialized_start=309 + _TESTJAVASCRIPTORDERJSON1._serialized_end=390 + _TESTJAVASCRIPTORDERJSON2._serialized_start=393 + _TESTJAVASCRIPTORDERJSON2._serialized_end=530 + _TESTLARGEINT._serialized_start=532 + _TESTLARGEINT._serialized_end=568 + _TESTNUMBERS._serialized_start=571 + _TESTNUMBERS._serialized_end=731 + _TESTNUMBERS_MYTYPE._serialized_start=691 + _TESTNUMBERS_MYTYPE._serialized_end=731 + _TESTCAMELCASE._serialized_start=733 + _TESTCAMELCASE._serialized_end=817 + _TESTBOOLMAP._serialized_start=819 + _TESTBOOLMAP._serialized_end=943 + _TESTBOOLMAP_BOOLMAPENTRY._serialized_start=897 + _TESTBOOLMAP_BOOLMAPENTRY._serialized_end=943 + _TESTRECURSION._serialized_start=945 + _TESTRECURSION._serialized_end=1024 + _TESTSTRINGMAP._serialized_start=1027 + _TESTSTRINGMAP._serialized_end=1161 + _TESTSTRINGMAP_STRINGMAPENTRY._serialized_start=1113 + _TESTSTRINGMAP_STRINGMAPENTRY._serialized_end=1161 + _TESTSTRINGSERIALIZER._serialized_start=1164 + _TESTSTRINGSERIALIZER._serialized_end=1360 + _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_start=1113 + _TESTSTRINGSERIALIZER_STRINGMAPENTRY._serialized_end=1161 + _TESTMESSAGEWITHEXTENSION._serialized_start=1362 + _TESTMESSAGEWITHEXTENSION._serialized_end=1398 + _TESTEXTENSION._serialized_start=1400 + _TESTEXTENSION._serialized_end=1522 + _TESTDEFAULTENUMVALUE._serialized_start=1524 + _TESTDEFAULTENUMVALUE._serialized_end=1605 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/util/json_format_proto3_pb2.py b/scripts/protobuf3/protobuf3/util/json_format_proto3_pb2.py new file mode 100644 index 0000000..5498dea --- /dev/null +++ b/scripts/protobuf3/protobuf3/util/json_format_proto3_pb2.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/util/json_format_proto3.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 +from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 +from google.protobuf import field_mask_pb2 as google_dot_protobuf_dot_field__mask__pb2 +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2 +from google.protobuf import unittest_pb2 as google_dot_protobuf_dot_unittest__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-google/protobuf/util/json_format_proto3.proto\x12\x06proto3\x1a\x19google/protobuf/any.proto\x1a\x1egoogle/protobuf/duration.proto\x1a google/protobuf/field_mask.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x1egoogle/protobuf/unittest.proto\"\x1c\n\x0bMessageType\x12\r\n\x05value\x18\x01 \x01(\x05\"\x94\x05\n\x0bTestMessage\x12\x12\n\nbool_value\x18\x01 \x01(\x08\x12\x13\n\x0bint32_value\x18\x02 \x01(\x05\x12\x13\n\x0bint64_value\x18\x03 \x01(\x03\x12\x14\n\x0cuint32_value\x18\x04 \x01(\r\x12\x14\n\x0cuint64_value\x18\x05 \x01(\x04\x12\x13\n\x0b\x66loat_value\x18\x06 \x01(\x02\x12\x14\n\x0c\x64ouble_value\x18\x07 \x01(\x01\x12\x14\n\x0cstring_value\x18\x08 \x01(\t\x12\x13\n\x0b\x62ytes_value\x18\t \x01(\x0c\x12$\n\nenum_value\x18\n \x01(\x0e\x32\x10.proto3.EnumType\x12*\n\rmessage_value\x18\x0b \x01(\x0b\x32\x13.proto3.MessageType\x12\x1b\n\x13repeated_bool_value\x18\x15 \x03(\x08\x12\x1c\n\x14repeated_int32_value\x18\x16 \x03(\x05\x12\x1c\n\x14repeated_int64_value\x18\x17 \x03(\x03\x12\x1d\n\x15repeated_uint32_value\x18\x18 \x03(\r\x12\x1d\n\x15repeated_uint64_value\x18\x19 \x03(\x04\x12\x1c\n\x14repeated_float_value\x18\x1a \x03(\x02\x12\x1d\n\x15repeated_double_value\x18\x1b \x03(\x01\x12\x1d\n\x15repeated_string_value\x18\x1c \x03(\t\x12\x1c\n\x14repeated_bytes_value\x18\x1d \x03(\x0c\x12-\n\x13repeated_enum_value\x18\x1e \x03(\x0e\x32\x10.proto3.EnumType\x12\x33\n\x16repeated_message_value\x18\x1f \x03(\x0b\x32\x13.proto3.MessageType\"\x8c\x02\n\tTestOneof\x12\x1b\n\x11oneof_int32_value\x18\x01 \x01(\x05H\x00\x12\x1c\n\x12oneof_string_value\x18\x02 \x01(\tH\x00\x12\x1b\n\x11oneof_bytes_value\x18\x03 \x01(\x0cH\x00\x12,\n\x10oneof_enum_value\x18\x04 \x01(\x0e\x32\x10.proto3.EnumTypeH\x00\x12\x32\n\x13oneof_message_value\x18\x05 \x01(\x0b\x32\x13.proto3.MessageTypeH\x00\x12\x36\n\x10oneof_null_value\x18\x06 \x01(\x0e\x32\x1a.google.protobuf.NullValueH\x00\x42\r\n\x0boneof_value\"\xe1\x04\n\x07TestMap\x12.\n\x08\x62ool_map\x18\x01 \x03(\x0b\x32\x1c.proto3.TestMap.BoolMapEntry\x12\x30\n\tint32_map\x18\x02 \x03(\x0b\x32\x1d.proto3.TestMap.Int32MapEntry\x12\x30\n\tint64_map\x18\x03 \x03(\x0b\x32\x1d.proto3.TestMap.Int64MapEntry\x12\x32\n\nuint32_map\x18\x04 \x03(\x0b\x32\x1e.proto3.TestMap.Uint32MapEntry\x12\x32\n\nuint64_map\x18\x05 \x03(\x0b\x32\x1e.proto3.TestMap.Uint64MapEntry\x12\x32\n\nstring_map\x18\x06 \x03(\x0b\x32\x1e.proto3.TestMap.StringMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x03\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"\x85\x06\n\rTestNestedMap\x12\x34\n\x08\x62ool_map\x18\x01 \x03(\x0b\x32\".proto3.TestNestedMap.BoolMapEntry\x12\x36\n\tint32_map\x18\x02 \x03(\x0b\x32#.proto3.TestNestedMap.Int32MapEntry\x12\x36\n\tint64_map\x18\x03 \x03(\x0b\x32#.proto3.TestNestedMap.Int64MapEntry\x12\x38\n\nuint32_map\x18\x04 \x03(\x0b\x32$.proto3.TestNestedMap.Uint32MapEntry\x12\x38\n\nuint64_map\x18\x05 \x03(\x0b\x32$.proto3.TestNestedMap.Uint64MapEntry\x12\x38\n\nstring_map\x18\x06 \x03(\x0b\x32$.proto3.TestNestedMap.StringMapEntry\x12\x32\n\x07map_map\x18\x07 \x03(\x0b\x32!.proto3.TestNestedMap.MapMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a/\n\rInt64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x03\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint32MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eUint64MapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x1a\x44\n\x0bMapMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b\x32\x15.proto3.TestNestedMap:\x02\x38\x01\"{\n\rTestStringMap\x12\x38\n\nstring_map\x18\x01 \x03(\x0b\x32$.proto3.TestStringMap.StringMapEntry\x1a\x30\n\x0eStringMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xee\x07\n\x0bTestWrapper\x12.\n\nbool_value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12\x30\n\x0bint32_value\x18\x02 \x01(\x0b\x32\x1b.google.protobuf.Int32Value\x12\x30\n\x0bint64_value\x18\x03 \x01(\x0b\x32\x1b.google.protobuf.Int64Value\x12\x32\n\x0cuint32_value\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x32\n\x0cuint64_value\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.UInt64Value\x12\x30\n\x0b\x66loat_value\x18\x06 \x01(\x0b\x32\x1b.google.protobuf.FloatValue\x12\x32\n\x0c\x64ouble_value\x18\x07 \x01(\x0b\x32\x1c.google.protobuf.DoubleValue\x12\x32\n\x0cstring_value\x18\x08 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x30\n\x0b\x62ytes_value\x18\t \x01(\x0b\x32\x1b.google.protobuf.BytesValue\x12\x37\n\x13repeated_bool_value\x18\x0b \x03(\x0b\x32\x1a.google.protobuf.BoolValue\x12\x39\n\x14repeated_int32_value\x18\x0c \x03(\x0b\x32\x1b.google.protobuf.Int32Value\x12\x39\n\x14repeated_int64_value\x18\r \x03(\x0b\x32\x1b.google.protobuf.Int64Value\x12;\n\x15repeated_uint32_value\x18\x0e \x03(\x0b\x32\x1c.google.protobuf.UInt32Value\x12;\n\x15repeated_uint64_value\x18\x0f \x03(\x0b\x32\x1c.google.protobuf.UInt64Value\x12\x39\n\x14repeated_float_value\x18\x10 \x03(\x0b\x32\x1b.google.protobuf.FloatValue\x12;\n\x15repeated_double_value\x18\x11 \x03(\x0b\x32\x1c.google.protobuf.DoubleValue\x12;\n\x15repeated_string_value\x18\x12 \x03(\x0b\x32\x1c.google.protobuf.StringValue\x12\x39\n\x14repeated_bytes_value\x18\x13 \x03(\x0b\x32\x1b.google.protobuf.BytesValue\"n\n\rTestTimestamp\x12)\n\x05value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x32\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x1a.google.protobuf.Timestamp\"k\n\x0cTestDuration\x12(\n\x05value\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x31\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x19.google.protobuf.Duration\":\n\rTestFieldMask\x12)\n\x05value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.FieldMask\"e\n\nTestStruct\x12&\n\x05value\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12/\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x17.google.protobuf.Struct\"\\\n\x07TestAny\x12#\n\x05value\x18\x01 \x01(\x0b\x32\x14.google.protobuf.Any\x12,\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x14.google.protobuf.Any\"b\n\tTestValue\x12%\n\x05value\x18\x01 \x01(\x0b\x32\x16.google.protobuf.Value\x12.\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x16.google.protobuf.Value\"n\n\rTestListValue\x12)\n\x05value\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.ListValue\x12\x32\n\x0erepeated_value\x18\x02 \x03(\x0b\x32\x1a.google.protobuf.ListValue\"\x89\x01\n\rTestBoolValue\x12\x12\n\nbool_value\x18\x01 \x01(\x08\x12\x34\n\x08\x62ool_map\x18\x02 \x03(\x0b\x32\".proto3.TestBoolValue.BoolMapEntry\x1a.\n\x0c\x42oolMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\x08\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\"+\n\x12TestCustomJsonName\x12\x15\n\x05value\x18\x01 \x01(\x05R\x06@value\"J\n\x0eTestExtensions\x12\x38\n\nextensions\x18\x01 \x01(\x0b\x32$.protobuf_unittest.TestAllExtensions\"\x84\x01\n\rTestEnumValue\x12%\n\x0b\x65num_value1\x18\x01 \x01(\x0e\x32\x10.proto3.EnumType\x12%\n\x0b\x65num_value2\x18\x02 \x01(\x0e\x32\x10.proto3.EnumType\x12%\n\x0b\x65num_value3\x18\x03 \x01(\x0e\x32\x10.proto3.EnumType*\x1c\n\x08\x45numType\x12\x07\n\x03\x46OO\x10\x00\x12\x07\n\x03\x42\x41R\x10\x01\x42,\n\x18\x63om.google.protobuf.utilB\x10JsonFormatProto3b\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.util.json_format_proto3_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\030com.google.protobuf.utilB\020JsonFormatProto3' + _TESTMAP_BOOLMAPENTRY._options = None + _TESTMAP_BOOLMAPENTRY._serialized_options = b'8\001' + _TESTMAP_INT32MAPENTRY._options = None + _TESTMAP_INT32MAPENTRY._serialized_options = b'8\001' + _TESTMAP_INT64MAPENTRY._options = None + _TESTMAP_INT64MAPENTRY._serialized_options = b'8\001' + _TESTMAP_UINT32MAPENTRY._options = None + _TESTMAP_UINT32MAPENTRY._serialized_options = b'8\001' + _TESTMAP_UINT64MAPENTRY._options = None + _TESTMAP_UINT64MAPENTRY._serialized_options = b'8\001' + _TESTMAP_STRINGMAPENTRY._options = None + _TESTMAP_STRINGMAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_BOOLMAPENTRY._options = None + _TESTNESTEDMAP_BOOLMAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_INT32MAPENTRY._options = None + _TESTNESTEDMAP_INT32MAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_INT64MAPENTRY._options = None + _TESTNESTEDMAP_INT64MAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_UINT32MAPENTRY._options = None + _TESTNESTEDMAP_UINT32MAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_UINT64MAPENTRY._options = None + _TESTNESTEDMAP_UINT64MAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_STRINGMAPENTRY._options = None + _TESTNESTEDMAP_STRINGMAPENTRY._serialized_options = b'8\001' + _TESTNESTEDMAP_MAPMAPENTRY._options = None + _TESTNESTEDMAP_MAPMAPENTRY._serialized_options = b'8\001' + _TESTSTRINGMAP_STRINGMAPENTRY._options = None + _TESTSTRINGMAP_STRINGMAPENTRY._serialized_options = b'8\001' + _TESTBOOLVALUE_BOOLMAPENTRY._options = None + _TESTBOOLVALUE_BOOLMAPENTRY._serialized_options = b'8\001' + _ENUMTYPE._serialized_start=4849 + _ENUMTYPE._serialized_end=4877 + _MESSAGETYPE._serialized_start=277 + _MESSAGETYPE._serialized_end=305 + _TESTMESSAGE._serialized_start=308 + _TESTMESSAGE._serialized_end=968 + _TESTONEOF._serialized_start=971 + _TESTONEOF._serialized_end=1239 + _TESTMAP._serialized_start=1242 + _TESTMAP._serialized_end=1851 + _TESTMAP_BOOLMAPENTRY._serialized_start=1557 + _TESTMAP_BOOLMAPENTRY._serialized_end=1603 + _TESTMAP_INT32MAPENTRY._serialized_start=1605 + _TESTMAP_INT32MAPENTRY._serialized_end=1652 + _TESTMAP_INT64MAPENTRY._serialized_start=1654 + _TESTMAP_INT64MAPENTRY._serialized_end=1701 + _TESTMAP_UINT32MAPENTRY._serialized_start=1703 + _TESTMAP_UINT32MAPENTRY._serialized_end=1751 + _TESTMAP_UINT64MAPENTRY._serialized_start=1753 + _TESTMAP_UINT64MAPENTRY._serialized_end=1801 + _TESTMAP_STRINGMAPENTRY._serialized_start=1803 + _TESTMAP_STRINGMAPENTRY._serialized_end=1851 + _TESTNESTEDMAP._serialized_start=1854 + _TESTNESTEDMAP._serialized_end=2627 + _TESTNESTEDMAP_BOOLMAPENTRY._serialized_start=1557 + _TESTNESTEDMAP_BOOLMAPENTRY._serialized_end=1603 + _TESTNESTEDMAP_INT32MAPENTRY._serialized_start=1605 + _TESTNESTEDMAP_INT32MAPENTRY._serialized_end=1652 + _TESTNESTEDMAP_INT64MAPENTRY._serialized_start=1654 + _TESTNESTEDMAP_INT64MAPENTRY._serialized_end=1701 + _TESTNESTEDMAP_UINT32MAPENTRY._serialized_start=1703 + _TESTNESTEDMAP_UINT32MAPENTRY._serialized_end=1751 + _TESTNESTEDMAP_UINT64MAPENTRY._serialized_start=1753 + _TESTNESTEDMAP_UINT64MAPENTRY._serialized_end=1801 + _TESTNESTEDMAP_STRINGMAPENTRY._serialized_start=1803 + _TESTNESTEDMAP_STRINGMAPENTRY._serialized_end=1851 + _TESTNESTEDMAP_MAPMAPENTRY._serialized_start=2559 + _TESTNESTEDMAP_MAPMAPENTRY._serialized_end=2627 + _TESTSTRINGMAP._serialized_start=2629 + _TESTSTRINGMAP._serialized_end=2752 + _TESTSTRINGMAP_STRINGMAPENTRY._serialized_start=2704 + _TESTSTRINGMAP_STRINGMAPENTRY._serialized_end=2752 + _TESTWRAPPER._serialized_start=2755 + _TESTWRAPPER._serialized_end=3761 + _TESTTIMESTAMP._serialized_start=3763 + _TESTTIMESTAMP._serialized_end=3873 + _TESTDURATION._serialized_start=3875 + _TESTDURATION._serialized_end=3982 + _TESTFIELDMASK._serialized_start=3984 + _TESTFIELDMASK._serialized_end=4042 + _TESTSTRUCT._serialized_start=4044 + _TESTSTRUCT._serialized_end=4145 + _TESTANY._serialized_start=4147 + _TESTANY._serialized_end=4239 + _TESTVALUE._serialized_start=4241 + _TESTVALUE._serialized_end=4339 + _TESTLISTVALUE._serialized_start=4341 + _TESTLISTVALUE._serialized_end=4451 + _TESTBOOLVALUE._serialized_start=4454 + _TESTBOOLVALUE._serialized_end=4591 + _TESTBOOLVALUE_BOOLMAPENTRY._serialized_start=1557 + _TESTBOOLVALUE_BOOLMAPENTRY._serialized_end=1603 + _TESTCUSTOMJSONNAME._serialized_start=4593 + _TESTCUSTOMJSONNAME._serialized_end=4636 + _TESTEXTENSIONS._serialized_start=4638 + _TESTEXTENSIONS._serialized_end=4712 + _TESTENUMVALUE._serialized_start=4715 + _TESTENUMVALUE._serialized_end=4847 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/protobuf3/wrappers_pb2.py b/scripts/protobuf3/protobuf3/wrappers_pb2.py new file mode 100644 index 0000000..e49eb4c --- /dev/null +++ b/scripts/protobuf3/protobuf3/wrappers_pb2.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google/protobuf/wrappers.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1egoogle/protobuf/wrappers.proto\x12\x0fgoogle.protobuf\"\x1c\n\x0b\x44oubleValue\x12\r\n\x05value\x18\x01 \x01(\x01\"\x1b\n\nFloatValue\x12\r\n\x05value\x18\x01 \x01(\x02\"\x1b\n\nInt64Value\x12\r\n\x05value\x18\x01 \x01(\x03\"\x1c\n\x0bUInt64Value\x12\r\n\x05value\x18\x01 \x01(\x04\"\x1b\n\nInt32Value\x12\r\n\x05value\x18\x01 \x01(\x05\"\x1c\n\x0bUInt32Value\x12\r\n\x05value\x18\x01 \x01(\r\"\x1a\n\tBoolValue\x12\r\n\x05value\x18\x01 \x01(\x08\"\x1c\n\x0bStringValue\x12\r\n\x05value\x18\x01 \x01(\t\"\x1b\n\nBytesValue\x12\r\n\x05value\x18\x01 \x01(\x0c\x42\x83\x01\n\x13\x63om.google.protobufB\rWrappersProtoP\x01Z1google.golang.org/protobuf/types/known/wrapperspb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.wrappers_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\rWrappersProtoP\001Z1google.golang.org/protobuf/types/known/wrapperspb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _DOUBLEVALUE._serialized_start=51 + _DOUBLEVALUE._serialized_end=79 + _FLOATVALUE._serialized_start=81 + _FLOATVALUE._serialized_end=108 + _INT64VALUE._serialized_start=110 + _INT64VALUE._serialized_end=137 + _UINT64VALUE._serialized_start=139 + _UINT64VALUE._serialized_end=167 + _INT32VALUE._serialized_start=169 + _INT32VALUE._serialized_end=196 + _UINT32VALUE._serialized_start=198 + _UINT32VALUE._serialized_end=226 + _BOOLVALUE._serialized_start=228 + _BOOLVALUE._serialized_end=254 + _STRINGVALUE._serialized_start=256 + _STRINGVALUE._serialized_end=284 + _BYTESVALUE._serialized_start=286 + _BYTESVALUE._serialized_end=313 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/protobuf3/pyproject.toml b/scripts/protobuf3/pyproject.toml new file mode 100644 index 0000000..100fbc9 --- /dev/null +++ b/scripts/protobuf3/pyproject.toml @@ -0,0 +1,19 @@ +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "protobuf3" +version = "3.20.2" +description = "protobuf3" +license = "CC BY-NC-ND 4.0" +authors = ["google"] +repository = "https://github.com/protocolbuffers/protobuf/" + + +[tool.poetry.urls] +"Issues" = "https://github.com/protocolbuffers/protobuf//issues" + +[tool.poetry.dependencies] +python = ">=3.7,<4.0" +requests = "^2.32.3" diff --git a/scripts/pyplayready/README.md b/scripts/pyplayready/README.md new file mode 100644 index 0000000..963f8ef --- /dev/null +++ b/scripts/pyplayready/README.md @@ -0,0 +1,90 @@ +# pyplayready +All of this is already public. 100% of this code has been derived from the mspr_toolkit. + +## Installation +```shell +pip install pyplayready +``` + +Run `pyplayready --help` to view available cli functions + +## Devices +Run the command below to create a Playready Device (.prd) from a `bgroupcert.dat` and `zgpriv.dat`: +```shell +pyplayready create-device -c bgroupcert.dat -k zgpriv.dat +``` + +Test a playready device: +```shell +pyplayready test DEVICE.prd +``` + +> [!IMPORTANT] +> There currently isn't a proper method of extracting Group Certificates/Keys. They can be found inside older Samsung phones/Smart TVs, Windows DLLs and set-top-boxes in encrypted form. + +Export a provisioned device to its raw .dat files +```shell +pyplayready export-device DEVICE.prd +``` + +## Usage +An example code snippet: + +```python +from pyplayready.cdm import Cdm +from pyplayready.device import Device +from pyplayready.system.pssh import PSSH + +import requests + +device = Device.load("C:/Path/To/A/Device.prd") +cdm = Cdm.from_device(device) +session_id = cdm.open() + +pssh = PSSH( + "AAADfHBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAA1xcAwAAAQABAFIDPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AH" + "QAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABh" + "AHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUg" + "BPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQA" + "UgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgA0AFIAcABsAGIAKwBUAGIATgBFAFMAOAB0AE" + "cAawBOAEYAVwBUAEUASABBAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AEsATABqADMAUQB6AFEAUAAvAE4AQQA9ADwALwBD" + "AEgARQBDAEsAUwBVAE0APgA8AEwAQQBfAFUAUgBMAD4AaAB0AHQAcABzADoALwAvAHAAcgBvAGYAZgBpAGMAaQBhAGwAcwBpAHQAZQAuAGsAZQ" + "B5AGQAZQBsAGkAdgBlAHIAeQAuAG0AZQBkAGkAYQBzAGUAcgB2AGkAYwBlAHMALgB3AGkAbgBkAG8AdwBzAC4AbgBlAHQALwBQAGwAYQB5AFIA" + "ZQBhAGQAeQAvADwALwBMAEEAXwBVAFIATAA+ADwAQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwASQBJAFMAXwBEAFIATQBfAF" + "YARQBSAFMASQBPAE4APgA4AC4AMQAuADIAMwAwADQALgAzADEAPAAvAEkASQBTAF8ARABSAE0AXwBWAEUAUgBTAEkATwBOAD4APAAvAEMAVQBT" + "AFQATwBNAEEAVABUAFIASQBCAFUAVABFAFMAPgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA==" +) + +wrm_headers = pssh.get_wrm_headers() +request = cdm.get_license_challenge(session_id, wrm_headers[0]) + +response = requests.post( + url="https://test.playready.microsoft.com/service/rightsmanager.asmx?cfg=(persist:false,sl:2000)", + headers={ + 'Content-Type': 'text/xml; charset=UTF-8', + }, + data=request, +) + +cdm.parse_license(session_id, response.text) + +for key in cdm.get_keys(session_id): + print(f"{key.key_id.hex}:{key.key.hex()}") + +cdm.close(session_id) +``` + +## Disclaimer + +1. This project requires a valid Microsoft Certificate and Group Key, which are not provided by this project. +2. Public test provisions are available and provided by Microsoft to use for testing projects such as this one. +3. This project does not condone piracy or any action against the terms of the DRM systems. +4. All efforts in this project have been the result of Reverse-Engineering, Publicly available research, and Trial & Error. +5. Do not use this program to decrypt or access any content for which you do not have the legal rights or explicit permission. +6. Unauthorized decryption or distribution of copyrighted materials is a violation of applicable laws and intellectual property rights. +7. This tool must not be used for any illegal activities, including but not limited to piracy, circumventing digital rights management (DRM), or unauthorized access to protected content. +8. The developers, contributors, and maintainers of this program are not responsible for any misuse or illegal activities performed using this software. +9. By using this program, you agree to comply with all applicable laws and regulations governing digital rights and copyright protections. + +## Credits ++ [mspr_toolkit](https://security-explorations.com/materials/mspr_toolkit.zip) diff --git a/scripts/pyplayready/pyplayready/__init__.py b/scripts/pyplayready/pyplayready/__init__.py new file mode 100644 index 0000000..0ff5604 --- /dev/null +++ b/scripts/pyplayready/pyplayready/__init__.py @@ -0,0 +1,14 @@ +from pyplayready.cdm import * +from pyplayready.crypto.ecc_key import * +from pyplayready.crypto.elgamal import * +from pyplayready.device import * +from pyplayready.license.key import * +from pyplayready.license.xml_key import * +from pyplayready.license.xmrlicense import * +from pyplayready.remote.remotecdm import * +from pyplayready.system.bcert import * +from pyplayready.system.pssh import * +from pyplayready.system.session import * + + +__version__ = "0.5.0" diff --git a/scripts/pyplayready/pyplayready/cdm.py b/scripts/pyplayready/pyplayready/cdm.py new file mode 100644 index 0000000..d56f890 --- /dev/null +++ b/scripts/pyplayready/pyplayready/cdm.py @@ -0,0 +1,311 @@ +from __future__ import annotations + +import base64 +import math +import time +from typing import List, Union +from uuid import UUID +import xml.etree.ElementTree as ET + +from Crypto.Cipher import AES +from Crypto.Hash import SHA256 +from Crypto.Random import get_random_bytes +from Crypto.Util.Padding import pad + +from ecpy.curves import Point, Curve + +from pyplayready.crypto import Crypto +from pyplayready.system.bcert import CertificateChain +from pyplayready.crypto.ecc_key import ECCKey +from pyplayready.license.key import Key +from pyplayready.license.xmrlicense import XMRLicense +from pyplayready.exceptions import (InvalidSession, TooManySessions, InvalidLicense) +from pyplayready.system.session import Session + + +class Cdm: + MAX_NUM_OF_SESSIONS = 16 + + def __init__( + self, + security_level: int, + certificate_chain: Union[CertificateChain, None], + encryption_key: Union[ECCKey, None], + signing_key: Union[ECCKey, None], + client_version: str = "10.0.16384.10011", + protocol_version: int = 1 + ): + self.security_level = security_level + self.certificate_chain = certificate_chain + self.encryption_key = encryption_key + self.signing_key = signing_key + self.client_version = client_version + self.protocol_version = protocol_version + + self.__crypto = Crypto() + self._wmrm_key = Point( + x=0xc8b6af16ee941aadaa5389b4af2c10e356be42af175ef3face93254e7b0b3d9b, + y=0x982b27b5cb2341326e56aa857dbfd5c634ce2cf9ea74fca8f2af5957efeea562, + curve=Curve.get_curve("secp256r1") + ) + self._rgbMagicConstantZero = bytes([0x7e, 0xe9, 0xed, 0x4a, 0xf7, 0x73, 0x22, 0x4f, 0x00, 0xb8, 0xea, 0x7e, 0xfb, 0x02, 0x7c, 0xbb]) + + self.__sessions: dict[bytes, Session] = {} + + @classmethod + def from_device(cls, device) -> Cdm: + """Initialize a Playready CDM from a Playready Device (.prd) file""" + return cls( + security_level=device.security_level, + certificate_chain=device.group_certificate, + encryption_key=device.encryption_key, + signing_key=device.signing_key + ) + + def open(self) -> bytes: + """ + Open a Playready Content Decryption Module (CDM) session. + + Raises: + TooManySessions: If the session cannot be opened as limit has been reached. + """ + if len(self.__sessions) > self.MAX_NUM_OF_SESSIONS: + raise TooManySessions(f"Too many Sessions open ({self.MAX_NUM_OF_SESSIONS}).") + + session = Session(len(self.__sessions) + 1) + self.__sessions[session.id] = session + + return session.id + + def close(self, session_id: bytes) -> None: + """ + Close a Playready Content Decryption Module (CDM) session. + + Parameters: + session_id: Session identifier. + + Raises: + InvalidSession: If the Session identifier is invalid. + """ + session = self.__sessions.get(session_id) + if not session: + raise InvalidSession(f"Session identifier {session_id!r} is invalid.") + del self.__sessions[session_id] + + def _get_key_data(self, session: Session) -> bytes: + return self.__crypto.ecc256_encrypt( + public_key=self._wmrm_key, + plaintext=session.xml_key.get_point() + ) + + def _get_cipher_data(self, session: Session) -> bytes: + b64_chain = base64.b64encode(self.certificate_chain.dumps()).decode() + body = ( + "<Data>" + f"<CertificateChains><CertificateChain>{b64_chain}</CertificateChain></CertificateChains>" + "<Features>" + '<Feature Name="AESCBC">""</Feature>' + "<REE>" + "<AESCBCS></AESCBCS>" + "</REE>" + "</Features>" + "</Data>" + ) + + cipher = AES.new( + key=session.xml_key.aes_key, + mode=AES.MODE_CBC, + iv=session.xml_key.aes_iv + ) + + ciphertext = cipher.encrypt(pad( + body.encode(), + AES.block_size + )) + + return session.xml_key.aes_iv + ciphertext + + def _build_digest_content( + self, + wrm_header: str, + nonce: str, + wmrm_cipher: str, + cert_cipher: str + ) -> str: + return ( + '<LA xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols" Id="SignedData" xml:space="preserve">' + f'<Version>{self.protocol_version}</Version>' + f'<ContentHeader>{wrm_header}</ContentHeader>' + '<CLIENTINFO>' + f'<CLIENTVERSION>{self.client_version}</CLIENTVERSION>' + '</CLIENTINFO>' + f'<LicenseNonce>{nonce}</LicenseNonce>' + f'<ClientTime>{math.floor(time.time())}</ClientTime>' + '<EncryptedData xmlns="http://www.w3.org/2001/04/xmlenc#" Type="http://www.w3.org/2001/04/xmlenc#Element">' + '<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"></EncryptionMethod>' + '<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' + '<EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">' + '<EncryptionMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#ecc256"></EncryptionMethod>' + '<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' + '<KeyName>WMRMServer</KeyName>' + '</KeyInfo>' + '<CipherData>' + f'<CipherValue>{wmrm_cipher}</CipherValue>' + '</CipherData>' + '</EncryptedKey>' + '</KeyInfo>' + '<CipherData>' + f'<CipherValue>{cert_cipher}</CipherValue>' + '</CipherData>' + '</EncryptedData>' + '</LA>' + ) + + @staticmethod + def _build_signed_info(digest_value: str) -> str: + return ( + '<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' + '<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod>' + '<SignatureMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#ecdsa-sha256"></SignatureMethod>' + '<Reference URI="#SignedData">' + '<DigestMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#sha256"></DigestMethod>' + f'<DigestValue>{digest_value}</DigestValue>' + '</Reference>' + '</SignedInfo>' + ) + + def get_license_challenge(self, session_id: bytes, wrm_header: str) -> str: + session = self.__sessions.get(session_id) + if not session: + raise InvalidSession(f"Session identifier {session_id!r} is invalid.") + + session.signing_key = self.signing_key + session.encryption_key = self.encryption_key + + la_content = self._build_digest_content( + wrm_header=wrm_header, + nonce=base64.b64encode(get_random_bytes(16)).decode(), + wmrm_cipher=base64.b64encode(self._get_key_data(session)).decode(), + cert_cipher=base64.b64encode(self._get_cipher_data(session)).decode() + ) + + la_hash_obj = SHA256.new() + la_hash_obj.update(la_content.encode()) + la_hash = la_hash_obj.digest() + + signed_info = self._build_signed_info(base64.b64encode(la_hash).decode()) + signature = self.__crypto.ecc256_sign(session.signing_key, signed_info.encode()) + + # haven't found a better way to do this. xmltodict.unparse doesn't work + main_body = ( + '<?xml version="1.0" encoding="utf-8"?>' + '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">' + '<soap:Body>' + '<AcquireLicense xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols">' + '<challenge>' + '<Challenge xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols/messages">' + + la_content + + '<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">' + + signed_info + + f'<SignatureValue>{base64.b64encode(signature).decode()}</SignatureValue>' + '<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' + '<KeyValue>' + '<ECCKeyValue>' + f'<PublicKey>{base64.b64encode(session.signing_key.public_bytes()).decode()}</PublicKey>' + '</ECCKeyValue>' + '</KeyValue>' + '</KeyInfo>' + '</Signature>' + '</Challenge>' + '</challenge>' + '</AcquireLicense>' + '</soap:Body>' + '</soap:Envelope>' + ) + + return main_body + + @staticmethod + def _verify_encryption_key(session: Session, licence: XMRLicense) -> bool: + ecc_keys = list(licence.get_object(42)) + if not ecc_keys: + raise InvalidLicense("No ECC public key in license") + + return ecc_keys[0].key == session.encryption_key.public_bytes() + + def parse_license(self, session_id: bytes, licence: str) -> None: + session = self.__sessions.get(session_id) + if not session: + raise InvalidSession(f"Session identifier {session_id!r} is invalid.") + + if not session.encryption_key or not session.signing_key: + raise InvalidSession("Cannot parse a license message without first making a license request") + + try: + root = ET.fromstring(licence) + license_elements = root.findall(".//{http://schemas.microsoft.com/DRM/2007/03/protocols}License") + + for license_element in license_elements: + parsed_licence = XMRLicense.loads(license_element.text) + + if not self._verify_encryption_key(session, parsed_licence): + raise InvalidLicense("Public encryption key does not match") + + is_scalable = bool(next(parsed_licence.get_object(81), None)) + + for content_key in parsed_licence.get_content_keys(): + cipher_type = Key.CipherType(content_key.cipher_type) + + if not cipher_type in (Key.CipherType.ECC_256, Key.CipherType.ECC_256_WITH_KZ, Key.CipherType.ECC_256_VIA_SYMMETRIC): + raise InvalidLicense(f"Invalid cipher type {cipher_type}") + + via_symmetric = Key.CipherType(content_key.cipher_type) == Key.CipherType.ECC_256_VIA_SYMMETRIC + + decrypted = self.__crypto.ecc256_decrypt( + private_key=session.encryption_key, + ciphertext=content_key.encrypted_key + ) + ci, ck = decrypted[:16], decrypted[16:32] + + if is_scalable: + ci, ck = decrypted[::2][:16], decrypted[1::2][:16] + + if via_symmetric: + embedded_root_license = content_key.encrypted_key[:144] + embedded_leaf_license = content_key.encrypted_key[144:] + + rgb_key = bytes(ck[i] ^ self._rgbMagicConstantZero[i] for i in range(16)) + content_key_prime = AES.new(ck, AES.MODE_ECB).encrypt(rgb_key) + + aux_key = next(parsed_licence.get_object(81))["auxiliary_keys"][0]["key"] + derived_aux_key = AES.new(content_key_prime, AES.MODE_ECB).encrypt(aux_key) + + uplink_x_key = bytes(bytearray(16)[i] ^ derived_aux_key[i] for i in range(16)) + secondary_key = AES.new(ck, AES.MODE_ECB).encrypt(embedded_root_license[128:]) + + embedded_leaf_license = AES.new(uplink_x_key, AES.MODE_ECB).encrypt(embedded_leaf_license) + embedded_leaf_license = AES.new(secondary_key, AES.MODE_ECB).encrypt(embedded_leaf_license) + + ci, ck = embedded_leaf_license[:16], embedded_leaf_license[16:] + + if not parsed_licence.check_signature(ci): + raise InvalidLicense("License integrity signature does not match") + + session.keys.append(Key( + key_id=UUID(bytes_le=content_key.key_id), + key_type=content_key.key_type, + cipher_type=content_key.cipher_type, + key_length=content_key.key_length, + key=ck + )) + except InvalidLicense as e: + raise InvalidLicense(e) + except Exception as e: + raise Exception(f"Unable to parse license, {e}") + + def get_keys(self, session_id: bytes) -> List[Key]: + session = self.__sessions.get(session_id) + if not session: + raise InvalidSession(f"Session identifier {session_id!r} is invalid.") + + return session.keys diff --git a/scripts/pyplayready/pyplayready/crypto/__init__.py b/scripts/pyplayready/pyplayready/crypto/__init__.py new file mode 100644 index 0000000..4e339eb --- /dev/null +++ b/scripts/pyplayready/pyplayready/crypto/__init__.py @@ -0,0 +1,96 @@ +from typing import Union, Tuple + +from Crypto.Hash import SHA256 +from Crypto.Hash.SHA256 import SHA256Hash +from Crypto.PublicKey.ECC import EccKey +from Crypto.Signature import DSS +from ecpy.curves import Point, Curve + +from pyplayready.crypto.elgamal import ElGamal +from pyplayready.crypto.ecc_key import ECCKey + + +class Crypto: + def __init__(self, curve: str = "secp256r1"): + self.curve = Curve.get_curve(curve) + self.elgamal = ElGamal(self.curve) + + def ecc256_encrypt(self, public_key: Union[ECCKey, Point], plaintext: Union[Point, bytes]) -> bytes: + if isinstance(public_key, ECCKey): + public_key = public_key.get_point(self.curve) + if not isinstance(public_key, Point): + raise ValueError(f"Expecting ECCKey or Point input, got {public_key!r}") + + if isinstance(plaintext, bytes): + plaintext = Point( + x=int.from_bytes(plaintext[:32], 'big'), + y=int.from_bytes(plaintext[32:64], 'big'), + curve=self.curve + ) + if not isinstance(plaintext, Point): + raise ValueError(f"Expecting Point or Bytes input, got {plaintext!r}") + + point1, point2 = self.elgamal.encrypt( + message_point=plaintext, + public_key=public_key + ) + return b''.join([ + self.elgamal.to_bytes(point1.x), + self.elgamal.to_bytes(point1.y), + self.elgamal.to_bytes(point2.x), + self.elgamal.to_bytes(point2.y) + ]) + + def ecc256_decrypt(self, private_key: ECCKey, ciphertext: Union[Tuple[Point, Point], bytes]) -> bytes: + if isinstance(ciphertext, bytes): + ciphertext = ( + Point( + x=int.from_bytes(ciphertext[:32], 'big'), + y=int.from_bytes(ciphertext[32:64], 'big'), + curve=self.curve + ), + Point( + x=int.from_bytes(ciphertext[64:96], 'big'), + y=int.from_bytes(ciphertext[96:128], 'big'), + curve=self.curve + ) + ) + if not isinstance(ciphertext, Tuple): + raise ValueError(f"Expecting Tuple[Point, Point] or Bytes input, got {ciphertext!r}") + + decrypted = self.elgamal.decrypt(ciphertext, int(private_key.key.d)) + return self.elgamal.to_bytes(decrypted.x) + + @staticmethod + def ecc256_sign(private_key: Union[ECCKey, EccKey], data: Union[SHA256Hash, bytes]) -> bytes: + if isinstance(private_key, ECCKey): + private_key = private_key.key + if not isinstance(private_key, EccKey): + raise ValueError(f"Expecting ECCKey or EccKey input, got {private_key!r}") + + if isinstance(data, bytes): + data = SHA256.new(data) + if not isinstance(data, SHA256Hash): + raise ValueError(f"Expecting SHA256Hash or Bytes input, got {data!r}") + + signer = DSS.new(private_key, 'fips-186-3') + return signer.sign(data) + + @staticmethod + def ecc256_verify(public_key: Union[ECCKey, EccKey], data: Union[SHA256Hash, bytes], signature: bytes) -> bool: + if isinstance(public_key, ECCKey): + public_key = public_key.key + if not isinstance(public_key, EccKey): + raise ValueError(f"Expecting ECCKey or EccKey input, got {public_key!r}") + + if isinstance(data, bytes): + data = SHA256.new(data) + if not isinstance(data, SHA256Hash): + raise ValueError(f"Expecting SHA256Hash or Bytes input, got {data!r}") + + verifier = DSS.new(public_key, 'fips-186-3') + try: + verifier.verify(data, signature) + return True + except ValueError: + return False diff --git a/scripts/pyplayready/pyplayready/crypto/ecc_key.py b/scripts/pyplayready/pyplayready/crypto/ecc_key.py new file mode 100644 index 0000000..2baab46 --- /dev/null +++ b/scripts/pyplayready/pyplayready/crypto/ecc_key.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +import base64 +from pathlib import Path +from typing import Union + +from Crypto.Hash import SHA256 +from Crypto.PublicKey import ECC +from Crypto.PublicKey.ECC import EccKey +from ecpy.curves import Curve, Point + + +class ECCKey: + """Represents a PlayReady ECC key pair""" + + def __init__(self, key: EccKey): + self.key = key + + @classmethod + def generate(cls): + """Generate a new ECC key pair""" + return cls(key=ECC.generate(curve='P-256')) + + @classmethod + def construct(cls, private_key: Union[bytes, int]): + """Construct an ECC key pair from private/public bytes/ints""" + if isinstance(private_key, bytes): + private_key = int.from_bytes(private_key, 'big') + if not isinstance(private_key, int): + raise ValueError(f"Expecting Bytes or Int input, got {private_key!r}") + + # The public is always derived from the private key; loading the other stuff won't work + key = ECC.construct( + curve='P-256', + d=private_key, + ) + + return cls(key=key) + + @classmethod + def loads(cls, data: Union[str, bytes]) -> ECCKey: + if isinstance(data, str): + data = base64.b64decode(data) + if not isinstance(data, bytes): + raise ValueError(f"Expecting Bytes or Base64 input, got {data!r}") + + if len(data) not in [96, 32]: + raise ValueError(f"Invalid data length. Expecting 96 or 32 bytes, got {len(data)}") + + return cls.construct(private_key=data[:32]) + + @classmethod + def load(cls, path: Union[Path, str]) -> ECCKey: + if not isinstance(path, (Path, str)): + raise ValueError(f"Expecting Path object or path string, got {path!r}") + with Path(path).open(mode="rb") as f: + return cls.loads(f.read()) + + def dumps(self, private_only=False): + if private_only: + return self.private_bytes() + return self.private_bytes() + self.public_bytes() + + def dump(self, path: Union[Path, str], private_only=False) -> None: + if not isinstance(path, (Path, str)): + raise ValueError(f"Expecting Path object or path string, got {path!r}") + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(self.dumps(private_only)) + + @staticmethod + def _to_bytes(n: int) -> bytes: + byte_len = (n.bit_length() + 7) // 8 + if byte_len % 2 != 0: + byte_len += 1 + return n.to_bytes(byte_len, 'big') + + def get_point(self, curve: Curve) -> Point: + return Point(self.key.pointQ.x, self.key.pointQ.y, curve) + + def private_bytes(self) -> bytes: + return self._to_bytes(int(self.key.d)) + + def private_sha256_digest(self) -> bytes: + hash_object = SHA256.new() + hash_object.update(self.private_bytes()) + return hash_object.digest() + + def public_bytes(self) -> bytes: + return self._to_bytes(int(self.key.pointQ.x)) + self._to_bytes(int(self.key.pointQ.y)) + + def public_sha256_digest(self) -> bytes: + hash_object = SHA256.new() + hash_object.update(self.public_bytes()) + return hash_object.digest() diff --git a/scripts/pyplayready/pyplayready/crypto/elgamal.py b/scripts/pyplayready/pyplayready/crypto/elgamal.py new file mode 100644 index 0000000..ed6c3db --- /dev/null +++ b/scripts/pyplayready/pyplayready/crypto/elgamal.py @@ -0,0 +1,42 @@ +from typing import Tuple + +from ecpy.curves import Curve, Point +import secrets + + +class ElGamal: + """ElGamal ECC utility using ecpy""" + + def __init__(self, curve: Curve): + """Initialize the utility with a given curve type ('secp256r1' for PlayReady)""" + self.curve = curve + + @staticmethod + def to_bytes(n: int) -> bytes: + byte_len = (n.bit_length() + 7) // 8 + if byte_len % 2 != 0: + byte_len += 1 + return n.to_bytes(byte_len, 'big') + + def encrypt(self, message_point: Point, public_key: Point) -> Tuple[Point, Point]: + """ + Encrypt a single point with a given public key + + Returns an encrypted point pair + """ + ephemeral_key = secrets.randbelow(self.curve.order) + point1 = ephemeral_key * self.curve.generator + point2 = message_point + (ephemeral_key * public_key) + return point1, point2 + + @staticmethod + def decrypt(encrypted: Tuple[Point, Point], private_key: int) -> Point: + """ + Decrypt and encrypted point pair with a given private key + + Returns a single decrypted point + """ + point1, point2 = encrypted + shared_secret = private_key * point1 + decrypted_message = point2 - shared_secret + return decrypted_message diff --git a/scripts/pyplayready/pyplayready/device/__init__.py b/scripts/pyplayready/pyplayready/device/__init__.py new file mode 100644 index 0000000..c4aaaf2 --- /dev/null +++ b/scripts/pyplayready/pyplayready/device/__init__.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +import base64 +from enum import IntEnum +from pathlib import Path +from typing import Union, Any, Optional + +from pyplayready.device.structs import DeviceStructs +from pyplayready.exceptions import OutdatedDevice +from pyplayready.system.bcert import CertificateChain +from pyplayready.crypto.ecc_key import ECCKey + + +class Device: + """Represents a PlayReady Device (.prd)""" + CURRENT_VERSION = 3 + + class SecurityLevel(IntEnum): + SL150 = 150 + SL2000 = 2000 + SL3000 = 3000 + + def __init__( + self, + *_: Any, + group_key: Optional[str, bytes, None], + encryption_key: Union[str, bytes], + signing_key: Union[str, bytes], + group_certificate: Union[str, bytes], + **__: Any + ): + if isinstance(group_key, str): + group_key = base64.b64decode(group_key) + + if isinstance(encryption_key, str): + encryption_key = base64.b64decode(encryption_key) + if not isinstance(encryption_key, bytes): + raise ValueError(f"Expecting Bytes or Base64 input, got {encryption_key!r}") + + if isinstance(signing_key, str): + signing_key = base64.b64decode(signing_key) + if not isinstance(signing_key, bytes): + raise ValueError(f"Expecting Bytes or Base64 input, got {signing_key!r}") + + if isinstance(group_certificate, str): + group_certificate = base64.b64decode(group_certificate) + if not isinstance(group_certificate, bytes): + raise ValueError(f"Expecting Bytes or Base64 input, got {group_certificate!r}") + + self.group_key = None if group_key is None else ECCKey.loads(group_key) + self.encryption_key = ECCKey.loads(encryption_key) + self.signing_key = ECCKey.loads(signing_key) + self.group_certificate = CertificateChain.loads(group_certificate) + self.security_level = self.group_certificate.get_security_level() + + @classmethod + def loads(cls, data: Union[str, bytes]) -> Device: + if isinstance(data, str): + data = base64.b64decode(data) + if not isinstance(data, bytes): + raise ValueError(f"Expecting Bytes or Base64 input, got {data!r}") + + parsed = DeviceStructs.prd.parse(data) + return cls(**{ + **parsed, + 'group_key': parsed.get('group_key', None) + }) + + @classmethod + def load(cls, path: Union[Path, str]) -> Device: + if not isinstance(path, (Path, str)): + raise ValueError(f"Expecting Path object or path string, got {path!r}") + with Path(path).open(mode="rb") as f: + return cls.loads(f.read()) + + def dumps(self) -> bytes: + if not self.group_key: + raise OutdatedDevice("Cannot dump a v2 device, re-create it or use a Device with a version of 3 or higher") + + return DeviceStructs.prd.build(dict( + version=self.CURRENT_VERSION, + group_key=self.group_key.dumps(), + encryption_key=self.encryption_key.dumps(), + signing_key=self.signing_key.dumps(), + group_certificate_length=len(self.group_certificate.dumps()), + group_certificate=self.group_certificate.dumps(), + )) + + def dump(self, path: Union[Path, str]) -> None: + if not isinstance(path, (Path, str)): + raise ValueError(f"Expecting Path object or path string, got {path!r}") + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(self.dumps()) + + def get_name(self) -> str: + name = f"{self.group_certificate.get_name()}_sl{self.group_certificate.get_security_level()}" + return ''.join(char for char in name if (char.isalnum() or char in '_- ')).strip().lower().replace(" ", "_") diff --git a/scripts/pyplayready/pyplayready/device/structs.py b/scripts/pyplayready/pyplayready/device/structs.py new file mode 100644 index 0000000..7a7bda2 --- /dev/null +++ b/scripts/pyplayready/pyplayready/device/structs.py @@ -0,0 +1,41 @@ +from construct import Struct, Const, Int8ub, Bytes, this, Int32ub, Switch, Embedded + + +class DeviceStructs: + magic = Const(b"PRD") + + # was never in production + v1 = Struct( + "group_key_length" / Int32ub, + "group_key" / Bytes(this.group_key_length), + "group_certificate_length" / Int32ub, + "group_certificate" / Bytes(this.group_certificate_length) + ) + + v2 = Struct( + "group_certificate_length" / Int32ub, + "group_certificate" / Bytes(this.group_certificate_length), + "encryption_key" / Bytes(96), + "signing_key" / Bytes(96), + ) + + v3 = Struct( + "group_key" / Bytes(96), + "encryption_key" / Bytes(96), + "signing_key" / Bytes(96), + "group_certificate_length" / Int32ub, + "group_certificate" / Bytes(this.group_certificate_length), + ) + + prd = Struct( + "signature" / magic, + "version" / Int8ub, + Embedded(Switch( + lambda ctx: ctx.version, + { + 1: v1, + 2: v2, + 3: v3 + } + )) + ) diff --git a/scripts/pyplayready/pyplayready/exceptions.py b/scripts/pyplayready/pyplayready/exceptions.py new file mode 100644 index 0000000..a2ca2c3 --- /dev/null +++ b/scripts/pyplayready/pyplayready/exceptions.py @@ -0,0 +1,38 @@ +class PyPlayreadyException(Exception): + """Exceptions used by pyplayready.""" + + +class TooManySessions(PyPlayreadyException): + """Too many Sessions are open.""" + + +class InvalidSession(PyPlayreadyException): + """No Session is open with the specified identifier.""" + + +class InvalidPssh(PyPlayreadyException): + """The Playready PSSH is invalid or empty.""" + + +class InvalidInitData(PyPlayreadyException): + """The Playready Cenc Header Data is invalid or empty.""" + + +class DeviceMismatch(PyPlayreadyException): + """The Remote CDMs Device information and the APIs Device information did not match.""" + + +class InvalidLicense(PyPlayreadyException): + """Unable to parse XMR License.""" + + +class InvalidCertificate(PyPlayreadyException): + """The BCert is not correctly formatted.""" + + +class InvalidCertificateChain(PyPlayreadyException): + """The BCertChain is not correctly formatted.""" + + +class OutdatedDevice(PyPlayreadyException): + """The PlayReady Device is outdated and does not support a specific operation.""" diff --git a/scripts/pyplayready/pyplayready/license/key.py b/scripts/pyplayready/pyplayready/license/key.py new file mode 100644 index 0000000..54ecca0 --- /dev/null +++ b/scripts/pyplayready/pyplayready/license/key.py @@ -0,0 +1,68 @@ +import base64 +from enum import Enum +from uuid import UUID +from typing import Union + + +class Key: + class KeyType(Enum): + INVALID = 0x0000 + AES_128_CTR = 0x0001 + RC4_CIPHER = 0x0002 + AES_128_ECB = 0x0003 + COCKTAIL = 0x0004 + AES_128_CBC = 0x0005 + KEYEXCHANGE = 0x0006 + UNKNOWN = 0xffff + + @classmethod + def _missing_(cls, value): + return cls.UNKNOWN + + class CipherType(Enum): + INVALID = 0x0000 + RSA_1024 = 0x0001 + CHAINED_LICENSE = 0x0002 + ECC_256 = 0x0003 + ECC_256_WITH_KZ = 0x0004 + TEE_TRANSIENT = 0x0005 + ECC_256_VIA_SYMMETRIC = 0x0006 + UNKNOWN = 0xffff + + @classmethod + def _missing_(cls, value): + return cls.UNKNOWN + + def __init__( + self, + key_id: UUID, + key_type: int, + cipher_type: int, + key_length: int, + key: bytes + ): + self.key_id = key_id + self.key_type = self.KeyType(key_type) + self.cipher_type = self.CipherType(cipher_type) + self.key_length = key_length + self.key = key + + @staticmethod + def kid_to_uuid(kid: Union[str, bytes]) -> UUID: + """ + Convert a Key ID from a string or bytes to a UUID object. + At first, this may seem very simple, but some types of Key IDs + may not be 16 bytes and some may be decimal vs. hex. + """ + if isinstance(kid, str): + kid = base64.b64decode(kid) + if not kid: + kid = b"\x00" * 16 + + if kid.decode(errors="replace").isdigit(): + return UUID(int=int(kid.decode())) + + if len(kid) < 16: + kid += b"\x00" * (16 - len(kid)) + + return UUID(bytes=kid) diff --git a/scripts/pyplayready/pyplayready/license/xml_key.py b/scripts/pyplayready/pyplayready/license/xml_key.py new file mode 100644 index 0000000..cc501fd --- /dev/null +++ b/scripts/pyplayready/pyplayready/license/xml_key.py @@ -0,0 +1,22 @@ +from ecpy.curves import Point, Curve + +from pyplayready.crypto.ecc_key import ECCKey +from pyplayready.crypto.elgamal import ElGamal + + +class XmlKey: + """Represents a PlayReady XMLKey""" + + def __init__(self): + self.curve = Curve.get_curve("secp256r1") + + self._shared_point = ECCKey.generate() + self.shared_key_x = self._shared_point.key.pointQ.x + self.shared_key_y = self._shared_point.key.pointQ.y + + self._shared_key_x_bytes = ElGamal.to_bytes(int(self.shared_key_x)) + self.aes_iv = self._shared_key_x_bytes[:16] + self.aes_key = self._shared_key_x_bytes[16:] + + def get_point(self) -> Point: + return Point(self.shared_key_x, self.shared_key_y, self.curve) diff --git a/scripts/pyplayready/pyplayready/license/xmrlicense.py b/scripts/pyplayready/pyplayready/license/xmrlicense.py new file mode 100644 index 0000000..1e0421d --- /dev/null +++ b/scripts/pyplayready/pyplayready/license/xmrlicense.py @@ -0,0 +1,254 @@ +from __future__ import annotations + +import base64 +from typing import Union + +from Crypto.Cipher import AES +from Crypto.Hash import CMAC +from construct import Const, GreedyRange, Struct, Int32ub, Bytes, Int16ub, this, Switch, LazyBound, Array, Container + + +class _XMRLicenseStructs: + PlayEnablerType = Struct( + "player_enabler_type" / Bytes(16) + ) + + DomainRestrictionObject = Struct( + "account_id" / Bytes(16), + "revision" / Int32ub + ) + + IssueDateObject = Struct( + "issue_date" / Int32ub + ) + + RevInfoVersionObject = Struct( + "sequence" / Int32ub + ) + + SecurityLevelObject = Struct( + "minimum_security_level" / Int16ub + ) + + EmbeddedLicenseSettingsObject = Struct( + "indicator" / Int16ub + ) + + ECCKeyObject = Struct( + "curve_type" / Int16ub, + "key_length" / Int16ub, + "key" / Bytes(this.key_length) + ) + + SignatureObject = Struct( + "signature_type" / Int16ub, + "signature_data_length" / Int16ub, + "signature_data" / Bytes(this.signature_data_length) + ) + + ContentKeyObject = Struct( + "key_id" / Bytes(16), + "key_type" / Int16ub, + "cipher_type" / Int16ub, + "key_length" / Int16ub, + "encrypted_key" / Bytes(this.key_length) + ) + + RightsSettingsObject = Struct( + "rights" / Int16ub + ) + + OutputProtectionLevelRestrictionObject = Struct( + "minimum_compressed_digital_video_opl" / Int16ub, + "minimum_uncompressed_digital_video_opl" / Int16ub, + "minimum_analog_video_opl" / Int16ub, + "minimum_digital_compressed_audio_opl" / Int16ub, + "minimum_digital_uncompressed_audio_opl" / Int16ub, + ) + + ExpirationRestrictionObject = Struct( + "begin_date" / Int32ub, + "end_date" / Int32ub + ) + + RemovalDateObject = Struct( + "removal_date" / Int32ub + ) + + UplinkKIDObject = Struct( + "uplink_kid" / Bytes(16), + "chained_checksum_type" / Int16ub, + "chained_checksum_length" / Int16ub, + "chained_checksum" / Bytes(this.chained_checksum_length) + ) + + AnalogVideoOutputConfigurationRestriction = Struct( + "video_output_protection_id" / Bytes(16), + "binary_configuration_data" / Bytes(this._.length - 24) + ) + + DigitalVideoOutputRestrictionObject = Struct( + "video_output_protection_id" / Bytes(16), + "binary_configuration_data" / Bytes(this._.length - 24) + ) + + DigitalAudioOutputRestrictionObject = Struct( + "audio_output_protection_id" / Bytes(16), + "binary_configuration_data" / Bytes(this._.length - 24) + ) + + PolicyMetadataObject = Struct( + "metadata_type" / Bytes(16), + "policy_data" / Bytes(this._.length - 24) + ) + + SecureStopRestrictionObject = Struct( + "metering_id" / Bytes(16) + ) + + MeteringRestrictionObject = Struct( + "metering_id" / Bytes(16) + ) + + ExpirationAfterFirstPlayRestrictionObject = Struct( + "seconds" / Int32ub + ) + + GracePeriodObject = Struct( + "grace_period" / Int32ub + ) + + SourceIdObject = Struct( + "source_id" / Int32ub + ) + + AuxiliaryKey = Struct( + "location" / Int32ub, + "key" / Bytes(16) + ) + + AuxiliaryKeysObject = Struct( + "count" / Int16ub, + "auxiliary_keys" / Array(this.count, AuxiliaryKey) + ) + + UplinkKeyObject3 = Struct( + "uplink_key_id" / Bytes(16), + "chained_length" / Int16ub, + "checksum" / Bytes(this.chained_length), + "count" / Int16ub, + "entries" / Array(this.count, Int32ub) + ) + + CopyEnablerObject = Struct( + "copy_enabler_type" / Bytes(16) + ) + + CopyCountRestrictionObject = Struct( + "count" / Int32ub + ) + + MoveObject = Struct( + "minimum_move_protection_level" / Int32ub + ) + + XmrObject = Struct( + "flags" / Int16ub, + "type" / Int16ub, + "length" / Int32ub, + "data" / Switch( + lambda ctx: ctx.type, + { + 0x0005: OutputProtectionLevelRestrictionObject, + 0x0008: AnalogVideoOutputConfigurationRestriction, + 0x000a: ContentKeyObject, + 0x000b: SignatureObject, + 0x000d: RightsSettingsObject, + 0x0012: ExpirationRestrictionObject, + 0x0013: IssueDateObject, + 0x0016: MeteringRestrictionObject, + 0x001a: GracePeriodObject, + 0x0022: SourceIdObject, + 0x002a: ECCKeyObject, + 0x002c: PolicyMetadataObject, + 0x0029: DomainRestrictionObject, + 0x0030: ExpirationAfterFirstPlayRestrictionObject, + 0x0031: DigitalAudioOutputRestrictionObject, + 0x0032: RevInfoVersionObject, + 0x0033: EmbeddedLicenseSettingsObject, + 0x0034: SecurityLevelObject, + 0x0037: MoveObject, + 0x0039: PlayEnablerType, + 0x003a: CopyEnablerObject, + 0x003b: UplinkKIDObject, + 0x003d: CopyCountRestrictionObject, + 0x0050: RemovalDateObject, + 0x0051: AuxiliaryKeysObject, + 0x0052: UplinkKeyObject3, + 0x005a: SecureStopRestrictionObject, + 0x0059: DigitalVideoOutputRestrictionObject + }, + default=LazyBound(lambda ctx: _XMRLicenseStructs.XmrObject) + ) + ) + + XmrLicense = Struct( + "signature" / Const(b"XMR\x00"), + "xmr_version" / Int32ub, + "rights_id" / Bytes(16), + "containers" / GreedyRange(XmrObject) + ) + + +class XMRLicense(_XMRLicenseStructs): + """Represents an XMRLicense""" + + def __init__( + self, + parsed_license: Container, + license_obj: _XMRLicenseStructs.XmrLicense = _XMRLicenseStructs.XmrLicense + ): + self.parsed = parsed_license + self._license_obj = license_obj + + @classmethod + def loads(cls, data: Union[str, bytes]) -> XMRLicense: + if isinstance(data, str): + data = base64.b64decode(data) + if not isinstance(data, bytes): + raise ValueError(f"Expecting Bytes or Base64 input, got {data!r}") + + licence = _XMRLicenseStructs.XmrLicense + return cls( + parsed_license=licence.parse(data), + license_obj=licence + ) + + def dumps(self) -> bytes: + return self._license_obj.build(self.parsed) + + def struct(self) -> _XMRLicenseStructs.XmrLicense: + return self._license_obj + + def _locate(self, container: Container): + if container.flags == 2 or container.flags == 3: + return self._locate(container.data) + else: + return container + + def get_object(self, type_: int): + for obj in self.parsed.containers: + container = self._locate(obj) + if container.type == type_: + yield container.data + + def get_content_keys(self): + yield from self.get_object(10) + + def check_signature(self, integrity_key: bytes) -> bool: + cmac = CMAC.new(integrity_key, ciphermod=AES) + + signature_data = next(self.get_object(11)) + cmac.update(self.dumps()[:-(signature_data.signature_data_length + 12)]) + + return signature_data.signature_data == cmac.digest() diff --git a/scripts/pyplayready/pyplayready/main.py b/scripts/pyplayready/pyplayready/main.py new file mode 100644 index 0000000..e18a927 --- /dev/null +++ b/scripts/pyplayready/pyplayready/main.py @@ -0,0 +1,320 @@ +import logging +from datetime import datetime +from pathlib import Path +from typing import Optional + +import click +import requests +from Crypto.Random import get_random_bytes + +from pyplayready import __version__, InvalidCertificateChain +from pyplayready.system.bcert import CertificateChain, Certificate +from pyplayready.cdm import Cdm +from pyplayready.device import Device +from pyplayready.crypto.ecc_key import ECCKey +from pyplayready.exceptions import OutdatedDevice +from pyplayready.system.pssh import PSSH + + +@click.group(invoke_without_command=True) +@click.option("-v", "--version", is_flag=True, default=False, help="Print version information.") +@click.option("-d", "--debug", is_flag=True, default=False, help="Enable DEBUG level logs.") +def main(version: bool, debug: bool) -> None: + """Python PlayReady CDM implementation""" + logging.basicConfig(level=logging.DEBUG if debug else logging.INFO) + log = logging.getLogger() + + current_year = datetime.now().year + copyright_years = f"2024-{current_year}" + + log.info("pyplayready version %s Copyright (c) %s DevLARLEY, Erevoc, DevataDev", __version__, copyright_years) + log.info("https://github.com/ready-dl/pyplayready") + log.info("Run 'pyplayready --help' for help") + if version: + return + + +@main.command(name="license") +@click.argument("device_path", type=Path) +@click.argument("pssh", type=PSSH) +@click.argument("server", type=str) +def license_(device_path: Path, pssh: PSSH, server: str) -> None: + """ + Make a License Request to a server using a given PSSH + Will return a list of all keys within the returned license + + Only works for standard license servers that don't use any license wrapping + """ + log = logging.getLogger("license") + + device = Device.load(device_path) + log.info(f"Loaded Device: {device.get_name()}") + + cdm = Cdm.from_device(device) + log.info("Loaded CDM") + + session_id = cdm.open() + log.info("Opened Session") + + challenge = cdm.get_license_challenge(session_id, pssh.get_wrm_headers()[0]) + log.info("Created License Request (Challenge)") + log.debug(challenge) + + license_res = requests.post( + url=server, + headers={ + 'Content-Type': 'text/xml; charset=UTF-8', + }, + data=challenge + ) + + if license_res.status_code != 200: + log.error(f"Failed to send challenge [{license_res.status_code}]: {license_res.text}") + return + + licence = license_res.text + log.info("Got License Message") + log.debug(licence) + + cdm.parse_license(session_id, licence) + log.info("License Parsed Successfully") + + for key in cdm.get_keys(session_id): + log.info(f"{key.key_id.hex}:{key.key.hex()}") + + cdm.close(session_id) + log.info("Clossed Session") + + +@main.command() +@click.argument("device", type=Path) +@click.option("-c", "--ckt", type=click.Choice(["aesctr", "aescbc"], case_sensitive=False), default="aesctr", help="Content Key Encryption Type") +@click.option("-sl", "--security_level", type=click.Choice(["150", "2000", "3000"], case_sensitive=False), default="2000", help="Minimum Security Level") +@click.pass_context +def test(ctx: click.Context, device: Path, ckt: str, security_level: str) -> None: + """ + Test the CDM code by getting Content Keys for the Tears Of Steel demo on the Playready Test Server. + https://testweb.playready.microsoft.com/Content/Content2X + + DASH Manifest URL: https://test.playready.microsoft.com/media/profficialsite/tearsofsteel_4k.ism/manifest.mpd + + MSS Manifest URL: https://test.playready.microsoft.com/media/profficialsite/tearsofsteel_4k.ism.smoothstreaming/manifest + + The device argument is a Path to a Playready Device (.prd) file which contains the device's group key and + group certificate. + """ + pssh = PSSH( + "AAADfHBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAA1xcAwAAAQABAFIDPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AH" + "QAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABh" + "AHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUg" + "BPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQA" + "UgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgA0AFIAcABsAGIAKwBUAGIATgBFAFMAOAB0AE" + "cAawBOAEYAVwBUAEUASABBAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AEsATABqADMAUQB6AFEAUAAvAE4AQQA9ADwALwBD" + "AEgARQBDAEsAUwBVAE0APgA8AEwAQQBfAFUAUgBMAD4AaAB0AHQAcABzADoALwAvAHAAcgBvAGYAZgBpAGMAaQBhAGwAcwBpAHQAZQAuAGsAZQ" + "B5AGQAZQBsAGkAdgBlAHIAeQAuAG0AZQBkAGkAYQBzAGUAcgB2AGkAYwBlAHMALgB3AGkAbgBkAG8AdwBzAC4AbgBlAHQALwBQAGwAYQB5AFIA" + "ZQBhAGQAeQAvADwALwBMAEEAXwBVAFIATAA+ADwAQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwASQBJAFMAXwBEAFIATQBfAF" + "YARQBSAFMASQBPAE4APgA4AC4AMQAuADIAMwAwADQALgAzADEAPAAvAEkASQBTAF8ARABSAE0AXwBWAEUAUgBTAEkATwBOAD4APAAvAEMAVQBT" + "AFQATwBNAEEAVABUAFIASQBCAFUAVABFAFMAPgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA==" + ) + + license_server = f"https://test.playready.microsoft.com/service/rightsmanager.asmx?cfg=(persist:false,sl:{security_level},ckt:{ckt})" + + ctx.invoke( + license_, + device_path=device, + pssh=pssh, + server=license_server + ) + + +@main.command() +@click.option("-k", "--group_key", type=Path, required=True, help="Device ECC private group key") +@click.option("-c", "--group_certificate", type=Path, required=True, help="Device group certificate chain") +@click.option("-o", "--output", type=Path, default=None, help="Output Path or Directory") +@click.pass_context +def create_device( + ctx: click.Context, + group_key: Path, + group_certificate: Path, + output: Optional[Path] = None +) -> None: + """Create a Playready Device (.prd) file from an ECC private group key and group certificate chain""" + if not group_key.is_file(): + raise click.UsageError("group_key: Not a path to a file, or it doesn't exist.", ctx) + if not group_certificate.is_file(): + raise click.UsageError("group_certificate: Not a path to a file, or it doesn't exist.", ctx) + + log = logging.getLogger("create-device") + + encryption_key = ECCKey.generate() + signing_key = ECCKey.generate() + + group_key = ECCKey.load(group_key) + certificate_chain = CertificateChain.load(group_certificate) + + if certificate_chain.get(0).get_issuer_key() != group_key.public_bytes(): + raise InvalidCertificateChain("Group key does not match this certificate") + + new_certificate = Certificate.new_leaf_cert( + cert_id=get_random_bytes(16), + security_level=certificate_chain.get_security_level(), + client_id=get_random_bytes(16), + signing_key=signing_key, + encryption_key=encryption_key, + group_key=group_key, + parent=certificate_chain + ) + certificate_chain.prepend(new_certificate) + + certificate_chain.verify() + + device = Device( + group_key=group_key.dumps(), + encryption_key=encryption_key.dumps(), + signing_key=signing_key.dumps(), + group_certificate=certificate_chain.dumps(), + ) + + if output and output.suffix: + if output.suffix.lower() != ".prd": + log.warning(f"Saving PRD with the file extension '{output.suffix}' but '.prd' is recommended.") + out_path = output + else: + out_dir = output or Path.cwd() + out_path = out_dir / f"{device.get_name()}.prd" + + if out_path.exists(): + log.error(f"A file already exists at the path '{out_path}', cannot overwrite.") + return + + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_bytes(device.dumps()) + + log.info("Created Playready Device (.prd) file, %s", out_path.name) + log.info(" + Security Level: %s", device.security_level) + log.info(" + Group Key: %s bytes", len(device.group_key.dumps())) + log.info(" + Encryption Key: %s bytes", len(device.encryption_key.dumps())) + log.info(" + Signing Key: %s bytes", len(device.signing_key.dumps())) + log.info(" + Group Certificate: %s bytes", len(device.group_certificate.dumps())) + log.info(" + Saved to: %s", out_path.absolute()) + + +@main.command() +@click.argument("prd_path", type=Path) +@click.option("-o", "--output", type=Path, default=None, help="Output Path or Directory") +@click.pass_context +def reprovision_device(ctx: click.Context, prd_path: Path, output: Optional[Path] = None) -> None: + """ + Reprovision a Playready Device (.prd) by creating a new leaf certificate and new encryption/signing keys. + Will override the device if an output path or directory is not specified + + Only works on PRD Devices of v3 or higher + """ + if not prd_path.is_file(): + raise click.UsageError("prd_path: Not a path to a file, or it doesn't exist.", ctx) + + log = logging.getLogger("reprovision-device") + log.info("Reprovisioning Playready Device (.prd) file, %s", prd_path.name) + + device = Device.load(prd_path) + + if device.group_key is None: + raise OutdatedDevice("Device does not support reprovisioning, re-create it or use a Device with a version of 3 or higher") + + device.group_certificate.remove(0) + + encryption_key = ECCKey.generate() + signing_key = ECCKey.generate() + + device.encryption_key = encryption_key + device.signing_key = signing_key + + new_certificate = Certificate.new_leaf_cert( + cert_id=get_random_bytes(16), + security_level=device.group_certificate.get_security_level(), + client_id=get_random_bytes(16), + signing_key=signing_key, + encryption_key=encryption_key, + group_key=device.group_key, + parent=device.group_certificate + ) + device.group_certificate.prepend(new_certificate) + + if output and output.suffix: + if output.suffix.lower() != ".prd": + log.warning(f"Saving PRD with the file extension '{output.suffix}' but '.prd' is recommended.") + out_path = output + else: + out_path = prd_path + + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_bytes(device.dumps()) + + log.info("Reprovisioned Playready Device (.prd) file, %s", out_path.name) + + +@main.command() +@click.argument("prd_path", type=Path) +@click.option("-o", "--out_dir", type=Path, default=None, help="Output Directory") +@click.pass_context +def export_device(ctx: click.Context, prd_path: Path, out_dir: Optional[Path] = None) -> None: + """ + Export a Playready Device (.prd) file to a Group Key and Group Certificate + If an output directory is not specified, it will be stored in the current working directory + """ + if not prd_path.is_file(): + raise click.UsageError("prd_path: Not a path to a file, or it doesn't exist.", ctx) + + log = logging.getLogger("export-device") + log.info("Exporting Playready Device (.prd) file, %s", prd_path.stem) + + if not out_dir: + out_dir = Path.cwd() + + out_path = out_dir / prd_path.stem + if out_path.exists(): + if any(out_path.iterdir()): + log.error("Output directory is not empty, cannot overwrite.") + return + else: + log.warning("Output directory already exists, but is empty.") + else: + out_path.mkdir(parents=True) + + device = Device.load(prd_path) + + log.info(f"SL{device.security_level} {device.get_name()}") + log.info(f"Saving to: {out_path}") + + if device.group_key: + group_key_path = out_path / "zgpriv.dat" + group_key_path.write_bytes(device.group_key.dumps(private_only=True)) + log.info("Exported Group Key as zgpriv.dat") + else: + log.warning("Cannot export zgpriv.dat, as v2 devices do not save the group key") + + # remove leaf cert to unprovision it + device.group_certificate.remove(0) + + client_id_path = out_path / "bgroupcert.dat" + client_id_path.write_bytes(device.group_certificate.dumps()) + log.info("Exported Group Certificate to bgroupcert.dat") + + +@main.command("serve", short_help="Serve your local CDM and Playready Devices Remotely.") +@click.argument("config_path", type=Path) +@click.option("-h", "--host", type=str, default="127.0.0.1", help="Host to serve from.") +@click.option("-p", "--port", type=int, default=7723, help="Port to serve from.") +def serve_(config_path: Path, host: str, port: int) -> None: + """ + Serve your local CDM and Playready Devices Remotely. + + [CONFIG] is a path to a serve config file. + See `serve.example.yml` for an example config file. + + Host as 127.0.0.1 may block remote access even if port-forwarded. + Instead, use 0.0.0.0 and ensure the TCP port you choose is forwarded. + """ + from pyplayready.remote import serve + import yaml + + config = yaml.safe_load(config_path.read_text(encoding="utf8")) + serve.run(config, host, port) diff --git a/scripts/pyplayready/pyplayready/remote/remotecdm.py b/scripts/pyplayready/pyplayready/remote/remotecdm.py new file mode 100644 index 0000000..9afd6b3 --- /dev/null +++ b/scripts/pyplayready/pyplayready/remote/remotecdm.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +import re + +import requests + +from pyplayready.cdm import Cdm +from pyplayready.device import Device +from pyplayready.license.key import Key + +from pyplayready.exceptions import (DeviceMismatch, InvalidInitData) + + +class RemoteCdm(Cdm): + """Remote Accessible CDM using pyplayready's serve schema.""" + + def __init__( + self, + security_level: int, + host: str, + secret: str, + device_name: str + ): + """Initialize a Playready Content Decryption Module (CDM).""" + if not security_level: + raise ValueError("Security Level must be provided") + if not isinstance(security_level, int): + raise TypeError(f"Expected security_level to be a {int} not {security_level!r}") + + if not host: + raise ValueError("API Host must be provided") + if not isinstance(host, str): + raise TypeError(f"Expected host to be a {str} not {host!r}") + + if not secret: + raise ValueError("API Secret must be provided") + if not isinstance(secret, str): + raise TypeError(f"Expected secret to be a {str} not {secret!r}") + + if not device_name: + raise ValueError("API Device name must be provided") + if not isinstance(device_name, str): + raise TypeError(f"Expected device_name to be a {str} not {device_name!r}") + + self.security_level = security_level + self.host = host + self.device_name = device_name + + # spoof certificate_chain and ecc_key just so we can construct via super call + super().__init__(security_level, None, None, None) + + self.__session = requests.Session() + self.__session.headers.update({ + "X-Secret-Key": secret + }) + + r = requests.head(self.host) + if r.status_code != 200: + raise ValueError(f"Could not test Remote API version [{r.status_code}]") + server = r.headers.get("Server") + if not server or "pyplayready serve" not in server.lower(): + raise ValueError(f"This Remote CDM API does not seem to be a pyplayready serve API ({server}).") + server_version_re = re.search(r"pyplayready serve v([\d.]+)", server, re.IGNORECASE) + if not server_version_re: + raise ValueError("The pyplayready server API is not stating the version correctly, cannot continue.") + server_version = server_version_re.group(1) + if server_version < "0.3.1": + raise ValueError(f"This pyplayready serve API version ({server_version}) is not supported.") + + @classmethod + def from_device(cls, device: Device) -> RemoteCdm: + raise NotImplementedError("You cannot load a RemoteCdm from a local Device file.") + + def open(self) -> bytes: + r = self.__session.get( + url=f"{self.host}/{self.device_name}/open" + ).json() + + if r['status'] != 200: + raise ValueError(f"Cannot Open CDM Session, {r['message']} [{r['status']}]") + r = r["data"] + + if int(r["device"]["security_level"]) != self.security_level: + raise DeviceMismatch("The Security Level specified does not match the one specified in the API response.") + + return bytes.fromhex(r["session_id"]) + + def close(self, session_id: bytes) -> None: + r = self.__session.get( + url=f"{self.host}/{self.device_name}/close/{session_id.hex()}" + ).json() + if r["status"] != 200: + raise ValueError(f"Cannot Close CDM Session, {r['message']} [{r['status']}]") + + def get_license_challenge( + self, + session_id: bytes, + wrm_header: str, + ) -> str: + if not wrm_header: + raise InvalidInitData("A wrm_header must be provided.") + if not isinstance(wrm_header, str): + raise InvalidInitData(f"Expected wrm_header to be a {str}, not {wrm_header!r}") + + r = self.__session.post( + url=f"{self.host}/{self.device_name}/get_license_challenge", + json={ + "session_id": session_id.hex(), + "init_data": wrm_header, + } + ).json() + if r["status"] != 200: + raise ValueError(f"Cannot get Challenge, {r['message']} [{r['status']}]") + r = r["data"] + + return r["challenge"] + + def parse_license(self, session_id: bytes, license_message: str) -> None: + if not license_message: + raise Exception("Cannot parse an empty license_message") + + if not isinstance(license_message, str): + raise Exception(f"Expected license_message to be a {str}, not {license_message!r}") + + r = self.__session.post( + url=f"{self.host}/{self.device_name}/parse_license", + json={ + "session_id": session_id.hex(), + "license_message": license_message + } + ).json() + if r["status"] != 200: + raise ValueError(f"Cannot parse License, {r['message']} [{r['status']}]") + + def get_keys(self, session_id: bytes) -> list[Key]: + r = self.__session.post( + url=f"{self.host}/{self.device_name}/get_keys", + json={ + "session_id": session_id.hex() + } + ).json() + if r["status"] != 200: + raise ValueError(f"Could not get Keys, {r['message']} [{r['status']}]") + r = r["data"] + + return [ + Key( + key_type=key["type"], + key_id=Key.kid_to_uuid(bytes.fromhex(key["key_id"])), + key=bytes.fromhex(key["key"]), + cipher_type=key["cipher_type"], + key_length=key["key_length"] + ) + for key in r["keys"] + ] + + +__all__ = ("RemoteCdm",) diff --git a/scripts/pyplayready/pyplayready/remote/serve.py b/scripts/pyplayready/pyplayready/remote/serve.py new file mode 100644 index 0000000..64a0b69 --- /dev/null +++ b/scripts/pyplayready/pyplayready/remote/serve.py @@ -0,0 +1,321 @@ +from pathlib import Path +from typing import Any, Optional, Union + +from aiohttp.typedefs import Handler +from aiohttp import web + +from pyplayready import __version__, PSSH +from pyplayready.cdm import Cdm +from pyplayready.device import Device + +from pyplayready.exceptions import (InvalidSession, TooManySessions, InvalidLicense, InvalidPssh) + +routes = web.RouteTableDef() + + +async def _startup(app: web.Application) -> None: + app["cdms"] = {} + app["config"]["devices"] = { + path.stem: path + for x in app["config"]["devices"] + for path in [Path(x)] + } + for device in app["config"]["devices"].values(): + if not device.is_file(): + raise FileNotFoundError(f"Device file does not exist: {device}") + + +async def _cleanup(app: web.Application) -> None: + app["cdms"].clear() + del app["cdms"] + app["config"].clear() + del app["config"] + + +@routes.get("/") +async def ping(_: Any) -> web.Response: + return web.json_response({ + "status": 200, + "message": "Pong!" + }) + + +@routes.get("/{device}/open") +async def open_(request: web.Request) -> web.Response: + secret_key = request.headers["X-Secret-Key"] + device_name = request.match_info["device"] + user = request.app["config"]["users"][secret_key] + + if device_name not in user["devices"] or device_name not in request.app["config"]["devices"]: + # we don't want to be verbose with the error as to not reveal device names + # by trial and error to users that are not authorized to use them + return web.json_response({ + "status": 403, + "message": f"Device '{device_name}' is not found or you are not authorized to use it." + }, status=403) + + cdm: Optional[Cdm] = request.app["cdms"].get((secret_key, device_name)) + if not cdm: + device = Device.load(request.app["config"]["devices"][device_name]) + cdm = request.app["cdms"][(secret_key, device_name)] = Cdm.from_device(device) + + try: + session_id = cdm.open() + except TooManySessions as e: + return web.json_response({ + "status": 400, + "message": str(e) + }, status=400) + + return web.json_response({ + "status": 200, + "message": "Success", + "data": { + "session_id": session_id.hex(), + "device": { + "security_level": cdm.security_level + } + } + }) + + +@routes.get("/{device}/close/{session_id}") +async def close(request: web.Request) -> web.Response: + secret_key = request.headers["X-Secret-Key"] + device_name = request.match_info["device"] + session_id = bytes.fromhex(request.match_info["session_id"]) + + cdm: Optional[Cdm] = request.app["cdms"].get((secret_key, device_name)) + if not cdm: + return web.json_response({ + "status": 400, + "message": f"No Cdm session for {device_name} has been opened yet. No session to close." + }, status=400) + + try: + cdm.close(session_id) + except InvalidSession: + return web.json_response({ + "status": 400, + "message": f"Invalid Session ID '{session_id.hex()}', it may have expired." + }, status=400) + + return web.json_response({ + "status": 200, + "message": f"Successfully closed Session '{session_id.hex()}'." + }) + + +@routes.post("/{device}/get_license_challenge") +async def get_license_challenge(request: web.Request) -> web.Response: + secret_key = request.headers["X-Secret-Key"] + device_name = request.match_info["device"] + + body = await request.json() + for required_field in ("session_id", "init_data"): + if not body.get(required_field): + return web.json_response({ + "status": 400, + "message": f"Missing required field '{required_field}' in JSON body." + }, status=400) + + # get session id + session_id = bytes.fromhex(body["session_id"]) + + # get cdm + cdm: Optional[Cdm] = request.app["cdms"].get((secret_key, device_name)) + if not cdm: + return web.json_response({ + "status": 400, + "message": f"No Cdm session for {device_name} has been opened yet. No session to use." + }, status=400) + + # get init data + init_data = body["init_data"] + + if not init_data.startswith("<WRMHEADER"): + try: + pssh = PSSH(init_data) + wrm_headers = pssh.get_wrm_headers() + if wrm_headers: + init_data = wrm_headers[0] + except InvalidPssh as e: + return web.json_response({ + "status": 500, + "message": f"Unable to parse base64 PSSH, {e}" + }, status=500) + + # get challenge + try: + license_request = cdm.get_license_challenge( + session_id=session_id, + wrm_header=init_data, + ) + except InvalidSession: + return web.json_response({ + "status": 400, + "message": f"Invalid Session ID '{session_id.hex()}', it may have expired." + }, status=400) + except Exception as e: + return web.json_response({ + "status": 500, + "message": f"Error, {e}" + }, status=500) + + return web.json_response({ + "status": 200, + "message": "Success", + "data": { + "challenge": license_request + } + }, status=200) + + +@routes.post("/{device}/parse_license") +async def parse_license(request: web.Request) -> web.Response: + secret_key = request.headers["X-Secret-Key"] + device_name = request.match_info["device"] + + body = await request.json() + for required_field in ("session_id", "license_message"): + if not body.get(required_field): + return web.json_response({ + "status": 400, + "message": f"Missing required field '{required_field}' in JSON body." + }, status=400) + + # get session id + session_id = bytes.fromhex(body["session_id"]) + + # get cdm + cdm: Optional[Cdm] = request.app["cdms"].get((secret_key, device_name)) + if not cdm: + return web.json_response({ + "status": 400, + "message": f"No Cdm session for {device_name} has been opened yet. No session to use." + }, status=400) + + # parse the license message + try: + cdm.parse_license(session_id, body["license_message"]) + except InvalidSession: + return web.json_response({ + "status": 400, + "message": f"Invalid Session ID '{session_id.hex()}', it may have expired." + }, status=400) + except InvalidLicense as e: + return web.json_response({ + "status": 400, + "message": f"Invalid License, {e}" + }, status=400) + except Exception as e: + return web.json_response({ + "status": 500, + "message": f"Error, {e}" + }, status=500) + + return web.json_response({ + "status": 200, + "message": "Successfully parsed and loaded the Keys from the License message." + }) + + +@routes.post("/{device}/get_keys") +async def get_keys(request: web.Request) -> web.Response: + secret_key = request.headers["X-Secret-Key"] + device_name = request.match_info["device"] + + body = await request.json() + for required_field in ("session_id",): + if not body.get(required_field): + return web.json_response({ + "status": 400, + "message": f"Missing required field '{required_field}' in JSON body." + }, status=400) + + # get session id + session_id = bytes.fromhex(body["session_id"]) + + # get cdm + cdm = request.app["cdms"].get((secret_key, device_name)) + if not cdm: + return web.json_response({ + "status": 400, + "message": f"No Cdm session for {device_name} has been opened yet. No session to use." + }, status=400) + + # get keys + try: + keys = cdm.get_keys(session_id) + except InvalidSession: + return web.json_response({ + "status": 400, + "message": f"Invalid Session ID '{session_id.hex()}', it may have expired." + }, status=400) + except Exception as e: + return web.json_response({ + "status": 500, + "message": f"Error, {e}" + }, status=500) + + # get the keys in json form + keys_json = [ + { + "key_id": key.key_id.hex, + "key": key.key.hex(), + "type": key.key_type.value, + "cipher_type": key.cipher_type.value, + "key_length": key.key_length, + } + for key in keys + ] + + return web.json_response({ + "status": 200, + "message": "Success", + "data": { + "keys": keys_json + } + }) + + +@web.middleware +async def authentication(request: web.Request, handler: Handler) -> web.Response: + secret_key = request.headers.get("X-Secret-Key") + + if request.path != "/" and not secret_key: + request.app.logger.debug(f"{request.remote} did not provide authorization.") + response = web.json_response({ + "status": "401", + "message": "Secret Key is Empty." + }, status=401) + elif request.path != "/" and secret_key not in request.app["config"]["users"]: + request.app.logger.debug(f"{request.remote} failed authentication with '{secret_key}'.") + response = web.json_response({ + "status": "401", + "message": "Secret Key is Invalid, the Key is case-sensitive." + }, status=401) + else: + try: + response = await handler(request) # type: ignore[assignment] + except web.HTTPException as e: + request.app.logger.error(f"An unexpected error has occurred, {e}") + response = web.json_response({ + "status": 500, + "message": e.reason + }, status=500) + + response.headers.update({ + "Server": f"https://github.com/ready-dl/pyplayready serve v{__version__}" + }) + + return response + + +def run(config: dict, host: Optional[Union[str, web.HostSequence]] = None, port: Optional[int] = None) -> None: + app = web.Application(middlewares=[authentication]) + app.on_startup.append(_startup) + app.on_cleanup.append(_cleanup) + app.add_routes(routes) + app["config"] = config + web.run_app(app, host=host, port=port) diff --git a/scripts/pyplayready/pyplayready/system/bcert.py b/scripts/pyplayready/pyplayready/system/bcert.py new file mode 100644 index 0000000..f6a1b2c --- /dev/null +++ b/scripts/pyplayready/pyplayready/system/bcert.py @@ -0,0 +1,506 @@ +from __future__ import annotations +import collections.abc + +from Crypto.PublicKey import ECC + +from pyplayready.crypto import Crypto +from pyplayready.exceptions import InvalidCertificateChain, InvalidCertificate + +# monkey patch for construct 2.8.8 compatibility +if not hasattr(collections, 'Sequence'): + collections.Sequence = collections.abc.Sequence + +import base64 +from pathlib import Path +from typing import Union + +from construct import Bytes, Const, Int32ub, GreedyRange, Switch, Container, ListContainer +from construct import Int16ub, Array +from construct import Struct, this + +from pyplayready.crypto.ecc_key import ECCKey + + +class _BCertStructs: + DrmBCertBasicInfo = Struct( + "cert_id" / Bytes(16), + "security_level" / Int32ub, + "flags" / Int32ub, + "cert_type" / Int32ub, + "public_key_digest" / Bytes(32), + "expiration_date" / Int32ub, + "client_id" / Bytes(16) + ) + + # TODO: untested + DrmBCertDomainInfo = Struct( + "service_id" / Bytes(16), + "account_id" / Bytes(16), + "revision_timestamp" / Int32ub, + "domain_url_length" / Int32ub, + "domain_url" / Bytes((this.domain_url_length + 3) & 0xfffffffc) + ) + + # TODO: untested + DrmBCertPCInfo = Struct( + "security_version" / Int32ub + ) + + # TODO: untested + DrmBCertDeviceInfo = Struct( + "max_license" / Int32ub, + "max_header" / Int32ub, + "max_chain_depth" / Int32ub + ) + + DrmBCertFeatureInfo = Struct( + "feature_count" / Int32ub, # max. 32 + "features" / Array(this.feature_count, Int32ub) + ) + + DrmBCertKeyInfo = Struct( + "key_count" / Int32ub, + "cert_keys" / Array(this.key_count, Struct( + "type" / Int16ub, + "length" / Int16ub, + "flags" / Int32ub, + "key" / Bytes(this.length // 8), + "usages_count" / Int32ub, + "usages" / Array(this.usages_count, Int32ub) + )) + ) + + DrmBCertManufacturerInfo = Struct( + "flags" / Int32ub, + "manufacturer_name_length" / Int32ub, + "manufacturer_name" / Bytes((this.manufacturer_name_length + 3) & 0xfffffffc), + "model_name_length" / Int32ub, + "model_name" / Bytes((this.model_name_length + 3) & 0xfffffffc), + "model_number_length" / Int32ub, + "model_number" / Bytes((this.model_number_length + 3) & 0xfffffffc), + ) + + DrmBCertSignatureInfo = Struct( + "signature_type" / Int16ub, + "signature_size" / Int16ub, + "signature" / Bytes(this.signature_size), + "signature_key_size" / Int32ub, + "signature_key" / Bytes(this.signature_key_size // 8) + ) + + # TODO: untested + DrmBCertSilverlightInfo = Struct( + "security_version" / Int32ub, + "platform_identifier" / Int32ub + ) + + # TODO: untested + DrmBCertMeteringInfo = Struct( + "metering_id" / Bytes(16), + "metering_url_length" / Int32ub, + "metering_url" / Bytes((this.metering_url_length + 3) & 0xfffffffc) + ) + + # TODO: untested + DrmBCertExtDataSignKeyInfo = Struct( + "key_type" / Int16ub, + "key_length" / Int16ub, + "flags" / Int32ub, + "key" / Bytes(this.key_length // 8) + ) + + # TODO: untested + BCertExtDataRecord = Struct( + "data_size" / Int32ub, + "data" / Bytes(this.data_size) + ) + + # TODO: untested + DrmBCertExtDataSignature = Struct( + "signature_type" / Int16ub, + "signature_size" / Int16ub, + "signature" / Bytes(this.signature_size) + ) + + # TODO: untested + BCertExtDataContainer = Struct( + "record_count" / Int32ub, # always 1 + "records" / Array(this.record_count, BCertExtDataRecord), + "signature" / DrmBCertExtDataSignature + ) + + # TODO: untested + DrmBCertServerInfo = Struct( + "warning_days" / Int32ub + ) + + # TODO: untested + DrmBcertSecurityVersion = Struct( + "security_version" / Int32ub, + "platform_identifier" / Int32ub + ) + + Attribute = Struct( + "flags" / Int16ub, + "tag" / Int16ub, + "length" / Int32ub, + "attribute" / Switch( + lambda this_: this_.tag, + { + 1: DrmBCertBasicInfo, + 2: DrmBCertDomainInfo, + 3: DrmBCertPCInfo, + 4: DrmBCertDeviceInfo, + 5: DrmBCertFeatureInfo, + 6: DrmBCertKeyInfo, + 7: DrmBCertManufacturerInfo, + 8: DrmBCertSignatureInfo, + 9: DrmBCertSilverlightInfo, + 10: DrmBCertMeteringInfo, + 11: DrmBCertExtDataSignKeyInfo, + 12: BCertExtDataContainer, + 13: DrmBCertExtDataSignature, + 14: Bytes(this.length - 8), + 15: DrmBCertServerInfo, + 16: DrmBcertSecurityVersion, + 17: DrmBcertSecurityVersion + }, + default=Bytes(this.length - 8) + ) + ) + + BCert = Struct( + "signature" / Const(b"CERT"), + "version" / Int32ub, + "total_length" / Int32ub, + "certificate_length" / Int32ub, + "attributes" / GreedyRange(Attribute) + ) + + BCertChain = Struct( + "signature" / Const(b"CHAI"), + "version" / Int32ub, + "total_length" / Int32ub, + "flags" / Int32ub, + "certificate_count" / Int32ub, + "certificates" / GreedyRange(BCert) + ) + + +class Certificate(_BCertStructs): + """Represents a BCert""" + + def __init__( + self, + parsed_bcert: Container, + bcert_obj: _BCertStructs.BCert = _BCertStructs.BCert + ): + self.parsed = parsed_bcert + self._BCERT = bcert_obj + + @classmethod + def new_leaf_cert( + cls, + cert_id: bytes, + security_level: int, + client_id: bytes, + signing_key: ECCKey, + encryption_key: ECCKey, + group_key: ECCKey, + parent: CertificateChain, + expiry: int = 0xFFFFFFFF + ) -> Certificate: + basic_info = Container( + cert_id=cert_id, + security_level=security_level, + flags=0, + cert_type=2, + public_key_digest=signing_key.public_sha256_digest(), + expiration_date=expiry, + client_id=client_id + ) + basic_info_attribute = Container( + flags=1, + tag=1, + length=len(_BCertStructs.DrmBCertBasicInfo.build(basic_info)) + 8, + attribute=basic_info + ) + + device_info = Container( + max_license=10240, + max_header=15360, + max_chain_depth=2 + ) + device_info_attribute = Container( + flags=1, + tag=4, + length=len(_BCertStructs.DrmBCertDeviceInfo.build(device_info)) + 8, + attribute=device_info + ) + + feature = Container( + feature_count=3, + features=ListContainer([ + # 1, # Transmitter + # 2, # Receiver + # 3, # SharedCertificate + 4, # SecureClock + # 5, # AntiRollBackClock + # 6, # ReservedMetering + # 7, # ReservedLicSync + # 8, # ReservedSymOpt + 9, # CRLS (Revocation Lists) + # 10, # ServerBasicEdition + # 11, # ServerStandardEdition + # 12, # ServerPremiumEdition + 13, # PlayReady3Features + # 14, # DeprecatedSecureStop + ]) + ) + feature_attribute = Container( + flags=1, + tag=5, + length=len(_BCertStructs.DrmBCertFeatureInfo.build(feature)) + 8, + attribute=feature + ) + + cert_key_sign = Container( + type=1, + length=512, # bits + flags=0, + key=signing_key.public_bytes(), + usages_count=1, + usages=ListContainer([ + 1 # KEYUSAGE_SIGN + ]) + ) + cert_key_encrypt = Container( + type=1, + length=512, # bits + flags=0, + key=encryption_key.public_bytes(), + usages_count=1, + usages=ListContainer([ + 2 # KEYUSAGE_ENCRYPT_KEY + ]) + ) + key_info = Container( + key_count=2, + cert_keys=ListContainer([ + cert_key_sign, + cert_key_encrypt + ]) + ) + key_info_attribute = Container( + flags=1, + tag=6, + length=len(_BCertStructs.DrmBCertKeyInfo.build(key_info)) + 8, + attribute=key_info + ) + + manufacturer_info = parent.get(0).get_attribute(7) + + new_bcert_container = Container( + signature=b"CERT", + version=1, + total_length=0, # filled at a later time + certificate_length=0, # filled at a later time + attributes=ListContainer([ + basic_info_attribute, + device_info_attribute, + feature_attribute, + key_info_attribute, + manufacturer_info, + ]) + ) + + payload = _BCertStructs.BCert.build(new_bcert_container) + new_bcert_container.certificate_length = len(payload) + new_bcert_container.total_length = len(payload) + 144 # signature length + + sign_payload = _BCertStructs.BCert.build(new_bcert_container) + signature = Crypto.ecc256_sign(group_key, sign_payload) + + signature_info = Container( + signature_type=1, + signature_size=64, + signature=signature, + signature_key_size=512, # bits + signature_key=group_key.public_bytes() + ) + signature_info_attribute = Container( + flags=1, + tag=8, + length=len(_BCertStructs.DrmBCertSignatureInfo.build(signature_info)) + 8, + attribute=signature_info + ) + new_bcert_container.attributes.append(signature_info_attribute) + + return cls(new_bcert_container) + + @classmethod + def loads(cls, data: Union[str, bytes]) -> Certificate: + if isinstance(data, str): + data = base64.b64decode(data) + if not isinstance(data, bytes): + raise ValueError(f"Expecting Bytes or Base64 input, got {data!r}") + + cert = _BCertStructs.BCert + return cls( + parsed_bcert=cert.parse(data), + bcert_obj=cert + ) + + def get_attribute(self, type_: int): + for attribute in self.parsed.attributes: + if attribute.tag == type_: + return attribute + + def get_security_level(self) -> int: + basic_info_attribute = self.get_attribute(1).attribute + if basic_info_attribute: + return basic_info_attribute.security_level + + @staticmethod + def _unpad(name: bytes): + return name.rstrip(b'\x00').decode("utf-8", errors="ignore") + + def get_name(self): + manufacturer_info = self.get_attribute(7).attribute + if manufacturer_info: + return f"{self._unpad(manufacturer_info.manufacturer_name)} {self._unpad(manufacturer_info.model_name)} {self._unpad(manufacturer_info.model_number)}" + + def get_issuer_key(self) -> Union[bytes, None]: + key_info_object = self.get_attribute(6) + if not key_info_object: + return + + key_info_attribute = key_info_object.attribute + return next(map(lambda key: key.key, filter(lambda key: 6 in key.usages, key_info_attribute.cert_keys)), None) + + def dumps(self) -> bytes: + return self._BCERT.build(self.parsed) + + def struct(self) -> _BCertStructs.BCert: + return self._BCERT + + def verify(self, public_key: bytes, index: int): + signature_object = self.get_attribute(8) + if not signature_object: + raise InvalidCertificate(f"No signature object found in certificate {index}") + + signature_attribute = signature_object.attribute + + raw_signature_key = signature_attribute.signature_key + if public_key != raw_signature_key: + raise InvalidCertificate(f"Signature keys of certificate {index} do not match") + + signature_key = ECC.construct( + curve='P-256', + point_x=int.from_bytes(raw_signature_key[:32], 'big'), + point_y=int.from_bytes(raw_signature_key[32:], 'big') + ) + + sign_payload = self.dumps()[:-signature_object.length] + + if not Crypto.ecc256_verify( + public_key=signature_key, + data=sign_payload, + signature=signature_attribute.signature + ): + raise InvalidCertificate(f"Signature of certificate {index} is not authentic") + + return self.get_issuer_key() + + +class CertificateChain(_BCertStructs): + """Represents a BCertChain""" + + ECC256MSBCertRootIssuerPubKey = bytes.fromhex("864d61cff2256e422c568b3c28001cfb3e1527658584ba0521b79b1828d936de1d826a8fc3e6e7fa7a90d5ca2946f1f64a2efb9f5dcffe7e434eb44293fac5ab") + + def __init__( + self, + parsed_bcert_chain: Container, + bcert_chain_obj: _BCertStructs.BCertChain = _BCertStructs.BCertChain + ): + self.parsed = parsed_bcert_chain + self._BCERT_CHAIN = bcert_chain_obj + + @classmethod + def loads(cls, data: Union[str, bytes]) -> CertificateChain: + if isinstance(data, str): + data = base64.b64decode(data) + if not isinstance(data, bytes): + raise ValueError(f"Expecting Bytes or Base64 input, got {data!r}") + + cert_chain = _BCertStructs.BCertChain + return cls( + parsed_bcert_chain=cert_chain.parse(data), + bcert_chain_obj=cert_chain + ) + + @classmethod + def load(cls, path: Union[Path, str]) -> CertificateChain: + if not isinstance(path, (Path, str)): + raise ValueError(f"Expecting Path object or path string, got {path!r}") + with Path(path).open(mode="rb") as f: + return cls.loads(f.read()) + + def dumps(self) -> bytes: + return self._BCERT_CHAIN.build(self.parsed) + + def struct(self) -> _BCertStructs.BCertChain: + return self._BCERT_CHAIN + + def get_security_level(self) -> int: + # not sure if there's a better way than this + return self.get(0).get_security_level() + + def get_name(self) -> str: + return self.get(0).get_name() + + def verify(self) -> bool: + issuer_key = self.ECC256MSBCertRootIssuerPubKey + + try: + for i in reversed(range(self.count())): + certificate = self.get(i) + issuer_key = certificate.verify(issuer_key, i) + + if not issuer_key and i != 0: + raise InvalidCertificate(f"Certificate {i} is not valid") + except InvalidCertificate as e: + raise InvalidCertificateChain(e) + + return True + + def append(self, bcert: Certificate) -> None: + self.parsed.certificate_count += 1 + self.parsed.certificates.append(bcert.parsed) + self.parsed.total_length += len(bcert.dumps()) + + def prepend(self, bcert: Certificate) -> None: + self.parsed.certificate_count += 1 + self.parsed.certificates.insert(0, bcert.parsed) + self.parsed.total_length += len(bcert.dumps()) + + def remove(self, index: int) -> None: + if self.count() <= 0: + raise InvalidCertificateChain("CertificateChain does not contain any Certificates") + if index >= self.count(): + raise IndexError(f"No Certificate at index {index}, {self.count()} total") + + self.parsed.certificate_count -= 1 + self.parsed.total_length -= len(self.get(index).dumps()) + self.parsed.certificates.pop(index) + + def get(self, index: int) -> Certificate: + if self.count() <= 0: + raise InvalidCertificateChain("CertificateChain does not contain any Certificates") + if index >= self.count(): + raise IndexError(f"No Certificate at index {index}, {self.count()} total") + + return Certificate(self.parsed.certificates[index]) + + def count(self) -> int: + return self.parsed.certificate_count diff --git a/scripts/pyplayready/pyplayready/system/pssh.py b/scripts/pyplayready/pyplayready/system/pssh.py new file mode 100644 index 0000000..16f6f12 --- /dev/null +++ b/scripts/pyplayready/pyplayready/system/pssh.py @@ -0,0 +1,96 @@ +import base64 +from typing import Union, List +from uuid import UUID + +from construct import Struct, Int32ul, Int16ul, Array, this, Bytes, Switch, Int32ub, Const, Container, ConstructError + +from pyplayready.exceptions import InvalidPssh +from pyplayready.system.wrmheader import WRMHeader + + +class _PlayreadyPSSHStructs: + PSSHBox = Struct( + "length" / Int32ub, + "pssh" / Const(b"pssh"), + "fullbox" / Int32ub, + "system_id" / Bytes(16), + "data_length" / Int32ub, + "data" / Bytes(this.data_length) + ) + + PlayreadyObject = Struct( + "type" / Int16ul, + "length" / Int16ul, + "data" / Switch( + this.type, + { + 1: Bytes(this.length) + }, + default=Bytes(this.length) + ) + ) + + PlayreadyHeader = Struct( + "length" / Int32ul, + "record_count" / Int16ul, + "records" / Array(this.record_count, PlayreadyObject) + ) + + +class PSSH(_PlayreadyPSSHStructs): + """Represents a PlayReady PSSH""" + + SYSTEM_ID = UUID(hex="9a04f07998404286ab92e65be0885f95") + + def __init__(self, data: Union[str, bytes]): + """Load a PSSH Box, PlayReady Header or PlayReady Object""" + + if not data: + raise InvalidPssh("Data must not be empty") + + if isinstance(data, str): + try: + data = base64.b64decode(data) + except Exception as e: + raise InvalidPssh(f"Could not decode data as Base64, {e}") + + self.wrm_headers: List[WRMHeader] + try: + # PSSH Box -> PlayReady Header + box = self.PSSHBox.parse(data) + prh = self.PlayreadyHeader.parse(box.data) + self.wrm_headers = self._read_playready_objects(prh) + except ConstructError: + if int.from_bytes(data[:2], byteorder="little") > 3: + try: + # PlayReady Header + prh = self.PlayreadyHeader.parse(data) + self.wrm_headers = self._read_playready_objects(prh) + except ConstructError: + raise InvalidPssh("Could not parse data as a PSSH Box nor a PlayReady Header") + else: + try: + # PlayReady Object + pro = self.PlayreadyObject.parse(data) + self.wrm_headers = [WRMHeader(pro.data)] + except ConstructError: + raise InvalidPssh("Could not parse data as a PSSH Box nor a PlayReady Object") + + @staticmethod + def _read_playready_objects(header: Container) -> List[WRMHeader]: + return list(map( + lambda pro: WRMHeader(pro.data), + filter( + lambda pro: pro.type == 1, + header.records + ) + )) + + def get_wrm_headers(self) -> List[str]: + """ + Return a list of all WRM Headers in the PSSH as plaintext strings + """ + return list(map( + lambda wrm_header: wrm_header.dumps(), + self.wrm_headers + )) diff --git a/scripts/pyplayready/pyplayready/system/session.py b/scripts/pyplayready/pyplayready/system/session.py new file mode 100644 index 0000000..f6be649 --- /dev/null +++ b/scripts/pyplayready/pyplayready/system/session.py @@ -0,0 +1,18 @@ +from Crypto.Random import get_random_bytes + +from pyplayready.license.key import Key +from pyplayready.crypto.ecc_key import ECCKey +from pyplayready.license.xml_key import XmlKey + + +class Session: + def __init__(self, number: int): + self.number = number + self.id = get_random_bytes(16) + self.xml_key = XmlKey() + self.signing_key: ECCKey = None + self.encryption_key: ECCKey = None + self.keys: list[Key] = [] + + +__all__ = ("Session",) diff --git a/scripts/pyplayready/pyplayready/system/wrmheader.py b/scripts/pyplayready/pyplayready/system/wrmheader.py new file mode 100644 index 0000000..6d70d66 --- /dev/null +++ b/scripts/pyplayready/pyplayready/system/wrmheader.py @@ -0,0 +1,165 @@ +import base64 +import binascii +from enum import Enum +from typing import Optional, List, Union, Tuple + +import xmltodict + + +class WRMHeader: + """Represents a PlayReady WRM Header""" + + class SignedKeyID: + def __init__( + self, + alg_id: str, + value: str, + checksum: str + ): + self.alg_id = alg_id + self.value = value + self.checksum = checksum + + def __repr__(self): + return f'SignedKeyID(alg_id={self.alg_id}, value="{self.value}", checksum="{self.checksum}")' + + class Version(Enum): + VERSION_4_0_0_0 = "4.0.0.0" + VERSION_4_1_0_0 = "4.1.0.0" + VERSION_4_2_0_0 = "4.2.0.0" + VERSION_4_3_0_0 = "4.3.0.0" + UNKNOWN = "UNKNOWN" + + @classmethod + def _missing_(cls, value): + return cls.UNKNOWN + + _RETURN_STRUCTURE = Tuple[List[SignedKeyID], Union[str, None], Union[str, None], Union[str, None]] + + def __init__(self, data: Union[str, bytes]): + """Load a WRM Header from either a string, base64 encoded data or bytes""" + + if not data: + raise ValueError("Data must not be empty") + + if isinstance(data, str): + try: + data = base64.b64decode(data).decode() + except (binascii.Error, binascii.Incomplete): + data = data.encode() + + self._raw_data: bytes = data + self._parsed = xmltodict.parse(self._raw_data) + + self._header = self._parsed.get('WRMHEADER') + if not self._header: + raise ValueError("Data is not a valid WRMHEADER") + + self.version = self.Version(self._header.get('@version')) + + @staticmethod + def _ensure_list(element: Union[dict, list]) -> List: + if isinstance(element, dict): + return [element] + return element + + @staticmethod + def _read_v4_0_0_0(data: dict) -> _RETURN_STRUCTURE: + protect_info = data.get("PROTECTINFO") + + return ( + [WRMHeader.SignedKeyID( + alg_id=protect_info["ALGID"], + value=data["KID"], + checksum=data.get("CHECKSUM") + )], + data.get("LA_URL"), + data.get("LUI_URL"), + data.get("DS_ID") + ) + + @staticmethod + def _read_v4_1_0_0(data: dict) -> _RETURN_STRUCTURE: + protect_info = data.get("PROTECTINFO") + + key_ids = [] + if protect_info: + kid = protect_info["KID"] + if kid: + key_ids = [WRMHeader.SignedKeyID( + alg_id=kid["@ALGID"], + value=kid["@VALUE"], + checksum=kid.get("@CHECKSUM") + )] + + return ( + key_ids, + data.get("LA_URL"), + data.get("LUI_URL"), + data.get("DS_ID") + ) + + @staticmethod + def _read_v4_2_0_0(data: dict) -> _RETURN_STRUCTURE: + protect_info = data.get("PROTECTINFO") + + key_ids = [] + if protect_info: + kids = protect_info["KIDS"] + if kids: + for kid in WRMHeader._ensure_list(kids["KID"]): + key_ids.append(WRMHeader.SignedKeyID( + alg_id=kid["@ALGID"], + value=kid["@VALUE"], + checksum=kid.get("@CHECKSUM") + )) + + return ( + key_ids, + data.get("LA_URL"), + data.get("LUI_URL"), + data.get("DS_ID") + ) + + @staticmethod + def _read_v4_3_0_0(data: dict) -> _RETURN_STRUCTURE: + protect_info = data.get("PROTECTINFO") + + key_ids = [] + if protect_info: + kids = protect_info["KIDS"] + for kid in WRMHeader._ensure_list(kids["KID"]): + key_ids.append(WRMHeader.SignedKeyID( + alg_id=kid.get("@ALGID"), + value=kid["@VALUE"], + checksum=kid.get("@CHECKSUM") + )) + + return ( + key_ids, + data.get("LA_URL"), + data.get("LUI_URL"), + data.get("DS_ID") + ) + + def read_attributes(self) -> _RETURN_STRUCTURE: + """ + Read any non-custom XML attributes + + Returns a tuple structured like this: Tuple[List[SignedKeyID], <LA_URL>, <LUI_URL>, <DS_ID>] + """ + data = self._header.get("DATA") + if not data: + raise ValueError("Not a valid PlayReady Header Record, WRMHEADER/DATA required") + + if self.version == self.Version.VERSION_4_0_0_0: + return self._read_v4_0_0_0(data) + elif self.version == self.Version.VERSION_4_1_0_0: + return self._read_v4_1_0_0(data) + elif self.version == self.Version.VERSION_4_2_0_0: + return self._read_v4_2_0_0(data) + elif self.version == self.Version.VERSION_4_3_0_0: + return self._read_v4_3_0_0(data) + + def dumps(self) -> str: + return self._raw_data.decode("utf-16-le") diff --git a/scripts/pyplayready/pyproject.toml b/scripts/pyplayready/pyproject.toml new file mode 100644 index 0000000..990c7ee --- /dev/null +++ b/scripts/pyplayready/pyproject.toml @@ -0,0 +1,44 @@ +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "pyplayready" +version = "0.5.0" +description = "pyplayready CDM (Content Decryption Module) implementation in Python." +license = "CC BY-NC-ND 4.0" +authors = ["DevLARLEY, Erevoc", "DevataDev"] +readme = "README.md" +repository = "https://github.com/ready-dl/pyplayready" +keywords = ["python", "drm", "playready", "microsoft"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "Natural Language :: English", + "Operating System :: OS Independent", + "Topic :: Multimedia :: Video", + "Topic :: Security :: Cryptography", + "Topic :: Software Development :: Libraries :: Python Modules" +] +include = [ + { path = "README.md", format = "sdist" }, + { path = "LICENSE", format = "sdist" }, +] + +[tool.poetry.urls] +"Issues" = "https://github.com/ready-dl/pyplayready/issues" + +[tool.poetry.dependencies] +python = ">=3.8,<4.0" +requests = "^2.32.3" +pycryptodome = "^3.21.0" +construct = "^2.8.8" +ECPy = "^1.2.5" +click = "^8.1.7" +xmltodict = "^0.14.2" +PyYAML = "^6.0.1" +aiohttp = {version = "^3.9.1", optional = true} + +[tool.poetry.scripts] +pyplayready = "pyplayready.main:main" diff --git a/scripts/pywidevine/README.md b/scripts/pywidevine/README.md new file mode 100644 index 0000000..db05668 --- /dev/null +++ b/scripts/pywidevine/README.md @@ -0,0 +1,166 @@ +<p align="center"> + <img src="docs/images/widevine_icon_24.png"> <a href="https://github.com/devine-dl/pywidevine">pywidevine</a> + <br/> + <sup><em>Python Widevine CDM implementation</em></sup> +</p> + +<p align="center"> + <a href="https://github.com/devine-dl/pywidevine/actions/workflows/ci.yml"> + <img src="https://github.com/devine-dl/pywidevine/actions/workflows/ci.yml/badge.svg" alt="Build status"> + </a> + <a href="https://pypi.org/project/pywidevine"> + <img src="https://img.shields.io/badge/python-3.8%2B-informational" alt="Python version"> + </a> + <a href="https://deepsource.io/gh/devine-dl/pywidevine"> + <img src="https://deepsource.io/gh/devine-dl/pywidevine.svg/?label=active+issues" alt="DeepSource"> + </a> +</p> +<p align="center"> + <a href="https://github.com/astral-sh/ruff"> + <img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Linter: Ruff"> + </a> + <a href="https://python-poetry.org"> + <img src="https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json" alt="Dependency management: Poetry"> + </a> +</p> + +## Features + +- 🚀 Seamless Installation via [pip](#installation) +- 🛡️ Robust Security with message signature verification +- 🙈 Privacy Mode with Service Certificates +- 🌐 Servable CDM API Server and Client with Authentication +- 📦 Custom provision serialization format (WVD v2) +- 🧰 Create, parse, or convert PSSH headers with ease +- 🗃️ User-friendly YAML configuration +- ❤️ Forever FOSS! + +## Installation + +```shell +$ pip install pywidevine +``` + +> **Note** +If pip gives you a warning about a path not being in your PATH environment variable then promptly add that path then +close all open command prompt/terminal windows, or `pywidevine` CLI won't work as it will not be found. + +Voilà 🎉 — You now have the `pywidevine` package installed! +You can now import pywidevine in scripts ([see below](#usage)). +A command-line interface is also available, try `pywidevine --help`. + +## Usage + +The following is a minimal example of using pywidevine in a script to get a License for Bitmovin's +Art of Motion Demo. + +```py +from pywidevine.cdm import Cdm +from pywidevine.device import Device +from pywidevine.pssh import PSSH + +import requests + +# prepare pssh +pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa" + "7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==") + +# load device +device = Device.load("C:/Path/To/A/Provision.wvd") + +# load cdm +cdm = Cdm.from_device(device) + +# open cdm session +session_id = cdm.open() + +# get license challenge +challenge = cdm.get_license_challenge(session_id, pssh) + +# send license challenge (assuming a generic license server SDK with no API front) +licence = requests.post("https://...", data=challenge) +licence.raise_for_status() + +# parse license challenge +cdm.parse_license(session_id, licence.content) + +# print keys +for key in cdm.get_keys(session_id): + print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}") + +# close session, disposes of session data +cdm.close(session_id) +``` + +> **Note** +> There are various features not shown in this specific example like: +> +> - Privacy Mode +> - Setting Service Certificates +> - Remote CDMs and Serving +> - Choosing a License Type to request +> - Creating WVD files +> - and much more! +> +> Take a look at the methods available in the [Cdm class](/pywidevine/cdm.py) and their doc-strings for +> further information. For more examples see the [CLI functions](/pywidevine/main.py) which uses a lot +> of previously mentioned features. + +## Disclaimer + +1. This project requires a valid Google-provisioned Private Key and Client Identification blob which are not + provided by this project. +2. Public test provisions are available and provided by Google to use for testing projects such as this one. +3. License Servers have the ability to block requests from any provision, and are likely already blocking test + provisions on production endpoints. +4. This project does not condone piracy or any action against the terms of the DRM systems. +5. All efforts in this project have been the result of Reverse-Engineering, Publicly available research, and Trial + & Error. + +## Key and Output Security + +*Licenses, Content Keys, and Decrypted Data is not secure in this CDM implementation.* + +The Content Decryption Module is meant to do all downloading, decrypting, and decoding of content, not just license +acquisition. This Python implementation only does License Acquisition within the CDM. + +The section of which a 'Decrypt Frame' call is made would be more of a 'Decrypt File' in this implementation. Just +returning the original file in plain text defeats the point of the DRM. Even if 'Decrypt File' was somehow secure, the +Content Keys used to decrypt the files are already exposed to the caller anyway, allowing them to manually decrypt. + +An attack on a 'Decrypt Frame' system would be analogous to doing an HDMI capture or similar attack. This is because it +would require re-encoding the video by splicing each individual frame with the right frame-rate, syncing to audio, and +more. + +While a 'Decrypt Video' system would be analogous to downloading a Video and passing it through a script. Not much of +an attack if at all. The only protection against a system like this would be monitoring the provision and acquisitions +of licenses and prevent them. This can be done by revoking the device provision, or the user or their authorization to +the service. + +There isn't any immediate way to secure either Key or Decrypted information within a Python environment that is not +Hardware backed. Even if obfuscation or some other form of Security by Obscurity was used, this is a Software-based +Content Protection Module (in Python no less) with no hardware backed security. It would be incredibly trivial to break +any sort of protection against retrieving the original video data. + +Though, it's not impossible. Google's Chrome Browser CDM is a simple library extension file programmed in C++ that has +been improving its security using math and obscurity for years. It's getting harder and harder to break with its latest +versions only being beaten by Brute-force style methods. However, they have a huge team of very skilled workers, and +making a CDM in C++ has immediate security benefits and a lot of methods to obscure and obfuscate the code. + +## Contributors + +<a href="https://github.com/rlaphoenix"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/17136956?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt=""/></a> +<a href="https://github.com/mediaminister"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/45148099?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt=""/></a> +<a href="https://github.com/sr0lle"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/111277375?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt=""/></a> + +## Licensing + +This software is licensed under the terms of [GNU General Public License, Version 3.0](LICENSE). +You can find a copy of the license in the LICENSE file in the root folder. + +- Widevine Icon © Google. +- Props to the awesome community for their shared research and insight into the Widevine Protocol and Key Derivation. + +* * * + +© rlaphoenix 2022-2023 diff --git a/scripts/pywidevine/pyproject.toml b/scripts/pywidevine/pyproject.toml new file mode 100644 index 0000000..2e96c70 --- /dev/null +++ b/scripts/pywidevine/pyproject.toml @@ -0,0 +1,86 @@ +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "pywidevine" +version = "1.8.0" +description = "Widevine CDM (Content Decryption Module) implementation in Python." +license = "GPL-3.0-only" +authors = ["rlaphoenix <rlaphoenix@pm.me>"] +readme = "README.md" +repository = "https://github.com/devine-dl/pywidevine" +keywords = ["python", "drm", "widevine", "google"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "Natural Language :: English", + "Operating System :: OS Independent", + "Topic :: Multimedia :: Video", + "Topic :: Security :: Cryptography", + "Topic :: Software Development :: Libraries :: Python Modules" +] +include = [ + { path = "CHANGELOG.md", format = "sdist" }, + { path = "README.md", format = "sdist" }, + { path = "LICENSE", format = "sdist" }, +] + +[tool.poetry.urls] +"Issues" = "https://github.com/devine-dl/pywidevine/issues" +"Discussions" = "https://github.com/devine-dl/pywidevine/discussions" +"Changelog" = "https://github.com/devine-dl/pywidevine/blob/master/CHANGELOG.md" + +[tool.poetry.dependencies] +python = ">=3.8,<4.0" +protobuf = "^4.25.1" +pycryptodome = "^3.19.0" +click = "^8.1.7" +requests = "^2.31.0" +Unidecode = "^1.3.7" +PyYAML = "^6.0.1" +aiohttp = {version = "^3.9.1", optional = true} + +[tool.poetry.group.dev.dependencies] +pre-commit = "^3.5.0" +mypy = "^1.7.1" +mypy-protobuf = "^3.5.0" +types-protobuf = "^4.24.0.4" +types-requests = "^2.31.0.10" +types-PyYAML = "^6.0.12.12" +isort = "^5.12.0" +ruff = "~0.1.7" + +[tool.poetry.extras] +serve = ["aiohttp"] + +[tool.poetry.scripts] +pywidevine = "pywidevine.main:main" + +[tool.ruff] +extend-exclude = [ + "*_pb2.py", + "*.pyi", +] +force-exclude = true +line-length = 120 +select = ["E4", "E7", "E9", "F", "W"] + +[tool.ruff.extend-per-file-ignores] +"pywidevine/__init__.py" = ["F403"] + +[tool.isort] +line_length = 118 +extend_skip_glob = ["*_pb2.py", "*.pyi"] + +[tool.mypy] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_untyped_defs = true +exclude = [ + '_pb2.pyi?$' # generated protobuffer files +] +follow_imports = "silent" +ignore_missing_imports = true +no_implicit_optional = true diff --git a/scripts/pywidevine/pywidevine/__init__.py b/scripts/pywidevine/pywidevine/__init__.py new file mode 100644 index 0000000..659ea25 --- /dev/null +++ b/scripts/pywidevine/pywidevine/__init__.py @@ -0,0 +1,8 @@ +from .cdm import * +from .device import * +from .key import * +from .pssh import * +from .remotecdm import * +from .session import * + +__version__ = "1.8.0" diff --git a/scripts/pywidevine/pywidevine/cdm.py b/scripts/pywidevine/pywidevine/cdm.py new file mode 100644 index 0000000..256cedf --- /dev/null +++ b/scripts/pywidevine/pywidevine/cdm.py @@ -0,0 +1,658 @@ +from __future__ import annotations + +import base64 +import binascii +import random +import subprocess +import sys +import time +from pathlib import Path +from typing import Optional, Union +from uuid import UUID + +from Crypto.Cipher import AES, PKCS1_OAEP +from Crypto.Hash import CMAC, HMAC, SHA1, SHA256 +from Crypto.PublicKey import RSA +from Crypto.Random import get_random_bytes +from Crypto.Signature import pss +from Crypto.Util import Padding +from google.protobuf.message import DecodeError + +from pywidevine.device import Device, DeviceTypes +from pywidevine.exceptions import (InvalidContext, InvalidInitData, InvalidLicenseMessage, InvalidLicenseType, + InvalidSession, NoKeysLoaded, SignatureMismatch, TooManySessions) +from pywidevine.key import Key +from pywidevine.license_protocol_pb2 import (ClientIdentification, DrmCertificate, EncryptedClientIdentification, + License, LicenseRequest, LicenseType, SignedDrmCertificate, + SignedMessage) +from pywidevine.pssh import PSSH +from pywidevine.session import Session +from pywidevine.utils import get_binary_path + + +class Cdm: + uuid = UUID(bytes=b"\xed\xef\x8b\xa9\x79\xd6\x4a\xce\xa3\xc8\x27\xdc\xd5\x1d\x21\xed") + urn = f"urn:uuid:{uuid}" + key_format = urn + service_certificate_challenge = b"\x08\x04" + common_privacy_cert = ( + # Used by Google's production license server (license.google.com) + # Not publicly accessible directly, but a lot of services have their own gateways to it + "CAUSxwUKwQIIAxIQFwW5F8wSBIaLBjM6L3cqjBiCtIKSBSKOAjCCAQoCggEBAJntWzsyfateJO/DtiqVtZhSCtW8yzdQPgZFuBTYdrjfQFEE" + "Qa2M462xG7iMTnJaXkqeB5UpHVhYQCOn4a8OOKkSeTkwCGELbxWMh4x+Ib/7/up34QGeHleB6KRfRiY9FOYOgFioYHrc4E+shFexN6jWfM3r" + "M3BdmDoh+07svUoQykdJDKR+ql1DghjduvHK3jOS8T1v+2RC/THhv0CwxgTRxLpMlSCkv5fuvWCSmvzu9Vu69WTi0Ods18Vcc6CCuZYSC4NZ" + "7c4kcHCCaA1vZ8bYLErF8xNEkKdO7DevSy8BDFnoKEPiWC8La59dsPxebt9k+9MItHEbzxJQAZyfWgkCAwEAAToUbGljZW5zZS53aWRldmlu" + "ZS5jb20SgAOuNHMUtag1KX8nE4j7e7jLUnfSSYI83dHaMLkzOVEes8y96gS5RLknwSE0bv296snUE5F+bsF2oQQ4RgpQO8GVK5uk5M4PxL/C" + "CpgIqq9L/NGcHc/N9XTMrCjRtBBBbPneiAQwHL2zNMr80NQJeEI6ZC5UYT3wr8+WykqSSdhV5Cs6cD7xdn9qm9Nta/gr52u/DLpP3lnSq8x2" + "/rZCR7hcQx+8pSJmthn8NpeVQ/ypy727+voOGlXnVaPHvOZV+WRvWCq5z3CqCLl5+Gf2Ogsrf9s2LFvE7NVV2FvKqcWTw4PIV9Sdqrd+QLeF" + "Hd/SSZiAjjWyWOddeOrAyhb3BHMEwg2T7eTo/xxvF+YkPj89qPwXCYcOxF+6gjomPwzvofcJOxkJkoMmMzcFBDopvab5tDQsyN9UPLGhGC98" + "X/8z8QSQ+spbJTYLdgFenFoGq47gLwDS6NWYYQSqzE3Udf2W7pzk4ybyG4PHBYV3s4cyzdq8amvtE/sNSdOKReuHpfQ=") + staging_privacy_cert = ( + # Used by Google's staging license server (staging.google.com) + # This can be publicly accessed without authentication using https://cwip-shaka-proxy.appspot.com/no_auth + "CAUSxQUKvwIIAxIQKHA0VMAI9jYYredEPbbEyBiL5/mQBSKOAjCCAQoCggEBALUhErjQXQI/zF2V4sJRwcZJtBd82NK+7zVbsGdD3mYePSq8" + "MYK3mUbVX9wI3+lUB4FemmJ0syKix/XgZ7tfCsB6idRa6pSyUW8HW2bvgR0NJuG5priU8rmFeWKqFxxPZmMNPkxgJxiJf14e+baq9a1Nuip+" + "FBdt8TSh0xhbWiGKwFpMQfCB7/+Ao6BAxQsJu8dA7tzY8U1nWpGYD5LKfdxkagatrVEB90oOSYzAHwBTK6wheFC9kF6QkjZWt9/v70JIZ2fz" + "PvYoPU9CVKtyWJOQvuVYCPHWaAgNRdiTwryi901goMDQoJk87wFgRwMzTDY4E5SGvJ2vJP1noH+a2UMCAwEAAToSc3RhZ2luZy5nb29nbGUu" + "Y29tEoADmD4wNSZ19AunFfwkm9rl1KxySaJmZSHkNlVzlSlyH/iA4KrvxeJ7yYDa6tq/P8OG0ISgLIJTeEjMdT/0l7ARp9qXeIoA4qprhM19" + "ccB6SOv2FgLMpaPzIDCnKVww2pFbkdwYubyVk7jei7UPDe3BKTi46eA5zd4Y+oLoG7AyYw/pVdhaVmzhVDAL9tTBvRJpZjVrKH1lexjOY9Dv" + "1F/FJp6X6rEctWPlVkOyb/SfEJwhAa/K81uDLyiPDZ1Flg4lnoX7XSTb0s+Cdkxd2b9yfvvpyGH4aTIfat4YkF9Nkvmm2mU224R1hx0WjocL" + "sjA89wxul4TJPS3oRa2CYr5+DU4uSgdZzvgtEJ0lksckKfjAF0K64rPeytvDPD5fS69eFuy3Tq26/LfGcF96njtvOUA4P5xRFtICogySKe6W" + "nCUZcYMDtQ0BMMM1LgawFNg4VA+KDCJ8ABHg9bOOTimO0sswHrRWSWX1XF15dXolCk65yEqz5lOfa2/fVomeopkU") + root_signed_cert = SignedDrmCertificate() + root_signed_cert.ParseFromString(base64.b64decode( + "CpwDCAASAQAY3ZSIiwUijgMwggGKAoIBgQC0/jnDZZAD2zwRlwnoaM3yw16b8udNI7EQ24dl39z7nzWgVwNTTPZtNX2meNuzNtI/nECplSZy" + "f7i+Zt/FIZh4FRZoXS9GDkPLioQ5q/uwNYAivjQji6tTW3LsS7VIaVM+R1/9Cf2ndhOPD5LWTN+udqm62SIQqZ1xRdbX4RklhZxTmpfrhNfM" + "qIiCIHAmIP1+QFAn4iWTb7w+cqD6wb0ptE2CXMG0y5xyfrDpihc+GWP8/YJIK7eyM7l97Eu6iR8nuJuISISqGJIOZfXIbBH/azbkdDTKjDOx" + "+biOtOYS4AKYeVJeRTP/Edzrw1O6fGAaET0A+9K3qjD6T15Id1sX3HXvb9IZbdy+f7B4j9yCYEy/5CkGXmmMOROtFCXtGbLynwGCDVZEiMg1" + "7B8RsyTgWQ035Ec86kt/lzEcgXyUikx9aBWE/6UI/Rjn5yvkRycSEbgj7FiTPKwS0ohtQT3F/hzcufjUUT4H5QNvpxLoEve1zqaWVT94tGSC" + "UNIzX5ECAwEAARKAA1jx1k0ECXvf1+9dOwI5F/oUNnVKOGeFVxKnFO41FtU9v0KG9mkAds2T9Hyy355EzUzUrgkYU0Qy7OBhG+XaE9NVxd0a" + "y5AeflvG6Q8in76FAv6QMcxrA4S9IsRV+vXyCM1lQVjofSnaBFiC9TdpvPNaV4QXezKHcLKwdpyywxXRESYqI3WZPrl3IjINvBoZwdVlkHZV" + "dA8OaU1fTY8Zr9/WFjGUqJJfT7x6Mfiujq0zt+kw0IwKimyDNfiKgbL+HIisKmbF/73mF9BiC9yKRfewPlrIHkokL2yl4xyIFIPVxe9enz2F" + "RXPia1BSV0z7kmxmdYrWDRuu8+yvUSIDXQouY5OcCwEgqKmELhfKrnPsIht5rvagcizfB0fbiIYwFHghESKIrNdUdPnzJsKlVshWTwApHQh7" + "evuVicPumFSePGuUBRMS9nG5qxPDDJtGCHs9Mmpoyh6ckGLF7RC5HxclzpC5bc3ERvWjYhN0AqdipPpV2d7PouaAdFUGSdUCDA==" + )) + root_cert = DrmCertificate() + root_cert.ParseFromString(root_signed_cert.drm_certificate) + + MAX_NUM_OF_SESSIONS = 16 + + def __init__( + self, + device_type: Union[DeviceTypes, str], + system_id: int, + security_level: int, + client_id: ClientIdentification, + rsa_key: RSA.RsaKey + ): + """Initialize a Widevine Content Decryption Module (CDM).""" + if not device_type: + raise ValueError("Device Type must be provided") + if isinstance(device_type, str): + device_type = DeviceTypes[device_type] + if not isinstance(device_type, DeviceTypes): + raise TypeError(f"Expected device_type to be a {DeviceTypes!r} not {device_type!r}") + + if not system_id: + raise ValueError("System ID must be provided") + if not isinstance(system_id, int): + raise TypeError(f"Expected system_id to be a {int} not {system_id!r}") + + if not security_level: + raise ValueError("Security Level must be provided") + if not isinstance(security_level, int): + raise TypeError(f"Expected security_level to be a {int} not {security_level!r}") + + if not client_id: + raise ValueError("Client ID must be provided") + if not isinstance(client_id, ClientIdentification): + raise TypeError(f"Expected client_id to be a {ClientIdentification} not {client_id!r}") + + if not rsa_key: + raise ValueError("RSA Key must be provided") + if not isinstance(rsa_key, RSA.RsaKey): + raise TypeError(f"Expected rsa_key to be a {RSA.RsaKey} not {rsa_key!r}") + + self.device_type = device_type + self.system_id = system_id + self.security_level = security_level + self.__client_id = client_id + + self.__signer = pss.new(rsa_key) + self.__decrypter = PKCS1_OAEP.new(rsa_key) + + self.__sessions: dict[bytes, Session] = {} + + @classmethod + def from_device(cls, device: Device) -> Cdm: + """Initialize a Widevine CDM from a Widevine Device (.wvd) file.""" + return cls( + device_type=device.type, + system_id=device.system_id, + security_level=device.security_level, + client_id=device.client_id, + rsa_key=device.private_key + ) + + def open(self) -> bytes: + """ + Open a Widevine Content Decryption Module (CDM) session. + + Raises: + TooManySessions: If the session cannot be opened as limit has been reached. + """ + if len(self.__sessions) > self.MAX_NUM_OF_SESSIONS: + raise TooManySessions(f"Too many Sessions open ({self.MAX_NUM_OF_SESSIONS}).") + + session = Session(len(self.__sessions) + 1) + self.__sessions[session.id] = session + + return session.id + + def close(self, session_id: bytes) -> None: + """ + Close a Widevine Content Decryption Module (CDM) session. + + Parameters: + session_id: Session identifier. + + Raises: + InvalidSession: If the Session identifier is invalid. + """ + session = self.__sessions.get(session_id) + if not session: + raise InvalidSession(f"Session identifier {session_id!r} is invalid.") + del self.__sessions[session_id] + + def set_service_certificate(self, session_id: bytes, certificate: Optional[Union[bytes, str]]) -> Optional[str]: + """ + Set a Service Privacy Certificate for Privacy Mode. (optional but recommended) + + The Service Certificate is used to encrypt Client IDs in Licenses. This is also + known as Privacy Mode and may be required for some services or for some devices. + Chrome CDM requires it as of the enforcement of VMP (Verified Media Path). + + We reject direct DrmCertificates as they do not have signature verification and + cannot be verified. You must provide a SignedDrmCertificate or a SignedMessage + containing a SignedDrmCertificate. + + Parameters: + session_id: Session identifier. + certificate: SignedDrmCertificate (or SignedMessage containing one) in Base64 + or Bytes form obtained from the Service. Some services have their own, + but most use the common privacy cert, (common_privacy_cert). If None, it + will remove the current certificate. + + Raises: + InvalidSession: If the Session identifier is invalid. + DecodeError: If the certificate could not be parsed as a SignedDrmCertificate + nor a SignedMessage containing a SignedDrmCertificate. + SignatureMismatch: If the Signature of the SignedDrmCertificate does not + match the underlying DrmCertificate. + + Returns the Service Provider ID of the verified DrmCertificate if successful. + If certificate is None, it will return the now-unset certificate's Provider ID, + or None if no certificate was set yet. + """ + session = self.__sessions.get(session_id) + if not session: + raise InvalidSession(f"Session identifier {session_id!r} is invalid.") + + if certificate is None: + if session.service_certificate: + drm_certificate = DrmCertificate() + drm_certificate.ParseFromString(session.service_certificate.drm_certificate) + provider_id = drm_certificate.provider_id + else: + provider_id = None + session.service_certificate = None + return provider_id + + if isinstance(certificate, str): + try: + certificate = base64.b64decode(certificate) # assuming base64 + except binascii.Error: + raise DecodeError("Could not decode certificate string as Base64, expected bytes.") + elif not isinstance(certificate, bytes): + raise DecodeError(f"Expecting Certificate to be bytes, not {certificate!r}") + + signed_message = SignedMessage() + signed_drm_certificate = SignedDrmCertificate() + drm_certificate = DrmCertificate() + + try: + signed_message.ParseFromString(certificate) + if all( + # See https://github.com/devine-dl/pywidevine/issues/41 + bytes(chunk) == signed_message.SerializeToString() + for chunk in zip(*[iter(certificate)] * len(signed_message.SerializeToString())) + ): + signed_drm_certificate.ParseFromString(signed_message.msg) + else: + signed_drm_certificate.ParseFromString(certificate) + if signed_drm_certificate.SerializeToString() != certificate: + raise DecodeError("partial parse") + except DecodeError as e: + # could be a direct unsigned DrmCertificate, but reject those anyway + raise DecodeError(f"Could not parse certificate as a SignedDrmCertificate, {e}") + + try: + pss. \ + new(RSA.import_key(self.root_cert.public_key)). \ + verify( + msg_hash=SHA1.new(signed_drm_certificate.drm_certificate), + signature=signed_drm_certificate.signature + ) + except (ValueError, TypeError): + raise SignatureMismatch("Signature Mismatch on SignedDrmCertificate, rejecting certificate") + + try: + drm_certificate.ParseFromString(signed_drm_certificate.drm_certificate) + if drm_certificate.SerializeToString() != signed_drm_certificate.drm_certificate: + raise DecodeError("partial parse") + except DecodeError as e: + raise DecodeError(f"Could not parse signed certificate's message as a DrmCertificate, {e}") + + # must be stored as a SignedDrmCertificate as the signature needs to be kept for RemoteCdm + # if we store as DrmCertificate (no signature) then RemoteCdm cannot verify the Certificate + session.service_certificate = signed_drm_certificate + return drm_certificate.provider_id + + def get_service_certificate(self, session_id: bytes) -> Optional[SignedDrmCertificate]: + """ + Get the currently set Service Privacy Certificate of the Session. + + Parameters: + session_id: Session identifier. + + Raises: + InvalidSession: If the Session identifier is invalid. + + Returns the Service Certificate if one is set, otherwise None. + """ + session = self.__sessions.get(session_id) + if not session: + raise InvalidSession(f"Session identifier {session_id!r} is invalid.") + + return session.service_certificate + + def get_license_challenge( + self, + session_id: bytes, + pssh: PSSH, + license_type: str = "STREAMING", + privacy_mode: bool = True + ) -> bytes: + """ + Get a License Request (Challenge) to send to a License Server. + + Parameters: + session_id: Session identifier. + pssh: PSSH Object to get the init data from. + license_type: Type of License you wish to exchange, often `STREAMING`. + - "STREAMING": Normal one-time-use license. + - "OFFLINE": Offline-use licence, usually for Downloaded content. + - "AUTOMATIC": License type decision is left to provider. + privacy_mode: Encrypt the Client ID using the Privacy Certificate. If the + privacy certificate is not set yet, this does nothing. + + Raises: + InvalidSession: If the Session identifier is invalid. + InvalidInitData: If the Init Data (or PSSH box) provided is invalid. + InvalidLicenseType: If the type_ parameter value is not a License Type. It + must be a LicenseType enum, or a string/int representing the enum's keys + or values. + + Returns a SignedMessage containing a LicenseRequest message. It's signed with + the Private Key of the device provision. + """ + session = self.__sessions.get(session_id) + if not session: + raise InvalidSession(f"Session identifier {session_id!r} is invalid.") + + if not pssh: + raise InvalidInitData("A pssh must be provided.") + if not isinstance(pssh, PSSH): + raise InvalidInitData(f"Expected pssh to be a {PSSH}, not {pssh!r}") + + if not isinstance(license_type, str): + raise InvalidLicenseType(f"Expected license_type to be a {str}, not {license_type!r}") + if license_type not in LicenseType.keys(): + raise InvalidLicenseType( + f"Invalid license_type value of '{license_type}'. " + f"Available values: {LicenseType.keys()}" + ) + + if self.device_type == DeviceTypes.ANDROID: + # OEMCrypto's request_id seems to be in AES CTR Counter block form with no suffix + # Bytes 5-8 does not seem random, in real tests they have been consecutive \x00 or \xFF + # Real example: A0DCE548000000000500000000000000 + request_id = (get_random_bytes(4) + (b"\x00" * 4)) # (?) + request_id += session.number.to_bytes(8, "little") # counter + # as you can see in the real example, it is stored as uppercase hex and re-encoded + # it's really 16 bytes of data, but it's stored as a 32-char HEX string (32 bytes) + request_id = request_id.hex().upper().encode() + else: + request_id = get_random_bytes(16) + + license_request = LicenseRequest( + client_id=( + self.__client_id + ) if not (session.service_certificate and privacy_mode) else None, + encrypted_client_id=self.encrypt_client_id( + client_id=self.__client_id, + service_certificate=session.service_certificate + ) if session.service_certificate and privacy_mode else None, + content_id=LicenseRequest.ContentIdentification( + widevine_pssh_data=LicenseRequest.ContentIdentification.WidevinePsshData( + pssh_data=[pssh.init_data], # either a WidevineCencHeader or custom data + license_type=license_type, + request_id=request_id + ) + ), + type="NEW", + request_time=int(time.time()), + protocol_version="VERSION_2_1", + key_control_nonce=random.randrange(1, 2 ** 31), + ).SerializeToString() + + signed_license_request = SignedMessage( + type="LICENSE_REQUEST", + msg=license_request, + signature=self.__signer.sign(SHA1.new(license_request)) + ).SerializeToString() + + session.context[request_id] = self.derive_context(license_request) + + return signed_license_request + + def parse_license(self, session_id: bytes, license_message: Union[SignedMessage, bytes, str]) -> None: + """ + Load Keys from a License Message from a License Server Response. + + License Messages can only be loaded a single time. An InvalidContext error will + be raised if you attempt to parse a License Message more than once. + + Parameters: + session_id: Session identifier. + license_message: A SignedMessage containing a License message. + + Raises: + InvalidSession: If the Session identifier is invalid. + InvalidLicenseMessage: The License message could not be decoded as a Signed + Message or License message. + InvalidContext: If the Session has no Context Data. This is likely to happen + if the License Challenge was not made by this CDM instance, or was not + by this CDM at all. It could also happen if the Session is closed after + calling parse_license but not before it got the context data. + SignatureMismatch: If the Signature of the License SignedMessage does not + match the underlying License. + """ + session = self.__sessions.get(session_id) + if not session: + raise InvalidSession(f"Session identifier {session_id!r} is invalid.") + + if not license_message: + raise InvalidLicenseMessage("Cannot parse an empty license_message") + + if isinstance(license_message, str): + try: + license_message = base64.b64decode(license_message) + except (binascii.Error, binascii.Incomplete) as e: + raise InvalidLicenseMessage(f"Could not decode license_message as Base64, {e}") + + if isinstance(license_message, bytes): + signed_message = SignedMessage() + try: + signed_message.ParseFromString(license_message) + if signed_message.SerializeToString() != license_message: + raise DecodeError(license_message) + except DecodeError as e: + raise InvalidLicenseMessage(f"Could not parse license_message as a SignedMessage, {e}") + license_message = signed_message + + if not isinstance(license_message, SignedMessage): + raise InvalidLicenseMessage(f"Expecting license_response to be a SignedMessage, got {license_message!r}") + + if license_message.type != SignedMessage.MessageType.Value("LICENSE"): + raise InvalidLicenseMessage( + f"Expecting a LICENSE message, not a " + f"'{SignedMessage.MessageType.Name(license_message.type)}' message." + ) + + licence = License() + licence.ParseFromString(license_message.msg) + + context = session.context.get(licence.id.request_id) + if not context: + raise InvalidContext("Cannot parse a license message without first making a license request") + + enc_key, mac_key_server, _ = self.derive_keys( + *context, + key=self.__decrypter.decrypt(license_message.session_key) + ) + + # 1. Explicitly use the original `license_message.msg` instead of a re-serializing from `licence` + # as some differences may end up in the output due to differences in the proto schema + # 2. The oemcrypto_core_message (unknown purpose) is part of the signature algorithm starting with + # OEM Crypto API v16 and if available, must be prefixed when HMAC'ing a signature. + + computed_signature = HMAC. \ + new(mac_key_server, digestmod=SHA256). \ + update(license_message.oemcrypto_core_message or b""). \ + update(license_message.msg). \ + digest() + + if license_message.signature != computed_signature: + raise SignatureMismatch("Signature Mismatch on License Message, rejecting license") + + session.keys = [ + Key.from_key_container(key, enc_key) + for key in licence.key + ] + + del session.context[licence.id.request_id] + + def get_keys(self, session_id: bytes, type_: Optional[Union[int, str]] = None) -> list[Key]: + """ + Get Keys from the loaded License message. + + Parameters: + session_id: Session identifier. + type_: (optional) Key Type to filter by and return. + + Raises: + InvalidSession: If the Session identifier is invalid. + TypeError: If the provided type_ is an unexpected value type. + ValueError: If the provided type_ is not a valid Key Type. + """ + session = self.__sessions.get(session_id) + if not session: + raise InvalidSession(f"Session identifier {session_id!r} is invalid.") + + try: + if isinstance(type_, str): + type_ = License.KeyContainer.KeyType.Value(type_) + elif isinstance(type_, int): + License.KeyContainer.KeyType.Name(type_) # only test + elif type_ is not None: + raise TypeError(f"Expected type_ to be a {License.KeyContainer.KeyType} or int, not {type_!r}") + except ValueError as e: + raise ValueError(f"Could not parse type_ as a {License.KeyContainer.KeyType}, {e}") + + return [ + key + for key in session.keys + if not type_ or key.type == License.KeyContainer.KeyType.Name(type_) + ] + + def decrypt( + self, + session_id: bytes, + input_file: Union[Path, str], + output_file: Union[Path, str], + temp_dir: Optional[Union[Path, str]] = None, + exists_ok: bool = False + ) -> int: + """ + Decrypt a Widevine-encrypted file using Shaka-packager. + Shaka-packager is much more stable than mp4decrypt. + + Parameters: + session_id: Session identifier. + input_file: File to be decrypted with Session's currently loaded keys. + output_file: Location to save decrypted file. + temp_dir: Directory to store temporary data while decrypting. + exists_ok: Allow overwriting the output_file if it exists. + + Raises: + ValueError: If the input or output paths have not been supplied or are + invalid. + FileNotFoundError: If the input file path does not exist. + FileExistsError: If the output file path already exists. Ignored if exists_ok + is set to True. + NoKeysLoaded: No License was parsed for this Session, No Keys available. + EnvironmentError: If the shaka-packager executable could not be found. + subprocess.CalledProcessError: If the shaka-packager call returned a non-zero + exit code. + """ + if not input_file: + raise ValueError("Cannot decrypt nothing, specify an input path") + if not output_file: + raise ValueError("Cannot decrypt nowhere, specify an output path") + + if not isinstance(input_file, (Path, str)): + raise ValueError(f"Expecting input_file to be a Path or str, got {input_file!r}") + if not isinstance(output_file, (Path, str)): + raise ValueError(f"Expecting output_file to be a Path or str, got {output_file!r}") + if not isinstance(temp_dir, (Path, str)) and temp_dir is not None: + raise ValueError(f"Expecting temp_dir to be a Path or str, got {temp_dir!r}") + + input_file = Path(input_file) + output_file = Path(output_file) + temp_dir_ = Path(temp_dir) if temp_dir else None + + if not input_file.is_file(): + raise FileNotFoundError(f"Input file does not exist, {input_file}") + if output_file.is_file() and not exists_ok: + raise FileExistsError(f"Output file already exists, {output_file}") + + session = self.__sessions.get(session_id) + if not session: + raise InvalidSession(f"Session identifier {session_id!r} is invalid.") + + if not session.keys: + raise NoKeysLoaded("No Keys are loaded yet, cannot decrypt") + + platform = {"win32": "win", "darwin": "osx"}.get(sys.platform, sys.platform) + executable = get_binary_path("shaka-packager", f"packager-{platform}", f"packager-{platform}-x64") + if not executable: + raise EnvironmentError("Shaka Packager executable not found but is required") + + args = [ + f"input={input_file},stream=0,output={output_file}", + "--enable_raw_key_decryption", + "--keys", ",".join([ + label + for i, key in enumerate(session.keys) + for label in [ + f"label=1_{i}:key_id={key.kid.hex}:key={key.key.hex()}", + # some services need the KID blanked, e.g., Apple TV+ + f"label=2_{i}:key_id={'0' * 32}:key={key.key.hex()}" + ] + if key.type == "CONTENT" + ]) + ] + + if temp_dir_: + temp_dir_.mkdir(parents=True, exist_ok=True) + args.extend(["--temp_dir", str(temp_dir_)]) + + return subprocess.check_call([executable, *args]) + + @staticmethod + def encrypt_client_id( + client_id: ClientIdentification, + service_certificate: Union[SignedDrmCertificate, DrmCertificate], + key: Optional[bytes] = None, + iv: Optional[bytes] = None + ) -> EncryptedClientIdentification: + """Encrypt the Client ID with the Service's Privacy Certificate.""" + privacy_key = key or get_random_bytes(16) + privacy_iv = iv or get_random_bytes(16) + + if isinstance(service_certificate, SignedDrmCertificate): + drm_certificate = DrmCertificate() + drm_certificate.ParseFromString(service_certificate.drm_certificate) + service_certificate = drm_certificate + if not isinstance(service_certificate, DrmCertificate): + raise ValueError(f"Expecting Service Certificate to be a DrmCertificate, not {service_certificate!r}") + + encrypted_client_id = EncryptedClientIdentification( + provider_id=service_certificate.provider_id, + service_certificate_serial_number=service_certificate.serial_number, + encrypted_client_id=AES. + new(privacy_key, AES.MODE_CBC, privacy_iv). + encrypt(Padding.pad(client_id.SerializeToString(), 16)), + encrypted_client_id_iv=privacy_iv, + encrypted_privacy_key=PKCS1_OAEP. + new(RSA.importKey(service_certificate.public_key)). + encrypt(privacy_key) + ) + + return encrypted_client_id + + @staticmethod + def derive_context(message: bytes) -> tuple[bytes, bytes]: + """Returns 2 Context Data used for computing the AES Encryption and HMAC Keys.""" + + def _get_enc_context(msg: bytes) -> bytes: + label = b"ENCRYPTION" + key_size = 16 * 8 # 128-bit + return label + b"\x00" + msg + key_size.to_bytes(4, "big") + + def _get_mac_context(msg: bytes) -> bytes: + label = b"AUTHENTICATION" + key_size = 32 * 8 * 2 # 512-bit + return label + b"\x00" + msg + key_size.to_bytes(4, "big") + + return _get_enc_context(message), _get_mac_context(message) + + @staticmethod + def derive_keys(enc_context: bytes, mac_context: bytes, key: bytes) -> tuple[bytes, bytes, bytes]: + """ + Returns 3 keys derived from the input message. + Key can either be a pre-provision device aes key, provision key, or a session key. + + For provisioning: + - enc: aes key used for unwrapping RSA key out of response + - mac_key_server: hmac-sha256 key used for verifying provisioning response + - mac_key_client: hmac-sha256 key used for signing provisioning request + + When used with a session key: + - enc: decrypting content and other keys + - mac_key_server: verifying response + - mac_key_client: renewals + + With key as pre-provision device key, it can be used to provision and get an + RSA device key and token/cert with key as session key (OAEP wrapped with the + post-provision RSA device key), it can be used to decrypt content and signing + keys and verify licenses. + """ + + def _derive(session_key: bytes, context: bytes, counter: int) -> bytes: + return CMAC. \ + new(session_key, ciphermod=AES). \ + update(counter.to_bytes(1, "big") + context). \ + digest() + + enc_key = _derive(key, enc_context, 1) + mac_key_server = _derive(key, mac_context, 1) + mac_key_server += _derive(key, mac_context, 2) + mac_key_client = _derive(key, mac_context, 3) + mac_key_client += _derive(key, mac_context, 4) + + return enc_key, mac_key_server, mac_key_client + + +__all__ = ("Cdm",) diff --git a/scripts/pywidevine/pywidevine/device.py b/scripts/pywidevine/pywidevine/device.py new file mode 100644 index 0000000..e5c8a3a --- /dev/null +++ b/scripts/pywidevine/pywidevine/device.py @@ -0,0 +1,276 @@ +from __future__ import annotations + +import json +import base64 +import logging +from enum import Enum +from pathlib import Path +from typing import Any, Optional, Union + +from construct import BitStruct, Bytes, Const, ConstructError, Container +from construct import Enum as CEnum +from construct import Int8ub, Int16ub +from construct import Optional as COptional +from construct import Padded, Padding, Struct, this +from Crypto.PublicKey import RSA +from google.protobuf.message import DecodeError + +from pywidevine.license_protocol_pb2 import ClientIdentification, DrmCertificate, FileHashes, SignedDrmCertificate + + +class DeviceTypes(Enum): + CHROME = 1 + ANDROID = 2 + PLAYREADY = 3 + + +class _Structures: + magic = Const(b"WVD") + + header = Struct( + "signature" / magic, + "version" / Int8ub + ) + + # - Removed vmp and vmp_len as it should already be within the Client ID + v2 = Struct( + "signature" / magic, + "version" / Const(Int8ub, 2), + "type_" / CEnum( + Int8ub, + **{t.name: t.value for t in DeviceTypes} + ), + "security_level" / Int8ub, + "flags" / Padded(1, COptional(BitStruct( + # no per-device flags yet + Padding(8) + ))), + "private_key_len" / Int16ub, + "private_key" / Bytes(this.private_key_len), + "client_id_len" / Int16ub, + "client_id" / Bytes(this.client_id_len) + ) + + # - Removed system_id as it can be retrieved from the Client ID's DRM Certificate + v1 = Struct( + "signature" / magic, + "version" / Const(Int8ub, 1), + "type_" / CEnum( + Int8ub, + **{t.name: t.value for t in DeviceTypes} + ), + "security_level" / Int8ub, + "flags" / Padded(1, COptional(BitStruct( + # no per-device flags yet + Padding(8) + ))), + "private_key_len" / Int16ub, + "private_key" / Bytes(this.private_key_len), + "client_id_len" / Int16ub, + "client_id" / Bytes(this.client_id_len), + "vmp_len" / Int16ub, + "vmp" / Bytes(this.vmp_len) + ) + + +class Device: + Structures = _Structures + supported_structure = Structures.v2 + + def __init__( + self, + *_: Any, + type_: DeviceTypes, + security_level: int, + flags: Optional[dict], + private_key: Optional[bytes], + client_id: Optional[bytes], + **__: Any + ): + """ + This is the device key data that is needed for the CDM (Content Decryption Module). + + Parameters: + type_: Device Type + security_level: Security level from 1 (the highest ranking) to 3 (the lowest ranking) + flags: Extra flags + private_key: Device Private Key + client_id: Device Client Identification Blob + """ + # *_,*__ is to ignore unwanted args, like signature and version from the struct + + if not client_id: + raise ValueError("Client ID is required, the WVD does not contain one or is malformed.") + if not private_key: + raise ValueError("Private Key is required, the WVD does not contain one or is malformed.") + + self.type = DeviceTypes[type_] if isinstance(type_, str) else type_ + self.security_level = security_level + self.flags = flags or {} + self.private_key = RSA.importKey(private_key) + self.client_id = ClientIdentification() + try: + self.client_id.ParseFromString(client_id) + if self.client_id.SerializeToString() != client_id: + raise DecodeError("partial parse") + except DecodeError as e: + raise DecodeError(f"Failed to parse client_id as a ClientIdentification, {e}") + + self.vmp = FileHashes() + if self.client_id.vmp_data: + try: + self.vmp.ParseFromString(self.client_id.vmp_data) + if self.vmp.SerializeToString() != self.client_id.vmp_data: + raise DecodeError("partial parse") + except DecodeError as e: + raise DecodeError(f"Failed to parse Client ID's VMP data as a FileHashes, {e}") + + signed_drm_certificate = SignedDrmCertificate() + drm_certificate = DrmCertificate() + + try: + signed_drm_certificate.ParseFromString(self.client_id.token) + if signed_drm_certificate.SerializeToString() != self.client_id.token: + raise DecodeError("partial parse") + except DecodeError as e: + raise DecodeError(f"Failed to parse the Signed DRM Certificate of the Client ID, {e}") + + try: + drm_certificate.ParseFromString(signed_drm_certificate.drm_certificate) + if drm_certificate.SerializeToString() != signed_drm_certificate.drm_certificate: + raise DecodeError("partial parse") + except DecodeError as e: + raise DecodeError(f"Failed to parse the DRM Certificate of the Client ID, {e}") + + self.system_id = drm_certificate.system_id + + def __repr__(self) -> str: + return "{name}({items})".format( + name=self.__class__.__name__, + items=", ".join([f"{k}={repr(v)}" for k, v in self.__dict__.items()]) + ) + + @classmethod + def loads(cls, data: Union[bytes, str]) -> Device: + if isinstance(data, str): + data = base64.b64decode(data) + if not isinstance(data, bytes): + raise ValueError(f"Expecting Bytes or Base64 input, got {data!r}") + return cls(**cls.supported_structure.parse(data)) + + @classmethod + def from_dir(cls, dir: Union[Path, str]) -> Device: + try: + with open(os.path.join(d, "wv.json")) as fd: + config = json.load(fd) + except FileNotFoundError: + raise FileNotFoundError("wv.json file is required") + + try: + with open(os.path.join(d, "device_private_key"), "rb") as fd: + private_key = fd.read() + except FileNotFoundError: + private_key = None + + with open(os.path.join(d, "device_client_id_blob"), "rb") as fd: + client_id = fd.read() + + try: + with open(os.path.join(d, "device_vmp_blob"), "rb") as fd: + vmp = fd.read() + except FileNotFoundError: + vmp = None + + return cls( + type=getattr(DeviceTypes, config["session_id_type"].upper()), + security_level=int(config["security_level"]), + flags={ + "send_key_control_nonce": config.get("send_key_control_nonce", config["session_id_type"] == "android"), + }, + private_key=private_key, + client_id=client_id, + vmp=vmp, + ) + + @classmethod + def load(cls, path: Union[Path, str]) -> Device: + if not isinstance(path, (Path, str)): + raise ValueError(f"Expecting Path object or path string, got {path!r}") + with Path(path).open(mode="rb") as f: + return cls(**cls.supported_structure.parse_stream(f)) + + def dumps(self) -> bytes: + private_key = self.private_key.export_key("DER") if self.private_key else None + return self.supported_structure.build(dict( + version=2, + type_=self.type.value, + security_level=self.security_level, + flags=self.flags, + private_key_len=len(private_key) if private_key else 0, + private_key=private_key, + client_id_len=len(self.client_id.SerializeToString()) if self.client_id else 0, + client_id=self.client_id.SerializeToString() if self.client_id else None + )) + + def dump(self, path: Union[Path, str]) -> None: + if not isinstance(path, (Path, str)): + raise ValueError(f"Expecting Path object or path string, got {path!r}") + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(self.dumps()) + + @classmethod + def migrate(cls, data: Union[bytes, str]) -> Device: + if isinstance(data, str): + data = base64.b64decode(data) + if not isinstance(data, bytes): + raise ValueError(f"Expecting Bytes or Base64 input, got {data!r}") + + header = _Structures.header.parse(data) + if header.version == 2: + raise ValueError("Device Data is already migrated to the latest version.") + if header.version == 0 or header.version > 2: + # we have never used version 0, likely data that just so happened to use the WVD magic + raise ValueError("Device Data does not seem to be a WVD file (v0).") + + if header.version == 1: # v1 to v2 + v1_struct = _Structures.v1.parse(data) + v1_struct.version = 2 # update version to 2 to allow loading + v1_struct.flags = Container() # blank flags that may have been used in v1 + + vmp = FileHashes() + if v1_struct.vmp: + try: + vmp.ParseFromString(v1_struct.vmp) + if vmp.SerializeToString() != v1_struct.vmp: + raise DecodeError("partial parse") + except DecodeError as e: + raise DecodeError(f"Failed to parse VMP data as FileHashes, {e}") + v1_struct.vmp = vmp + + client_id = ClientIdentification() + try: + client_id.ParseFromString(v1_struct.client_id) + if client_id.SerializeToString() != v1_struct.client_id: + raise DecodeError("partial parse") + except DecodeError as e: + raise DecodeError(f"Failed to parse VMP data as FileHashes, {e}") + + new_vmp_data = v1_struct.vmp.SerializeToString() + if client_id.vmp_data and client_id.vmp_data != new_vmp_data: + logging.getLogger("migrate").warning("Client ID already has Verified Media Path data") + client_id.vmp_data = new_vmp_data + v1_struct.client_id = client_id.SerializeToString() + + try: + data = _Structures.v2.build(v1_struct) + except ConstructError as e: + raise ValueError(f"Migration failed, {e}") + + try: + return cls.loads(data) + except ConstructError as e: + raise ValueError(f"Device Data seems to be corrupt or invalid, or migration failed, {e}") + + +__all__ = ("Device", "DeviceTypes") diff --git a/scripts/pywidevine/pywidevine/exceptions.py b/scripts/pywidevine/pywidevine/exceptions.py new file mode 100644 index 0000000..3d6c5c7 --- /dev/null +++ b/scripts/pywidevine/pywidevine/exceptions.py @@ -0,0 +1,38 @@ +class PyWidevineException(Exception): + """Exceptions used by pywidevine.""" + + +class TooManySessions(PyWidevineException): + """Too many Sessions are open.""" + + +class InvalidSession(PyWidevineException): + """No Session is open with the specified identifier.""" + + +class InvalidInitData(PyWidevineException): + """The Widevine Cenc Header Data is invalid or empty.""" + + +class InvalidLicenseType(PyWidevineException): + """The License Type is an Invalid Value.""" + + +class InvalidLicenseMessage(PyWidevineException): + """The License Message is Invalid or Missing.""" + + +class InvalidContext(PyWidevineException): + """The Context is Invalid or Missing.""" + + +class SignatureMismatch(PyWidevineException): + """The Signature did not match.""" + + +class NoKeysLoaded(PyWidevineException): + """No License was parsed for this Session, No Keys available.""" + + +class DeviceMismatch(PyWidevineException): + """The Remote CDMs Device information and the APIs Device information did not match.""" diff --git a/scripts/pywidevine/pywidevine/key.py b/scripts/pywidevine/pywidevine/key.py new file mode 100644 index 0000000..c5f8b31 --- /dev/null +++ b/scripts/pywidevine/pywidevine/key.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import base64 +from typing import Optional, Union +from uuid import UUID + +from Crypto.Cipher import AES +from Crypto.Util import Padding + +from pywidevine.license_protocol_pb2 import License + + +class Key: + def __init__(self, type_: str, kid: UUID, key: bytes, permissions: Optional[list[str]] = None): + self.type = type_ + self.kid = kid + self.key = key + self.permissions = permissions or [] + + def __repr__(self) -> str: + return "{name}({items})".format( + name=self.__class__.__name__, + items=", ".join([f"{k}={repr(v)}" for k, v in self.__dict__.items()]) + ) + + @classmethod + def from_key_container(cls, key: License.KeyContainer, enc_key: bytes) -> Key: + """Load Key from a KeyContainer object.""" + permissions = [] + if key.type == License.KeyContainer.KeyType.Value("OPERATOR_SESSION"): + for descriptor, value in key.operator_session_key_permissions.ListFields(): + if value == 1: + permissions.append(descriptor.name) + + return Key( + type_=License.KeyContainer.KeyType.Name(key.type), + kid=cls.kid_to_uuid(key.id), + key=Padding.unpad( + AES.new(enc_key, AES.MODE_CBC, iv=key.iv).decrypt(key.key), + 16 + ), + permissions=permissions + ) + + @staticmethod + def kid_to_uuid(kid: Union[str, bytes]) -> UUID: + """ + Convert a Key ID from a string or bytes to a UUID object. + At first this may seem very simple but some types of Key IDs + may not be 16 bytes and some may be decimal vs. hex. + """ + if isinstance(kid, str): + kid = base64.b64decode(kid) + if not kid: + kid = b"\x00" * 16 + + if kid.decode(errors="replace").isdigit(): + return UUID(int=int(kid.decode())) + + if len(kid) < 16: + kid += b"\x00" * (16 - len(kid)) + + return UUID(bytes=kid) + + +__all__ = ("Key",) diff --git a/scripts/pywidevine/pywidevine/license_protocol.proto b/scripts/pywidevine/pywidevine/license_protocol.proto new file mode 100644 index 0000000..cd2fe4f --- /dev/null +++ b/scripts/pywidevine/pywidevine/license_protocol.proto @@ -0,0 +1,752 @@ +syntax = "proto2"; + +package pywidevine_license_protocol; + +// need this if we are using libprotobuf-cpp-2.3.0-lite +option optimize_for = LITE_RUNTIME; + +option java_package = "com.rlaphoenix.pywidevine.protos"; + +enum LicenseType { + STREAMING = 1; + OFFLINE = 2; + // License type decision is left to provider. + AUTOMATIC = 3; +} + +enum PlatformVerificationStatus { + // The platform is not verified. + PLATFORM_UNVERIFIED = 0; + // Tampering detected on the platform. + PLATFORM_TAMPERED = 1; + // The platform has been verified by means of software. + PLATFORM_SOFTWARE_VERIFIED = 2; + // The platform has been verified by means of hardware (e.g. secure boot). + PLATFORM_HARDWARE_VERIFIED = 3; + // Platform verification was not performed. + PLATFORM_NO_VERIFICATION = 4; + // Platform and secure storage capability have been verified by means of + // software. + PLATFORM_SECURE_STORAGE_SOFTWARE_VERIFIED = 5; +} + +// LicenseIdentification is propagated from LicenseRequest to License, +// incrementing version with each iteration. +message LicenseIdentification { + optional bytes request_id = 1; + optional bytes session_id = 2; + optional bytes purchase_id = 3; + optional LicenseType type = 4; + optional int32 version = 5; + optional bytes provider_session_token = 6; +} + +message License { + message Policy { + // Indicates that playback of the content is allowed. + optional bool can_play = 1 [default = false]; + + // Indicates that the license may be persisted to non-volatile + // storage for offline use. + optional bool can_persist = 2 [default = false]; + + // Indicates that renewal of this license is allowed. + optional bool can_renew = 3 [default = false]; + + // For the |*duration*| fields, playback must halt when + // license_start_time (seconds since the epoch (UTC)) + + // license_duration_seconds is exceeded. A value of 0 + // indicates that there is no limit to the duration. + + // Indicates the rental window. + optional int64 rental_duration_seconds = 4 [default = 0]; + + // Indicates the viewing window, once playback has begun. + optional int64 playback_duration_seconds = 5 [default = 0]; + + // Indicates the time window for this specific license. + optional int64 license_duration_seconds = 6 [default = 0]; + + // The |renewal*| fields only apply if |can_renew| is true. + + // The window of time, in which playback is allowed to continue while + // renewal is attempted, yet unsuccessful due to backend problems with + // the license server. + optional int64 renewal_recovery_duration_seconds = 7 [default = 0]; + + // All renewal requests for this license shall be directed to the + // specified URL. + optional string renewal_server_url = 8; + + // How many seconds after license_start_time, before renewal is first + // attempted. + optional int64 renewal_delay_seconds = 9 [default = 0]; + + // Specifies the delay in seconds between subsequent license + // renewal requests, in case of failure. + optional int64 renewal_retry_interval_seconds = 10 [default = 0]; + + // Indicates that the license shall be sent for renewal when usage is + // started. + optional bool renew_with_usage = 11 [default = false]; + + // Indicates to client that license renewal and release requests ought to + // include ClientIdentification (client_id). + optional bool always_include_client_id = 12 [default = false]; + + // Duration of grace period before playback_duration_seconds (short window) + // goes into effect. Optional. + optional int64 play_start_grace_period_seconds = 13 [default = 0]; + + // Enables "soft enforcement" of playback_duration_seconds, letting the user + // finish playback even if short window expires. Optional. + optional bool soft_enforce_playback_duration = 14 [default = false]; + + // Enables "soft enforcement" of rental_duration_seconds. Initial playback + // must always start before rental duration expires. In order to allow + // subsequent playbacks to start after the rental duration expires, + // soft_enforce_playback_duration must be true. Otherwise, subsequent + // playbacks will not be allowed once rental duration expires. Optional. + optional bool soft_enforce_rental_duration = 15 [default = true]; + } + + message KeyContainer { + enum KeyType { + SIGNING = 1; // Exactly one key of this type must appear. + CONTENT = 2; // Content key. + KEY_CONTROL = 3; // Key control block for license renewals. No key. + OPERATOR_SESSION = 4; // wrapped keys for auxiliary crypto operations. + ENTITLEMENT = 5; // Entitlement keys. + OEM_CONTENT = 6; // Partner-specific content key. + } + + // The SecurityLevel enumeration allows the server to communicate the level + // of robustness required by the client, in order to use the key. + enum SecurityLevel { + // Software-based whitebox crypto is required. + SW_SECURE_CRYPTO = 1; + + // Software crypto and an obfuscated decoder is required. + SW_SECURE_DECODE = 2; + + // The key material and crypto operations must be performed within a + // hardware backed trusted execution environment. + HW_SECURE_CRYPTO = 3; + + // The crypto and decoding of content must be performed within a hardware + // backed trusted execution environment. + HW_SECURE_DECODE = 4; + + // The crypto, decoding and all handling of the media (compressed and + // uncompressed) must be handled within a hardware backed trusted + // execution environment. + HW_SECURE_ALL = 5; + } + + message KeyControl { + // |key_control| is documented in: + // Widevine Modular DRM Security Integration Guide for CENC + // If present, the key control must be communicated to the secure + // environment prior to any usage. This message is automatically generated + // by the Widevine License Server SDK. + optional bytes key_control_block = 1; + optional bytes iv = 2; + } + + message OutputProtection { + // Indicates whether HDCP is required on digital outputs, and which + // version should be used. + enum HDCP { + HDCP_NONE = 0; + HDCP_V1 = 1; + HDCP_V2 = 2; + HDCP_V2_1 = 3; + HDCP_V2_2 = 4; + HDCP_V2_3 = 5; + HDCP_NO_DIGITAL_OUTPUT = 0xff; + } + optional HDCP hdcp = 1 [default = HDCP_NONE]; + + // Indicate the CGMS setting to be inserted on analog output. + enum CGMS { + CGMS_NONE = 42; + COPY_FREE = 0; + COPY_ONCE = 2; + COPY_NEVER = 3; + } + optional CGMS cgms_flags = 2 [default = CGMS_NONE]; + + enum HdcpSrmRule { + HDCP_SRM_RULE_NONE = 0; + // In 'required_protection', this means most current SRM is required. + // Update the SRM on the device. If update cannot happen, + // do not allow the key. + // In 'requested_protection', this means most current SRM is requested. + // Update the SRM on the device. If update cannot happen, + // allow use of the key anyway. + CURRENT_SRM = 1; + } + optional HdcpSrmRule hdcp_srm_rule = 3 [default = HDCP_SRM_RULE_NONE]; + // Optional requirement to indicate analog output is not allowed. + optional bool disable_analog_output = 4 [default = false]; + // Optional requirement to indicate digital output is not allowed. + optional bool disable_digital_output = 5 [default = false]; + } + + message VideoResolutionConstraint { + // Minimum and maximum video resolutions in the range (height x width). + optional uint32 min_resolution_pixels = 1; + optional uint32 max_resolution_pixels = 2; + // Optional output protection requirements for this range. If not + // specified, the OutputProtection in the KeyContainer applies. + optional OutputProtection required_protection = 3; + } + + message OperatorSessionKeyPermissions { + // Permissions/key usage flags for operator service keys + // (type = OPERATOR_SESSION). + optional bool allow_encrypt = 1 [default = false]; + optional bool allow_decrypt = 2 [default = false]; + optional bool allow_sign = 3 [default = false]; + optional bool allow_signature_verify = 4 [default = false]; + } + + optional bytes id = 1; + optional bytes iv = 2; + optional bytes key = 3; + optional KeyType type = 4; + optional SecurityLevel level = 5 [default = SW_SECURE_CRYPTO]; + optional OutputProtection required_protection = 6; + // NOTE: Use of requested_protection is not recommended as it is only + // supported on a small number of platforms. + optional OutputProtection requested_protection = 7; + optional KeyControl key_control = 8; + optional OperatorSessionKeyPermissions operator_session_key_permissions = 9; + // Optional video resolution constraints. If the video resolution of the + // content being decrypted/decoded falls within one of the specified ranges, + // the optional required_protections may be applied. Otherwise an error will + // be reported. + // NOTE: Use of this feature is not recommended, as it is only supported on + // a small number of platforms. + repeated VideoResolutionConstraint video_resolution_constraints = 10; + // Optional flag to indicate the key must only be used if the client + // supports anti rollback of the user table. Content provider can query the + // client capabilities to determine if the client support this feature. + optional bool anti_rollback_usage_table = 11 [default = false]; + // Optional not limited to commonly known track types such as SD, HD. + // It can be some provider defined label to identify the track. + optional string track_label = 12; + } + + optional LicenseIdentification id = 1; + optional Policy policy = 2; + repeated KeyContainer key = 3; + // Time of the request in seconds (UTC) as set in + // LicenseRequest.request_time. If this time is not set in the request, + // the local time at the license service is used in this field. + optional int64 license_start_time = 4; + optional bool remote_attestation_verified = 5 [default = false]; + // Client token generated by the content provider. Optional. + optional bytes provider_client_token = 6; + // 4cc code specifying the CENC protection scheme as defined in the CENC 3.0 + // specification. Propagated from Widevine PSSH box. Optional. + optional uint32 protection_scheme = 7; + // 8 byte verification field "HDCPDATA" followed by unsigned 32 bit minimum + // HDCP SRM version (whether the version is for HDCP1 SRM or HDCP2 SRM + // depends on client max_hdcp_version). + // Additional details can be found in Widevine Modular DRM Security + // Integration Guide for CENC. + optional bytes srm_requirement = 8; + // If present this contains a signed SRM file (either HDCP1 SRM or HDCP2 SRM + // depending on client max_hdcp_version) that should be installed on the + // client device. + optional bytes srm_update = 9; + // Indicates the status of any type of platform verification performed by the + // server. + optional PlatformVerificationStatus platform_verification_status = 10 + [default = PLATFORM_NO_VERIFICATION]; + // IDs of the groups for which keys are delivered in this license, if any. + repeated bytes group_ids = 11; +} + +enum ProtocolVersion { + VERSION_2_0 = 20; + VERSION_2_1 = 21; + VERSION_2_2 = 22; +} + +message LicenseRequest { + message ContentIdentification { + message WidevinePsshData { + repeated bytes pssh_data = 1; + optional LicenseType license_type = 2; + optional bytes request_id = 3; // Opaque, client-specified. + } + + message WebmKeyId { + optional bytes header = 1; + optional LicenseType license_type = 2; + optional bytes request_id = 3; // Opaque, client-specified. + } + + message ExistingLicense { + optional LicenseIdentification license_id = 1; + optional int64 seconds_since_started = 2; + optional int64 seconds_since_last_played = 3; + optional bytes session_usage_table_entry = 4; + } + + message InitData { + enum InitDataType { + CENC = 1; + WEBM = 2; + } + + optional InitDataType init_data_type = 1 [default = CENC]; + optional bytes init_data = 2; + optional LicenseType license_type = 3; + optional bytes request_id = 4; + } + + oneof content_id_variant { + // Exactly one of these must be present. + WidevinePsshData widevine_pssh_data = 1; + WebmKeyId webm_key_id = 2; + ExistingLicense existing_license = 3; + InitData init_data = 4; + } + } + + enum RequestType { + NEW = 1; + RENEWAL = 2; + RELEASE = 3; + } + + // The client_id provides information authenticating the calling device. It + // contains the Widevine keybox token that was installed on the device at the + // factory. This field or encrypted_client_id below is required for a valid + // license request, but both should never be present in the same request. + optional ClientIdentification client_id = 1; + optional ContentIdentification content_id = 2; + optional RequestType type = 3; + // Time of the request in seconds (UTC) as set by the client. + optional int64 request_time = 4; + // Old-style decimal-encoded string key control nonce. + optional bytes key_control_nonce_deprecated = 5; + optional ProtocolVersion protocol_version = 6 [default = VERSION_2_0]; + // New-style uint32 key control nonce, please use instead of + // key_control_nonce_deprecated. + optional uint32 key_control_nonce = 7; + // Encrypted ClientIdentification message, used for privacy purposes. + optional EncryptedClientIdentification encrypted_client_id = 8; +} + +message MetricData { + enum MetricType { + // The time spent in the 'stage', specified in microseconds. + LATENCY = 1; + // The UNIX epoch timestamp at which the 'stage' was first accessed in + // microseconds. + TIMESTAMP = 2; + } + + message TypeValue { + optional MetricType type = 1; + // The value associated with 'type'. For example if type == LATENCY, the + // value would be the time in microseconds spent in this 'stage'. + optional int64 value = 2 [default = 0]; + } + + // 'stage' that is currently processing the SignedMessage. Required. + optional string stage_name = 1; + // metric and associated value. + repeated TypeValue metric_data = 2; +} + +message VersionInfo { + // License SDK version reported by the Widevine License SDK. This field + // is populated automatically by the SDK. + optional string license_sdk_version = 1; + // Version of the service hosting the license SDK. This field is optional. + // It may be provided by the hosting service. + optional string license_service_version = 2; +} + +message SignedMessage { + enum MessageType { + LICENSE_REQUEST = 1; + LICENSE = 2; + ERROR_RESPONSE = 3; + SERVICE_CERTIFICATE_REQUEST = 4; + SERVICE_CERTIFICATE = 5; + SUB_LICENSE = 6; + CAS_LICENSE_REQUEST = 7; + CAS_LICENSE = 8; + EXTERNAL_LICENSE_REQUEST = 9; + EXTERNAL_LICENSE = 10; + } + + enum SessionKeyType { + UNDEFINED = 0; + WRAPPED_AES_KEY = 1; + EPHERMERAL_ECC_PUBLIC_KEY = 2; + } + optional MessageType type = 1; + optional bytes msg = 2; + // Required field that contains the signature of the bytes of msg. + // For license requests, the signing algorithm is determined by the + // certificate contained in the request. + // For license responses, the signing algorithm is HMAC with signing key based + // on |session_key|. + optional bytes signature = 3; + // If populated, the contents of this field will be signaled by the + // |session_key_type| type. If the |session_key_type| is WRAPPED_AES_KEY the + // key is the bytes of an encrypted AES key. If the |session_key_type| is + // EPHERMERAL_ECC_PUBLIC_KEY the field contains the bytes of an RFC5208 ASN1 + // serialized ECC public key. + optional bytes session_key = 4; + // Remote attestation data which will be present in the initial license + // request for ChromeOS client devices operating in verified mode. Remote + // attestation challenge data is |msg| field above. Optional. + optional bytes remote_attestation = 5; + + repeated MetricData metric_data = 6; + // Version information from the SDK and license service. This information is + // provided in the license response. + optional VersionInfo service_version_info = 7; + // Optional field that contains the algorithm type used to generate the + // session_key and signature in a LICENSE message. + optional SessionKeyType session_key_type = 8 [default = WRAPPED_AES_KEY]; + // The core message is the simple serialization of fields used by OEMCrypto. + // This field was introduced in OEMCrypto API v16. + optional bytes oemcrypto_core_message = 9; +} + +enum HashAlgorithmProto { + // Unspecified hash algorithm: SHA_256 shall be used for ECC based algorithms + // and SHA_1 shall be used otherwise. + HASH_ALGORITHM_UNSPECIFIED = 0; + HASH_ALGORITHM_SHA_1 = 1; + HASH_ALGORITHM_SHA_256 = 2; + HASH_ALGORITHM_SHA_384 = 3; +} + +// ClientIdentification message used to authenticate the client device. +message ClientIdentification { + enum TokenType { + KEYBOX = 0; + DRM_DEVICE_CERTIFICATE = 1; + REMOTE_ATTESTATION_CERTIFICATE = 2; + OEM_DEVICE_CERTIFICATE = 3; + } + + message NameValue { + optional string name = 1; + optional string value = 2; + } + + // Capabilities which not all clients may support. Used for the license + // exchange protocol only. + message ClientCapabilities { + enum HdcpVersion { + HDCP_NONE = 0; + HDCP_V1 = 1; + HDCP_V2 = 2; + HDCP_V2_1 = 3; + HDCP_V2_2 = 4; + HDCP_V2_3 = 5; + HDCP_NO_DIGITAL_OUTPUT = 0xff; + } + + enum CertificateKeyType { + RSA_2048 = 0; + RSA_3072 = 1; + ECC_SECP256R1 = 2; + ECC_SECP384R1 = 3; + ECC_SECP521R1 = 4; + } + + enum AnalogOutputCapabilities { + ANALOG_OUTPUT_UNKNOWN = 0; + ANALOG_OUTPUT_NONE = 1; + ANALOG_OUTPUT_SUPPORTED = 2; + ANALOG_OUTPUT_SUPPORTS_CGMS_A = 3; + } + + optional bool client_token = 1 [default = false]; + optional bool session_token = 2 [default = false]; + optional bool video_resolution_constraints = 3 [default = false]; + optional HdcpVersion max_hdcp_version = 4 [default = HDCP_NONE]; + optional uint32 oem_crypto_api_version = 5; + // Client has hardware support for protecting the usage table, such as + // storing the generation number in secure memory. For Details, see: + // Widevine Modular DRM Security Integration Guide for CENC + optional bool anti_rollback_usage_table = 6 [default = false]; + // The client shall report |srm_version| if available. + optional uint32 srm_version = 7; + // A device may have SRM data, and report a version, but may not be capable + // of updating SRM data. + optional bool can_update_srm = 8 [default = false]; + repeated CertificateKeyType supported_certificate_key_type = 9; + optional AnalogOutputCapabilities analog_output_capabilities = 10 + [default = ANALOG_OUTPUT_UNKNOWN]; + optional bool can_disable_analog_output = 11 [default = false]; + // Clients can indicate a performance level supported by OEMCrypto. + // This will allow applications and providers to choose an appropriate + // quality of content to serve. Currently defined tiers are + // 1 (low), 2 (medium) and 3 (high). Any other value indicates that + // the resource rating is unavailable or reporting erroneous values + // for that device. For details see, + // Widevine Modular DRM Security Integration Guide for CENC + optional uint32 resource_rating_tier = 12 [default = 0]; + } + + message ClientCredentials { + optional TokenType type = 1 [default = KEYBOX]; + optional bytes token = 2; + } + + // Type of factory-provisioned device root of trust. Optional. + optional TokenType type = 1 [default = KEYBOX]; + // Factory-provisioned device root of trust. Required. + optional bytes token = 2; + // Optional client information name/value pairs. + repeated NameValue client_info = 3; + // Client token generated by the content provider. Optional. + optional bytes provider_client_token = 4; + // Number of licenses received by the client to which the token above belongs. + // Only present if client_token is specified. + optional uint32 license_counter = 5; + // List of non-baseline client capabilities. + optional ClientCapabilities client_capabilities = 6; + // Serialized VmpData message. Optional. + optional bytes vmp_data = 7; + // Optional field that may contain additional provisioning credentials. + repeated ClientCredentials device_credentials = 8; +} + +// EncryptedClientIdentification message used to hold ClientIdentification +// messages encrypted for privacy purposes. +message EncryptedClientIdentification { + // Provider ID for which the ClientIdentifcation is encrypted (owner of + // service certificate). + optional string provider_id = 1; + // Serial number for the service certificate for which ClientIdentification is + // encrypted. + optional bytes service_certificate_serial_number = 2; + // Serialized ClientIdentification message, encrypted with the privacy key + // using AES-128-CBC with PKCS#5 padding. + optional bytes encrypted_client_id = 3; + // Initialization vector needed to decrypt encrypted_client_id. + optional bytes encrypted_client_id_iv = 4; + // AES-128 privacy key, encrypted with the service public key using RSA-OAEP. + optional bytes encrypted_privacy_key = 5; +} + +// DRM certificate definition for user devices, intermediate, service, and root +// certificates. +message DrmCertificate { + enum Type { + ROOT = 0; // ProtoBestPractices: ignore. + DEVICE_MODEL = 1; + DEVICE = 2; + SERVICE = 3; + PROVISIONER = 4; + } + enum ServiceType { + UNKNOWN_SERVICE_TYPE = 0; + LICENSE_SERVER_SDK = 1; + LICENSE_SERVER_PROXY_SDK = 2; + PROVISIONING_SDK = 3; + CAS_PROXY_SDK = 4; + } + enum Algorithm { + UNKNOWN_ALGORITHM = 0; + RSA = 1; + ECC_SECP256R1 = 2; + ECC_SECP384R1 = 3; + ECC_SECP521R1 = 4; + } + + message EncryptionKey { + // Device public key. PKCS#1 ASN.1 DER-encoded. Required. + optional bytes public_key = 1; + // Required. The algorithm field contains the curve used to create the + // |public_key| if algorithm is one of the ECC types. + // The |algorithm| is used for both to determine the if the certificate is + // ECC or RSA. The |algorithm| also specifies the parameters that were used + // to create |public_key| and are used to create an ephemeral session key. + optional Algorithm algorithm = 2 [default = RSA]; + } + + // Type of certificate. Required. + optional Type type = 1; + // 128-bit globally unique serial number of certificate. + // Value is 0 for root certificate. Required. + optional bytes serial_number = 2; + // POSIX time, in seconds, when the certificate was created. Required. + optional uint32 creation_time_seconds = 3; + // POSIX time, in seconds, when the certificate should expire. Value of zero + // denotes indefinite expiry time. For more information on limited lifespan + // DRM certificates see (go/limited-lifespan-drm-certificates). + optional uint32 expiration_time_seconds = 12; + // Device public key. PKCS#1 ASN.1 DER-encoded. Required. + optional bytes public_key = 4; + // Widevine system ID for the device. Required for intermediate and + // user device certificates. + optional uint32 system_id = 5; + // Deprecated field, which used to indicate whether the device was a test + // (non-production) device. The test_device field in ProvisionedDeviceInfo + // below should be observed instead. + optional bool test_device_deprecated = 6 [deprecated = true]; + // Service identifier (web origin) for the provider which owns the + // certificate. Required for service and provisioner certificates. + optional string provider_id = 7; + // This field is used only when type = SERVICE to specify which SDK uses + // service certificate. This repeated field is treated as a set. A certificate + // may be used for the specified service SDK if the appropriate ServiceType + // is specified in this field. + repeated ServiceType service_types = 8; + // Required. The algorithm field contains the curve used to create the + // |public_key| if algorithm is one of the ECC types. + // The |algorithm| is used for both to determine the if the certificate is ECC + // or RSA. The |algorithm| also specifies the parameters that were used to + // create |public_key| and are used to create an ephemeral session key. + optional Algorithm algorithm = 9 [default = RSA]; + // Optional. May be present in DEVICE certificate types. This is the root + // of trust identifier that holds an encrypted value that identifies the + // keybox or other root of trust that was used to provision a DEVICE drm + // certificate. + optional bytes rot_id = 10; + // Optional. May be present in devices that explicitly support dual keys. When + // present the |public_key| is used for verification of received license + // request messages. + optional EncryptionKey encryption_key = 11; +} + +// DrmCertificate signed by a higher (CA) DRM certificate. +message SignedDrmCertificate { + // Serialized certificate. Required. + optional bytes drm_certificate = 1; + // Signature of certificate. Signed with root or intermediate + // certificate specified below. Required. + optional bytes signature = 2; + // SignedDrmCertificate used to sign this certificate. + optional SignedDrmCertificate signer = 3; + // Optional field that indicates the hash algorithm used in signature scheme. + optional HashAlgorithmProto hash_algorithm = 4; +} + +message WidevinePsshData { + enum Type { + SINGLE = 0; // Single PSSH to be used to retrieve content keys. + ENTITLEMENT = 1; // Primary PSSH used to retrieve entitlement keys. + ENTITLED_KEY = 2; // Secondary PSSH containing entitled key(s). + } + + message EntitledKey { + // ID of entitlement key used for wrapping |key|. + optional bytes entitlement_key_id = 1; + // ID of the entitled key. + optional bytes key_id = 2; + // Wrapped key. Required. + optional bytes key = 3; + // IV used for wrapping |key|. Required. + optional bytes iv = 4; + // Size of entitlement key used for wrapping |key|. + optional uint32 entitlement_key_size_bytes = 5 [default = 32]; + } + + // Entitlement or content key IDs. Can onnly present in SINGLE or ENTITLEMENT + // PSSHs. May be repeated to facilitate delivery of multiple keys in a + // single license. Cannot be used in conjunction with content_id or + // group_ids, which are the preferred mechanism. + repeated bytes key_ids = 2; + + // Content identifier which may map to multiple entitlement or content key + // IDs to facilitate the delivery of multiple keys in a single license. + // Cannot be present in conjunction with key_ids, but if used must be in all + // PSSHs. + optional bytes content_id = 4; + + // Crypto period index, for media using key rotation. Always corresponds to + // The content key period. This means that if using entitlement licensing + // the ENTITLED_KEY PSSHs will have sequential crypto_period_index's, whereas + // the ENTITELEMENT PSSHs will have gaps in the sequence. Required if doing + // key rotation. + optional uint32 crypto_period_index = 7; + + // Protection scheme identifying the encryption algorithm. The protection + // scheme is represented as a uint32 value. The uint32 contains 4 bytes each + // representing a single ascii character in one of the 4CC protection scheme + // values. To be deprecated in favor of signaling from content. + // 'cenc' (AES-CTR) protection_scheme = 0x63656E63, + // 'cbc1' (AES-CBC) protection_scheme = 0x63626331, + // 'cens' (AES-CTR pattern encryption) protection_scheme = 0x63656E73, + // 'cbcs' (AES-CBC pattern encryption) protection_scheme = 0x63626373. + optional uint32 protection_scheme = 9; + + // Optional. For media using key rotation, this represents the duration + // of each crypto period in seconds. + optional uint32 crypto_period_seconds = 10; + + // Type of PSSH. Required if not SINGLE. + optional Type type = 11 [default = SINGLE]; + + // Key sequence for Widevine-managed keys. Optional. + optional uint32 key_sequence = 12; + + // Group identifiers for all groups to which the content belongs. This can + // be used to deliver licenses to unlock multiple titles / channels. + // Optional, and may only be present in ENTITLEMENT and ENTITLED_KEY PSSHs, and + // not in conjunction with key_ids. + repeated bytes group_ids = 13; + + // Copy/copies of the content key used to decrypt the media stream in which + // the PSSH box is embedded, each wrapped with a different entitlement key. + // May also contain sub-licenses to support devices with OEMCrypto 13 or + // older. May be repeated if using group entitlement keys. Present only in + // PSSHs of type ENTITLED_KEY. + repeated EntitledKey entitled_keys = 14; + + // Video feature identifier, which is used in conjunction with |content_id| + // to determine the set of keys to be returned in the license. Cannot be + // present in conjunction with |key_ids|. + // Current values are "HDR". + optional string video_feature = 15; + + //////////////////////////// Deprecated Fields //////////////////////////// + enum Algorithm { + UNENCRYPTED = 0; + AESCTR = 1; + }; + optional Algorithm algorithm = 1 [deprecated = true]; + + // Content provider name. + optional string provider = 3 [deprecated = true]; + + // Track type. Acceptable values are SD, HD and AUDIO. Used to + // differentiate content keys used by an asset. + optional string track_type = 5 [deprecated = true]; + + // The name of a registered policy to be used for this asset. + optional string policy = 6 [deprecated = true]; + + // Optional protected context for group content. The grouped_license is a + // serialized SignedMessage. + optional bytes grouped_license = 8 [deprecated = true]; +} + +// File Hashes for Verified Media Path (VMP) support. +message FileHashes { + message Signature { + optional string filename = 1; + optional bool test_signing = 2; //0 - release, 1 - testing + optional bytes SHA512Hash = 3; + optional bool main_exe = 4; //0 for dlls, 1 for exe, this is field 3 in file + optional bytes signature = 5; + } + optional bytes signer = 1; + repeated Signature signatures = 2; +} diff --git a/scripts/pywidevine/pywidevine/license_protocol_pb2.py b/scripts/pywidevine/pywidevine/license_protocol_pb2.py new file mode 100644 index 0000000..92ae262 --- /dev/null +++ b/scripts/pywidevine/pywidevine/license_protocol_pb2.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: license_protocol.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16license_protocol.proto\x12\x1bpywidevine_license_protocol\"\xbd\x01\n\x15LicenseIdentification\x12\x12\n\nrequest_id\x18\x01 \x01(\x0c\x12\x12\n\nsession_id\x18\x02 \x01(\x0c\x12\x13\n\x0bpurchase_id\x18\x03 \x01(\x0c\x12\x36\n\x04type\x18\x04 \x01(\x0e\x32(.pywidevine_license_protocol.LicenseType\x12\x0f\n\x07version\x18\x05 \x01(\x05\x12\x1e\n\x16provider_session_token\x18\x06 \x01(\x0c\"\xf1\x18\n\x07License\x12>\n\x02id\x18\x01 \x01(\x0b\x32\x32.pywidevine_license_protocol.LicenseIdentification\x12;\n\x06policy\x18\x02 \x01(\x0b\x32+.pywidevine_license_protocol.License.Policy\x12>\n\x03key\x18\x03 \x03(\x0b\x32\x31.pywidevine_license_protocol.License.KeyContainer\x12\x1a\n\x12license_start_time\x18\x04 \x01(\x03\x12*\n\x1bremote_attestation_verified\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1d\n\x15provider_client_token\x18\x06 \x01(\x0c\x12\x19\n\x11protection_scheme\x18\x07 \x01(\r\x12\x17\n\x0fsrm_requirement\x18\x08 \x01(\x0c\x12\x12\n\nsrm_update\x18\t \x01(\x0c\x12w\n\x1cplatform_verification_status\x18\n \x01(\x0e\x32\x37.pywidevine_license_protocol.PlatformVerificationStatus:\x18PLATFORM_NO_VERIFICATION\x12\x11\n\tgroup_ids\x18\x0b \x03(\x0c\x1a\xae\x04\n\x06Policy\x12\x17\n\x08\x63\x61n_play\x18\x01 \x01(\x08:\x05\x66\x61lse\x12\x1a\n\x0b\x63\x61n_persist\x18\x02 \x01(\x08:\x05\x66\x61lse\x12\x18\n\tcan_renew\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\"\n\x17rental_duration_seconds\x18\x04 \x01(\x03:\x01\x30\x12$\n\x19playback_duration_seconds\x18\x05 \x01(\x03:\x01\x30\x12#\n\x18license_duration_seconds\x18\x06 \x01(\x03:\x01\x30\x12,\n!renewal_recovery_duration_seconds\x18\x07 \x01(\x03:\x01\x30\x12\x1a\n\x12renewal_server_url\x18\x08 \x01(\t\x12 \n\x15renewal_delay_seconds\x18\t \x01(\x03:\x01\x30\x12)\n\x1erenewal_retry_interval_seconds\x18\n \x01(\x03:\x01\x30\x12\x1f\n\x10renew_with_usage\x18\x0b \x01(\x08:\x05\x66\x61lse\x12\'\n\x18\x61lways_include_client_id\x18\x0c \x01(\x08:\x05\x66\x61lse\x12*\n\x1fplay_start_grace_period_seconds\x18\r \x01(\x03:\x01\x30\x12-\n\x1esoft_enforce_playback_duration\x18\x0e \x01(\x08:\x05\x66\x61lse\x12*\n\x1csoft_enforce_rental_duration\x18\x0f \x01(\x08:\x04true\x1a\xbc\x10\n\x0cKeyContainer\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\n\n\x02iv\x18\x02 \x01(\x0c\x12\x0b\n\x03key\x18\x03 \x01(\x0c\x12G\n\x04type\x18\x04 \x01(\x0e\x32\x39.pywidevine_license_protocol.License.KeyContainer.KeyType\x12`\n\x05level\x18\x05 \x01(\x0e\x32?.pywidevine_license_protocol.License.KeyContainer.SecurityLevel:\x10SW_SECURE_CRYPTO\x12_\n\x13required_protection\x18\x06 \x01(\x0b\x32\x42.pywidevine_license_protocol.License.KeyContainer.OutputProtection\x12`\n\x14requested_protection\x18\x07 \x01(\x0b\x32\x42.pywidevine_license_protocol.License.KeyContainer.OutputProtection\x12Q\n\x0bkey_control\x18\x08 \x01(\x0b\x32<.pywidevine_license_protocol.License.KeyContainer.KeyControl\x12y\n operator_session_key_permissions\x18\t \x01(\x0b\x32O.pywidevine_license_protocol.License.KeyContainer.OperatorSessionKeyPermissions\x12q\n\x1cvideo_resolution_constraints\x18\n \x03(\x0b\x32K.pywidevine_license_protocol.License.KeyContainer.VideoResolutionConstraint\x12(\n\x19\x61nti_rollback_usage_table\x18\x0b \x01(\x08:\x05\x66\x61lse\x12\x13\n\x0btrack_label\x18\x0c \x01(\t\x1a\x33\n\nKeyControl\x12\x19\n\x11key_control_block\x18\x01 \x01(\x0c\x12\n\n\x02iv\x18\x02 \x01(\x0c\x1a\x9c\x05\n\x10OutputProtection\x12`\n\x04hdcp\x18\x01 \x01(\x0e\x32G.pywidevine_license_protocol.License.KeyContainer.OutputProtection.HDCP:\tHDCP_NONE\x12\x66\n\ncgms_flags\x18\x02 \x01(\x0e\x32G.pywidevine_license_protocol.License.KeyContainer.OutputProtection.CGMS:\tCGMS_NONE\x12y\n\rhdcp_srm_rule\x18\x03 \x01(\x0e\x32N.pywidevine_license_protocol.License.KeyContainer.OutputProtection.HdcpSrmRule:\x12HDCP_SRM_RULE_NONE\x12$\n\x15\x64isable_analog_output\x18\x04 \x01(\x08:\x05\x66\x61lse\x12%\n\x16\x64isable_digital_output\x18\x05 \x01(\x08:\x05\x66\x61lse\"y\n\x04HDCP\x12\r\n\tHDCP_NONE\x10\x00\x12\x0b\n\x07HDCP_V1\x10\x01\x12\x0b\n\x07HDCP_V2\x10\x02\x12\r\n\tHDCP_V2_1\x10\x03\x12\r\n\tHDCP_V2_2\x10\x04\x12\r\n\tHDCP_V2_3\x10\x05\x12\x1b\n\x16HDCP_NO_DIGITAL_OUTPUT\x10\xff\x01\"C\n\x04\x43GMS\x12\r\n\tCGMS_NONE\x10*\x12\r\n\tCOPY_FREE\x10\x00\x12\r\n\tCOPY_ONCE\x10\x02\x12\x0e\n\nCOPY_NEVER\x10\x03\"6\n\x0bHdcpSrmRule\x12\x16\n\x12HDCP_SRM_RULE_NONE\x10\x00\x12\x0f\n\x0b\x43URRENT_SRM\x10\x01\x1a\xba\x01\n\x19VideoResolutionConstraint\x12\x1d\n\x15min_resolution_pixels\x18\x01 \x01(\r\x12\x1d\n\x15max_resolution_pixels\x18\x02 \x01(\r\x12_\n\x13required_protection\x18\x03 \x01(\x0b\x32\x42.pywidevine_license_protocol.License.KeyContainer.OutputProtection\x1a\x9d\x01\n\x1dOperatorSessionKeyPermissions\x12\x1c\n\rallow_encrypt\x18\x01 \x01(\x08:\x05\x66\x61lse\x12\x1c\n\rallow_decrypt\x18\x02 \x01(\x08:\x05\x66\x61lse\x12\x19\n\nallow_sign\x18\x03 \x01(\x08:\x05\x66\x61lse\x12%\n\x16\x61llow_signature_verify\x18\x04 \x01(\x08:\x05\x66\x61lse\"l\n\x07KeyType\x12\x0b\n\x07SIGNING\x10\x01\x12\x0b\n\x07\x43ONTENT\x10\x02\x12\x0f\n\x0bKEY_CONTROL\x10\x03\x12\x14\n\x10OPERATOR_SESSION\x10\x04\x12\x0f\n\x0b\x45NTITLEMENT\x10\x05\x12\x0f\n\x0bOEM_CONTENT\x10\x06\"z\n\rSecurityLevel\x12\x14\n\x10SW_SECURE_CRYPTO\x10\x01\x12\x14\n\x10SW_SECURE_DECODE\x10\x02\x12\x14\n\x10HW_SECURE_CRYPTO\x10\x03\x12\x14\n\x10HW_SECURE_DECODE\x10\x04\x12\x11\n\rHW_SECURE_ALL\x10\x05\"\xbd\r\n\x0eLicenseRequest\x12\x44\n\tclient_id\x18\x01 \x01(\x0b\x32\x31.pywidevine_license_protocol.ClientIdentification\x12U\n\ncontent_id\x18\x02 \x01(\x0b\x32\x41.pywidevine_license_protocol.LicenseRequest.ContentIdentification\x12\x45\n\x04type\x18\x03 \x01(\x0e\x32\x37.pywidevine_license_protocol.LicenseRequest.RequestType\x12\x14\n\x0crequest_time\x18\x04 \x01(\x03\x12$\n\x1ckey_control_nonce_deprecated\x18\x05 \x01(\x0c\x12S\n\x10protocol_version\x18\x06 \x01(\x0e\x32,.pywidevine_license_protocol.ProtocolVersion:\x0bVERSION_2_0\x12\x19\n\x11key_control_nonce\x18\x07 \x01(\r\x12W\n\x13\x65ncrypted_client_id\x18\x08 \x01(\x0b\x32:.pywidevine_license_protocol.EncryptedClientIdentification\x1a\x8f\t\n\x15\x43ontentIdentification\x12p\n\x12widevine_pssh_data\x18\x01 \x01(\x0b\x32R.pywidevine_license_protocol.LicenseRequest.ContentIdentification.WidevinePsshDataH\x00\x12\x62\n\x0bwebm_key_id\x18\x02 \x01(\x0b\x32K.pywidevine_license_protocol.LicenseRequest.ContentIdentification.WebmKeyIdH\x00\x12m\n\x10\x65xisting_license\x18\x03 \x01(\x0b\x32Q.pywidevine_license_protocol.LicenseRequest.ContentIdentification.ExistingLicenseH\x00\x12_\n\tinit_data\x18\x04 \x01(\x0b\x32J.pywidevine_license_protocol.LicenseRequest.ContentIdentification.InitDataH\x00\x1ay\n\x10WidevinePsshData\x12\x11\n\tpssh_data\x18\x01 \x03(\x0c\x12>\n\x0clicense_type\x18\x02 \x01(\x0e\x32(.pywidevine_license_protocol.LicenseType\x12\x12\n\nrequest_id\x18\x03 \x01(\x0c\x1ao\n\tWebmKeyId\x12\x0e\n\x06header\x18\x01 \x01(\x0c\x12>\n\x0clicense_type\x18\x02 \x01(\x0e\x32(.pywidevine_license_protocol.LicenseType\x12\x12\n\nrequest_id\x18\x03 \x01(\x0c\x1a\xbe\x01\n\x0f\x45xistingLicense\x12\x46\n\nlicense_id\x18\x01 \x01(\x0b\x32\x32.pywidevine_license_protocol.LicenseIdentification\x12\x1d\n\x15seconds_since_started\x18\x02 \x01(\x03\x12!\n\x19seconds_since_last_played\x18\x03 \x01(\x03\x12!\n\x19session_usage_table_entry\x18\x04 \x01(\x0c\x1a\x8c\x02\n\x08InitData\x12u\n\x0einit_data_type\x18\x01 \x01(\x0e\x32W.pywidevine_license_protocol.LicenseRequest.ContentIdentification.InitData.InitDataType:\x04\x43\x45NC\x12\x11\n\tinit_data\x18\x02 \x01(\x0c\x12>\n\x0clicense_type\x18\x03 \x01(\x0e\x32(.pywidevine_license_protocol.LicenseType\x12\x12\n\nrequest_id\x18\x04 \x01(\x0c\"\"\n\x0cInitDataType\x12\x08\n\x04\x43\x45NC\x10\x01\x12\x08\n\x04WEBM\x10\x02\x42\x14\n\x12\x63ontent_id_variant\"0\n\x0bRequestType\x12\x07\n\x03NEW\x10\x01\x12\x0b\n\x07RENEWAL\x10\x02\x12\x0b\n\x07RELEASE\x10\x03\"\xf3\x01\n\nMetricData\x12\x12\n\nstage_name\x18\x01 \x01(\t\x12\x46\n\x0bmetric_data\x18\x02 \x03(\x0b\x32\x31.pywidevine_license_protocol.MetricData.TypeValue\x1a_\n\tTypeValue\x12@\n\x04type\x18\x01 \x01(\x0e\x32\x32.pywidevine_license_protocol.MetricData.MetricType\x12\x10\n\x05value\x18\x02 \x01(\x03:\x01\x30\"(\n\nMetricType\x12\x0b\n\x07LATENCY\x10\x01\x12\r\n\tTIMESTAMP\x10\x02\"K\n\x0bVersionInfo\x12\x1b\n\x13license_sdk_version\x18\x01 \x01(\t\x12\x1f\n\x17license_service_version\x18\x02 \x01(\t\"\xf6\x05\n\rSignedMessage\x12\x44\n\x04type\x18\x01 \x01(\x0e\x32\x36.pywidevine_license_protocol.SignedMessage.MessageType\x12\x0b\n\x03msg\x18\x02 \x01(\x0c\x12\x11\n\tsignature\x18\x03 \x01(\x0c\x12\x13\n\x0bsession_key\x18\x04 \x01(\x0c\x12\x1a\n\x12remote_attestation\x18\x05 \x01(\x0c\x12<\n\x0bmetric_data\x18\x06 \x03(\x0b\x32\'.pywidevine_license_protocol.MetricData\x12\x46\n\x14service_version_info\x18\x07 \x01(\x0b\x32(.pywidevine_license_protocol.VersionInfo\x12\x64\n\x10session_key_type\x18\x08 \x01(\x0e\x32\x39.pywidevine_license_protocol.SignedMessage.SessionKeyType:\x0fWRAPPED_AES_KEY\x12\x1e\n\x16oemcrypto_core_message\x18\t \x01(\x0c\"\xec\x01\n\x0bMessageType\x12\x13\n\x0fLICENSE_REQUEST\x10\x01\x12\x0b\n\x07LICENSE\x10\x02\x12\x12\n\x0e\x45RROR_RESPONSE\x10\x03\x12\x1f\n\x1bSERVICE_CERTIFICATE_REQUEST\x10\x04\x12\x17\n\x13SERVICE_CERTIFICATE\x10\x05\x12\x0f\n\x0bSUB_LICENSE\x10\x06\x12\x17\n\x13\x43\x41S_LICENSE_REQUEST\x10\x07\x12\x0f\n\x0b\x43\x41S_LICENSE\x10\x08\x12\x1c\n\x18\x45XTERNAL_LICENSE_REQUEST\x10\t\x12\x14\n\x10\x45XTERNAL_LICENSE\x10\n\"S\n\x0eSessionKeyType\x12\r\n\tUNDEFINED\x10\x00\x12\x13\n\x0fWRAPPED_AES_KEY\x10\x01\x12\x1d\n\x19\x45PHERMERAL_ECC_PUBLIC_KEY\x10\x02\"\xc7\x0e\n\x14\x43lientIdentification\x12Q\n\x04type\x18\x01 \x01(\x0e\x32;.pywidevine_license_protocol.ClientIdentification.TokenType:\x06KEYBOX\x12\r\n\x05token\x18\x02 \x01(\x0c\x12P\n\x0b\x63lient_info\x18\x03 \x03(\x0b\x32;.pywidevine_license_protocol.ClientIdentification.NameValue\x12\x1d\n\x15provider_client_token\x18\x04 \x01(\x0c\x12\x17\n\x0flicense_counter\x18\x05 \x01(\r\x12\x61\n\x13\x63lient_capabilities\x18\x06 \x01(\x0b\x32\x44.pywidevine_license_protocol.ClientIdentification.ClientCapabilities\x12\x10\n\x08vmp_data\x18\x07 \x01(\x0c\x12_\n\x12\x64\x65vice_credentials\x18\x08 \x03(\x0b\x32\x43.pywidevine_license_protocol.ClientIdentification.ClientCredentials\x1a(\n\tNameValue\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\x1a\xd6\x08\n\x12\x43lientCapabilities\x12\x1b\n\x0c\x63lient_token\x18\x01 \x01(\x08:\x05\x66\x61lse\x12\x1c\n\rsession_token\x18\x02 \x01(\x08:\x05\x66\x61lse\x12+\n\x1cvideo_resolution_constraints\x18\x03 \x01(\x08:\x05\x66\x61lse\x12u\n\x10max_hdcp_version\x18\x04 \x01(\x0e\x32P.pywidevine_license_protocol.ClientIdentification.ClientCapabilities.HdcpVersion:\tHDCP_NONE\x12\x1e\n\x16oem_crypto_api_version\x18\x05 \x01(\r\x12(\n\x19\x61nti_rollback_usage_table\x18\x06 \x01(\x08:\x05\x66\x61lse\x12\x13\n\x0bsrm_version\x18\x07 \x01(\r\x12\x1d\n\x0e\x63\x61n_update_srm\x18\x08 \x01(\x08:\x05\x66\x61lse\x12\x7f\n\x1esupported_certificate_key_type\x18\t \x03(\x0e\x32W.pywidevine_license_protocol.ClientIdentification.ClientCapabilities.CertificateKeyType\x12\x98\x01\n\x1a\x61nalog_output_capabilities\x18\n \x01(\x0e\x32].pywidevine_license_protocol.ClientIdentification.ClientCapabilities.AnalogOutputCapabilities:\x15\x41NALOG_OUTPUT_UNKNOWN\x12(\n\x19\x63\x61n_disable_analog_output\x18\x0b \x01(\x08:\x05\x66\x61lse\x12\x1f\n\x14resource_rating_tier\x18\x0c \x01(\r:\x01\x30\"\x80\x01\n\x0bHdcpVersion\x12\r\n\tHDCP_NONE\x10\x00\x12\x0b\n\x07HDCP_V1\x10\x01\x12\x0b\n\x07HDCP_V2\x10\x02\x12\r\n\tHDCP_V2_1\x10\x03\x12\r\n\tHDCP_V2_2\x10\x04\x12\r\n\tHDCP_V2_3\x10\x05\x12\x1b\n\x16HDCP_NO_DIGITAL_OUTPUT\x10\xff\x01\"i\n\x12\x43\x65rtificateKeyType\x12\x0c\n\x08RSA_2048\x10\x00\x12\x0c\n\x08RSA_3072\x10\x01\x12\x11\n\rECC_SECP256R1\x10\x02\x12\x11\n\rECC_SECP384R1\x10\x03\x12\x11\n\rECC_SECP521R1\x10\x04\"\x8d\x01\n\x18\x41nalogOutputCapabilities\x12\x19\n\x15\x41NALOG_OUTPUT_UNKNOWN\x10\x00\x12\x16\n\x12\x41NALOG_OUTPUT_NONE\x10\x01\x12\x1b\n\x17\x41NALOG_OUTPUT_SUPPORTED\x10\x02\x12!\n\x1d\x41NALOG_OUTPUT_SUPPORTS_CGMS_A\x10\x03\x1au\n\x11\x43lientCredentials\x12Q\n\x04type\x18\x01 \x01(\x0e\x32;.pywidevine_license_protocol.ClientIdentification.TokenType:\x06KEYBOX\x12\r\n\x05token\x18\x02 \x01(\x0c\"s\n\tTokenType\x12\n\n\x06KEYBOX\x10\x00\x12\x1a\n\x16\x44RM_DEVICE_CERTIFICATE\x10\x01\x12\"\n\x1eREMOTE_ATTESTATION_CERTIFICATE\x10\x02\x12\x1a\n\x16OEM_DEVICE_CERTIFICATE\x10\x03\"\xbb\x01\n\x1d\x45ncryptedClientIdentification\x12\x13\n\x0bprovider_id\x18\x01 \x01(\t\x12)\n!service_certificate_serial_number\x18\x02 \x01(\x0c\x12\x1b\n\x13\x65ncrypted_client_id\x18\x03 \x01(\x0c\x12\x1e\n\x16\x65ncrypted_client_id_iv\x18\x04 \x01(\x0c\x12\x1d\n\x15\x65ncrypted_privacy_key\x18\x05 \x01(\x0c\"\xba\x07\n\x0e\x44rmCertificate\x12>\n\x04type\x18\x01 \x01(\x0e\x32\x30.pywidevine_license_protocol.DrmCertificate.Type\x12\x15\n\rserial_number\x18\x02 \x01(\x0c\x12\x1d\n\x15\x63reation_time_seconds\x18\x03 \x01(\r\x12\x1f\n\x17\x65xpiration_time_seconds\x18\x0c \x01(\r\x12\x12\n\npublic_key\x18\x04 \x01(\x0c\x12\x11\n\tsystem_id\x18\x05 \x01(\r\x12\"\n\x16test_device_deprecated\x18\x06 \x01(\x08\x42\x02\x18\x01\x12\x13\n\x0bprovider_id\x18\x07 \x01(\t\x12N\n\rservice_types\x18\x08 \x03(\x0e\x32\x37.pywidevine_license_protocol.DrmCertificate.ServiceType\x12M\n\talgorithm\x18\t \x01(\x0e\x32\x35.pywidevine_license_protocol.DrmCertificate.Algorithm:\x03RSA\x12\x0e\n\x06rot_id\x18\n \x01(\x0c\x12Q\n\x0e\x65ncryption_key\x18\x0b \x01(\x0b\x32\x39.pywidevine_license_protocol.DrmCertificate.EncryptionKey\x1ar\n\rEncryptionKey\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12M\n\talgorithm\x18\x02 \x01(\x0e\x32\x35.pywidevine_license_protocol.DrmCertificate.Algorithm:\x03RSA\"L\n\x04Type\x12\x08\n\x04ROOT\x10\x00\x12\x10\n\x0c\x44\x45VICE_MODEL\x10\x01\x12\n\n\x06\x44\x45VICE\x10\x02\x12\x0b\n\x07SERVICE\x10\x03\x12\x0f\n\x0bPROVISIONER\x10\x04\"\x86\x01\n\x0bServiceType\x12\x18\n\x14UNKNOWN_SERVICE_TYPE\x10\x00\x12\x16\n\x12LICENSE_SERVER_SDK\x10\x01\x12\x1c\n\x18LICENSE_SERVER_PROXY_SDK\x10\x02\x12\x14\n\x10PROVISIONING_SDK\x10\x03\x12\x11\n\rCAS_PROXY_SDK\x10\x04\"d\n\tAlgorithm\x12\x15\n\x11UNKNOWN_ALGORITHM\x10\x00\x12\x07\n\x03RSA\x10\x01\x12\x11\n\rECC_SECP256R1\x10\x02\x12\x11\n\rECC_SECP384R1\x10\x03\x12\x11\n\rECC_SECP521R1\x10\x04\"\xce\x01\n\x14SignedDrmCertificate\x12\x17\n\x0f\x64rm_certificate\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12\x41\n\x06signer\x18\x03 \x01(\x0b\x32\x31.pywidevine_license_protocol.SignedDrmCertificate\x12G\n\x0ehash_algorithm\x18\x04 \x01(\x0e\x32/.pywidevine_license_protocol.HashAlgorithmProto\"\xf6\x05\n\x10WidevinePsshData\x12\x0f\n\x07key_ids\x18\x02 \x03(\x0c\x12\x12\n\ncontent_id\x18\x04 \x01(\x0c\x12\x1b\n\x13\x63rypto_period_index\x18\x07 \x01(\r\x12\x19\n\x11protection_scheme\x18\t \x01(\r\x12\x1d\n\x15\x63rypto_period_seconds\x18\n \x01(\r\x12H\n\x04type\x18\x0b \x01(\x0e\x32\x32.pywidevine_license_protocol.WidevinePsshData.Type:\x06SINGLE\x12\x14\n\x0ckey_sequence\x18\x0c \x01(\r\x12\x11\n\tgroup_ids\x18\r \x03(\x0c\x12P\n\rentitled_keys\x18\x0e \x03(\x0b\x32\x39.pywidevine_license_protocol.WidevinePsshData.EntitledKey\x12\x15\n\rvideo_feature\x18\x0f \x01(\t\x12N\n\talgorithm\x18\x01 \x01(\x0e\x32\x37.pywidevine_license_protocol.WidevinePsshData.AlgorithmB\x02\x18\x01\x12\x14\n\x08provider\x18\x03 \x01(\tB\x02\x18\x01\x12\x16\n\ntrack_type\x18\x05 \x01(\tB\x02\x18\x01\x12\x12\n\x06policy\x18\x06 \x01(\tB\x02\x18\x01\x12\x1b\n\x0fgrouped_license\x18\x08 \x01(\x0c\x42\x02\x18\x01\x1az\n\x0b\x45ntitledKey\x12\x1a\n\x12\x65ntitlement_key_id\x18\x01 \x01(\x0c\x12\x0e\n\x06key_id\x18\x02 \x01(\x0c\x12\x0b\n\x03key\x18\x03 \x01(\x0c\x12\n\n\x02iv\x18\x04 \x01(\x0c\x12&\n\x1a\x65ntitlement_key_size_bytes\x18\x05 \x01(\r:\x02\x33\x32\"5\n\x04Type\x12\n\n\x06SINGLE\x10\x00\x12\x0f\n\x0b\x45NTITLEMENT\x10\x01\x12\x10\n\x0c\x45NTITLED_KEY\x10\x02\"(\n\tAlgorithm\x12\x0f\n\x0bUNENCRYPTED\x10\x00\x12\n\n\x06\x41\x45SCTR\x10\x01\"\xd1\x01\n\nFileHashes\x12\x0e\n\x06signer\x18\x01 \x01(\x0c\x12\x45\n\nsignatures\x18\x02 \x03(\x0b\x32\x31.pywidevine_license_protocol.FileHashes.Signature\x1al\n\tSignature\x12\x10\n\x08\x66ilename\x18\x01 \x01(\t\x12\x14\n\x0ctest_signing\x18\x02 \x01(\x08\x12\x12\n\nSHA512Hash\x18\x03 \x01(\x0c\x12\x10\n\x08main_exe\x18\x04 \x01(\x08\x12\x11\n\tsignature\x18\x05 \x01(\x0c*8\n\x0bLicenseType\x12\r\n\tSTREAMING\x10\x01\x12\x0b\n\x07OFFLINE\x10\x02\x12\r\n\tAUTOMATIC\x10\x03*\xd9\x01\n\x1aPlatformVerificationStatus\x12\x17\n\x13PLATFORM_UNVERIFIED\x10\x00\x12\x15\n\x11PLATFORM_TAMPERED\x10\x01\x12\x1e\n\x1aPLATFORM_SOFTWARE_VERIFIED\x10\x02\x12\x1e\n\x1aPLATFORM_HARDWARE_VERIFIED\x10\x03\x12\x1c\n\x18PLATFORM_NO_VERIFICATION\x10\x04\x12-\n)PLATFORM_SECURE_STORAGE_SOFTWARE_VERIFIED\x10\x05*D\n\x0fProtocolVersion\x12\x0f\n\x0bVERSION_2_0\x10\x14\x12\x0f\n\x0bVERSION_2_1\x10\x15\x12\x0f\n\x0bVERSION_2_2\x10\x16*\x86\x01\n\x12HashAlgorithmProto\x12\x1e\n\x1aHASH_ALGORITHM_UNSPECIFIED\x10\x00\x12\x18\n\x14HASH_ALGORITHM_SHA_1\x10\x01\x12\x1a\n\x16HASH_ALGORITHM_SHA_256\x10\x02\x12\x1a\n\x16HASH_ALGORITHM_SHA_384\x10\x03\x42$\n com.rlaphoenix.pywidevine.protosH\x03') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'license_protocol_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n com.rlaphoenix.pywidevine.protosH\003' + _globals['_DRMCERTIFICATE'].fields_by_name['test_device_deprecated']._options = None + _globals['_DRMCERTIFICATE'].fields_by_name['test_device_deprecated']._serialized_options = b'\030\001' + _globals['_WIDEVINEPSSHDATA'].fields_by_name['algorithm']._options = None + _globals['_WIDEVINEPSSHDATA'].fields_by_name['algorithm']._serialized_options = b'\030\001' + _globals['_WIDEVINEPSSHDATA'].fields_by_name['provider']._options = None + _globals['_WIDEVINEPSSHDATA'].fields_by_name['provider']._serialized_options = b'\030\001' + _globals['_WIDEVINEPSSHDATA'].fields_by_name['track_type']._options = None + _globals['_WIDEVINEPSSHDATA'].fields_by_name['track_type']._serialized_options = b'\030\001' + _globals['_WIDEVINEPSSHDATA'].fields_by_name['policy']._options = None + _globals['_WIDEVINEPSSHDATA'].fields_by_name['policy']._serialized_options = b'\030\001' + _globals['_WIDEVINEPSSHDATA'].fields_by_name['grouped_license']._options = None + _globals['_WIDEVINEPSSHDATA'].fields_by_name['grouped_license']._serialized_options = b'\030\001' + _globals['_LICENSETYPE']._serialized_start=10442 + _globals['_LICENSETYPE']._serialized_end=10498 + _globals['_PLATFORMVERIFICATIONSTATUS']._serialized_start=10501 + _globals['_PLATFORMVERIFICATIONSTATUS']._serialized_end=10718 + _globals['_PROTOCOLVERSION']._serialized_start=10720 + _globals['_PROTOCOLVERSION']._serialized_end=10788 + _globals['_HASHALGORITHMPROTO']._serialized_start=10791 + _globals['_HASHALGORITHMPROTO']._serialized_end=10925 + _globals['_LICENSEIDENTIFICATION']._serialized_start=56 + _globals['_LICENSEIDENTIFICATION']._serialized_end=245 + _globals['_LICENSE']._serialized_start=248 + _globals['_LICENSE']._serialized_end=3433 + _globals['_LICENSE_POLICY']._serialized_start=764 + _globals['_LICENSE_POLICY']._serialized_end=1322 + _globals['_LICENSE_KEYCONTAINER']._serialized_start=1325 + _globals['_LICENSE_KEYCONTAINER']._serialized_end=3433 + _globals['_LICENSE_KEYCONTAINER_KEYCONTROL']._serialized_start=2128 + _globals['_LICENSE_KEYCONTAINER_KEYCONTROL']._serialized_end=2179 + _globals['_LICENSE_KEYCONTAINER_OUTPUTPROTECTION']._serialized_start=2182 + _globals['_LICENSE_KEYCONTAINER_OUTPUTPROTECTION']._serialized_end=2850 + _globals['_LICENSE_KEYCONTAINER_OUTPUTPROTECTION_HDCP']._serialized_start=2604 + _globals['_LICENSE_KEYCONTAINER_OUTPUTPROTECTION_HDCP']._serialized_end=2725 + _globals['_LICENSE_KEYCONTAINER_OUTPUTPROTECTION_CGMS']._serialized_start=2727 + _globals['_LICENSE_KEYCONTAINER_OUTPUTPROTECTION_CGMS']._serialized_end=2794 + _globals['_LICENSE_KEYCONTAINER_OUTPUTPROTECTION_HDCPSRMRULE']._serialized_start=2796 + _globals['_LICENSE_KEYCONTAINER_OUTPUTPROTECTION_HDCPSRMRULE']._serialized_end=2850 + _globals['_LICENSE_KEYCONTAINER_VIDEORESOLUTIONCONSTRAINT']._serialized_start=2853 + _globals['_LICENSE_KEYCONTAINER_VIDEORESOLUTIONCONSTRAINT']._serialized_end=3039 + _globals['_LICENSE_KEYCONTAINER_OPERATORSESSIONKEYPERMISSIONS']._serialized_start=3042 + _globals['_LICENSE_KEYCONTAINER_OPERATORSESSIONKEYPERMISSIONS']._serialized_end=3199 + _globals['_LICENSE_KEYCONTAINER_KEYTYPE']._serialized_start=3201 + _globals['_LICENSE_KEYCONTAINER_KEYTYPE']._serialized_end=3309 + _globals['_LICENSE_KEYCONTAINER_SECURITYLEVEL']._serialized_start=3311 + _globals['_LICENSE_KEYCONTAINER_SECURITYLEVEL']._serialized_end=3433 + _globals['_LICENSEREQUEST']._serialized_start=3436 + _globals['_LICENSEREQUEST']._serialized_end=5161 + _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION']._serialized_start=3944 + _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION']._serialized_end=5111 + _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_WIDEVINEPSSHDATA']._serialized_start=4391 + _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_WIDEVINEPSSHDATA']._serialized_end=4512 + _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_WEBMKEYID']._serialized_start=4514 + _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_WEBMKEYID']._serialized_end=4625 + _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_EXISTINGLICENSE']._serialized_start=4628 + _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_EXISTINGLICENSE']._serialized_end=4818 + _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_INITDATA']._serialized_start=4821 + _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_INITDATA']._serialized_end=5089 + _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_INITDATA_INITDATATYPE']._serialized_start=5055 + _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_INITDATA_INITDATATYPE']._serialized_end=5089 + _globals['_LICENSEREQUEST_REQUESTTYPE']._serialized_start=5113 + _globals['_LICENSEREQUEST_REQUESTTYPE']._serialized_end=5161 + _globals['_METRICDATA']._serialized_start=5164 + _globals['_METRICDATA']._serialized_end=5407 + _globals['_METRICDATA_TYPEVALUE']._serialized_start=5270 + _globals['_METRICDATA_TYPEVALUE']._serialized_end=5365 + _globals['_METRICDATA_METRICTYPE']._serialized_start=5367 + _globals['_METRICDATA_METRICTYPE']._serialized_end=5407 + _globals['_VERSIONINFO']._serialized_start=5409 + _globals['_VERSIONINFO']._serialized_end=5484 + _globals['_SIGNEDMESSAGE']._serialized_start=5487 + _globals['_SIGNEDMESSAGE']._serialized_end=6245 + _globals['_SIGNEDMESSAGE_MESSAGETYPE']._serialized_start=5924 + _globals['_SIGNEDMESSAGE_MESSAGETYPE']._serialized_end=6160 + _globals['_SIGNEDMESSAGE_SESSIONKEYTYPE']._serialized_start=6162 + _globals['_SIGNEDMESSAGE_SESSIONKEYTYPE']._serialized_end=6245 + _globals['_CLIENTIDENTIFICATION']._serialized_start=6248 + _globals['_CLIENTIDENTIFICATION']._serialized_end=8111 + _globals['_CLIENTIDENTIFICATION_NAMEVALUE']._serialized_start=6722 + _globals['_CLIENTIDENTIFICATION_NAMEVALUE']._serialized_end=6762 + _globals['_CLIENTIDENTIFICATION_CLIENTCAPABILITIES']._serialized_start=6765 + _globals['_CLIENTIDENTIFICATION_CLIENTCAPABILITIES']._serialized_end=7875 + _globals['_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_HDCPVERSION']._serialized_start=7496 + _globals['_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_HDCPVERSION']._serialized_end=7624 + _globals['_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_CERTIFICATEKEYTYPE']._serialized_start=7626 + _globals['_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_CERTIFICATEKEYTYPE']._serialized_end=7731 + _globals['_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_ANALOGOUTPUTCAPABILITIES']._serialized_start=7734 + _globals['_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_ANALOGOUTPUTCAPABILITIES']._serialized_end=7875 + _globals['_CLIENTIDENTIFICATION_CLIENTCREDENTIALS']._serialized_start=7877 + _globals['_CLIENTIDENTIFICATION_CLIENTCREDENTIALS']._serialized_end=7994 + _globals['_CLIENTIDENTIFICATION_TOKENTYPE']._serialized_start=7996 + _globals['_CLIENTIDENTIFICATION_TOKENTYPE']._serialized_end=8111 + _globals['_ENCRYPTEDCLIENTIDENTIFICATION']._serialized_start=8114 + _globals['_ENCRYPTEDCLIENTIDENTIFICATION']._serialized_end=8301 + _globals['_DRMCERTIFICATE']._serialized_start=8304 + _globals['_DRMCERTIFICATE']._serialized_end=9258 + _globals['_DRMCERTIFICATE_ENCRYPTIONKEY']._serialized_start=8827 + _globals['_DRMCERTIFICATE_ENCRYPTIONKEY']._serialized_end=8941 + _globals['_DRMCERTIFICATE_TYPE']._serialized_start=8943 + _globals['_DRMCERTIFICATE_TYPE']._serialized_end=9019 + _globals['_DRMCERTIFICATE_SERVICETYPE']._serialized_start=9022 + _globals['_DRMCERTIFICATE_SERVICETYPE']._serialized_end=9156 + _globals['_DRMCERTIFICATE_ALGORITHM']._serialized_start=9158 + _globals['_DRMCERTIFICATE_ALGORITHM']._serialized_end=9258 + _globals['_SIGNEDDRMCERTIFICATE']._serialized_start=9261 + _globals['_SIGNEDDRMCERTIFICATE']._serialized_end=9467 + _globals['_WIDEVINEPSSHDATA']._serialized_start=9470 + _globals['_WIDEVINEPSSHDATA']._serialized_end=10228 + _globals['_WIDEVINEPSSHDATA_ENTITLEDKEY']._serialized_start=10009 + _globals['_WIDEVINEPSSHDATA_ENTITLEDKEY']._serialized_end=10131 + _globals['_WIDEVINEPSSHDATA_TYPE']._serialized_start=10133 + _globals['_WIDEVINEPSSHDATA_TYPE']._serialized_end=10186 + _globals['_WIDEVINEPSSHDATA_ALGORITHM']._serialized_start=10188 + _globals['_WIDEVINEPSSHDATA_ALGORITHM']._serialized_end=10228 + _globals['_FILEHASHES']._serialized_start=10231 + _globals['_FILEHASHES']._serialized_end=10440 + _globals['_FILEHASHES_SIGNATURE']._serialized_start=10332 + _globals['_FILEHASHES_SIGNATURE']._serialized_end=10440 +# @@protoc_insertion_point(module_scope) diff --git a/scripts/pywidevine/pywidevine/license_protocol_pb2.pyi b/scripts/pywidevine/pywidevine/license_protocol_pb2.pyi new file mode 100644 index 0000000..44f543e --- /dev/null +++ b/scripts/pywidevine/pywidevine/license_protocol_pb2.pyi @@ -0,0 +1,607 @@ +# mypy: ignore-errors + +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union + +AUTOMATIC: LicenseType +DESCRIPTOR: _descriptor.FileDescriptor +HASH_ALGORITHM_SHA_1: HashAlgorithmProto +HASH_ALGORITHM_SHA_256: HashAlgorithmProto +HASH_ALGORITHM_SHA_384: HashAlgorithmProto +HASH_ALGORITHM_UNSPECIFIED: HashAlgorithmProto +OFFLINE: LicenseType +PLATFORM_HARDWARE_VERIFIED: PlatformVerificationStatus +PLATFORM_NO_VERIFICATION: PlatformVerificationStatus +PLATFORM_SECURE_STORAGE_SOFTWARE_VERIFIED: PlatformVerificationStatus +PLATFORM_SOFTWARE_VERIFIED: PlatformVerificationStatus +PLATFORM_TAMPERED: PlatformVerificationStatus +PLATFORM_UNVERIFIED: PlatformVerificationStatus +STREAMING: LicenseType +VERSION_2_0: ProtocolVersion +VERSION_2_1: ProtocolVersion +VERSION_2_2: ProtocolVersion + +class ClientIdentification(_message.Message): + __slots__ = ["client_capabilities", "client_info", "device_credentials", "license_counter", "provider_client_token", "token", "type", "vmp_data"] + class TokenType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + class ClientCapabilities(_message.Message): + __slots__ = ["analog_output_capabilities", "anti_rollback_usage_table", "can_disable_analog_output", "can_update_srm", "client_token", "max_hdcp_version", "oem_crypto_api_version", "resource_rating_tier", "session_token", "srm_version", "supported_certificate_key_type", "video_resolution_constraints"] + class AnalogOutputCapabilities(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + class CertificateKeyType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + class HdcpVersion(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + ANALOG_OUTPUT_CAPABILITIES_FIELD_NUMBER: _ClassVar[int] + ANALOG_OUTPUT_NONE: ClientIdentification.ClientCapabilities.AnalogOutputCapabilities + ANALOG_OUTPUT_SUPPORTED: ClientIdentification.ClientCapabilities.AnalogOutputCapabilities + ANALOG_OUTPUT_SUPPORTS_CGMS_A: ClientIdentification.ClientCapabilities.AnalogOutputCapabilities + ANALOG_OUTPUT_UNKNOWN: ClientIdentification.ClientCapabilities.AnalogOutputCapabilities + ANTI_ROLLBACK_USAGE_TABLE_FIELD_NUMBER: _ClassVar[int] + CAN_DISABLE_ANALOG_OUTPUT_FIELD_NUMBER: _ClassVar[int] + CAN_UPDATE_SRM_FIELD_NUMBER: _ClassVar[int] + CLIENT_TOKEN_FIELD_NUMBER: _ClassVar[int] + ECC_SECP256R1: ClientIdentification.ClientCapabilities.CertificateKeyType + ECC_SECP384R1: ClientIdentification.ClientCapabilities.CertificateKeyType + ECC_SECP521R1: ClientIdentification.ClientCapabilities.CertificateKeyType + HDCP_NONE: ClientIdentification.ClientCapabilities.HdcpVersion + HDCP_NO_DIGITAL_OUTPUT: ClientIdentification.ClientCapabilities.HdcpVersion + HDCP_V1: ClientIdentification.ClientCapabilities.HdcpVersion + HDCP_V2: ClientIdentification.ClientCapabilities.HdcpVersion + HDCP_V2_1: ClientIdentification.ClientCapabilities.HdcpVersion + HDCP_V2_2: ClientIdentification.ClientCapabilities.HdcpVersion + HDCP_V2_3: ClientIdentification.ClientCapabilities.HdcpVersion + MAX_HDCP_VERSION_FIELD_NUMBER: _ClassVar[int] + OEM_CRYPTO_API_VERSION_FIELD_NUMBER: _ClassVar[int] + RESOURCE_RATING_TIER_FIELD_NUMBER: _ClassVar[int] + RSA_2048: ClientIdentification.ClientCapabilities.CertificateKeyType + RSA_3072: ClientIdentification.ClientCapabilities.CertificateKeyType + SESSION_TOKEN_FIELD_NUMBER: _ClassVar[int] + SRM_VERSION_FIELD_NUMBER: _ClassVar[int] + SUPPORTED_CERTIFICATE_KEY_TYPE_FIELD_NUMBER: _ClassVar[int] + VIDEO_RESOLUTION_CONSTRAINTS_FIELD_NUMBER: _ClassVar[int] + analog_output_capabilities: ClientIdentification.ClientCapabilities.AnalogOutputCapabilities + anti_rollback_usage_table: bool + can_disable_analog_output: bool + can_update_srm: bool + client_token: bool + max_hdcp_version: ClientIdentification.ClientCapabilities.HdcpVersion + oem_crypto_api_version: int + resource_rating_tier: int + session_token: bool + srm_version: int + supported_certificate_key_type: _containers.RepeatedScalarFieldContainer[ClientIdentification.ClientCapabilities.CertificateKeyType] + video_resolution_constraints: bool + def __init__(self, client_token: bool = ..., session_token: bool = ..., video_resolution_constraints: bool = ..., max_hdcp_version: _Optional[_Union[ClientIdentification.ClientCapabilities.HdcpVersion, str]] = ..., oem_crypto_api_version: _Optional[int] = ..., anti_rollback_usage_table: bool = ..., srm_version: _Optional[int] = ..., can_update_srm: bool = ..., supported_certificate_key_type: _Optional[_Iterable[_Union[ClientIdentification.ClientCapabilities.CertificateKeyType, str]]] = ..., analog_output_capabilities: _Optional[_Union[ClientIdentification.ClientCapabilities.AnalogOutputCapabilities, str]] = ..., can_disable_analog_output: bool = ..., resource_rating_tier: _Optional[int] = ...) -> None: ... + class ClientCredentials(_message.Message): + __slots__ = ["token", "type"] + TOKEN_FIELD_NUMBER: _ClassVar[int] + TYPE_FIELD_NUMBER: _ClassVar[int] + token: bytes + type: ClientIdentification.TokenType + def __init__(self, type: _Optional[_Union[ClientIdentification.TokenType, str]] = ..., token: _Optional[bytes] = ...) -> None: ... + class NameValue(_message.Message): + __slots__ = ["name", "value"] + NAME_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + name: str + value: str + def __init__(self, name: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ... + CLIENT_CAPABILITIES_FIELD_NUMBER: _ClassVar[int] + CLIENT_INFO_FIELD_NUMBER: _ClassVar[int] + DEVICE_CREDENTIALS_FIELD_NUMBER: _ClassVar[int] + DRM_DEVICE_CERTIFICATE: ClientIdentification.TokenType + KEYBOX: ClientIdentification.TokenType + LICENSE_COUNTER_FIELD_NUMBER: _ClassVar[int] + OEM_DEVICE_CERTIFICATE: ClientIdentification.TokenType + PROVIDER_CLIENT_TOKEN_FIELD_NUMBER: _ClassVar[int] + REMOTE_ATTESTATION_CERTIFICATE: ClientIdentification.TokenType + TOKEN_FIELD_NUMBER: _ClassVar[int] + TYPE_FIELD_NUMBER: _ClassVar[int] + VMP_DATA_FIELD_NUMBER: _ClassVar[int] + client_capabilities: ClientIdentification.ClientCapabilities + client_info: _containers.RepeatedCompositeFieldContainer[ClientIdentification.NameValue] + device_credentials: _containers.RepeatedCompositeFieldContainer[ClientIdentification.ClientCredentials] + license_counter: int + provider_client_token: bytes + token: bytes + type: ClientIdentification.TokenType + vmp_data: bytes + def __init__(self, type: _Optional[_Union[ClientIdentification.TokenType, str]] = ..., token: _Optional[bytes] = ..., client_info: _Optional[_Iterable[_Union[ClientIdentification.NameValue, _Mapping]]] = ..., provider_client_token: _Optional[bytes] = ..., license_counter: _Optional[int] = ..., client_capabilities: _Optional[_Union[ClientIdentification.ClientCapabilities, _Mapping]] = ..., vmp_data: _Optional[bytes] = ..., device_credentials: _Optional[_Iterable[_Union[ClientIdentification.ClientCredentials, _Mapping]]] = ...) -> None: ... + +class DrmCertificate(_message.Message): + __slots__ = ["algorithm", "creation_time_seconds", "encryption_key", "expiration_time_seconds", "provider_id", "public_key", "rot_id", "serial_number", "service_types", "system_id", "test_device_deprecated", "type"] + class Algorithm(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + class ServiceType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + class Type(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + class EncryptionKey(_message.Message): + __slots__ = ["algorithm", "public_key"] + ALGORITHM_FIELD_NUMBER: _ClassVar[int] + PUBLIC_KEY_FIELD_NUMBER: _ClassVar[int] + algorithm: DrmCertificate.Algorithm + public_key: bytes + def __init__(self, public_key: _Optional[bytes] = ..., algorithm: _Optional[_Union[DrmCertificate.Algorithm, str]] = ...) -> None: ... + ALGORITHM_FIELD_NUMBER: _ClassVar[int] + CAS_PROXY_SDK: DrmCertificate.ServiceType + CREATION_TIME_SECONDS_FIELD_NUMBER: _ClassVar[int] + DEVICE: DrmCertificate.Type + DEVICE_MODEL: DrmCertificate.Type + ECC_SECP256R1: DrmCertificate.Algorithm + ECC_SECP384R1: DrmCertificate.Algorithm + ECC_SECP521R1: DrmCertificate.Algorithm + ENCRYPTION_KEY_FIELD_NUMBER: _ClassVar[int] + EXPIRATION_TIME_SECONDS_FIELD_NUMBER: _ClassVar[int] + LICENSE_SERVER_PROXY_SDK: DrmCertificate.ServiceType + LICENSE_SERVER_SDK: DrmCertificate.ServiceType + PROVIDER_ID_FIELD_NUMBER: _ClassVar[int] + PROVISIONER: DrmCertificate.Type + PROVISIONING_SDK: DrmCertificate.ServiceType + PUBLIC_KEY_FIELD_NUMBER: _ClassVar[int] + ROOT: DrmCertificate.Type + ROT_ID_FIELD_NUMBER: _ClassVar[int] + RSA: DrmCertificate.Algorithm + SERIAL_NUMBER_FIELD_NUMBER: _ClassVar[int] + SERVICE: DrmCertificate.Type + SERVICE_TYPES_FIELD_NUMBER: _ClassVar[int] + SYSTEM_ID_FIELD_NUMBER: _ClassVar[int] + TEST_DEVICE_DEPRECATED_FIELD_NUMBER: _ClassVar[int] + TYPE_FIELD_NUMBER: _ClassVar[int] + UNKNOWN_ALGORITHM: DrmCertificate.Algorithm + UNKNOWN_SERVICE_TYPE: DrmCertificate.ServiceType + algorithm: DrmCertificate.Algorithm + creation_time_seconds: int + encryption_key: DrmCertificate.EncryptionKey + expiration_time_seconds: int + provider_id: str + public_key: bytes + rot_id: bytes + serial_number: bytes + service_types: _containers.RepeatedScalarFieldContainer[DrmCertificate.ServiceType] + system_id: int + test_device_deprecated: bool + type: DrmCertificate.Type + def __init__(self, type: _Optional[_Union[DrmCertificate.Type, str]] = ..., serial_number: _Optional[bytes] = ..., creation_time_seconds: _Optional[int] = ..., expiration_time_seconds: _Optional[int] = ..., public_key: _Optional[bytes] = ..., system_id: _Optional[int] = ..., test_device_deprecated: bool = ..., provider_id: _Optional[str] = ..., service_types: _Optional[_Iterable[_Union[DrmCertificate.ServiceType, str]]] = ..., algorithm: _Optional[_Union[DrmCertificate.Algorithm, str]] = ..., rot_id: _Optional[bytes] = ..., encryption_key: _Optional[_Union[DrmCertificate.EncryptionKey, _Mapping]] = ...) -> None: ... + +class EncryptedClientIdentification(_message.Message): + __slots__ = ["encrypted_client_id", "encrypted_client_id_iv", "encrypted_privacy_key", "provider_id", "service_certificate_serial_number"] + ENCRYPTED_CLIENT_ID_FIELD_NUMBER: _ClassVar[int] + ENCRYPTED_CLIENT_ID_IV_FIELD_NUMBER: _ClassVar[int] + ENCRYPTED_PRIVACY_KEY_FIELD_NUMBER: _ClassVar[int] + PROVIDER_ID_FIELD_NUMBER: _ClassVar[int] + SERVICE_CERTIFICATE_SERIAL_NUMBER_FIELD_NUMBER: _ClassVar[int] + encrypted_client_id: bytes + encrypted_client_id_iv: bytes + encrypted_privacy_key: bytes + provider_id: str + service_certificate_serial_number: bytes + def __init__(self, provider_id: _Optional[str] = ..., service_certificate_serial_number: _Optional[bytes] = ..., encrypted_client_id: _Optional[bytes] = ..., encrypted_client_id_iv: _Optional[bytes] = ..., encrypted_privacy_key: _Optional[bytes] = ...) -> None: ... + +class FileHashes(_message.Message): + __slots__ = ["signatures", "signer"] + class Signature(_message.Message): + __slots__ = ["SHA512Hash", "filename", "main_exe", "signature", "test_signing"] + FILENAME_FIELD_NUMBER: _ClassVar[int] + MAIN_EXE_FIELD_NUMBER: _ClassVar[int] + SHA512HASH_FIELD_NUMBER: _ClassVar[int] + SHA512Hash: bytes + SIGNATURE_FIELD_NUMBER: _ClassVar[int] + TEST_SIGNING_FIELD_NUMBER: _ClassVar[int] + filename: str + main_exe: bool + signature: bytes + test_signing: bool + def __init__(self, filename: _Optional[str] = ..., test_signing: bool = ..., SHA512Hash: _Optional[bytes] = ..., main_exe: bool = ..., signature: _Optional[bytes] = ...) -> None: ... + SIGNATURES_FIELD_NUMBER: _ClassVar[int] + SIGNER_FIELD_NUMBER: _ClassVar[int] + signatures: _containers.RepeatedCompositeFieldContainer[FileHashes.Signature] + signer: bytes + def __init__(self, signer: _Optional[bytes] = ..., signatures: _Optional[_Iterable[_Union[FileHashes.Signature, _Mapping]]] = ...) -> None: ... + +class License(_message.Message): + __slots__ = ["group_ids", "id", "key", "license_start_time", "platform_verification_status", "policy", "protection_scheme", "provider_client_token", "remote_attestation_verified", "srm_requirement", "srm_update"] + class KeyContainer(_message.Message): + __slots__ = ["anti_rollback_usage_table", "id", "iv", "key", "key_control", "level", "operator_session_key_permissions", "requested_protection", "required_protection", "track_label", "type", "video_resolution_constraints"] + class KeyType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + class SecurityLevel(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + class KeyControl(_message.Message): + __slots__ = ["iv", "key_control_block"] + IV_FIELD_NUMBER: _ClassVar[int] + KEY_CONTROL_BLOCK_FIELD_NUMBER: _ClassVar[int] + iv: bytes + key_control_block: bytes + def __init__(self, key_control_block: _Optional[bytes] = ..., iv: _Optional[bytes] = ...) -> None: ... + class OperatorSessionKeyPermissions(_message.Message): + __slots__ = ["allow_decrypt", "allow_encrypt", "allow_sign", "allow_signature_verify"] + ALLOW_DECRYPT_FIELD_NUMBER: _ClassVar[int] + ALLOW_ENCRYPT_FIELD_NUMBER: _ClassVar[int] + ALLOW_SIGNATURE_VERIFY_FIELD_NUMBER: _ClassVar[int] + ALLOW_SIGN_FIELD_NUMBER: _ClassVar[int] + allow_decrypt: bool + allow_encrypt: bool + allow_sign: bool + allow_signature_verify: bool + def __init__(self, allow_encrypt: bool = ..., allow_decrypt: bool = ..., allow_sign: bool = ..., allow_signature_verify: bool = ...) -> None: ... + class OutputProtection(_message.Message): + __slots__ = ["cgms_flags", "disable_analog_output", "disable_digital_output", "hdcp", "hdcp_srm_rule"] + class CGMS(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + class HDCP(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + class HdcpSrmRule(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + CGMS_FLAGS_FIELD_NUMBER: _ClassVar[int] + CGMS_NONE: License.KeyContainer.OutputProtection.CGMS + COPY_FREE: License.KeyContainer.OutputProtection.CGMS + COPY_NEVER: License.KeyContainer.OutputProtection.CGMS + COPY_ONCE: License.KeyContainer.OutputProtection.CGMS + CURRENT_SRM: License.KeyContainer.OutputProtection.HdcpSrmRule + DISABLE_ANALOG_OUTPUT_FIELD_NUMBER: _ClassVar[int] + DISABLE_DIGITAL_OUTPUT_FIELD_NUMBER: _ClassVar[int] + HDCP_FIELD_NUMBER: _ClassVar[int] + HDCP_NONE: License.KeyContainer.OutputProtection.HDCP + HDCP_NO_DIGITAL_OUTPUT: License.KeyContainer.OutputProtection.HDCP + HDCP_SRM_RULE_FIELD_NUMBER: _ClassVar[int] + HDCP_SRM_RULE_NONE: License.KeyContainer.OutputProtection.HdcpSrmRule + HDCP_V1: License.KeyContainer.OutputProtection.HDCP + HDCP_V2: License.KeyContainer.OutputProtection.HDCP + HDCP_V2_1: License.KeyContainer.OutputProtection.HDCP + HDCP_V2_2: License.KeyContainer.OutputProtection.HDCP + HDCP_V2_3: License.KeyContainer.OutputProtection.HDCP + cgms_flags: License.KeyContainer.OutputProtection.CGMS + disable_analog_output: bool + disable_digital_output: bool + hdcp: License.KeyContainer.OutputProtection.HDCP + hdcp_srm_rule: License.KeyContainer.OutputProtection.HdcpSrmRule + def __init__(self, hdcp: _Optional[_Union[License.KeyContainer.OutputProtection.HDCP, str]] = ..., cgms_flags: _Optional[_Union[License.KeyContainer.OutputProtection.CGMS, str]] = ..., hdcp_srm_rule: _Optional[_Union[License.KeyContainer.OutputProtection.HdcpSrmRule, str]] = ..., disable_analog_output: bool = ..., disable_digital_output: bool = ...) -> None: ... + class VideoResolutionConstraint(_message.Message): + __slots__ = ["max_resolution_pixels", "min_resolution_pixels", "required_protection"] + MAX_RESOLUTION_PIXELS_FIELD_NUMBER: _ClassVar[int] + MIN_RESOLUTION_PIXELS_FIELD_NUMBER: _ClassVar[int] + REQUIRED_PROTECTION_FIELD_NUMBER: _ClassVar[int] + max_resolution_pixels: int + min_resolution_pixels: int + required_protection: License.KeyContainer.OutputProtection + def __init__(self, min_resolution_pixels: _Optional[int] = ..., max_resolution_pixels: _Optional[int] = ..., required_protection: _Optional[_Union[License.KeyContainer.OutputProtection, _Mapping]] = ...) -> None: ... + ANTI_ROLLBACK_USAGE_TABLE_FIELD_NUMBER: _ClassVar[int] + CONTENT: License.KeyContainer.KeyType + ENTITLEMENT: License.KeyContainer.KeyType + HW_SECURE_ALL: License.KeyContainer.SecurityLevel + HW_SECURE_CRYPTO: License.KeyContainer.SecurityLevel + HW_SECURE_DECODE: License.KeyContainer.SecurityLevel + ID_FIELD_NUMBER: _ClassVar[int] + IV_FIELD_NUMBER: _ClassVar[int] + KEY_CONTROL: License.KeyContainer.KeyType + KEY_CONTROL_FIELD_NUMBER: _ClassVar[int] + KEY_FIELD_NUMBER: _ClassVar[int] + LEVEL_FIELD_NUMBER: _ClassVar[int] + OEM_CONTENT: License.KeyContainer.KeyType + OPERATOR_SESSION: License.KeyContainer.KeyType + OPERATOR_SESSION_KEY_PERMISSIONS_FIELD_NUMBER: _ClassVar[int] + REQUESTED_PROTECTION_FIELD_NUMBER: _ClassVar[int] + REQUIRED_PROTECTION_FIELD_NUMBER: _ClassVar[int] + SIGNING: License.KeyContainer.KeyType + SW_SECURE_CRYPTO: License.KeyContainer.SecurityLevel + SW_SECURE_DECODE: License.KeyContainer.SecurityLevel + TRACK_LABEL_FIELD_NUMBER: _ClassVar[int] + TYPE_FIELD_NUMBER: _ClassVar[int] + VIDEO_RESOLUTION_CONSTRAINTS_FIELD_NUMBER: _ClassVar[int] + anti_rollback_usage_table: bool + id: bytes + iv: bytes + key: bytes + key_control: License.KeyContainer.KeyControl + level: License.KeyContainer.SecurityLevel + operator_session_key_permissions: License.KeyContainer.OperatorSessionKeyPermissions + requested_protection: License.KeyContainer.OutputProtection + required_protection: License.KeyContainer.OutputProtection + track_label: str + type: License.KeyContainer.KeyType + video_resolution_constraints: _containers.RepeatedCompositeFieldContainer[License.KeyContainer.VideoResolutionConstraint] + def __init__(self, id: _Optional[bytes] = ..., iv: _Optional[bytes] = ..., key: _Optional[bytes] = ..., type: _Optional[_Union[License.KeyContainer.KeyType, str]] = ..., level: _Optional[_Union[License.KeyContainer.SecurityLevel, str]] = ..., required_protection: _Optional[_Union[License.KeyContainer.OutputProtection, _Mapping]] = ..., requested_protection: _Optional[_Union[License.KeyContainer.OutputProtection, _Mapping]] = ..., key_control: _Optional[_Union[License.KeyContainer.KeyControl, _Mapping]] = ..., operator_session_key_permissions: _Optional[_Union[License.KeyContainer.OperatorSessionKeyPermissions, _Mapping]] = ..., video_resolution_constraints: _Optional[_Iterable[_Union[License.KeyContainer.VideoResolutionConstraint, _Mapping]]] = ..., anti_rollback_usage_table: bool = ..., track_label: _Optional[str] = ...) -> None: ... + class Policy(_message.Message): + __slots__ = ["always_include_client_id", "can_persist", "can_play", "can_renew", "license_duration_seconds", "play_start_grace_period_seconds", "playback_duration_seconds", "renew_with_usage", "renewal_delay_seconds", "renewal_recovery_duration_seconds", "renewal_retry_interval_seconds", "renewal_server_url", "rental_duration_seconds", "soft_enforce_playback_duration", "soft_enforce_rental_duration"] + ALWAYS_INCLUDE_CLIENT_ID_FIELD_NUMBER: _ClassVar[int] + CAN_PERSIST_FIELD_NUMBER: _ClassVar[int] + CAN_PLAY_FIELD_NUMBER: _ClassVar[int] + CAN_RENEW_FIELD_NUMBER: _ClassVar[int] + LICENSE_DURATION_SECONDS_FIELD_NUMBER: _ClassVar[int] + PLAYBACK_DURATION_SECONDS_FIELD_NUMBER: _ClassVar[int] + PLAY_START_GRACE_PERIOD_SECONDS_FIELD_NUMBER: _ClassVar[int] + RENEWAL_DELAY_SECONDS_FIELD_NUMBER: _ClassVar[int] + RENEWAL_RECOVERY_DURATION_SECONDS_FIELD_NUMBER: _ClassVar[int] + RENEWAL_RETRY_INTERVAL_SECONDS_FIELD_NUMBER: _ClassVar[int] + RENEWAL_SERVER_URL_FIELD_NUMBER: _ClassVar[int] + RENEW_WITH_USAGE_FIELD_NUMBER: _ClassVar[int] + RENTAL_DURATION_SECONDS_FIELD_NUMBER: _ClassVar[int] + SOFT_ENFORCE_PLAYBACK_DURATION_FIELD_NUMBER: _ClassVar[int] + SOFT_ENFORCE_RENTAL_DURATION_FIELD_NUMBER: _ClassVar[int] + always_include_client_id: bool + can_persist: bool + can_play: bool + can_renew: bool + license_duration_seconds: int + play_start_grace_period_seconds: int + playback_duration_seconds: int + renew_with_usage: bool + renewal_delay_seconds: int + renewal_recovery_duration_seconds: int + renewal_retry_interval_seconds: int + renewal_server_url: str + rental_duration_seconds: int + soft_enforce_playback_duration: bool + soft_enforce_rental_duration: bool + def __init__(self, can_play: bool = ..., can_persist: bool = ..., can_renew: bool = ..., rental_duration_seconds: _Optional[int] = ..., playback_duration_seconds: _Optional[int] = ..., license_duration_seconds: _Optional[int] = ..., renewal_recovery_duration_seconds: _Optional[int] = ..., renewal_server_url: _Optional[str] = ..., renewal_delay_seconds: _Optional[int] = ..., renewal_retry_interval_seconds: _Optional[int] = ..., renew_with_usage: bool = ..., always_include_client_id: bool = ..., play_start_grace_period_seconds: _Optional[int] = ..., soft_enforce_playback_duration: bool = ..., soft_enforce_rental_duration: bool = ...) -> None: ... + GROUP_IDS_FIELD_NUMBER: _ClassVar[int] + ID_FIELD_NUMBER: _ClassVar[int] + KEY_FIELD_NUMBER: _ClassVar[int] + LICENSE_START_TIME_FIELD_NUMBER: _ClassVar[int] + PLATFORM_VERIFICATION_STATUS_FIELD_NUMBER: _ClassVar[int] + POLICY_FIELD_NUMBER: _ClassVar[int] + PROTECTION_SCHEME_FIELD_NUMBER: _ClassVar[int] + PROVIDER_CLIENT_TOKEN_FIELD_NUMBER: _ClassVar[int] + REMOTE_ATTESTATION_VERIFIED_FIELD_NUMBER: _ClassVar[int] + SRM_REQUIREMENT_FIELD_NUMBER: _ClassVar[int] + SRM_UPDATE_FIELD_NUMBER: _ClassVar[int] + group_ids: _containers.RepeatedScalarFieldContainer[bytes] + id: LicenseIdentification + key: _containers.RepeatedCompositeFieldContainer[License.KeyContainer] + license_start_time: int + platform_verification_status: PlatformVerificationStatus + policy: License.Policy + protection_scheme: int + provider_client_token: bytes + remote_attestation_verified: bool + srm_requirement: bytes + srm_update: bytes + def __init__(self, id: _Optional[_Union[LicenseIdentification, _Mapping]] = ..., policy: _Optional[_Union[License.Policy, _Mapping]] = ..., key: _Optional[_Iterable[_Union[License.KeyContainer, _Mapping]]] = ..., license_start_time: _Optional[int] = ..., remote_attestation_verified: bool = ..., provider_client_token: _Optional[bytes] = ..., protection_scheme: _Optional[int] = ..., srm_requirement: _Optional[bytes] = ..., srm_update: _Optional[bytes] = ..., platform_verification_status: _Optional[_Union[PlatformVerificationStatus, str]] = ..., group_ids: _Optional[_Iterable[bytes]] = ...) -> None: ... + +class LicenseIdentification(_message.Message): + __slots__ = ["provider_session_token", "purchase_id", "request_id", "session_id", "type", "version"] + PROVIDER_SESSION_TOKEN_FIELD_NUMBER: _ClassVar[int] + PURCHASE_ID_FIELD_NUMBER: _ClassVar[int] + REQUEST_ID_FIELD_NUMBER: _ClassVar[int] + SESSION_ID_FIELD_NUMBER: _ClassVar[int] + TYPE_FIELD_NUMBER: _ClassVar[int] + VERSION_FIELD_NUMBER: _ClassVar[int] + provider_session_token: bytes + purchase_id: bytes + request_id: bytes + session_id: bytes + type: LicenseType + version: int + def __init__(self, request_id: _Optional[bytes] = ..., session_id: _Optional[bytes] = ..., purchase_id: _Optional[bytes] = ..., type: _Optional[_Union[LicenseType, str]] = ..., version: _Optional[int] = ..., provider_session_token: _Optional[bytes] = ...) -> None: ... + +class LicenseRequest(_message.Message): + __slots__ = ["client_id", "content_id", "encrypted_client_id", "key_control_nonce", "key_control_nonce_deprecated", "protocol_version", "request_time", "type"] + class RequestType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + class ContentIdentification(_message.Message): + __slots__ = ["existing_license", "init_data", "webm_key_id", "widevine_pssh_data"] + class ExistingLicense(_message.Message): + __slots__ = ["license_id", "seconds_since_last_played", "seconds_since_started", "session_usage_table_entry"] + LICENSE_ID_FIELD_NUMBER: _ClassVar[int] + SECONDS_SINCE_LAST_PLAYED_FIELD_NUMBER: _ClassVar[int] + SECONDS_SINCE_STARTED_FIELD_NUMBER: _ClassVar[int] + SESSION_USAGE_TABLE_ENTRY_FIELD_NUMBER: _ClassVar[int] + license_id: LicenseIdentification + seconds_since_last_played: int + seconds_since_started: int + session_usage_table_entry: bytes + def __init__(self, license_id: _Optional[_Union[LicenseIdentification, _Mapping]] = ..., seconds_since_started: _Optional[int] = ..., seconds_since_last_played: _Optional[int] = ..., session_usage_table_entry: _Optional[bytes] = ...) -> None: ... + class InitData(_message.Message): + __slots__ = ["init_data", "init_data_type", "license_type", "request_id"] + class InitDataType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + CENC: LicenseRequest.ContentIdentification.InitData.InitDataType + INIT_DATA_FIELD_NUMBER: _ClassVar[int] + INIT_DATA_TYPE_FIELD_NUMBER: _ClassVar[int] + LICENSE_TYPE_FIELD_NUMBER: _ClassVar[int] + REQUEST_ID_FIELD_NUMBER: _ClassVar[int] + WEBM: LicenseRequest.ContentIdentification.InitData.InitDataType + init_data: bytes + init_data_type: LicenseRequest.ContentIdentification.InitData.InitDataType + license_type: LicenseType + request_id: bytes + def __init__(self, init_data_type: _Optional[_Union[LicenseRequest.ContentIdentification.InitData.InitDataType, str]] = ..., init_data: _Optional[bytes] = ..., license_type: _Optional[_Union[LicenseType, str]] = ..., request_id: _Optional[bytes] = ...) -> None: ... + class WebmKeyId(_message.Message): + __slots__ = ["header", "license_type", "request_id"] + HEADER_FIELD_NUMBER: _ClassVar[int] + LICENSE_TYPE_FIELD_NUMBER: _ClassVar[int] + REQUEST_ID_FIELD_NUMBER: _ClassVar[int] + header: bytes + license_type: LicenseType + request_id: bytes + def __init__(self, header: _Optional[bytes] = ..., license_type: _Optional[_Union[LicenseType, str]] = ..., request_id: _Optional[bytes] = ...) -> None: ... + class WidevinePsshData(_message.Message): + __slots__ = ["license_type", "pssh_data", "request_id"] + LICENSE_TYPE_FIELD_NUMBER: _ClassVar[int] + PSSH_DATA_FIELD_NUMBER: _ClassVar[int] + REQUEST_ID_FIELD_NUMBER: _ClassVar[int] + license_type: LicenseType + pssh_data: _containers.RepeatedScalarFieldContainer[bytes] + request_id: bytes + def __init__(self, pssh_data: _Optional[_Iterable[bytes]] = ..., license_type: _Optional[_Union[LicenseType, str]] = ..., request_id: _Optional[bytes] = ...) -> None: ... + EXISTING_LICENSE_FIELD_NUMBER: _ClassVar[int] + INIT_DATA_FIELD_NUMBER: _ClassVar[int] + WEBM_KEY_ID_FIELD_NUMBER: _ClassVar[int] + WIDEVINE_PSSH_DATA_FIELD_NUMBER: _ClassVar[int] + existing_license: LicenseRequest.ContentIdentification.ExistingLicense + init_data: LicenseRequest.ContentIdentification.InitData + webm_key_id: LicenseRequest.ContentIdentification.WebmKeyId + widevine_pssh_data: LicenseRequest.ContentIdentification.WidevinePsshData + def __init__(self, widevine_pssh_data: _Optional[_Union[LicenseRequest.ContentIdentification.WidevinePsshData, _Mapping]] = ..., webm_key_id: _Optional[_Union[LicenseRequest.ContentIdentification.WebmKeyId, _Mapping]] = ..., existing_license: _Optional[_Union[LicenseRequest.ContentIdentification.ExistingLicense, _Mapping]] = ..., init_data: _Optional[_Union[LicenseRequest.ContentIdentification.InitData, _Mapping]] = ...) -> None: ... + CLIENT_ID_FIELD_NUMBER: _ClassVar[int] + CONTENT_ID_FIELD_NUMBER: _ClassVar[int] + ENCRYPTED_CLIENT_ID_FIELD_NUMBER: _ClassVar[int] + KEY_CONTROL_NONCE_DEPRECATED_FIELD_NUMBER: _ClassVar[int] + KEY_CONTROL_NONCE_FIELD_NUMBER: _ClassVar[int] + NEW: LicenseRequest.RequestType + PROTOCOL_VERSION_FIELD_NUMBER: _ClassVar[int] + RELEASE: LicenseRequest.RequestType + RENEWAL: LicenseRequest.RequestType + REQUEST_TIME_FIELD_NUMBER: _ClassVar[int] + TYPE_FIELD_NUMBER: _ClassVar[int] + client_id: ClientIdentification + content_id: LicenseRequest.ContentIdentification + encrypted_client_id: EncryptedClientIdentification + key_control_nonce: int + key_control_nonce_deprecated: bytes + protocol_version: ProtocolVersion + request_time: int + type: LicenseRequest.RequestType + def __init__(self, client_id: _Optional[_Union[ClientIdentification, _Mapping]] = ..., content_id: _Optional[_Union[LicenseRequest.ContentIdentification, _Mapping]] = ..., type: _Optional[_Union[LicenseRequest.RequestType, str]] = ..., request_time: _Optional[int] = ..., key_control_nonce_deprecated: _Optional[bytes] = ..., protocol_version: _Optional[_Union[ProtocolVersion, str]] = ..., key_control_nonce: _Optional[int] = ..., encrypted_client_id: _Optional[_Union[EncryptedClientIdentification, _Mapping]] = ...) -> None: ... + +class MetricData(_message.Message): + __slots__ = ["metric_data", "stage_name"] + class MetricType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + class TypeValue(_message.Message): + __slots__ = ["type", "value"] + TYPE_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + type: MetricData.MetricType + value: int + def __init__(self, type: _Optional[_Union[MetricData.MetricType, str]] = ..., value: _Optional[int] = ...) -> None: ... + LATENCY: MetricData.MetricType + METRIC_DATA_FIELD_NUMBER: _ClassVar[int] + STAGE_NAME_FIELD_NUMBER: _ClassVar[int] + TIMESTAMP: MetricData.MetricType + metric_data: _containers.RepeatedCompositeFieldContainer[MetricData.TypeValue] + stage_name: str + def __init__(self, stage_name: _Optional[str] = ..., metric_data: _Optional[_Iterable[_Union[MetricData.TypeValue, _Mapping]]] = ...) -> None: ... + +class SignedDrmCertificate(_message.Message): + __slots__ = ["drm_certificate", "hash_algorithm", "signature", "signer"] + DRM_CERTIFICATE_FIELD_NUMBER: _ClassVar[int] + HASH_ALGORITHM_FIELD_NUMBER: _ClassVar[int] + SIGNATURE_FIELD_NUMBER: _ClassVar[int] + SIGNER_FIELD_NUMBER: _ClassVar[int] + drm_certificate: bytes + hash_algorithm: HashAlgorithmProto + signature: bytes + signer: SignedDrmCertificate + def __init__(self, drm_certificate: _Optional[bytes] = ..., signature: _Optional[bytes] = ..., signer: _Optional[_Union[SignedDrmCertificate, _Mapping]] = ..., hash_algorithm: _Optional[_Union[HashAlgorithmProto, str]] = ...) -> None: ... + +class SignedMessage(_message.Message): + __slots__ = ["metric_data", "msg", "oemcrypto_core_message", "remote_attestation", "service_version_info", "session_key", "session_key_type", "signature", "type"] + class MessageType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + class SessionKeyType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + CAS_LICENSE: SignedMessage.MessageType + CAS_LICENSE_REQUEST: SignedMessage.MessageType + EPHERMERAL_ECC_PUBLIC_KEY: SignedMessage.SessionKeyType + ERROR_RESPONSE: SignedMessage.MessageType + EXTERNAL_LICENSE: SignedMessage.MessageType + EXTERNAL_LICENSE_REQUEST: SignedMessage.MessageType + LICENSE: SignedMessage.MessageType + LICENSE_REQUEST: SignedMessage.MessageType + METRIC_DATA_FIELD_NUMBER: _ClassVar[int] + MSG_FIELD_NUMBER: _ClassVar[int] + OEMCRYPTO_CORE_MESSAGE_FIELD_NUMBER: _ClassVar[int] + REMOTE_ATTESTATION_FIELD_NUMBER: _ClassVar[int] + SERVICE_CERTIFICATE: SignedMessage.MessageType + SERVICE_CERTIFICATE_REQUEST: SignedMessage.MessageType + SERVICE_VERSION_INFO_FIELD_NUMBER: _ClassVar[int] + SESSION_KEY_FIELD_NUMBER: _ClassVar[int] + SESSION_KEY_TYPE_FIELD_NUMBER: _ClassVar[int] + SIGNATURE_FIELD_NUMBER: _ClassVar[int] + SUB_LICENSE: SignedMessage.MessageType + TYPE_FIELD_NUMBER: _ClassVar[int] + UNDEFINED: SignedMessage.SessionKeyType + WRAPPED_AES_KEY: SignedMessage.SessionKeyType + metric_data: _containers.RepeatedCompositeFieldContainer[MetricData] + msg: bytes + oemcrypto_core_message: bytes + remote_attestation: bytes + service_version_info: VersionInfo + session_key: bytes + session_key_type: SignedMessage.SessionKeyType + signature: bytes + type: SignedMessage.MessageType + def __init__(self, type: _Optional[_Union[SignedMessage.MessageType, str]] = ..., msg: _Optional[bytes] = ..., signature: _Optional[bytes] = ..., session_key: _Optional[bytes] = ..., remote_attestation: _Optional[bytes] = ..., metric_data: _Optional[_Iterable[_Union[MetricData, _Mapping]]] = ..., service_version_info: _Optional[_Union[VersionInfo, _Mapping]] = ..., session_key_type: _Optional[_Union[SignedMessage.SessionKeyType, str]] = ..., oemcrypto_core_message: _Optional[bytes] = ...) -> None: ... + +class VersionInfo(_message.Message): + __slots__ = ["license_sdk_version", "license_service_version"] + LICENSE_SDK_VERSION_FIELD_NUMBER: _ClassVar[int] + LICENSE_SERVICE_VERSION_FIELD_NUMBER: _ClassVar[int] + license_sdk_version: str + license_service_version: str + def __init__(self, license_sdk_version: _Optional[str] = ..., license_service_version: _Optional[str] = ...) -> None: ... + +class WidevinePsshData(_message.Message): + __slots__ = ["algorithm", "content_id", "crypto_period_index", "crypto_period_seconds", "entitled_keys", "group_ids", "grouped_license", "key_ids", "key_sequence", "policy", "protection_scheme", "provider", "track_type", "type", "video_feature"] + class Algorithm(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + class Type(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + class EntitledKey(_message.Message): + __slots__ = ["entitlement_key_id", "entitlement_key_size_bytes", "iv", "key", "key_id"] + ENTITLEMENT_KEY_ID_FIELD_NUMBER: _ClassVar[int] + ENTITLEMENT_KEY_SIZE_BYTES_FIELD_NUMBER: _ClassVar[int] + IV_FIELD_NUMBER: _ClassVar[int] + KEY_FIELD_NUMBER: _ClassVar[int] + KEY_ID_FIELD_NUMBER: _ClassVar[int] + entitlement_key_id: bytes + entitlement_key_size_bytes: int + iv: bytes + key: bytes + key_id: bytes + def __init__(self, entitlement_key_id: _Optional[bytes] = ..., key_id: _Optional[bytes] = ..., key: _Optional[bytes] = ..., iv: _Optional[bytes] = ..., entitlement_key_size_bytes: _Optional[int] = ...) -> None: ... + AESCTR: WidevinePsshData.Algorithm + ALGORITHM_FIELD_NUMBER: _ClassVar[int] + CONTENT_ID_FIELD_NUMBER: _ClassVar[int] + CRYPTO_PERIOD_INDEX_FIELD_NUMBER: _ClassVar[int] + CRYPTO_PERIOD_SECONDS_FIELD_NUMBER: _ClassVar[int] + ENTITLED_KEY: WidevinePsshData.Type + ENTITLED_KEYS_FIELD_NUMBER: _ClassVar[int] + ENTITLEMENT: WidevinePsshData.Type + GROUPED_LICENSE_FIELD_NUMBER: _ClassVar[int] + GROUP_IDS_FIELD_NUMBER: _ClassVar[int] + KEY_IDS_FIELD_NUMBER: _ClassVar[int] + KEY_SEQUENCE_FIELD_NUMBER: _ClassVar[int] + POLICY_FIELD_NUMBER: _ClassVar[int] + PROTECTION_SCHEME_FIELD_NUMBER: _ClassVar[int] + PROVIDER_FIELD_NUMBER: _ClassVar[int] + SINGLE: WidevinePsshData.Type + TRACK_TYPE_FIELD_NUMBER: _ClassVar[int] + TYPE_FIELD_NUMBER: _ClassVar[int] + UNENCRYPTED: WidevinePsshData.Algorithm + VIDEO_FEATURE_FIELD_NUMBER: _ClassVar[int] + algorithm: WidevinePsshData.Algorithm + content_id: bytes + crypto_period_index: int + crypto_period_seconds: int + entitled_keys: _containers.RepeatedCompositeFieldContainer[WidevinePsshData.EntitledKey] + group_ids: _containers.RepeatedScalarFieldContainer[bytes] + grouped_license: bytes + key_ids: _containers.RepeatedScalarFieldContainer[bytes] + key_sequence: int + policy: str + protection_scheme: int + provider: str + track_type: str + type: WidevinePsshData.Type + video_feature: str + def __init__(self, key_ids: _Optional[_Iterable[bytes]] = ..., content_id: _Optional[bytes] = ..., crypto_period_index: _Optional[int] = ..., protection_scheme: _Optional[int] = ..., crypto_period_seconds: _Optional[int] = ..., type: _Optional[_Union[WidevinePsshData.Type, str]] = ..., key_sequence: _Optional[int] = ..., group_ids: _Optional[_Iterable[bytes]] = ..., entitled_keys: _Optional[_Iterable[_Union[WidevinePsshData.EntitledKey, _Mapping]]] = ..., video_feature: _Optional[str] = ..., algorithm: _Optional[_Union[WidevinePsshData.Algorithm, str]] = ..., provider: _Optional[str] = ..., track_type: _Optional[str] = ..., policy: _Optional[str] = ..., grouped_license: _Optional[bytes] = ...) -> None: ... + +class LicenseType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + +class PlatformVerificationStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + +class ProtocolVersion(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] + +class HashAlgorithmProto(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = [] diff --git a/scripts/pywidevine/pywidevine/main.py b/scripts/pywidevine/pywidevine/main.py new file mode 100644 index 0000000..301af51 --- /dev/null +++ b/scripts/pywidevine/pywidevine/main.py @@ -0,0 +1,398 @@ +import logging +from datetime import datetime +from pathlib import Path +from typing import Optional +from zlib import crc32 + +import click +import requests +import yaml +from construct import ConstructError +from google.protobuf.json_format import MessageToDict +from unidecode import UnidecodeError, unidecode + +from pywidevine import __version__ +from pywidevine.cdm import Cdm +from pywidevine.device import Device, DeviceTypes +from pywidevine.license_protocol_pb2 import FileHashes, LicenseType +from pywidevine.pssh import PSSH + + +@click.group(invoke_without_command=True) +@click.option("-v", "--version", is_flag=True, default=False, help="Print version information.") +@click.option("-d", "--debug", is_flag=True, default=False, help="Enable DEBUG level logs.") +def main(version: bool, debug: bool) -> None: + """pywidevine—Python Widevine CDM implementation.""" + logging.basicConfig(level=logging.DEBUG if debug else logging.INFO) + log = logging.getLogger() + + current_year = datetime.now().year + copyright_years = f"2022-{current_year}" + + log.info("pywidevine version %s Copyright (c) %s rlaphoenix", __version__, copyright_years) + log.info("https://github.com/devine-dl/pywidevine") + if version: + return + + +@main.command(name="license") +@click.argument("device_path", type=Path) +@click.argument("pssh", type=PSSH) +@click.argument("server", type=str) +@click.option("-t", "--type", "license_type", type=click.Choice(LicenseType.keys(), case_sensitive=False), + default="STREAMING", + help="License Type to Request.") +@click.option("-p", "--privacy", is_flag=True, default=False, + help="Use Privacy Mode, off by default.") +def license_(device_path: Path, pssh: PSSH, server: str, license_type: str, privacy: bool) -> None: + """ + Make a License Request for PSSH to SERVER using DEVICE. + It will return a list of all keys within the returned license. + + This expects the Licence Server to be a simple opaque interface where the Challenge + is sent as is (as bytes), and the License response is returned as is (as bytes). + This is a common behavior for some License Servers and is our only option for a generic + licensing function. + + You may modify this function to change how it sends the Challenge and how it parses + the License response. However, for non-generic license calls, I recommend creating a + new script that imports and uses the pywidevine module instead. This generic function + is only useful as a quick generic license call. + + This is also a great way of showing you how to use pywidevine in your own projects. + """ + log = logging.getLogger("license") + + # load device + device = Device.load(device_path) + log.info("[+] Loaded Device (%s L%s)", device.system_id, device.security_level) + log.debug(device) + + # load cdm + cdm = Cdm.from_device(device) + log.info("[+] Loaded CDM") + log.debug(cdm) + + # open cdm session + session_id = cdm.open() + log.info("[+] Opened CDM Session: %s", session_id.hex()) + + if privacy: + # get service cert for license server via cert challenge + service_cert_res = requests.post( + url=server, + data=cdm.service_certificate_challenge + ) + if service_cert_res.status_code != 200: + log.error( + "[-] Failed to get Service Privacy Certificate: [%s] %s", + service_cert_res.status_code, + service_cert_res.text + ) + return + service_cert = service_cert_res.content + provider_id = cdm.set_service_certificate(session_id, service_cert) + log.info("[+] Set Service Privacy Certificate: %s", provider_id) + log.debug(service_cert) + + # get license challenge + challenge = cdm.get_license_challenge(session_id, pssh, license_type, privacy_mode=True) + log.info("[+] Created License Request Message (Challenge)") + log.debug(challenge) + + # send license challenge + license_res = requests.post( + url=server, + data=challenge + ) + if license_res.status_code != 200: + log.error("[-] Failed to send challenge: [%s] %s", license_res.status_code, license_res.text) + return + licence = license_res.content + log.info("[+] Got License Message") + log.debug(licence) + + # parse license challenge + cdm.parse_license(session_id, licence) + log.info("[+] License Parsed Successfully") + + # print keys + for key in cdm.get_keys(session_id): + log.info("[%s] %s:%s", key.type, key.kid.hex, key.key.hex()) + + # close session, disposes of session data + cdm.close(session_id) + + +@main.command() +@click.argument("device", type=Path) +@click.option("-p", "--privacy", is_flag=True, default=False, + help="Use Privacy Mode, off by default.") +@click.pass_context +def test(ctx: click.Context, device: Path, privacy: bool) -> None: + """ + Test the CDM code by getting Content Keys for Bitmovin's Art of Motion example. + https://bitmovin.com/demos/drm + https://bitmovin-a.akamaihd.net/content/art-of-motion_drm/mpds/11331.mpd + + The device argument is a Path to a Widevine Device (.wvd) file which contains + the device private key among other required information. + """ + # The PSSH is the same for all tracks both video and audio. + # However, this might not be the case for all services/manifests. + pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa" + "7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==") + + # This License Server requires no authorization at all, no cookies, no credentials + # nothing. This is often not the case for real services. + license_server = "https://cwip-shaka-proxy.appspot.com/no_auth" + + # Specify OFFLINE if it's a PSSH for a download/offline mode title, e.g., the + # Download feature on Netflix Apps. Otherwise, use STREAMING or AUTOMATIC. + license_type = "STREAMING" + + # this runs the `cdm license` CLI-command code with the data we set above + # it will print information as it goes to the terminal + ctx.invoke( + license_, + device_path=device, + pssh=pssh, + server=license_server, + license_type=license_type, + privacy=privacy + ) + + +@main.command() +@click.option("-t", "--type", "type_", type=click.Choice([x.name for x in DeviceTypes], case_sensitive=False), + required=True, help="Device Type") +@click.option("-l", "--level", type=click.IntRange(1, 3), required=True, help="Device Security Level") +@click.option("-k", "--key", type=Path, required=True, help="Device RSA Private Key in PEM or DER format") +@click.option("-c", "--client_id", type=Path, required=True, help="Widevine ClientIdentification Blob file") +@click.option("-v", "--vmp", type=Path, default=None, help="Widevine FileHashes Blob file") +@click.option("-o", "--output", type=Path, default=None, help="Output Path or Directory") +@click.pass_context +def create_device( + ctx: click.Context, + type_: str, + level: int, + key: Path, + client_id: Path, + vmp: Optional[Path] = None, + output: Optional[Path] = None +) -> None: + """ + Create a Widevine Device (.wvd) file from an RSA Private Key (PEM or DER) and Client ID Blob. + Optionally also a VMP (Verified Media Path) Blob, which will be stored in the Client ID. + """ + if not key.is_file(): + raise click.UsageError("key: Not a path to a file, or it doesn't exist.", ctx) + if not client_id.is_file(): + raise click.UsageError("client_id: Not a path to a file, or it doesn't exist.", ctx) + if vmp and not vmp.is_file(): + raise click.UsageError("vmp: Not a path to a file, or it doesn't exist.", ctx) + + log = logging.getLogger("create-device") + + device = Device( + type_=DeviceTypes[type_.upper()], + security_level=level, + flags=None, + private_key=key.read_bytes(), + client_id=client_id.read_bytes() + ) + + if vmp: + new_vmp_data = vmp.read_bytes() + if device.client_id.vmp_data and device.client_id.vmp_data != new_vmp_data: + log.warning("Client ID already has Verified Media Path data") + device.client_id.vmp_data = new_vmp_data + + client_info = {} + for entry in device.client_id.client_info: + client_info[entry.name] = entry.value + + wvd_bin = device.dumps() + + name = f"{client_info['company_name']} {client_info['model_name']}" + if client_info.get("widevine_cdm_version"): + name += f" {client_info['widevine_cdm_version']}" + name += f" {crc32(wvd_bin).to_bytes(4, 'big').hex()}" + + try: + name = unidecode(name.strip().lower().replace(" ", "_")) + except UnidecodeError as e: + raise click.ClickException(f"Failed to sanitize name, {e}") + + if output and output.suffix: + if output.suffix.lower() != ".wvd": + log.warning(f"Saving WVD with the file extension '{output.suffix}' but '.wvd' is recommended.") + out_path = output + else: + out_dir = output or Path.cwd() + out_path = out_dir / f"{name}_{device.system_id}_l{device.security_level}.wvd" + + if out_path.exists(): + log.error(f"A file already exists at the path '{out_path}', cannot overwrite.") + return + + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_bytes(wvd_bin) + + log.info("Created Widevine Device (.wvd) file, %s", out_path.name) + log.info(" + Type: %s", device.type.name) + log.info(" + System ID: %s", device.system_id) + log.info(" + Security Level: %s", device.security_level) + log.info(" + Flags: %s", device.flags) + log.info(" + Private Key: %s (%s bit)", bool(device.private_key), device.private_key.size_in_bits()) + log.info(" + Client ID: %s (%s bytes)", bool(device.client_id), len(device.client_id.SerializeToString())) + if device.client_id.vmp_data: + file_hashes_ = FileHashes() + file_hashes_.ParseFromString(device.client_id.vmp_data) + log.info(" + VMP: True (%s signatures)", len(file_hashes_.signatures)) + else: + log.info(" + VMP: False") + log.info(" + Saved to: %s", out_path.absolute()) + + +@main.command() +@click.argument("wvd_path", type=Path) +@click.option("-o", "--out_dir", type=Path, default=None, help="Output Directory") +@click.pass_context +def export_device(ctx: click.Context, wvd_path: Path, out_dir: Optional[Path] = None) -> None: + """ + Export a Widevine Device (.wvd) file to an RSA Private Key (PEM and DER) and Client ID Blob. + Optionally also a VMP (Verified Media Path) Blob, which will be stored in the Client ID. + + If an output directory is not specified, it will be stored in the current working directory. + """ + if not wvd_path.is_file(): + raise click.UsageError("wvd_path: Not a path to a file, or it doesn't exist.", ctx) + + log = logging.getLogger("export-device") + log.info("Exporting Widevine Device (.wvd) file, %s", wvd_path.stem) + + if not out_dir: + out_dir = Path.cwd() + + out_path = out_dir / wvd_path.stem + if out_path.exists(): + if any(out_path.iterdir()): + log.error("Output directory is not empty, cannot overwrite.") + return + else: + log.warning("Output directory already exists, but is empty.") + else: + out_path.mkdir(parents=True) + + device = Device.load(wvd_path) + + log.info(f"L{device.security_level} {device.system_id} {device.type.name}") + log.info(f"Saving to: {out_path}") + + device_meta = { + "wvd": { + "device_type": device.type.name, + "security_level": device.security_level, + **device.flags + }, + "client_info": {}, + "capabilities": MessageToDict(device.client_id, preserving_proto_field_name=True)["client_capabilities"] + } + for client_info in device.client_id.client_info: + device_meta["client_info"][client_info.name] = client_info.value + + device_meta_path = out_path / "metadata.yml" + device_meta_path.write_text(yaml.dump(device_meta), encoding="utf8") + log.info("Exported Device Metadata as metadata.yml") + + if device.private_key: + private_key_path = out_path / "private_key.pem" + private_key_path.write_text( + data=device.private_key.export_key().decode(), + encoding="utf8" + ) + private_key_path.with_suffix(".der").write_bytes( + device.private_key.export_key(format="DER") + ) + log.info("Exported Private Key as private_key.der and private_key.pem") + else: + log.warning("No Private Key available") + + if device.client_id: + client_id_path = out_path / "client_id.bin" + client_id_path.write_bytes(device.client_id.SerializeToString()) + log.info("Exported Client ID as client_id.bin") + else: + log.warning("No Client ID available") + + if device.client_id.vmp_data: + vmp_path = out_path / "vmp.bin" + vmp_path.write_bytes(device.client_id.vmp_data) + log.info("Exported VMP (File Hashes) as vmp.bin") + else: + log.info("No VMP (File Hashes) available") + + +@main.command() +@click.argument("path", type=Path) +@click.pass_context +def migrate(ctx: click.Context, path: Path) -> None: + """ + Upgrade from earlier versions of the Widevine Device (.wvd) format. + + The path argument can be a direct path to a Widevine Device (.wvd) file, or a path + to a folder of Widevine Devices files. + + The migrated devices are saved to its original location, overwriting the old version. + """ + if not path.exists(): + raise click.UsageError(f"path: The path '{path}' does not exist.", ctx) + + log = logging.getLogger("migrate") + + if path.is_dir(): + devices = list(path.glob("*.wvd")) + else: + devices = [path] + + migrated = 0 + for device in devices: + log.info("Migrating %s...", device.name) + + try: + new_device = Device.migrate(device.read_bytes()) + except (ConstructError, ValueError) as e: + log.error(" - %s", e) + continue + + log.debug(new_device) + new_device.dump(device) + + log.info(" + Success") + migrated += 1 + + log.info("Migrated %s/%s devices!", migrated, len(devices)) + + +@main.command("serve", short_help="Serve your local CDM and Widevine Devices Remotely.") +@click.argument("config_path", type=Path) +@click.option("-h", "--host", type=str, default="127.0.0.1", help="Host to serve from.") +@click.option("-p", "--port", type=int, default=8786, help="Port to serve from.") +def serve_(config_path: Path, host: str, port: int) -> None: + """ + Serve your local CDM and Widevine Devices Remotely. + + \b + [CONFIG] is a path to a serve config file. + See `serve.example.yml` for an example config file. + + \b + Host as 127.0.0.1 may block remote access even if port-forwarded. + Instead, use 0.0.0.0 and ensure the TCP port you choose is forwarded. + """ + from pywidevine import serve # isort:skip + import yaml # isort:skip + + config = yaml.safe_load(config_path.read_text(encoding="utf8")) + serve.run(config, host, port) diff --git a/scripts/pywidevine/pywidevine/pssh.py b/scripts/pywidevine/pywidevine/pssh.py new file mode 100644 index 0000000..b81111a --- /dev/null +++ b/scripts/pywidevine/pywidevine/pssh.py @@ -0,0 +1,442 @@ +from __future__ import annotations + +import base64 +import binascii +import string +from io import BytesIO +from typing import Optional, Union +from uuid import UUID +from xml.etree.ElementTree import XML + +import construct +from construct import Container +from google.protobuf.message import DecodeError +from pymp4.parser import Box + +from pywidevine.license_protocol_pb2 import WidevinePsshData + + +class PSSH: + """ + MP4 PSSH Box-related utilities. + Allows you to load, create, and modify various kinds of DRM system headers. + """ + + class SystemId: + Widevine = UUID(hex="edef8ba979d64acea3c827dcd51d21ed") + PlayReady = UUID(hex="9a04f07998404286ab92e65be0885f95") + + def __init__(self, data: Union[Container, str, bytes], strict: bool = False): + """ + Load a PSSH box, WidevineCencHeader, or PlayReadyHeader. + + When loading a WidevineCencHeader or PlayReadyHeader, a new v0 PSSH box will be + created and the header will be parsed and stored in the init_data field. However, + PlayReadyHeaders (and PlayReadyObjects) are not yet currently parsed and are + stored as bytes. + + [Strict mode (strict=True)] + + Supports the following forms of input data in either Base64 or Bytes form: + - Full PSSH mp4 boxes (as defined by pymp4 Box). + - Full Widevine Cenc Headers (as defined by WidevinePsshData proto). + - Full PlayReady Objects and Headers (as defined by Microsoft Docs). + + [Lenient mode (strict=False, default)] + + If the data is not supported in Strict mode, and is assumed not to be corrupt or + parsed incorrectly, the License Server likely accepts a custom init_data value + during a License Request call. This is uncommon behavior but not out of realm of + possibilities. For example, Netflix does this with it's MSL WidevineExchange + scheme. + + Lenient mode will craft a new v0 PSSH box with the init_data field set to + the provided data as-is. The data will first be base64 decoded. This behavior + may not work in your scenario and if that's the case please manually craft + your own PSSH box with the init_data field to be used in License Requests. + + Raises: + ValueError: If the data is empty. + TypeError: If the data is an unexpected type. + binascii.Error: If the data could not be decoded as Base64 if provided as a + string. + DecodeError: If the data could not be parsed as a PSSH mp4 box nor a Widevine + Cenc Header and strict mode is enabled. + """ + if not data: + raise ValueError("Data must not be empty.") + + if isinstance(data, Container): + box = data + else: + if isinstance(data, str): + try: + data = base64.b64decode(data) + except (binascii.Error, binascii.Incomplete) as e: + raise binascii.Error(f"Could not decode data as Base64, {e}") + + if not isinstance(data, bytes): + raise TypeError(f"Expected data to be a {Container}, bytes, or base64, not {data!r}") + + try: + box = Box.parse(data) + except (IOError, construct.ConstructError): # not a box + try: + widevine_pssh_data = WidevinePsshData() + widevine_pssh_data.ParseFromString(data) + data_serialized = widevine_pssh_data.SerializeToString() + if data_serialized != data: # not actually a WidevinePsshData + raise DecodeError() + box = Box.parse(Box.build(dict( + type=b"pssh", + version=0, + flags=0, + system_ID=PSSH.SystemId.Widevine, + init_data=data_serialized + ))) + except DecodeError: # not a widevine cenc header + if "</WRMHEADER>".encode("utf-16-le") in data: + # TODO: Actually parse `data` as a PlayReadyHeader object and store that instead + box = Box.parse(Box.build(dict( + type=b"pssh", + version=0, + flags=0, + system_ID=PSSH.SystemId.PlayReady, + init_data=data + ))) + elif strict: + raise DecodeError(f"Could not parse data as a {Container} nor a {WidevinePsshData}.") + else: + # Data is not a WidevineCencHeader nor a PlayReadyHeader. + # The license server likely has something custom to parse it. + # See doc-string about Lenient mode for more information. + box = Box.parse(Box.build(dict( + type=b"pssh", + version=0, + flags=0, + system_ID=PSSH.SystemId.Widevine, + init_data=data + ))) + + self.version = box.version + self.flags = box.flags + self.system_id = box.system_ID + self.__key_ids = box.key_IDs + self.init_data = box.init_data + + def __repr__(self) -> str: + return f"PSSH<{self.system_id}>(v{self.version}; {self.flags}, {self.key_ids}, {self.init_data})" + + def __str__(self) -> str: + return self.dumps() + + @classmethod + def new( + cls, + system_id: UUID, + key_ids: Optional[list[Union[UUID, str, bytes]]] = None, + init_data: Optional[Union[WidevinePsshData, str, bytes]] = None, + version: int = 0, + flags: int = 0 + ) -> PSSH: + """Craft a new version 0 or 1 PSSH Box.""" + if not system_id: + raise ValueError("A System ID must be specified.") + if not isinstance(system_id, UUID): + raise TypeError(f"Expected system_id to be a UUID, not {system_id!r}") + + if key_ids is not None and not isinstance(key_ids, list): + raise TypeError(f"Expected key_ids to be a list not {key_ids!r}") + + if init_data is not None and not isinstance(init_data, (WidevinePsshData, str, bytes)): + raise TypeError(f"Expected init_data to be a {WidevinePsshData}, base64, or bytes, not {init_data!r}") + + if not isinstance(version, int): + raise TypeError(f"Expected version to be an int not {version!r}") + if version not in (0, 1): + raise ValueError(f"Invalid version, must be either 0 or 1, not {version}.") + + if not isinstance(flags, int): + raise TypeError(f"Expected flags to be an int not {flags!r}") + if flags < 0: + raise ValueError("Invalid flags, cannot be less than 0.") + + if version == 0 and key_ids is not None and init_data is not None: + # v0 boxes use only init_data in the pssh field, but we can use the key_ids within the init_data + raise ValueError("Version 0 PSSH boxes must use only init_data, not init_data and key_ids.") + elif version == 1: + # TODO: I cannot tell if they need either init_data or key_ids exclusively, or both is fine + # So for now I will just make sure at least one is supplied + if init_data is None and key_ids is None: + raise ValueError("Version 1 PSSH boxes must use either init_data or key_ids but neither were provided") + + if init_data is not None: + if isinstance(init_data, WidevinePsshData): + init_data = init_data.SerializeToString() + elif isinstance(init_data, str): + if all(c in string.hexdigits for c in init_data): + init_data = bytes.fromhex(init_data) + else: + init_data = base64.b64decode(init_data) + elif not isinstance(init_data, bytes): + raise TypeError( + f"Expecting init_data to be {WidevinePsshData}, hex, base64, or bytes, not {init_data!r}" + ) + + pssh = cls(Box.parse(Box.build(dict( + type=b"pssh", + version=version, + flags=flags, + system_ID=system_id, + init_data=[init_data, b""][init_data is None] + # key_IDs should not be set yet + )))) + + if key_ids: + # We must reinforce the version because pymp4 forces v0 if key_IDs is not set. + # The set_key_ids() func will set it efficiently in both init_data and the box where needed. + # The version must be reinforced ONLY if we have key_id data or there's a possibility of making + # a v1 PSSH box, that did not have key_IDs set in the PSSH box. + pssh.version = version + pssh.set_key_ids(key_ids) + + return pssh + + @property + def key_ids(self) -> list[UUID]: + """ + Get all Key IDs from within the Box or Init Data, wherever possible. + + Supports: + - Version 1 PSSH Boxes + - WidevineCencHeaders + - PlayReadyHeaders (4.0.0.0->4.3.0.0) + """ + if self.version == 1 and self.__key_ids: + return self.__key_ids + + if self.system_id == PSSH.SystemId.Widevine: + # TODO: What if its not a Widevine Cenc Header but the System ID is set as Widevine? + cenc_header = WidevinePsshData() + cenc_header.ParseFromString(self.init_data) + return [ + # the key_ids value may or may not be hex underlying + ( + UUID(bytes=key_id) if len(key_id) == 16 else # normal + UUID(hex=key_id.decode()) if len(key_id) == 32 else # stored as hex + UUID(int=int.from_bytes(key_id, "big")) # assuming as number + ) + for key_id in cenc_header.key_ids + ] + + if self.system_id == PSSH.SystemId.PlayReady: + # Assuming init data is a PRO (PlayReadyObject) + # https://learn.microsoft.com/en-us/playready/specifications/playready-header-specification + pro_data = BytesIO(self.init_data) + pro_length = int.from_bytes(pro_data.read(4), "little") + if pro_length != len(self.init_data): + raise ValueError("The PlayReadyObject seems to be corrupt (too big or small, or missing data).") + pro_record_count = int.from_bytes(pro_data.read(2), "little") + + for _ in range(pro_record_count): + prr_type = int.from_bytes(pro_data.read(2), "little") + prr_length = int.from_bytes(pro_data.read(2), "little") + prr_value = pro_data.read(prr_length) + if prr_type != 0x01: + # No PlayReady Header, skip and hope for something else + # TODO: Add support for Embedded License Stores (0x03) + continue + + wrm_ns = {"wrm": "http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader"} + prr_header = XML(prr_value.decode("utf-16-le")) + prr_header_version = prr_header.get("version") + if prr_header_version == "4.0.0.0": + key_ids = [ + x.text + for x in prr_header.findall("./wrm:DATA/wrm:KID", wrm_ns) + if x.text + ] + elif prr_header_version == "4.1.0.0": + key_ids = [ + x.attrib["VALUE"] + for x in prr_header.findall("./wrm:DATA/wrm:PROTECTINFO/wrm:KID", wrm_ns) + ] + elif prr_header_version in ("4.2.0.0", "4.3.0.0"): + # TODO: Retain the Encryption Scheme information in v4.3.0.0 + # This is because some Key IDs can be AES-CTR while some are AES-CBC. + # Conversion to WidevineCencHeader could use this information. + key_ids = [ + x.attrib["VALUE"] + for x in prr_header.findall("./wrm:DATA/wrm:PROTECTINFO/wrm:KIDS/wrm:KID", wrm_ns) + ] + else: + raise ValueError(f"Unsupported PlayReadyHeader version {prr_header_version}") + + return [ + UUID(bytes=base64.b64decode(key_id)) + for key_id in key_ids + ] + + raise ValueError("Unsupported PlayReadyObject, no PlayReadyHeader within the object.") + + raise ValueError(f"This PSSH is not supported by key_ids() property, {self.dumps()}") + + def dump(self) -> bytes: + """Export the PSSH object as a full PSSH box in bytes form.""" + return Box.build(dict( + type=b"pssh", + version=self.version, + flags=self.flags, + system_ID=self.system_id, + key_IDs=self.key_ids if self.version == 1 and self.key_ids else None, + init_data=self.init_data + )) + + def dumps(self) -> str: + """Export the PSSH object as a full PSSH box in base64 form.""" + return base64.b64encode(self.dump()).decode() + + def to_widevine(self) -> None: + """ + Convert PlayReady PSSH data to Widevine PSSH data. + + There's only a limited amount of information within a PlayReady PSSH header that + can be used in a Widevine PSSH Header. The converted data may or may not result + in an accepted PSSH. It depends on what the License Server is expecting. + """ + if self.system_id == PSSH.SystemId.Widevine: + raise ValueError("This is already a Widevine PSSH") + + widevine_pssh_data = WidevinePsshData( + key_ids=[x.bytes for x in self.key_ids], + algorithm="AESCTR" + ) + + if self.version == 1: + # ensure both cenc header and box has same Key IDs + # v1 uses both this and within init data for basically no reason + self.__key_ids = self.key_ids + + self.init_data = widevine_pssh_data.SerializeToString() + self.system_id = PSSH.SystemId.Widevine + + def to_playready( + self, + la_url: Optional[str] = None, + lui_url: Optional[str] = None, + ds_id: Optional[bytes] = None, + decryptor_setup: Optional[str] = None, + custom_data: Optional[str] = None + ) -> None: + """ + Convert Widevine PSSH data to PlayReady v4.3.0.0 PSSH data. + + Note that it is impossible to create the CHECKSUM values for AES-CTR Key IDs + as you must encrypt the Key ID with the Content Encryption Key using AES-ECB. + This may cause software incompatibilities. + + Parameters: + la_url: Contains the URL for the license acquisition Web service. + Only absolute URLs are allowed. + lui_url: Contains the URL for the license acquisition Web service. + Only absolute URLs are allowed. + ds_id: Service ID for the domain service. + decryptor_setup: This tag may only contain the value "ONDEMAND". It + indicates to an application that it should not expect the full + license chain for the content to be available for acquisition, or + already present on the client machine, prior to setting up the + media graph. If this tag is not set then it indicates that an + application can enforce the license to be acquired, or already + present on the client machine, prior to setting up the media graph. + custom_data: The content author can add custom XML inside this + element. Microsoft code does not act on any data contained inside + this element. The Syntax of this params XML is not validated. + """ + if self.system_id == PSSH.SystemId.PlayReady: + raise ValueError("This is already a PlayReady PSSH") + + key_ids_xml = "" + for key_id in self.key_ids: + # Note that it's impossible to create the CHECKSUM value without the Key for the KID + key_ids_xml += f""" + <KID ALGID="AESCTR" VALUE="{base64.b64encode(key_id.bytes).decode()}"></KID> + """ + + prr_value = f""" + <WRMHEADER xmlns="http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader" version="4.3.0.0"> + <DATA> + <PROTECTINFO> + <KIDS>{key_ids_xml}</KIDS> + </PROTECTINFO> + {'<LA_URL>%s</LA_URL>' % la_url if la_url else ''} + {'<LUI_URL>%s</LUI_URL>' % lui_url if lui_url else ''} + {'<DS_ID>%s</DS_ID>' % base64.b64encode(ds_id).decode() if ds_id else ''} + {'<DECRYPTORSETUP>%s</DECRYPTORSETUP>' % decryptor_setup if decryptor_setup else ''} + {'<CUSTOMATTRIBUTES xmlns="">%s</CUSTOMATTRIBUTES>' % custom_data if custom_data else ''} + </DATA> + </WRMHEADER> + """.encode("utf-16-le") + + prr_length = len(prr_value).to_bytes(2, "little") + prr_type = (1).to_bytes(2, "little") # Has PlayReadyHeader + pro_record_count = (1).to_bytes(2, "little") + pro = pro_record_count + prr_type + prr_length + prr_value + pro = (len(pro) + 4).to_bytes(4, "little") + pro + + self.init_data = pro + self.system_id = PSSH.SystemId.PlayReady + + def set_key_ids(self, key_ids: list[Union[UUID, str, bytes]]) -> None: + """Overwrite all Key IDs with the specified Key IDs.""" + if self.system_id != PSSH.SystemId.Widevine: + # TODO: Add support for setting the Key IDs in a PlayReady Header + raise ValueError(f"Only Widevine PSSH Boxes are supported, not {self.system_id}.") + + key_id_uuids = self.parse_key_ids(key_ids) + + if self.version == 1 or self.__key_ids: + # only use v1 box key_ids if version is 1, or it's already being used + # this is in case the service stupidly expects it for version 0 + self.__key_ids = key_id_uuids + + cenc_header = WidevinePsshData() + cenc_header.ParseFromString(self.init_data) + + cenc_header.key_ids[:] = [ + key_id.bytes + for key_id in key_id_uuids + ] + + self.init_data = cenc_header.SerializeToString() + + @staticmethod + def parse_key_ids(key_ids: list[Union[UUID, str, bytes]]) -> list[UUID]: + """ + Parse a list of Key IDs in hex, base64, or bytes to UUIDs. + + Raises TypeError if `key_ids` is not a list, or the list contains one + or more items that are not a UUID, str, or bytes object. + """ + if not isinstance(key_ids, list): + raise TypeError(f"Expected key_ids to be a list, not {key_ids!r}") + + if not all(isinstance(x, (UUID, str, bytes)) for x in key_ids): + raise TypeError("Some items of key_ids are not a UUID, str, or bytes. Unsure how to continue...") + + uuids = [ + UUID(bytes=key_id_b) + for key_id in key_ids + for key_id_b in [ + key_id.bytes if isinstance(key_id, UUID) else + ( + bytes.fromhex(key_id) if all(c in string.hexdigits for c in key_id) else + base64.b64decode(key_id) + ) if isinstance(key_id, str) else + key_id + ] + ] + + return uuids + + +__all__ = ("PSSH",) diff --git a/scripts/pywidevine/pywidevine/remotecdm.py b/scripts/pywidevine/pywidevine/remotecdm.py new file mode 100644 index 0000000..7e79ce0 --- /dev/null +++ b/scripts/pywidevine/pywidevine/remotecdm.py @@ -0,0 +1,300 @@ +from __future__ import annotations + +import base64 +import binascii +import re +from typing import Optional, Union + +import requests +from Crypto.Hash import SHA1 +from Crypto.PublicKey import RSA +from Crypto.Signature import pss +from google.protobuf.message import DecodeError + +from pywidevine.cdm import Cdm +from pywidevine.device import Device, DeviceTypes +from pywidevine.exceptions import (DeviceMismatch, InvalidInitData, InvalidLicenseMessage, InvalidLicenseType, + SignatureMismatch) +from pywidevine.key import Key +from pywidevine.license_protocol_pb2 import (ClientIdentification, License, LicenseType, SignedDrmCertificate, + SignedMessage) +from pywidevine.pssh import PSSH + + +class RemoteCdm(Cdm): + """Remote Accessible CDM using pywidevine's serve schema.""" + + def __init__( + self, + device_type: Union[DeviceTypes, str], + system_id: int, + security_level: int, + host: str, + secret: str, + device_name: str + ): + """Initialize a Widevine Content Decryption Module (CDM).""" + if not device_type: + raise ValueError("Device Type must be provided") + if isinstance(device_type, str): + device_type = DeviceTypes[device_type] + if not isinstance(device_type, DeviceTypes): + raise TypeError(f"Expected device_type to be a {DeviceTypes!r} not {device_type!r}") + + if not system_id: + raise ValueError("System ID must be provided") + if not isinstance(system_id, int): + raise TypeError(f"Expected system_id to be a {int} not {system_id!r}") + + if not security_level: + raise ValueError("Security Level must be provided") + if not isinstance(security_level, int): + raise TypeError(f"Expected security_level to be a {int} not {security_level!r}") + + if not host: + raise ValueError("API Host must be provided") + if not isinstance(host, str): + raise TypeError(f"Expected host to be a {str} not {host!r}") + + if not secret: + raise ValueError("API Secret must be provided") + if not isinstance(secret, str): + raise TypeError(f"Expected secret to be a {str} not {secret!r}") + + if not device_name: + raise ValueError("API Device name must be provided") + if not isinstance(device_name, str): + raise TypeError(f"Expected device_name to be a {str} not {device_name!r}") + + self.device_type = device_type + self.system_id = system_id + self.security_level = security_level + self.host = host + self.device_name = device_name + + # spoof client_id and rsa_key just so we can construct via super call + super().__init__(device_type, system_id, security_level, ClientIdentification(), RSA.generate(2048)) + + self.__session = requests.Session() + self.__session.headers.update({ + "X-Secret-Key": secret + }) + + r = requests.head(self.host) + if r.status_code != 200: + raise ValueError(f"Could not test Remote API version [{r.status_code}]") + server = r.headers.get("Server") + if not server or "pywidevine serve" not in server.lower(): + raise ValueError(f"This Remote CDM API does not seem to be a pywidevine serve API ({server}).") + server_version_re = re.search(r"pywidevine serve v([\d.]+)", server, re.IGNORECASE) + if not server_version_re: + raise ValueError("The pywidevine server API is not stating the version correctly, cannot continue.") + server_version = server_version_re.group(1) + if server_version < "1.4.3": + raise ValueError(f"This pywidevine serve API version ({server_version}) is not supported.") + + @classmethod + def from_device(cls, device: Device) -> RemoteCdm: + raise NotImplementedError("You cannot load a RemoteCdm from a local Device file.") + + def open(self) -> bytes: + r = self.__session.get( + url=f"{self.host}/{self.device_name}/open" + ).json() + if r['status'] != 200: + raise ValueError(f"Cannot Open CDM Session, {r['message']} [{r['status']}]") + r = r["data"] + + if int(r["device"]["system_id"]) != self.system_id: + raise DeviceMismatch("The System ID specified does not match the one specified in the API response.") + + if int(r["device"]["security_level"]) != self.security_level: + raise DeviceMismatch("The Security Level specified does not match the one specified in the API response.") + + return bytes.fromhex(r["session_id"]) + + def close(self, session_id: bytes) -> None: + r = self.__session.get( + url=f"{self.host}/{self.device_name}/close/{session_id.hex()}" + ).json() + if r["status"] != 200: + raise ValueError(f"Cannot Close CDM Session, {r['message']} [{r['status']}]") + + def set_service_certificate(self, session_id: bytes, certificate: Optional[Union[bytes, str]]) -> str: + if certificate is None: + certificate_b64 = None + elif isinstance(certificate, str): + certificate_b64 = certificate # assuming base64 + elif isinstance(certificate, bytes): + certificate_b64 = base64.b64encode(certificate).decode() + else: + raise DecodeError(f"Expecting Certificate to be base64 or bytes, not {certificate!r}") + + r = self.__session.post( + url=f"{self.host}/{self.device_name}/set_service_certificate", + json={ + "session_id": session_id.hex(), + "certificate": certificate_b64 + } + ).json() + if r["status"] != 200: + raise ValueError(f"Cannot Set CDMs Service Certificate, {r['message']} [{r['status']}]") + r = r["data"] + + return r["provider_id"] + + def get_service_certificate(self, session_id: bytes) -> Optional[SignedDrmCertificate]: + r = self.__session.post( + url=f"{self.host}/{self.device_name}/get_service_certificate", + json={ + "session_id": session_id.hex() + } + ).json() + if r["status"] != 200: + raise ValueError(f"Cannot Get CDMs Service Certificate, {r['message']} [{r['status']}]") + r = r["data"] + + service_certificate = r["service_certificate"] + if not service_certificate: + return None + + service_certificate = base64.b64decode(service_certificate) + signed_drm_certificate = SignedDrmCertificate() + + try: + signed_drm_certificate.ParseFromString(service_certificate) + if signed_drm_certificate.SerializeToString() != service_certificate: + raise DecodeError("partial parse") + except DecodeError as e: + # could be a direct unsigned DrmCertificate, but reject those anyway + raise DecodeError(f"Could not parse certificate as a SignedDrmCertificate, {e}") + + try: + pss. \ + new(RSA.import_key(self.root_cert.public_key)). \ + verify( + msg_hash=SHA1.new(signed_drm_certificate.drm_certificate), + signature=signed_drm_certificate.signature + ) + except (ValueError, TypeError): + raise SignatureMismatch("Signature Mismatch on SignedDrmCertificate, rejecting certificate") + + return signed_drm_certificate + + def get_license_challenge( + self, + session_id: bytes, + pssh: PSSH, + license_type: str = "STREAMING", + privacy_mode: bool = True + ) -> bytes: + if not pssh: + raise InvalidInitData("A pssh must be provided.") + if not isinstance(pssh, PSSH): + raise InvalidInitData(f"Expected pssh to be a {PSSH}, not {pssh!r}") + + if not isinstance(license_type, str): + raise InvalidLicenseType(f"Expected license_type to be a {str}, not {license_type!r}") + if license_type not in LicenseType.keys(): + raise InvalidLicenseType( + f"Invalid license_type value of '{license_type}'. " + f"Available values: {LicenseType.keys()}" + ) + + r = self.__session.post( + url=f"{self.host}/{self.device_name}/get_license_challenge/{license_type}", + json={ + "session_id": session_id.hex(), + "init_data": pssh.dumps(), + "privacy_mode": privacy_mode + } + ).json() + if r["status"] != 200: + raise ValueError(f"Cannot get Challenge, {r['message']} [{r['status']}]") + r = r["data"] + + try: + challenge = base64.b64decode(r["challenge_b64"]) + license_message = SignedMessage() + license_message.ParseFromString(challenge) + if license_message.SerializeToString() != challenge: + raise DecodeError("partial parse") + except DecodeError as e: + raise InvalidLicenseMessage(f"Failed to parse license request, {e}") + + return license_message.SerializeToString() + + def parse_license(self, session_id: bytes, license_message: Union[SignedMessage, bytes, str]) -> None: + if not license_message: + raise InvalidLicenseMessage("Cannot parse an empty license_message") + + if isinstance(license_message, str): + try: + license_message = base64.b64decode(license_message) + except (binascii.Error, binascii.Incomplete) as e: + raise InvalidLicenseMessage(f"Could not decode license_message as Base64, {e}") + + if isinstance(license_message, bytes): + signed_message = SignedMessage() + try: + signed_message.ParseFromString(license_message) + if signed_message.SerializeToString() != license_message: + raise DecodeError("partial parse") + except DecodeError as e: + raise InvalidLicenseMessage(f"Could not parse license_message as a SignedMessage, {e}") + license_message = signed_message + + if not isinstance(license_message, SignedMessage): + raise InvalidLicenseMessage(f"Expecting license_response to be a SignedMessage, got {license_message!r}") + + if license_message.type != SignedMessage.MessageType.Value("LICENSE"): + raise InvalidLicenseMessage( + f"Expecting a LICENSE message, not a " + f"'{SignedMessage.MessageType.Name(license_message.type)}' message." + ) + + r = self.__session.post( + url=f"{self.host}/{self.device_name}/parse_license", + json={ + "session_id": session_id.hex(), + "license_message": base64.b64encode(license_message.SerializeToString()).decode() + } + ).json() + if r["status"] != 200: + raise ValueError(f"Cannot parse License, {r['message']} [{r['status']}]") + + def get_keys(self, session_id: bytes, type_: Optional[Union[int, str]] = None) -> list[Key]: + try: + if isinstance(type_, str): + License.KeyContainer.KeyType.Value(type_) # only test + elif isinstance(type_, int): + type_ = License.KeyContainer.KeyType.Name(type_) + elif type_ is None: + type_ = "ALL" + else: + raise TypeError(f"Expected type_ to be a {License.KeyContainer.KeyType} or int, not {type_!r}") + except ValueError as e: + raise ValueError(f"Could not parse type_ as a {License.KeyContainer.KeyType}, {e}") + + r = self.__session.post( + url=f"{self.host}/{self.device_name}/get_keys/{type_}", + json={ + "session_id": session_id.hex() + } + ).json() + if r["status"] != 200: + raise ValueError(f"Could not get {type_} Keys, {r['message']} [{r['status']}]") + r = r["data"] + + return [ + Key( + type_=key["type"], + kid=Key.kid_to_uuid(bytes.fromhex(key["key_id"])), + key=bytes.fromhex(key["key"]), + permissions=key["permissions"] + ) + for key in r["keys"] + ] + + +__all__ = ("RemoteCdm",) diff --git a/scripts/pywidevine/pywidevine/serve.py b/scripts/pywidevine/pywidevine/serve.py new file mode 100644 index 0000000..07b0602 --- /dev/null +++ b/scripts/pywidevine/pywidevine/serve.py @@ -0,0 +1,458 @@ +import base64 +import sys +from pathlib import Path +from typing import Any, Optional, Union + +from aiohttp.typedefs import Handler +from google.protobuf.message import DecodeError + +from pywidevine.pssh import PSSH + +try: + from aiohttp import web +except ImportError: + print( + "Missing the extra dependencies for serve functionality. " + "You may install them under poetry with `poetry install -E serve`, " + "or under pip with `pip install pywidevine[serve]`." + ) + sys.exit(1) + +from pywidevine import __version__ +from pywidevine.cdm import Cdm +from pywidevine.device import Device +from pywidevine.exceptions import (InvalidContext, InvalidInitData, InvalidLicenseMessage, InvalidLicenseType, + InvalidSession, SignatureMismatch, TooManySessions) + +routes = web.RouteTableDef() + + +async def _startup(app: web.Application) -> None: + app["cdms"] = {} + app["config"]["devices"] = { + path.stem: path + for x in app["config"]["devices"] + for path in [Path(x)] + } + for device in app["config"]["devices"].values(): + if not device.is_file(): + raise FileNotFoundError(f"Device file does not exist: {device}") + + +async def _cleanup(app: web.Application) -> None: + app["cdms"].clear() + del app["cdms"] + app["config"].clear() + del app["config"] + + +@routes.get("/") +async def ping(_: Any) -> web.Response: + return web.json_response({ + "status": 200, + "message": "Pong!" + }) + + +@routes.get("/{device}/open") +async def open_(request: web.Request) -> web.Response: + secret_key = request.headers["X-Secret-Key"] + device_name = request.match_info["device"] + user = request.app["config"]["users"][secret_key] + + if device_name not in user["devices"] or device_name not in request.app["config"]["devices"]: + # we don't want to be verbose with the error as to not reveal device names + # by trial and error to users that are not authorized to use them + return web.json_response({ + "status": 403, + "message": f"Device '{device_name}' is not found or you are not authorized to use it." + }, status=403) + + cdm: Optional[Cdm] = request.app["cdms"].get((secret_key, device_name)) + if not cdm: + device = Device.load(request.app["config"]["devices"][device_name]) + cdm = request.app["cdms"][(secret_key, device_name)] = Cdm.from_device(device) + + try: + session_id = cdm.open() + except TooManySessions as e: + return web.json_response({ + "status": 400, + "message": str(e) + }, status=400) + + return web.json_response({ + "status": 200, + "message": "Success", + "data": { + "session_id": session_id.hex(), + "device": { + "system_id": cdm.system_id, + "security_level": cdm.security_level + } + } + }) + + +@routes.get("/{device}/close/{session_id}") +async def close(request: web.Request) -> web.Response: + secret_key = request.headers["X-Secret-Key"] + device_name = request.match_info["device"] + session_id = bytes.fromhex(request.match_info["session_id"]) + + cdm: Optional[Cdm] = request.app["cdms"].get((secret_key, device_name)) + if not cdm: + return web.json_response({ + "status": 400, + "message": f"No Cdm session for {device_name} has been opened yet. No session to close." + }, status=400) + + try: + cdm.close(session_id) + except InvalidSession: + return web.json_response({ + "status": 400, + "message": f"Invalid Session ID '{session_id.hex()}', it may have expired." + }, status=400) + + return web.json_response({ + "status": 200, + "message": f"Successfully closed Session '{session_id.hex()}'." + }) + + +@routes.post("/{device}/set_service_certificate") +async def set_service_certificate(request: web.Request) -> web.Response: + secret_key = request.headers["X-Secret-Key"] + device_name = request.match_info["device"] + + body = await request.json() + for required_field in ("session_id", "certificate"): + if required_field == "certificate": + has_field = required_field in body # it needs the key, but can be empty/null + else: + has_field = body.get(required_field) + if not has_field: + return web.json_response({ + "status": 400, + "message": f"Missing required field '{required_field}' in JSON body." + }, status=400) + + # get session id + session_id = bytes.fromhex(body["session_id"]) + + # get cdm + cdm: Optional[Cdm] = request.app["cdms"].get((secret_key, device_name)) + if not cdm: + return web.json_response({ + "status": 400, + "message": f"No Cdm session for {device_name} has been opened yet. No session to use." + }, status=400) + + # set service certificate + certificate = body.get("certificate") + try: + provider_id = cdm.set_service_certificate(session_id, certificate) + except InvalidSession: + return web.json_response({ + "status": 400, + "message": f"Invalid Session ID '{session_id.hex()}', it may have expired." + }, status=400) + except DecodeError as e: + return web.json_response({ + "status": 400, + "message": f"Invalid Service Certificate, {e}" + }, status=400) + except SignatureMismatch: + return web.json_response({ + "status": 400, + "message": "Signature Validation failed on the Service Certificate, rejecting." + }, status=400) + + return web.json_response({ + "status": 200, + "message": f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.", + "data": { + "provider_id": provider_id + } + }) + + +@routes.post("/{device}/get_service_certificate") +async def get_service_certificate(request: web.Request) -> web.Response: + secret_key = request.headers["X-Secret-Key"] + device_name = request.match_info["device"] + + body = await request.json() + for required_field in ("session_id",): + if not body.get(required_field): + return web.json_response({ + "status": 400, + "message": f"Missing required field '{required_field}' in JSON body." + }, status=400) + + # get session id + session_id = bytes.fromhex(body["session_id"]) + + # get cdm + cdm: Optional[Cdm] = request.app["cdms"].get((secret_key, device_name)) + if not cdm: + return web.json_response({ + "status": 400, + "message": f"No Cdm session for {device_name} has been opened yet. No session to use." + }, status=400) + + # get service certificate + try: + service_certificate = cdm.get_service_certificate(session_id) + except InvalidSession: + return web.json_response({ + "status": 400, + "message": f"Invalid Session ID '{session_id.hex()}', it may have expired." + }, status=400) + + if service_certificate: + service_certificate_b64 = base64.b64encode(service_certificate.SerializeToString()).decode() + else: + service_certificate_b64 = None + + return web.json_response({ + "status": 200, + "message": "Successfully got the Service Certificate.", + "data": { + "service_certificate": service_certificate_b64 + } + }) + + +@routes.post("/{device}/get_license_challenge/{license_type}") +async def get_license_challenge(request: web.Request) -> web.Response: + secret_key = request.headers["X-Secret-Key"] + device_name = request.match_info["device"] + license_type = request.match_info["license_type"] + + body = await request.json() + for required_field in ("session_id", "init_data"): + if not body.get(required_field): + return web.json_response({ + "status": 400, + "message": f"Missing required field '{required_field}' in JSON body." + }, status=400) + + # get session id + session_id = bytes.fromhex(body["session_id"]) + + # get privacy mode flag + privacy_mode = body.get("privacy_mode", True) + + # get cdm + cdm: Optional[Cdm] = request.app["cdms"].get((secret_key, device_name)) + if not cdm: + return web.json_response({ + "status": 400, + "message": f"No Cdm session for {device_name} has been opened yet. No session to use." + }, status=400) + + # enforce service certificate (opt-in) + if request.app["config"].get("force_privacy_mode"): + privacy_mode = True + if not cdm.get_service_certificate(session_id): + return web.json_response({ + "status": 403, + "message": "No Service Certificate set but Privacy Mode is Enforced." + }, status=403) + + # get init data + init_data = PSSH(body["init_data"]) + + # get challenge + try: + license_request = cdm.get_license_challenge( + session_id=session_id, + pssh=init_data, + license_type=license_type, + privacy_mode=privacy_mode + ) + except InvalidSession: + return web.json_response({ + "status": 400, + "message": f"Invalid Session ID '{session_id.hex()}', it may have expired." + }, status=400) + except InvalidInitData as e: + return web.json_response({ + "status": 400, + "message": f"Invalid Init Data, {e}" + }, status=400) + except InvalidLicenseType: + return web.json_response({ + "status": 400, + "message": f"Invalid License Type '{license_type}'" + }, status=400) + + return web.json_response({ + "status": 200, + "message": "Success", + "data": { + "challenge_b64": base64.b64encode(license_request).decode() + } + }, status=200) + + +@routes.post("/{device}/parse_license") +async def parse_license(request: web.Request) -> web.Response: + secret_key = request.headers["X-Secret-Key"] + device_name = request.match_info["device"] + + body = await request.json() + for required_field in ("session_id", "license_message"): + if not body.get(required_field): + return web.json_response({ + "status": 400, + "message": f"Missing required field '{required_field}' in JSON body." + }, status=400) + + # get session id + session_id = bytes.fromhex(body["session_id"]) + + # get cdm + cdm: Optional[Cdm] = request.app["cdms"].get((secret_key, device_name)) + if not cdm: + return web.json_response({ + "status": 400, + "message": f"No Cdm session for {device_name} has been opened yet. No session to use." + }, status=400) + + # parse the license message + try: + cdm.parse_license(session_id, body["license_message"]) + except InvalidSession: + return web.json_response({ + "status": 400, + "message": f"Invalid Session ID '{session_id.hex()}', it may have expired." + }, status=400) + except InvalidLicenseMessage as e: + return web.json_response({ + "status": 400, + "message": f"Invalid License Message, {e}" + }, status=400) + except InvalidContext as e: + return web.json_response({ + "status": 400, + "message": f"Invalid Context, {e}" + }, status=400) + except SignatureMismatch: + return web.json_response({ + "status": 400, + "message": "Signature Validation failed on the License Message, rejecting." + }, status=400) + + return web.json_response({ + "status": 200, + "message": "Successfully parsed and loaded the Keys from the License message." + }) + + +@routes.post("/{device}/get_keys/{key_type}") +async def get_keys(request: web.Request) -> web.Response: + secret_key = request.headers["X-Secret-Key"] + device_name = request.match_info["device"] + + body = await request.json() + for required_field in ("session_id",): + if not body.get(required_field): + return web.json_response({ + "status": 400, + "message": f"Missing required field '{required_field}' in JSON body." + }, status=400) + + # get session id + session_id = bytes.fromhex(body["session_id"]) + + # get key type + key_type: Optional[str] = request.match_info["key_type"] + if key_type == "ALL": + key_type = None + + # get cdm + cdm = request.app["cdms"].get((secret_key, device_name)) + if not cdm: + return web.json_response({ + "status": 400, + "message": f"No Cdm session for {device_name} has been opened yet. No session to use." + }, status=400) + + # get keys + try: + keys = cdm.get_keys(session_id, key_type) + except InvalidSession: + return web.json_response({ + "status": 400, + "message": f"Invalid Session ID '{session_id.hex()}', it may have expired." + }, status=400) + except ValueError as e: + return web.json_response({ + "status": 400, + "message": f"The Key Type value '{key_type}' is invalid, {e}" + }, status=400) + + # get the keys in json form + keys_json = [ + { + "key_id": key.kid.hex, + "key": key.key.hex(), + "type": key.type, + "permissions": key.permissions, + } + for key in keys + if not key_type or key.type == key_type + ] + + return web.json_response({ + "status": 200, + "message": "Success", + "data": { + "keys": keys_json + } + }) + + +@web.middleware +async def authentication(request: web.Request, handler: Handler) -> web.Response: + secret_key = request.headers.get("X-Secret-Key") + + if request.path != "/" and not secret_key: + request.app.logger.debug(f"{request.remote} did not provide authorization.") + response = web.json_response({ + "status": "401", + "message": "Secret Key is Empty." + }, status=401) + elif request.path != "/" and secret_key not in request.app["config"]["users"]: + request.app.logger.debug(f"{request.remote} failed authentication with '{secret_key}'.") + response = web.json_response({ + "status": "401", + "message": "Secret Key is Invalid, the Key is case-sensitive." + }, status=401) + else: + try: + response = await handler(request) # type: ignore[assignment] + except web.HTTPException as e: + request.app.logger.error(f"An unexpected error has occurred, {e}") + response = web.json_response({ + "status": 500, + "message": e.reason + }, status=500) + + response.headers.update({ + "Server": f"https://github.com/devine-dl/pywidevine serve v{__version__}" + }) + + return response + + +def run(config: dict, host: Optional[Union[str, web.HostSequence]] = None, port: Optional[int] = None) -> None: + app = web.Application(middlewares=[authentication]) + app.on_startup.append(_startup) + app.on_cleanup.append(_cleanup) + app.add_routes(routes) + app["config"] = config + web.run_app(app, host=host, port=port) diff --git a/scripts/pywidevine/pywidevine/session.py b/scripts/pywidevine/pywidevine/session.py new file mode 100644 index 0000000..7cb816f --- /dev/null +++ b/scripts/pywidevine/pywidevine/session.py @@ -0,0 +1,18 @@ +from typing import Optional + +from Crypto.Random import get_random_bytes + +from pywidevine.key import Key +from pywidevine.license_protocol_pb2 import SignedDrmCertificate + + +class Session: + def __init__(self, number: int): + self.number = number + self.id = get_random_bytes(16) + self.service_certificate: Optional[SignedDrmCertificate] = None + self.context: dict[bytes, tuple[bytes, bytes]] = {} + self.keys: list[Key] = [] + + +__all__ = ("Session",) diff --git a/scripts/pywidevine/pywidevine/utils.py b/scripts/pywidevine/pywidevine/utils.py new file mode 100644 index 0000000..6556d3e --- /dev/null +++ b/scripts/pywidevine/pywidevine/utils.py @@ -0,0 +1,12 @@ +import shutil +from pathlib import Path +from typing import Optional + + +def get_binary_path(*names: str) -> Optional[Path]: + """Get the path of the first found binary name.""" + for name in names: + path = shutil.which(name) + if path: + return Path(path) + return None diff --git a/venv.cmd b/venv.cmd new file mode 100644 index 0000000..9ff4941 --- /dev/null +++ b/venv.cmd @@ -0,0 +1 @@ +cmd /k .\.venv\Scripts\activate.bat \ No newline at end of file diff --git a/vinetrimmer.spec b/vinetrimmer.spec new file mode 100644 index 0000000..57b736d --- /dev/null +++ b/vinetrimmer.spec @@ -0,0 +1,44 @@ +# -*- mode: python ; coding: utf-8 -*- + + +block_cipher = None + + +a = Analysis(['vinetrimmer\\vinetrimmer.py'], + pathex=['E:\\PitbullTools\\VT-0.1.0'], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=True, + win_private_assemblies=True, + cipher=block_cipher, + noarchive=False) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) + +exe = EXE(pyz, + a.scripts, + [], + exclude_binaries=True, + name='vinetrimmer', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True, + disable_windowed_traceback=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None , icon='assets\\icon.ico') +coll = COLLECT(exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='vinetrimmer') diff --git a/vinetrimmer/__init__.py b/vinetrimmer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vinetrimmer/certs/bbciplayer.pem b/vinetrimmer/certs/bbciplayer.pem new file mode 100644 index 0000000..bafc6fb --- /dev/null +++ b/vinetrimmer/certs/bbciplayer.pem @@ -0,0 +1,53 @@ +-----BEGIN CERTIFICATE----- +MIIEOzCCAyOgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBozELMAkGA1UEBhMCVVMx +EzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCUN1cGVydGlubzEeMBwGA1UE +CxMVUHJvZCBSb290IENlcnRpZmljYXRlMRkwFwYDVQQLExBEaWdpdGFsIFByb2R1 +Y3RzMQ8wDQYDVQQKEwZBbWF6b24xHzAdBgNVBAMTFkFtYXpvbiBGaXJlVFYgUm9v +dENBMDEwHhcNMTQxMDE1MDA1ODI2WhcNMzQxMDEwMDA1ODI2WjCBmTELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCUN1cGVydGlubzEd +MBsGA1UECxMURGV2IFJvb3QgQ2VydGlmaWNhdGUxGTAXBgNVBAsTEERpZ2l0YWwg +UHJvZHVjdHMxDzANBgNVBAoTBkFtYXpvbjEWMBQGA1UEAxMNRmlyZVRWUHJvZDAw +MTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAMDVS5L0UDxZs06JF2Wv +ndMJj7HTdeJX9oImYh7k+McED5vy9063JysqdKKlo5IdDockns84THV6SBVAAi0D +zpB8ttI5AA3Ywv1YP2b98iCqv9hPjVgtOg4qo1vd+J1tWHIHyfEzqiOEuW59Uwlh +UNaocrmdcqlg2Zb2gUrm6vvVjQ8Yr43coL6pA2NDIsrOFxsFYiwZVMvp6j2Y8vqk +E8rvNm8srdcAaf4Wtpnah2gtAcr+u5X4LYva0O6k4hD4Gg4vPClPgBWl6EHtAvqC +FZoJl8L53vUV5AhLB7JBM0Q1WTDH5k85cXOkEwN648ngOaeKO0ljbwYTnv4xCWce +6EsCAQOjgYMwgYAwHwYDVR0jBBgwFoAUZ6DRIJSK+hfX+GVprqinlc+i5fgwHQYD +VR0OBBYEFNyCOfHckuirZvAqzO0Wn6KNkeGPMAkGA1UdEwQCMAAwEwYDVR0lBAww +CgYIKwYBBQUHAwIwEQYJYIZIAYb4QgEBBAQDAgeAMAsGA1UdDwQEAwIHgDANBgkq +hkiG9w0BAQUFAAOCAQEAvWPwxoUaWr0WKWExGtzP8IFUE+f+9IFcK3hYyvBlK9LN +7boVdxqubFxH30Sf8/tVsX1JA9C7ns3gOcWftu13yKs+DgdhjtnFVH+in36EidFA +QG35PMOSIm4cZUy00N1EtpTjFceAnauf5IM6MfdAZT4Esl/ONPzyTbXttBVZABk1 +WWeG0G047TVUz3b+kGNU3sdK9F/h6dbKw4k7eebLf/J66JJydAHrlXIuWzGkCn1j +35gGDyPj7y06V5uz2U3b9Le7YXCg6BBjpQ7L+Egw9UlJjh7ZQ1u6GdB5A0paV3EP +PNMJ7bzFIupz3vIOvNgQUxek5IEHs6Jywc5prrNLKw== +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDA1UuS9FA8WbNO +iRdlr53TCY+x03XiV/aCJmIe5PjHBA+b8vdOtycrKnSipaOSHQ6HJJ7POEx1ekgV +QAItA86QfLbSOQAN2ML9WD9m/fIgqr/YT41YLToOKqNb3fidbVhyB8nxM6ojhLlu +fVMJYVDWqHK5nXKpYNmW9oFK5ur71Y0PGK+N3KC+qQNjQyLKzhcbBWIsGVTL6eo9 +mPL6pBPK7zZvLK3XAGn+FraZ2odoLQHK/ruV+C2L2tDupOIQ+BoOLzwpT4AVpehB +7QL6ghWaCZfC+d71FeQISweyQTNENVkwx+ZPOXFzpBMDeuPJ4DmnijtJY28GE57+ +MQlnHuhLAgEDAoIBAQCAjjJh+DV9kSI0W2TudRPiBl/L4k6W5U8BbuwUmKXaArUS +ofo0ehocxvhsbm0ME18EwxSKJYhOUYVjgAFzV98K/c820Kqz5ddTkCpEqUwVxyqQ +NQjlc3wJccI9PqW+SOWhWoagzRwXrdD0U4yw64s5xaHRE6HGQJEPTwDcmfH9N+BW +/uU8aW5AfNpxjG7nHatrhIB55p6n4sE5EhN0gJOV09f0GNoZPQXbOUUpBV9McCal +l+USjZAFdHmIjXPpGQDzRIY5bcXUC0YbTpk+QJhkgTcImKDRfwABatHvyLx9iiV5 +tefhWXah18I7dmAwNdS7E8BZh/wy2R05t4Dzib9rAoGBAOSnrep2nMUEP25wRAdA +3X51zv08SKZHzoEn4LQKY+/89TTF8vVKl0B6KYiZamxibjSTmh4BXr8fwQk+bk1B +QxFwduFy7u1N7wHR5N9XAi4Knjh1A+GqoRb88nN7oXmzC7q6stVQROix2eEQIY5o +bDFTzYZFyh4iLvE4n5uZuG/RAoGBANfk7G08obZZrk1IrHUvRBeD76Q49sCIR0dA +HSHBf0ZtPD27FHFmjaC7F0ZC6AwTFpM/AMX4xRZj6xFjYmbyD4cw0ZFgO+oJpf1H +Ej3GHwL4qYzBEQwQNk0JOFlO8p7U2mY/hDUs7lbPBo6aJ8UZI0k7HxR9TVaXnwHu +/ixgF9lbAoGBAJhvyUb5vditf571gq+Ak6mj31N9hcQv3wDFQHgG7Uqoo3iD904x +uir8G7BmRvLsSXhiZr6rlH9qgLDUSYjWLLZK+euh9J4z9KvhQz+PVslcaXr4rUEc +a2SooaJSa6Z3XScnIeOK2JshO+tgFl7wSCDiiQQuhr7BdKDQame70EqLAoGBAI/t +8vN9wSQ7yYjbHaN02A+tSm17TysFhNoqvhaA/4RI0tPSDaDvCRXSD4QsmrK3ZGIq +AIP7Lg7tR2Ds7EShX6914QuVfUaxGVOEttPZagH7G7MrYLK1eYiw0DuJ9xSN5u7V +As4d9DnfWbRmxS4QwjDSFLhTiORlFKv0qXLqupDnAoGAeEkrxJ8IiwaTHgYym3mS +kShyjsV+MmVBlTsQ+FZn1S3y4aWqlDa5KLdT/X10Ax84sPNemAQU0exa3t8s9lwH ++OsDjKKoxjCT7Kl3rGPyAHJrfVVySeEeX+8DKdVJr0rSPdBI8ckECy3AzlVja+ww +ROCtzc1TurxnNA5qWD3n3f4= +-----END PRIVATE KEY----- diff --git a/vinetrimmer/commands/__init__.py b/vinetrimmer/commands/__init__.py new file mode 100644 index 0000000..e520dfb --- /dev/null +++ b/vinetrimmer/commands/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from vinetrimmer.commands.dl import dl diff --git a/vinetrimmer/commands/dl.py b/vinetrimmer/commands/dl.py new file mode 100644 index 0000000..871c3e4 --- /dev/null +++ b/vinetrimmer/commands/dl.py @@ -0,0 +1,700 @@ +import base64 +import html +import logging +import os +import shutil +import subprocess +import sys +import traceback +from http.cookiejar import MozillaCookieJar + +import time +import click +import requests +from appdirs import AppDirs +from langcodes import Language +from pymediainfo import MediaInfo +from pathlib import Path + +from vinetrimmer import services +from vinetrimmer.config import Config, config, credentials, directories, filenames +from vinetrimmer.objects import AudioTrack, Credential, TextTrack, Title, Titles, VideoTrack +from vinetrimmer.objects.tracks import Track +from vinetrimmer.objects.vaults import InsertResult, Vault, Vaults +from vinetrimmer.utils import is_close_match +from vinetrimmer.utils.click import (AliasedGroup, ContextData, acodec_param, language_param, quality_param, + range_param, vcodec_param, wanted_param) +from vinetrimmer.utils.collections import as_list, merge_dict +from vinetrimmer.utils.io import load_yaml +from pywidevine import Device, Cdm, RemoteCdm + +from vinetrimmer.vendor.pymp4.parser import Box + +from pyplayready.cdm import Cdm as CdmPr +from pyplayready import Device as DevicePR +from pyplayready.system.pssh import PSSH +from pyplayready.crypto.ecc_key import ECCKey +from pyplayready.system.bcert import CertificateChain, Certificate +from Crypto.Random import get_random_bytes + + +def reprovision_device(prd_path) -> None: + """ + Reprovision a Playready Device (.prd) by creating a new leaf certificate and new encryption/signing keys. + Will override the device if an output path or directory is not specified + + Only works on PRD Devices of v3 or higher + """ + prd_path = Path(prd_path) + if not prd_path.is_file(): + raise Exception("prd_path: Not a path to a file, or it doesn't exist.") + + device = DevicePR.load(prd_path) + + if device.group_key is None: + raise Exception("Device does not support reprovisioning, re-create it or use a Device with a version of 3 or higher") + + device.group_certificate.remove(0) + + encryption_key = ECCKey.generate() + signing_key = ECCKey.generate() + + device.encryption_key = encryption_key + device.signing_key = signing_key + + new_certificate = Certificate.new_leaf_cert( + cert_id=get_random_bytes(16), + security_level=device.group_certificate.get_security_level(), + client_id=get_random_bytes(16), + signing_key=signing_key, + encryption_key=encryption_key, + group_key=device.group_key, + parent=device.group_certificate + ) + device.group_certificate.prepend(new_certificate) + + prd_path.parent.mkdir(parents=True, exist_ok=True) + prd_path.write_bytes(device.dumps()) + + +def get_cdm(log, service, profile=None, cdm_name=None): + """ + Get CDM Device (either remote or local) for a specified service. + Raises a ValueError if there's a problem getting a CDM. + """ + if not cdm_name: + cdm_name = config.cdm.get(service) or config.cdm.get("default") + if not cdm_name: + raise ValueError("A CDM to use wasn't listed in the vinetrimmer.yml config") + if isinstance(cdm_name, dict): + if not profile: + raise ValueError("CDM config is mapped for profiles, but no profile was chosen") + cdm_name = cdm_name.get(profile) or config.cdm.get("default") + if not cdm_name: + raise ValueError(f"A CDM to use was not mapped for the profile {profile}") + + try: + try: + device = Device.load(os.path.join(directories.devices, f"{cdm_name}.wvd")) + except: + device_path = os.path.abspath(os.path.join(directories.devices, f"{cdm_name}.prd")) + if ( int( time.time() ) - int( os.path.getmtime( device_path ) ) ) > 600000: #roughly a week + try: + reprovision_device(device_path) + log.info(f" + Reprovisioned Playready Device (.prd) file, {cdm_name}") + except Exception as e: + log.warning(f"Reprovision Failed - {e}") + device = DevicePR.load(device_path) + return device + except FileNotFoundError: + try: + return Device.from_dir(os.path.join(directories.devices, cdm_name)) + except: + pass + + cdm_api = next(iter(x for x in config.cdm_api if x["name"] == cdm_name), None) + if cdm_api: + try: + return RemoteCdm(**cdm_api) + except: + from vinetrimmer.utils.widevine.device import RemoteDevice + return RemoteDevice(**cdm_api) # seller distributed some wack version of serve in pywidevine + + raise ValueError(f"Device {cdm_name!r} not found") + + +def get_service_config(service): + """Get both service config and service secrets as one merged dictionary.""" + service_config = load_yaml(filenames.service_config.format(service=service.lower())) + + user_config = (load_yaml(filenames.user_service_config.format(service=service.lower())) + or load_yaml(filenames.user_service_config.format(service=service))) + + if user_config: + merge_dict(service_config, user_config) + + return service_config + + +def get_profile(service): + """ + Get the default profile for a service from the config. + """ + profile = config.profiles.get(service) + if profile is False: + return None # auth-less service if `false` in config + if not profile: + profile = config.profiles.get("default") + if not profile: + raise ValueError(f"No profile has been defined for '{service}' in the config.") + + return profile + + +def get_cookie_jar(service, profile): + """Get the profile's cookies if available.""" + cookie_file = os.path.join(directories.cookies, service.lower(), f"{profile}.txt") + if not os.path.isfile(cookie_file): + cookie_file = os.path.join(directories.cookies, service, f"{profile}.txt") + if os.path.isfile(cookie_file): + cookie_jar = MozillaCookieJar(cookie_file) + with open(cookie_file, "r+", encoding="utf-8") as fd: + unescaped = html.unescape(fd.read()) + fd.seek(0) + fd.truncate() + fd.write(unescaped) + cookie_jar.load(ignore_discard=True, ignore_expires=True) + return cookie_jar + return None + + +def get_credentials(service, profile="default"): + """Get the profile's credentials if available.""" + cred = credentials.get(service, {}) + + if isinstance(cred, dict): + cred = cred.get(profile) + elif profile != "default": + return None + + if cred: + if isinstance(cred, list): + return Credential(*cred) + else: + return Credential.loads(cred) + + +@click.group(name="dl", short_help="Download from a service.", cls=AliasedGroup, context_settings=dict( + help_option_names=["-?", "-h", "--help"], + max_content_width=116, # max PEP8 line-width, -4 to adjust for initial indent + default_map=config.arguments +)) +@click.option("--debug", is_flag=True, hidden=True) # Handled by vinetrimmer.py +@click.option("-p", "--profile", type=str, default=None, + help="Profile to use when multiple profiles are defined for a service.") +@click.option("-q", "--quality", callback=quality_param, default=None, + help="Download Resolution, defaults to best available.") +@click.option("-v", "--vcodec", callback=vcodec_param, default="H264", + help="Video Codec, defaults to H264.") +@click.option("-a", "--acodec", callback=acodec_param, default=None, + help="Audio Codec") +@click.option("-vb", "--vbitrate", "vbitrate", type=int, + default=None, + help="Video Bitrate, defaults to Max.") +@click.option("-ab", "--abitrate", "abitrate", type=int, + default=None, + help="Audio Bitrate, defaults to Max.") +@click.option("-aa", "--atmos", is_flag=True, default=False, + help="Prefer Atmos Audio") +@click.option("-r", "--range", "range_", callback=range_param, default="SDR", + help="Video Color Range, defaults to SDR.") +@click.option("-w", "--wanted", callback=wanted_param, default=None, + help="Wanted episodes, e.g. `S01-S05,S07`, `S01E01-S02E03`, `S02-S02E03`, e.t.c, defaults to all.") +@click.option("-al", "--alang", callback=language_param, default="orig", + help="Language wanted for audio.") +@click.option("-sl", "--slang", callback=language_param, default="all", + help="Language wanted for subtitles.") +@click.option("--proxy", type=str, default=None, + help="Proxy URI to use. If a 2-letter country is provided, it will try get a proxy from the config.") +@click.option("-A", "--audio-only", is_flag=True, default=False, + help="Only download audio tracks.") +@click.option("-S", "--subs-only", is_flag=True, default=False, + help="Only download subtitle tracks.") +@click.option("-C", "--chapters-only", is_flag=True, default=False, + help="Only download chapters.") +@click.option("-ns", "--no-subs", is_flag=True, default=False, + help="Do not download subtitle tracks.") +@click.option("-na", "--no-audio", is_flag=True, default=False, + help="Do not download audio tracks.") +@click.option("-nv", "--no-video", is_flag=True, default=False, + help="Do not download video tracks.") +@click.option("-nc", "--no-chapters", is_flag=True, default=False, + help="Do not download chapters tracks.") +@click.option("-ad", "--audio-description", is_flag=True, default=False, + help="Download audio description tracks.") +@click.option("--list", "list_", is_flag=True, default=False, + help="Skip downloading and list available tracks and what tracks would have been downloaded.") +@click.option("--selected", is_flag=True, default=False, + help="List selected tracks and what tracks are downloaded.") +@click.option("--cdm", type=str, default=None, + help="Override the CDM that will be used for decryption.") +@click.option("--keys", is_flag=True, default=False, + help="Skip downloading, retrieve the decryption keys (via CDM or Key Vaults) and print them.") +@click.option("--cache", is_flag=True, default=False, + help="Disable the use of the CDM and only retrieve decryption keys from Key Vaults. " + "If a needed key is unable to be retrieved from any Key Vaults, the title is skipped.") +@click.option("--no-cache", is_flag=True, default=False, + help="Disable the use of Key Vaults and only retrieve decryption keys from the CDM.") +@click.option("--no-proxy", is_flag=True, default=False, + help="Force disable all proxy use.") +@click.option("-nm", "--no-mux", is_flag=True, default=False, + help="Do not mux the downloaded and decrypted tracks.") +@click.option("--mux", is_flag=True, default=False, + help="Force muxing when using --audio-only/--subs-only/--chapters-only.") +@click.pass_context +def dl(ctx, profile, cdm, *_, **__): + log = logging.getLogger("dl") + + service = ctx.params.get("service_name") or services.get_service_key(ctx.invoked_subcommand) + if not service: + log.exit(" - Unable to find service") + + profile = profile or get_profile(service) + service_config = get_service_config(service) + vaults = [] + for vault in config.key_vaults: + try: + vaults.append(Config.load_vault(vault)) + except Exception as e: + log.error(f" - Failed to load vault {vault['name']!r}: {e}") + vaults = Vaults(vaults, service=service) + local_vaults = sum(v.type == Vault.Types.LOCAL for v in vaults) + remote_vaults = sum(v.type == Vault.Types.REMOTE for v in vaults) + log.info(f" + {local_vaults} Local Vault{'' if local_vaults == 1 else 's'}") + log.info(f" + {remote_vaults} Remote Vault{'' if remote_vaults == 1 else 's'}") + + try: + device = get_cdm(log, service, profile, cdm) + except ValueError as e: + raise log.exit(f" - {e}") + + device_name = device.system_id if "set_service_certificate" in dir(device) else device.get_name() + log.info(f" + Loaded {device.__class__.__name__}: {device_name} (L{device.security_level})") + cdm = Cdm(device) if "set_service_certificate" in dir(device) else CdmPr.from_device(device) + + if profile: + cookies = get_cookie_jar(service, profile) + credentials = get_credentials(service, profile) + if not cookies and not credentials and service_config.get("needs_auth", True): + raise log.exit(f" - Profile {profile!r} has no cookies or credentials") + else: + cookies = None + credentials = None + + ctx.obj = ContextData( + config=service_config, + vaults=vaults, + cdm=cdm, + profile=profile, + cookies=cookies, + credentials=credentials, + ) + + +@dl.result_callback() +@click.pass_context +def result(ctx, service, quality, range_, wanted, alang, slang, audio_only, subs_only, chapters_only, audio_description, + list_, keys, cache, no_cache, no_subs, no_audio, no_video, no_chapters, atmos, vbitrate: int, abitrate: int, no_mux, mux, selected, *_, **__): + def ccextractor(): + log.info("Extracting EIA-608 captions from stream with CCExtractor") + track_id = f"ccextractor-{track.id}" + # TODO: Is it possible to determine the language of EIA-608 captions? + cc_lang = track.language + try: + cc = track.ccextractor( + track_id=track_id, + out_path=filenames.subtitles.format(id=track_id, language_code=cc_lang), + language=cc_lang, + original=False, + ) + except EnvironmentError: + log.warning(" - CCExtractor not found, cannot extract captions") + else: + if cc: + title.tracks.add(cc) + log.info(" + Extracted") + else: + log.info(" + No captions found") + + log = service.log + + service_name = service.__class__.__name__ + + log.info("Retrieving Titles") + try: + titles = Titles(as_list(service.get_titles())) + except requests.HTTPError as e: + log.debug(traceback.format_exc()) + raise log.exit(f" - HTTP Error {e.response.status_code}: {e.response.reason}") + if not titles: + raise log.exit(" - No titles returned!") + titles.order() + titles.print() + + for title in titles.with_wanted(wanted): + if title.type == Title.Types.TV: + log.info("Getting tracks for {title} S{season:02}E{episode:02}{name} [{id}]".format( + title=title.name, + season=title.season or 0, + episode=title.episode or 0, + name=f" - {title.episode_name}" if title.episode_name else "", + id=title.id, + )) + else: + log.info("Getting tracks for {title}{year} [{id}]".format( + title=title.name, + year=f" ({title.year})" if title.year else "", + id=title.id, + )) + + try: + title.tracks.add(service.get_tracks(title), warn_only=True) + title.tracks.add(service.get_chapters(title)) + #if not next((x for x in title.tracks.videos if x.language.language in (v_lang or lang)), None) and title.tracks.videos: + # lang = [title.tracks.videos[0].language["language"]] + # log.info("Defaulting language to {lang} for tracks".format(lang=lang[0].upper())) + except requests.HTTPError as e: + log.debug(traceback.format_exc()) + raise log.exit(f" - HTTP Error {e.response.status_code}: {e.response.reason}") + title.tracks.sort_videos() + title.tracks.sort_audios(by_language=alang) + title.tracks.sort_subtitles(by_language=slang) + title.tracks.sort_chapters() + + for track in title.tracks: + if track.language == Language.get("none"): + track.language = title.original_lang + track.is_original_lang = is_close_match(track.language, [title.original_lang]) + + if not list(title.tracks): + log.error(" - No tracks returned!") + continue + if not selected: + log.info("> All Tracks:") + title.tracks.print() + + try: + #title.tracks.select_videos(by_quality=quality, by_range=range_, one_only=True) + title.tracks.select_videos(by_quality=quality, by_vbitrate=vbitrate, by_range=range_, one_only=True) + title.tracks.select_audios(by_language=alang, by_bitrate=abitrate, with_descriptive=audio_description) + # title.tracks.select_audios(by_language=alang, with_descriptive=audio_description) + title.tracks.select_subtitles(by_language=slang, with_forced=True) + except ValueError as e: + log.error(f" - {e}") + continue + + if no_video: + title.tracks.videos.clear() + if no_audio: + title.tracks.audios.clear() + if no_subs: + title.tracks.subtitles.clear() + if no_chapters: + title.tracks.chapters.clear() + if audio_only or subs_only or chapters_only: + title.tracks.videos.clear() + if audio_only: + if not subs_only: + title.tracks.subtitles.clear() + if not chapters_only: + title.tracks.chapters.clear() + elif subs_only: + if not audio_only: + title.tracks.audios.clear() + if not chapters_only: + title.tracks.chapters.clear() + elif chapters_only: + if not audio_only: + title.tracks.audios.clear() + if not subs_only: + title.tracks.subtitles.clear() + + if not mux: + no_mux = True + + log.info("> Selected Tracks:") + title.tracks.print() + + + if list_: + continue # only wanted to see what tracks were available and chosen + + skip_title = False + for track in title.tracks: + if not keys: + log.info(f"Downloading: {track}") + if (service_name == "AppleTVPlus" or service_name == "iTunes") and "VID" in str(track): + track.encrypted = True + if track.encrypted: + if not track.get_pssh(service.session): + raise log.exit(" - Failed to get PSSH") + log.info(f" + PSSH: {track.pssh}") + if not track.get_kid(service.session): + raise log.exit(" - Failed to get KID") + log.info(f" + KID: {track.kid}") + if not keys: + if track.needs_proxy: + proxy = next(iter(service.session.proxies.values()), None) + else: + proxy = None + track.download(directories.temp, headers=service.session.headers, proxy=proxy) + log.info(" + Downloaded") + if isinstance(track, VideoTrack) and track.needs_ccextractor_first and not no_subs: + ccextractor() + if track.encrypted: + log.info("Decrypting...") + if track.key: + log.info(f" + KEY: {track.key} (Static)") + elif not no_cache: + track.key, vault_used = ctx.obj.vaults.get(track.kid, title.id) + if track.key: + log.info(f" + KEY: {track.key} (From {vault_used.name} {vault_used.type.name} Key Vault)") + for vault in ctx.obj.vaults.vaults: + if vault == vault_used: + continue + result = ctx.obj.vaults.insert_key( + vault, service_name.lower(), track.kid, track.key, title.id, commit=True + ) + if result == InsertResult.SUCCESS: + log.info(f" + Cached to {vault} vault") + elif result == InsertResult.ALREADY_EXISTS: + log.info(f" + Already exists in {vault} vault") + if not track.key: + if cache: + skip_title = True + break + if "common_privacy_cert" in dir(ctx.obj.cdm): + session_id = ctx.obj.cdm.open(track.pssh) + log.info(f"CDM Session ID - {session_id.hex()}") + ctx.obj.cdm.set_service_certificate( + session_id, + service.certificate( + challenge=ctx.obj.cdm.service_certificate_challenge, + title=title, + track=track, + session_id=session_id + ) or ctx.obj.cdm.common_privacy_cert + ) + ctx.obj.cdm.parse_license( + session_id, + service.license( + challenge=ctx.obj.cdm.get_license_challenge(session_id), + title=title, + track=track, + session_id=session_id + ) + ) + else: + session_id = ctx.obj.cdm.open() + log.info(f"CDM Session ID - {session_id.hex()}") + wrm_header = PSSH(track.pssh).get_wrm_headers()[0] + challenge = ctx.obj.cdm.get_license_challenge(session_id, wrm_header) + #log.info(f"{wrm_header} --> {challenge}") + licence = service.license( + challenge=challenge, + title=title, + track=track, + ) + log.debug(licence) + ctx.obj.cdm.parse_license( + session_id, + licence # expects the XML License not base64 encoded str. + ) + + + content_keys = [ + (x.kid, x.key) for x in ctx.obj.cdm.get_keys(session_id, content_only=True) + ] if "common_privacy_cert" in dir(ctx.obj.cdm) else [ + (str(x.key_id).replace("-", ""), x.key.hex()) for x in ctx.obj.cdm.get_keys(session_id) + ] + + ctx.obj.cdm.close(session_id) + + if not content_keys: + raise log.exit(" - No content keys were returned by the CDM!") + log.info(f" + Obtained content keys from the CDM") + + for kid, key in content_keys: + if kid == "b770d5b4bb6b594daf985845aae9aa5f": + # Amazon HDCP test key + continue + log.info(f" + {kid}:{key}") + + # cache keys into all key vaults + for vault in ctx.obj.vaults.vaults: + log.info(f"Caching to {vault} vault") + cached = 0 + already_exists = 0 + for kid, key in content_keys: + result = ctx.obj.vaults.insert_key(vault, service_name.lower(), kid, key, title.id) + if result == InsertResult.FAILURE: + log.warning(f" - Failed, table {service_name.lower()} doesn't exist in the vault.") + elif result == InsertResult.SUCCESS: + cached += 1 + elif result == InsertResult.ALREADY_EXISTS: + already_exists += 1 + ctx.obj.vaults.commit(vault) + log.info(f" + Cached {cached}/{len(content_keys)} keys") + if already_exists: + log.info(f" + {already_exists}/{len(content_keys)} keys already existed in vault") + if cached + already_exists < len(content_keys): + log.warning(f" Failed to cache {len(content_keys) - cached - already_exists} keys") + # use matching content key for the tracks key id + track.key = next((key for kid, key in content_keys if kid == track.kid), None) + if track.key: + log.info(f" + KEY: {track.key} (From CDM)") + else: + raise log.exit(f" - No content key with the key ID \"{track.kid}\" was returned") + if keys: + continue + # TODO: Move decryption code to Track + if not config.decrypter: + raise log.exit(" - No decrypter specified") + if config.decrypter == "packager" and not (track.descriptor == Track.Descriptor.ISM): + platform = {"win32": "win", "darwin": "osx"}.get(sys.platform, sys.platform) + names = ["shaka-packager", "packager", f"packager-{platform}"] + executable = next((x for x in (shutil.which(x) for x in names) if x), None) + if not executable: + raise log.exit(" - Unable to find packager binary") + dec = os.path.splitext(track.locate())[0] + ".dec.mp4" + + os.makedirs(directories.temp, exist_ok=True) + try: + os.makedirs(directories.temp, exist_ok=True) + subprocess.run([ + executable, + "input={},stream={},output={}".format( + track.locate(), + track.__class__.__name__.lower().replace("track", ""), + dec + ), + "--enable_raw_key_decryption", "--keys", + ",".join([ + f"label=0:key_id={track.kid.lower()}:key={track.key.lower()}", + # Apple TV+ needs this as shaka pulls the incorrect KID, idk why + f"label=1:key_id=00000000000000000000000000000000:key={track.key.lower()}", + ]), + "--temp_dir", directories.temp + ], check=True) + except subprocess.CalledProcessError: + raise log.exit(" - Failed!") + + elif config.decrypter == "mp4decrypt" or (track.descriptor == Track.Descriptor.ISM): + executable = shutil.which("mp4decrypt") + if not executable: + raise log.exit(" - Unable to find mp4decrypt binary") + dec = os.path.splitext(track.locate())[0] + ".dec.mp4" + try: + subprocess.run([ + executable, + "--show-progress", + "--key", f"{track.kid.lower()}:{track.key.lower()}", + track.locate(), + dec, + ]) + except subprocess.CalledProcessError: + raise log.exit(" - Failed!") + else: + log.exit(f" - Unsupported decrypter: {config.decrypter}") + track.swap(dec) + log.info(" + Decrypted") + + if keys: + continue + + if track.needs_repack or (config.decrypter == "mp4decrypt" and isinstance(track, (VideoTrack, AudioTrack))): + log.info("Repackaging stream with FFmpeg (to fix malformed streams)") + track.repackage() + log.info(" + Repackaged") + + if isinstance(track, VideoTrack) and track.needs_ccextractor and not no_subs: + ccextractor() + if skip_title: + for track in title.tracks: + track.delete() + continue + if keys: + continue + if not list(title.tracks) and not title.tracks.chapters: + continue + # mux all final tracks to a single mkv file + if no_mux: + if title.tracks.chapters: + final_file_path = directories.downloads + if title.type == Title.Types.TV: + final_file_path = os.path.join(final_file_path, title.parse_filename(folder=True)) + os.makedirs(final_file_path, exist_ok=True) + chapters_loc = filenames.chapters.format(filename=title.filename) + title.tracks.export_chapters(chapters_loc) + shutil.move(chapters_loc, os.path.join(final_file_path, os.path.basename(chapters_loc))) + for track in title.tracks: + media_info = MediaInfo.parse(track.locate()) + #log.debug(media_info) + final_file_path = directories.downloads + if title.type == Title.Types.TV: + final_file_path = os.path.join( + final_file_path, title.parse_filename(folder=True) + ) + os.makedirs(final_file_path, exist_ok=True) + filename = title.parse_filename(media_info=media_info) + if isinstance(track, (AudioTrack, TextTrack)): + filename += f".{track.language}" + extension = track.codec if isinstance(track, TextTrack) else os.path.splitext(track.locate())[1][1:] + if isinstance(track, AudioTrack) and extension == "mp4": + extension = "m4a" + track.move(os.path.join(final_file_path, f"{filename}.{track.id}.{extension}")) + else: + log.info("Muxing tracks into an MKV container") + muxed_location, returncode = title.tracks.mux(title.filename) + if returncode == 1: + log.warning(" - mkvmerge had at least one warning, will continue anyway...") + elif returncode >= 2: + raise log.exit(" - Failed to mux tracks into MKV file") + log.info(" + Muxed") + for track in title.tracks: + track.delete() + if title.tracks.chapters: + try: + os.unlink(filenames.chapters.format(filename=title.filename)) + except FileNotFoundError: + pass + media_info = MediaInfo.parse(muxed_location) + final_file_path = directories.downloads + if title.type == Title.Types.TV: + final_file_path = os.path.join( + final_file_path, title.parse_filename(media_info=media_info, folder=True) + ) + os.makedirs(final_file_path, exist_ok=True) + # rename muxed mkv file with new data from mediainfo data of it + if audio_only: + extension = "mka" + elif subs_only: + extension = "mks" + else: + extension = "mkv" + shutil.move( + muxed_location, + os.path.join(final_file_path, f"{title.parse_filename(media_info=media_info)}.{extension}") + ) + + + log.info("Processed all titles!") + + +def load_services(): + for service in services.__dict__.values(): + if callable(getattr(service, "cli", None)): + dl.add_command(service.cli) + + +load_services() diff --git a/vinetrimmer/commands/dl_1.py b/vinetrimmer/commands/dl_1.py new file mode 100644 index 0000000..3214663 --- /dev/null +++ b/vinetrimmer/commands/dl_1.py @@ -0,0 +1,646 @@ +import base64 +import html +import logging +import os +import shutil +import subprocess +import sys +import traceback +from http.cookiejar import MozillaCookieJar + +import click +import requests +from appdirs import AppDirs +from langcodes import Language +from pymediainfo import MediaInfo + +from vinetrimmer import services +from vinetrimmer.config import Config, config, credentials, directories, filenames +from vinetrimmer.objects import AudioTrack, Credential, TextTrack, Title, Titles, VideoTrack +from vinetrimmer.objects.vaults import InsertResult, Vault, Vaults +from vinetrimmer.utils import Cdm, is_close_match +from vinetrimmer.utils.click import (AliasedGroup, ContextData, acodec_param, language_param, quality_param, + range_param, vcodec_param, wanted_param) +from vinetrimmer.utils.collections import as_list, merge_dict +from vinetrimmer.utils.io import load_yaml +from vinetrimmer.utils.widevine.device import LocalDevice, RemoteDevice +from vinetrimmer.vendor.pymp4.parser import Box + +from vinetrimmer.utils.playready.cdm import Cdm as CdmPr +from vinetrimmer.utils.playready.device import Device +from vinetrimmer.utils.playready.pssh import PSSH + +def get_cdm(service, profile=None, cdm_name=None): + """ + Get CDM Device (either remote or local) for a specified service. + Raises a ValueError if there's a problem getting a CDM. + """ + if not cdm_name: + cdm_name = config.cdm.get(service) or config.cdm.get("default") + if not cdm_name: + raise ValueError("A CDM to use wasn't listed in the vinetrimmer.yml config") + if isinstance(cdm_name, dict): + if not profile: + raise ValueError("CDM config is mapped for profiles, but no profile was chosen") + cdm_name = cdm_name.get(profile) or config.cdm.get("default") + if not cdm_name: + raise ValueError(f"A CDM to use was not mapped for the profile {profile}") + if "sl2000" or "sl3000" in cdm_name.lower(): + + device_pr = Device.load(os.path.join(directories.devices, f"{cdm_name}.prd")) + return device_pr + try: + + return LocalDevice.load(os.path.join(directories.devices, f"{cdm_name}.wvd")) + except FileNotFoundError: + dirs = [ + os.path.join(directories.devices, cdm_name), + os.path.join(AppDirs("pywidevine", False).user_data_dir, "devices", cdm_name), + os.path.join(AppDirs("pywidevine", False).site_data_dir, "devices", cdm_name), + ] + + for d in dirs: + try: + return LocalDevice.from_dir(d) + except FileNotFoundError: + pass + + cdm_api = next(iter(x for x in config.cdm_api if x["name"] == cdm_name), None) + if cdm_api: + return RemoteDevice(**cdm_api) + + raise ValueError(f"Device {cdm_name!r} not found") + + +def get_service_config(service): + """Get both service config and service secrets as one merged dictionary.""" + service_config = load_yaml(filenames.service_config.format(service=service.lower())) + + user_config = (load_yaml(filenames.user_service_config.format(service=service.lower())) + or load_yaml(filenames.user_service_config.format(service=service))) + + if user_config: + merge_dict(service_config, user_config) + + return service_config + + +def get_profile(service): + """ + Get the default profile for a service from the config. + """ + profile = config.profiles.get(service) + if profile is False: + return None # auth-less service if `false` in config + if not profile: + profile = config.profiles.get("default") + if not profile: + raise ValueError(f"No profile has been defined for '{service}' in the config.") + + return profile + + +def get_cookie_jar(service, profile): + """Get the profile's cookies if available.""" + cookie_file = os.path.join(directories.cookies, service.lower(), f"{profile}.txt") + if not os.path.isfile(cookie_file): + cookie_file = os.path.join(directories.cookies, service, f"{profile}.txt") + if os.path.isfile(cookie_file): + cookie_jar = MozillaCookieJar(cookie_file) + with open(cookie_file, "r+", encoding="utf-8") as fd: + unescaped = html.unescape(fd.read()) + fd.seek(0) + fd.truncate() + fd.write(unescaped) + cookie_jar.load(ignore_discard=True, ignore_expires=True) + return cookie_jar + return None + + +def get_credentials(service, profile="default"): + """Get the profile's credentials if available.""" + cred = credentials.get(service, {}) + + if isinstance(cred, dict): + cred = cred.get(profile) + elif profile != "default": + return None + + if cred: + if isinstance(cred, list): + return Credential(*cred) + else: + return Credential.loads(cred) + + +@click.group(name="dl", short_help="Download from a service.", cls=AliasedGroup, context_settings=dict( + help_option_names=["-?", "-h", "--help"], + max_content_width=116, # max PEP8 line-width, -4 to adjust for initial indent + default_map=config.arguments +)) +@click.option("--debug", is_flag=True, hidden=True) # Handled by vinetrimmer.py +@click.option("-p", "--profile", type=str, default=None, + help="Profile to use when multiple profiles are defined for a service.") +@click.option("-q", "--quality", callback=quality_param, default=None, + help="Download Resolution, defaults to best available.") +@click.option("-v", "--vcodec", callback=vcodec_param, default="H264", + help="Video Codec, defaults to H264.") +@click.option("-a", "--acodec", callback=acodec_param, default=None, + help="Audio Codec") +@click.option("-vb", "--vbitrate", "vbitrate", type=int, + default=None, + help="Video Bitrate, defaults to Max.") +@click.option("-ab", "--abitrate", "abitrate", type=int, + default=None, + help="Audio Bitrate, defaults to Max.") +@click.option("-aa", "--atmos", is_flag=True, default=False, + help="Prefer Atmos Audio") +@click.option("-r", "--range", "range_", callback=range_param, default="SDR", + help="Video Color Range, defaults to SDR.") +@click.option("-w", "--wanted", callback=wanted_param, default=None, + help="Wanted episodes, e.g. `S01-S05,S07`, `S01E01-S02E03`, `S02-S02E03`, e.t.c, defaults to all.") +@click.option("-al", "--alang", callback=language_param, default="orig", + help="Language wanted for audio.") +@click.option("-sl", "--slang", callback=language_param, default="all", + help="Language wanted for subtitles.") +@click.option("--proxy", type=str, default=None, + help="Proxy URI to use. If a 2-letter country is provided, it will try get a proxy from the config.") +@click.option("-A", "--audio-only", is_flag=True, default=False, + help="Only download audio tracks.") +@click.option("-S", "--subs-only", is_flag=True, default=False, + help="Only download subtitle tracks.") +@click.option("-C", "--chapters-only", is_flag=True, default=False, + help="Only download chapters.") +@click.option("-ns", "--no-subs", is_flag=True, default=False, + help="Do not download subtitle tracks.") +@click.option("-na", "--no-audio", is_flag=True, default=False, + help="Do not download audio tracks.") +@click.option("-nv", "--no-video", is_flag=True, default=False, + help="Do not download video tracks.") +@click.option("-nc", "--no-chapters", is_flag=True, default=False, + help="Do not download chapters tracks.") +@click.option("-ad", "--audio-description", is_flag=True, default=False, + help="Download audio description tracks.") +@click.option("--list", "list_", is_flag=True, default=False, + help="Skip downloading and list available tracks and what tracks would have been downloaded.") +@click.option("--selected", is_flag=True, default=False, + help="List selected tracks and what tracks are downloaded.") +@click.option("--cdm", type=str, default=None, + help="Override the CDM that will be used for decryption.") +@click.option("--keys", is_flag=True, default=False, + help="Skip downloading, retrieve the decryption keys (via CDM or Key Vaults) and print them.") +@click.option("--cache", is_flag=True, default=False, + help="Disable the use of the CDM and only retrieve decryption keys from Key Vaults. " + "If a needed key is unable to be retrieved from any Key Vaults, the title is skipped.") +@click.option("--no-cache", is_flag=True, default=False, + help="Disable the use of Key Vaults and only retrieve decryption keys from the CDM.") +@click.option("--no-proxy", is_flag=True, default=False, + help="Force disable all proxy use.") +@click.option("-nm", "--no-mux", is_flag=True, default=False, + help="Do not mux the downloaded and decrypted tracks.") +@click.option("--mux", is_flag=True, default=False, + help="Force muxing when using --audio-only/--subs-only/--chapters-only.") +@click.pass_context +def dl(ctx, profile, cdm, *_, **__): + log = logging.getLogger("dl") + + service = ctx.params.get("service_name") or services.get_service_key(ctx.invoked_subcommand) + if not service: + log.exit(" - Unable to find service") + + profile = profile or get_profile(service) + service_config = get_service_config(service) + vaults = [] + for vault in config.key_vaults: + try: + vaults.append(Config.load_vault(vault)) + except Exception as e: + log.error(f" - Failed to load vault {vault['name']!r}: {e}") + vaults = Vaults(vaults, service=service) + local_vaults = sum(v.type == Vault.Types.LOCAL for v in vaults) + remote_vaults = sum(v.type == Vault.Types.REMOTE for v in vaults) + log.info(f" + {local_vaults} Local Vault{'' if local_vaults == 1 else 's'}") + log.info(f" + {remote_vaults} Remote Vault{'' if remote_vaults == 1 else 's'}") + + try: + device = get_cdm(service, profile, cdm) + except ValueError as e: + raise log.exit(f" - {e}") + device_name = device.get_name() if "sl3000" or "sl2000" in device.get_name() else device.system_id + log.info(f" + Loaded {device.__class__.__name__}: {device_name} (L{device.security_level})") + cdm = CdmPr.from_device(device) if "sl3000" or "sl2000" in device.get_name() else Cdm(device) + + if profile: + cookies = get_cookie_jar(service, profile) + credentials = get_credentials(service, profile) + if not cookies and not credentials and service_config.get("needs_auth", True): + raise log.exit(f" - Profile {profile!r} has no cookies or credentials") + else: + cookies = None + credentials = None + + ctx.obj = ContextData( + config=service_config, + vaults=vaults, + cdm=cdm, + profile=profile, + cookies=cookies, + credentials=credentials, + ) + + +@dl.result_callback() +@click.pass_context +def result(ctx, service, quality, range_, wanted, alang, slang, audio_only, subs_only, chapters_only, audio_description, + list_, keys, cache, no_cache, no_subs, no_audio, no_video, no_chapters, atmos, vbitrate: int, abitrate: int, no_mux, mux, selected, *_, **__): + def ccextractor(): + log.info("Extracting EIA-608 captions from stream with CCExtractor") + track_id = f"ccextractor-{track.id}" + # TODO: Is it possible to determine the language of EIA-608 captions? + cc_lang = track.language + try: + cc = track.ccextractor( + track_id=track_id, + out_path=filenames.subtitles.format(id=track_id, language_code=cc_lang), + language=cc_lang, + original=False, + ) + except EnvironmentError: + log.warning(" - CCExtractor not found, cannot extract captions") + else: + if cc: + title.tracks.add(cc) + log.info(" + Extracted") + else: + log.info(" + No captions found") + + log = service.log + + service_name = service.__class__.__name__ + + log.info("Retrieving Titles") + try: + titles = Titles(as_list(service.get_titles())) + except requests.HTTPError as e: + log.debug(traceback.format_exc()) + raise log.exit(f" - HTTP Error {e.response.status_code}: {e.response.reason}") + if not titles: + raise log.exit(" - No titles returned!") + titles.order() + titles.print() + + for title in titles.with_wanted(wanted): + if title.type == Title.Types.TV: + log.info("Getting tracks for {title} S{season:02}E{episode:02}{name} [{id}]".format( + title=title.name, + season=title.season or 0, + episode=title.episode or 0, + name=f" - {title.episode_name}" if title.episode_name else "", + id=title.id, + )) + else: + log.info("Getting tracks for {title}{year} [{id}]".format( + title=title.name, + year=f" ({title.year})" if title.year else "", + id=title.id, + )) + + try: + title.tracks.add(service.get_tracks(title), warn_only=True) + title.tracks.add(service.get_chapters(title)) + #if not next((x for x in title.tracks.videos if x.language.language in (v_lang or lang)), None) and title.tracks.videos: + # lang = [title.tracks.videos[0].language["language"]] + # log.info("Defaulting language to {lang} for tracks".format(lang=lang[0].upper())) + except requests.HTTPError as e: + log.debug(traceback.format_exc()) + raise log.exit(f" - HTTP Error {e.response.status_code}: {e.response.reason}") + title.tracks.sort_videos() + title.tracks.sort_audios(by_language=alang) + title.tracks.sort_subtitles(by_language=slang) + title.tracks.sort_chapters() + + for track in title.tracks: + if track.language == Language.get("none"): + track.language = title.original_lang + track.is_original_lang = is_close_match(track.language, [title.original_lang]) + + if not list(title.tracks): + log.error(" - No tracks returned!") + continue + if not selected: + log.info("> All Tracks:") + title.tracks.print() + + try: + #title.tracks.select_videos(by_quality=quality, by_range=range_, one_only=True) + title.tracks.select_videos(by_quality=quality, by_vbitrate=vbitrate, by_range=range_, one_only=True) + title.tracks.select_audios(by_language=alang, by_bitrate=abitrate, with_descriptive=audio_description) + # title.tracks.select_audios(by_language=alang, with_descriptive=audio_description) + title.tracks.select_subtitles(by_language=slang, with_forced=None) + except ValueError as e: + log.error(f" - {e}") + continue + + if no_video: + title.tracks.videos.clear() + + if no_audio: + title.tracks.audios.clear() + + if no_subs: + title.tracks.subtitles.clear() + + if no_chapters: + title.tracks.chapters.clear() + + if audio_only or subs_only or chapters_only: + title.tracks.videos.clear() + if audio_only: + if not subs_only: + title.tracks.subtitles.clear() + if not chapters_only: + title.tracks.chapters.clear() + elif subs_only: + if not audio_only: + title.tracks.audios.clear() + if not chapters_only: + title.tracks.chapters.clear() + elif chapters_only: + if not audio_only: + title.tracks.audios.clear() + if not subs_only: + title.tracks.subtitles.clear() + + if not mux: + no_mux = True + + log.info("> Selected Tracks:") + title.tracks.print() + + + if list_: + continue # only wanted to see what tracks were available and chosen + + skip_title = False + for track in title.tracks: + if not keys: + log.info(f"Downloading: {track}") + if (service_name == "AppleTVPlus" or service_name == "iTunes") and "VID" in str(track): + track.encrypted = True + if track.encrypted: + if not track.get_pssh(service.session): + raise log.exit(" - Failed to get PSSH") + log.info(f" + PSSH: {track.pssh}") + if not track.get_kid(service.session): + raise log.exit(" - Failed to get KID") + log.info(f" + KID: {track.kid}") + if not keys: + if track.needs_proxy: + proxy = next(iter(service.session.proxies.values()), None) + else: + proxy = None + track.download(directories.temp, headers=service.session.headers, proxy=proxy) + log.info(" + Downloaded") + if isinstance(track, VideoTrack) and track.needs_ccextractor_first and not no_subs: + ccextractor() + if track.encrypted: + log.info("Decrypting...") + if track.key: + log.info(f" + KEY: {track.key} (Static)") + elif not no_cache: + track.key, vault_used = ctx.obj.vaults.get(track.kid, title.id) + if track.key: + log.info(f" + KEY: {track.key} (From {vault_used.name} {vault_used.type.name} Key Vault)") + for vault in ctx.obj.vaults.vaults: + if vault == vault_used: + continue + result = ctx.obj.vaults.insert_key( + vault, service_name.lower(), track.kid, track.key, title.id, commit=True + ) + if result == InsertResult.SUCCESS: + log.info(f" + Cached to {vault} vault") + elif result == InsertResult.ALREADY_EXISTS: + log.info(f" + Already exists in {vault} vault") + if not track.key: + if cache: + skip_title = True + break + try: + if "common_privacy_cert" in list(ctx.obj.cdm.__dict__.keys()): + session_id = ctx.obj.cdm.open(track.pssh) + + ctx.obj.cdm.set_service_certificate( + session_id, + service.certificate( + challenge=ctx.obj.cdm.service_certificate_challenge, + title=title, + track=track, + session_id=session_id + ) or ctx.obj.cdm.common_privacy_cert + ) + ctx.obj.cdm.parse_license( + session_id, + service.license( + challenge=ctx.obj.cdm.get_license_challenge(session_id), + title=title, + track=track, + session_id=session_id + ) + ) + else: + wrm_header = PSSH(track.pssh).wrm_headers[0] + challenge = ctx.obj.cdm.get_license_challenge(wrm_header) + #log.info(f"{wrm_header} --> {challenge}") + ctx.obj.cdm.parse_license( + base64.b64decode( + service.license( + challenge=challenge, + title=title, + track=track, + ).encode("ascii") + ).decode("ascii") + ) + except requests.HTTPError as e: + log.debug(traceback.format_exc()) + raise log.exit(f" - HTTP Error {e.response.status_code}: {e.response.reason}") + + + content_keys = [ + (x.kid, x.key) for x in ctx.obj.cdm.get_keys(session_id, content_only=True) + ] if "common_privacy_cert" in list(ctx.obj.cdm.__dict__.keys()) else [ + (str(x.key_id).replace("-", ""), x.key.hex()) for x in ctx.obj.cdm.get_keys() + ] + if not content_keys: + raise log.exit(" - No content keys were returned by the CDM!") + log.info(f" + Obtained content keys from the CDM") + + for kid, key in content_keys: + if kid == "b770d5b4bb6b594daf985845aae9aa5f": + # Amazon HDCP test key + continue + log.info(f" + {kid}:{key}") + + # cache keys into all key vaults + for vault in ctx.obj.vaults.vaults: + log.info(f"Caching to {vault} vault") + cached = 0 + already_exists = 0 + for kid, key in content_keys: + result = ctx.obj.vaults.insert_key(vault, service_name.lower(), kid, key, title.id) + if result == InsertResult.FAILURE: + log.warning(f" - Failed, table {service_name.lower()} doesn't exist in the vault.") + elif result == InsertResult.SUCCESS: + cached += 1 + elif result == InsertResult.ALREADY_EXISTS: + already_exists += 1 + ctx.obj.vaults.commit(vault) + log.info(f" + Cached {cached}/{len(content_keys)} keys") + if already_exists: + log.info(f" + {already_exists}/{len(content_keys)} keys already existed in vault") + if cached + already_exists < len(content_keys): + log.warning(f" Failed to cache {len(content_keys) - cached - already_exists} keys") + # use matching content key for the tracks key id + track.key = next((key for kid, key in content_keys if kid == track.kid), None) + if track.key: + log.info(f" + KEY: {track.key} (From CDM)") + else: + raise log.exit(f" - No content key with the key ID \"{track.kid}\" was returned") + if keys: + continue + # TODO: Move decryption code to Track + if not config.decrypter: + raise log.exit(" - No decrypter specified") + if config.decrypter == "packager": + platform = {"win32": "win", "darwin": "osx"}.get(sys.platform, sys.platform) + names = ["shaka-packager", "packager", f"packager-{platform}"] + executable = next((x for x in (shutil.which(x) for x in names) if x), None) + if not executable: + raise log.exit(" - Unable to find packager binary") + dec = os.path.splitext(track.locate())[0] + ".dec.mp4" + try: + os.makedirs(directories.temp, exist_ok=True) + subprocess.run([ + executable, + "input={},stream={},output={}".format( + track.locate(), + track.__class__.__name__.lower().replace("track", ""), + dec + ), + "--enable_raw_key_decryption", "--keys", + ",".join([ + f"label=0:key_id={track.kid.lower()}:key={track.key.lower()}", + # Apple TV+ needs this as shaka pulls the incorrect KID, idk why + f"label=1:key_id=00000000000000000000000000000000:key={track.key.lower()}", + ]), + "--temp_dir", directories.temp + ], check=True) + except subprocess.CalledProcessError: + raise log.exit(" - Failed!") + elif config.decrypter == "mp4decrypt": + executable = shutil.which("mp4decrypt") + if not executable: + raise log.exit(" - Unable to find mp4decrypt binary") + dec = os.path.splitext(track.locate())[0] + ".dec.mp4" + try: + subprocess.run([ + executable, + "--show-progress", + "--key", f"{track.kid.lower()}:{track.key.lower()}", + track.locate(), + dec, + ]) + except subprocess.CalledProcessError: + raise log.exit(" - Failed!") + else: + log.exit(f" - Unsupported decrypter: {config.decrypter}") + track.swap(dec) + log.info(" + Decrypted") + + if keys: + continue + + if track.needs_repack or (config.decrypter == "mp4decrypt" and isinstance(track, (VideoTrack, AudioTrack))): + log.info("Repackaging stream with FFmpeg (to fix malformed streams)") + track.repackage() + log.info(" + Repackaged") + + if isinstance(track, VideoTrack) and track.needs_ccextractor and not no_subs: + ccextractor() + if skip_title: + for track in title.tracks: + track.delete() + continue + if keys: + continue + if not list(title.tracks) and not title.tracks.chapters: + continue + # mux all final tracks to a single mkv file + if no_mux: + if title.tracks.chapters: + final_file_path = directories.downloads + if title.type == Title.Types.TV: + final_file_path = os.path.join(final_file_path, title.parse_filename(folder=True)) + os.makedirs(final_file_path, exist_ok=True) + chapters_loc = filenames.chapters.format(filename=title.filename) + title.tracks.export_chapters(chapters_loc) + shutil.move(chapters_loc, os.path.join(final_file_path, os.path.basename(chapters_loc))) + for track in title.tracks: + media_info = MediaInfo.parse(track.locate()) + final_file_path = directories.downloads + if title.type == Title.Types.TV: + final_file_path = os.path.join( + final_file_path, title.parse_filename(folder=True) + ) + os.makedirs(final_file_path, exist_ok=True) + filename = title.parse_filename(media_info=media_info) + if isinstance(track, (AudioTrack, TextTrack)): + filename += f".{track.language}" + extension = track.codec if isinstance(track, TextTrack) else os.path.splitext(track.locate())[1][1:] + if isinstance(track, AudioTrack) and extension == "mp4": + extension = "m4a" + track.move(os.path.join(final_file_path, f"{filename}.{track.id}.{extension}")) + else: + log.info("Muxing tracks into an MKV container") + muxed_location, returncode = title.tracks.mux(title.filename) + if returncode == 1: + log.warning(" - mkvmerge had at least one warning, will continue anyway...") + elif returncode >= 2: + raise log.exit(" - Failed to mux tracks into MKV file") + log.info(" + Muxed") + for track in title.tracks: + track.delete() + if title.tracks.chapters: + try: + os.unlink(filenames.chapters.format(filename=title.filename)) + except FileNotFoundError: + pass + media_info = MediaInfo.parse(muxed_location) + final_file_path = directories.downloads + if title.type == Title.Types.TV: + final_file_path = os.path.join( + final_file_path, title.parse_filename(media_info=media_info, folder=True) + ) + os.makedirs(final_file_path, exist_ok=True) + # rename muxed mkv file with new data from mediainfo data of it + if audio_only: + extension = "mka" + elif subs_only: + extension = "mks" + else: + extension = "mkv" + shutil.move( + muxed_location, + os.path.join(final_file_path, f"{title.parse_filename(media_info=media_info)}.{extension}") + ) + + + log.info("Processed all titles!") + + +def load_services(): + for service in services.__dict__.values(): + if callable(getattr(service, "cli", None)): + dl.add_command(service.cli) + + +load_services() diff --git a/vinetrimmer/commands/dl_2.py b/vinetrimmer/commands/dl_2.py new file mode 100644 index 0000000..e58e136 --- /dev/null +++ b/vinetrimmer/commands/dl_2.py @@ -0,0 +1,656 @@ +import base64 +import html +import logging +import os +import shutil +import subprocess +import sys +import traceback +from http.cookiejar import MozillaCookieJar + +import click +import requests +from appdirs import AppDirs +from langcodes import Language +from pymediainfo import MediaInfo + +from vinetrimmer import services +from vinetrimmer.config import Config, config, credentials, directories, filenames +from vinetrimmer.objects import AudioTrack, Credential, TextTrack, Title, Titles, VideoTrack +from vinetrimmer.objects.vaults import InsertResult, Vault, Vaults +from vinetrimmer.utils import Cdm, is_close_match +from vinetrimmer.utils.click import (AliasedGroup, ContextData, acodec_param, language_param, quality_param, + range_param, vcodec_param, wanted_param) +from vinetrimmer.utils.collections import as_list, merge_dict +from vinetrimmer.utils.io import load_yaml +from vinetrimmer.utils.widevine.device import LocalDevice, RemoteDevice +from vinetrimmer.vendor.pymp4.parser import Box + +from vinetrimmer.utils.playready.cdm import Cdm as CdmPr +from vinetrimmer.utils.playready.device import Device +from vinetrimmer.utils.playready.pssh import PSSH + +def get_cdm(service, profile=None, cdm_name=None): + """ + Get CDM Device (either remote or local) for a specified service. + Raises a ValueError if there's a problem getting a CDM. + """ + if not cdm_name: + cdm_name = config.cdm.get(service) or config.cdm.get("default") + if not cdm_name: + raise ValueError("A CDM to use wasn't listed in the vinetrimmer.yml config") + if isinstance(cdm_name, dict): + if not profile: + raise ValueError("CDM config is mapped for profiles, but no profile was chosen") + cdm_name = cdm_name.get(profile) or config.cdm.get("default") + if not cdm_name: + raise ValueError(f"A CDM to use was not mapped for the profile {profile}") + if "sl2000" or "sl3000" in cdm_name.lower(): + + device_pr = Device.load(os.path.join(directories.devices, f"{cdm_name}.prd")) + return device_pr + try: + + return LocalDevice.load(os.path.join(directories.devices, f"{cdm_name}.wvd")) + except FileNotFoundError: + dirs = [ + os.path.join(directories.devices, cdm_name), + os.path.join(AppDirs("pywidevine", False).user_data_dir, "devices", cdm_name), + os.path.join(AppDirs("pywidevine", False).site_data_dir, "devices", cdm_name), + ] + + for d in dirs: + try: + return LocalDevice.from_dir(d) + except FileNotFoundError: + pass + + cdm_api = next(iter(x for x in config.cdm_api if x["name"] == cdm_name), None) + if cdm_api: + return RemoteDevice(**cdm_api) + + raise ValueError(f"Device {cdm_name!r} not found") + + +def get_service_config(service): + """Get both service config and service secrets as one merged dictionary.""" + service_config = load_yaml(filenames.service_config.format(service=service.lower())) + + user_config = (load_yaml(filenames.user_service_config.format(service=service.lower())) + or load_yaml(filenames.user_service_config.format(service=service))) + + if user_config: + merge_dict(service_config, user_config) + + return service_config + + +def get_profile(service): + """ + Get the default profile for a service from the config. + """ + profile = config.profiles.get(service) + if profile is False: + return None # auth-less service if `false` in config + if not profile: + profile = config.profiles.get("default") + if not profile: + raise ValueError(f"No profile has been defined for '{service}' in the config.") + + return profile + + +def get_cookie_jar(service, profile): + """Get the profile's cookies if available.""" + cookie_file = os.path.join(directories.cookies, service.lower(), f"{profile}.txt") + if not os.path.isfile(cookie_file): + cookie_file = os.path.join(directories.cookies, service, f"{profile}.txt") + if os.path.isfile(cookie_file): + cookie_jar = MozillaCookieJar(cookie_file) + with open(cookie_file, "r+", encoding="utf-8") as fd: + unescaped = html.unescape(fd.read()) + fd.seek(0) + fd.truncate() + fd.write(unescaped) + cookie_jar.load(ignore_discard=True, ignore_expires=True) + return cookie_jar + return None + + +def get_credentials(service, profile="default"): + """Get the profile's credentials if available.""" + cred = credentials.get(service, {}) + + if isinstance(cred, dict): + cred = cred.get(profile) + elif profile != "default": + return None + + if cred: + if isinstance(cred, list): + return Credential(*cred) + else: + return Credential.loads(cred) + + +@click.group(name="dl", short_help="Download from a service.", cls=AliasedGroup, context_settings=dict( + help_option_names=["-?", "-h", "--help"], + max_content_width=116, # max PEP8 line-width, -4 to adjust for initial indent + default_map=config.arguments +)) +@click.option("--debug", is_flag=True, hidden=True) # Handled by vinetrimmer.py +@click.option("-p", "--profile", type=str, default=None, + help="Profile to use when multiple profiles are defined for a service.") +@click.option("-q", "--quality", callback=quality_param, default=None, + help="Download Resolution, defaults to best available.") +@click.option("-v", "--vcodec", callback=vcodec_param, default="H264", + help="Video Codec, defaults to H264.") +@click.option("-a", "--acodec", callback=acodec_param, default=None, + help="Audio Codec") +@click.option("-vb", "--vbitrate", "vbitrate", type=int, + default=None, + help="Video Bitrate, defaults to Max.") +@click.option("-ab", "--abitrate", "abitrate", type=int, + default=None, + help="Audio Bitrate, defaults to Max.") +@click.option("-aa", "--atmos", is_flag=True, default=False, + help="Prefer Atmos Audio") +@click.option("-r", "--range", "range_", callback=range_param, default="SDR", + help="Video Color Range, defaults to SDR.") +@click.option("-w", "--wanted", callback=wanted_param, default=None, + help="Wanted episodes, e.g. `S01-S05,S07`, `S01E01-S02E03`, `S02-S02E03`, e.t.c, defaults to all.") +@click.option("-al", "--alang", callback=language_param, default="orig", + help="Language wanted for audio.") +@click.option("-sl", "--slang", callback=language_param, default="all", + help="Language wanted for subtitles.") +@click.option("--proxy", type=str, default=None, + help="Proxy URI to use. If a 2-letter country is provided, it will try get a proxy from the config.") +@click.option("-A", "--audio-only", is_flag=True, default=False, + help="Only download audio tracks.") +@click.option("-S", "--subs-only", is_flag=True, default=False, + help="Only download subtitle tracks.") +@click.option("-C", "--chapters-only", is_flag=True, default=False, + help="Only download chapters.") +@click.option("-ns", "--no-subs", is_flag=True, default=False, + help="Do not download subtitle tracks.") +@click.option("-na", "--no-audio", is_flag=True, default=False, + help="Do not download audio tracks.") +@click.option("-nv", "--no-video", is_flag=True, default=False, + help="Do not download video tracks.") +@click.option("-nc", "--no-chapters", is_flag=True, default=False, + help="Do not download chapters tracks.") +@click.option("-ad", "--audio-description", is_flag=True, default=False, + help="Download audio description tracks.") +@click.option("--list", "list_", is_flag=True, default=False, + help="Skip downloading and list available tracks and what tracks would have been downloaded.") +@click.option("--selected", is_flag=True, default=False, + help="List selected tracks and what tracks are downloaded.") +@click.option("--cdm", type=str, default=None, + help="Override the CDM that will be used for decryption.") +@click.option("--keys", is_flag=True, default=False, + help="Skip downloading, retrieve the decryption keys (via CDM or Key Vaults) and print them.") +@click.option("--cache", is_flag=True, default=False, + help="Disable the use of the CDM and only retrieve decryption keys from Key Vaults. " + "If a needed key is unable to be retrieved from any Key Vaults, the title is skipped.") +@click.option("--no-cache", is_flag=True, default=False, + help="Disable the use of Key Vaults and only retrieve decryption keys from the CDM.") +@click.option("--no-proxy", is_flag=True, default=False, + help="Force disable all proxy use.") +@click.option("-nm", "--no-mux", is_flag=True, default=False, + help="Do not mux the downloaded and decrypted tracks.") +@click.option("--mux", is_flag=True, default=False, + help="Force muxing when using --audio-only/--subs-only/--chapters-only.") +@click.pass_context +def dl(ctx, profile, cdm, *_, **__): + log = logging.getLogger("dl") + + service = ctx.params.get("service_name") or services.get_service_key(ctx.invoked_subcommand) + if not service: + log.exit(" - Unable to find service") + + profile = profile or get_profile(service) + service_config = get_service_config(service) + vaults = [] + for vault in config.key_vaults: + try: + vaults.append(Config.load_vault(vault)) + except Exception as e: + log.error(f" - Failed to load vault {vault['name']!r}: {e}") + vaults = Vaults(vaults, service=service) + local_vaults = sum(v.type == Vault.Types.LOCAL for v in vaults) + remote_vaults = sum(v.type == Vault.Types.REMOTE for v in vaults) + log.info(f" + {local_vaults} Local Vault{'' if local_vaults == 1 else 's'}") + log.info(f" + {remote_vaults} Remote Vault{'' if remote_vaults == 1 else 's'}") + + try: + device = get_cdm(service, profile, cdm) + except ValueError as e: + raise log.exit(f" - {e}") + device_name = device.get_name() if "sl3000" or "sl2000" in device.get_name() else device.system_id + log.info(f" + Loaded {device.__class__.__name__}: {device_name} (L{device.security_level})") + cdm = CdmPr.from_device(device) if "sl3000" or "sl2000" in device.get_name() else Cdm(device) + + if profile: + cookies = get_cookie_jar(service, profile) + credentials = get_credentials(service, profile) + if not cookies and not credentials and service_config.get("needs_auth", True): + raise log.exit(f" - Profile {profile!r} has no cookies or credentials") + else: + cookies = None + credentials = None + + ctx.obj = ContextData( + config=service_config, + vaults=vaults, + cdm=cdm, + profile=profile, + cookies=cookies, + credentials=credentials, + ) + + +@dl.result_callback() +@click.pass_context +def result(ctx, service, quality, range_, wanted, alang, slang, audio_only, subs_only, chapters_only, audio_description, + list_, keys, cache, no_cache, no_subs, no_audio, no_video, no_chapters, atmos, vbitrate: int, abitrate: int, no_mux, mux, selected, *_, **__): + def ccextractor(): + log.info("Extracting EIA-608 captions from stream with CCExtractor") + track_id = f"ccextractor-{track.id}" + # TODO: Is it possible to determine the language of EIA-608 captions? + cc_lang = track.language + try: + cc = track.ccextractor( + track_id=track_id, + out_path=filenames.subtitles.format(id=track_id, language_code=cc_lang), + language=cc_lang, + original=False, + ) + except EnvironmentError: + log.warning(" - CCExtractor not found, cannot extract captions") + else: + if cc: + title.tracks.add(cc) + log.info(" + Extracted") + else: + log.info(" + No captions found") + + log = service.log + + service_name = service.__class__.__name__ + + log.info("Retrieving Titles") + try: + titles = Titles(as_list(service.get_titles())) + except requests.HTTPError as e: + log.debug(traceback.format_exc()) + raise log.exit(f" - HTTP Error {e.response.status_code}: {e.response.reason}") + if not titles: + raise log.exit(" - No titles returned!") + titles.order() + titles.print() + + for title in titles.with_wanted(wanted): + if title.type == Title.Types.TV: + log.info("Getting tracks for {title} S{season:02}E{episode:02}{name} [{id}]".format( + title=title.name, + season=title.season or 0, + episode=title.episode or 0, + name=f" - {title.episode_name}" if title.episode_name else "", + id=title.id, + )) + else: + log.info("Getting tracks for {title}{year} [{id}]".format( + title=title.name, + year=f" ({title.year})" if title.year else "", + id=title.id, + )) + + try: + title.tracks.add(service.get_tracks(title), warn_only=True) + title.tracks.add(service.get_chapters(title)) + #if not next((x for x in title.tracks.videos if x.language.language in (v_lang or lang)), None) and title.tracks.videos: + # lang = [title.tracks.videos[0].language["language"]] + # log.info("Defaulting language to {lang} for tracks".format(lang=lang[0].upper())) + except requests.HTTPError as e: + log.debug(traceback.format_exc()) + raise log.exit(f" - HTTP Error {e.response.status_code}: {e.response.reason}") + title.tracks.sort_videos() + title.tracks.sort_audios(by_language=alang) + title.tracks.sort_subtitles(by_language=slang) + title.tracks.sort_chapters() + + for track in title.tracks: + if track.language == Language.get("none"): + track.language = title.original_lang + track.is_original_lang = is_close_match(track.language, [title.original_lang]) + + if not list(title.tracks): + log.error(" - No tracks returned!") + continue + if not selected: + log.info("> All Tracks:") + title.tracks.print() + + try: + #title.tracks.select_videos(by_quality=quality, by_range=range_, one_only=True) + title.tracks.select_videos(by_quality=quality, by_vbitrate=vbitrate, by_range=range_, one_only=True) + title.tracks.select_audios(by_language=alang, by_bitrate=abitrate, with_descriptive=audio_description) + # title.tracks.select_audios(by_language=alang, with_descriptive=audio_description) + title.tracks.select_subtitles(by_language=slang, with_forced=True) + except ValueError as e: + log.error(f" - {e}") + continue + + if no_video: + title.tracks.videos.clear() + + if no_audio: + title.tracks.audios.clear() + + if no_subs: + title.tracks.subtitles.clear() + + if no_chapters: + title.tracks.chapters.clear() + + if audio_only or subs_only or chapters_only: + title.tracks.videos.clear() + if audio_only: + if not subs_only: + title.tracks.subtitles.clear() + if not chapters_only: + title.tracks.chapters.clear() + elif subs_only: + if not audio_only: + title.tracks.audios.clear() + if not chapters_only: + title.tracks.chapters.clear() + elif chapters_only: + if not audio_only: + title.tracks.audios.clear() + if not subs_only: + title.tracks.subtitles.clear() + + if not mux: + no_mux = True + + log.info("> Selected Tracks:") + title.tracks.print() + + + if list_: + continue # only wanted to see what tracks were available and chosen + + skip_title = False + for track in title.tracks: + if not keys: + log.info(f"Downloading: {track}") + if (service_name == "AppleTVPlus" or service_name == "iTunes") and "VID" in str(track): + track.encrypted = True + if track.encrypted: + if not track.get_pssh(service.session): + raise log.exit(" - Failed to get PSSH") + log.info(f" + PSSH: {track.pssh}") + if not track.get_kid(service.session): + raise log.exit(" - Failed to get KID") + log.info(f" + KID: {track.kid}") + if not keys: + if track.needs_proxy: + proxy = next(iter(service.session.proxies.values()), None) + else: + proxy = None + track.download(directories.temp, headers=service.session.headers, proxy=proxy) + log.info(" + Downloaded") + if isinstance(track, VideoTrack) and track.needs_ccextractor_first and not no_subs: + ccextractor() + if track.encrypted: + log.info("Decrypting...") + if track.key: + log.info(f" + KEY: {track.key} (Static)") + elif not no_cache: + track.key, vault_used = ctx.obj.vaults.get(track.kid, title.id) + if track.key: + log.info(f" + KEY: {track.key} (From {vault_used.name} {vault_used.type.name} Key Vault)") + for vault in ctx.obj.vaults.vaults: + if vault == vault_used: + continue + result = ctx.obj.vaults.insert_key( + vault, service_name.lower(), track.kid, track.key, title.id, commit=True + ) + if result == InsertResult.SUCCESS: + log.info(f" + Cached to {vault} vault") + elif result == InsertResult.ALREADY_EXISTS: + log.info(f" + Already exists in {vault} vault") + if not track.key: + if cache: + skip_title = True + break + try: + if "common_privacy_cert" in list(ctx.obj.cdm.__dict__.keys()): + session_id = ctx.obj.cdm.open(track.pssh) + + ctx.obj.cdm.set_service_certificate( + session_id, + service.certificate( + challenge=ctx.obj.cdm.service_certificate_challenge, + title=title, + track=track, + session_id=session_id + ) or ctx.obj.cdm.common_privacy_cert + ) + ctx.obj.cdm.parse_license( + session_id, + service.license( + challenge=ctx.obj.cdm.get_license_challenge(session_id), + title=title, + track=track, + session_id=session_id + ) + ) + else: + wrm_header = PSSH(track.pssh).wrm_headers[0] + challenge = ctx.obj.cdm.get_license_challenge(wrm_header) + #log.info(f"{wrm_header} --> {challenge}") + ctx.obj.cdm.parse_license( + + service.license( + challenge=challenge, + title=title, + track=track, + ) + + ) + except: + wrm_header = PSSH(track.pssh).wrm_headers[0] + challenge = ctx.obj.cdm.get_license_challenge(wrm_header) + #log.info(f"{wrm_header} --> {challenge}") + ctx.obj.cdm.parse_license( + base64.b64decode( + service.license( + challenge=challenge, + title=title, + track=track, + ).encode("ascii") + ).decode("ascii") + ) + + + content_keys = [ + (x.kid, x.key) for x in ctx.obj.cdm.get_keys(session_id, content_only=True) + ] if "common_privacy_cert" in list(ctx.obj.cdm.__dict__.keys()) else [ + (str(x.key_id).replace("-", ""), x.key.hex()) for x in ctx.obj.cdm.get_keys() + ] + if not content_keys: + raise log.exit(" - No content keys were returned by the CDM!") + log.info(f" + Obtained content keys from the CDM") + + for kid, key in content_keys: + if kid == "b770d5b4bb6b594daf985845aae9aa5f": + # Amazon HDCP test key + continue + log.info(f" + {kid}:{key}") + + # cache keys into all key vaults + for vault in ctx.obj.vaults.vaults: + log.info(f"Caching to {vault} vault") + cached = 0 + already_exists = 0 + for kid, key in content_keys: + result = ctx.obj.vaults.insert_key(vault, service_name.lower(), kid, key, title.id) + if result == InsertResult.FAILURE: + log.warning(f" - Failed, table {service_name.lower()} doesn't exist in the vault.") + elif result == InsertResult.SUCCESS: + cached += 1 + elif result == InsertResult.ALREADY_EXISTS: + already_exists += 1 + ctx.obj.vaults.commit(vault) + log.info(f" + Cached {cached}/{len(content_keys)} keys") + if already_exists: + log.info(f" + {already_exists}/{len(content_keys)} keys already existed in vault") + if cached + already_exists < len(content_keys): + log.warning(f" Failed to cache {len(content_keys) - cached - already_exists} keys") + # use matching content key for the tracks key id + track.key = next((key for kid, key in content_keys if kid == track.kid), None) + if track.key: + log.info(f" + KEY: {track.key} (From CDM)") + else: + raise log.exit(f" - No content key with the key ID \"{track.kid}\" was returned") + if keys: + continue + # TODO: Move decryption code to Track + if not config.decrypter: + raise log.exit(" - No decrypter specified") + if config.decrypter == "packager": + platform = {"win32": "win", "darwin": "osx"}.get(sys.platform, sys.platform) + names = ["shaka-packager", "packager", f"packager-{platform}"] + executable = next((x for x in (shutil.which(x) for x in names) if x), None) + if not executable: + raise log.exit(" - Unable to find packager binary") + dec = os.path.splitext(track.locate())[0] + ".dec.mp4" + try: + os.makedirs(directories.temp, exist_ok=True) + subprocess.run([ + executable, + "input={},stream={},output={}".format( + track.locate(), + track.__class__.__name__.lower().replace("track", ""), + dec + ), + "--enable_raw_key_decryption", "--keys", + ",".join([ + f"label=0:key_id={track.kid.lower()}:key={track.key.lower()}", + # Apple TV+ needs this as shaka pulls the incorrect KID, idk why + f"label=1:key_id=00000000000000000000000000000000:key={track.key.lower()}", + ]), + "--temp_dir", directories.temp + ], check=True) + except subprocess.CalledProcessError: + raise log.exit(" - Failed!") + elif config.decrypter == "mp4decrypt": + executable = shutil.which("mp4decrypt") + if not executable: + raise log.exit(" - Unable to find mp4decrypt binary") + dec = os.path.splitext(track.locate())[0] + ".dec.mp4" + try: + subprocess.run([ + executable, + "--show-progress", + "--key", f"{track.kid.lower()}:{track.key.lower()}", + track.locate(), + dec, + ]) + except subprocess.CalledProcessError: + raise log.exit(" - Failed!") + else: + log.exit(f" - Unsupported decrypter: {config.decrypter}") + track.swap(dec) + log.info(" + Decrypted") + + if keys: + continue + + if track.needs_repack or (config.decrypter == "mp4decrypt" and isinstance(track, (VideoTrack, AudioTrack))): + log.info("Repackaging stream with FFmpeg (to fix malformed streams)") + track.repackage() + log.info(" + Repackaged") + + if isinstance(track, VideoTrack) and track.needs_ccextractor and not no_subs: + ccextractor() + if skip_title: + for track in title.tracks: + track.delete() + continue + if keys: + continue + if not list(title.tracks) and not title.tracks.chapters: + continue + # mux all final tracks to a single mkv file + if no_mux: + if title.tracks.chapters: + final_file_path = directories.downloads + if title.type == Title.Types.TV: + final_file_path = os.path.join(final_file_path, title.parse_filename(folder=True)) + os.makedirs(final_file_path, exist_ok=True) + chapters_loc = filenames.chapters.format(filename=title.filename) + title.tracks.export_chapters(chapters_loc) + shutil.move(chapters_loc, os.path.join(final_file_path, os.path.basename(chapters_loc))) + for track in title.tracks: + media_info = MediaInfo.parse(track.locate()) + final_file_path = directories.downloads + if title.type == Title.Types.TV: + final_file_path = os.path.join( + final_file_path, title.parse_filename(folder=True) + ) + os.makedirs(final_file_path, exist_ok=True) + filename = title.parse_filename(media_info=media_info) + if isinstance(track, (AudioTrack, TextTrack)): + filename += f".{track.language}" + extension = track.codec if isinstance(track, TextTrack) else os.path.splitext(track.locate())[1][1:] + if isinstance(track, AudioTrack) and extension == "mp4": + extension = "m4a" + track.move(os.path.join(final_file_path, f"{filename}.{track.id}.{extension}")) + else: + log.info("Muxing tracks into an MKV container") + muxed_location, returncode = title.tracks.mux(title.filename) + if returncode == 1: + log.warning(" - mkvmerge had at least one warning, will continue anyway...") + elif returncode >= 2: + raise log.exit(" - Failed to mux tracks into MKV file") + log.info(" + Muxed") + for track in title.tracks: + track.delete() + if title.tracks.chapters: + try: + os.unlink(filenames.chapters.format(filename=title.filename)) + except FileNotFoundError: + pass + media_info = MediaInfo.parse(muxed_location) + final_file_path = directories.downloads + if title.type == Title.Types.TV: + final_file_path = os.path.join( + final_file_path, title.parse_filename(media_info=media_info, folder=True) + ) + os.makedirs(final_file_path, exist_ok=True) + # rename muxed mkv file with new data from mediainfo data of it + if audio_only: + extension = "mka" + elif subs_only: + extension = "mks" + else: + extension = "mkv" + shutil.move( + muxed_location, + os.path.join(final_file_path, f"{title.parse_filename(media_info=media_info)}.{extension}") + ) + + + log.info("Processed all titles!") + + +def load_services(): + for service in services.__dict__.values(): + if callable(getattr(service, "cli", None)): + dl.add_command(service.cli) + + +load_services() diff --git a/vinetrimmer/commands/dl_new.py b/vinetrimmer/commands/dl_new.py new file mode 100644 index 0000000..ac45ebb --- /dev/null +++ b/vinetrimmer/commands/dl_new.py @@ -0,0 +1,644 @@ +import base64 +import html +import logging +import os +import shutil +import subprocess +import sys +import traceback +from http.cookiejar import MozillaCookieJar + +import click +import requests +from appdirs import AppDirs +from langcodes import Language +from pymediainfo import MediaInfo + +from vinetrimmer import services +from vinetrimmer.config import Config, config, credentials, directories, filenames +from vinetrimmer.objects import AudioTrack, Credential, TextTrack, Title, Titles, VideoTrack +from vinetrimmer.objects.vaults import InsertResult, Vault, Vaults +from vinetrimmer.utils import Cdm, is_close_match +from vinetrimmer.utils.click import (AliasedGroup, ContextData, acodec_param, language_param, quality_param, + range_param, vcodec_param, wanted_param) +from vinetrimmer.utils.collections import as_list, merge_dict +from vinetrimmer.utils.io import load_yaml +from vinetrimmer.utils.widevine.device import LocalDevice, RemoteDevice +from vinetrimmer.vendor.pymp4.parser import Box + +from vinetrimmer.utils.playready.cdm import Cdm as CdmPr +from vinetrimmer.utils.playready.device import Device +from vinetrimmer.utils.playready.pssh import PSSH + +def get_cdm(service, profile=None, cdm_name=None): + """ + Get CDM Device (either remote or local) for a specified service. + Raises a ValueError if there's a problem getting a CDM. + """ + if not cdm_name: + cdm_name = config.cdm.get(service) or config.cdm.get("default") + if not cdm_name: + raise ValueError("A CDM to use wasn't listed in the vinetrimmer.yml config") + if isinstance(cdm_name, dict): + if not profile: + raise ValueError("CDM config is mapped for profiles, but no profile was chosen") + cdm_name = cdm_name.get(profile) or config.cdm.get("default") + if not cdm_name: + raise ValueError(f"A CDM to use was not mapped for the profile {profile}") + if "sl2000" or "sl3000" in cdm_name.lower(): + + device_pr = Device.load(os.path.join(directories.devices, f"{cdm_name}.prd")) + return device_pr + try: + + return LocalDevice.load(os.path.join(directories.devices, f"{cdm_name}.wvd")) + except FileNotFoundError: + dirs = [ + os.path.join(directories.devices, cdm_name), + os.path.join(AppDirs("pywidevine", False).user_data_dir, "devices", cdm_name), + os.path.join(AppDirs("pywidevine", False).site_data_dir, "devices", cdm_name), + ] + + for d in dirs: + try: + return LocalDevice.from_dir(d) + except FileNotFoundError: + pass + + cdm_api = next(iter(x for x in config.cdm_api if x["name"] == cdm_name), None) + if cdm_api: + return RemoteDevice(**cdm_api) + + raise ValueError(f"Device {cdm_name!r} not found") + + +def get_service_config(service): + """Get both service config and service secrets as one merged dictionary.""" + service_config = load_yaml(filenames.service_config.format(service=service.lower())) + + user_config = (load_yaml(filenames.user_service_config.format(service=service.lower())) + or load_yaml(filenames.user_service_config.format(service=service))) + + if user_config: + merge_dict(service_config, user_config) + + return service_config + + +def get_profile(service): + """ + Get the default profile for a service from the config. + """ + profile = config.profiles.get(service) + if profile is False: + return None # auth-less service if `false` in config + if not profile: + profile = config.profiles.get("default") + if not profile: + raise ValueError(f"No profile has been defined for '{service}' in the config.") + + return profile + + +def get_cookie_jar(service, profile): + """Get the profile's cookies if available.""" + cookie_file = os.path.join(directories.cookies, service.lower(), f"{profile}.txt") + if not os.path.isfile(cookie_file): + cookie_file = os.path.join(directories.cookies, service, f"{profile}.txt") + if os.path.isfile(cookie_file): + cookie_jar = MozillaCookieJar(cookie_file) + with open(cookie_file, "r+", encoding="utf-8") as fd: + unescaped = html.unescape(fd.read()) + fd.seek(0) + fd.truncate() + fd.write(unescaped) + cookie_jar.load(ignore_discard=True, ignore_expires=True) + return cookie_jar + return None + + +def get_credentials(service, profile="default"): + """Get the profile's credentials if available.""" + cred = credentials.get(service, {}) + + if isinstance(cred, dict): + cred = cred.get(profile) + elif profile != "default": + return None + + if cred: + if isinstance(cred, list): + return Credential(*cred) + else: + return Credential.loads(cred) + + +@click.group(name="dl", short_help="Download from a service.", cls=AliasedGroup, context_settings=dict( + help_option_names=["-?", "-h", "--help"], + max_content_width=116, # max PEP8 line-width, -4 to adjust for initial indent + default_map=config.arguments +)) +@click.option("--debug", is_flag=True, hidden=True) # Handled by vinetrimmer.py +@click.option("-p", "--profile", type=str, default=None, + help="Profile to use when multiple profiles are defined for a service.") +@click.option("-q", "--quality", callback=quality_param, default=None, + help="Download Resolution, defaults to best available.") +@click.option("-v", "--vcodec", callback=vcodec_param, default="H264", + help="Video Codec, defaults to H264.") +@click.option("-a", "--acodec", callback=acodec_param, default=None, + help="Audio Codec") +@click.option("-vb", "--vbitrate", "vbitrate", type=int, + default=None, + help="Video Bitrate, defaults to Max.") +@click.option("-ab", "--abitrate", "abitrate", type=int, + default=None, + help="Audio Bitrate, defaults to Max.") +@click.option("-aa", "--atmos", is_flag=True, default=False, + help="Prefer Atmos Audio") +@click.option("-r", "--range", "range_", callback=range_param, default="SDR", + help="Video Color Range, defaults to SDR.") +@click.option("-w", "--wanted", callback=wanted_param, default=None, + help="Wanted episodes, e.g. `S01-S05,S07`, `S01E01-S02E03`, `S02-S02E03`, e.t.c, defaults to all.") +@click.option("-al", "--alang", callback=language_param, default="orig", + help="Language wanted for audio.") +@click.option("-sl", "--slang", callback=language_param, default="all", + help="Language wanted for subtitles.") +@click.option("--proxy", type=str, default=None, + help="Proxy URI to use. If a 2-letter country is provided, it will try get a proxy from the config.") +@click.option("-A", "--audio-only", is_flag=True, default=False, + help="Only download audio tracks.") +@click.option("-S", "--subs-only", is_flag=True, default=False, + help="Only download subtitle tracks.") +@click.option("-C", "--chapters-only", is_flag=True, default=False, + help="Only download chapters.") +@click.option("-ns", "--no-subs", is_flag=True, default=False, + help="Do not download subtitle tracks.") +@click.option("-na", "--no-audio", is_flag=True, default=False, + help="Do not download audio tracks.") +@click.option("-nv", "--no-video", is_flag=True, default=False, + help="Do not download video tracks.") +@click.option("-nc", "--no-chapters", is_flag=True, default=False, + help="Do not download chapters tracks.") +@click.option("-ad", "--audio-description", is_flag=True, default=False, + help="Download audio description tracks.") +@click.option("--list", "list_", is_flag=True, default=False, + help="Skip downloading and list available tracks and what tracks would have been downloaded.") +@click.option("--selected", is_flag=True, default=False, + help="List selected tracks and what tracks are downloaded.") +@click.option("--cdm", type=str, default=None, + help="Override the CDM that will be used for decryption.") +@click.option("--keys", is_flag=True, default=False, + help="Skip downloading, retrieve the decryption keys (via CDM or Key Vaults) and print them.") +@click.option("--cache", is_flag=True, default=False, + help="Disable the use of the CDM and only retrieve decryption keys from Key Vaults. " + "If a needed key is unable to be retrieved from any Key Vaults, the title is skipped.") +@click.option("--no-cache", is_flag=True, default=False, + help="Disable the use of Key Vaults and only retrieve decryption keys from the CDM.") +@click.option("--no-proxy", is_flag=True, default=False, + help="Force disable all proxy use.") +@click.option("-nm", "--no-mux", is_flag=True, default=False, + help="Do not mux the downloaded and decrypted tracks.") +@click.option("--mux", is_flag=True, default=False, + help="Force muxing when using --audio-only/--subs-only/--chapters-only.") +@click.pass_context +def dl(ctx, profile, cdm, *_, **__): + log = logging.getLogger("dl") + + service = ctx.params.get("service_name") or services.get_service_key(ctx.invoked_subcommand) + if not service: + log.exit(" - Unable to find service") + + profile = profile or get_profile(service) + service_config = get_service_config(service) + vaults = [] + for vault in config.key_vaults: + try: + vaults.append(Config.load_vault(vault)) + except Exception as e: + log.error(f" - Failed to load vault {vault['name']!r}: {e}") + vaults = Vaults(vaults, service=service) + local_vaults = sum(v.type == Vault.Types.LOCAL for v in vaults) + remote_vaults = sum(v.type == Vault.Types.REMOTE for v in vaults) + log.info(f" + {local_vaults} Local Vault{'' if local_vaults == 1 else 's'}") + log.info(f" + {remote_vaults} Remote Vault{'' if remote_vaults == 1 else 's'}") + + try: + device = get_cdm(service, profile, cdm) + except ValueError as e: + raise log.exit(f" - {e}") + device_name = device.get_name() if "sl3000" or "sl2000" in device.get_name() else device.system_id + log.info(f" + Loaded {device.__class__.__name__}: {device_name} (L{device.security_level})") + cdm = CdmPr.from_device(device) if "sl3000" or "sl2000" in device.get_name() else Cdm(device) + + if profile: + cookies = get_cookie_jar(service, profile) + credentials = get_credentials(service, profile) + if not cookies and not credentials and service_config.get("needs_auth", True): + raise log.exit(f" - Profile {profile!r} has no cookies or credentials") + else: + cookies = None + credentials = None + + ctx.obj = ContextData( + config=service_config, + vaults=vaults, + cdm=cdm, + profile=profile, + cookies=cookies, + credentials=credentials, + ) + + +@dl.result_callback() +@click.pass_context +def result(ctx, service, quality, range_, wanted, alang, slang, audio_only, subs_only, chapters_only, audio_description, + list_, keys, cache, no_cache, no_subs, no_audio, no_video, no_chapters, atmos, vbitrate: int, abitrate: int, no_mux, mux, selected, *_, **__): + def ccextractor(): + log.info("Extracting EIA-608 captions from stream with CCExtractor") + track_id = f"ccextractor-{track.id}" + # TODO: Is it possible to determine the language of EIA-608 captions? + cc_lang = track.language + try: + cc = track.ccextractor( + track_id=track_id, + out_path=filenames.subtitles.format(id=track_id, language_code=cc_lang), + language=cc_lang, + original=False, + ) + except EnvironmentError: + log.warning(" - CCExtractor not found, cannot extract captions") + else: + if cc: + title.tracks.add(cc) + log.info(" + Extracted") + else: + log.info(" + No captions found") + + log = service.log + + service_name = service.__class__.__name__ + + log.info("Retrieving Titles") + try: + titles = Titles(as_list(service.get_titles())) + except requests.HTTPError as e: + log.debug(traceback.format_exc()) + raise log.exit(f" - HTTP Error {e.response.status_code}: {e.response.reason}") + if not titles: + raise log.exit(" - No titles returned!") + titles.order() + titles.print() + + for title in titles.with_wanted(wanted): + if title.type == Title.Types.TV: + log.info("Getting tracks for {title} S{season:02}E{episode:02}{name} [{id}]".format( + title=title.name, + season=title.season or 0, + episode=title.episode or 0, + name=f" - {title.episode_name}" if title.episode_name else "", + id=title.id, + )) + else: + log.info("Getting tracks for {title}{year} [{id}]".format( + title=title.name, + year=f" ({title.year})" if title.year else "", + id=title.id, + )) + + try: + title.tracks.add(service.get_tracks(title), warn_only=True) + title.tracks.add(service.get_chapters(title)) + #if not next((x for x in title.tracks.videos if x.language.language in (v_lang or lang)), None) and title.tracks.videos: + # lang = [title.tracks.videos[0].language["language"]] + # log.info("Defaulting language to {lang} for tracks".format(lang=lang[0].upper())) + except requests.HTTPError as e: + log.debug(traceback.format_exc()) + raise log.exit(f" - HTTP Error {e.response.status_code}: {e.response.reason}") + title.tracks.sort_videos() + title.tracks.sort_audios(by_language=alang) + title.tracks.sort_subtitles(by_language=slang) + title.tracks.sort_chapters() + + for track in title.tracks: + if track.language == Language.get("none"): + track.language = title.original_lang + track.is_original_lang = is_close_match(track.language, [title.original_lang]) + + if not list(title.tracks): + log.error(" - No tracks returned!") + continue + if not selected: + log.info("> All Tracks:") + title.tracks.print() + + try: + #title.tracks.select_videos(by_quality=quality, by_range=range_, one_only=True) + title.tracks.select_videos(by_quality=quality, by_vbitrate=vbitrate, by_range=range_, one_only=True) + title.tracks.select_audios(by_language=alang, by_bitrate=abitrate, with_descriptive=audio_description) + # title.tracks.select_audios(by_language=alang, with_descriptive=audio_description) + title.tracks.select_subtitles(by_language=slang, with_forced=None) + except ValueError as e: + log.error(f" - {e}") + continue + + if no_video: + title.tracks.videos.clear() + + if no_audio: + title.tracks.audios.clear() + + if no_subs: + title.tracks.subtitles.clear() + + if no_chapters: + title.tracks.chapters.clear() + + if audio_only or subs_only or chapters_only: + title.tracks.videos.clear() + if audio_only: + if not subs_only: + title.tracks.subtitles.clear() + if not chapters_only: + title.tracks.chapters.clear() + elif subs_only: + if not audio_only: + title.tracks.audios.clear() + if not chapters_only: + title.tracks.chapters.clear() + elif chapters_only: + if not audio_only: + title.tracks.audios.clear() + if not subs_only: + title.tracks.subtitles.clear() + + if not mux: + no_mux = True + + log.info("> Selected Tracks:") + title.tracks.print() + + + if list_: + continue # only wanted to see what tracks were available and chosen + + skip_title = False + for track in title.tracks: + if not keys: + log.info(f"Downloading: {track}") + if (service_name == "AppleTVPlus" or service_name == "iTunes") and "VID" in str(track): + track.encrypted = True + if track.encrypted: + if not track.get_pssh(service.session): + raise log.exit(" - Failed to get PSSH") + log.info(f" + PSSH: {track.pssh}") + if not track.get_kid(service.session): + raise log.exit(" - Failed to get KID") + log.info(f" + KID: {track.kid}") + if not keys: + if track.needs_proxy: + proxy = next(iter(service.session.proxies.values()), None) + else: + proxy = None + track.download(directories.temp, headers=service.session.headers, proxy=proxy) + log.info(" + Downloaded") + if isinstance(track, VideoTrack) and track.needs_ccextractor_first and not no_subs: + ccextractor() + if track.encrypted: + log.info("Decrypting...") + if track.key: + log.info(f" + KEY: {track.key} (Static)") + elif not no_cache: + track.key, vault_used = ctx.obj.vaults.get(track.kid, title.id) + if track.key: + log.info(f" + KEY: {track.key} (From {vault_used.name} {vault_used.type.name} Key Vault)") + for vault in ctx.obj.vaults.vaults: + if vault == vault_used: + continue + result = ctx.obj.vaults.insert_key( + vault, service_name.lower(), track.kid, track.key, title.id, commit=True + ) + if result == InsertResult.SUCCESS: + log.info(f" + Cached to {vault} vault") + elif result == InsertResult.ALREADY_EXISTS: + log.info(f" + Already exists in {vault} vault") + if not track.key: + if cache: + skip_title = True + break + try: + if "common_privacy_cert" in list(ctx.obj.cdm.__dict__.keys()): + session_id = ctx.obj.cdm.open(track.pssh) + + ctx.obj.cdm.set_service_certificate( + session_id, + service.certificate( + challenge=ctx.obj.cdm.service_certificate_challenge, + title=title, + track=track, + session_id=session_id + ) or ctx.obj.cdm.common_privacy_cert + ) + ctx.obj.cdm.parse_license( + session_id, + service.license( + challenge=ctx.obj.cdm.get_license_challenge(session_id), + title=title, + track=track, + session_id=session_id + ) + ) + else: + wrm_header = PSSH(track.pssh).wrm_headers[0] + challenge = ctx.obj.cdm.get_license_challenge(wrm_header) + #log.info(f"{wrm_header} --> {challenge}") + ctx.obj.cdm.parse_license( + service.license( + challenge=challenge, + title=title, + track=track, + ) + ) + except requests.HTTPError as e: + log.debug(traceback.format_exc()) + raise log.exit(f" - HTTP Error {e.response.status_code}: {e.response.reason}") + + + content_keys = [ + (x.kid, x.key) for x in ctx.obj.cdm.get_keys(session_id, content_only=True) + ] if "common_privacy_cert" in list(ctx.obj.cdm.__dict__.keys()) else [ + (str(x.key_id).replace("-", ""), x.key.hex()) for x in ctx.obj.cdm.get_keys() + ] + if not content_keys: + raise log.exit(" - No content keys were returned by the CDM!") + log.info(f" + Obtained content keys from the CDM") + + for kid, key in content_keys: + if kid == "b770d5b4bb6b594daf985845aae9aa5f": + # Amazon HDCP test key + continue + log.info(f" + {kid}:{key}") + + # cache keys into all key vaults + for vault in ctx.obj.vaults.vaults: + log.info(f"Caching to {vault} vault") + cached = 0 + already_exists = 0 + for kid, key in content_keys: + result = ctx.obj.vaults.insert_key(vault, service_name.lower(), kid, key, title.id) + if result == InsertResult.FAILURE: + log.warning(f" - Failed, table {service_name.lower()} doesn't exist in the vault.") + elif result == InsertResult.SUCCESS: + cached += 1 + elif result == InsertResult.ALREADY_EXISTS: + already_exists += 1 + ctx.obj.vaults.commit(vault) + log.info(f" + Cached {cached}/{len(content_keys)} keys") + if already_exists: + log.info(f" + {already_exists}/{len(content_keys)} keys already existed in vault") + if cached + already_exists < len(content_keys): + log.warning(f" Failed to cache {len(content_keys) - cached - already_exists} keys") + # use matching content key for the tracks key id + track.key = next((key for kid, key in content_keys if kid == track.kid), None) + if track.key: + log.info(f" + KEY: {track.key} (From CDM)") + else: + raise log.exit(f" - No content key with the key ID \"{track.kid}\" was returned") + if keys: + continue + # TODO: Move decryption code to Track + if not config.decrypter: + raise log.exit(" - No decrypter specified") + if config.decrypter == "packager": + platform = {"win32": "win", "darwin": "osx"}.get(sys.platform, sys.platform) + names = ["shaka-packager", "packager", f"packager-{platform}"] + executable = next((x for x in (shutil.which(x) for x in names) if x), None) + if not executable: + raise log.exit(" - Unable to find packager binary") + dec = os.path.splitext(track.locate())[0] + ".dec.mp4" + try: + os.makedirs(directories.temp, exist_ok=True) + subprocess.run([ + executable, + "input={},stream={},output={}".format( + track.locate(), + track.__class__.__name__.lower().replace("track", ""), + dec + ), + "--enable_raw_key_decryption", "--keys", + ",".join([ + f"label=0:key_id={track.kid.lower()}:key={track.key.lower()}", + # Apple TV+ needs this as shaka pulls the incorrect KID, idk why + f"label=1:key_id=00000000000000000000000000000000:key={track.key.lower()}", + ]), + "--temp_dir", directories.temp + ], check=True) + except subprocess.CalledProcessError: + raise log.exit(" - Failed!") + elif config.decrypter == "mp4decrypt": + executable = shutil.which("mp4decrypt") + if not executable: + raise log.exit(" - Unable to find mp4decrypt binary") + dec = os.path.splitext(track.locate())[0] + ".dec.mp4" + try: + subprocess.run([ + executable, + "--show-progress", + "--key", f"{track.kid.lower()}:{track.key.lower()}", + track.locate(), + dec, + ]) + except subprocess.CalledProcessError: + raise log.exit(" - Failed!") + else: + log.exit(f" - Unsupported decrypter: {config.decrypter}") + track.swap(dec) + log.info(" + Decrypted") + + if keys: + continue + + if track.needs_repack or (config.decrypter == "mp4decrypt" and isinstance(track, (VideoTrack, AudioTrack))): + log.info("Repackaging stream with FFmpeg (to fix malformed streams)") + track.repackage() + log.info(" + Repackaged") + + if isinstance(track, VideoTrack) and track.needs_ccextractor and not no_subs: + ccextractor() + if skip_title: + for track in title.tracks: + track.delete() + continue + if keys: + continue + if not list(title.tracks) and not title.tracks.chapters: + continue + # mux all final tracks to a single mkv file + if no_mux: + if title.tracks.chapters: + final_file_path = directories.downloads + if title.type == Title.Types.TV: + final_file_path = os.path.join(final_file_path, title.parse_filename(folder=True)) + os.makedirs(final_file_path, exist_ok=True) + chapters_loc = filenames.chapters.format(filename=title.filename) + title.tracks.export_chapters(chapters_loc) + shutil.move(chapters_loc, os.path.join(final_file_path, os.path.basename(chapters_loc))) + for track in title.tracks: + media_info = MediaInfo.parse(track.locate()) + final_file_path = directories.downloads + if title.type == Title.Types.TV: + final_file_path = os.path.join( + final_file_path, title.parse_filename(folder=True) + ) + os.makedirs(final_file_path, exist_ok=True) + filename = title.parse_filename(media_info=media_info) + if isinstance(track, (AudioTrack, TextTrack)): + filename += f".{track.language}" + extension = track.codec if isinstance(track, TextTrack) else os.path.splitext(track.locate())[1][1:] + if isinstance(track, AudioTrack) and extension == "mp4": + extension = "m4a" + track.move(os.path.join(final_file_path, f"{filename}.{track.id}.{extension}")) + else: + log.info("Muxing tracks into an MKV container") + muxed_location, returncode = title.tracks.mux(title.filename) + if returncode == 1: + log.warning(" - mkvmerge had at least one warning, will continue anyway...") + elif returncode >= 2: + raise log.exit(" - Failed to mux tracks into MKV file") + log.info(" + Muxed") + for track in title.tracks: + track.delete() + if title.tracks.chapters: + try: + os.unlink(filenames.chapters.format(filename=title.filename)) + except FileNotFoundError: + pass + media_info = MediaInfo.parse(muxed_location) + final_file_path = directories.downloads + if title.type == Title.Types.TV: + final_file_path = os.path.join( + final_file_path, title.parse_filename(media_info=media_info, folder=True) + ) + os.makedirs(final_file_path, exist_ok=True) + # rename muxed mkv file with new data from mediainfo data of it + if audio_only: + extension = "mka" + elif subs_only: + extension = "mks" + else: + extension = "mkv" + shutil.move( + muxed_location, + os.path.join(final_file_path, f"{title.parse_filename(media_info=media_info)}.{extension}") + ) + + + log.info("Processed all titles!") + + +def load_services(): + for service in services.__dict__.values(): + if callable(getattr(service, "cli", None)): + dl.add_command(service.cli) + + +load_services() diff --git a/vinetrimmer/config/Services/amazon.yml b/vinetrimmer/config/Services/amazon.yml new file mode 100644 index 0000000..d7f7390 --- /dev/null +++ b/vinetrimmer/config/Services/amazon.yml @@ -0,0 +1,146 @@ +certificate: | + CAUSwgUKvAIIAxIQCuQRtZRasVgFt7DIvVtVHBi17OSpBSKOAjCCAQoCggEBAKU2UrYVOSDlcXajWhpEgGhqGraJtFdUPgu6plJGy9ViaRn5mhyXON5PXm + w1krQdi0SLxf00FfIgnYFLpDfvNeItGn9rcx0RNPwP39PW7aW0Fbqi6VCaKWlR24kRpd7NQ4woyMXr7xlBWPwPNxK4xmR/6UuvKyYWEkroyeIjWHAqgCjC + mpfIpVcPsyrnMuPFGl82MMVnAhTweTKnEPOqJpxQ1bdQvVNCvkba5gjOTbEnJ7aXegwhmCdRQzXjTeEV2dO8oo5YfxW6pRBovzF6wYBMQYpSCJIA24ptAP + /2TkneyJuqm4hJNFvtF8fsBgTQQ4TIhnX4bZ9imuhivYLa6HsCAwEAAToPYW1hem9uLmNvbS1wcm9kEoADETQD6R0H/h9fyg0Hw7mj0M7T4s0bcBf4fMhA + Rpwk2X4HpvB49bJ5Yvc4t41mAnXGe/wiXbzsddKMiMffkSE1QWK1CFPBgziU23y1PjQToGiIv/sJIFRKRJ4qMBxIl95xlvSEzKdt68n7wqGa442+uAgk7C + XU3uTfVofYY76CrPBnEKQfad/CVqTh48geNTb4qRH1TX30NzCsB9NWlcdvg10pCnWSm8cSHu1d9yH+2yQgsGe52QoHHCqHNzG/wAxMYWTevXQW7EPTBeFy + SPY0xUN+2F2FhCf5/A7uFUHywd0zNTswh0QJc93LBTh46clRLO+d4RKBiBSj3rah6Y5iXMw9N9o58tCRc9gFHrjfMNubopWHjDOO3ATUgqXrTp+fKVCmsG + uGl1ComHxXV9i1AqHwzzY2JY2vFqo73jR3IElr6oChPIwcNokmNc0D4TXtjE0BoYkbWKJfHvJJihzMOvDicWUsemVHvua9/FBtpbHgpbgwijFPjtQF9Ldb + 8Swf + +device: + + old: # !<< take note that this is done per-profile + domain: Device + app_name: AIV + app_version: '3.12.0' + device_model: 'SHIELD Android TV' + os_version: '28' + device_type: A1KAXIG6VXSG8Y + device_serial: '13f5b56b4a17de5d136f0e4c28236109' # os.urandom().hex() + device_name: "Build/LMY47D Shield TV" + software_version: '248' + + old2: + 'domain': 'DeviceLegacy' + 'device_type': A1KAXIG6VXSG8Y, + 'device_serial': '870f53d1b509594c2f8cd5e340a7d374' + 'app_name': 'com.amazon.avod.thirdpartyclient' + 'app_version': '296016847' + 'device_model': 'mdarcy/nvidia/SHIELD Android TV' + 'os_version': 'NVIDIA/mdarcy/mdarcy:11/RQ1A.210105.003/7094531_2971.7725:user/release-keys' + + old3: + domain: Device + app_name: com.amazon.amazonvideo.livingroom + app_version: '1.4' + device_model: PadFone + os_version: '6.2.5' + device_type: 'A2SNKIF736WF4T' + device_name: 'T008 Build/JSS15Q PadFone' # "%FIRST_NAME%'s%DUPE_STRATEGY_1ST% PadFone" + device_serial: 'c1ebbb433da4afdf' + + old4: + domain: Device + app_name: com.amazon.amazonvideo.livingroom + app_version: '1.4' + device_model: 'Hisense TV' + os_version: '3.9.5' + device_type: 'A3T3XXY42KZQNP' # A2B5DGIWVDH8J3, A3GTP8TAF8V3YG, AFTHA001 # https://developer.amazon.com/docs/fire-tv/identify-amazon-fire-tv-devices.html, https://github.com/giofrida/Hisense-Amazon-Enabler + device_name: "%FIRST_NAME%'s%DUPE_STRATEGY_1ST% Hisense" # KS964, Build/RP1A.201005.001 + device_serial: '8e3ddf49ee384247' + + default: + domain: Device + app_name: com.amazon.amazonvideo.livingroom + app_version: '1.1' + device_model: Hisense + os_version: '6.0.1' #6.10.19 + device_type: 'A3REWRVYBYPKUM' + device_name: '%FIRST_NAME%''s%DUPE_STRATEGY_1ST% Hisense' + device_serial: '3cc61028646759e273' + + +device_types: + browser: 'AOAGZA014O5RE' # all browsers? all platforms? + tv_generic: 'A2SNKIF736WF4T' # type is shared among various random smart tvs + pc_app: 'A1RTAM01W29CUP' + mobile_app: 'A43PXU4ZN2AL1' + echo: 'A7WXQPH584YP' # echo Gen2 + echo_dot: 'A32DOYMUN6DTXA' # echo dot Gen3 + echo_studio: 'A3RBAYBE7VM004' # for audio stuff, this is probably the one to use + fire_7: 'A2M4YX06LWP8WI' + fire_7_again: 'A1Q7QCGNMXAKYW' # not sure the difference + fire_hd_8: 'A1C66CX2XD756O' + fire_hd_8_again: 'A38EHHIB10L47V' # not sure the difference + fire_hd_8_plus_2020: 'AVU7CPPF2ZRAS' + fire_hd_10: 'A1ZB65LA390I4K' + fire_tv: 'A2E0SNTXJVT7WK' # this is not the stick, this is the older stick-like diamond shaped one + fire_tv_gen2: 'A12GXV8XMS007S' + fire_tv_cube: 'A2JKHJ0PX4J3L3' # this is the STB-style big bulky cube + fire_tv_stick_gen1: 'ADVBD696BHNV5' # non-4k fire tv stick + fire_tv_stick_gen2: 'A2LWARUGJLBYEW' + fire_tv_stick_with_alexa: 'A265XOI9586NML' + fire_tv_stick_4k: 'A2GFL5ZMWNE0PX' # 4k fire tv stick + fire_tv_stick_4k_gen3: 'AKPGW064GI9HE' + nvidia_shield: 'A1KAXIG6VXSG8Y' # nvidia shield, unknown which one or if all + +endpoints: + browse: '/cdp/catalog/Browse' + details: '/gp/video/api/getDetailPage' + getDetailWidgets: '/gp/video/api/getDetailWidgets' + playback: '/cdp/catalog/GetPlaybackResources' + licence: '/cdp/catalog/GetPlaybackResources' + # chapters/scenes + xray: '/swift/page/xray' + # device registration + ontv: '/region/eu/ontv/code?ref_=atv_auth_red_aft' #/gp/video/ontv/code + devicelink: '/gp/video/api/codeBasedLinking' + codepair: '/auth/create/codepair' + register: '/auth/register' + token: '/auth/token' + #cookies: '/ap/exchangetoken/cookies' + +regions: + us: + base: 'www.amazon.com' + base_api: 'api.amazon.com' + base_manifest: 'atv-ps.amazon.com' + marketplace_id: 'ATVPDKIKX0DER' + + gb: + base: 'www.amazon.co.uk' + base_api: 'api.amazon.co.uk' + base_manifest: 'atv-ps-eu.amazon.co.uk' + marketplace_id: 'A2IR4J4NTCP2M5' # A1F83G8C2ARO7P is also another marketplace_id + + it: + base: 'www.amazon.it' + base_api: 'api.amazon.it' + base_manifest: 'atv-ps-eu.primevideo.com' + marketplace_id: 'A3K6Y4MI8GDYMT' + + de: + base: 'www.amazon.de' + base_api: 'api.amazon.de' + base_manifest: 'atv-ps-eu.amazon.de' + marketplace_id: 'A1PA6795UKMFR9' + + au: + base: 'www.amazon.com.au' + base_api: 'api.amazon.com.au' + base_manifest: 'atv-ps-fe.amazon.com.au' + marketplace_id: 'A3K6Y4MI8GDYMT' + + jp: + base: 'www.amazon.co.jp' + base_api: 'api.amazon.co.jp' + base_manifest: 'atv-ps-fe.amazon.co.jp' + marketplace_id: 'A1VC38T7YXB528' + + pl: + base: 'www.amazon.com' + base_api: 'api.amazon.com' + base_manifest: 'atv-ps-eu.primevideo.com' + marketplace_id: 'A3K6Y4MI8GDYMT' diff --git a/vinetrimmer/config/Services/amazon_old.yml b/vinetrimmer/config/Services/amazon_old.yml new file mode 100644 index 0000000..4c4ae01 --- /dev/null +++ b/vinetrimmer/config/Services/amazon_old.yml @@ -0,0 +1,118 @@ +certificate: | + CAUSwgUKvAIIAxIQCuQRtZRasVgFt7DIvVtVHBi17OSpBSKOAjCCAQoCggEBAKU2UrYVOSDlcXajWhpEgGhqGraJtFdUPgu6plJGy9ViaRn5mhyXON5PXm + w1krQdi0SLxf00FfIgnYFLpDfvNeItGn9rcx0RNPwP39PW7aW0Fbqi6VCaKWlR24kRpd7NQ4woyMXr7xlBWPwPNxK4xmR/6UuvKyYWEkroyeIjWHAqgCjC + mpfIpVcPsyrnMuPFGl82MMVnAhTweTKnEPOqJpxQ1bdQvVNCvkba5gjOTbEnJ7aXegwhmCdRQzXjTeEV2dO8oo5YfxW6pRBovzF6wYBMQYpSCJIA24ptAP + /2TkneyJuqm4hJNFvtF8fsBgTQQ4TIhnX4bZ9imuhivYLa6HsCAwEAAToPYW1hem9uLmNvbS1wcm9kEoADETQD6R0H/h9fyg0Hw7mj0M7T4s0bcBf4fMhA + Rpwk2X4HpvB49bJ5Yvc4t41mAnXGe/wiXbzsddKMiMffkSE1QWK1CFPBgziU23y1PjQToGiIv/sJIFRKRJ4qMBxIl95xlvSEzKdt68n7wqGa442+uAgk7C + XU3uTfVofYY76CrPBnEKQfad/CVqTh48geNTb4qRH1TX30NzCsB9NWlcdvg10pCnWSm8cSHu1d9yH+2yQgsGe52QoHHCqHNzG/wAxMYWTevXQW7EPTBeFy + SPY0xUN+2F2FhCf5/A7uFUHywd0zNTswh0QJc93LBTh46clRLO+d4RKBiBSj3rah6Y5iXMw9N9o58tCRc9gFHrjfMNubopWHjDOO3ATUgqXrTp+fKVCmsG + uGl1ComHxXV9i1AqHwzzY2JY2vFqo73jR3IElr6oChPIwcNokmNc0D4TXtjE0BoYkbWKJfHvJJihzMOvDicWUsemVHvua9/FBtpbHgpbgwijFPjtQF9Ldb + 8Swf + +# device: +# new: +# domain: Device +# app_name: 'com.amazon.amazonvideo.livingroom' # AIV, com.amazon.amazonvideo.livingroom +# app_version: '1.1' # AIV: 3.12.0, livingroom: 1.1 +# device_model: 'Nexus Player' # SHIELD Android TV, Nexus Player +# os_version: '8.0.0' # 25, 8.0.0 +# device_type: 'A2SNKIF736WF4T' +# device_name: '%FIRST_NAME%''s%DUPE_STRATEGY_1ST% Nexus Player' +# device_serial: 'a906a7f9bfd6a7ab' # f8eb5625fd608718 + +#below gets ism manifest +device: + default: # !<< take note that this is done per-profile + domain: Device + app_name: AIV + app_version: '3.12.0' + device_model: 'SHIELD Android TV' + os_version: '28' + device_type: A1KAXIG6VXSG8Y + device_serial: '13f5b56b4a17de5d136f0e4c28236109' # `os.urandom(8).hex()` + device_name: "%FIRST_NAME%'s%DUPE_STRATEGY_1ST% Shield TV" + software_version: '248' + + + +device_types: + browser: 'AOAGZA014O5RE' # all browsers? all platforms? + tv_generic: 'A2SNKIF736WF4T' # type is shared among various random smart tvs + pc_app: 'A1RTAM01W29CUP' + mobile_app: 'A43PXU4ZN2AL1' + echo: 'A7WXQPH584YP' # echo Gen2 + echo_dot: 'A32DOYMUN6DTXA' # echo dot Gen3 + echo_studio: 'A3RBAYBE7VM004' # for audio stuff, this is probably the one to use + fire_7: 'A2M4YX06LWP8WI' + fire_7_again: 'A1Q7QCGNMXAKYW' # not sure the difference + fire_hd_8: 'A1C66CX2XD756O' + fire_hd_8_again: 'A38EHHIB10L47V' # not sure the difference + fire_hd_8_plus_2020: 'AVU7CPPF2ZRAS' + fire_hd_10: 'A1ZB65LA390I4K' + fire_tv: 'A2E0SNTXJVT7WK' # this is not the stick, this is the older stick-like diamond shaped one + fire_tv_gen2: 'A12GXV8XMS007S' + fire_tv_cube: 'A2JKHJ0PX4J3L3' # this is the STB-style big bulky cube + fire_tv_stick_gen1: 'ADVBD696BHNV5' # non-4k fire tv stick + fire_tv_stick_gen2: 'A2LWARUGJLBYEW' + fire_tv_stick_with_alexa: 'A265XOI9586NML' + fire_tv_stick_4k: 'A2GFL5ZMWNE0PX' # 4k fire tv stick + fire_tv_stick_4k_gen3: 'AKPGW064GI9HE' + nvidia_shield: 'A1KAXIG6VXSG8Y' # nvidia shield, unknown which one or if all + +endpoints: + browse: '/cdp/catalog/Browse' + details: '/gp/video/api/getDetailPage' + getDetailWidgets: '/gp/video/api/getDetailWidgets' + playback: '/cdp/catalog/GetPlaybackResources' + licence: '/cdp/catalog/GetPlaybackResources' + # chapters/scenes + xray: '/swift/page/xray' + # device registration + ontv: '/gp/video/ontv/code' + devicelink: '/gp/video/api/codeBasedLinking' + codepair: '/auth/create/codepair' + register: '/auth/register' + token: '/auth/token' + +regions: + us: + base: 'www.amazon.com' + base_api: 'api.amazon.com' + base_manifest: 'atv-ps.amazon.com' + marketplace_id: 'ATVPDKIKX0DER' + + gb: + base: 'www.amazon.co.uk' + base_api: 'api.amazon.co.uk' + base_manifest: 'atv-ps-eu.amazon.co.uk' + marketplace_id: 'A2IR4J4NTCP2M5' # A1F83G8C2ARO7P is also another marketplace_id + + it: + base: 'www.amazon.it' + base_api: 'api.amazon.it' + base_manifest: 'atv-ps-eu.primevideo.com' + marketplace_id: 'A3K6Y4MI8GDYMT' + + de: + base: 'www.amazon.de' + base_api: 'api.amazon.de' + base_manifest: 'atv-ps-eu.amazon.de' + marketplace_id: 'A1PA6795UKMFR9' + + au: + base: 'www.amazon.com.au' + base_api: 'api.amazon.com.au' + base_manifest: 'atv-ps-fe.amazon.com.au' + marketplace_id: 'A3K6Y4MI8GDYMT' + + jp: + base: 'www.amazon.co.jp' + base_api: 'api.amazon.co.jp' + base_manifest: 'atv-ps-fe.amazon.co.jp' + marketplace_id: 'A1VC38T7YXB528' + + pl: + base: 'www.amazon.com' + base_api: 'api.amazon.com' + base_manifest: 'atv-ps-eu.primevideo.com' + marketplace_id: 'A3K6Y4MI8GDYMT' diff --git a/vinetrimmer/config/Services/appletvplus.yml b/vinetrimmer/config/Services/appletvplus.yml new file mode 100644 index 0000000..3667e9d --- /dev/null +++ b/vinetrimmer/config/Services/appletvplus.yml @@ -0,0 +1,7 @@ +user_agent: 'ATVE/6.2.0 Android/10 build/6A226 maker/Google model/Chromecast FW/QTS2.200918.0337115981' # AppleTV6,2/11.1 | ATVE/1.1 FireOS/6.2.6.8 build/4A93 maker/Amazon model/FireTVStick4K FW/NS6268/2315 + +endpoints: + title: 'https://tv.apple.com/api/uts/v3/{type}/{id}' + tv_episodes: 'https://tv.apple.com/api/uts/v2/view/show/{id}/episodes' + manifest: 'https://tv.apple.com/api/uts/v2/view/product/{id}/personalized' + license: 'https://play.itunes.apple.com/WebObjects/MZPlay.woa/wa/fpsRequest' # 'https://play.itunes.apple.com/WebObjects/MZPlay.woa/web/video/subscription/license' diff --git a/vinetrimmer/config/Services/appletvplus1.yml b/vinetrimmer/config/Services/appletvplus1.yml new file mode 100644 index 0000000..8ea3d4a --- /dev/null +++ b/vinetrimmer/config/Services/appletvplus1.yml @@ -0,0 +1,24 @@ +user_agent: 'ATVE/1.1 FireOS/6.2.6.8 build/4A93 maker/Amazon model/FireTVStick4K FW/NS6268/2315' # AppleTV6,2/11.1 | ATVE/1.1 FireOS/6.2.6.8 build/4A93 maker/Amazon model/FireTVStick4K FW/NS6268/2315 + +endpoints: + title: 'https://tv.apple.com/api/uts/v3/{type}/{id}' + tv_episodes: 'https://tv.apple.com/api/uts/v2/view/show/{id}/episodes' + manifest: 'https://tv.apple.com/api/uts/v2/view/product/{id}/personalized' + license: 'https://play.itunes.apple.com/WebObjects/MZPlay.woa/wa/fpsRequest' # 'https://play.itunes.apple.com/WebObjects/MZPlay.woa/web/video/subscription/license' + +device: + # I have no idea what these values are or what they represent. + # These might not even be device related. + utsk: '6e3013c6d6fae3c2::::::e08f7cfb96228836' # 6e3013c6d6fae3c2::::::e08f7cfb96228836 6e3013c6d6fae3c2::::::e08f7cfb96228836 (?), 6e3013c6d6fae3c2::::::235656c069bb0efb (web) + caller: 'web' # ?, vz, web + sf: '143441' # "storefront", country | 143441: US, 143444: GB | 143443: DE For other countrys ->https://gist.github.com/BrychanOdlum/2208578ba151d1d7c4edeeda15b4e9b1 + v: '46' # "version" | latest: 56 + pfm: 'appletv' # "platform" | appletv, vz, web + mfr: 'Apple' # "manufacturer" | Apple, Amazon + locale: 'en-US' + l: 'en' + brandId: 'tvs.sbd.4000' # ctx_brand/brandId + count: '100' + skip: '0' + svcId: 'tvs.vds.4105' + a: '1783518684' diff --git a/vinetrimmer/config/Services/max.yml b/vinetrimmer/config/Services/max.yml new file mode 100644 index 0000000..9bc052b --- /dev/null +++ b/vinetrimmer/config/Services/max.yml @@ -0,0 +1,8 @@ +endpoints: + contentRoutes: 'https://default.any-any.prd.api.max.com/cms/routes/%s/%s?include=default' + moviePages: 'https://default.any-any.prd.api.max.com/content/videos/%s/activeVideoForShow?&include=edit' + playbackInfo: 'https://default.any-any.prd.api.max.com/any/playback/v1/playbackInfo' + showPages: 'https://default.any-any.prd.api.max.com/cms/collections/generic-show-page-rail-episodes-tabbed-content?include=default&pf[show.id]=%s&%s' +account: + email: 'marccin1996@gmail.com' + pass: 'MarcinPawlak1996' \ No newline at end of file diff --git a/vinetrimmer/config/Services/netflix.yml b/vinetrimmer/config/Services/netflix.yml new file mode 100644 index 0000000..4fe4650 --- /dev/null +++ b/vinetrimmer/config/Services/netflix.yml @@ -0,0 +1,240 @@ +certificate: | + CAUSwwUKvQIIAxIQ5US6QAvBDzfTtjb4tU/7QxiH8c+TBSKOAjCCAQoCggEBAObzvlu2hZRsapAPx4Aa4GUZj4/GjxgXUtBH4THSkM40x63wQeyVxlEEo + 1D/T1FkVM/S+tiKbJiIGaT0Yb5LTAHcJEhODB40TXlwPfcxBjJLfOkF3jP6wIlqbb6OPVkDi6KMTZ3EYL6BEFGfD1ag/LDsPxG6EZIn3k4S3ODcej6YSz + G4TnGD0szj5m6uj/2azPZsWAlSNBRUejmP6Tiota7g5u6AWZz0MsgCiEvnxRHmTRee+LO6U4dswzF3Odr2XBPD/hIAtp0RX8JlcGazBS0GABMMo2qNfCi + SiGdyl2xZJq4fq99LoVfCLNChkn1N2NIYLrStQHa35pgObvhwi7ECAwEAAToQdGVzdC5uZXRmbGl4LmNvbRKAA4TTLzJbDZaKfozb9vDv5qpW5A/DNL9g + bnJJi/AIZB3QOW2veGmKT3xaKNQ4NSvo/EyfVlhc4ujd4QPrFgYztGLNrxeyRF0J8XzGOPsvv9Mc9uLHKfiZQuy21KZYWF7HNedJ4qpAe6gqZ6uq7Se7f + 2JbelzENX8rsTpppKvkgPRIKLspFwv0EJQLPWD1zjew2PjoGEwJYlKbSbHVcUNygplaGmPkUCBThDh7p/5Lx5ff2d/oPpIlFvhqntmfOfumt4i+ZL3fFa + ObvkjpQFVAajqmfipY0KAtiUYYJAJSbm2DnrqP7+DmO9hmRMm9uJkXC2MxbmeNtJHAHdbgKsqjLHDiqwk1JplFMoC9KNMp2pUNdX9TkcrtJoEDqIn3zX9 + p+itdt3a9mVFc7/ZL4xpraYdQvOwP5LmXj9galK3s+eQJ7bkX6cCi+2X+iBmCMx4R0XJ3/1gxiM5LiStibCnfInub1nNgJDojxFA3jH/IuUcblEf/5Y0s + 1SzokBnR8V0KbA== + +payload_challenge: "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c29hcDpFbnZlbG9wZSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4c2Q9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczpzb2FwPSJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy9zb2FwL2VudmVsb3BlLyI+PHNvYXA6Qm9keT48QWNxdWlyZUxpY2Vuc2UgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vRFJNLzIwMDcvMDMvcHJvdG9jb2xzIj48Y2hhbGxlbmdlPjxDaGFsbGVuZ2UgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vRFJNLzIwMDcvMDMvcHJvdG9jb2xzL21lc3NhZ2VzIj48TEEgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vRFJNLzIwMDcvMDMvcHJvdG9jb2xzIiBJZD0iU2lnbmVkRGF0YSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PFZlcnNpb24+NDwvVmVyc2lvbj48Q29udGVudEhlYWRlcj48V1JNSEVBREVSIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL0RSTS8yMDA3LzAzL1BsYXlSZWFkeUhlYWRlciIgdmVyc2lvbj0iNC4yLjAuMCI+PERBVEE+PFBST1RFQ1RJTkZPPjxLSURTPjxLSUQgQUxHSUQ9IkFFU0NUUiIgVkFMVUU9IkFBQUFBTVlFeElFQUFBQUFBQUFBQUE9PSI+PC9LSUQ+PC9LSURTPjwvUFJPVEVDVElORk8+PExBX1VSTD5odHRwOi8vY2FwcHJzdnIwNi9zaWx2ZXJsaWdodDUvcmlnaHRzbWFuYWdlci5hc214PC9MQV9VUkw+PExVSV9VUkw+aHR0cDovL2NhcHByc3ZyMDYvc2lsdmVybGlnaHQ1L3JpZ2h0c21hbmFnZXIuYXNteDwvTFVJX1VSTD48REVDUllQVE9SU0VUVVA+T05ERU1BTkQ8L0RFQ1JZUFRPUlNFVFVQPjwvREFUQT48L1dSTUhFQURFUj48L0NvbnRlbnRIZWFkZXI+PENMSUVOVElORk8+PENMSUVOVFZFUlNJT04+MTAuMC4xNjM4NC4xMDAxMTwvQ0xJRU5UVkVSU0lPTj48L0NMSUVOVElORk8+PFJldm9jYXRpb25MaXN0cz48UmV2TGlzdEluZm8+PExpc3RJRD5pb3lkVGxLMnAwV1hrV2tscHJSNUh3PT08L0xpc3RJRD48VmVyc2lvbj4xMzwvVmVyc2lvbj48L1Jldkxpc3RJbmZvPjxSZXZMaXN0SW5mbz48TGlzdElEPmdDNElLS1BIc1VDQ1Zobmx0dGliSnc9PTwvTGlzdElEPjxWZXJzaW9uPjExPC9WZXJzaW9uPjwvUmV2TGlzdEluZm8+PFJldkxpc3RJbmZvPjxMaXN0SUQ+RWYvUlVvalQzVTZDdDJqcVRDQ2hiQT09PC9MaXN0SUQ+PFZlcnNpb24+NzA8L1ZlcnNpb24+PC9SZXZMaXN0SW5mbz48L1Jldm9jYXRpb25MaXN0cz48TGljZW5zZU5vbmNlPnNuQVVnc2l2em9ibjVOTG0wSW5oUHc9PTwvTGljZW5zZU5vbmNlPjxDbGllbnRUaW1lPjE2OTczMDc3NjU8L0NsaWVudFRpbWU+IDxFbmNyeXB0ZWREYXRhIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjxFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjYWVzMTI4LWNiYyI+PC9FbmNyeXB0aW9uTWV0aG9kPjxLZXlJbmZvIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48RW5jcnlwdGVkS2V5IHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjxFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9EUk0vMjAwNy8wMy9wcm90b2NvbHMjZWNjMjU2Ij48L0VuY3J5cHRpb25NZXRob2Q+PEtleUluZm8geG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxLZXlOYW1lPldNUk1TZXJ2ZXI8L0tleU5hbWU+PC9LZXlJbmZvPjxDaXBoZXJEYXRhPjxDaXBoZXJWYWx1ZT5GY0E3S2FyWFlMSlpoVjFoQkFmQjloTmh0WHZFUUhzb2Nlc0d1bWZKUFFCcUcvL3JUYWQzcDhsS0JBZGRtOGZwTURuaXl6U3I5RkQ1OUFISUZFOWQ3bzh2c3E4UFFMWVZMaWM3c0k2QUlkelBKRUUvZmJLWUtNR2x4WEp2UXZmbkdUT0VHS3J4M3paV2NMQUxRaTFSRzBEbUF2bFJ3dHdHOWRTRi9nNTJhYlk9PC9DaXBoZXJWYWx1ZT48L0NpcGhlckRhdGE+PC9FbmNyeXB0ZWRLZXk+PC9LZXlJbmZvPjxDaXBoZXJEYXRhPjxDaXBoZXJWYWx1ZT5hSllvODBaWHBCTHd3dHdoYUtJcGVZYmtDV0Nub3hrTkIyUlZ4UUhPODQxN3dGVFFZU3VNdEhrL2NlekJDRTlRUi9PeE9pVHVzN25vbm9GLzVoVXRFYnd0Q1FwbW12T2czdE03RDJxY0xkSUxjcHlqZGlxa0Q2eHhnM0hhNEx4ZTdNdEJkUzN3ekVBSndONnUrUm9LWDBMUWo3MFd0MzJnMG84ZVJuem02U1lDZlM2REJRVHV5YktrQkI2WVNYRFQ0OGxhaWZ6ZHRPVGMvQUMyREZZQjlmeFVQUWhrelk4Zms1eFc4RFdCWFl6U2NJRlRNKzArb2Q1bVJaOHI4cWRXdW9EZmw1cU5YOTdHUFpWYTQ0V2VTV2RobGNFbW15ODJpcEFXZjAwdzBkWElsSlNOa1JUVEVpVk9Zc2Nnd1QwTmx4TmMxa2JHZW9OS1c5VmNOSVV4QlFWRXNIM2NtREw3ZTlJcm5LZkhHcHR6QnY5d2lsbUV1Q0RDb1Z4VGVKdk5PQjloOVlJOHErbkcrRTJRSjNtRS9LUE5HZWZwQjQzb3FQRHBwMUhGQ3c4OW9LS1JZUEM2bWZhazF2ZUtNdXEyMDZlYWlKVlVxZWZ4QlRhYTZuMHN2MFNsY0lPdFlYRFd6U0J0cFNiSGVmNGpLM1hDbmxRcmhiNXV2WndaYXloUjNRakErT09IU1NwS3BNc0tEMzZiM1M2ditMVFNEMjVsSmtuNXNhVCtHL0F0dDVFejhHdUp0dWwzdFIrd0I3d1ZwTHpIUFliNkdlTy9zU2N6czRyOVNjWFlwcHpjR1lHd09DN1dmRGZ0ZGZQUUk1MzV5S1ZReGxRamsrN01oa0p5ZWJmdzRmY3gxbnpsMHBTUnA5SzNLdWlsSi9YZWF3bXIvd1piNjhTL01xUDg3Nm5BcEdCYWJ4RWRENHdGWmZRaVErWC9lU0ZkSE8yRkduekttS3JQTERHeSs1cjI2Y0RCa2tlRUw1ekdnMXh5d0ZndzJnM1pabmtHRjlFYTFtRk1OZHZsN1RYcHBseDdsZjh0WnhmUDc3OXVycnhUYlVnWU5tV05oaDJ1OTY1QVNTZS9TbFJ0QUViR0xqME5VK090alJlYU5EWnVEeEFXMkFhanp2Q0QvcFZtOVdsMXoxNWo1WnR6dXJlYWJZbkhyZFBPdXZjRTF1RHBGQ29xMGhEcVpOWWRZNVR6Q09pQ1VaN3pxWnhza1EycXpaTTk5bnc3VmF5N1I3ZW9ZRDNuVmR5MlN3aWpETWQyMnk3ZlBNYXc4S204WHU3NTNhSjV6M3VjNkFNeFBoenZpUXFsTmx2VFBZejlJNFlESnM4aS9TTnhlZmZEa05BZkQ2TjkxTUJoejZ3cGpKNHhMVnhyeWNpa2tGSmpCS3MrODdMVGxhVmNleXJVa1BEKzRCZjdSZnFPQVBsZVRZcThwblFRZjV1UDdQbmZqQkVPYUJCV2E0a2Y4OEhOVlptSjkxa2xoaUxrQVQ4bkVOa2U4NFZHTkhXNll6SElXTzc1ejJZTTA4Q2lNS2QyQUxsVVM3RHdRdlgwNWVPdEhWbC9VTGlnT25mNDRMNThkK0tqUm1hQW5YcmxKVTRHMXBMVmFGbGU4MkVVREdubHl0ZVBkeE5vU28zdWYxRmdsMEFkOFRlRUtkMzc2R05EMVZiUXlDNFh4YzNNcGZXdWJ1aEM4MDhRa1c5cXo0VHhpaVZjNG1wREtBSzMvR1BjbnhzKzlZMy9NbkFMZnlPaE9jNkJ3RGhPR09pZENkQlpobGhDS0NQdkEwcktyd2FGQUlIRXFWblc0QmRsRldTWmpkNXBBYUl5SndtRHBWRS82TmNqVGR0Zm5SV3lsS1lldEdEajJtTEV5SHFlTFdpblpBaWxLc3UrNTlqL2ozT3BFQUFvT0JpVmc1dTM0NzYzbE5qc0Z6cUxxUitvKy92R0RORXlFalVMYzFBZGo1NEZPQm9oWmRGZHpHM0lKNjkwcGtHZ1B0dXgvNjhBZm0wQWtRUE5UL2RBc0h3bTRwdjZPV3RmUGVmZnBHdHdNZEVXRU5DUDcxKy96VUpWdzBDakZDd01TN0ljM013ai9MQzkzTTl5ZDVSYkJ4Q1dyMkZPekNZR2VOcGdXclpCS29LaC9iM3FpT2pTK3BnRkxMek1qbzlvZkRYeUpkbDYwSTNSdEZOZ0k4NjJwWDFUazRaa3ZFYWJ6cHBQcmNwS2h5czgxRVNiNUYxRktsZGdCbVFlbE13MDZWTjVNejFMclYvUWY3NVJ0T0xFanNlOTJ4Q2JDelMwSHFoTjV4b0NVNzZWSlBaK1hCdlFUT0p3ZmFhRzRXMzhkN1Y1OHZ2VFRLZnlUNnd3azdRUjlwZEdJNGxTV0VMMWIvRnkxbzRlSys3YkhRa0JHWnZ6TDhoa0dpdlMwdGwvR1RwTnZ1SzlQbFNpbS9ZVytpVm41eTd0cWhtcnpPWDA1aTFQYlZFU0p5YkVKVnpQWTJGcTlqVzBTRHd0YWZVMFllc1FjZTZJNVByc2U4eEF2MnlxMGoyUk5obFB6VzMvbFVidGd4VVZ2ZjU5SG1NUXVtSTV5Nm0rcDl2akw5dHlOMExXL2xMVTRuaHpIMmF2aE01dUY3NzB6VHNjd1hVOU9YUEE5dlRtVzZjMWoydU53T1FIR3Vrb0dERm1sdVhGUWdlWHJJZEo1b2FPVWh3OTlBWlNmV2w0ZG82ZTJHVUpla2IrYW5CODVrZVJZeU1mOWN6NW54dlJ0MndhZ3FSRnlpMG1uZ2RZRDQ5VStyZitWQ2tJdm1QQ1d6b1FrOXFoL0cySTJDTVY5cDBTazQwbThseDUrWkFNK3AzUEV1TnRjSFpTVm16RFEyUnQrWVJCdmU3SUE1KzRmcVBkeGFiMEYwcnNmVm85NEdZTjJicm1XUzdkK0tlMUJPQmVkTU8wREJQeGdQY0h5VE9DdnRxVUxIOStURXhDb3BvWDdRc0EvaGcvM1ZSU09wRHc4Z0pnVllQL2ZjNG5uVk16TEVPK1FiVVhVeTNQQURuaDB4QXU5aFducTNDRmNMc2M2bnB0bzFFRDhOYW83MkVhNFNubUk3NXZMbXJJb1kvMmlSajhTblVQSE9PRnZlN3hiNTFBM2V3NDZRT1UwNjRVN3NyZ25aSURvSVdXTjRtUlVvYmQ0ODFQSGFzOWF6ZEs5NUFnQ3A2ZnoxTzJJelVkZ2Z3TGUxN1NwMEVySElNd1BCaWwzRDVrOW5xcmR5NWErRFNabmtMc1V2eE5GQVM0R2JpZ3FXS3FaazFpWC9USWtEQWpUdGUydTk1N2NoN1lCcFFUYkRVZVNyQ1FOUUlhdUw0QjVYSkJFWXBsWmMrTlBsTmYyOWgxb3ZtZEw3b2dEZW1lL1pzY0IwTWtma01vWkJGL2VCa1dvZFZaSTFEd3dPUy83RyswaFpGWEI1TDVYUHhHTWRMYy94RnRLZ282N0VnWEpaQVFUTnlOWlhKR0pWRzlLNHQzRU5ZQVNUMWZhL3JudStEdldlRVlKQ0lRUkdWWnl2Y1ZRcHZybVVhZVIvQU53Rm5YNmliT0plUThFaHVOR0l3Mjg2Q3dMbE9Qd3pwRXlOVFMrcVBPY2lidmpBL2hISk53dDZ4czZpZXdFNmVLemYzdGhPL2REVDVPeEc0QWtnWVhhUHczSTIxc0hFMVIzc2RyR2Zza0JzZzlWWmkxUG4yRFZSRzcwUXBXcW1wNzZLS25EV05PVW5QSkRkcXNicFN4RXEyQTJZU2NISitoMlZkeDYzZDBsajJqZ240Y2ZteUdqa294MGxBcWxEOXBGYUEwR1RUVnNrRk55RnY4bnFXbEFuNzBQbDN3KzkydisxeldOQk5XcHJha3BhZGJRUUJCMnZiVmtSN0FuZkhURXh0R1ZpdHRMWHBUcWJldTBYVHdsKy9jQm1oYTAyQWFablA3WEZiK2cwVDF4dFptL3B5Y2ozdTczbG9RMGV1QXlLT3FuUkRHQTBFcFNOVlNaOGRDeXFaVTdwOHBwOXlTbEZjcFN4eEJTM0xERlcrbWZRS3YzUEFXRFNWSzY3dVcwdE5jTTNGZDhDOXRQMnNIaE1KbVFLZDNzNkdaaGxKb3U3MVVRRE5oM1RSc0lSdTBSKzZuUUVqZG9pSXhYV2hkQk9lYnNSR2N5eUhoam1FNEd2U2YzWGp1SEJGdnJnVndIeUgxTi9WdFdBVE00VHhiZlNzKzVNeHE0Qi9KTDRURzNQSmtkdkwxSTRIazBBaU96QnhOQ1R1ZGYyeUJRK0NISktHZTUyc3JHNzBWNFR3WXNUY2JzUkl3VmhmNTloWjAzMmZIRDBPZEJYenpKMkYvYzFvckdMVXAycXFRSGFKS1FLeFYzZEUrY20rUEdiQjFtY251U3dBQTN2ZDZNNVdQbVZkWENaa2FKTnFyWTFpVnJValZST0F2K1VRa05ZUXJsUEx5UjdPUC9hV2VaR05MNkxGaWorR1V1Q3pxMW1BSmt1M2JTQ05hRW12QmduRXdnbVlVZzUvcHBlQUVzSUJxME1ZaTVWSXdWWk5lTE82dW4xNUtrL0d5QkRBQ2dadVBmbEVVOUt0UDVhaDl2NldKTktCMjN5ejR2ekczYmpyM3pPZHd4eGsveXFsbDF0QmsrZ2FyRTZ0MjRldzBwYWxwRHZzbEhMNGx6YUg4L2t0VmNURkdxNlBvZjVLOHlYWm5KMHIwckFrWW5PVFZlWXdKdk5iVFg4QTltSCtrT0x0WVVBdTB3T0p1QytOeTlINWZNVTlZYnFoOC9sd2JMUFRxYUY3UWNzQ1JPZkEray9YU0ZOK1plR295Zno2bXFSWjhmUjlNbnpzdG5kSjZLZG5OMUVLUjR1RTgwbDMzNkdteU4wWVUrdmxpb2h5VkpDTW9BZ1pmbXNPYUVVa2Y5WlBNS2M4eEtQY3hDQ0pjWVdzWUpzTkV1RGFwenpmVEYxRnJKRGtYOG9JRUpWVG9QS1pnTzROSEw5aWUzNnU4L1JuRENQTVZ6WE1IWTNCTVI5SmtjYThjZ0J2WmRTcXRtT0dGeFFVVzZpaG9Qdlg5S2Y0Yk9DL1hXU3V0bGFiVExMelUzVWFpNlNzeXJ5aG5rYjZFWVgyd0c4V1RXaXVJdUlHUndqN09hVEJxQVkvUUpLZ3d4SlcvaW9KT1RDNWttQjViR2loSlFnZUNWN0paeEg0V3JlSHBuT1hFaXhhRUsvRjZoRVZoVUdZdkljUUhSL2pGRGoxaitiRXNJNWxuaWZsRTZyakNObG0yZGZDZjdyS2dwZkl2MFY5ekdaWnhta3pOb0xicW5TczRMYU1acUhGaWgzZlhnSkNZZ0MzODZNcWRwN2RoanhORUdmN29LZHhCNzVUemdmTjE3d2pXZXcrUTU4WTBBMnVmTHV3MjhVU09udFdqUk5FeEF3NkxwYUJhZmRXZjFNWlJwejlIcjZtUEFVOHFRVlA0blBNZHVqSVdxNmV4bjVZSmZrTXFhVlN1YmRUbHE5UlhzbTVNSTNhK0xCeXB3Wm0yeGlOQ2xnMTZqOUdxcmk2OXVteTlaUTVxV1BIUnhuN0pWd2JuYWJKTm1xOVJOR2Q3T0Z5RVFTdnEyQTJ0UnJEU1ViOFZlaHpDTGdaL0d3TGN6ZjdCM2x1SGVGNTNiWTA4ZkY0YnNqSVQ2Wm4wU0hIWWdSM2N4NForNEtzc0trMytZalZBMHY3cjAra0NoS0lQYWtrOUtkNFZGL0RMbzJST2h3Q0JsNTBUMUVIdUlHeFVJaGxCNHB3MWRaSWdwNzNJdlJCV2VGNVI4b2VnTDJpVGRYWkdqYWo0SGh2Y3QrMXZTemIxV3NTM0YrdmZKd3hDdUNRaGNFWG01djZBdzRaR1c0dXpHdm8zZGNRdmNYUlNEQUsxUml2a0RXcG1SOHlFaUdKRWlYbkNwd0VkTW5Vd2hvdGhmVnpUNFlBdEhJRFU5SUVzTG5iTkJibnJEeWhlRVFkN1JXUFVjOU5vbEZGemlRUXFmUVVSWTBEcDlEWTdnaUJucm8zcmNtTXh4TjBRd0hLc1ZMcUtzc2lqNzZPR0NRYmdXTUl2b2Zrd2haNXp3MERZZ1E5d1pRa1k0dStOM1FhL0NnSHJUNU53QmRpN0VOa0tBaTlzZnpORytGZ3pZbGVWdWl6QUM4MWVFUzI4TmNEREgzZ1NlNXhLaTZzWis4ZFkxT3pBVmV0Zk1TLzRuS1F4K3JDa1krL0I4bUppNXJrYWhQaVBDMHZFNzBSNHhrM2Y0bHhsTlEyVUlaaHJhRExsditGcTROY0QxSUtpZkRMQ2d0RmZoalNUaDZ4SlNjdE5hK1p4bjg1YlN1a1JPeCtudDZ3RE1TWEliaVlYWGVJV0J4c0ltV2hiMVR5dVVDQ2xmbTltSnBiNHBLZGtCK1l2YmNRN0U4bGdDK0FIQlZNdi9mU0VyRE1yeU1TbnBpT0dBb1ZOSEtsWHNnQmsrUmhCT3VlREUyV1VhTDhjZWQyTDdqOU1Dc3VMNzhIT28yMEVyUjhtOFdGR09CNjhPVGdkUnh0bDFydldvTCtuekdQZFE1N3ZDSDhEQU9YQXZqbnNFdTk3WHVtWjlwdXoybUxNbGhpZS80YkFEaDF5djRsVFFVak9oOXV6ZFV3aHJIdVZIcHVERDNOWm9kMS9GbkVDbGRXZjlZU1pzN2ZSZC9menVPVFRlNGFrSlJlM1MyT2VPMFhIazh5ZDRVTHRQZHd0OCtLZ3pGM3BSb2lxS202RjdCNU0yRGtTZ3l2aWYrRUNIUVRFMk1IK0JKWjBHUU1UZ0x3MDJRSDhPSnJqTDZmUzNGSHRjSEJBeERBRjZIcDd6blE0bDVxYS82a2h4bnFkQVdDWUNMNDV3WmdRVC90ai9sVFVBYXJHcE53ZERqUFYyTHBPNjcrVnExTzkwbDk1SFlRYVEyNGlhck9IYWo0NTlmbHduY2VhaUJzN25kOXl6OW5zdG9xMHcweXlUZ05tWWRiQnFQaC9HUlYrNzVUQ083RFQ0M2MxNG9Wdkg0bFpSR01NU1k4Vm9rSkhMU2p0Z2FKRkRQVzJ6c0o1UnpHNThQZkY1Vy9nYmV2ZWlMVXBLaEIvYllFU280ZjdtdDYxZ1FWaGhxaE1kcGlkRFdGRENrZmVhUVE9PC9DaXBoZXJWYWx1ZT48L0NpcGhlckRhdGE+PC9FbmNyeXB0ZWREYXRhPjwvTEE+PFNpZ25hdHVyZSB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PFNpZ25lZEluZm8geG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvVFIvMjAwMS9SRUMteG1sLWMxNG4tMjAwMTAzMTUiPjwvQ2Fub25pY2FsaXphdGlvbk1ldGhvZD48U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9EUk0vMjAwNy8wMy9wcm90b2NvbHMjZWNkc2Etc2hhMjU2Ij48L1NpZ25hdHVyZU1ldGhvZD48UmVmZXJlbmNlIFVSST0iI1NpZ25lZERhdGEiPjxEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL0RSTS8yMDA3LzAzL3Byb3RvY29scyNzaGEyNTYiPjwvRGlnZXN0TWV0aG9kPjxEaWdlc3RWYWx1ZT5Md1YyN0RNVkxjTXpzNHEwa2JxVkJEeVhkVkVOU3ExcUt1Si8xVlVDeDlzPTwvRGlnZXN0VmFsdWU+PC9SZWZlcmVuY2U+PC9TaWduZWRJbmZvPjxTaWduYXR1cmVWYWx1ZT55blBvMXlKN1hUcFhoMVpHdHdOSUtzeGFZNHd0d2J4cjJQODdXNzA0MTdvZE55dW5wUXY0dXBJSUZoT0EyeEZEb1JDQkR2QVYzQTQyb1NSNzdITk5xQT09PC9TaWduYXR1cmVWYWx1ZT48S2V5SW5mbyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PEtleVZhbHVlPjxFQ0NLZXlWYWx1ZT48UHVibGljS2V5PldjclVld3VCRCtNT2toMTZFdGJmTkMvWm9WSjM5Y3NGcVJUNEFRWmFJdjVkdXdhZjFmczR0anpzSWlPQTljZUVIVEtZM2VMSkJWVzQrVFhrR1RsNElBPT08L1B1YmxpY0tleT48L0VDQ0tleVZhbHVlPjwvS2V5VmFsdWU+PC9LZXlJbmZvPjwvU2lnbmF0dXJlPjwvQ2hhbGxlbmdlPjwvY2hhbGxlbmdlPjwvQWNxdWlyZUxpY2Vuc2U+PC9zb2FwOkJvZHk+PC9zb2FwOkVudmVsb3BlPg==" + + +esn_map: + # key map of CDM WVD `SystemID = 'ESN you want to use for that CDM WVD'` + 54321: 'NFCDIE-03-L3GCH5UJ17J0MT4QF1Y9NFV016J8YM' +endpoints: + website: 'https://www.netflix.com/nq/website/memberapi/{build_id}/pathEvaluator' + manifest: 'https://www.netflix.com/nq/msl_v1/cadmium/pbo_manifests/^1.0.0/router' + licence: 'https://www.netflix.com/nq/msl_v1/cadmium/pbo_licenses/^1.0.0/router' + metadata: 'https://www.netflix.com/nq/website/memberapi/{build_id}/metadata' + # https://www.netflix.com/msl/playapi/cadmium/logblob/1?reqAttempt=1&reqPriority=0&reqName=logblob + # possibly newer api? seems to work fine on handshaking, rest untested + +configuration: + drm_system: 'playready' # chrome and android: widevine, edge: playready + drm_version: 30 # widevine: 25, playready: 30 + supported_hdcp_versions: ['2.2'] # 720p-max: 1.4, chrome: empty, 4k: 2.2 + is_hdcp_engaged: true # chrome: false + +profiles: + video: + H264: + BPL: + - 'playready-h264bpl30-dash' + + MPL: + - 'playready-h264mpl30-dash' + - 'playready-h264mpl31-dash' + - 'playready-h264mpl40-dash' + + HPL: + - 'playready-h264hpl30-dash' + - 'playready-h264hpl31-dash' + - 'playready-h264hpl40-dash' + + QC: + - h264mpl30-dash-playready-prk-qc + - h264mpl31-dash-playready-prk-qc + - h264mpl40-dash-playready-prk-qc + H265: + SDR: + - 'hevc-main10-L30-dash-cenc' + - 'hevc-main10-L30-dash-cenc-prk' + - 'hevc-main10-L30-dash-cenc-prk-do' + - 'hevc-main10-L31-dash-cenc' + - 'hevc-main10-L31-dash-cenc-prk' + - 'hevc-main10-L31-dash-cenc-prk-do' + - 'hevc-main10-L40-dash-cenc' + - 'hevc-main10-L40-dash-cenc-prk' + - 'hevc-main10-L40-dash-cenc-prk-do' + - 'hevc-main10-L50-dash-cenc' + - 'hevc-main10-L41-dash-cenc-prk' + - 'hevc-main10-L41-dash-cenc-prk-do' + - 'hevc-main10-L50-dash-cenc' + - 'hevc-main10-L50-dash-cenc-prk' + - 'hevc-main10-L50-dash-cenc-prk-do' + - 'hevc-main10-L51-dash-cenc' + - 'hevc-main10-L51-dash-cenc-prk' + - 'hevc-main10-L51-dash-cenc-prk-do' + + SDR_DO: + + - 'hevc-main10-L30-dash-cenc-prk-do' + + - 'hevc-main10-L31-dash-cenc-prk-do' + + - 'hevc-main10-L40-dash-cenc-prk-do' + + - 'hevc-main10-L41-dash-cenc-prk-do' + + - 'hevc-main10-L50-dash-cenc-prk-do' + + - 'hevc-main10-L51-dash-cenc-prk-do' + + SDR_CENC: + - 'hevc-main10-L30-dash-cenc' + + - 'hevc-main10-L31-dash-cenc' + + - 'hevc-main10-L40-dash-cenc' + + - 'hevc-main10-L50-dash-cenc' + + - 'hevc-main10-L50-dash-cenc' + + - 'hevc-main10-L51-dash-cenc' + + + HDR10: + - 'hevc-hdr-main10-L30-dash-cenc' + - 'hevc-hdr-main10-L30-dash-cenc-prk' + - 'hevc-hdr-main10-L30-dash-cenc-prk-do' + - 'hevc-hdr-main10-L31-dash-cenc' + - 'hevc-hdr-main10-L31-dash-cenc-prk' + - 'hevc-hdr-main10-L31-dash-cenc-prk-do' + - 'hevc-hdr-main10-L40-dash-cenc' + - 'hevc-hdr-main10-L40-dash-cenc-prk' + - 'hevc-hdr-main10-L40-dash-cenc-prk-do' + - 'hevc-hdr-main10-L41-dash-cenc' + - 'hevc-hdr-main10-L41-dash-cenc-prk' + - 'hevc-hdr-main10-L41-dash-cenc-prk-do' + - 'hevc-hdr-main10-L50-dash-cenc' + - 'hevc-hdr-main10-L50-dash-cenc-prk' + - 'hevc-hdr-main10-L50-dash-cenc-prk-do' + - 'hevc-hdr-main10-L51-dash-cenc' + - 'hevc-hdr-main10-L51-dash-cenc-prk' + - 'hevc-hdr-main10-L51-dash-cenc-prk-do' + + HDR10_CENC: + - 'hevc-hdr-main10-L30-dash-cenc' + + - 'hevc-hdr-main10-L31-dash-cenc' + + - 'hevc-hdr-main10-L40-dash-cenc' + + - 'hevc-hdr-main10-L41-dash-cenc' + + - 'hevc-hdr-main10-L50-dash-cenc' + + - 'hevc-hdr-main10-L51-dash-cenc' + + HDR10_DO: + + - 'hevc-hdr-main10-L30-dash-cenc-prk-do' + + - 'hevc-hdr-main10-L31-dash-cenc-prk-do' + + - 'hevc-hdr-main10-L40-dash-cenc-prk-do' + + - 'hevc-hdr-main10-L41-dash-cenc-prk-do' + + - 'hevc-hdr-main10-L50-dash-cenc-prk-do' + + - 'hevc-hdr-main10-L51-dash-cenc-prk-do' + + DV: + - 'hevc-dv5-main10-L30-dash-cenc' + - 'hevc-dv5-main10-L30-dash-cenc-prk' + - 'hevc-dv5-main10-L30-dash-cenc-prk-do' + - 'hevc-dv5-main10-L31-dash-cenc' + - 'hevc-dv5-main10-L31-dash-cenc-prk' + - 'hevc-dv5-main10-L31-dash-cenc-prk-do' + - 'hevc-dv5-main10-L40-dash-cenc' + - 'hevc-dv5-main10-L40-dash-cenc-prk' + - 'hevc-dv5-main10-L40-dash-cenc-prk-do' + - 'hevc-dv5-main10-L41-dash-cenc' + - 'hevc-dv5-main10-L41-dash-cenc-prk' + - 'hevc-dv5-main10-L41-dash-cenc-prk-do' + - 'hevc-dv5-main10-L50-dash-cenc' + - 'hevc-dv5-main10-L50-dash-cenc-prk' + - 'hevc-dv5-main10-L50-dash-cenc-prk-do' + - 'hevc-dv5-main10-L51-dash-cenc' + - 'hevc-dv5-main10-L51-dash-cenc-prk' + - 'hevc-dv5-main10-L51-dash-cenc-prk-do' + + AV1: + - 'av1-main-L30-dash-cbcs' + - 'av1-main-L30-dash-cbcs-prk' + - 'av1-main-L31-dash-cbcs' + - 'av1-main-L31-dash-cbcs-prk' + - 'av1-main-L40-dash-cbcs' + - 'av1-main-L40-dash-cbcs-prk' + - 'av1-main-L41-dash-cbcs' + - 'av1-main-L41-dash-cbcs-prk' + - 'av1-main-L50-dash-cbcs' + - 'av1-main-L50-dash-cbcs-prk' + - 'av1-main-L51-dash-cbcs' + - 'av1-main-L51-dash-cbcs-prk' + + VP9: + P0: + - 'vp9-profile0-L30-dash-cenc' + - 'vp9-profile0-L30-dash-cenc-prk' + - 'vp9-profile0-L31-dash-cenc' + - 'vp9-profile0-L31-dash-cenc-prk' + - 'vp9-profile0-L40-dash-cenc' + - 'vp9-profile0-L40-dash-cenc-prk' + - 'vp9-profile0-L41-dash-cenc' + - 'vp9-profile0-L41-dash-cenc-prk' + + P1: + - 'vp9-profile1-L30-dash-cenc' + - 'vp9-profile1-L30-dash-cenc-prk' + - 'vp9-profile1-L31-dash-cenc' + - 'vp9-profile1-L31-dash-cenc-prk' + - 'vp9-profile1-L40-dash-cenc' + - 'vp9-profile1-L40-dash-cenc-prk' + - 'vp9-profile1-L41-dash-cenc' + - 'vp9-profile1-L41-dash-cenc-prk' + + P2: + - 'vp9-profile2-L30-dash-cenc' + - 'vp9-profile2-L30-dash-cenc-prk' + - 'vp9-profile2-L31-dash-cenc' + - 'vp9-profile2-L31-dash-cenc-prk' + - 'vp9-profile2-L40-dash-cenc' + - 'vp9-profile2-L40-dash-cenc-prk' + - 'vp9-profile2-L41-dash-cenc' + - 'vp9-profile2-L41-dash-cenc-prk' + + HDR10: + - 'vp9-hdr-profile2-L30-dash-cenc-prk' + - 'vp9-hdr-profile2-L31-dash-cenc-prk' + - 'vp9-hdr-profile2-L40-dash-cenc-prk' + - 'vp9-hdr-profile2-L41-dash-cenc-prk' + - 'vp9-hdr-profile2-L50-dash-cenc-prk' + - 'vp9-hdr-profile2-L51-dash-cenc-prk' + audio: + AAC: + - 'heaac-2-dash' + - 'heaac-2hq-dash' + - 'heaac-5.1-dash' + + AC3: + - 'dd-5.1-dash' + + EC3: + - 'ddplus-2.0-dash' + - 'ddplus-5.1-dash' + - 'ddplus-5.1hq-dash' + - 'ddplus-atmos-dash' + + VORB: + - 'playready-oggvorbis-2-dash' + - 'playready-oggvorbis-5-dash' + + subtitles: + - 'webvtt-lssdh-ios8' diff --git a/vinetrimmer/config/Services/peacock.yml b/vinetrimmer/config/Services/peacock.yml new file mode 100644 index 0000000..0b9a880 --- /dev/null +++ b/vinetrimmer/config/Services/peacock.yml @@ -0,0 +1,27 @@ +endpoints: + stream_tv: 'https://www.peacocktv.com/stream-tv/{title_id}' + config: 'https://config.clients.peacocktv.com/{territory}/{provider}/{proposition}/{device}/PROD/{version}/config.json' + login: 'https://rango.id.peacocktv.com/signin/service/international' + personas: 'https://persona.id.peacocktv.com/persona-store/personas' + tokens: 'https://ovp.peacocktv.com/auth/tokens' + me: 'https://ovp.peacocktv.com/auth/users/me' + node: 'https://atom.peacocktv.com/adapter-calypso/v3/query/node' + vod: 'https://ovp.peacocktv.com/video/playouts/vod' + +client: + config_version: '1.0.8' + territory: 'US' + provider: 'NBCU' + proposition: 'NBCUOTT' + platform: 'ANDROID' # PC, ANDROID + device: 'TABLET' # COMPUTER, TABLET + id: 'Jcvf1y0whKOI29vRXcJy' + drm_device_id: 'UNKNOWN' + client_sdk: 'NBCU-WEB-v4' # NBCU-ANDROID-v3 NBCU-ANDRTV-v4 + auth_scheme: 'MESSO' + auth_issuer: 'NOWTV' + +security: + signature_hmac_key_v4: 'FvT9VtwvhtSZvqnExMsvDDTEvBqR3HdsMcBFtWYV' + signature_hmac_key_v6: 'izU6EJqqu6DOhOWSk5X4p9dod3fNqH7vzKtYDK8d' + signature_format: 'SkyOTT client="{client}",signature="{signature}",timestamp="{timestamp}",version="1.0"' diff --git a/vinetrimmer/config/__init__.py b/vinetrimmer/config/__init__.py new file mode 100644 index 0000000..16c8306 --- /dev/null +++ b/vinetrimmer/config/__init__.py @@ -0,0 +1,72 @@ +import os +import tempfile +from types import SimpleNamespace +from pathlib import Path + +import yaml +from appdirs import AppDirs +from requests.utils import CaseInsensitiveDict + +from vinetrimmer.objects.vaults import Vault +from vinetrimmer.utils.collections import merge_dict + + +class Config: + @staticmethod + def load_vault(vault): + return Vault(**{ + "type_" if k == "type" else k: v for k, v in vault.items() + }) + + +class Directories: + def __init__(self): + self.app_dirs = AppDirs("vinetrimmer", False) + self.package_root = Path(__file__).resolve().parent.parent + self.configuration = self.package_root / "config" + self.user_configs = self.package_root + self.service_configs = self.user_configs / "Services" + self.data = self.package_root + self.downloads = Path(__file__).resolve().parents[2] / "Downloads" + self.temp = Path(__file__).resolve().parents[2] / "Temp" + self.cache = self.package_root / "Cache" + self.cookies = self.data / "Cookies" + self.logs = self.package_root / "Logs" + self.devices = self.data / "devices" + +class Filenames: + def __init__(self): + self.log = os.path.join(directories.logs, "vinetrimmer_{time}.log") + self.root_config = os.path.join(directories.configuration, "vinetrimmer.yml") + self.user_root_config = os.path.join(directories.user_configs, "vinetrimmer.yml") + self.service_config = os.path.join(directories.configuration, "services", "{service}.yml") + self.user_service_config = os.path.join(directories.service_configs, "{service}.yml") + self.subtitles = os.path.join(directories.temp, "TextTrack_{id}_{language_code}.srt") + self.chapters = os.path.join(directories.temp, "{filename}_chapters.txt") + + +directories = Directories() +filenames = Filenames() +with open(filenames.root_config) as fd: + config = yaml.safe_load(fd) +with open(filenames.user_root_config) as fd: + user_config = yaml.safe_load(fd) +merge_dict(config, user_config) +config = SimpleNamespace(**config) +credentials = config.credentials + +# This serves two purposes: +# - Allow `range` to be used in the arguments section in the config rather than just `range_` +# - Allow sections like [arguments.Amazon] to work even if an alias (e.g. AMZN or amzn) is used. +# CaseInsensitiveDict is used for `arguments` above to achieve case insensitivity. +# NOTE: The import cannot be moved to the top of the file, it will cause a circular import error. +from vinetrimmer.services import SERVICE_MAP # noqa: E402 + +if "range_" not in config.arguments: + config.arguments["range_"] = config.arguments.get("range_") +for service, aliases in SERVICE_MAP.items(): + for alias in aliases: + config.arguments[alias] = config.arguments.get(service) +config.arguments = CaseInsensitiveDict(config.arguments) + + diff --git a/vinetrimmer/config/vinetrimmer.yml b/vinetrimmer/config/vinetrimmer.yml new file mode 100644 index 0000000..62cefda --- /dev/null +++ b/vinetrimmer/config/vinetrimmer.yml @@ -0,0 +1,48 @@ +# This is the default configuration file for vinetrimmer. +# DO NOT edit this file. +# +# Copy example_configs/vinetrimmer.yml to the appropriate directory for your platform instead +# (see the README for more details). + +decrypter: 'packager' +tag: 'JEFF' +tag_sd: 'JEFF' + +arguments: {} + +aria2c: + file_allocation: 'prealloc' + + +cdm: + default: 'hisense_smarttv_he55a7000euwts_sl3000' + +cdm_api: [] + +credentials: + HuluJP: 'email:password' # single profile + +directories: + temp: '' + +headers: + User-Agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0' + +key_vaults: + - type: 'local' + name: 'Local' + path: '{data_dir}/key_store.db' + +output_template: + movies: '{title}.{year}.{quality}.{source}.WEB-DL.{video}.{audio}-{tag}' + series: '{title}.{season_episode}.{episode_name}.{quality}.{source}.WEB-DL.{video}.{audio}-{tag}' + use_last_audio: false + +profiles: + default: 'default' + +proxies: {} + +nordvpn: {} + + diff --git a/vinetrimmer/constants.py b/vinetrimmer/constants.py new file mode 100644 index 0000000..d4afae2 --- /dev/null +++ b/vinetrimmer/constants.py @@ -0,0 +1,38 @@ +LANGUAGE_MUX_MAP = { + # List of language tags that cannot be used by mkvmerge and need replacements. + "none": "und", + "nb": "nor", +} + +TERRITORY_MAP = { + "001": "", + "150": "European", + "419": "Latin American", + "AU": "Australian", + "BE": "Flemish", + "BR": "Brazilian", + "CA": "Canadian", + "CZ": "", + "DK": "", + "EG": "Egyptian", + "ES": "European", + "FR": "European", + "GB": "British", + "GR": "", + "HK": "Hong Kong", + "IL": "", + "IN": "", + "JP": "", + "KR": "", + "MY": "", + "NO": "", + "PH": "", + "PS": "Palestinian", + "PT": "European", + "SE": "", + "SY": "Syrian", + "US": "American", +} + +# The max distance of languages to be considered "same", e.g. en, en-US, en-AU +LANGUAGE_MAX_DISTANCE = 5 diff --git a/vinetrimmer/key_store.db b/vinetrimmer/key_store.db new file mode 100644 index 0000000..b836e33 Binary files /dev/null and b/vinetrimmer/key_store.db differ diff --git a/vinetrimmer/objects/__init__.py b/vinetrimmer/objects/__init__.py new file mode 100644 index 0000000..739e2e4 --- /dev/null +++ b/vinetrimmer/objects/__init__.py @@ -0,0 +1,4 @@ +# flake8: noqa +from vinetrimmer.objects.credential import Credential +from vinetrimmer.objects.titles import Title, Titles +from vinetrimmer.objects.tracks import AudioTrack, MenuTrack, TextTrack, Track, Tracks, VideoTrack diff --git a/vinetrimmer/objects/credential.py b/vinetrimmer/objects/credential.py new file mode 100644 index 0000000..ed57a5b --- /dev/null +++ b/vinetrimmer/objects/credential.py @@ -0,0 +1,81 @@ +import hashlib +import re + +import requests +import validators + + +class Credential: + """Username (or Email) and Password Credential.""" + + def __init__(self, username, password, extra=None): + self.username = username + self.password = password + self.extra = extra + self.sha1 = hashlib.sha1(self.dumps().encode()).hexdigest() + + def __bool__(self): + return bool(self.username) and bool(self.password) + + def __str__(self): + return self.dumps() + + def __repr__(self): + return "{name}({items})".format( + name=self.__class__.__name__, + items=", ".join([f"{k}={repr(v)}" for k, v in self.__dict__.items()]) + ) + + def dumps(self): + """Return credential data as a string.""" + return f"{self.username}:{self.password}" + (f":{self.extra}" if self.extra else "") + + def dump(self, path): + """Write credential data to a file.""" + with open(path, "w", encoding="utf-8") as fd: + fd.write(self.dumps()) + + @classmethod + def loads(cls, text): + """ + Load credential from a text string. + + Format: {username}:{password} + Rules: + Only one Credential must be in this text contents. + All whitespace before and after all text will be removed. + Any whitespace between text will be kept and used. + The credential can be spanned across one or multiple lines as long as it + abides with all the above rules and the format. + + Example that follows the format and rules: + `\tJohnd\noe@gm\n\rail.com\n:Pass1\n23\n\r \t \t` + >>>Credential(username='Johndoe@gmail.com', password='Pass123') + """ + text = "".join([ + x.strip() for x in text.splitlines(keepends=False) + ]).strip() + credential = re.fullmatch(r"^([^:]+?):([^:]+?)(?::(.+))?$", text) + if credential: + return cls(*credential.groups()) + raise ValueError("No credentials found in text string. Expecting the format `username:password`") + + @classmethod + def load(cls, uri, session=None): + """ + Load Credential from a remote URL string or a local file path. + Use Credential.loads() for loading from text content and seeing the rules and + format expected to be found in the URIs contents. + + Parameters: + uri: Remote URL string or a local file path. + session: Python-requests session to use for Remote URL strings. This can be + used to set custom Headers, Proxies, etc. + """ + if validators.url(uri): + # remote file + return cls.loads((session or requests).get(uri).text) + else: + # local file + with open(uri, encoding="utf-8") as fd: + return cls.loads(fd.read()) diff --git a/vinetrimmer/objects/titles.py b/vinetrimmer/objects/titles.py new file mode 100644 index 0000000..5501f01 --- /dev/null +++ b/vinetrimmer/objects/titles.py @@ -0,0 +1,204 @@ +import logging +import re +import unicodedata +from enum import Enum + +from langcodes import Language +from unidecode import unidecode + +from vinetrimmer.objects.tracks import Tracks + +VIDEO_CODEC_MAP = { + "AVC": "H.264", + "HEVC": "H.265" +} +DYNAMIC_RANGE_MAP = { + "HDR10": "HDR", + "HDR10+": "HDR", + "HDR10 / HDR10+": "HDR", + "Dolby Vision": "DV" +} +AUDIO_CODEC_MAP = { + "E-AC-3": "DDP", + "AC-3": "DD" +} + + +class Title: + def __init__(self, id_, type_, name=None, year=None, season=None, episode=None, episode_name=None, + original_lang=None, source=None, service_data=None, tracks=None, filename=None): + self.id = id_ + self.type = type_ + self.name = name + self.year = int(year or 0) + self.season = int(season or 0) + self.episode = int(episode or 0) + self.episode_name = episode_name + self.original_lang = Language.get(original_lang) if original_lang else None + self.source = source + self.service_data = service_data or {} + self.tracks = tracks or Tracks() + self.filename = filename + + if not self.filename: + # auto generated initial filename + self.filename = self.parse_filename() + + def parse_filename(self, media_info=None, folder=False): + from vinetrimmer.config import config + + if media_info: + video_track = next(iter(media_info.video_tracks), None) + if config.output_template.get("use_last_audio", False): + audio_track = next(iter(reversed(media_info.audio_tracks)), None) + else: + audio_track = next(iter(media_info.audio_tracks), None) + else: + video_track = None + audio_track = None + + # create the initial filename string + + if video_track: + quality = video_track.height + aspect = [int(float(x)) for x in video_track.other_display_aspect_ratio[0].split(":")] + if len(aspect) == 1: + aspect.append(1) + aspect_w, aspect_h = aspect + if aspect_w / aspect_h not in (16 / 9, 4 / 3): + # We want the resolution represented in a 4:3 or 16:9 canvas + # if it's not 4:3 or 16:9, calculate as if it's inside a 16:9 canvas + # otherwise the track's height value is fine + # We are assuming this title is some weird aspect ratio so most + # likely a movie or HD source, so it's most likely widescreen so + # 16:9 canvas makes the most sense + quality = int(video_track.width * (9 / 16)) + if video_track.width == 1248: # AMZN weird resolution (1248x520) + quality = 720 + if isinstance(self.tracks.videos[0].extra, dict) and self.tracks.videos[0].extra.get("quality"): + quality = self.tracks.videos[0].extra["quality"] + else: + quality = "" + + if audio_track: + audio = f"{AUDIO_CODEC_MAP.get(audio_track.format) or audio_track.format}" + audio += f"{float(sum({'LFE': 0.1}.get(x, 1) for x in audio_track.channel_layout.split(' '))):.1f} " + if audio_track.format_additionalfeatures and "JOC" in audio_track.format_additionalfeatures: + audio += "Atmos " + else: + audio = "" + + video = "" + if video_track: + if (video_track.hdr_format or "").startswith("Dolby Vision"): + video += "DV " + elif video_track.hdr_format_commercial: + video += f"{DYNAMIC_RANGE_MAP.get(video_track.hdr_format_commercial)} " + elif ("HLG" in (video_track.transfer_characteristics or "") + or "HLG" in (video_track.transfer_characteristics_original or "")): + video += "HLG " + if float(video_track.frame_rate) > 30 and self.source != "iP": + video += "HFR " + video += f"{VIDEO_CODEC_MAP.get(video_track.format) or video_track.format}" + + tag = config.tag + if quality and quality <= 576: + tag = config.tag_sd or tag + + if self.type == Title.Types.MOVIE: + filename = config.output_template["movies"].format( + title=self.name, + year=self.year or "", + quality=f"{quality}p" if quality else "", + source=self.source, + audio=audio, + video=video, + tag=tag, + ) + else: + episode_name = self.episode_name + # TODO: Maybe we should only strip these if all episodes have such names. + if re.fullmatch(r"(?:Episode|Chapter|Capitulo|Folge) \d+", episode_name or ""): + episode_name = None + + filename = config.output_template["series"].format( + title=self.name, + season_episode=(f"S{self.season:02}" + + (f"E{self.episode:02}" if (self.episode is not None and not folder) else "")), + episode_name=(episode_name or "") if not folder else "", + quality=f"{quality}p" if quality else "", + source=self.source, + audio=audio, + video=video, + tag=tag, + ) + + filename = re.sub(r"\s+", ".", filename) + filename = re.sub(r"\.\.+", ".", filename) + filename = re.sub(fr"\.+(-{re.escape(config.tag)})$", r"\1", filename) + filename = filename.rstrip().rstrip(".") # remove whitespace and last right-sided . if needed + + return self.normalize_filename(filename) + + @staticmethod + def normalize_filename(filename): + # replace all non-ASCII characters with ASCII equivalents + filename = filename.replace("æ", "ae") + filename = filename.replace("ø", "oe") + filename = filename.replace("å", "aa") + filename = filename.replace("'", "") + filename = unidecode(filename) + filename = "".join(c for c in filename if unicodedata.category(c) != "Mn") + + # remove or replace further characters as needed + filename = filename.replace("/", " - ") # e.g. amazon multi-episode titles + filename = filename.replace("&", " and ") + filename = filename.replace("$", "S") + filename = re.sub(r"[:; ]", ".", filename) # structural chars to . + filename = re.sub(r"[\\*!?¿,'\"()<>|#]", "", filename) # unwanted chars + filename = re.sub(r"[. ]{2,}", ".", filename) # replace 2+ neighbour dots and spaces with . + return filename + + def is_wanted(self, wanted): + if self.type != Title.Types.TV or not wanted: + return True + return f"{self.season}x{self.episode}" in wanted + + class Types(Enum): + MOVIE = 1 + TV = 2 + + +class Titles(list): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.title_name = None + + if self: + self.title_name = self[0].name + + def print(self): + log = logging.getLogger("Titles") + log.info(f"Title: {self.title_name}") + if any(x.type == Title.Types.TV for x in self): + log.info(f"Total Episodes: {len(self)}") + log.info( + "By Season: {}".format( + ", ".join(list(dict.fromkeys( + f"{x.season} ({len([y for y in self if y.season == x.season])})" + for x in self if x.type == Title.Types.TV + ))) + ) + ) + + def order(self): + """This will order the Titles to be oldest first.""" + self.sort(key=lambda t: int(t.year or 0)) + self.sort(key=lambda t: int(t.episode or 0)) + self.sort(key=lambda t: int(t.season or 0)) + + def with_wanted(self, wanted): + """Yield only wanted tracks.""" + for title in self: + if title.is_wanted(wanted): + yield title diff --git a/vinetrimmer/objects/tracks.py b/vinetrimmer/objects/tracks.py new file mode 100644 index 0000000..41a58e2 --- /dev/null +++ b/vinetrimmer/objects/tracks.py @@ -0,0 +1,1364 @@ +import asyncio +import base64 +import logging +import math +import os +import re +import shutil +import subprocess +import sys +import uuid +from collections import defaultdict +from enum import Enum +from io import BytesIO, TextIOWrapper +import humanfriendly +import m3u8 +import pycaption +import requests +import pysubs2 +from langcodes import Language +from requests import Session +from vinetrimmer import config +from vinetrimmer.constants import LANGUAGE_MUX_MAP, TERRITORY_MAP +from vinetrimmer.utils import get_boxes, get_closest_match, is_close_match, try_get +from vinetrimmer.utils.collections import as_list +from vinetrimmer.utils.io import aria2c, download_range, saldl, m3u8dl +from vinetrimmer.utils.subprocess import ffprobe +#from vinetrimmer.utils.widevine.protos.widevine_pb2 import WidevineCencHeader +from vinetrimmer.utils.xml import load_xml +from vinetrimmer.vendor.pymp4.parser import Box, MP4 + +CODEC_MAP = { + # Video + "avc1": "H.264", + "avc3": "H.264", + "hev1": "H.265", + "hvc1": "H.265", + "dvh1": "H.265", + "dvhe": "H.265", + # Audio + "aac": "AAC", + "mp4a": "AAC", + "stereo": "AAC", + "HE": "HE-AAC", + "ac3": "AC3", + "ac-3": "AC3", + "eac": "E-AC3", + "eac-3": "E-AC3", + "ec-3": "E-AC3", + "atmos": "E-AC3", + # Subtitles + "srt": "SRT", + "vtt": "VTT", + "wvtt": "VTT", + "dfxp": "TTML", + "stpp": "TTML", + "ttml": "TTML", + "tt": "TTML", +} + + +class Track: + class Descriptor(Enum): + URL = 1 # Direct URL, nothing fancy + M3U = 2 # https://en.wikipedia.org/wiki/M3U (and M3U8) + MPD = 3 # https://en.wikipedia.org/wiki/Dynamic_Adaptive_Streaming_over_HTTP + ISM = 4 # https://bitmovin.com/blog/microsoft-smooth-streaming-mss/ + + def __init__(self, id_, source, url, codec, language=None, descriptor=Descriptor.URL, + needs_proxy=False, needs_repack=False, encrypted=False, pssh=None, note=None, kid=None, key=None, extra=None): + self.id = id_ + self.source = source + self.url = url + # required basic metadata + self.note= note + self.codec = codec + #self.language = Language.get(language or "none") + self.language = Language.get(language or "en") + self.is_original_lang = False # will be set later + # optional io metadata + self.descriptor = descriptor + self.needs_proxy = bool(needs_proxy) + self.needs_repack = bool(needs_repack) + # decryption + self.encrypted = bool(encrypted) + self.pssh = pssh + self.kid = kid + self.key = key + # extra data + self.extra = extra or {} # allow anything for extra, but default to a dict + + # should only be set internally + self._location = None + + def __repr__(self): + return "{name}({items})".format( + name=self.__class__.__name__, + items=", ".join([f"{k}={repr(v)}" for k, v in self.__dict__.items()]) + ) + + def __eq__(self, other): + return isinstance(other, Track) and self.id == other.id + + def get_track_name(self): + """Return the base track name. This may be enhanced in subclasses.""" + if self.language is None: + self.language = Language.get("en") + if ((self.language.language or "").lower() == (self.language.territory or "").lower() + and self.language.territory not in TERRITORY_MAP): + self.language.territory = None # e.g. de-DE + if self.language.territory == "US": + self.language.territory = None + language = self.language.simplify_script() + extra_parts = [] + if language.script is not None: + extra_parts.append(language.script_name()) + if language.territory is not None: + territory = language.territory_name() + extra_parts.append(TERRITORY_MAP.get(language.territory, territory)) + return ", ".join(extra_parts) or None + + def get_data_chunk(self, session=None): + """Get the data chunk from the track's stream.""" + if not session: + session = Session() + + url = None + + if self.descriptor == self.Descriptor.M3U: + master = m3u8.loads(session.get(as_list(self.url)[0]).text, uri=self.url) + for segment in master.segments: + if not segment.init_section: + continue + if self.source in ["DSNP", "STRP"] and re.match(r"^[a-zA-Z0-9]{4}-(BUMPER|DUB_CARD)/", segment.init_section.uri): + continue + url = ("" if re.match("^https?://", segment.init_section.uri) else segment.init_section.base_uri) + url += segment.init_section.uri + break + + if not url: + url = as_list(self.url)[0] + + with session.get(url, stream=True) as s: + # assuming enough to contain the pssh/kid + for chunk in s.iter_content(20000): + # we only want the first chunk + return chunk + + # assuming 20000 bytes is enough to contain the pssh/kid box + return download_range(url, 20000, proxy=proxy) + + def get_pssh(self, session=None): + """ + Get the PSSH of the track. + + Parameters: + session: Requests Session, best to provide one if cookies/headers/proxies are needed. + + Returns: + True if PSSH is now available, False otherwise. PSSH will be stored in Track.pssh + automatically. + """ + + if self.pssh or not self.encrypted: + return True + + if self.descriptor == self.Descriptor.M3U: + # if an m3u, try get from playlist + master = m3u8.loads(session.get(as_list(self.url)[0]).text, uri=self.url) + for x in master.session_keys: + if x and x.keyformat.lower == "com.microsoft.playready": + self.pssh = x.uri.split(",")[-1] + break + for x in master.keys: + if x and "com.microsoft.playready" in str(x): + self.pssh = str(x).split("\"")[1].split(",")[-1] + break + + try: + xml_str = base64.b64decode(self.pssh).decode("utf-16-le", "ignore") + xml_str = xml_str[xml_str.index("<"):] + + xml = load_xml(xml_str).find("DATA") # root: WRMHEADER + + self.kid = xml.findtext("KID") # v4.0.0.0 + if not self.kid: # v4.1.0.0 + self.kid = next(iter(xml.xpath("PROTECTINFO/KID/@VALUE")), None) + if not self.kid: # v4.3.0.0 + self.kid = next(iter(xml.xpath("PROTECTINFO/KIDS/KID/@VALUE")), None) # can be multiple? + + self.kid = uuid.UUID(base64.b64decode(self.kid).hex()).bytes_le.hex() + + return True + + except: pass + + # boxes = [] + + # if self.descriptor == self.Descriptor.M3U: + # # if an m3u, try get from playlist + # master = m3u8.loads(session.get(as_list(self.url)[0]).text, uri=self.url) + # boxes.extend([ + # Box.parse(base64.b64decode(x.uri.split(",")[-1])) + # for x in (master.session_keys or master.keys) + # if x and x.keyformat.lower() == Cdm.urn + # ]) + + # data = self.get_data_chunk(session) + # if data: + # boxes.extend(list(get_boxes(data, b"pssh"))) + + # for box in boxes: + # if box.system_ID == Cdm.uuid: + # print(box.system_ID) + # self.pssh = box + # return True + + # for box in boxes: + # if box.system_ID == uuid.UUID("{9a04f079-9840-4286-ab92-e65be0885f95}"): + # xml_str = Box.build(box) + # xml_str = xml_str.decode("utf-16-le", "ignore") + # xml_str = xml_str[xml_str.index("<"):] + + # xml = load_xml(xml_str).find("DATA") # root: WRMHEADER + + # kid = xml.findtext("KID") # v4.0.0.0 + # if not kid: # v4.1.0.0 + # kid = next(iter(xml.xpath("PROTECTINFO/KID/@VALUE")), None) + # if not kid: # v4.3.0.0 + # kid = next(iter(xml.xpath("PROTECTINFO/KIDS/KID/@VALUE")), None) # can be multiple? + + # # self.pssh = Box.parse(Box.build(dict( + # # type=b"pssh", + # # version=0, + # # flags=0, + # # system_ID="9a04f079-9840-4286-ab92-e65be0885f95", + # # init_data=b"\x12\x10" + base64.b64decode(kid) + # # ))) + # return True + + return False + + def get_kid(self, session=None): + """ + Get the KID (encryption key id) of the Track. + The KID corresponds to the Encrypted segments of an encrypted Track. + + Parameters: + session: Requests Session, best to provide one if cookies/headers/proxies are needed. + + Returns: + True if KID is now available, False otherwise. KID will be stored in Track.kid + automatically. + """ + + try: + xml_str = base64.b64decode(self.pssh).decode("utf-16-le", "ignore") + xml_str = xml_str[xml_str.index("<"):] + + xml = load_xml(xml_str).find("DATA") # root: WRMHEADER + + self.kid = xml.findtext("KID") # v4.0.0.0 + if not self.kid: # v4.1.0.0 + self.kid = next(iter(xml.xpath("PROTECTINFO/KID/@VALUE")), None) + if not self.kid: # v4.3.0.0 + self.kid = next(iter(xml.xpath("PROTECTINFO/KIDS/KID/@VALUE")), None) # can be multiple? + + self.kid = uuid.UUID(base64.b64decode(self.kid).hex()).bytes_le.hex() + + except: pass + + if self.kid or not self.encrypted: + return True + + # boxes = [] + + # data = self.get_data_chunk(session) + + # if data: + # # try get via ffprobe, needed for non mp4 data e.g. WEBM from Google Play + # probe = ffprobe(data) + # if probe: + # kid = try_get(probe, lambda x: x["streams"]["tags"]["enc_key_id"]) + # if kid: + # kid = base64.b64decode(kid).hex() + # if kid != "00" * 16: + # self.kid = kid + # return True + # # get tenc and pssh boxes if available + # boxes.extend(list(get_boxes(data, b"tenc"))) + # boxes.extend(list(get_boxes(data, b"pssh"))) + + # # get the track's pssh box if available + # if self.get_pssh(): + # boxes.append(self.pssh) + + # # loop all found boxes and try find a KID + # for box in sorted(boxes, key=lambda b: b.type == b"tenc", reverse=True): + # if box.type == b"tenc": + # kid = box.key_ID.hex + # if kid != "00" * 16: + # self.kid = kid + # return True + # if box.type == b"pssh": + # if box.system_ID == Cdm.uuid: + # # Note: assumes only the first KID of a list is wanted + # if getattr(box, "key_IDs", None): + # kid = box.key_IDs[0].hex + # if kid != "00" * 16: + # self.kid = kid + # return True + # cenc_header = WidevineCencHeader() + # cenc_header.ParseFromString(box.init_data) + # if getattr(cenc_header, "key_id", None): + # kid = cenc_header.key_id[0] + # try: + # int(kid, 16) # KID may be already hex + # except ValueError: + # kid = kid.hex() + # else: + # kid = kid.decode() + # if kid != "00" * 16: + # self.kid = kid + # return True + + return False + + def download(self, out, name=None, headers=None, proxy=None): + """ + Download the Track and apply any necessary post-edits like Subtitle conversion. + + Parameters: + out: Output Directory Path for the downloaded track. + name: Override the default filename format. + Expects to contain `{type}`, `{id}`, and `{enc}`. All of them must be used. + headers: Headers to use when downloading. + proxy: Proxy to use when downloading. + + Returns: + Where the file was saved. + """ + if os.path.isfile(out): + raise ValueError("Path must be to a directory and not a file") + + os.makedirs(out, exist_ok=True) + + name = (name or "{type}_{id}_{enc}").format( + type=self.__class__.__name__, + id=self.id, + enc="enc" if self.encrypted else "dec" + ) + ".mp4" + save_path = os.path.join(out, name) + + if self.descriptor == self.Descriptor.M3U: + master = m3u8.loads( + requests.get( + as_list(self.url)[0], + headers=headers, + proxies={"all": proxy} if self.needs_proxy and proxy else None + ).text, + uri=as_list(self.url)[0] + ) + + # Keys may be [] or [None] if unencrypted + if any(master.keys + master.session_keys): + self.encrypted = True + self.get_kid() + self.get_pssh() + + durations = [] + duration = 0 + for segment in master.segments: + if segment.discontinuity: + durations.append(duration) + duration = 0 + duration += segment.duration + durations.append(duration) + largest_continuity = durations.index(max(durations)) + + discontinuity = 0 + has_init = False + segments = [] + for segment in master.segments: + if segment.discontinuity: + discontinuity += 1 + has_init = False + if self.source in ["DSNP", "STRP"] and re.search( + r"[a-zA-Z0-9]{4}-(BUMPER|DUB_CARD)/", + segment.uri + (segment.init_section.uri if segment.init_section else '') + ): + continue + if self.source == "ATVP" and discontinuity != largest_continuity: + # the amount of pre and post-roll sections change all the time + # only way to know which section to get is by getting the largest + continue + if segment.init_section and not has_init: + segments.append( + ("" if re.match("^https?://", segment.init_section.uri) else segment.init_section.base_uri) + + segment.init_section.uri + ) + has_init = True + segments.append( + ("" if re.match("^https?://", segment.uri) else segment.base_uri) + + segment.uri + ) + self.url = segments + if self.source == "CORE": + asyncio.run(saldl( + self.url, + save_path, + headers, + proxy if self.needs_proxy else None + )) + elif self.descriptor == self.Descriptor.ISM: + asyncio.run(m3u8dl( + self.url, + save_path, + self + )) + else: + asyncio.run(aria2c( + self.url, + save_path, + headers, + proxy if self.needs_proxy else None + )) + + if os.stat(save_path).st_size <= 3: # Empty UTF-8 BOM == 3 bytes + raise IOError( + "Download failed, the downloaded file is empty. " + f"This {'was' if self.needs_proxy else 'was not'} downloaded with a proxy." + + ( + " Perhaps you need to set `needs_proxy` as True to use the proxy for this track." + if not self.needs_proxy else "" + ) + ) + + self._location = save_path + return save_path + + def delete(self): + if self._location: + os.unlink(self._location) + self._location = None + + def repackage(self): + if not self._location: + raise ValueError("Cannot repackage a Track that has not been downloaded.") + fixed_file = f"{self._location}_fixed.mkv" + try: + subprocess.run([ + "ffmpeg", "-hide_banner", + "-loglevel", "panic", + "-i", self._location, + # Following are very important! + "-map_metadata", "-1", # don't transfer metadata to output file + "-fflags", "bitexact", # only have minimal tag data, reproducible mux + "-codec", "copy", + fixed_file + ], check=True) + self.swap(fixed_file) + except subprocess.CalledProcessError: + pass + + def locate(self): + return self._location + + def move(self, target): + if not self._location: + return False + ok = os.path.realpath(shutil.move(self._location, target)) == os.path.realpath(target) + if ok: + self._location = target + return ok + + def swap(self, target): + if not os.path.exists(target) or not self._location: + return False + os.unlink(self._location) + os.rename(target, self._location) + return True + + @staticmethod + def pt_to_sec(d): + if isinstance(d, float): + return d + if d[0:2] == "P0": + d = d.replace("P0Y0M0DT", "PT") + if d[0:2] != "PT": + raise ValueError("Input data is not a valid time string.") + d = d[2:].upper() # skip `PT` + m = re.findall(r"([\d.]+.)", d) + return sum( + float(x[0:-1]) * {"H": 60 * 60, "M": 60, "S": 1}[x[-1].upper()] + for x in m + ) + + +class VideoTrack(Track): + def __init__(self, *args, bitrate, width, size=None, height, fps=None, hdr10=False, hlg=False, dv=False, + needs_ccextractor=False, needs_ccextractor_first=False, **kwargs): + super().__init__(*args, **kwargs) + # required + self.bitrate = int(math.ceil(float(bitrate))) if bitrate else None + self.width = int(width) + self.height = int(height) + # optional + if "/" in str(fps): + num, den = fps.split("/") + self.fps = int(num) / int(den) + elif fps: + self.fps = float(fps) + else: + self.fps = None + self.size = size if size else None + self.hdr10 = bool(hdr10) + self.hlg = bool(hlg) + self.dv = bool(dv) + self.needs_ccextractor = needs_ccextractor + self.needs_ccextractor_first = needs_ccextractor_first + + def __str__(self): + codec = next((CODEC_MAP[x] for x in CODEC_MAP if (self.codec or "").startswith(x)), self.codec) + fps = f"{self.fps:.3f}" if self.fps else "Unknown" + size = f" ({humanfriendly.format_size(self.size, binary=True)})" if self.size else "" + return " | ".join([ + "├─ VID", + f"[{codec}, {'HDR10' if self.hdr10 else 'HLG' if self.hlg else 'DV' if self.dv else 'SDR'}]", + f"{self.width}x{self.height} @ {self.bitrate // 1000 if self.bitrate else '?'} kb/s{size}, {fps} FPS" + ]) + + def ccextractor(self, track_id, out_path, language, original=False): + """Return a TextTrack object representing CC track extracted by CCExtractor.""" + if not self._location: + raise ValueError("You must download the track first.") + + executable = shutil.which("ccextractor") or shutil.which("ccextractorwin") + if not executable: + raise EnvironmentError("ccextractor executable was not found.") + + p = subprocess.Popen([ + executable, + "-quiet", "-trim", "-noru", "-ru1", + self._location, "-o", out_path + ], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + for line in TextIOWrapper(p.stdout, encoding="utf-8"): + if "[iso file] Unknown box type ID32" not in line: + sys.stdout.write(line) + returncode = p.wait() + if returncode and returncode != 10: + raise self.log.exit(f" - ccextractor exited with return code {returncode}") + + if os.path.exists(out_path): + if os.stat(out_path).st_size <= 3: + # An empty UTF-8 file with BOM is 3 bytes. + # If the subtitle file is empty, mkvmerge will fail to mux. + os.unlink(out_path) + return None + cc_track = TextTrack( + id_=track_id, + source=self.source, + url="", # doesn't need to be downloaded + codec="srt", + language=language, + is_original_lang=original, # TODO: Figure out if this is the original title language + cc=True + ) + cc_track._location = out_path + return cc_track + + return None + + +class AudioTrack(Track): + #def __init__(self, *args, bitrate, channels=None, descriptive=False, **kwargs): + def __init__(self, *args, bitrate, size=None, channels=None, + descriptive: bool = False, atmos: bool = False, **kwargs): + super().__init__(*args, **kwargs) + # required + self.bitrate = int(math.ceil(float(bitrate))) if bitrate else None + self.size = size if size else None + self.channels = self.parse_channels(channels) if channels else None + self.atmos = bool(atmos) + # optional + self.descriptive = bool(descriptive) + + @staticmethod + def parse_channels(channels): + """ + Converts a string to a float-like string which represents audio channels. + E.g. "2" -> "2.0", "6" -> "5.1". + """ + # TODO: Support all possible DASH channel configurations (https://datatracker.ietf.org/doc/html/rfc8216) + if channels == "A000": + return "2.0" + if channels == "F801": + return "5.1" + + try: + channels = str(float(channels)) + except ValueError: + channels = str(channels) + + if channels == "6.0": + return "5.1" + + return channels + + def get_track_name(self): + """Return the base Track Name.""" + track_name = super().get_track_name() or "" + flag = self.descriptive and "Descriptive" + if flag: + if track_name: + flag = f" ({flag})" + track_name += flag + return track_name or None + + def __str__(self): + size = f" ({humanfriendly.format_size(self.size, binary=True)})" if self.size else "" + codec = next((CODEC_MAP[x] for x in CODEC_MAP if (self.codec or "").startswith(x)), self.codec) + return " | ".join([x for x in [ + "├─ AUD", + f"[{codec}]", + f"[{self.codec}{', atmos' if self.atmos else ''}]", + f"{self.channels}" if self.channels else None, + f"{self.bitrate // 1000 if self.bitrate else '?'} kb/s{size}", + f"{self.language}", + " ".join([self.get_track_name() or "", "[Original]" if self.is_original_lang else ""]).strip() + ] if x]) + + +class TextTrack(Track): + def __init__(self, *args, cc=False, sdh=False, forced=False, **kwargs): + """ + Information on Subtitle Types: + https://bit.ly/2Oe4fLC (3PlayMedia Blog on SUB vs CC vs SDH). + However, I wouldn't pay much attention to the claims about SDH needing to + be in the original source language. It's logically not true. + + CC == Closed Captions. Source: Basically every site. + SDH = Subtitles for the Deaf or Hard-of-Hearing. Source: Basically every site. + HOH = Exact same as SDH. Is a term used in the UK. Source: https://bit.ly/2PGJatz (ICO UK) + + More in-depth information, examples, and stuff to look for can be found in the Parameter + explanation list below. + + Parameters: + cc: Closed Caption. + - Intended as if you couldn't hear the audio at all. + - Can have Sound as well as Dialogue, but doesn't have to. + - Original source would be from an EIA-CC encoded stream. Typically all + upper-case characters. + Indicators of it being CC without knowing original source: + - Extracted with CCExtractor, or + - >>> (or similar) being used at the start of some or all lines, or + - All text is uppercase or at least the majority, or + - Subtitles are Scrolling-text style (one line appears, oldest line + then disappears). + Just because you downloaded it as a SRT or VTT or such, doesn't mean it + isn't from an EIA-CC stream. And I wouldn't take the streaming services + (CC) as gospel either as they tend to get it wrong too. + sdh: Deaf or Hard-of-Hearing. Also known as HOH in the UK (EU?). + - Intended as if you couldn't hear the audio at all. + - MUST have Sound as well as Dialogue to be considered SDH. + - It has no "syntax" or "format" but is not transmitted using archaic + forms like EIA-CC streams, would be intended for transmission via + SubRip (SRT), WebVTT (VTT), TTML, etc. + If you can see important audio/sound transcriptions and not just dialogue + and it doesn't have the indicators of CC, then it's most likely SDH. + If it doesn't have important audio/sounds transcriptions it might just be + regular subtitling (you wouldn't mark as CC or SDH). This would be the + case for most translation subtitles. Like Anime for example. + forced: Typically used if there's important information at some point in time + like watching Dubbed content and an important Sign or Letter is shown + or someone talking in a different language. + Forced tracks are recommended by the Matroska Spec to be played if + the player's current playback audio language matches a subtitle + marked as "forced". + However, that doesn't mean every player works like this but there is + no other way to reliably work with Forced subtitles where multiple + forced subtitles may be in the output file. Just know what to expect + with "forced" subtitles. + """ + super().__init__(*args, **kwargs) + self.cc = bool(cc) + self.sdh = bool(sdh) + if self.cc and self.sdh: + raise ValueError("A text track cannot be both CC and SDH.") + self.forced = bool(forced) + if (self.cc or self.sdh) and self.forced: + raise ValueError("A text track cannot be CC/SDH as well as Forced.") + + def get_track_name(self): + """Return the base Track Name.""" + track_name = super().get_track_name() or "" + flag = self.cc and "CC" or self.sdh and "SDH" or self.forced and "Forced" + if flag: + if track_name: + flag = f" ({flag})" + track_name += flag + return track_name or None + + @staticmethod + def parse(data, codec): + # TODO: Use an "enum" for subtitle codecs + if not isinstance(data, bytes): + raise ValueError(f"Subtitle data must be parsed as bytes data, not {data.__class__.__name__}") + try: + if codec.startswith("stpp"): + captions = defaultdict(list) + for segment in ( + TextTrack.parse(box.data, "ttml") + for box in MP4.parse_stream(BytesIO(data)) if box.type == b"mdat" + ): + lang = segment.get_languages()[0] + for caption in segment.get_captions(lang): + prev_caption = captions and captions[lang][-1] + + if prev_caption and (prev_caption.start, prev_caption.end) == (caption.start, caption.end): + # Merge cues with equal start and end timestamps. + # + # pycaption normally does this itself, but we need to do it manually here + # for the next merge to work properly. + prev_caption.nodes += [pycaption.CaptionNode.create_break(), *caption.nodes] + elif prev_caption and caption.start <= prev_caption.end: + # If the previous cue's end timestamp is less or equal to the current cue's start timestamp, + # just extend the previous one's end timestamp to the current one's end timestamp. + # This is to get rid of duplicates, as STPP may duplicate cues at segment boundaries. + prev_caption.end = caption.end + else: + captions[lang].append(caption) + + return pycaption.CaptionSet(captions) + if codec in ["dfxp", "ttml", "tt"]: + text = data.decode("utf-8").replace("tt:", "") + return pycaption.DFXPReader().read(text) + if codec in ["vtt", "webvtt", "wvtt"] or codec.startswith("webvtt"): + text = data.decode("utf-8").replace("\r", "").replace("\n\n\n", "\n \n\n").replace("\n\n<", "\n<") + text = re.sub(r"‏", "\u202B", text) + return pycaption.WebVTTReader().read(text) + if codec.lower() == "ass": + try: + subs = pysubs2.load(data.decode('utf-8')) + captions = {} + for line in subs: + if line.start is not None and line.end is not None and line.text: + caption = pycaption.Caption( + start=line.start.to_time().total_seconds(), + end=line.end.to_time().total_seconds(), + nodes=[pycaption.CaptionNode.create_text(line.text)] + ) + if line.style: + caption.style = line.style.name # Optionally include the style name + if line.actor: + caption.actor = line.actor # Optionally include the actor name + if line.effect: + caption.effect = line.effect # Optionally include the effect + captions[line.style.name] = captions.get(line.style.name, []) + [caption] + + return pycaption.CaptionSet(captions) + except Exception as e: + raise ValueError(f"Failed to parse .ass subtitle: {str(e)}") + except pycaption.exceptions.CaptionReadSyntaxError: + raise SyntaxError(f"A syntax error has occurred when reading the \"{codec}\" subtitle") + except pycaption.exceptions.CaptionReadNoCaptions: + return pycaption.CaptionSet({"en": []}) + + raise ValueError(f"Unknown subtitle format: {codec!r}") + + @staticmethod + def convert_to_srt(data, codec): + if isinstance(data, bytes): + data = data.decode() + + from vinetrimmer.utils.ttml2ssa import Ttml2Ssa + ttml = Ttml2Ssa() + if codec in ["dfxp", "ttml", "tt"] or codec.startswith("ttml"): + ttml.parse_ttml_from_string(data) + else: # codec in ["vtt", "webvtt", "wvtt"] or codec.startswith("webvtt"): + ttml.parse_vtt_from_string(data) + + for entry in ttml.entries: + text = str(entry['text']) + line_split = text.splitlines() + if len(line_split) == 3: + text = f"{line_split[0]}\n" \ + f"{line_split[1]} {line_split[2]}" + if len(line_split) == 4: + text = f"{line_split[0]} {line_split[1]}\n" \ + f"{line_split[2]} {line_split[3]}" + entry['text'] = text + + # return pycaption.SRTWriter().write(TextTrack.parse(data, codec)) + return ttml.generate_srt() + + @staticmethod + def convert_to_srt2(data, codec): + return pycaption.SRTWriter().write(TextTrack.parse(data, codec)) + + + def download(self, out, name=None, headers=None, proxy=None): + save_path = super().download(out, name, headers, proxy) + if self.codec.lower() == "ass": + return save_path # Return the .ass file as-is without any conversion + elif self.source == "iP": + with open(save_path, "r+b") as fd: + data = fd.read() + fd.seek(0) + fd.truncate() + fd.write(self.convert_to_srt2(data, self.codec).encode("utf-8")) + self.codec = "srt" + return save_path + elif self.codec.lower() != "srt": + with open(save_path, "r+b") as fd: + data = fd.read() + fd.seek(0) + fd.truncate() + fd.write(self.convert_to_srt(data, self.codec).encode("utf-8")) + self.codec = "srt" + return save_path + + def __str__(self): + codec = next((CODEC_MAP[x] for x in CODEC_MAP if (self.codec or "").startswith(x)), self.codec) + return " | ".join([x for x in [ + "├─ SUB", + f"[{codec}]", + f"{self.language}", + " ".join([self.get_track_name() or "", "[Original]" if self.is_original_lang else ""]).strip() + ] if x]) + + +class MenuTrack: + line_1 = re.compile(r"^CHAPTER(?P<number>\d+)=(?P<timecode>[\d\\.]+)$") + line_2 = re.compile(r"^CHAPTER(?P<number>\d+)NAME=(?P<title>[\d\\.]+)$") + + def __init__(self, number, title, timecode): + self.id = f"chapter-{number}" + self.number = number + self.title = title + if "." not in timecode: + timecode += ".000" + self.timecode = timecode + + def __bool__(self): + return bool( + self.number and self.number >= 0 and + self.title and + self.timecode + ) + + def __repr__(self): + """ + OGM-based Simple Chapter Format intended for use with MKVToolNix. + + This format is not officially part of the Matroska spec. This was a format + designed for OGM tools that MKVToolNix has since re-used. More Information: + https://mkvtoolnix.download/doc/mkvmerge.html#mkvmerge.chapters.simple + """ + return "CHAPTER{num}={time}\nCHAPTER{num}NAME={name}".format( + num=f"{self.number:02}", + time=self.timecode, + name=self.title + ) + + def __str__(self): + return " | ".join([ + "├─ CHP", + f"[{self.number:02}]", + self.timecode, + self.title + ]) + + @classmethod + def loads(cls, data): + """Load chapter data from a string.""" + lines = [x.strip() for x in data.strip().splitlines(keepends=False)] + if len(lines) > 2: + return MenuTrack.loads("\n".join(lines)) + one, two = lines + + one_m = cls.line_1.match(one) + two_m = cls.line_2.match(two) + if not one_m or not two_m: + raise SyntaxError(f"An unexpected syntax error near:\n{one}\n{two}") + + one_str, timecode = one_m.groups() + two_str, title = two_m.groups() + one_num, two_num = int(one_str.lstrip("0")), int(two_str.lstrip("0")) + + if one_num != two_num: + raise SyntaxError(f"The chapter numbers ({one_num},{two_num}) does not match.") + if not timecode: + raise SyntaxError("The timecode is missing.") + if not title: + raise SyntaxError("The title is missing.") + + return cls(number=one_num, title=title, timecode=timecode) + + @classmethod + def load(cls, path): + """Load chapter data from a file.""" + with open(path, encoding="utf-8") as fd: + return cls.loads(fd.read()) + + def dumps(self): + """Return chapter data as a string.""" + return repr(self) + + def dump(self, path): + """Write chapter data to a file.""" + with open(path, "w", encoding="utf-8") as fd: + return fd.write(self.dumps()) + + @staticmethod + def format_duration(seconds): + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) + return f"{hours:02.0f}:{minutes:02.0f}:{seconds:06.3f}" + + +class Tracks: + """ + Tracks. + Stores video, audio, and subtitle tracks. It also stores chapter/menu entries. + It provides convenience functions for listing, sorting, and selecting tracks. + """ + + TRACK_ORDER_MAP = { + VideoTrack: 0, + AudioTrack: 1, + TextTrack: 2, + MenuTrack: 3 + } + + def __init__(self, *args): + self.videos = [] + self.audios = [] + self.subtitles = [] + self.chapters = [] + + if args: + self.add(as_list(*args)) + + def __iter__(self): + return iter(as_list(self.videos, self.audios, self.subtitles)) + + def __repr__(self): + return "{name}({items})".format( + name=self.__class__.__name__, + items=", ".join([f"{k}={repr(v)}" for k, v in self.__dict__.items()]) + ) + + def __str__(self): + rep = "" + last_track_type = None + tracks = [*list(self), *self.chapters] + for track in sorted(tracks, key=lambda t: self.TRACK_ORDER_MAP[type(t)]): + if type(track) != last_track_type: + last_track_type = type(track) + count = sum(type(x) is type(track) for x in tracks) + rep += "{count} {type} Track{plural}{colon}\n".format( + count=count, + type=track.__class__.__name__.replace("Track", ""), + plural="s" if count != 1 else "", + colon=":" if count > 0 else "" + ) + rep += f"{track}\n" + + return rep.rstrip() + + def exists(self, by_id=None, by_url=None): + """Check if a track already exists by various methods.""" + if by_id: # recommended + return any(x.id == by_id for x in self) + if by_url: + return any(x.url == by_url for x in self) + return False + + def add(self, tracks, warn_only=True): + """Add a provided track to its appropriate array and ensuring it's not a duplicate.""" + if isinstance(tracks, Tracks): + tracks = [*list(tracks), *tracks.chapters] + + duplicates = 0 + for track in as_list(tracks): + if self.exists(by_id=track.id): + if not warn_only: + raise ValueError( + "One or more of the provided Tracks is a duplicate. " + "Track IDs must be unique but accurate using static values. The " + "value should stay the same no matter when you request the same " + "content. Use a value that has relation to the track content " + "itself and is static or permanent and not random/RNG data that " + "wont change each refresh or conflict in edge cases." + ) + duplicates += 1 + continue + + if isinstance(track, VideoTrack): + self.videos.append(track) + elif isinstance(track, AudioTrack): + self.audios.append(track) + elif isinstance(track, TextTrack): + self.subtitles.append(track) + elif isinstance(track, MenuTrack): + self.chapters.append(track) + else: + raise ValueError("Track type was not set or is invalid.") + + log = logging.getLogger("Tracks") + + if duplicates: + log.warning(f" - Found and skipped {duplicates} duplicate tracks") + + def print(self, level=logging.INFO): + """Print the __str__ to log at a specified level.""" + log = logging.getLogger("Tracks") + for line in str(self).splitlines(keepends=False): + log.log(level, line) + + def sort_videos(self, by_language=None): + """Sort video tracks by bitrate, and optionally language.""" + if not self.videos: + return + # bitrate + self.videos = sorted(self.videos, key=lambda x: float(x.bitrate or 0.0), reverse=True) + # language + for language in reversed(by_language or []): + if str(language) == "all": + language = next((x.language for x in self.videos if x.is_original_lang), "") + if not language: + continue + self.videos = sorted( + self.videos, + key=lambda x: "" if is_close_match(language, [x.language]) else str(x.language) + ) + + def sort_audios(self, by_language=None): + """Sort audio tracks by bitrate, descriptive, and optionally language.""" + if not self.audios: + return + # bitrate + self.audios = sorted(self.audios, key=lambda x: float(x.bitrate or 0.0), reverse=True) + # channels + self.audios = sorted(self.audios, key=lambda x: float(x.channels.replace("ch", "").replace("/JOC", "") if x.channels is not None else 0.0), reverse=True) + # descriptive + self.audios = sorted(self.audios, key=lambda x: str(x.language) if x.descriptive else "") + # language + for language in reversed(by_language or []): + if str(language) == "all": + language = next((x.language for x in self.audios if x.is_original_lang), "") + if not language: + continue + self.audios = sorted( + self.audios, + key=lambda x: "" if is_close_match(language, [x.language]) else str(x.language) + ) + + def sort_subtitles(self, by_language=None): + """Sort subtitle tracks by sdh, cc, forced, and optionally language.""" + if not self.subtitles: + return + # sdh/cc + self.subtitles = sorted( + self.subtitles, key=lambda x: str(x.language) + ("-cc" if x.cc else "") + ("-sdh" if x.sdh else "") + ) + # forced + self.subtitles = sorted(self.subtitles, key=lambda x: not x.forced) + # language + for language in reversed(by_language or []): + if str(language) == "all": + language = next((x.language for x in self.subtitles if x.is_original_lang), "") + if not language: + continue + self.subtitles = sorted( + self.subtitles, + key=lambda x: "" if is_close_match(language, [x.language]) else str(x.language) + ) + + def sort_chapters(self): + """Sort chapter tracks by chapter number.""" + if not self.chapters: + return + # number + self.chapters = sorted(self.chapters, key=lambda x: x.number) + + @staticmethod + def select_by_language(languages, tracks, one_per_lang=True): + """ + Filter a track list by language. + + If one_per_lang is True, only the first matched track will be returned for + each language. It presumes the first match is what is wanted. + + This means if you intend for it to return the best track per language, + then ensure the iterable is sorted in ascending order (first = best, last = worst). + """ + if "orig" in languages: + nonoriglangs = languages.remove("orig") + else: nonoriglangs = languages + if not tracks: + return + if "all" not in languages: + track_type = tracks[0].__class__.__name__.lower().replace("track", "").replace("text", "subtitle") + orig_tracks = tracks + tracks = [ + x for x in tracks + if is_close_match(x.language, languages) or (x.is_original_lang and "orig" in languages and not any(lang in x.language for lang in nonoriglangs)) + ] + if not tracks: + if languages == ["orig"]: + all_languages = set(x.language for x in orig_tracks) + if len(all_languages) == 1: + # If there's only one language available, take it + languages = list(all_languages) + tracks = [ + x for x in orig_tracks + if is_close_match(x.language, languages) or (x.is_original_lang and "orig" in languages) + ] + else: + raise ValueError( + f"There's no original {track_type} track. Please specify a language manually with " + f"{'-al' if track_type == 'audio' else '-sl'}." + ) + else: + raise ValueError( + f"There's no {track_type} tracks that match the language{'' if len(languages) == 1 else 's'}: " + f"{', '.join(languages)}" + ) + if one_per_lang: + if "all" in languages: + languages = list(sorted(set(x.language for x in tracks), key=str)) + for language in languages: + if language == "orig": + yield next(x for x in tracks if x.is_original_lang) + else: + match = get_closest_match(language, [x.language for x in tracks]) + if match: + yield next(x for x in tracks if x.language == match) + else: + for track in tracks: + yield track + + def select_videos (self, by_language=None, by_vbitrate=None, by_quality=None, by_range=None, + one_only: bool = True, by_codec=None, + ) -> None: + """Filter video tracks by language and other criteria.""" + if by_quality: + # Note: Do not merge these list comprehensions. They must be done separately so the results + # from the 16:9 canvas check is only used if there's no exact height resolution match. + videos_quality = [x for x in self.videos if x.height == by_quality] + if not videos_quality: + videos_quality = [x for x in self.videos if int(x.width * (9 / 16)) == by_quality] + if not videos_quality: + # AMZN weird resolution (1248x520) + videos_quality = [x for x in self.videos if x.width == 1248 and by_quality == 720] + if not videos_quality: + videos_quality = [x for x in self.videos if (x.width, x.height) < (1024, 576) and by_quality == "SD"] + if not videos_quality: + videos_quality = [ + x for x in self.videos if isinstance(x.extra, dict) and x.extra.get("quality") == by_quality + ] + if not videos_quality: + raise ValueError(f"There's no {by_quality}p resolution video track. Aborting.") + self.videos = videos_quality + if by_vbitrate: + self.videos = [x for x in self.videos if int(x.bitrate) <= int(by_vbitrate * 1001)] + if by_codec: + codec_videos = list(filter(lambda x: any(y for y in self.VIDEO_CODEC_MAP[by_codec] if y in x.codec), self.videos)) + if not codec_videos and not should_fallback: + raise ValueError(f"There's no {by_codec} video tracks. Aborting.") + else: + self.videos = (codec_videos if codec_videos else self.videos) + if by_range: + self.videos = [x for x in self.videos if { + "HDR10": x.hdr10, + "HLG": x.hlg, + "DV": x.dv, + "SDR": not x.hdr10 and not x.dv + }.get((by_range or "").upper(), True)] + if not self.videos: + raise ValueError(f"There's no {by_range} video track. Aborting.") + if by_language: + self.videos = list(self.select_by_language(by_language, self.videos)) + if one_only and self.videos: + self.videos = [self.videos[0]] + + def select_audios( + self, + with_descriptive: bool = True, + with_atmos: bool = False, + by_language=None, + by_bitrate=None, + by_channels=None, + by_codec=None, + should_fallback: bool = False + ) -> None: + """Filter audio tracks by language and other criteria.""" + if not with_descriptive: + self.audios = [x for x in self.audios if not x.descriptive] + if by_codec: + codec_audio = list(filter(lambda x: any(y for y in self.AUDIO_CODEC_MAP[by_codec] if y in x.codec), self.audio)) + if not codec_audio and not should_fallback: + raise ValueError(f"There's no {by_codec} audio tracks. Aborting.") + else: + self.audios = (codec_audio if codec_audio else self.audios) + if by_channels: + channels_audio = list(filter(lambda x: x.channels == by_channels, self.audios)) + if not channels_audio and not should_fallback: + raise ValueError(f"There's no {by_channels} {by_codec} audio tracks. Aborting.") + else: + self.audios = (channels_audio if channels_audio else self.audios) + if with_atmos: + atmos_audio = list(filter(lambda x: x.atmos, self.audios)) + self.audios = (atmos_audio if atmos_audio else self.audios) # Fallback if no atmos + if by_bitrate: + self.audios = [x for x in self.audios if int(x.bitrate) <= int(by_bitrate * 1000)] + if by_language: + # Todo: Optimize select_by_language + self.audios = list(self.select_by_language(by_language, self.audios, one_per_lang=True)) + \ + list(self.select_by_language(by_language, [x for x in self.audios if x.descriptive], one_per_lang=True)) + + def select_subtitles(self, by_language=None, with_cc=True, with_sdh=True, with_forced=True): + """Filter subtitle tracks by language and other criteria.""" + if not with_cc: + self.subtitles = [x for x in self.subtitles if not x.cc] + if not with_sdh: + self.subtitles = [x for x in self.subtitles if not x.sdh] + if isinstance(with_forced, list): + self.subtitles = [ + x for x in self.subtitles + if not x.forced or is_close_match(x.language, with_forced) + ] + if not with_forced: + self.subtitles = [x for x in self.subtitles if not x.forced] + if by_language: + self.subtitles = list(self.select_by_language(by_language, self.subtitles, one_per_lang=True)) + + def export_chapters(self, to_file=None): + """Export all chapters in order to a string or file.""" + self.sort_chapters() + data = "\n".join(map(repr, self.chapters)) + if to_file: + os.makedirs(os.path.dirname(to_file), exist_ok=True) + with open(to_file, "w", encoding="utf-8") as fd: + fd.write(data) + return data + + # converter code + + @staticmethod + def from_m3u8(*args, **kwargs): + from vinetrimmer import parsers + return parsers.m3u8.parse(*args, **kwargs) + + @staticmethod + def from_mpd(*args, **kwargs): + from vinetrimmer import parsers + return parsers.mpd.parse(**kwargs) + + @staticmethod + def from_ism(*args, **kwargs): + from vinetrimmer import parsers + return parsers.ism.parse(**kwargs) + + def mux(self, prefix): + """ + Takes the Video, Audio and Subtitle Tracks, and muxes them into an MKV file. + It will attempt to detect Forced/Default tracks, and will try to parse the language codes of the Tracks + """ + if self.videos: + muxed_location = self.videos[0].locate() + if not muxed_location: + raise ValueError("The provided video track has not yet been downloaded.") + muxed_location = os.path.splitext(muxed_location)[0] + ".muxed.mkv" + elif self.audios: + muxed_location = self.audios[0].locate() + if not muxed_location: + raise ValueError("A provided audio track has not yet been downloaded.") + muxed_location = os.path.splitext(muxed_location)[0] + ".muxed.mka" + elif self.subtitles: + muxed_location = self.subtitles[0].locate() + if not muxed_location: + raise ValueError("A provided subtitle track has not yet been downloaded.") + muxed_location = os.path.splitext(muxed_location)[0] + ".muxed.mks" + elif self.chapters: + muxed_location = config.filenames.chapters.format(filename=prefix) + if not muxed_location: + raise ValueError("A provided chapter has not yet been downloaded.") + muxed_location = os.path.splitext(muxed_location)[0] + ".muxed.mks" + else: + raise ValueError("No tracks provided, at least one track must be provided.") + + muxed_location = os.path.join(config.directories.downloads, os.path.basename(muxed_location)) + + cl = [ + "mkvmerge", + "--output", + muxed_location + ] + + for i, vt in enumerate(self.videos): + location = vt.locate() + if not location: + raise ValueError("Somehow a Video Track was not downloaded before muxing...") + cl.extend([ + "--language", "0:und", + "--disable-language-ietf", + "--default-track", f"0:{i == 0}", + "--compression", "0:none", # disable extra compression + "(", location, ")" + ]) + for i, at in enumerate(self.audios): + location = at.locate() + if not location: + raise ValueError("Somehow an Audio Track was not downloaded before muxing...") + cl.extend([ + "--track-name", f"0:{at.get_track_name() or ''}", + "--language", "0:{}".format(LANGUAGE_MUX_MAP.get( + str(at.language), at.language.to_alpha3() + )), + "--disable-language-ietf", + "--default-track", f"0:{i == 0}", + "--compression", "0:none", # disable extra compression + "(", location, ")" + ]) + for st in self.subtitles: + location = st.locate() + if not location: + raise ValueError("Somehow a Text Track was not downloaded before muxing...") + default = bool(self.audios and is_close_match(st.language, [self.audios[0].language]) and st.forced) + cl.extend([ + "--track-name", f"0:{st.get_track_name() or ''}", + "--language", "0:{}".format(LANGUAGE_MUX_MAP.get( + str(st.language), st.language.to_alpha3() + )), + "--disable-language-ietf", + "--sub-charset", "0:UTF-8", + "--forced-track", f"0:{st.forced}", + "--default-track", f"0:{default}", + "--compression", "0:none", # disable extra compression (probably zlib) + "(", location, ")" + ]) + if self.chapters: + location = config.filenames.chapters.format(filename=prefix) + self.export_chapters(location) + cl.extend(["--chapters", location]) + + # let potential failures go to caller, caller should handle + p = subprocess.Popen(cl, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + in_progress = False + for line in TextIOWrapper(p.stdout, encoding="utf-8"): + if re.search(r"Using the (?:demultiplexer|output module) for the format", line): + continue + if line.startswith("Progress:"): + in_progress = True + sys.stdout.write("\r" + line.rstrip('\n')) + else: + if in_progress: + in_progress = False + sys.stdout.write("\n") + sys.stdout.write(line) + returncode = p.wait() + return muxed_location, returncode diff --git a/vinetrimmer/objects/vaults.py b/vinetrimmer/objects/vaults.py new file mode 100644 index 0000000..cfe32c4 --- /dev/null +++ b/vinetrimmer/objects/vaults.py @@ -0,0 +1,222 @@ +import os +import sqlite3 +from enum import Enum + +import pymysql + +from vinetrimmer.utils.AtomicSQL import AtomicSQL + + +class InsertResult(Enum): + FAILURE = 0 + SUCCESS = 1 + ALREADY_EXISTS = 2 + + +class Vault: + """ + Key Vault. + This defines various details about the vault, including its Connection object. + """ + + def __init__(self, type_, name, ticket=None, path=None, username=None, password=None, database=None, + host=None, port=3306): + from vinetrimmer.config import directories + + try: + self.type = self.Types[type_.upper()] + except KeyError: + raise ValueError(f"Invalid vault type [{type_}]") + self.name = name + self.con = None + if self.type == Vault.Types.LOCAL: + if not path: + raise ValueError("Local vault has no path specified") + self.con = sqlite3.connect(os.path.expanduser(path).format(data_dir=directories.data)) + elif self.type == Vault.Types.REMOTE: + self.con = pymysql.connect( + user=username, + password=password or "", + db=database, + host=host, + port=port, + cursorclass=pymysql.cursors.DictCursor # TODO: Needed? Maybe use it on sqlite3 too? + ) + else: + raise ValueError(f"Invalid vault type [{self.type.name}]") + self.ph = {self.Types.LOCAL: "?", self.Types.REMOTE: "%s"}[self.type] + self.ticket = ticket + + self.perms = self.get_permissions() + if not self.has_permission("SELECT"): + raise ValueError(f"Cannot use vault. Vault {self.name} has no SELECT permission.") + + def __str__(self): + return f"{self.name} ({self.type.name})" + + def get_permissions(self): + if self.type == self.Types.LOCAL: + return [tuple([["*"], tuple(["*", "*"])])] + + with self.con.cursor() as c: + c.execute("SHOW GRANTS") + grants = c.fetchall() + grants = [next(iter(x.values())) for x in grants] + grants = [tuple(x[6:].split(" TO ")[0].split(" ON ")) for x in list(grants)] + grants = [( + list(map(str.strip, perms.replace("ALL PRIVILEGES", "*").split(","))), + location.replace("`", "").split(".") + ) for perms, location in grants] + + return grants + + def has_permission(self, operation, database=None, table=None): + grants = [x for x in self.perms if x[0] == ["*"] or operation.upper() in x[0]] + if grants and database: + grants = [x for x in grants if x[1][0] in (database, "*")] + if grants and table: + grants = [x for x in grants if x[1][1] in (table, "*")] + return bool(grants) + + class Types(Enum): + LOCAL = 1 + REMOTE = 2 + + +class Vaults: + """ + Key Vaults. + Keeps hold of Vault objects, with convenience functions for + using multiple vaults in one actions, e.g. searching vaults + for a key based on kid. + This object uses AtomicSQL for accessing the vault connections + instead of directly. This is to provide thread safety but isn't + strictly necessary. + """ + + def __init__(self, vaults, service): + self.adb = AtomicSQL() + self.vaults = sorted(vaults, key=lambda v: 0 if v.type == Vault.Types.LOCAL else 1) + self.service = service.lower() + for vault in self.vaults: + vault.ticket = self.adb.load(vault.con) + self.create_table(vault, self.service, commit=True) + + def __iter__(self): + return iter(self.vaults) + + def get(self, kid, title): + for vault in self.vaults: + # Note on why it matches by KID instead of PSSH: + # Matching cache by pssh is not efficient. The PSSH can be made differently by all different + # clients for all different reasons, e.g. only having the init data, but the cached PSSH is + # a manually crafted PSSH, which may not match other clients manually crafted PSSH, and such. + # So it searches by KID instead for this reason, as the KID has no possibility of being different + # client to client other than capitalization. There is an unknown with KID matching, It's unknown + # for *sure* if the KIDs ever conflict or not with another bitrate/stream/title. I haven't seen + # this happen ever and neither has anyone I have asked. + if not self.table_exists(vault, self.service): + continue # this service has no service table, so no keys, just skip + if not vault.ticket: + raise ValueError(f"Vault {vault.name} does not have a valid ticket available.") + c = self.adb.safe_execute( + vault.ticket, + lambda db, cursor: cursor.execute( + "SELECT `id`, `key_`, `title` FROM `{1}` WHERE `kid`={0}".format(vault.ph, self.service), + [kid] + ) + ).fetchone() + if c: + if isinstance(c, dict): + c = list(c.values()) + if not c[2] and vault.has_permission("UPDATE", table=self.service): + self.adb.safe_execute( + vault.ticket, + lambda db, cursor: cursor.execute( + "UPDATE `{1}` SET `title`={0} WHERE `id`={0}".format(vault.ph, self.service), + [title, c[0]] + ) + ) + self.commit(vault) + return c[1], vault + return None, None + + def table_exists(self, vault, table): + if not vault.ticket: + raise ValueError(f"Vault {vault.name} does not have a valid ticket available.") + if vault.type == Vault.Types.LOCAL: + return self.adb.safe_execute( + vault.ticket, + lambda db, cursor: cursor.execute( + f"SELECT count(name) FROM sqlite_master WHERE type='table' AND name={vault.ph}", + [table] + ) + ).fetchone()[0] == 1 + return list(self.adb.safe_execute( + vault.ticket, + lambda db, cursor: cursor.execute( + f"SELECT count(TABLE_NAME) FROM information_schema.TABLES WHERE TABLE_NAME={vault.ph}", + [table] + ) + ).fetchone().values())[0] == 1 + + def create_table(self, vault, table, commit=False): + if self.table_exists(vault, table): + return + if not vault.ticket: + raise ValueError(f"Vault {vault.name} does not have a valid ticket available.") + if vault.has_permission("CREATE"): + print(f"Creating `{table}` table in {vault} key vault...") + self.adb.safe_execute( + vault.ticket, + lambda db, cursor: cursor.execute( + "CREATE TABLE IF NOT EXISTS {} (".format(table) + ( + """ + "id" INTEGER NOT NULL UNIQUE, + "kid" TEXT NOT NULL COLLATE NOCASE, + "key_" TEXT NOT NULL COLLATE NOCASE, + "title" TEXT, + PRIMARY KEY("id" AUTOINCREMENT), + UNIQUE("kid", "key_") + """ if vault.type == Vault.Types.LOCAL else + """ + id INTEGER AUTO_INCREMENT PRIMARY KEY, + kid VARCHAR(255) NOT NULL, + key_ VARCHAR(255) NOT NULL, + title TEXT, + UNIQUE(kid, key_) + """ + ) + ");" + ) + ) + if commit: + self.commit(vault) + + def insert_key(self, vault, table, kid, key, title, commit=False): + if not self.table_exists(vault, table): + return InsertResult.FAILURE + if not vault.ticket: + raise ValueError(f"Vault {vault.name} does not have a valid ticket available.") + if not vault.has_permission("INSERT", table=table): + raise ValueError(f"Cannot insert key into Vault. Vault {vault.name} has no INSERT permission.") + if self.adb.safe_execute( + vault.ticket, + lambda db, cursor: cursor.execute( + "SELECT `id` FROM `{1}` WHERE `kid`={0} AND `key_`={0}".format(vault.ph, self.service), + [kid, key] + ) + ).fetchone(): + return InsertResult.ALREADY_EXISTS + self.adb.safe_execute( + vault.ticket, + lambda db, cursor: cursor.execute( + "INSERT INTO `{1}` (kid, key_, title) VALUES ({0}, {0}, {0})".format(vault.ph, table), + (kid, key, title) + ) + ) + if commit: + self.commit(vault) + return InsertResult.SUCCESS + + def commit(self, vault): + self.adb.commit(vault.ticket) diff --git a/vinetrimmer/parsers/__init__.py b/vinetrimmer/parsers/__init__.py new file mode 100644 index 0000000..ffb85bf --- /dev/null +++ b/vinetrimmer/parsers/__init__.py @@ -0,0 +1,3 @@ +from vinetrimmer.parsers import m3u8, mpd, ism + +__all__ = ["m3u8", "mpd", "ism"] diff --git a/vinetrimmer/parsers/ism.py b/vinetrimmer/parsers/ism.py new file mode 100644 index 0000000..4f9690c --- /dev/null +++ b/vinetrimmer/parsers/ism.py @@ -0,0 +1,242 @@ +import xmltodict +import asyncio +import base64 +import json +import math +import os +import re +import urllib.parse +import uuid +from copy import copy +from hashlib import md5 + +import requests +from langcodes import Language +from langcodes.tag_parser import LanguageTagError + +from vinetrimmer import config +from vinetrimmer.objects import AudioTrack, TextTrack, Track, Tracks, VideoTrack +from vinetrimmer.utils.io import aria2c +from vinetrimmer.vendor.pymp4.parser import Box + +# A stream from ISM is always in fragments +# Example fragment URL +# https://test.playready.microsoft.com/media/profficialsite/tearsofsteel_4k.ism.smoothstreaming/QualityLevels(128003)/Fragments(aac_UND_2_128=0) +# based on https://github.com/SASUKE-DUCK/pywks/blob/dba8a83a0722221bd8d3e53d624b91050b46cfde/cdm/wks.py#L722 +def parse(*, url=None, data=None, source, session=None, downloader=None): + """ + Convert an Smooth Streaming ISM (IIS Smooth Streaming Manifest) document to a Tracks object + with video, audio and subtitle track objects where available. + + :param url: URL of the ISM document. + :param data: The ISM document as a string. + :param source: Source tag for the returned tracks. + :param session: Used for any remote calls, e.g. getting the MPD document from an URL. + Can be useful for setting custom headers, proxies, etc. + :param downloader: Downloader to use. Accepted values are None (use requests to download) + and aria2c. + + Don't forget to manually handle the addition of any needed or extra information or values + like `encrypted`, `pssh`, `hdr10`, `dv`, etc. Essentially anything that is per-service + should be looked at. Some of these values like `pssh` will be attempted to be set automatically + if possible but if you definitely have the values in the service, then set them. + + Examples: + url = "https://test.playready.microsoft.com/media/profficialsite/tearsofsteel_4k.ism.smoothstreaming/manifest" # https://testweb.playready.microsoft.com/Content/Content2X + session = requests.Session(headers={"X-Example": "foo"}) + tracks = Tracks.from_ism( + url, + session=session, + source="MICROSOFT", + ) + + url = "https://test.playready.microsoft.com/media/profficialsite/tearsofsteel_4k.ism.smoothstreaming/manifest" + session = requests.Session(headers={"X-Example": "foo"}) + tracks = Tracks.from_ism(url=url, data=session.get(url).text, source="MICROSOFT") + """ + if not data: + if not url: + raise ValueError("Neither a URL nor a document was provided to Tracks.from_ism") + base_url = url.rsplit('/', 1)[0] + '/' + if downloader is None: + data = (session or requests).get(url).text + elif downloader == "aria2c": + out = os.path.join(config.directories.temp, url.split("/")[-1]) + asyncio.run(aria2c(url, out)) + + with open(out, encoding="utf-8") as fd: + data = fd.read() + + try: + os.unlink(out) + except FileNotFoundError: + pass + else: + raise ValueError(f"Unsupported downloader: {downloader}") + + ism = xmltodict.parse(data) + if not ism["SmoothStreamingMedia"]: + raise ValueError("Non-ISM document provided to Tracks.from_ism") + + encrypted = \ + ( True if ism['SmoothStreamingMedia']['Protection']['ProtectionHeader']['@SystemID'] + and + ism['SmoothStreamingMedia']['Protection']['ProtectionHeader']['@SystemID'].replace("-", "").upper() + in ["9A04F07998404286AB92E65BE0885F95", 'EDEF8BA979D64ACEA3C827DCD51D21ED'] + else False + ) + pssh = ism['SmoothStreamingMedia']['Protection']['ProtectionHeader']['#text'] + pr_pssh_dec = base64.b64decode(pssh).decode('utf16') + pr_pssh_dec = pr_pssh_dec[pr_pssh_dec.index('<'):] + pr_pssh_xml = xmltodict.parse(pr_pssh_dec) + kid_hex = base64.b64decode(pr_pssh_xml['WRMHEADER']['DATA']['KID']).hex() + kid = uuid.UUID(kid_hex).bytes_le + + stream_indices = ism['SmoothStreamingMedia']['StreamIndex'] + + assert int(ism['SmoothStreamingMedia'].get('@Duration')) + assert int(ism['SmoothStreamingMedia'].get('@TimeScale')) + + # Seconds + duration = int(ism['SmoothStreamingMedia'].get('@Duration')) / int(ism['SmoothStreamingMedia'].get('@TimeScale')) + + # List to store information for each stream + tracks = [] + + # Iterate over each StreamIndex (as it might be a list) + for stream_info in stream_indices if isinstance(stream_indices, list) else [stream_indices]: + + # For some reason Chunks will be roughly equal to int of half of duration in seconds + #chunks_calc = int(duration / 2) + #chunks = stream_info['@Chunks'] + #if chunks != chunks_calc: + # log.warn(f"{chunks} number of fragments is not equal to calculated number of fragments {chunks_calc}") + + + type_info = stream_info['@Type'] + + + # Handle the case where there can be multiple QualityLevel elements + quality_levels = stream_info.get('QualityLevel', []) + if not isinstance(quality_levels, list): + quality_levels = [quality_levels] + + for quality_level in quality_levels: + fourCC = quality_level.get('@FourCC', 'N/A') + bitrate = int ( quality_level.get('@Bitrate', 'N/A') ) # Bytes per second + + global max_width + global max_height + # Additional attributes for video streams + if type_info == 'video': + max_width = quality_level.get('@MaxWidth', 'N/A') + max_height = quality_level.get('@MaxHeight', 'N/A') + resolution = f"{max_width}x{max_height}" + else: + resolution = 'N/A' + + privateData = quality_level.get('@CodecPrivateData', 'N/A').strip() + if privateData == "N/A" or privateData == "": + privateData = None + #privateData = GenCodecPrivateDataForAAC() + + if fourCC in ["H264", "X264", "DAVC", "AVC1"]: + try: + result = re.compile(r"00000001\d7([0-9a-fA-F]{6})").match(privateData)[1] + codec = f"avc1.{result}" + except: + codec = "avc1.4D401E" + elif fourCC[:3] == "AAC": # ["AAC", "AACL", "AACH", "AACP"] + mpProfile = 2 + if fourCC == "AACH": + mpProfile = 5 # High Efficiency AAC Profile + elif privateData != "N/A" or privateData != "": + mpProfile = (int(privateData[:2], 16) & 0xF8) >> 3 + if mpProfile == 0: mpProfile = 2 # Return default audio codec + codec = f"mp4a.40.{mpProfile}" + else: + codec = fourCC + + # Additional attributes for audio streams + lang = stream_info.get('@Language', 'N/A').strip() + + try: + lang = Language.get(lang.split("-")[0]) + except: + lang = Language.get("und") + + audio_id = stream_info.get('@AudioTrackId', 'N/A') + + track_id = "{codec}-{lang}-{bitrate}-{extra}".format( + codec=codec, + lang=lang, + bitrate=bitrate or 0, # subs may not state bandwidth + extra=(audio_id or "") + (quality_level.get("@Index") or "") + privateData, + ) + track_id = md5(track_id.encode()).hexdigest() + + url_template: str = stream_info.get("@Url").replace("{bitrate}", f"{bitrate}").replace("{start time}", "0") + init_url = url.replace("manifest", url_template) + #init = (session or requests).get(init_url).text + # if pssh: + # pssh = base64.b64decode(pssh) + # # noinspection PyBroadException + # try: + # pssh = Box.parse(pssh) + + # except Exception: + # pssh = Box.parse(Box.build(dict( + # type=b"pssh", + # version=0, # can only assume version & flag are 0 + # flags=0, + # system_ID=Cdm.uuid, + # init_data=pssh + # ))) + if type_info == 'video': + tracks.append(VideoTrack( + id_=track_id, + source=source, + url=url, + # metadata + codec=(codec or "").split(".")[0], + language=lang, + bitrate=bitrate, + width=max_width, + height=max_height, + fps=None, + hdr10=codec and codec[0:4] in ("hvc1", "hev1"), # and codec[5] == 2, # hevc.2XXXXX or hvc1.2XXXXX Needs the hevc full codec script translated from Nm3u8 + hlg=False, + dv=codec and codec[0:4] in ("dvhe", "dvh1"), + # switches/options + descriptor=Track.Descriptor.ISM, + # decryption + needs_repack=True, # Necessary + encrypted=encrypted, + pssh=pssh, + kid=kid, + # extra + extra=(list(quality_level), list(stream_info),) # Either set size as a attribute of VideoTrack or append to extra here. + )) + elif type_info == 'audio': + atmos = ( str( quality_level.get('@HasAtmos', 'N/A') ).lower() == "true" ) or ( "ATM" in stream_info.get('@Name', 'N/A') ) + tracks.append(AudioTrack( + id_=track_id, + source=source, + url=url, + # metadata + codec=(codec or "").split(".")[0], + language=lang, + bitrate=bitrate, + channels=quality_level.get('@Channels', 'N/A'), + atmos=atmos, + # switches/options + descriptor=Track.Descriptor.ISM, + # decryption + needs_repack=False, # Necessary + encrypted=encrypted, + pssh=pssh, + kid=kid, + # extra + extra=(dict(quality_level), dict(stream_info),) + )) + return tracks diff --git a/vinetrimmer/parsers/m3u8.py b/vinetrimmer/parsers/m3u8.py new file mode 100644 index 0000000..07cf8b6 --- /dev/null +++ b/vinetrimmer/parsers/m3u8.py @@ -0,0 +1,116 @@ +import base64 +import re +from hashlib import md5 + +from vinetrimmer.objects import AudioTrack, TextTrack, Track, Tracks, VideoTrack +#from vinetrimmer.utils import Cdm +from vinetrimmer.vendor.pymp4.parser import Box + + +def parse(master, source=None): + """ + Convert a Variant Playlist M3U8 document to a Tracks object with Video, Audio and + Subtitle Track objects. This is not an M3U8 parser, use https://github.com/globocom/m3u8 + to parse, and then feed the parsed M3U8 object. + + :param master: M3U8 object of the `m3u8` project: https://github.com/globocom/m3u8 + :param source: Source tag for the returned tracks. + + The resulting Track objects' URL will be to another M3U8 file, but this time to an + actual media stream and not to a variant playlist. The m3u8 downloader code will take + care of that, as the tracks downloader will be set to `M3U8`. + + Don't forget to manually handle the addition of any needed or extra information or values. + Like `encrypted`, `pssh`, `hdr10`, `dv`, e.t.c. Essentially anything that is per-service + should be looked at. Some of these values like `pssh` and `dv` will try to be set automatically + if possible but if you definitely have the values in the service, then set them. + Subtitle Codec will default to vtt as it has no codec information. + + Example: + tracks = Tracks.from_m3u8(m3u8.load(url)) + # check the m3u8 project for more info and ways to parse m3u8 documents + """ + if not master.is_variant: + raise ValueError("Tracks.from_m3u8: Expected a Variant Playlist M3U8 document...") + + # get pssh if available + # uses master.data.session_keys instead of master.keys as master.keys is ONLY EXT-X-KEYS and + # doesn't include EXT-X-SESSION-KEYS which is whats used for variant playlist M3U8. + keys = [x.uri for x in master.session_keys if x.keyformat.lower() == "com.microsoft.playready"] + pssh = keys[0].split(",")[-1] if keys else None + # if pssh: + # pssh = base64.b64decode(pssh) + # # noinspection PyBroadException + # try: + # pssh = Box.parse(pssh) + + # except Exception: + # pssh = Box.parse(Box.build(dict( + # type=b"pssh", + # version=0, # can only assume version & flag are 0 + # flags=0, + # system_ID=Cdm.uuid, + # init_data=pssh + # ))) + + return Tracks( + # VIDEO + [VideoTrack( + id_=md5(str(x).encode()).hexdigest()[0:7], # 7 chars only for filename length + source=source, + url=("" if re.match("^https?://", x.uri) else x.base_uri) + x.uri, + # metadata + codec=x.stream_info.codecs.split(",")[0].split(".")[0], # first codec may not be for the video + language=None, # playlists don't state the language, fallback must be used + bitrate=x.stream_info.average_bandwidth or x.stream_info.bandwidth, + width=x.stream_info.resolution[0], + height=x.stream_info.resolution[1], + fps=x.stream_info.frame_rate, + hdr10=(x.stream_info.codecs.split(".")[0] not in ("dvhe", "dvh1") + and (x.stream_info.video_range or "SDR").strip('"') != "SDR"), + hlg=False, # TODO: Can we get this from the manifest.xml? + dv=x.stream_info.codecs.split(".")[0] in ("dvhe", "dvh1"), + # switches/options + descriptor=Track.Descriptor.M3U, + # decryption + encrypted=bool(master.keys or master.session_keys), + pssh=pssh, + # extra + extra=x + ) for x in master.playlists], + # AUDIO + [AudioTrack( + id_=md5(str(x).encode()).hexdigest()[0:6], + source=source, + url=("" if re.match("^https?://", x.uri) else x.base_uri) + x.uri, + # metadata + codec=x.group_id.replace("audio-", "").split("-")[0].split(".")[0], + language=x.language, + bitrate=0, # TODO: M3U doesn't seem to state bitrate? + channels=x.channels, + atmos=(x.channels or "").endswith("/JOC"), + descriptive="public.accessibility.describes-video" in (x.characteristics or ""), + # switches/options + descriptor=Track.Descriptor.M3U, + # decryption + encrypted=False, # don't know for sure if encrypted + pssh=pssh, + # extra + extra=x + ) for x in master.media if x.type == "AUDIO" and x.uri], + # SUBTITLES + [TextTrack( + id_=md5(str(x).encode()).hexdigest()[0:6], + source=source, + url=("" if re.match("^https?://", x.uri) else x.base_uri) + x.uri, + # metadata + codec="vtt", # assuming VTT, codec info isn't shown + language=x.language, + forced=x.forced == "YES", + sdh="public.accessibility.describes-music-and-sound" in (x.characteristics or ""), + # switches/options + descriptor=Track.Descriptor.M3U, + # extra + extra=x + ) for x in master.media if x.type == "SUBTITLES"] + ) diff --git a/vinetrimmer/parsers/mpd.py b/vinetrimmer/parsers/mpd.py new file mode 100644 index 0000000..f00e27e --- /dev/null +++ b/vinetrimmer/parsers/mpd.py @@ -0,0 +1,467 @@ +import xmltodict +import asyncio +import base64 +import json +import math +import os +import re +import urllib.parse +import uuid +from copy import copy +from hashlib import md5 + +import requests +from langcodes import Language +from langcodes.tag_parser import LanguageTagError + +from vinetrimmer import config +from vinetrimmer.objects import AudioTrack, TextTrack, Track, Tracks, VideoTrack +from vinetrimmer.utils.io import aria2c +from vinetrimmer.utils.xml import load_xml + + +def parse(*, url=None, data=None, source, session=None, downloader=None): + """ + Convert an MPEG-DASH MPD (Media Presentation Description) document to a Tracks object + with video, audio and subtitle track objects where available. + + :param url: URL of the MPD document. + :param data: The MPD document as a string. + :param source: Source tag for the returned tracks. + :param session: Used for any remote calls, e.g. getting the MPD document from an URL. + Can be useful for setting custom headers, proxies, etc. + :param downloader: Downloader to use. Accepted values are None (use requests to download) + and aria2c. + + Don't forget to manually handle the addition of any needed or extra information or values + like `encrypted`, `pssh`, `hdr10`, `dv`, etc. Essentially anything that is per-service + should be looked at. Some of these values like `pssh` will be attempted to be set automatically + if possible but if you definitely have the values in the service, then set them. + + Examples: + url = "http://media.developer.dolby.com/DolbyVision_Atmos/profile8.1_DASH/p8.1.mpd" + session = requests.Session(headers={"X-Example": "foo"}) + tracks = Tracks.from_mpd( + url, + session=session, + source="DOLBY" + ) + + url = "http://media.developer.dolby.com/DolbyVision_Atmos/profile8.1_DASH/p8.1.mpd" + session = requests.Session(headers={"X-Example": "foo"}) + tracks = Tracks.from_mpd(url=url, data=session.get(url).text, source="DOLBY") + """ + + tracks = [] + if not data: + if not url: + raise ValueError("Neither a URL nor a document was provided to Tracks.from_mpd") + global base_url + base_url = url.rsplit('/', 1)[0] + '/' + if downloader is None: + data = (session or requests).get(url).text + elif downloader == "aria2c": + out = os.path.join(config.directories.temp, url.split("/")[-1]) + asyncio.run(aria2c(url, out)) + + with open(out, encoding="utf-8") as fd: + data = fd.read() + + try: + os.unlink(out) + except FileNotFoundError: + pass + else: + raise ValueError(f"Unsupported downloader: {downloader}") + + root = load_xml(data) + if root.tag != "MPD": + raise ValueError("Non-MPD document provided to Tracks.from_mpd") + + for period in root.findall("Period"): + if source == "HULU" and next(iter(period.xpath("SegmentType/@value")), "content") != "content": + continue + + period_base_url = period.findtext("BaseURL") or root.findtext("BaseURL") + if url and not period_base_url or not re.match("^https?://", period_base_url.lower()): + period_base_url = urllib.parse.urljoin(url, period_base_url) + period_base_url = period_base_url.replace('manifests.api.hbo.com', 'cmaf.cf.eu.hbomaxcdn.com') + + for adaptation_set in period.findall("AdaptationSet"): + if any(x.get("schemeIdUri") == "http://dashif.org/guidelines/trickmode" + for x in adaptation_set.findall("EssentialProperty") + + adaptation_set.findall("SupplementalProperty")): + # Skip trick mode streams (used for fast-forward/rewind) + continue + + for rep in adaptation_set.findall("Representation"): + # content type + try: + content_type = next(x for x in [ + rep.get("contentType"), + rep.get("mimeType"), + adaptation_set.get("contentType"), + adaptation_set.get("mimeType") + ] if bool(x)) + except StopIteration: + raise ValueError("No content type value could be found") + else: + content_type = content_type.split("/")[0] + if content_type.startswith("image"): + continue # most likely seek thumbnails + # codec + codecs = rep.get("codecs") or adaptation_set.get("codecs") + if content_type == "text": + mime = adaptation_set.get("mimeType") + if mime and not mime.endswith("/mp4"): + codecs = mime.split("/")[1] + # language + track_lang = None + for lang in [rep.get("lang"), adaptation_set.get("lang")]: + lang = (lang or "").strip() + if not lang: + continue + try: + t = Language.get(lang.split("-")[0]) + if t == Language.get("und") or not t.is_valid(): + raise LanguageTagError() + except LanguageTagError: + continue + else: + track_lang = Language.get(lang) + break + + # content protection + protections = rep.findall("ContentProtection") + adaptation_set.findall("ContentProtection") + encrypted = bool(protections) + pssh = None + kid = None + for protection in protections: + # For HMAX, the PSSH has multiple keys but the PlayReady ContentProtection tag + # contains the correct KID + kid = protection.get("default_KID") + if kid: + kid = uuid.UUID(kid).hex + else: + kid = protection.get("kid") + if kid: + kid = uuid.UUID(bytes_le=base64.b64decode(kid)).hex + if (protection.get("schemeIdUri") or "").lower() != "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95": + continue + pssh = protection.findtext("pssh") + + + rep_base_url = rep.findtext("BaseURL") + if rep_base_url and source not in ["DSCP", "DSNY"]: # TODO: Don't hardcode services + # this mpd allows us to download the entire file in one go, no segmentation necessary! + if not re.match("^https?://", rep_base_url.lower()): + rep_base_url = urllib.parse.urljoin(period_base_url, rep_base_url) + query = urllib.parse.urlparse(url).query + if query and not urllib.parse.urlparse(rep_base_url).query: + rep_base_url += "?" + query + track_url = rep_base_url + + else: + # this mpd provides no way to download the entire file in one go :( + segment_template = rep.find("SegmentTemplate") + if segment_template is None: + segment_template = adaptation_set.find("SegmentTemplate") + if segment_template is None: + raise ValueError("Couldn't find a SegmentTemplate for a Representation.") + segment_template = copy(segment_template) + + # join value with base url + for item in ("initialization", "media"): + if not segment_template.get(item): + continue + segment_template.set( + item, segment_template.get(item).replace("$RepresentationID$", rep.get("id")) + ) + query = urllib.parse.urlparse(url).query + if query and not urllib.parse.urlparse(segment_template.get(item)).query: + segment_template.set(item, segment_template.get(item) + "?" + query) + if not re.match("^https?://", segment_template.get(item).lower()): + segment_template.set(item, urllib.parse.urljoin( + period_base_url if not rep_base_url else rep_base_url, segment_template.get(item) + )) + + period_duration = period.get("duration") + if period_duration: + period_duration = Track.pt_to_sec(period_duration) + mpd_duration = root.get("mediaPresentationDuration") + if mpd_duration: + mpd_duration = Track.pt_to_sec(mpd_duration) + + track_url = [] + + def replace_fields(link, **kwargs): + for field, value in kwargs.items(): + link = link.replace(f"${field}$", str(value)) + m = re.search(fr"\${re.escape(field)}%([a-z0-9]+)\$", link, flags=re.I) + if m: + link = link.replace(m.group(), f"{value:{m.group(1)}}") + return link + + initialization = segment_template.get("initialization") + if initialization: + # header/init segment + track_url.append(replace_fields( + initialization, + Bandwidth=rep.get("bandwidth"), + RepresentationID=rep.get("id") + )) + + start_number = int(segment_template.get("startNumber") or 1) + + segment_timeline = segment_template.find("SegmentTimeline") + if segment_timeline is not None: + seg_time_list = [] + current_time = 0 + for s in segment_timeline.findall("S"): + if s.get("t"): + current_time = int(s.get("t")) + for _ in range(1 + (int(s.get("r") or 0))): + seg_time_list.append(current_time) + current_time += int(s.get("d")) + seg_num_list = list(range(start_number, len(seg_time_list) + start_number)) + track_url += [ + replace_fields( + segment_template.get("media"), + Bandwidth=rep.get("bandwidth"), + Number=n, + RepresentationID=rep.get("id"), + Time=t + ) + for t, n in zip(seg_time_list, seg_num_list) + ] + else: + period_duration = period_duration or mpd_duration + segment_duration = ( + float(segment_template.get("duration")) / float(segment_template.get("timescale") or 1) + ) + total_segments = math.ceil(period_duration / segment_duration) + track_url += [ + replace_fields( + segment_template.get("media"), + Bandwidth=rep.get("bandwidth"), + Number=s, + RepresentationID=rep.get("id"), + Time=s + ) + for s in range(start_number, start_number + total_segments) + ] + + # for some reason it's incredibly common for services to not provide + # a good and actually unique track ID, sometimes because of the lang + # dialect not being represented in the id, or the bitrate, or such. + # this combines all of them as one and hashes it to keep it small(ish). + track_id = "{codec}-{lang}-{bitrate}-{extra}".format( + codec=codecs, + lang=track_lang, + bitrate=rep.get("bandwidth") or 0, # subs may not state bandwidth + extra=(adaptation_set.get("audioTrackId") or "") + (rep.get("id") or ""), + ) + track_id = md5(track_id.encode()).hexdigest() + + if content_type == "video": + tracks.append(VideoTrack( + id_=track_id, + source=source, + url=track_url, + # metadata + codec=(codecs or "").split(".")[0], + language=track_lang, + bitrate=rep.get("bandwidth"), + width=int(rep.get("width") or 0) or adaptation_set.get("width"), + height=int(rep.get("height") or 0) or adaptation_set.get("height"), + fps=rep.get("frameRate") or adaptation_set.get("frameRate"), + hdr10=any( + x.get("schemeIdUri") == "urn:mpeg:mpegB:cicp:TransferCharacteristics" + and x.get("value") == "16" # PQ + for x in adaptation_set.findall("SupplementalProperty") + ) or any( + x.get("schemeIdUri") == "http://dashif.org/metadata/hdr" + and x.get("value") == "SMPTE2094-40" # HDR10+ + for x in adaptation_set.findall("SupplementalProperty") + ), + hlg=any( + x.get("schemeIdUri") == "urn:mpeg:mpegB:cicp:TransferCharacteristics" + and x.get("value") == "18" # HLG + for x in adaptation_set.findall("SupplementalProperty") + ), + dv=codecs and codecs.startswith(("dvhe", "dvh1")), + # switches/options + descriptor=Track.Descriptor.MPD, + # decryption + encrypted=encrypted, + pssh=pssh, + kid=kid, + # extra + extra=(rep, adaptation_set) + )) + elif content_type == "audio": + tracks.append(AudioTrack( + id_=track_id, + source=source, + url=track_url, + # metadata + codec=(codecs or "").split(".")[0], + language=track_lang, + bitrate=rep.get("bandwidth"), + channels=next(iter( + rep.xpath("AudioChannelConfiguration/@value") + or adaptation_set.xpath("AudioChannelConfiguration/@value") + ), None), + descriptive=any( + x.get("schemeIdUri") == "urn:mpeg:dash:role:2011" and x.get("value") == "description" + for x in adaptation_set.findall("Accessibility") + ), + # switches/options + descriptor=Track.Descriptor.MPD, + # decryption + encrypted=encrypted, + pssh=pssh, + kid=kid, + # extra + extra=(rep, adaptation_set) + )) + elif content_type == "text": + if source == 'HMAX': + # HMAX SUBS + segment_template = rep.find("SegmentTemplate") + + sub_path_url = rep.findtext("BaseURL") + if not sub_path_url: + sub_path_url = segment_template.get('media') + + try: + path = re.search(r"(t\/.+?\/)t", sub_path_url).group(1) + except AttributeError: + path = 't/sub/' + + is_normal = any(x.get("value") == "subtitle" for x in adaptation_set.findall("Role")) + is_sdh = any(x.get("value") == "caption" for x in adaptation_set.findall("Role")) + is_forced = any(x.get("value") == "forced-subtitle" for x in adaptation_set.findall("Role")) + + if is_normal: + track_url = [base_url + path + adaptation_set.get('lang') + '_sub.vtt'] + elif is_sdh: + track_url = [base_url + path + adaptation_set.get('lang') + '_sdh.vtt'] + elif is_forced: + track_url = [base_url + path + adaptation_set.get('lang') + '_forced.vtt'] + + tracks.append(TextTrack( + id_=track_id, + source=source, + url=track_url, + # metadata + codec=(codecs or "").split(".")[0], + language=track_lang, + forced=is_forced, + sdh=is_sdh, + # switches/options + descriptor=Track.Descriptor.MPD, + # extra + extra=(rep, adaptation_set) + )) + else: + tracks.append(TextTrack( + id_=track_id, + source=source, + url=track_url, + # metadata + codec=(codecs or "").split(".")[0], + language=track_lang, + # switches/options + descriptor=Track.Descriptor.MPD, + # extra + extra=(rep, adaptation_set) + )) + + # r = session.get(url=url) + # mpd = json.loads(json.dumps(xmltodict.parse(r.text))) + # period = mpd['MPD']['Period'] + + # try: + # base_url = urllib.parse.urljoin(mpd['MPD']['BaseURL'], period['BaseURL']) + # print('1', base_url) + # except KeyError: + # base_url = url.rsplit('/', 1)[0] + '/' + + # try: + # stracks = [] + # for pb in period: + # stracks = stracks + pb['AdaptationSet'] + # except TypeError: + # stracks = period['AdaptationSet'] + + # def force_instance(item): + # if isinstance(item['Representation'], list): + # X = item['Representation'] + # else: + # X = [item['Representation']] + # return X + + # # subtitles + # subs_list = [] + # for subs_tracks in stracks: + # if subs_tracks['@contentType'] == 'text': + # for x in force_instance(subs_tracks): + + # try: + # sub_path_url = x['BaseURL'] + # except KeyError: + # sub_path_url = x['SegmentTemplate']['@media'] + + # try: + # path = re.search(r'(t\/.+?\/)t', sub_path_url).group(1) + # except AttributeError: + # path = 't/sub/' + + # isCC = False + # if subs_tracks["Role"]["@value"] == "caption": + # isCC = True + # isNormal = False + + # if isCC: + # lang_id = str(Language.get(subs_tracks['@lang'])) + '-sdh' + # sub_url = base_url + path + subs_tracks['@lang'] + '_sdh.vtt' + # trackType = 'SDH' + # else: + # lang_id = str(Language.get(subs_tracks['@lang'])) + # sub_url = base_url + path + subs_tracks['@lang'] + '_sub.vtt' + # isNormal = True + # trackType = 'NORMAL' + + # isForced = False + # if subs_tracks["Role"]["@value"] == "forced-subtitle": + # isForced = True + # isNormal = False + # trackType = 'FORCED' + # lang_id = str(Language.get(subs_tracks['@lang'])) + '-forced' + # sub_url = base_url + path + subs_tracks['@lang'] + '_forced.vtt' + + + # tracks.append(TextTrack( + # id_=lang_id, + # source=source, + # url=sub_url, + # # metadata + # codec=(codecs or "").split(".")[0], + # language=str(Language.get(subs_tracks['@lang'])), + # forced=isForced, + # sdh=isCC, + # # switches/options + # descriptor=Track.Descriptor.MPD, + # # extra + # extra=(x, subs_tracks) + # )) + + + # Add tracks, but warn only. Assume any duplicate track cannot be handled. + # Since the custom track id above uses all kinds of data, there realistically would + # be no other workaround. + tracks_obj = Tracks() + tracks_obj.add(tracks, warn_only=True) + + return tracks_obj diff --git a/vinetrimmer/services/BaseService.py b/vinetrimmer/services/BaseService.py new file mode 100644 index 0000000..bf3b444 --- /dev/null +++ b/vinetrimmer/services/BaseService.py @@ -0,0 +1,315 @@ +import json +import logging +import os +import re +from abc import ABC + +import requests +from requests.adapters import HTTPAdapter, Retry + +from vinetrimmer.config import config, directories +from vinetrimmer.utils import try_get +from vinetrimmer.utils.collections import as_list +from vinetrimmer.utils.io import get_ip_info + + +class BaseService(ABC): + """ + The service base class. + This should not be directly used as a service file, instead make a new class deriving this one: + + ``` + from vinetrimmer.services.BaseService import BaseService + + ... + + class ServiceName(BaseService): + + ALIASES = ["DSNP", "disneyplus", "disney+"] # first being the service tag (case-sensitive) + GEOFENCE = ["us"] # required region/country for this service. empty list == no specific region. + + def __init__(self, title, **kwargs): + self.title = title + + # make sure the above 3 occur BEFORE the super().__init__() call below. + super().__init__(**kwargs) # re-route the Base related init args + + # service specific variables are recommended to be placed after the super().__init__() call + + # instead of flooding up __init__ with logic, initialize the variables as default values + # here, and then call a new service specific (e.g. "configure()") in which has all the + # preparation logic. This allows for cleaner looking service code. + + # from here, simply implement all the @abstractmethod functions seen in BaseClass. + + # e.g. def get_titles(... + + # After all the Abstract functions, I recommend putting any service specific functions + # separated by a comment denoting that. + + # After those, I also recommend putting any service specific classes once again separated + # by a comment denoting that. + ``` + + This class deals with initializing and preparing of all related code that's common among services. + """ + + # Abstract class variables + ALIASES = [] # begin with source tag (case-sensitive) and name aliases (case-insensitive) + GEOFENCE = [] # list of ip regions required to use the service. empty list == no specific region. + + def __init__(self, ctx): + self.config = ctx.obj.config + self.cookies = ctx.obj.cookies + self.credentials = ctx.obj.credentials + + self.log = logging.getLogger(self.ALIASES[0]) + self.session = self.get_session() + + if ctx.parent.params["no_proxy"]: + return + + proxy = ctx.parent.params["proxy"] or next(iter(self.GEOFENCE), None) + if proxy: + if len("".join(i for i in proxy if not i.isdigit())) == 2: # e.g. ie, ie12, us1356 + proxy = self.get_proxy(proxy) + if proxy: + if "://" not in proxy: + # assume a https proxy port + proxy = f"https://{proxy}" + self.session.proxies.update({"all": proxy}) + else: + self.log.info(" + Proxy was skipped as current region matches") + + def get_session(self): + """ + Creates a Python-requests Session, adds common headers + from config, cookies, retry handler, and a proxy if available. + :returns: Prepared Python-requests Session + """ + session = requests.Session() + session.mount("https://", HTTPAdapter( + max_retries=Retry( + total=5, + backoff_factor=1, + status_forcelist=[429, 500, 502, 503, 504], + ) + )) + session.hooks = { + "response": lambda r, *_, **__: r.raise_for_status(), + } + session.headers.update(config.headers) + session.cookies.update(self.cookies or {}) + return session + + # Abstract functions + + def get_titles(self): + """ + Get Titles for the provided title ID. + + Return a Title object for every unique piece of content found by the Title ID. + Each `Title` object should be thought of as one output file/download. E.g. a movie should be one Title, + and each episode of a TV show would also be one Title, where as a Season would be multiple Title's, one + per episode. + + Each Title object must contain `title_name` (the Show or Movie name). + For TV, it also requires `season` and `episode` numbers, with `episode_name` being optional + but ideally added as well. + For Movies, it has no further requirements but `year` would ideally be added. + + You can return one Title object, or a List of Title objects. + + For any further data specific to each title that you may need in the later abstract methods, + add that data to the `service_data` variable which can be of any type or value you wish. + + :return: One of or a List of Title objects. + """ + raise NotImplementedError + + def get_tracks(self, title): + """ + Get Track objects of the Title. + + Return a Tracks object, which itself can contain Video, Audio, Subtitle or even Chapters. + Tracks.videos, Tracks.audios, Tracks.subtitles, and Track.chapters should be a List of Track objects. + + Each Track in the Tracks should represent a Video/Audio Stream/Representation/Adaptation or + a Subtitle file. + + While one Track should only hold information for one stream/downloadable, try to get as many + unique Track objects per stream type so Stream selection by the root code can give you more + options in terms of Resolution, Bitrate, Codecs, Language, e.t.c. + + No decision making or filtering of which Tracks get returned should happen here. It can be + considered an error to filter for e.g. resolution, codec, and such. All filtering based on + arguments will be done by the root code automatically when needed. + + Make sure you correctly mark which Tracks are encrypted or not via its `encrypted` variable. + + If you are able to obtain the Track's KID (Key ID) as a 32 char (16 bit) HEX string, provide + it to the Track's `kid` variable as it will speed up the decryption process later on. It may + or may not be needed, that depends on the service. Generally if you can provide it, without + downloading any of the Track's stream data, then do. + + :param title: The current `Title` from get_titles that is being executed. + :return: Tracks object containing Video, Audio, Subtitles, and Chapters, if available. + """ + raise NotImplementedError + + def get_chapters(self, title): + """ + Get MenuTracks chapter objects of the Title. + + Return a list of MenuTracks objects. This will be run after get_tracks. If there's anything + from the get_tracks that may be needed, e.g. "device_id" or a-like, store it in the class + via `self` and re-use the value in get_chapters. + + How it's used is generally the same as get_titles. These are only separated as to reduce + function complexity and keep them focused on simple tasks. + + You do not need to sort or order the chapters in any way. However, you do need to filter + and alter them as needed by the service. No modification is made after get_chapters is + ran. So that means ensure that the MenuTracks returned have consistent Chapter Titles + and Chapter Numbers. + + :param title: The current `Title` from get_titles that is being executed. + :return: List of MenuTrack objects, if available, empty list otherwise. + """ + return [] + + def certificate(self, challenge, title, track, session_id): + """ + Get the Service Privacy Certificate. + This is supplied to the Widevine CDM for privacy mode operations. + + If the certificate is a common certificate (one shared among various services), + then return `None` and it will be used instead. + + Once you obtain the certificate, hardcode the certificate here and return it to reduce + unnecessary HTTP requests. + + :param challenge: The service challenge, providing this to a License endpoint should return the + privacy certificate that the service uses. + :param title: The current `Title` from get_titles that is being executed. This is provided in + case it has data needed to be used, e.g. for a HTTP request. + :param track: The current `Track` needing decryption. Provided for same reason as `title`. + :param session_id: This is the session ID bytes blob used for storing Widevine session data. + It has no real meaning or syntax to its value, but some HTTP requests may ask for one. + :return: The Service Privacy Certificate as Bytes or a Base64 string. Don't Base64 Encode or + Decode the data, return as is to reduce unnecessary computations. + """ + return self.license(challenge, title, track, session_id) + + def license(self, challenge, title, track, session_id): + """ + Get the License response for the specified challenge and title data. + This can be decrypted and read by the Widevine CDM to return various keys + like Content Keys or HDCP test keys. + + This is a very important request to get correct. A bad, unexpected, or missing value + in the request can cause your key to be detected and promptly banned, revoked, + disabled, or downgraded. + + :param challenge: The license challenge from the Widevine CDM. + :param title: The current `Title` from get_titles that is being executed. This is provided in + case it has data needed to be used, e.g. for a HTTP request. + :param track: The current `Track` needing decryption. Provided for same reason as `title`. + :param session_id: This is the session ID bytes blob used for storing Widevine session data. + It has no real meaning or syntax to its value, but some HTTP requests may ask for one. + :return: The License response as Bytes or a Base64 string. Don't Base64 Encode or + Decode the data, return as is to reduce unnecessary computations. + """ + raise NotImplementedError + + # Convenience functions to be used by the inheritor + + def parse_title(self, ctx, title): + title = title or ctx.parent.params.get("title") + if not title: + self.log.exit(" - No title ID specified") + if not getattr(self, "TITLE_RE"): + self.title = title + return {} + for regex in as_list(self.TITLE_RE): + m = re.search(regex, title) + if m: + self.title = m.group("id") + return m.groupdict() + self.log.warning(f" - Unable to parse title ID {title!r}, using as-is") + self.title = title + + def get_cache(self, key): + """ + Get path object for an item from service Cache. The path object can then be + used to read or write to the cache under the item's key. + + Parameters: + key: A string similar to a relative path to an item. + """ + return os.path.join(directories.cache, self.ALIASES[0], key) + + # Functions intended to be used here in BaseClass internally only + + def get_proxy(self, region): + if not region: + raise self.log.exit("Region cannot be empty") + region = region.lower() + + self.log.info(f"Obtaining a proxy to \"{region}\"") + + if get_ip_info()["countryCode"].lower() == "".join(i for i in region if not i.isdigit()): + return None # no proxy necessary + + if config.proxies.get(region): + proxy = config.proxies[region] + self.log.info(f" + {proxy}") + elif config.nordvpn.get("username") and config.nordvpn.get("password"): + proxy = self.get_nordvpn_proxy(region) + self.log.info(f" + {proxy} (via NordVPN)") + else: + raise self.log.exit(" - Unable to obtain a proxy") + + if "://" not in proxy: + # assume a https proxy port + proxy = f"https://{proxy}" + + return proxy + + def get_nordvpn_proxy(self, region): + proxy = f"https://{config.nordvpn['username']}:{config.nordvpn['password']}@" + if any(char.isdigit() for char in region): + proxy += f"{region}.nordvpn.com" # direct server id + elif try_get(config.nordvpn, lambda x: x["servers"][region]): + proxy += f"{region}{config.nordvpn['servers'][region]}.nordvpn.com" # configured server id + else: + hostname = self.get_nordvpn_server(region) # get current recommended server id + if not hostname: + raise self.log.exit(f" - NordVPN doesn't contain any servers for the country \"{region}\"") + proxy += hostname + return proxy + ":89" # https: 89, http: 80 + + def get_nordvpn_server(self, country): + """ + Get the recommended NordVPN server hostname for a specified Country. + :param country: Country (in alpha 2 format, e.g. 'US' for United States) + :returns: Recommended NordVPN server hostname, e.g. `us123.nordvpn.com` + """ + # Get the Country's NordVPN ID + countries = self.session.get( + url="https://nordvpn.com/wp-admin/admin-ajax.php", + params={"action": "servers_countries"} + ).json() + country_id = [x["id"] for x in countries if x["code"].lower() == country.lower()] + if not country_id: + return None + country_id = country_id[0] + # Get the most recommended server for the country and return it + recommendations = self.session.get( + url="https://nordvpn.com/wp-admin/admin-ajax.php", + params={ + "action": "servers_recommendations", + "filters": json.dumps({"country_id": country_id}) + } + ).json() + return recommendations[0]["hostname"] diff --git a/vinetrimmer/services/__init__.py b/vinetrimmer/services/__init__.py new file mode 100644 index 0000000..5fb0aa0 --- /dev/null +++ b/vinetrimmer/services/__init__.py @@ -0,0 +1,47 @@ +import os +import re +from copy import copy + +from vinetrimmer.services.BaseService import BaseService + +SERVICE_MAP = {} + +from vinetrimmer.services.amazon import Amazon +from vinetrimmer.services.appletvplus import AppleTVPlus +from vinetrimmer.services.max import Max +from vinetrimmer.services.netflix import Netflix + +# Below dynamic imports fuck with compiling when using Nuitka - exec() call is the problem +#for service in os.listdir(os.path.dirname(__file__)): +# if service.startswith("_") or not service.endswith(".py"): +# continue + +# service = os.path.splitext(service)[0] + +# if service in ("__init__", "BaseService"): +# continue + +# with open(os.path.join(os.path.dirname(__file__), f"{service}.py"), encoding="utf-8") as fd: +# code = "" +# for line in fd.readlines(): +# if re.match(r"\s*(?:import(?! click)|from)\s", line): +# continue +# code += line +# if re.match(r"\s*super\(\)\.__init__\(", line): +# break +# exec(code) + +for x in copy(globals()).values(): + if isinstance(x, type) and issubclass(x, BaseService) and x != BaseService: + SERVICE_MAP[x.__name__] = x.ALIASES + + +def get_service_key(value): + """ + Get the Service Key name (e.g. DisneyPlus, not dsnp, disney+, etc.) from the SERVICE_MAP. + Input value can be of any case-sensitivity and can be either the key itself or an alias. + """ + value = value.lower() + for key, aliases in SERVICE_MAP.items(): + if value in map(str.lower, aliases) or value == key.lower(): + return key diff --git a/vinetrimmer/services/amazon.py b/vinetrimmer/services/amazon.py new file mode 100644 index 0000000..b4c218a --- /dev/null +++ b/vinetrimmer/services/amazon.py @@ -0,0 +1,1327 @@ +from __future__ import annotations + +import base64 +import hashlib +import json +import os +import re +import time +from collections import defaultdict +from pathlib import Path +from urllib.parse import urlencode, quote +from typing import Union +from uuid import uuid4 + +import click +import jsonpickle +import requests +from click import Context +from langcodes import Language +from tldextract import tldextract +from click.core import ParameterSource + +from vinetrimmer.objects import TextTrack, Title, Tracks +from vinetrimmer.objects.tracks import MenuTrack +from vinetrimmer.services.BaseService import BaseService +from vinetrimmer.utils import is_close_match +from vinetrimmer.utils.Logger import Logger +from pywidevine import Device + + + +class Amazon(BaseService): + + """ + Service code for Amazon VOD (https://amazon.com) and Amazon Prime Video (https://primevideo.com). + + \b + Authorization: Cookies + Security: UHD@L1/SL3000 FHD@L3(ChromeCDM)/SL2000 SD@L3, Maintains their own license server like Netflix, be cautious. + + + \b + Region is chosen automatically based on domain extension found in cookies. + Prime Video specific code will be run if the ASIN is detected to be a prime video variant. + Use 'Amazon Video ASIN Display' for Tampermonkey addon for ASIN + https://greasyfork.org/en/scripts/381997-amazon-video-asin-display + + vt dl --list -z uk -q 1080 Amazon B09SLGYLK8 + """ + + ALIASES = ["AMZN", "amazon"] + TITLE_RE = r"^(?:https?://(?:www\.)?(?P<domain>amazon\.(?P<region>com|co\.uk|de|co\.jp)|primevideo\.com)(?:/.+)?/)?(?P<id>[A-Z0-9]{10,}|amzn1\.dv\.gti\.[a-f0-9-]+)" # noqa: E501 + + REGION_TLD_MAP = { + "au": "com.au", + "br": "com.br", + "jp": "co.jp", + "mx": "com.mx", + "tr": "com.tr", + "gb": "co.uk", + "us": "com", + } + VIDEO_RANGE_MAP = { + "SDR": "None", + "HDR10": "Hdr10", + "DV": "DolbyVision", + } + + @staticmethod + @click.command(name="Amazon", short_help="https://amazon.com, https://primevideo.com", help=__doc__) + @click.argument("title", type=str, required=False) + @click.option("-b", "--bitrate", default="CBR", + type=click.Choice(["CVBR", "CBR", "CVBR+CBR"], case_sensitive=False), + help="Video Bitrate Mode to download in. CVBR=Constrained Variable Bitrate, CBR=Constant Bitrate.") + @click.option("-c", "--cdn", default=None, type=str, + help="CDN to download from, defaults to the CDN with the highest weight set by Amazon.") + # UHD, HD, SD. UHD only returns HEVC, ever, even for <=HD only content + @click.option("-vq", "--vquality", default="UHD", + type=click.Choice(["SD", "HD", "UHD"], case_sensitive=False), + help="Manifest quality to request.") + @click.option("-s", "--single", is_flag=True, default=False, + help="Force single episode/season instead of getting series ASIN.") + @click.option("-am", "--amanifest", default="H265", + type=click.Choice(["CVBR", "CBR", "H265"], case_sensitive=False), + help="Manifest to use for audio. Defaults to H265 if the video manifest is missing 640k audio.") + @click.option("-aq", "--aquality", default="SD", + type=click.Choice(["SD", "HD", "UHD"], case_sensitive=False), + help="Manifest quality to request for audio. Defaults to the same as --quality.") + @click.option("-ism", "--ism", is_flag=True, default=False, + help="Set manifest override to SmoothStreaming. Defaults to DASH w/o this flag.") + @click.pass_context + def cli(ctx, **kwargs): + return Amazon(ctx, **kwargs) + + def __init__(self, ctx, title, bitrate: str, cdn: str, vquality: str, single: bool, amanifest: str, aquality: str, ism: bool): + m = self.parse_title(ctx, title) + self.bitrate = bitrate + self.bitrate_source = ctx.get_parameter_source("bitrate") + self.cdn = cdn + self.vquality = vquality + self.vquality_source = ctx.get_parameter_source("vquality") + self.single = single + self.amanifest = amanifest + self.aquality = aquality + self.ism = ism + super().__init__(ctx) + + assert ctx.parent is not None + + self.vcodec = ctx.parent.params["vcodec"] or "H264" + self.range = ctx.parent.params["range_"] or "SDR" + self.chapters_only = ctx.parent.params["chapters_only"] + self.atmos = ctx.parent.params["atmos"] + self.quality = ctx.parent.params.get("quality") or 1080 + + self.cdm = ctx.obj.cdm + self.profile = ctx.obj.profile + + self.region: dict[str, str] = {} + self.endpoints: dict[str, str] = {} + self.device: dict[str, str] = {} + + self.pv = False + self.device_token = None + self.device_id: None + self.customer_id = None + self.client_id = "f22dbddb-ef2c-48c5-8876-bed0d47594fd" # browser client id + + if self.vquality_source != ParameterSource.COMMANDLINE: + if 0 < self.quality <= 576 and self.range == "SDR": + self.log.info(" + Setting manifest quality to SD") + self.vquality = "SD" + + if self.quality > 1080: + self.log.info(" + Setting manifest quality to UHD to be able to get 2160p video track") + self.vquality = "UHD" + self.vcodec = "H265" + + self.vquality = self.vquality or "HD" + + if self.bitrate_source != ParameterSource.COMMANDLINE: + if self.vcodec == "H265" and self.range == "SDR" and self.bitrate != "CVBR+CBR": + self.bitrate = "CVBR+CBR" + self.log.info(" + Changed bitrate mode to CVBR+CBR to be able to get H.265 SDR video track") + + if self.vquality == "UHD" and self.range != "SDR" and self.bitrate != "CBR": + self.bitrate = "CBR" + self.log.info(f" + Changed bitrate mode to CBR to be able to get highest quality UHD {self.range} video track") + + self.orig_bitrate = self.bitrate + + if self.ism: + self.manifestTypeTry = "SmoothStreaming" + self.log.info("Setting manifestType to SmoothStreaming (ISM)") + else: + self.manifestTypeTry = "DASH" + self.log.info("Setting manifestType to DASH (MPD)") + + self.configure() + + # Abstracted functions + + def get_titles(self): + res = self.session.get( + url=self.endpoints["details"], + params={ + "titleID": self.title, + "isElcano": "1", + "sections": ["Atf", "Btf"] + }, + headers={ + "Accept": "application/json" + } + ) + + if not res.ok: + raise self.log.exit(f"Unable to get title: {res.text} [{res.status_code}]") + + data = res.json()["widgets"] + product_details = data.get("productDetails", {}).get("detail") + + if not product_details: + error = res.json()["degradations"][0] + raise self.log.exit(f"Unable to get title: {error['message']} [{error['code']}]") + + titles = [] + + if data["pageContext"]["subPageType"] == "Movie": + card = data["productDetails"]["detail"] + titles.append(Title( + id_=card["catalogId"], + type_=Title.Types.MOVIE, + name=product_details["title"], + #year=card["releaseYear"], + year=card.get("releaseYear", ""), + # language is obtained afterward + original_lang=None, + source=self.ALIASES[0], + service_data=card + )) + else: + if ("titleContent" not in data.keys()) or (data["titleContent"] == []): + episodes = data["episodeList"]["episodes"] + for episode in episodes: + details = episode["detail"] + titles.append( + Title( + id_=details["catalogId"], + type_=Title.Types.TV, + name=product_details["parentTitle"], + season=data["productDetails"]["detail"]["seasonNumber"], + episode=episode["self"]["sequenceNumber"], + episode_name=details["title"], + # language is obtained afterward + original_lang=None, + source=self.ALIASES[0], + service_data=details, + ) + ) + if len(titles) == 25: + page_count = 1 + pagination_data = data.get('episodeList', {}).get('actions', {}).get('pagination', []) + token = next((quote(item.get('token')) for item in pagination_data if item.get('tokenType') == 'NextPage'), None) + while True: + page_count += 1 + res = self.session.get( + url=self.endpoints["getDetailWidgets"], + params={ + "titleID": self.title, + "isTvodOnRow": "1", + "widgets": f'[{{"widgetType":"EpisodeList","widgetToken":"{token}"}}]' + }, + headers={ + "Accept": "application/json" + } + ).json() + episodeList = res['widgets'].get('episodeList', {}) + for item in episodeList.get('episodes', []): + episode = int(item.get('self', {}).get('sequenceNumber', {})) + titles.append(Title( + id_=item["detail"]["catalogId"], + type_=Title.Types.TV, + name=product_details["parentTitle"], + season=product_details["seasonNumber"], + episode=episode, + episode_name=item["detail"]["title"], + # language is obtained afterward + original_lang=None, + source=self.ALIASES[0], + service_data=item + )) + pagination_data = res['widgets'].get('episodeList', {}).get('actions', {}).get('pagination', []) + token = next((quote(item.get('token')) for item in pagination_data if item.get('tokenType') == 'NextPage'), None) + if not token: + break + else: + cards = [ + x["detail"] + for x in data["titleContent"][0]["cards"] + if not self.single or + (self.single and self.title in data["self"]["asins"]) or (self.single and self.title in data["self"]["compactGTI"]) or + (self.single and self.title in x["self"]["asins"]) or (self.single and self.title == x["detail"]["catalogId"]) + ] + for card in cards: + episode_number = card.get("episodeNumber", 0) + if episode_number != 0: + titles.append(Title( + id_=card["catalogId"], + type_=Title.Types.TV, + name=product_details["parentTitle"], + season=product_details["seasonNumber"], + episode=episode_number, + episode_name=card["title"], + # language is obtained afterward + original_lang=None, + source=self.ALIASES[0], + service_data=card + )) + + if not self.single: + temp_title = self.title + temp_single = self.single + + self.single = True + for season in data.get('seasonSelector', []): + season_link = season["seasonLink"] + match = re.search(r'/([a-zA-Z0-9]+)\/ref=', season_link) #extract other season id using re + if match: + extracted_value = match.group(1) + if data["self"]["compactGTI"] == extracted_value: #skip entered asin season data and grab rest id's + continue + + self.title = extracted_value + for title in self.get_titles(): + titles.append(title) + + self.title = temp_title + self.single = temp_single + + + if titles: + # TODO: Needs playback permission on first title, title needs to be available + original_lang = self.get_original_language(self.get_manifest( + next((x for x in titles if x.type == Title.Types.MOVIE or x.episode > 0), titles[0]), + video_codec=self.vcodec, + bitrate_mode=self.bitrate, + quality="UHD", + ignore_errors=True + )) + if original_lang: + for title in titles: + title.original_lang = Language.get(original_lang) + else: + #self.log.warning(" - Unable to obtain the title's original language, setting 'en' default...") + for title in titles: + title.original_lang = Language.get("en") + + filtered_titles = [] + season_episode_count = defaultdict(int) + for title in titles: + key = (title.season, title.episode) + if season_episode_count[key] < 1: + filtered_titles.append(title) + season_episode_count[key] += 1 + + titles = filtered_titles + + return titles + + def get_tracks(self, title: Title) -> Tracks: + tracks = Tracks() + if self.chapters_only: + return [] + + manifest, chosen_manifest, tracks = self.get_best_quality(title) + + manifest = self.get_manifest( + title, + video_codec=self.vcodec, + bitrate_mode=self.bitrate, + quality="UHD", + hdr=self.range, + manifest_type=self.manifestTypeTry, + ignore_errors=False + + ) + + # Move rightsException termination here so that script can attempt continuing + if "rightsException" in manifest["returnedTitleRendition"]["selectedEntitlement"]: + self.log.error(" - The profile used does not have the rights to this title.") + return + + self.customer_id = manifest["returnedTitleRendition"]["selectedEntitlement"]["grantedByCustomerId"] + + default_url_set = manifest["playbackUrls"]["urlSets"][manifest["playbackUrls"]["defaultUrlSetId"]] + encoding_version = default_url_set["urls"]["manifest"]["encodingVersion"] + self.log.info(f" + Detected encodingVersion={encoding_version}") + + chosen_manifest = self.choose_manifest(manifest, self.cdn) + + try: + manifest_url = self.clean_mpd_url(chosen_manifest["avUrlInfoList"][0]["url"], False) + except: + chosen_manifest = default_url_set["urls"]["manifest"] + manifest_url = self.clean_mpd_url(chosen_manifest["url"], optimise=False) + + + self.log.debug(manifest_url) + self.log.info(" + Downloading Manifest") + + try: + tracks = Tracks([ + x for x in iter(Tracks.from_mpd( + url=manifest_url, + session=self.session, + source=self.ALIASES[0], + )) + ]) + except ValueError: + tracks = Tracks([ + x for x in iter(Tracks.from_ism( + url=manifest_url, + session=self.session, + source=self.ALIASES[0], + )) + ]) + except: + raise self.log.exit(f"Unsupported manifest type: {chosen_manifest['streamingTechnology']}\n{manifest_url}") + + need_separate_audio = ((self.aquality or self.vquality) != self.vquality + or self.amanifest == "CVBR" and (self.vcodec, self.bitrate) != ("H264", "CVBR") + or self.amanifest == "CBR" and (self.vcodec, self.bitrate) != ("H264", "CBR") + or self.amanifest == "H265" and self.vcodec != "H265" + or self.amanifest != "H265" and self.vcodec == "H265") + + if not need_separate_audio: + audios = defaultdict(list) + for audio in tracks.audios: + audios[audio.language].append(audio) + + for lang in audios: + if not any((x.bitrate or 0) >= 640000 for x in audios[lang]): + need_separate_audio = True + break + + if need_separate_audio and not self.atmos: + manifest_type = self.amanifest or "H265" + self.log.info(f"Getting audio from {manifest_type} manifest for potential higher bitrate or better codec") + audio_manifest = self.get_manifest( + title=title, + video_codec="H265" if manifest_type == "H265" else "H264", + bitrate_mode="CVBR" if manifest_type != "CBR" else "CBR", + quality=self.aquality or self.vquality, + hdr=None, + ignore_errors=True + ) + if not audio_manifest: + self.log.warning(f" - Unable to get {manifest_type} audio manifests, skipping") + elif not (chosen_audio_manifest := self.choose_manifest(audio_manifest, self.cdn)): + self.log.warning(f" - No {manifest_type} audio manifests available, skipping") + else: + audio_mpd_url = self.clean_mpd_url(chosen_audio_manifest["avUrlInfoList"][0]["url"], optimise=False) + self.log.debug(audio_mpd_url) + self.log.info(" + Downloading HEVC manifest") + + try: + audio_mpd = Tracks([ + x for x in iter(Tracks.from_mpd( + url=audio_mpd_url, + session=self.session, + source=self.ALIASES[0], + )) + ]) + except KeyError: + self.log.warning(f" - Title has no {self.amanifest} stream, cannot get higher quality audio") + else: + tracks.add(audio_mpd.audios, warn_only=True) # expecting possible dupes, ignore + + need_uhd_audio = self.atmos + + if not self.amanifest and ((self.aquality == "UHD" and self.vquality != "UHD") or not self.aquality): + audios = defaultdict(list) + for audio in tracks.audios: + audios[audio.language].append(audio) + for lang in audios: + if not any((x.bitrate or 0) >= 640000 for x in audios[lang]): + need_uhd_audio = True + break + + if need_uhd_audio and (self.config.get("device") or {}).get(self.profile, None): + self.log.info("Getting audio from UHD manifest for potential higher bitrate or better codec") + temp_device = self.device + temp_device_token = self.device_token + temp_device_id = self.device_id + uhd_audio_manifest = None + + + if (self.cdm.device.type == Device.Types.CHROME if "common_privacy_cert" in dir(self.cdm) else True) and self.quality < 2160: + self.log.info(f" + Switching to device to get UHD manifest") + self.register_device() + + + uhd_audio_manifest = self.get_manifest( + title=title, + video_codec="H265", + bitrate_mode="CVBR+CBR", + quality="UHD", + hdr="DV", # Needed for 576kbps Atmos sometimes + manifest_type="DASH", + ignore_errors=True + ) + + self.log.debug(uhd_audio_manifest) + + self.device = temp_device + self.device_token = temp_device_token + self.device_id = temp_device_id + + if not uhd_audio_manifest: + self.log.warning(f" - Unable to get UHD manifests, skipping") + elif not (chosen_uhd_audio_manifest := self.choose_manifest(uhd_audio_manifest, self.cdn)): + self.log.warning(f" - No UHD manifests available, skipping") + else: + uhd_audio_mpd_url = self.clean_mpd_url(chosen_uhd_audio_manifest["avUrlInfoList"][0]["url"], optimise=False) + self.log.debug(uhd_audio_mpd_url) + self.log.info(" + Downloading UHD manifest") + + try: + uhd_audio_mpd = Tracks([ + x for x in iter(Tracks.from_mpd( + url=uhd_audio_mpd_url, + session=self.session, + source=self.ALIASES[0], + )) + ]) + except ValueError: + uhd_audio_mpd = Tracks([ + x for x in iter(Tracks.from_ism( + url=uhd_audio_mpd_url, + session=self.session, + source=self.ALIASES[0], + )) + ]) + except KeyError: + self.log.warning(f" - Title has no UHD stream, cannot get higher quality audio") + + # replace the audio tracks with DV manifest version if atmos is present + if any(x for x in uhd_audio_mpd.audios if x.atmos): + tracks.audios = uhd_audio_mpd.audios + + for video in tracks.videos: + try: + video.hdr10 = chosen_manifest["hdrFormat"] == "Hdr10" + video.dv = chosen_manifest["hdrFormat"] == "DolbyVision" + except: + video.hdr10 = chosen_manifest["dynamicRange"] == "Hdr10" + video.dv = chosen_manifest["dynamicRange"] == "DolbyVision" + + + for audio in tracks.audios: + audio.descriptive = audio.extra[1].get("audioTrackSubtype") == "descriptive" or audio.extra[1].get("AudioTrackSubtype") == "descriptive" + # Amazon @lang is just the lang code, no dialect, @audioTrackId has it. + audio_track_id = audio.extra[1].get("audioTrackId") or audio.extra[1].get("AudioTrackId") + if audio_track_id: + audio.language = Language.get(audio_track_id.split("_")[0]) # e.g. es-419_ec3_blabla + + for sub in manifest.get("subtitleUrls", []) + manifest.get("forcedNarratives", []): + tracks.add(TextTrack( + id_=sub.get( + "timedTextTrackId", + f"{sub['languageCode']}_{sub['type']}_{sub['subtype']}_{sub['index']}" + ), + source=self.ALIASES[0], + url=os.path.splitext(sub["url"])[0] + ".srt", # DFXP -> SRT forcefully seems to work fine + # metadata + codec="srt", # sub["format"].lower(), + language=sub["languageCode"], + #is_original_lang=title.original_lang and is_close_match(sub["languageCode"], [title.original_lang]), + forced="forced" in sub["displayName"], + sdh=sub["type"].lower() == "sdh" # TODO: what other sub types? cc? forced? + ), warn_only=True) # expecting possible dupes, ignore + + return tracks + + def get_chapters(self, title: Title) -> list[MenuTrack]: + """Get chapters from Amazon's XRay Scenes API.""" + manifest = self.get_manifest( + title, + video_codec=self.vcodec, + bitrate_mode=self.bitrate, + quality="UHD", + manifest_type=self.manifestTypeTry, + hdr=self.range + ) + + if "xrayMetadata" in manifest: + xray_params = manifest["xrayMetadata"]["parameters"] + elif self.chapters_only: + xray_params = { + "pageId": "fullScreen", + "pageType": "xray", + "serviceToken": json.dumps({ + "consumptionType": "Streaming", + "deviceClass": "normal", + "playbackMode": "playback", + "vcid": manifest["returnedTitleRendition"]["contentId"], + }) + } + else: + return [] + + xray_params.update({ + "deviceID": self.device_id, + "deviceTypeID": self.config["device_types"]["browser"], # must be browser device type + "marketplaceID": self.region["marketplace_id"], + "gascEnabled": str(self.pv).lower(), + "decorationScheme": "none", + "version": "inception-v2", + "uxLocale": "en-US", + "featureScheme": "XRAY_WEB_2020_V1" + }) + + xray = self.session.get( + url=self.endpoints["xray"], + params=xray_params + ).json().get("page") + + if not xray: + return [] + + widgets = xray["sections"]["center"]["widgets"]["widgetList"] + + scenes = next((x for x in widgets if x["tabType"] == "scenesTab"), None) + if not scenes: + return [] + scenes = scenes["widgets"]["widgetList"][0]["items"]["itemList"] + + chapters = [] + + for scene in scenes: + chapter_title = scene["textMap"]["PRIMARY"] + match = re.search(r"(\d+\. |)(.+)", chapter_title) + if match: + chapter_title = match.group(2) + chapters.append(MenuTrack( + number=int(scene["id"].replace("/xray/scene/", "")), + title=chapter_title, + timecode=scene["textMap"]["TERTIARY"].replace("Starts at ", "") + )) + + return chapters + + def certificate(self, **_): + return self.config["certificate"] + + def license(self, challenge: Union[bytes, str], title: Title, **_): + lic_challenge = base64.b64encode(challenge).decode("utf-8") if isinstance(challenge, bytes) else base64.b64encode(challenge.encode("utf-8")).decode("utf-8") + self.log.debug(f"Challenge - {lic_challenge}") + + try: + lic = self.session.post( + url=self.endpoints["licence"], + params={ + "asin": title.id, + "consumptionType": "Streaming", + "desiredResources": "PlayReadyLicense", + "deviceTypeID": self.device["device_type"], + "deviceID": self.device_id, + "firmware": 1, + "gascEnabled": str(self.pv).lower(), + "marketplaceID": self.region["marketplace_id"], + "resourceUsage": "ImmediateConsumption", + "videoMaterialType": "Feature", + "operatingSystemName": "Linux" if self.vquality == "SD" else "Windows", + "operatingSystemVersion": "unknown" if self.vquality == "SD" else "10.0", + "customerID": self.customer_id, + "deviceDrmOverride": "Playready", + "deviceStreamingTechnologyOverride": "SmoothStreaming", + "deviceVideoQualityOverride": self.vquality, + "deviceHdrFormatsOverride": self.VIDEO_RANGE_MAP.get(self.range, "None"), + }, + headers={ + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": f"Bearer {self.device_token}" + }, + data={ + "playReadyChallenge": lic_challenge, # expects base64 + "includeHdcpTestKeyInLicense": "true" + } + ).json() + if "errorsByResource" in lic: + + error_code = lic["errorsByResource"]["PlayReadyLicense"] + self.log.debug(error_code) + if "errorCode" in error_code: + error_code = error_code["errorCode"] + elif "type" in error_code: + error_code = error_code["type"] + if error_code == "PRS.NoRights.AnonymizerIP": + raise self.log.exit(" - Amazon detected a Proxy/VPN and refused to return a license!") + message = lic["errorsByResource"]["PlayReadyLicense"]["message"] + raise self.log.exit(f" - Amazon reported an error during the License request: {message} [{error_code}]") + if "error" in lic: + error_code = lic["error"] + if "errorCode" in error_code: + error_code = error_code["errorCode"] + elif "type" in error_code: + error_code = error_code["type"] + if error_code == "PRS.NoRights.AnonymizerIP": + raise self.log.exit(" - Amazon detected a Proxy/VPN and refused to return a license!") + message = lic["error"]["message"] + raise self.log.exit(f" - Amazon reported an error during the License request: {message} [{error_code}]") + except: + lic = self.session.post( + url=self.endpoints["licence"], + params={ + "asin": title.id, + "consumptionType": "Streaming", + "desiredResources": "PlayReadyLicense", + "deviceTypeID": self.device["device_type"], + "deviceID": self.device_id, + "firmware": 1, + "gascEnabled": str(self.pv).lower(), + "marketplaceID": self.region["marketplace_id"], + "resourceUsage": "ImmediateConsumption", + "videoMaterialType": "Feature", + "operatingSystemName": "Linux" if self.vquality == "SD" else "Windows", + "operatingSystemVersion": "unknown" if self.vquality == "SD" else "10.0", + "customerID": self.customer_id, + "deviceDrmOverride": "Playready", #CENC or Playready + "deviceStreamingTechnologyOverride": "DASH", + "deviceVideoQualityOverride": self.vquality, + "deviceHdrFormatsOverride": self.VIDEO_RANGE_MAP.get(self.range, "None"), + }, + headers={ + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": f"Bearer {self.device_token}" + }, + data={ + "playReadyChallenge": lic_challenge, # expects base64 + "includeHdcpTestKeyInLicense": "true" + } + ).json() + if "errorsByResource" in lic: + + error_code = lic["errorsByResource"]["PlayReadyLicense"] + self.log.debug(error_code) + if "errorCode" in error_code: + error_code = error_code["errorCode"] + elif "type" in error_code: + error_code = error_code["type"] + if error_code == "PRS.NoRights.AnonymizerIP": + raise self.log.exit(" - Amazon detected a Proxy/VPN and refused to return a license!") + message = lic["errorsByResource"]["PlayReadyLicense"]["message"] + raise self.log.exit(f" - Amazon reported an error during the License request: {message} [{error_code}]") + if "error" in lic: + error_code = lic["error"] + if "errorCode" in error_code: + error_code = error_code["errorCode"] + elif "type" in error_code: + error_code = error_code["type"] + if error_code == "PRS.NoRights.AnonymizerIP": + raise self.log.exit(" - Amazon detected a Proxy/VPN and refused to return a license!") + message = lic["error"]["message"] + raise self.log.exit(f" - Amazon reported an error during the License request: {message} [{error_code}]") + #self.log.debug(lic["playReadyLicense"]["encodedLicenseResponse"]) + + return base64.b64decode(lic["playReadyLicense"]["encodedLicenseResponse"].encode("utf-8")).decode("utf-8") # Return Xml licence + + # Service specific functions + + def configure(self) -> None: + if len(self.title) > 10: + self.pv = True + + self.log.info("Getting Account Region") + self.region = self.get_region() + if not self.region: + raise self.log.exit(" - Failed to get Amazon Account region") + self.GEOFENCE.append(self.region["code"]) + self.log.info(f" + Region: {self.region['code']}") + + # endpoints must be prepared AFTER region data is retrieved + self.endpoints = self.prepare_endpoints(self.config["endpoints"], self.region) + + self.session.headers.update({ + "Origin": f"https://{self.region['base']}" + }) + + self.device = (self.config.get("device") or {}).get(self.profile, {}) + if (self.quality > 1080 or self.range != "SDR") and self.vcodec == "H265" and (self.cdm.device.type == Device.Types.CHROME if "common_privacy_cert" in dir(self.cdm) else True): + self.log.info(f"Using device to get UHD manifests") + self.register_device() + elif not self.device or self.vquality != "UHD" or (self.cdm.device.type == Device.Types.CHROME if "common_privacy_cert" in dir(self.cdm) else False): + # falling back to browser-based device ID + if not self.device: + self.log.warning( + "No Device information was provided for %s, using browser device...", + self.profile + ) + self.device_id = hashlib.sha224( + ("CustomerID" + self.session.headers["User-Agent"]).encode("utf-8") + ).hexdigest() + self.device = {"device_type": self.config["device_types"]["browser"]} + else: + self.register_device() + + def register_device(self) -> None: + self.device = (self.config.get("device") or {}).get(self.profile, {}) + device_cache_path = self.get_cache("device_tokens_{profile}_{hash}.json".format( + profile=self.profile, + hash=hashlib.md5(json.dumps(self.device).encode()).hexdigest()[0:6] + )) + self.device_token = self.DeviceRegistration( + device=self.device, + endpoints=self.endpoints, + log=self.log, + cache_path=device_cache_path, + session=self.session + ).bearer + self.device_id = self.device.get("device_serial") + if not self.device_id: + raise self.log.exit(f" - A device serial is required in the config, perhaps use: {os.urandom(8).hex()}") + + def get_region(self) -> dict: + domain_region = self.get_domain_region() + if not domain_region: + return {} + + region = self.config["regions"].get(domain_region) + if not region: + raise self.log.exit(f" - There's no region configuration data for the region: {domain_region}") + + region["code"] = domain_region + + if self.pv: + res = self.session.get("https://www.primevideo.com").text + match = re.search(r'ue_furl *= *([\'"])fls-(na|eu|fe)\.amazon\.[a-z.]+\1', res) + if match: + pv_region = match.group(2).lower() + else: + raise self.log.exit(" - Failed to get PrimeVideo region") + pv_region = {"na": "atv-ps"}.get(pv_region, f"atv-ps-{pv_region}") + region["base_manifest"] = f"{pv_region}.primevideo.com" + region["base"] = "www.primevideo.com" + + return region + + def get_domain_region(self): + """Get the region of the cookies from the domain.""" + tlds = [tldextract.extract(x.domain) for x in self.cookies if x.domain_specified] + tld = next((x.suffix for x in tlds if x.domain.lower() in ("amazon", "primevideo")), None) + if tld: + tld = tld.split(".")[-1] + return {"com": "us", "uk": "gb"}.get(tld, tld) + + def prepare_endpoint(self, name: str, uri: str, region: dict) -> str: + if name in ("browse", "playback", "licence", "xray"): + return f"https://{(region['base_manifest'])}{uri}" + if name in ("ontv", "devicelink", "details", "getDetailWidgets"): + if self.pv: + host = "www.primevideo.com" + else: + host = region["base"] + return f"https://{host}{uri}" + if name in ("codepair", "register", "token"): + return f"https://{self.config['regions']['us']['base_api']}{uri}" + raise ValueError(f"Unknown endpoint: {name}") + + def prepare_endpoints(self, endpoints: dict, region: dict) -> dict: + return {k: self.prepare_endpoint(k, v, region) for k, v in endpoints.items()} + + def choose_manifest(self, manifest: dict, cdn=None): + """Get manifest URL for the title based on CDN weight (or specified CDN).""" + if cdn: + cdn = cdn.lower() + manifest = next((x for x in manifest["audioVideoUrls"]["avCdnUrlSets"] if x["cdn"].lower() == cdn), {}) + if not manifest: + raise self.log.exit(f" - There isn't any manifests available on the CDN \"{cdn}\" for this title") + else: + manifest = next((x for x in sorted([x for x in manifest["audioVideoUrls"]["avCdnUrlSets"]], key=lambda x: int(x["cdnWeightsRank"]))), {}) + + return manifest + + def get_manifest( + self, + title: Title, + video_codec: str, + bitrate_mode: str, + quality: str, + manifest_type: str = None, + hdr=None, + ignore_errors: bool = False + ) -> dict: + res = self.session.get( + url=self.endpoints["playback"], + params={ + "asin": title.id, + "consumptionType": "Streaming", + "desiredResources": ",".join([ + "PlaybackUrls", + "AudioVideoUrls", + "CatalogMetadata", + "ForcedNarratives", + "SubtitlePresets", + "SubtitleUrls", + "TransitionTimecodes", + "TrickplayUrls", + "CuepointPlaylist", + "XRayMetadata", + "PlaybackSettings", + ]), + "deviceID": self.device_id, + "deviceTypeID": self.device["device_type"], + "firmware": 1, + "gascEnabled": str(self.pv).lower(), + "marketplaceID": self.region["marketplace_id"], + "resourceUsage": "CacheResources", + "videoMaterialType": "Feature", + "playerType": "html5", + "clientId": self.client_id, + **({ + "operatingSystemName": "Linux" if quality == "SD" else "Windows", + "operatingSystemVersion": "unknown" if quality == "SD" else "10.0", + } if not self.device_token else {}), + "deviceDrmOverride": "Playready" if manifest_type and (manifest_type == "SmoothStreaming") else "CENC", + "deviceStreamingTechnologyOverride": manifest_type if manifest_type else "DASH", + "deviceProtocolOverride": "Https", + "deviceVideoCodecOverride": video_codec, + "deviceBitrateAdaptationsOverride": bitrate_mode.replace("+", ","), + "deviceVideoQualityOverride": quality, + "deviceHdrFormatsOverride": self.VIDEO_RANGE_MAP.get(hdr, "None"), + "supportedDRMKeyScheme": "DUAL_KEY", # ? + "liveManifestType": "live,accumulating", # ? + "titleDecorationScheme": "primary-content", + "subtitleFormat": "TTMLv2", + "languageFeature": "MLFv2", # ? + "uxLocale": "en_US", + "xrayDeviceClass": "normal", + "xrayPlaybackMode": "playback", + "xrayToken": "XRAY_WEB_2020_V1", + "playbackSettingsFormatVersion": "1.0.0", + "playerAttributes": json.dumps({"frameRate": "HFR"}), + # possibly old/unused/does nothing: + "audioTrackId": "all", + }, + headers={ + "Authorization": f"Bearer {self.device_token}" if self.device_token else None, + }, + ) + try: + manifest = res.json() + except json.JSONDecodeError: + if ignore_errors: + return {} + + raise self.log.exit(" - Amazon didn't return JSON data when obtaining the Playback Manifest.") + + if "error" in manifest: + if ignore_errors: + return {} + raise self.log.exit(" - Amazon reported an error when obtaining the Playback Manifest.") + + # Commented out as we move the rights exception check elsewhere + # if "rightsException" in manifest["returnedTitleRendition"]["selectedEntitlement"]: + # if ignore_errors: + # return {} + # raise self.log.exit(" - The profile used does not have the rights to this title.") + + # Below checks ignore NoRights errors + + if ( + manifest.get("errorsByResource", {}).get("PlaybackUrls") and + manifest["errorsByResource"]["PlaybackUrls"].get("errorCode") != "PRS.NoRights.NotOwned" + ): + if ignore_errors: + return {} + error = manifest["errorsByResource"]["PlaybackUrls"] + raise self.log.exit(f" - Amazon had an error with the Playback Urls: {error['message']} [{error['errorCode']}]") + + if ( + manifest.get("errorsByResource", {}).get("AudioVideoUrls") and + manifest["errorsByResource"]["AudioVideoUrls"].get("errorCode") != "PRS.NoRights.NotOwned" + ): + if ignore_errors: + return {} + error = manifest["errorsByResource"]["AudioVideoUrls"] + raise self.log.exit(f" - Amazon had an error with the A/V Urls: {error['message']} [{error['errorCode']}]") + + #self.log.debug(manifest) + + return manifest + + @staticmethod + def get_original_language(manifest): + """Get a title's original language from manifest data.""" + try: + return next( + x["language"].replace("_", "-") + for x in manifest["catalogMetadata"]["playback"]["audioTracks"] + if x["isOriginalLanguage"] + ) + except (KeyError, StopIteration): + pass + + if "defaultAudioTrackId" in manifest.get("playbackUrls", {}): + try: + return manifest["playbackUrls"]["defaultAudioTrackId"].split("_")[0] + except IndexError: + pass + + try: + return sorted( + manifest["audioVideoUrls"]["audioTrackMetadata"], + key=lambda x: x["index"] + )[0]["languageCode"] + except (KeyError, IndexError): + pass + + return None + + @staticmethod + def clean_mpd_url(mpd_url, optimise): + """Clean up an Amazon MPD manifest url.""" + if optimise: + return mpd_url.replace("~", "") + "?encoding=segmentBase" + if match := re.match(r"(https?://.*/)d.?/.*~/(.*)", mpd_url): + return "".join(match.groups()) + elif match := re.match(r"(https?://.*/)d.?/.*\$.*?/(.*)", mpd_url): + return "".join(match.groups()) + else: + try: + mpd_url = "".join( + re.split(r"(?i)(/)", mpd_url)[:5] + re.split(r"(?i)(/)", mpd_url)[9:] + ) + except IndexError: + self.log.warning("Unable to parse manifest URL") + return mpd_url + + raise ValueError("Unable to parse manifest URL") + + def get_best_quality(self, title): + """ + Choose the best quality manifest from CBR / CVBR + """ + + track_list = [] + bitrates = [self.orig_bitrate] + + if self.vcodec != "H265": + bitrates = self.orig_bitrate.split('+') + + for bitrate in bitrates: + manifest = self.get_manifest( + title, + video_codec=self.vcodec, + bitrate_mode=bitrate, + quality="UHD", + hdr=self.range, + ignore_errors=False + ) + + if not manifest: + self.log.warning(f"Skipping {bitrate} manifest due to error") + continue + + # return three empty objects if a rightsException error exists to correlate to manifest, chosen_manifest, tracks + if "rightsException" in manifest["returnedTitleRendition"]["selectedEntitlement"]: + return None, None, None + + self.customer_id = manifest["returnedTitleRendition"]["selectedEntitlement"]["grantedByCustomerId"] + + default_url_set = manifest["playbackUrls"]["urlSets"][manifest["playbackUrls"]["defaultUrlSetId"]] + encoding_version = default_url_set["urls"]["manifest"]["encodingVersion"] + self.log.info(f" + Detected encodingVersion={encoding_version}") + + + #self.log.debug(manifest) + + chosen_manifest = self.choose_manifest(manifest, self.cdn) + + try: + mpd_url = self.clean_mpd_url(chosen_manifest["avUrlInfoList"][0]["url"], optimise=False) + except: + chosen_manifest = default_url_set["urls"]["manifest"] + mpd_url = self.clean_mpd_url(chosen_manifest["url"], optimise=False) + + self.log.debug(mpd_url) + self.log.info(f" + Downloading {bitrate} Manifest") + self.log.debug(f"Obtained Manifest Type: {chosen_manifest['streamingTechnology']}") + try: + tracks = Tracks([ + x for x in iter(Tracks.from_mpd( + url=mpd_url, + session=self.session, + source=self.ALIASES[0], + )) + ]) + except ValueError: + tracks = Tracks([ + x for x in iter(Tracks.from_ism( + url=mpd_url, + session=self.session, + source=self.ALIASES[0], + )) + ]) + except: + raise self.log.exit(f"Unsupported manifest type: {chosen_manifest['streamingTechnology']}\n{mpd_url}") + + for video in tracks.videos: + video.note = bitrate + + max_size = max(tracks.videos, key=lambda x: int(x.size or 0)).size + + + + track_list.append({ + 'bitrate': bitrate, + 'max_size': max_size, + 'manifest': manifest, + 'chosen_manifest': chosen_manifest, + 'tracks': tracks + }) + + best_quality = max(track_list, key=lambda x: x['max_size']) + + if len(self.bitrate.split('+')) > 1: + self.bitrate = best_quality['bitrate'] + self.log.info("Selected video manifest bitrate: %s", best_quality['bitrate']) + + return best_quality['manifest'], best_quality['chosen_manifest'], best_quality['tracks'] + + # Service specific classes + + class DeviceRegistration: + + def __init__(self, device: dict, endpoints: dict, cache_path: Path, session: requests.Session, log: Logger): + self.session = session + self.device = device + self.endpoints = endpoints + self.cache_path = Path(cache_path) + self.log = log + + self.device = {k: str(v) if not isinstance(v, str) else v for k, v in self.device.items()} + + self.bearer = None + if os.path.isfile(self.cache_path): + with open(self.cache_path, encoding="utf-8") as fd: + cache = jsonpickle.decode(fd.read()) + #self.device["device_serial"] = cache["device_serial"] + #if cache.get("expires_in", 0) > int(time.time()): + # # not expired, lets use + # self.log.info(" + Using cached device bearer") + # self.bearer = cache["access_token"] + #else: + # expired, refresh + self.log.info("Refreshing cached device bearer...") + refreshed_tokens = self.refresh(self.device, cache["refresh_token"], cache["access_token"]) + refreshed_tokens["refresh_token"] = cache["refresh_token"] + # expires_in seems to be in minutes, create a unix timestamp and add the minutes in seconds + refreshed_tokens["expires_in"] = int(time.time()) + int(refreshed_tokens["expires_in"]) + with open(self.cache_path, "w", encoding="utf-8") as fd: + fd.write(jsonpickle.encode(refreshed_tokens)) + self.bearer = refreshed_tokens["access_token"] + else: + self.log.info(" + Registering new device bearer") + self.bearer = self.register(self.device) + + def register(self, device: dict) -> dict: + """ + Register device to the account + :param device: Device data to register + :return: Device bearer tokens + """ + # OnTV csrf + csrf_token = self.get_csrf_token() + + # Code pair + code_pair = self.get_code_pair(device) + + # Device link + response = self.session.post( + url=self.endpoints["devicelink"], + headers={ + "Accept": "*/*", + "Accept-Language": "en-US,en;q=0.9,es-US;q=0.8,es;q=0.7", # needed? + "Content-Type": "application/x-www-form-urlencoded", + "Referer": self.endpoints["ontv"] + }, + params=urlencode({ + # any reason it urlencodes here? requests can take a param dict... + "ref_": "atv_set_rd_reg", + "publicCode": code_pair["public_code"], # public code pair + "token": csrf_token # csrf token + }) + ) + if response.status_code != 200: + raise self.log.exit(f"Unexpected response with the codeBasedLinking request: {response.text} [{response.status_code}]") + + # Register + response = self.session.post( + url=self.endpoints["register"], + headers={ + "Content-Type": "application/json", + "Accept-Language": "en-US" + }, + json={ + "auth_data": { + "code_pair": code_pair + }, + "registration_data": device, + "requested_token_type": ["bearer"], + "requested_extensions": ["device_info", "customer_info"] + }, + cookies=None # for some reason, may fail if cookies are present. Odd. + ) + if response.status_code != 200: + raise self.log.exit(f"Unable to register: {response.text} [{response.status_code}]") + bearer = response.json()["response"]["success"]["tokens"]["bearer"] + bearer["expires_in"] = int(time.time()) + int(bearer["expires_in"]) + + # Cache bearer + os.makedirs(os.path.dirname(self.cache_path), exist_ok=True) + with open(self.cache_path, "w", encoding="utf-8") as fd: + fd.write(jsonpickle.encode(bearer)) + + return bearer["access_token"] + + def refresh(self, device: dict, refresh_token: str, access_token: str) -> dict: + """ + json3 = { + 'app_name': 'ioBroker Alexa2', + 'app_version': '2.2.556530.0', + 'di.sdk.version': '6.12.4', + 'source_token': refresh_token, + 'package_name': 'com.amazon.echo', + 'di.hw.version': 'iPhone', + 'platform': 'iOS', + 'requested_token_type': 'access_token', + 'source_token_type': 'refresh_token', + 'di.os.name': 'iOS', + 'di.os.version': '16.6', + 'current_version': '6.12.4' + } + json4 = { + 'di.os.name': 'iOS', + 'app_version': '2.2.223830.0', + 'domain': '.' + 'api.amazon.com', + 'source_token': refresh_token, + 'requested_token_type': 'auth_cookies', + 'source_token_type': 'refresh_token', + 'di.hw.version': 'iPhone', + 'di.sdk.version': '6.10.0', + 'cookies': {}, + 'app_name': 'Amazon Alexa', + 'di.os.version': '11.4.1' + } + """ + # https://gitlab.com/keatontaylor/alexapy/-/commit/540b6333d973177bbc98e6ef39b00134f80ef0bb + + cookies = { + 'at-main': access_token, + } + headers = { + 'User-Agent': 'AmazonWebView/Amazon Alexa/2.2.223830.0/iOS/11.4.1/iPhone', + 'Accept-Language': 'en-US', + 'Accept-Charset': 'utf-8', + 'Connection': 'keep-alive', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': '*/*' + } + data = { + 'di.os.name': 'iOS', + 'app_version': '2.2.223830.0', + 'domain': '.' + 'api.amazon.com', + 'source_token': refresh_token, + 'requested_token_type': 'auth_cookies', + 'source_token_type': 'refresh_token', + 'di.hw.version': 'iPhone', + 'di.sdk.version': '6.10.0', + 'app_name': 'Amazon Alexa', + 'di.os.version': '11.4.1' + } + + try: + # using the refresh token get the cookies needed for making calls to alexa.amazon.com + response = requests.post(url=self.endpoints["token"], headers=headers, cookies=cookies, data=data).json() + # Extract the cookies from the response + raw_cookies = response['response']['tokens']['cookies']['.amazon.com'] + except: + error = response['response']["error"] + self.cache_path.unlink(missing_ok=True) + raise self.log.exit(f"Error when refreshing cookies: {error['message']} [{error['code']}]") + + # Create a new cookies object to be used with requsts. + cookies = {} + for cookie in raw_cookies: + cookies[cookie['Name']] = cookie['Value'] + + headers = { + 'Content-Type': 'application/json; charset=utf-8', + 'Accept-Encoding': 'gzip, deflate, br', + 'Connection': 'keep-alive', + 'Accept': 'application/json; charset=utf-8', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 PitanguiBridge/2.2.389238.0-[HARDWARE=iPhone12_3][SOFTWARE=14.3]', + 'Accept-Language': 'en-US,en-US;q=1.0', + } + + json_data = { + **device, + 'requested_token_type': 'access_token', + 'source_token_type': 'refresh_token', + "source_token": refresh_token, + } # https://github.com/Sandmann79/xbmc/blob/dab17d913ee877d96115e6f799623bca158f3f24/plugin.video.amazon-test/resources/lib/login.py#L593 + + # make the call and print the response. + response = requests.post(url=self.endpoints["token"], headers=headers, cookies=cookies, json=json_data).json() + + if "error" in response: + self.cache_path.unlink(missing_ok=True) # Remove the cached device as its tokens have expired + raise self.log.exit( + f"Failed to refresh device token: {response['error_description']} [{response['error']}]" + ) + self.log.debug(response) + if response["token_type"] != "bearer": + raise self.log.exit("Unexpected returned refreshed token type") + + return response + + def get_csrf_token(self) -> str: + """ + On the amazon website, you need a token that is in the html page, + this token is used to register the device + :return: OnTV Page's CSRF Token + """ + res = self.session.get(self.endpoints["ontv"]) + response = res.text + if 'input type="hidden" name="appAction" value="SIGNIN"' in response: + raise self.log.exit( + "Cookies are signed out, cannot get ontv CSRF token. " + f"Expecting profile to have cookies for: {self.endpoints['ontv']}" + ) + for match in re.finditer(r"<script type=\"text/template\">(.+)</script>", response): + prop = json.loads(match.group(1)) + prop = prop.get("props", {}).get("codeEntry", {}).get("token") + if prop: + return prop + raise self.log.exit("Unable to get ontv CSRF token \n Navigate to /region/eu/ontv/code?ref_=atv_auth_red_aft, login and save cookies from that page to default.txt") + + def get_code_pair(self, device: dict) -> dict: + """ + Getting code pairs based on the device that you are using + :return: public and private code pairs + """ + res = self.session.post( + url=self.endpoints["codepair"], + headers={ + "Content-Type": "application/json", + "Accept-Language": "en-US" + }, + json={"code_data": device} + ).json() + if "error" in res: + raise self.log.exit(f"Unable to get code pair: {res['error_description']} [{res['error']}]") + return res diff --git a/vinetrimmer/services/appletvplus.py b/vinetrimmer/services/appletvplus.py new file mode 100644 index 0000000..8abd723 --- /dev/null +++ b/vinetrimmer/services/appletvplus.py @@ -0,0 +1,311 @@ +import base64 +import json +import re +from datetime import datetime +from urllib.parse import unquote + +import click +import m3u8 +import requests + +from vinetrimmer.objects import AudioTrack, TextTrack, Title, Tracks, VideoTrack +from vinetrimmer.services.BaseService import BaseService +from vinetrimmer.utils.collections import as_list +from vinetrimmer.vendor.pymp4.parser import Box + + +class AppleTVPlus(BaseService): + """ + Service code for Apple's TV Plus streaming service (https://tv.apple.com). + + \b + WIP: decrypt and removal of bumper/dub cards + + \b + Authorization: Cookies + Security: UHD@L1 FHD@L1 HD@L3 + """ + + ALIASES = ["ATVP", "appletvplus", "appletv+"] + TITLE_RE = [ + r"^(?:https?://tv\.apple\.com(?:/[a-z]{2})?/(?:movie|show|episode)/[a-z0-9-]+/)?(?P<id>umc\.cmc\.[a-z0-9]+)", + r"^(?:https?://tv\.apple\.com(?:/[a-z]{2})?/(?:movie|show|episode|sporting-event)/[a-z0-9-]+/)?(?P<id>umc\.cse\.[a-z0-9]+)", + ] + + VIDEO_CODEC_MAP = { + "H264": ["avc"], + "H265": ["hvc", "hev", "dvh"] + } + AUDIO_CODEC_MAP = { + "AAC": ["HE", "stereo"], + "AC3": ["ac3"], + "EC3": ["ec3", "atmos"] + } + + @staticmethod + @click.command(name="AppleTVPlus", short_help="https://tv.apple.com") + @click.argument("title", type=str, required=False) + @click.option("-c", "--condensed", is_flag=True, default=False, + help="To retrieve Condensed Recap instead of default Full Game") + @click.pass_context + def cli(ctx, **kwargs): + return AppleTVPlus(ctx, **kwargs) + + def __init__(self, ctx: click.Context, title, condensed: bool): + super().__init__(ctx) + self.parse_title(ctx, title) + + self.vcodec = ctx.parent.params["vcodec"] + self.acodec = ctx.parent.params["acodec"] + self.alang = ctx.parent.params["alang"] + self.subs_only = ctx.parent.params["subs_only"] + + self.extra_server_parameters = None + + self.condensed = condensed + self.type = 1 # show = 0, movie = 1, sporting-event = 2 + + self.configure() + + def get_titles(self): + r = None + for i in range(3): + try: + self.params = { + 'utsk': '6e3013c6d6fae3c2::::::9318c17fb39d6b9c', + 'caller': 'web', + 'sf': self.storefront, + 'v': '82' if self.type == 2 else '46', + 'pfm': 'appletv', + 'mfr': 'Apple', + 'locale': 'en-US', + 'l': 'en', + 'ctx_brand': 'tvs.sbd.4000', + 'count': '100', + 'skip': '0', + } + r = self.session.get( + url=self.config["endpoints"]["title"].format(type={0: "shows", 1: "movies", 2: "sporting-events"}[i], id=self.title), + params=self.params + ) + except requests.HTTPError as e: + if e.response.status_code != 404: + raise + else: + if r.ok: + break + if not r: + raise self.log.exit(f" - Title ID {self.title!r} could not be found.") + try: + title_information = r.json()["data"]["content"] + except json.JSONDecodeError: + raise ValueError(f"Failed to load title manifest: {r.text}") + + if title_information["type"] == "Movie": + return Title( + id_=self.title, + type_=Title.Types.MOVIE, + name=title_information["title"], + #year=datetime.utcfromtimestamp(title_information["releaseDate"] / 1000).year, + original_lang=title_information["originalSpokenLanguages"][0]["locale"], + source=self.ALIASES[0], + service_data=title_information + ) + elif title_information["type"] == "SportingEvent": + self.type = 2 + return Title( + id_=self.title, + type_=Title.Types.MOVIE, + name=title_information["title"], + #year=datetime.utcfromtimestamp(title_information["releaseDate"] / 1000).year, + #original_lang=title_information["originalSpokenLanguages"][0]["locale"], + source=self.ALIASES[0], + service_data=title_information + ) + else: + self.type = 0 + r = self.session.get( + url=self.config["endpoints"]["tv_episodes"].format(id=self.title), + params=self.params + ) + try: + episodes = r.json()["data"]["episodes"] + except json.JSONDecodeError: + raise ValueError(f"Failed to load episodes list: {r.text}") + + return [Title( + id_=self.title, + type_=Title.Types.TV, + name=episode["showTitle"], + season=episode["seasonNumber"], + episode=episode["episodeNumber"], + episode_name=episode.get("title"), + original_lang=title_information["originalSpokenLanguages"][0]["locale"], + source=self.ALIASES[0], + service_data=episode + ) for episode in episodes] + + def get_tracks(self, title): + self.params = { + 'utsk': '6e3013c6d6fae3c2::::::9318c17fb39d6b9c', + 'caller': 'web', + 'sf': self.storefront, + 'v': '82', + 'pfm': 'appletv', + 'mfr': 'Apple', + 'locale': 'en-US', + 'l': 'en', + 'ctx_brand': 'tvs.sbd.4000', + 'count': '100', + 'skip': '0', + } + r = self.session.get(url=self.config["endpoints"]["title"].format(type={0: "shows", 1: "movies", 2: "sporting-events"}[self.type], id=self.title), + params=self.params + ) + try: + stream_data = r.json() + #print(stream_data) + except json.JSONDecodeError: + raise ValueError(f"Failed to load stream data: {r.text}") + stream_data = stream_data["data"]["playables"] + if self.condensed == True: + tvs_sbd = list(stream_data.keys())[1] + else: + tvs_sbd = list(stream_data.keys())[0] + stream_data = stream_data[tvs_sbd] + if not stream_data["isEntitledToPlay"]: + raise self.log.exit(" - User is not entitled to play this title") + + self.extra_server_parameters = stream_data["assets"]["fpsKeyServerQueryParameters"] + + r = requests.get(url=stream_data["assets"]["hlsUrl"], headers={'User-Agent': 'AppleTV6,2/11.1'}) + res = r.text + + tracks = Tracks.from_m3u8( + master=m3u8.loads(res, r.url), + source=self.ALIASES[0] + ) + + for track in tracks: + track.extra = {"manifest": track.extra} + + quality = None + for line in res.splitlines(): + if line.startswith("#--"): + quality = {"SD": 480, "HD720": 720, "HD": 1080, "UHD": 2160}.get(line.split()[2]) + elif not line.startswith("#"): + track = next((x for x in tracks.videos if x.extra["manifest"].uri == line), None) + if track: + track.extra["quality"] = quality + + for track in tracks: + track_data = track.extra["manifest"] + #if isinstance(track, VideoTrack) and not tracks.subtitles: + # track.needs_ccextractor_first = True + if isinstance(track, VideoTrack): + track.encrypted = True + if isinstance(track, AudioTrack): + track.encrypted = True + bitrate = re.search(r"&g=(\d+?)&", track_data.uri) + if not bitrate: + bitrate = re.search(r"_gr(\d+)_", track_data.uri) # new + if bitrate: + track.bitrate = int(bitrate[1][-3::]) * 1000 # e.g. 128->128,000, 2448->448,000 + else: + raise ValueError(f"Unable to get a bitrate value for Track {track.id}") + track.codec = track.codec.replace("_vod", "") + if isinstance(track, TextTrack): + track.codec = "vtt" + + tracks.videos = [x for x in tracks.videos if (x.codec or "")[:3] in self.VIDEO_CODEC_MAP[self.vcodec]] + + if self.acodec: + tracks.audios = [ + x for x in tracks.audios if (x.codec or "").split("-")[0] in self.AUDIO_CODEC_MAP[self.acodec] + ] + + tracks.subtitles = [ + x for x in tracks.subtitles + if (x.language in self.alang or (x.is_original_lang and "orig" in self.alang) or "all" in self.alang) + or self.subs_only + or not x.sdh + ] + + try: + return Tracks([ + # multiple CDNs, only want one + x for x in tracks + if any( + cdn in as_list(x.url)[0].split("?")[1].split("&") for cdn in ["cdn=ak", "cdn=vod-ak-aoc.tv.apple.com"] + ) + ]) + except: + return Tracks([ + # multiple CDNs, only want one + x for x in tracks + #if any( + # cdn in as_list(x.url)[0].split("?")[1].split("&") for cdn in ["cdn=ak", "cdn=vod-ak-aoc.tv.apple.com"] + #) + ]) + + def get_chapters(self, title): + return [] + + def certificate(self, **_): + return None # will use common privacy cert + + def license(self, challenge, track, **_): + try: + res = self.session.post( + url=self.config["endpoints"]["license"], + json={ + 'streaming-request': { + 'version': 1, + 'streaming-keys': [ + { + "challenge": base64.b64encode(challenge.encode('utf-8')).decode('utf-8'), + "key-system": "com.microsoft.playready", + "uri": f"data:text/plain;charset=UTF-16;base64,{track.pssh}", + "id": 1, + "lease-action": 'start', + "adamId": self.extra_server_parameters['adamId'], + "isExternal": True, + "svcId": self.extra_server_parameters['svcId'], + }, + ], + }, + } + ).json() + except requests.HTTPError as e: + print(e) + if not e.response.text: + raise self.log.exit(" - No license returned!") + raise self.log.exit(f" - Unable to obtain license (error code: {e.response.json()['errorCode']})") + return res['streaming-response']['streaming-keys'][0]["license"] + + # Service specific functions + + def configure(self): + cc = self.session.cookies.get_dict()["itua"] + r = self.session.get("https://gist.githubusercontent.com/BrychanOdlum/2208578ba151d1d7c4edeeda15b4e9b1/raw/8f01e4a4cb02cf97a48aba4665286b0e8de14b8e/storefrontmappings.json").json() + for g in r: + if g['code'] == cc: + self.storefront = g['storefrontId'] + + environment = self.get_environment_config() + if not environment: + raise ValueError("Failed to get AppleTV+ WEB TV App Environment Configuration...") + self.session.headers.update({ + "User-Agent": self.config["user_agent"], + "Authorization": f"Bearer {environment['MEDIA_API']['token']}", + "media-user-token": self.session.cookies.get_dict()["media-user-token"], + "x-apple-music-user-token": self.session.cookies.get_dict()["media-user-token"] + }) + + def get_environment_config(self): + """Loads environment config data from WEB App's <meta> tag.""" + res = self.session.get("https://tv.apple.com").text + env = re.search(r'web-tv-app/config/environment"[\s\S]*?content="([^"]+)', res) + if not env: + return None + return json.loads(unquote(env[1])) diff --git a/vinetrimmer/services/appletvplus1.py b/vinetrimmer/services/appletvplus1.py new file mode 100644 index 0000000..81d3b4f --- /dev/null +++ b/vinetrimmer/services/appletvplus1.py @@ -0,0 +1,417 @@ +import click + +from base64 import b64encode, b64decode +from datetime import datetime, timedelta +from json import loads, JSONDecodeError +from m3u8 import loads as m3u8loads +from re import search +from requests import get, HTTPError +from typing import Any, Optional, Union +from urllib.parse import unquote + +from vinetrimmer.objects import Title, Tracks, VideoTrack, AudioTrack, TextTrack, MenuTrack # fmt: skip +from vinetrimmer.services.BaseService import BaseService + + + +class AppleTVPlus(BaseService): + """ + Service code for Apple's TV Plus streaming service (https://tv.apple.com). + + \b + WIP: decrypt and removal of bumper/dub cards + + \b + Authorization: Cookies + Security: + Playready: + SL150: Untested + SL2000: 1080p + SL3000: 2160p + + Widevine: + L1: 2160p + L2: Untested + L3 (Chrome): 540p + L3 (Android): 540p + """ + + ALIASES = ["ATVP", "appletvplus", "appletv+"] + + TITLE_RE = r"^(?:https?://tv\.apple\.com(?:/[a-z]{2})?/(?:movie|show|episode)/[a-z0-9-]+/)?(?P<id>umc\.cmc\.[a-z0-9]+)" # noqa: E501 + + + VIDEO_CODEC_MAP = { + "H264": ["avc"], + "H265": ["hvc", "hev", "dvh"] + } + AUDIO_CODEC_MAP = { + "AAC": ["HE", "stereo"], + "AC3": ["ac3"], + "EC3": ["ec3", "atmos"] + } + + @staticmethod + @click.command(name="AppleTVPlus", short_help="https://tv.apple.com") + @click.argument("title", type=str, required=False) + @click.pass_context + def cli(ctx, **kwargs): + return AppleTVPlus(ctx, **kwargs) + + def __init__(self, ctx, title: str) -> None: + super().__init__(ctx=ctx) + self.parse_title(ctx=ctx, title=title) + + self.acodec = ctx.parent.params["acodec"] + self.alang = ctx.parent.params["alang"] + self.subs_only = ctx.parent.params["subs_only"] + self.vcodec = ctx.parent.params["vcodec"] + self.range = ctx.parent.params["range_"] or "SDR" + self.quality = ctx.parent.params["quality"] or 1080 + + self.extra_server_parameters: Optional[dict] = None + + if ("HDR" in self.range) or ("DV" in self.range) or ((self.quality > 1080) if self.quality else False): + self.log.info(" - Setting Video codec to H265 to get UHD") + self.vcodec = "H265" + + self.configure() + + def get_titles(self) -> list[Title]: + titles = list() + + req = None + for i in range(2): + try: + req = self.session.get( + url=self.config["endpoints"]["title"].format(type={0: "shows", 1: "movies"}[i], id=self.title), + params=self.config["device"] + ) + + except HTTPError as error: + if error.response.status_code != 404: + raise + else: + if req.ok: + break + + if not req: + raise self.log.exit(f" - Title ID {self.title!r} could not be found.") + try: + title = req.json()["data"]["content"] + except json.JSONDecodeError: + raise ValueError(f"Failed to load title manifest: {r.text}") + + self.log.debug(title) + + if title["type"] == "Movie": + titles.append( + Title( + id_=self.title, + type_=Title.Types.MOVIE, + name=title["title"], + year=datetime.utcfromtimestamp(title["releaseDate"] / 1000).year, + original_lang=title["originalSpokenLanguages"][0]["locale"] if "originalSpokenLanguages" in title.keys() else "und", + source=self.ALIASES[0], + service_data=title, + ) + ) + + else: + req = self.session.get( + url=self.config["endpoints"]["tv_episodes"].format(id=self.title), + params=self.config["device"] + ) + + try: + episodes = req.json()["data"]["episodes"] + except JSONDecodeError: + raise ValueError(f"Failed to load episodes list: {req.text}") + + for episode in episodes: + titles.append( + Title( + id_=self.title, + type_=Title.Types.TV, + name=episode["showTitle"], + year=datetime.utcfromtimestamp(title["releaseDate"] / 1000).year, + season=episode["seasonNumber"], + episode=episode["episodeNumber"], + episode_name=episode.get("title"), + original_lang=title["originalSpokenLanguages"][0]["locale"] if "originalSpokenLanguages" in title.keys() else "und", + source=self.ALIASES[0], + service_data=episode, + ) + ) + + return titles + + def get_tracks(self, title: Title) -> Tracks: + tracks = Tracks() + + req = self.session.get( + url=self.config["endpoints"]["manifest"].format(id=title.service_data["id"]), + params=self.config["device"] + ) + + + try: + data = req.json() + except JSONDecodeError: + raise ValueError(f"Failed to load stream data: {req.text}") + + stream_data = data["data"]["content"]["playables"][0] + + if not stream_data["isEntitledToPlay"]: + self.log.debug(stream_data) + raise self.log.exit(" - User is not entitled to play this title") + + self.extra_server_parameters = stream_data["assets"]["fpsKeyServerQueryParameters"] + + self.log.debug(self.extra_server_parameters) + self.log.debug(stream_data["assets"]["hlsUrl"]) + + req = get( + url=stream_data["assets"]["hlsUrl"], + headers={"User-Agent": "AppleTV6,2/11.1"}, # 'ATVE/1.1 FireOS/6.2.6.8 build/4A93 maker/Amazon model/FireTVStick4K FW/NS6268/2315' + ) + + tracks.add( + Tracks.from_m3u8( + master=m3u8loads(content=req.text, uri=req.url), source=self.ALIASES[0] + ) + ) + + + for track in tracks: + track.extra = {"url": track.url, "manifest.xml": track.extra} + track_data = track.extra["manifest.xml"] + if isinstance(track, VideoTrack): + track.encrypted = True + track.needs_ccextractor_first = True + track.needs_proxy = False + + elif isinstance(track, AudioTrack): + track.encrypted = True + track.needs_proxy = False + bitrate = search(pattern=r"&g=(\d+?)&", string=track_data.uri ) + if not bitrate: + bitrate = re.search(r"_gr(\d+)_", track_data.uri) # new + if bitrate: + track.bitrate = int(bitrate[1][-3::]) * 1000 + + else: + raise ValueError(f"Unable to get a bitrate value for Track {track.id}") + + track.codec = track.codec.replace("_vod", "") + + elif isinstance(track, TextTrack): + track.codec = "vtt" + + quality = None + for line in req.text.splitlines(): + if line.startswith("#--"): + quality = {"SD": 480, "HD720": 720, "HD": 1080, "UHD": 2160}.get(line.split()[2]) + + elif not line.startswith("#"): + track = next( + (x for x in tracks.videos if x.extra["manifest.xml"].uri == line), None + ) + if track: + track.extra["quality"] = quality + + tracks.videos = [ + x + for x in tracks.videos + if (x.codec or "")[:3] in self.VIDEO_CODEC_MAP[self.vcodec] + ] + + if self.acodec: + tracks.audios = [ + x + for x in tracks.audios + if (x.codec or "").split("-")[0] in self.AUDIO_CODEC_MAP[self.acodec] + ] + + tracks.subtitles = [ + x + for x in tracks.subtitles + if ( + x.language in self.alang + or (x.is_original_lang and "orig" in self.alang) + or "all" in self.alang + ) + or self.subs_only + or not x.sdh + ] + + try: + return Tracks([ + # multiple CDNs, only want one + x for x in tracks + if any( + cdn in as_list(x.url)[0].split("?")[1].split("&") for cdn in ["cdn=ak", "cdn=vod-ak-aoc.tv.apple.com"] + ) + ]) + except: + return Tracks([x for x in tracks]) + + def get_chapters(self, title: Title) -> list[MenuTrack]: + chapters = list() + + return chapters + + def certificate(self, **_: Any) -> Optional[Union[str, bytes]]: + return None + + def license(self, challenge: bytes, track: Tracks, **_): + try: + req = self.session.post( + url=self.config["endpoints"]["license"], + json={ + "streaming-request": { + "version": 1, + "streaming-keys": [ + { + "challenge": b64encode(challenge.encode("UTF-8")).decode("UTF-8"), + "key-system": "com.microsoft.playready", + "uri": f"data:text/plain;charset=UTF-16;base64,{track.pssh}", + "id": 1, + "lease-action": "start", + "adamId": self.extra_server_parameters["adamId"], + "isExternal": True, + "svcId": self.extra_server_parameters["svcId"], + }, + ], + }, + }, + params=self.config["device"] + ) + + except HTTPError as error: + self.log.warn(e) + if not error.response.text: + raise self.log.exit(" - No License Returned!") + + error = { + -1001: "Invalid PSSH!", + -1002: "Title not Owned!", + -1021: "Insufficient Security!", + }.get(error.response.json()["errorCode"]) + + raise self.log.exit( + f" - Failed to Get License! -> Error Code : {error.response.json()['errorCode']}" + ) + + data = req.json() + + if data["streaming-response"]["streaming-keys"][0]["status"] != 0: + status = data["streaming-response"]["streaming-keys"][0]["status"] + error = { + -1001: "Invalid PSSH!", + -1002: "Title not Owned!", + -1021: "Insufficient Security!", + }.get(status) + + raise self.log.exit(f" - Failed to Get License! -> {error} ({status})") + + return b64decode( + data["streaming-response"]["streaming-keys"][0]["license"] + ).decode() + + def configure(self) -> None: + self.log.info(" + Logging into Apple TV+...") + environment = self.get_environment_config() + if not environment: + raise ValueError("Failed to get AppleTV+ WEB TV App Environment Configuration...") + self.session.headers.update({ + "User-Agent": self.config["user_agent"], + "Authorization": f"Bearer {environment['MEDIA_API']['token']}", + "media-user-token": self.session.cookies.get_dict()["media-user-token"], + "x-apple-music-user-token": self.session.cookies.get_dict()["media-user-token"] + }) + + def get_environment_config(self): + """Loads environment config data from WEB App's <meta> tag.""" + res = self.session.get("https://tv.apple.com").text + env = search(pattern = r'web-tv-app/config/environment"[\s\S]*?content="([^"]+)', string = res) + if not env: + raise ValueError( + "Failed to get AppleTV+ WEB TV App Environment Configuration..." + ) + return loads(unquote(env[1])) + + def scan(self, start: int, length: int) -> list: + + # poetry run vt dl -al en -sl en --selected --proxy http://192.168.0.99:9766 --keys -q 2160 -v H265 ATVP + # poetry run vt dl -al en -sl en --selected --proxy http://192.168.0.99:9766 --keys -q 2160 -v H265 -r DV ATVP + + urls = [] + params = self.config["device"] + params["utscf"] = "OjAAAAEAAAAAAAAAEAAAACMA" + params["nextToken"] = str(start) + + r = None + try: + r = self.session.get( + url=self.config["endpoints"]["homecanvas"], + params=params + ) + except requests.HTTPError as e: + if e.response.status_code != 404: + raise + + if not r: + raise self.log.exit(f" - Canvas endpoint errored out") + try: + shelves = r.json()["data"]["canvas"]["shelves"] + except json.JSONDecodeError: + raise ValueError(f"Failed to load title manifest: {r.text}") + + # TODO - Add check userisentitledtoplay before appending url + for shelf in shelves: + items = shelf["items"] + for item in items: + urls.append(item["url"]) + + url_regex = re.compile(r"^(?:https?://tv\.apple\.com(?:/[a-z]{2})?/(?P<type>movie|show|episode)/[a-z0-9-]+/)?(?P<id>umc\.cmc\.[a-z0-9]+)") + + for url in urls: + match = url_regex.match(url) + + if match: + # Extract the title type and ID + title_type = match.group("type") + "s" # None if not present + title_id = match.group("id") + + else: + continue + + r = None + try: + r = self.session.get( + url=self.config["endpoints"]["title"].format(type=title_type, id=title_id), + params=self.config["device"] + ) + except requests.HTTPError as e: + if e.response.status_code != 404: + raise + if not r: + raise self.log.exit(f" - Title ID {self.title!r} could not be found.") + try: + shelves = r.json()["data"]["canvas"]["shelves"] + except json.JSONDecodeError: + raise ValueError(f"Failed to load title manifest: {r.text}") + + for shelf in shelves: + if "uts.col.ContentRelated" in shelf["id"]: + items = shelf["items"] + for item in items: + if item["url"] not in urls: + # TODO - Add check userisentitledtoplay before appending url + urls.append(item["url"]) + + if len(urls) >= length: + break + + return urls \ No newline at end of file diff --git a/vinetrimmer/services/appletvplus2.py b/vinetrimmer/services/appletvplus2.py new file mode 100644 index 0000000..162125b --- /dev/null +++ b/vinetrimmer/services/appletvplus2.py @@ -0,0 +1,318 @@ +import base64 +import json +import re +from datetime import datetime +from urllib.parse import unquote + +import click +import m3u8 +import requests + +from vinetrimmer.objects import AudioTrack, TextTrack, Title, Tracks, VideoTrack +from vinetrimmer.services.BaseService import BaseService +from vinetrimmer.utils.collections import as_list +from vinetrimmer.vendor.pymp4.parser import Box + + +class AppleTVPlus(BaseService): + """ + Service code for Apple's TV Plus streaming service (https://tv.apple.com). + + \b + WIP: decrypt and removal of bumper/dub cards + + \b + Authorization: Cookies + Security: UHD@L1 FHD@L1 HD@L3 + """ + + ALIASES = ["ATVP", "appletvplus", "appletv+"] + TITLE_RE = r"^(?:https?://tv\.apple\.com(?:/[a-z]{2})?/(?:movie|show|episode)/[a-z0-9-]+/)?(?P<id>umc\.cmc\.[a-z0-9]+)" # noqa: E501 + + VIDEO_CODEC_MAP = { + "H264": ["avc"], + "H265": ["hvc", "hev", "dvh"] + } + AUDIO_CODEC_MAP = { + "AAC": ["HE", "stereo"], + "AC3": ["ac3"], + "EC3": ["ec3", "atmos"] + } + + @staticmethod + @click.command(name="AppleTVPlus", short_help="https://tv.apple.com") + @click.argument("title", type=str, required=False) + @click.option("-svcId", "--serviceid", type=str, default="tvs.vds.4055", help="Define ServiceID if needed.") + @click.option("-sf", "--storefront", type=int, default="143450", help="Define storefront int if needed.") + @click.pass_context + def cli(ctx, **kwargs): + return AppleTVPlus(ctx, **kwargs) + + def __init__(self, ctx, title, serviceid: bool, storefront): + super().__init__(ctx) + self.parse_title(ctx, title) + self.serviceid = serviceid + self.storefront = storefront + + self.vcodec = ctx.parent.params["vcodec"] + self.acodec = ctx.parent.params["acodec"] + self.alang = ctx.parent.params["alang"] + self.subs_only = ctx.parent.params["subs_only"] + + self.extra_server_parameters = None + + self.configure() + + def get_titles(self): + r = None + for i in range(2): + try: + self.params = { + 'utsk': '6e3013c6d6fae3c2::::::9318c17fb39d6b9c', + 'caller': 'web', + 'sf': self.storefront, + 'v': '46', + 'pfm': 'appletv', + 'mfr': 'Apple', + 'locale': 'en-US', + 'l': 'en', + 'ctx_brand': 'tvs.sbd.4000', + 'count': '100', + 'skip': '0', + 'svcId': 'tvs.vds.4105' + } + r = self.session.get( + url=self.config["endpoints"]["title"].format(type={0: "shows", 1: "movies"}[i], id=self.title), + params=self.params + ) + except requests.HTTPError as e: + if e.response.status_code != 404: + raise + else: + if r.ok: + break + if not r: + raise self.log.exit(f" - Title ID {self.title!r} could not be found.") + try: + title_information = r.json()["data"]["content"] + except json.JSONDecodeError: + raise ValueError(f"Failed to load title manifest: {r.text}") + + if title_information["type"] == "Movie": + return Title( + id_=self.title, + type_=Title.Types.MOVIE, + name=title_information["title"], + #year=datetime.utcfromtimestamp(title_information["releaseDate"] / 1000).year, + original_lang=title_information["originalSpokenLanguages"][0]["locale"], + source=self.ALIASES[0], + service_data=title_information + ) + else: + r = self.session.get( + url=self.config["endpoints"]["tv_episodes"].format(id=self.title), + params=self.params + ) + try: + episodes = r.json()["data"]["episodes"] + except json.JSONDecodeError: + raise ValueError(f"Failed to load episodes list: {r.text}") + + return [Title( + id_=self.title, + type_=Title.Types.TV, + name=episode["showTitle"], + season=episode["seasonNumber"], + episode=episode["episodeNumber"], + episode_name=episode.get("title"), + original_lang=title_information["originalSpokenLanguages"][0]["locale"], + source=self.ALIASES[0], + service_data=episode + ) for episode in episodes] + + def get_tracks(self, title): + self.params = { + 'utsk': '6e3013c6d6fae3c2::::::9318c17fb39d6b9c', + 'caller': 'web', + 'sf': self.storefront, + 'v': '46', + 'pfm': 'appletv', + 'mfr': 'Apple', + 'locale': 'en-US', + 'l': 'en', + 'ctx_brand': 'tvs.sbd.4000', + 'count': '100', + 'skip': '0', + 'svcId': 'tvs.vds.4105' + } + r = self.session.get( + url=self.config["endpoints"]["manifest"].format(id=title.service_data["id"]), + params=self.params + ) + try: + stream_data = r.json() + except json.JSONDecodeError: + raise ValueError(f"Failed to load stream data: {r.text}") + stream_data = stream_data["data"]["content"]["playables"][0] + + if not stream_data["isEntitledToPlay"]: + raise self.log.exit(" - User is not entitled to play this title") + + self.extra_server_parameters = stream_data["assets"]["fpsKeyServerQueryParameters"] + print(stream_data["assets"]["hlsUrl"]) + r = requests.get(url=stream_data["assets"]["hlsUrl"], headers={'User-Agent': 'AppleTV6,2/11.1'}) + res = r.text + + tracks = Tracks.from_m3u8( + master=m3u8.loads(res, r.url), + source=self.ALIASES[0] + ) + + for track in tracks: + track.extra = {"manifest": track.extra} + + quality = None + for line in res.splitlines(): + if line.startswith("#--"): + quality = {"SD": 480, "HD720": 720, "HD": 1080, "UHD": 2160}.get(line.split()[2]) + elif not line.startswith("#"): + track = next((x for x in tracks.videos if x.extra["manifest"].uri == line), None) + if track: + track.extra["quality"] = quality + + for track in tracks: + track_data = track.extra["manifest"] + #if isinstance(track, VideoTrack) and not tracks.subtitles: + # track.needs_ccextractor_first = True + if isinstance(track, VideoTrack): + track.encrypted = True + if isinstance(track, AudioTrack): + track.encrypted = True + bitrate = re.search(r"&g=(\d+?)&", track_data.uri) + if not bitrate: + bitrate = re.search(r"_gr(\d+)_", track_data.uri) # new + if bitrate: + track.bitrate = int(bitrate[1][-3::]) * 1000 # e.g. 128->128,000, 2448->448,000 + else: + raise ValueError(f"Unable to get a bitrate value for Track {track.id}") + track.codec = track.codec.replace("_vod", "") + if isinstance(track, TextTrack): + track.codec = "vtt" + + tracks.videos = [x for x in tracks.videos if (x.codec or "")[:3] in self.VIDEO_CODEC_MAP[self.vcodec]] + + if self.acodec: + tracks.audios = [ + x for x in tracks.audios if (x.codec or "").split("-")[0] in self.AUDIO_CODEC_MAP[self.acodec] + ] + + tracks.subtitles = [ + x for x in tracks.subtitles + if (x.language in self.alang or (x.is_original_lang and "orig" in self.alang) or "all" in self.alang) + or self.subs_only + or not x.sdh + ] + + try: + return Tracks([ + # multiple CDNs, only want one + x for x in tracks + if any( + cdn in as_list(x.url)[0].split("?")[1].split("&") for cdn in ["cdn=ak", "cdn=vod-ak-aoc.tv.apple.com"] + ) + ]) + except: + return Tracks([ + # multiple CDNs, only want one + x for x in tracks + #if any( + # cdn in as_list(x.url)[0].split("?")[1].split("&") for cdn in ["cdn=ak", "cdn=vod-ak-aoc.tv.apple.com"] + #) + ]) + + def get_chapters(self, title): + return [] + + def certificate(self, **_): + return None # will use common privacy cert + + def license(self, challenge, track, **_): + try: + res = self.session.post( + url=self.config["endpoints"]["license"], + json={ + 'streaming-request': { + 'version': 1, + 'streaming-keys': [ + { + "challenge": base64.b64encode(challenge.encode('utf-8')).decode('utf-8'), + "key-system": "com.microsoft.playready", + "uri": f"data:text/plain;charset=UTF-16;base64,{track.pssh}", + "id": 1, + "lease-action": 'start', + "adamId": self.extra_server_parameters['adamId'], + "isExternal": True, + 'svcId': self.extra_server_parameters["svcId"] + }, + ], + }, + } + ).json() + except requests.HTTPError as e: + print(e) + if not e.response.text: + raise self.log.exit(" - No license returned!") + raise self.log.exit(f" - Unable to obtain license (error code: {e.response.json()['errorCode']})") + if "license" not in res['streaming-response']['streaming-keys'][0]: + print("Custom license") + try: + res = self.session.post( + url=self.config["endpoints"]["license"], + json={ + 'streaming-request': { + 'version': 1, + 'streaming-keys': [ + { + "challenge": base64.b64encode(challenge.encode('utf-8')).decode('utf-8'), + "key-system": "com.microsoft.playready", + "uri": f"data:text/plain;charset=UTF-16;base64,{track.pssh}", + "id": 1, + "lease-action": 'start', + "adamId": self.extra_server_parameters['adamId'], + "isExternal": True, + "svcId": self.extra_server_parameters["svcId"] + }, + ], + }, + } + ).json() + self.log.debug(res) + except requests.HTTPError as e: + print(e) + if not e.response.text: + raise self.log.exit(" - No license returned!") + raise self.log.exit(f" - Unable to obtain license (error code: {e.response.json()['errorCode']})") + return res['streaming-response']['streaming-keys'][0]["license"] + return res['streaming-response']['streaming-keys'][0]["license"] + + + # Service specific functions + + def configure(self): + environment = self.get_environment_config() + if not environment: + raise ValueError("Failed to get AppleTV+ WEB TV App Environment Configuration...") + self.session.headers.update({ + "User-Agent": self.config["user_agent"], + "Authorization": f"Bearer {environment['MEDIA_API']['token']}", + "media-user-token": self.session.cookies.get_dict()["media-user-token"], + "x-apple-music-user-token": self.session.cookies.get_dict()["media-user-token"] + }) + + def get_environment_config(self): + """Loads environment config data from WEB App's <meta> tag.""" + res = self.session.get("https://tv.apple.com").text + env = re.search(r'web-tv-app/config/environment"[\s\S]*?content="([^"]+)', res) + if not env: + return None + return json.loads(unquote(env[1])) diff --git a/vinetrimmer/services/max.py b/vinetrimmer/services/max.py new file mode 100644 index 0000000..5f80c38 --- /dev/null +++ b/vinetrimmer/services/max.py @@ -0,0 +1,507 @@ +import json +import os.path +import re +import sys +import time +import uuid +from datetime import datetime, timedelta +from hashlib import md5 + +import click +import httpx +import isodate +import requests +import xmltodict +from langcodes import Language + +from vinetrimmer.objects import TextTrack, Title, Tracks, VideoTrack +from vinetrimmer.objects.tracks import AudioTrack, MenuTrack +from vinetrimmer.services.BaseService import BaseService +from vinetrimmer.utils import is_close_match, short_hash, try_get + + +class Max(BaseService): + """ + Service code for MAX's streaming service (https://max.com). + + \b + Authorization: Cookies + Security: UHD@L1 FHD@L1 HD@L3 + """ + + ALIASES = ["MAX", "max"] + TITLE_RE = r"^(?:https?://(?:www\.|play\.)?max\.com/)?(?P<type>[^/]+)/(?P<id>[^/]+)" + + VIDEO_CODEC_MAP = { + "H264": ["avc1"], + "H265": ["hvc1", "dvh1"] + } + + AUDIO_CODEC_MAP = { + "AAC": "mp4a", + "AC3": "ac-3", + "EC3": "ec-3" + } + + @staticmethod + @click.command(name="Max", short_help="https://max.com") + @click.argument("title", type=str, required=False) + # @click.option("-m", "--movie", is_flag=True, default=False, help="Title is a movie.") + @click.pass_context + def cli(ctx, **kwargs): + return Max(ctx, **kwargs) + + def __init__(self, ctx, title): + super().__init__(ctx) + self.title = self.parse_title(ctx, title) + # self.movie = movie + + # self.cdm = ctx.obj.cdm + + self.vcodec = ctx.parent.params["vcodec"] + self.acodec = ctx.parent.params["acodec"] + self.range = ctx.parent.params["range_"] + self.alang = ctx.parent.params["alang"] + # self.api_region = self.config.get(ctx.obj.profile, {}).get('api_region', 'comet-latam') + + # self.license_api = None + # self.client_grant = None + # self.auth_grant = None + # self.profile_id = None + # self.entitlements = None + + + if self.range == 'HDR10' or self.range == 'DV' or self.quality > 1080: + self.log.info(" + Setting VideoCodec to H265 to be able to get 2160p video track") + self.vcodec = "H265" + + self.configure() + + def get_titles(self): + content_type = self.title['type'] + external_id = self.title['id'] + + response = self.session.get( + f"https://default.any-any.prd.api.max.com/cms/routes/{content_type}/{external_id}?include=default", + ) + + content_data = [x for x in response.json()["included"] if "attributes" in x and "title" in + x["attributes"] and x["attributes"]["alias"] == "generic-%s-blueprint-page" % (re.sub(r"-", "", content_type))][0]["attributes"] + content_title = content_data["title"] + + #if content_type == "movie": + if content_type == "movie" or content_type == "standalone": + metadata = self.session.get( + url=f"https://default.any-any.prd.api.max.com/content/videos/{external_id}/activeVideoForShow?&include=edit" + ).json()['data'] + + release_date = metadata["attributes"].get("airDate") or metadata["attributes"].get("firstAvailableDate") + year = datetime.strptime(release_date, '%Y-%m-%dT%H:%M:%SZ').year + return Title( + id_=external_id, + type_=Title.Types.MOVIE, + name=content_title, + year=year, + # original_lang=, + source=self.ALIASES[0], + service_data=metadata, + ) + + if content_type == "show" or content_type == "mini-series": + episodes = [] + if content_type == "mini-series": + alias = "generic-miniseries-page-rail-episodes" + else: + alias = "generic-%s-page-rail-episodes-tabbed-content" % (content_type) + + included_dt = response.json()["included"] + season_data = [data for included in included_dt for key, data in included.items() + if key == "attributes" for k,d in data.items() if d == alias][0] + season_data = season_data["component"]["filters"][0] + + seasons = [int(season["value"]) for season in season_data["options"]] + + season_parameters = [(int(season["value"]), season["parameter"]) for season in season_data["options"] + for season_number in seasons if int(season["id"]) == int(season_number)] + + if not season_parameters: + raise self.log.exit("season(s) %s not found") + + data_paginas = self.session.get(url="https://default.any-any.prd.api.max.com/cms/collections/generic-show-page-rail-episodes-tabbed-content?include=default&pf[show.id]=%s" % (external_id)).json() + total_pages = data_paginas['data']['meta']['itemsTotalPages'] + + for pagina in range(1, total_pages + 1): + for (value, parameter) in season_parameters: + data = self.session.get(url="https://default.any-any.prd.api.max.com/cms/collections/generic-show-page-rail-episodes-tabbed-content?include=default&pf[show.id]=%s&%s&page[items.number]=%s" % (external_id, parameter, pagina)).json() + try: + episodes_dt = sorted([dt for dt in data["included"] if "attributes" in dt and "videoType" in + dt["attributes"] and dt["attributes"]["videoType"] == "EPISODE" + and int(dt["attributes"]["seasonNumber"]) == int(value)], key=lambda x: x["attributes"]["episodeNumber"]) + except KeyError: + raise self.log.exit("season episodes were not found") + + episodes.extend(episodes_dt) + + titles = [] + release_date = episodes[0]["attributes"].get("airDate") or episodes[0]["attributes"].get("firstAvailableDate") + year = datetime.strptime(release_date, '%Y-%m-%dT%H:%M:%SZ').year + + for episode in episodes: + titles.append( + Title( + id_=episode['id'], + type_=Title.Types.TV, + name=content_title, + year=year, + season=episode['attributes']['seasonNumber'], + episode=episode['attributes']['episodeNumber'], + episode_name=episode['attributes']['name'], + # original_lang=edit.get('originalAudioLanguage'), + source=self.ALIASES[0], + service_data=episode + ) + ) + + return titles + + def get_tracks(self, title: Title): + edit_id = title.service_data['relationships']['edit']['data']['id'] + + response = self.session.post( + url=self.config['endpoints']['playbackInfo'], + json={ + "appBundle": "com.wbd.stream", + "applicationSessionId": str(uuid.uuid4()), + "capabilities": { + "codecs": { + "audio": { + "decoders": [ + { + "codec": "eac3", + "profiles": [ + "lc", + "he", + "hev2", + "xhe", + 'atmos', + ] + }, + { + 'codec': 'ac3', + 'profiles': [] + } + ] + }, + "video": { + "decoders": [ + { + "codec": "h264", + "levelConstraints": { + "framerate": { + "max": 960, + "min": 0 + }, + "height": { + "max": 2200, + "min": 64 + }, + "width": { + "max": 3900, + "min": 64 + } + }, + "maxLevel": "6.2", + "profiles": [ + "baseline", + "main", + "high" + ] + }, + { + "codec": "h265", + "levelConstraints": { + "framerate": { + "max": 960, + "min": 0 + }, + "height": { + "max": 2200, + "min": 144 + }, + "width": { + "max": 3900, + "min": 144 + } + }, + "maxLevel": "6.2", + "profiles": [ + "main", + "main10" + ] + } + ], + "hdrFormats": [ + 'dolbyvision8', 'dolbyvision5', 'dolbyvision', + 'hdr10plus', 'hdr10', 'hlg' + ] + } + }, + "contentProtection": { + "contentDecryptionModules": [ + { + "drmKeySystem": 'playready', + "maxSecurityLevel": 'sl2000', + + } + ] + }, + "devicePlatform": { + "network": { + "capabilities": { + "protocols": { + "http": {"byteRangeRequests": True} + } + }, + "lastKnownStatus": {"networkTransportType": "wifi"} + }, + "videoSink": { + "capabilities": { + "colorGamuts": ["standard"], + "hdrFormats": [] + }, + "lastKnownStatus": { + "height": 2200, + "width": 3900 + } + } + }, + "manifests": {"formats": {"dash": {}}} + }, + "consumptionType": "streaming", + "deviceInfo": { + "browser": { + "name": "Discovery Player Android androidTV", + "version": "1.8.1-canary.102" + }, + "deviceId": "", + "deviceType": "androidtv", + "make": "NVIDIA", + "model": "SHIELD Android TV", + "os": { + "name": "ANDROID", + "version": "10" + }, + "platform": "android", + "player": { + "mediaEngine": { + "name": "exoPlayer", + "version": "1.2.1" + }, + "playerView": { + "height": 2160, + "width": 3840 + }, + "sdk": { + "name": "Discovery Player Android androidTV", + "version": "1.8.1-canary.102" + } + } + }, + "editId": edit_id, + "firstPlay": True, + "gdpr": False, + "playbackSessionId": str(uuid.uuid4()), + "userPreferences": {"uiLanguage": "en"} + } + ) + + playback_data = response.json() + + # TEST + video_info = next(x for x in playback_data['videos'] if x['type'] == 'main') + title.original_lang = Language.get(video_info['defaultAudioSelection']['language']) + + fallback_url = playback_data["fallback"]["manifest"]["url"] + + try: + self.license_url = playback_data["drm"]["schemes"]["playready"]["licenseUrl"] + #self.log.info('DID NOT FIND LICENSE URL') + drm_protection_enabled = True + except (KeyError, IndexError): + drm_protection_enabled = False + + manifest_url = fallback_url.replace('_fallback', '') + + tracks: Tracks = Tracks.from_mpd( + url=manifest_url, + source=self.ALIASES[0] + ) + # remove partial subs + tracks.subtitles.clear() + + subtitles = self.get_subtitles(manifest_url, fallback_url) + + subs = [] + for subtitle in subtitles: + subs.append( + TextTrack( + id_=md5(subtitle["url"].encode()).hexdigest(), + source=self.ALIASES[0], + url=subtitle["url"], + codec=subtitle['format'], + language=subtitle["language"], + forced=subtitle['name'] == 'Forced', + sdh=subtitle['name'] == 'SDH' + ) + ) + + tracks.add(subs) + + if self.vcodec: + tracks.videos = [x for x in tracks.videos if (x.codec or "")[:4] in self.VIDEO_CODEC_MAP[self.vcodec]] + + if self.acodec: + tracks.audios = [x for x in tracks.audios if (x.codec or "")[:4] == self.AUDIO_CODEC_MAP[self.acodec]] + + for track in tracks: + track.needs_proxy = False + if isinstance(track, VideoTrack): + codec = track.extra[0].get("codecs") + supplementalcodec = track.extra[0].get("{urn:scte:dash:scte214-extensions}supplementalCodecs") or "" + #track.hdr10 = codec[0:4] in ("hvc1", "hev1") and codec[5] == "2" + track.hdr10 = codec[0:4] in ("dvh1", "dvhe") or supplementalcodec[0:4] in ("dvh1", "dvhe") + track.dv = codec[0:4] in ("dvh1", "dvhe") or supplementalcodec[0:4] in ("dvh1", "dvhe") + if isinstance(track, TextTrack) and track.codec == "": + track.codec = "webvtt" + + title.service_data['info'] = video_info + + return tracks + + def get_chapters(self, title: Title): + chapters = [] + video_info = title.service_data['info'] + if 'annotations' in video_info: + chapters.append(MenuTrack(number=1, title='Chapter 1', timecode='00:00:00.0000')) + chapters.append(MenuTrack(number=2, title='Credits', timecode=self.convert_timecode(video_info['annotations'][0]['start']))) + chapters.append(MenuTrack(number=3, title='Chapter 2', timecode=self.convert_timecode(video_info['annotations'][0]['end']))) + + return chapters + + def certificate(self, challenge, **_): + return self.license(challenge) + + def license(self, challenge, **_): + if isinstance(challenge, str): challenge = bytes(challenge, "utf-8") + return self.session.post( + url=self.license_url, + data=challenge # expects bytes + ).content + + def configure(self): + token = self.session.cookies.get_dict()["st"] + device_id = json.loads(self.session.cookies.get_dict()["session"]) + self.session.headers.update({ + 'User-Agent': 'BEAM-Android/5.0.0 (motorola/moto g(6) play)', + 'Accept': 'application/json, text/plain, */*', + 'Content-Type': 'application/json', + 'x-disco-client': 'ANDROID:9:beam:5.0.0', + 'x-disco-params': 'realm=bolt,bid=beam,features=ar,rr', + 'x-device-info': 'BEAM-Android/5.0.0 (motorola/moto g(6) play; ANDROID/9; 9cac27069847250f/b6746ddc-7bc7-471f-a16c-f6aaf0c34d26)', + 'traceparent': '00-053c91686df1e7ee0b0b0f7fda45ee6a-f5a98d6877ba2515-01', + 'tracestate': f'wbd=session:{device_id}', + 'Origin': 'https://play.max.com', + 'Referer': 'https://play.max.com/', + }) + + auth_token = self.get_device_token() + self.session.headers.update({ + "x-wbd-session-state": auth_token + }) + + def get_device_token(self): + response = self.session.post( + 'https://default.any-any.prd.api.max.com/session-context/headwaiter/v1/bootstrap', + ) + response.raise_for_status() + + return response.headers.get('x-wbd-session-state') + + @staticmethod + def convert_timecode(time): + secs, ms = divmod(time, 1) + mins, secs = divmod(secs, 60) + hours, mins = divmod(mins, 60) + ms = ms * 10000 + chapter_time = '%02d:%02d:%02d.%04d' % (hours, mins, secs, ms) + + return chapter_time + + def get_subtitles(self, mpd_url, fallback_url): + base_url = "/".join(fallback_url.split("/")[:-1]) + "/" + xml = xmltodict.parse(requests.get(mpd_url).text) + + try: + tracks = xml["MPD"]["Period"][0]["AdaptationSet"] + except KeyError: + tracks = xml["MPD"]["Period"]["AdaptationSet"] + + subs_tracks_js = [] + for subs_tracks in tracks: + if subs_tracks['@contentType'] == 'text': + for x in self.force_instance(subs_tracks, "Representation"): + try: + path = re.search(r'(t/\w+/)', x["SegmentTemplate"]["@media"])[1] + except AttributeError: + path = 't/sub/' + + is_sdh = False + text = "" + if subs_tracks["Role"]["@value"] == "caption": + #url = base_url + path + subs_tracks['@lang'] + '_cc.vtt' + url = base_url + path + subs_tracks['@lang'] + ('_sdh.vtt' if 'sdh' in subs_tracks["Label"].lower() else '_cc.vtt') + is_sdh = True + text = " (SDH)" + + is_forced = False + text = "" + if subs_tracks["Role"]["@value"] == "forced-subtitle": + url = base_url + path + subs_tracks['@lang'] + '_forced.vtt' + text = " (Forced)" + is_forced = True + + if subs_tracks["Role"]["@value"] == "subtitle": + url = base_url + path + subs_tracks['@lang'] + '_sub.vtt' + + subs_tracks_js.append({ + "url": url, + "format": "vtt", + "language": subs_tracks["@lang"], + "languageDescription": Language.make(language=subs_tracks["@lang"].split('-')[0]).display_name() + text, + "name": "SDH" if is_sdh else "Forced" if is_forced else "Full", + }) + + subs_tracks_js = self.remove_dupe(subs_tracks_js) + + return subs_tracks_js + + @staticmethod + def force_instance(data, variable): + if isinstance(data[variable], list): + X = data[variable] + else: + X = [data[variable]] + return X + + @staticmethod + def remove_dupe(items): + valores_chave = set() + new_items = [] + + for item in items: + valor = item['url'] + if valor not in valores_chave: + new_items.append(item) + valores_chave.add(valor) + + return new_items diff --git a/vinetrimmer/services/netflix.py b/vinetrimmer/services/netflix.py new file mode 100644 index 0000000..7bd0f74 --- /dev/null +++ b/vinetrimmer/services/netflix.py @@ -0,0 +1,771 @@ +import base64 +import json +import os +import re +import sys +import time +from datetime import timedelta + +import click +import jsonpickle +import requests +from langcodes import Language + +from vinetrimmer.objects import AudioTrack, MenuTrack, TextTrack, Title, Tracks, VideoTrack +from vinetrimmer.services.BaseService import BaseService +from vinetrimmer.utils.collections import as_list, flatten +from vinetrimmer.utils.MSL import MSL +from vinetrimmer.utils.MSL.schemes import KeyExchangeSchemes +from vinetrimmer.utils.MSL.schemes.UserAuthentication import UserAuthentication +from pywidevine.device import DeviceTypes +from vinetrimmer.vendor.pymp4.parser import Box +from vinetrimmer.utils.gen_esn import chrome_esn_generator + +class Netflix(BaseService): + """ + Service code for the Netflix streaming service (https://netflix.com). + + \b + Authorization: Cookies if ChromeCDM, Cookies + Credentials otherwise. + Security: UHD@L1 HD@L3*, heavily monitors UHD, but doesn't seem to care about <= FHD. + + *MPL: FHD with Android L3, sporadically available with ChromeCDM + HPL: 1080p with ChromeCDM, 720p/1080p with other L3 (varies per title) + + \b + Tips: - The library of contents as well as regional availability is available at https://unogs.com + However, Do note that Netflix locked everyone out of being able to automate the available data + meaning the reliability and amount of information may be reduced. + - You could combine the information from https://unogs.com with https://justwatch.com for further data + - The ESN you choose is important to match the CDM you provide + - Need 4K manifests? Try use an Nvidia Shield-based ESN with the system ID changed to yours. The "shield" + term gives it 4K, and the system ID passes the key exchange verifications as it matches your CDM. They + essentially don't check if the device is actually a Shield by the verified system ID. + - ESNs capable of 4K manifests can provide HFR streams for everything other than H264. Other ESNs can + seemingly get HFR from the VP9 P2 profile or higher. I don't think H264 ever gets HFR. + + TODO: Implement the MSL v2 API response's `crop_x` and `crop_y` values with Matroska's cropping metadata + """ + + ALIASES = ["NF", "netflix"] + #GEOFENCE = ['us'] + TITLE_RE = [ + r"^(?:https?://(?:www\.)?netflix\.com(?:/[a-z0-9]{2})?/(?:title/|watch/|.+jbv=))?(?P<id>\d+)", + r"^https?://(?:www\.)?unogs\.com/title/(?P<id>\d+)", + ] + + NF_LANG_MAP = { + "es": "es-419", + "pt": "pt-PT", + } + + @staticmethod + @click.command(name="Netflix", short_help="https://netflix.com") + @click.argument("title", type=str, required=False) + @click.option("-p", "--profile", type=click.Choice(["MPL", "HPL", "QC", "MPL+HPL", "MPL+HPL+QC", "MPL+QC"], case_sensitive=False), + default="MPL+HPL+QC", + help="H.264 profile to use. Default is best available.") + @click.option("--meta-lang", type=str, help="Language to use for metadata") + @click.option("--single", is_flag=True, default=False, help="Single title mode. Must use for trailers.") + @click.pass_context + def cli(ctx, **kwargs): + return Netflix(ctx, **kwargs) + + def __init__(self, ctx, title, profile, meta_lang, single): + super().__init__(ctx) + self.parse_title(ctx, title) + self.profile = profile + self.meta_lang = meta_lang + self.single = single + + if ctx.parent.params["proxy"] and len("".join(i for i in ctx.parent.params["proxy"] if not i.isdigit())) == 2: + self.GEOFENCE.append(ctx.parent.params["proxy"]) + + self.vcodec = ctx.parent.params["vcodec"] + self.acodec = ctx.parent.params["acodec"] + self.range = ctx.parent.params["range_"] + self.quality = ctx.parent.params["quality"] + self.audio_only = ctx.parent.params["audio_only"] + self.subs_only = ctx.parent.params["subs_only"] + self.chapters_only = ctx.parent.params["chapters_only"] + + self.cdm = ctx.obj.cdm + + # General + self.download_proxied = len(self.GEOFENCE) > 0 # needed if the title is unavailable at home IP + self.profiles = [] + + # MSL + self.msl = None + self.esn = None + self.userauthdata = None + + # Web API values + self.react_context = {} + + # DRM/Manifest values + self.session_id = None + + self.configure() + + def get_titles(self): + metadata = self.get_metadata(self.title)["video"] + if metadata["type"] == "movie" or self.single: + titles = [Title( + id_=self.title, + type_=Title.Types.MOVIE, + name=metadata["title"], + year=metadata["year"], + source=self.ALIASES[0], + service_data=metadata + )] + else: + episodes = [episode for season in [ + [dict(x, **{"season": season["seq"]}) for x in season["episodes"]] + for season in metadata["seasons"] + ] for episode in season] + titles = [Title( + id_=self.title, + type_=Title.Types.TV, + name=metadata["title"], + season=episode.get("season"), + episode=episode.get("seq"), + episode_name=episode.get("title"), + source=self.ALIASES[0], + service_data=episode + ) for episode in episodes] + + # TODO: Get original language without making an extra manifest.xml request + self.log.warning("HEVC PROFILES for the first title sometimes FAIL with Validation error so we use H264 HPL as a first trial, if it does not exist, we try H264 MPL") + try: + manifest = self.get_manifest(titles[0], self.profiles) + except: + try: + manifest = self.get_manifest(titles[0], self.config["profiles"]["video"]["H264"]["HPL"]) + except: + manifest = self.get_manifest(titles[0], self.config["profiles"]["video"]["H264"]["MPL"]) + + + original_language = self.get_original_language(manifest) + + for title in titles: + + title.original_lang = original_language + + return titles + + def get_tracks(self, title): + if self.vcodec == "H264": + # If H.264, get both MPL and HPL tracks as they alternate in terms of bitrate + tracks = Tracks() + + self.config["profiles"]["video"]["H264"]["MPL+HPL+QC"] = ( + self.config["profiles"]["video"]["H264"]["MPL"] + self.config["profiles"]["video"]["H264"]["HPL"] + self.config["profiles"]["video"]["H264"]["QC"] + ) + + if self.audio_only or self.subs_only or self.chapters_only: + profiles = ["MPL+HPL+QC"] + else: + profiles = self.profile.split("+") + + for profile in profiles: + try: + manifest = self.get_manifest(title, self.config["profiles"]["video"]["H264"][profile]) + except: + manifest = self.get_manifest(title, self.config["profiles"]["video"]["H264"]["MPL"] + self.config["profiles"]["video"]["H264"]["HPL"]) + manifest_tracks = self.manifest_as_tracks(manifest) + license_url = manifest["links"]["license"]["href"] + + + if self.cdm.device.security_level == 3 and self.cdm.device.type == DeviceTypes.ANDROID: + max_quality = max(x.height for x in manifest_tracks.videos) + if profile == "MPL" and max_quality >= 720: + manifest_sd = self.get_manifest(title, self.config["profiles"]["video"]["H264"]["BPL"]) + license_url_sd = manifest_sd["links"]["license"]["href"] + if "SD_LADDER" in manifest_sd["video_tracks"][0]["streams"][0]["tags"]: + # SD manifest.xml is new encode encrypted with different keys that won't work for HD + continue + license_url = license_url_sd + if profile == "HPL" and max_quality >= 1080: + if "SEGMENT_MAP_2KEY" in manifest["video_tracks"][0]["streams"][0]["tags"]: + # 1080p license restricted from Android L3, 720p license will work for 1080p + manifest_720 = self.get_manifest( + title, [x for x in self.config["profiles"]["video"]["H264"]["HPL"] if "l40" not in x] + ) + license_url = manifest_720["links"]["license"]["href"] + else: + # Older encode, can't use 720p keys for 1080p + continue + + for track in manifest_tracks: + if track.encrypted: + track.extra["license_url"] = license_url + tracks.add(manifest_tracks, warn_only=True) + return tracks + + elif self.vcodec == "H265": + # If H.264, get both MPL and HPL tracks as they alternate in terms of bitrate + tracks = Tracks() + + if self.range == "SDR": + self.profile = "SDR_DO+SDR_CENC" + + self.config["profiles"]["video"]["H265"]["SDR_DO+SDR_CENC"] = ( + self.config["profiles"]["video"]["H265"]["SDR_DO"] + self.config["profiles"]["video"]["H265"]["SDR_CENC"] + ) + elif self.range == "HDR10": + + self.profile = "HDR10_DO+HDR10_CENC" + + self.config["profiles"]["video"]["H265"]["HDR10_DO+HDR10_CENC"] = ( + self.config["profiles"]["video"]["H265"]["HDR10_DO"] + self.config["profiles"]["video"]["H265"]["HDR10_CENC"] + ) + else: + self.log.error("Dolby Vision is not supported yes") + sys.exit(1) + + + profiles = self.profile.split("+") + self.log.debug(profiles) + + for profile in profiles: + manifest = self.get_manifest(title, self.config["profiles"]["video"]["H265"][profile]) + + manifest_tracks = self.manifest_as_tracks(manifest) + license_url = manifest["links"]["license"]["href"] + + for track in manifest_tracks: + if track.encrypted: + track.extra["license_url"] = license_url + tracks.add(manifest_tracks, warn_only=True) + return tracks + else: + manifest = self.get_manifest(title, self.profiles) + manifest_tracks = self.manifest_as_tracks(manifest) + license_url = manifest["links"]["license"]["href"] + for track in manifest_tracks: + if track.encrypted: + track.extra["license_url"] = license_url + if isinstance(track, VideoTrack): + # TODO: Needs something better than this + track.hdr10 = track.codec.split("-")[1] == "hdr" # hevc-hdr, vp9-hdr + track.dv = track.codec.startswith("hevc-dv") + return manifest_tracks + + def get_chapters(self, title): + metadata = self.get_metadata(title.id)["video"] + + if metadata["type"] == "movie" or self.single: + episode = metadata + else: + season = next(x for x in metadata["seasons"] if x["seq"] == title.season) + episode = next(x for x in season["episodes"] if x["seq"] == title.episode) + + if not (episode.get("skipMarkers") and episode.get("creditsOffset")): + return [] + + chapters = {} + for item in episode["skipMarkers"]: + chapters[item] = {"start": 0, "end": 0} + if not episode["skipMarkers"][item]: + continue + if episode["skipMarkers"][item]["start"] is None: + chapters[item]["start"] = 0 + else: + chapters[item]["start"] = (episode["skipMarkers"][item]["start"] / 1000) + if episode["skipMarkers"][item]["end"] is None: + chapters[item]["end"] = 0 + else: + chapters[item]["end"] = (episode["skipMarkers"][item]["end"] / 1000) + + cc, intro = 1, 0 + chaps = [MenuTrack( + number=1, + title=f"Part {cc:02}", + timecode="0:00:00.000", + )] + + for item in chapters: + if chapters[item]["start"] != 0: + if intro == 0: + cc += 1 + chaps.append(MenuTrack( + number=cc, + title="Intro", + timecode=(str(timedelta(seconds=int(chapters[item]["start"] - 1))) + ".500")[:11], + )) + cc += 1 + chaps.append(MenuTrack( + number=cc, + title=f"Part {(cc - 1):02}", + timecode=(str(timedelta(seconds=int(chapters[item]["end"]))) + ".250")[:11], + )) + else: + cc += 1 + chaps.append(MenuTrack( + number=cc, + title=f"Part {cc:02}", + timecode=(str(timedelta(seconds=int(chapters[item]["start"] - 1))) + ".500")[:11], + )) + cc += 1 + chaps.append(MenuTrack( + number=cc, + title=f"Part {cc:02}", + timecode=(str(timedelta(seconds=int(chapters[item]["end"]))) + ".250")[:11], + )) + cc += 1 + + if cc == 1: + chaps.append(MenuTrack( + number=2, + title="Credits", + timecode=(str(timedelta(seconds=int(episode["creditsOffset"] - 1))) + ".450")[:11], + )) + else: + chaps.append(MenuTrack( + number=cc, + title="Credits", + timecode=(str(timedelta(seconds=int(episode["creditsOffset"] - 1))) + ".450")[:11], + )) + + return chaps + + def certificate(self, **_): + return self.config["certificate"] + + def license(self, challenge, track, session_id, **_): + if not self.msl: + raise self.log.exit(" - Cannot get license, MSL client has not been created yet.") + header, payload_data = self.msl.send_message( + endpoint=self.config["endpoints"]["licence"], + params={}, + application_data={ + "version": 2, + "url": track.extra["license_url"], + "id": int(time.time() * 10000), + "esn": self.esn, + "languages": ["en-US"], + "uiVersion": self.react_context["serverDefs"]["data"]["uiVersion"], + "clientVersion": "6.0026.291.011", + "params": [{ + "sessionId": base64.b64encode(session_id).decode("utf-8"), + "clientTime": int(time.time()), + "challengeBase64": base64.b64encode(challenge).decode("utf-8"), + "xid": str(int((int(time.time()) + 0.1612) * 1000)), + }], + "echo": "sessionId" + }, + userauthdata=self.userauthdata + ) + if not payload_data: + raise self.log.exit(f" - Failed to get license: {header['message']} [{header['code']}]") + if "error" in payload_data[0]: + error = payload_data[0]["error"] + error_display = error.get("display") + error_detail = re.sub(r" \(E3-[^)]+\)", "", error.get("detail", "")) + + if error_display: + self.log.critical(f" - {error_display}") + if error_detail: + self.log.critical(f" - {error_detail}") + + if not (error_display or error_detail): + self.log.critical(f" - {error}") + + sys.exit(1) + + return payload_data[0]["licenseResponseBase64"] + + # Service specific functions + + def configure(self): + self.session.headers.update({"Origin": "https://netflix.com"}) + self.profiles = self.get_profiles() + self.log.info("Initializing a Netflix MSL client") + # Grab ESN based on CDM from secrets if no ESN argument provided + if self.cdm.device.type == DeviceTypes.CHROME: # ESN GENERATOR FOR CHROME + self.esn = chrome_esn_generator() + else: + sel.log.info(self.config) + esn_map = self.config.get("esn_map", {}) + self.log.info(esn_map) + self.esn = esn_map.get(self.cdm.device.system_id) or esn_map.get(str(self.cdm.device.system_id)) + if not self.esn: + raise self.log.exit(" - No ESN specified") + self.log.info(f" + ESN: {self.esn}") + scheme = { + DeviceTypes.CHROME: KeyExchangeSchemes.AsymmetricWrapped, + DeviceTypes.ANDROID: KeyExchangeSchemes.Widevine + }[self.cdm.device.type] + self.log.info(f" + Scheme: {scheme}") + self.msl = MSL.handshake( + scheme=scheme, + session=self.session, + endpoint=self.config["endpoints"]["manifest.xml"], + sender=self.esn, + cdm=self.cdm, + msl_keys_path=self.get_cache("msl_{id}_{esn}_{scheme}.json".format( + id=self.cdm.device.system_id, + esn=self.esn, + scheme=scheme + )) + ) + if not self.session.cookies: + raise self.log.exit(" - No cookies provided, cannot log in.") + if self.cdm.device.type == DeviceTypes.CHROME: + self.userauthdata = UserAuthentication.NetflixIDCookies( + netflixid=self.session.cookies.get_dict()["NetflixId"], + securenetflixid=self.session.cookies.get_dict()["SecureNetflixId"] + ) + else: + if not self.credentials: + raise self.log.exit(" - Credentials are required for Android CDMs, and none were provided.") + # need to get cookies via an android-like way + # outdated + # self.android_login(credentials.username, credentials.password) + # need to use EmailPassword for userauthdata, it specifically checks for this + self.userauthdata = UserAuthentication.EmailPassword( + email=self.credentials.username, + password=self.credentials.password + ) + self.react_context = self.get_react_context() + + def get_profiles(self): + if self.range in ("HDR10", "DV") and self.vcodec not in ("H265", "VP9"): + self.vcodec = "H265" + profiles = self.config["profiles"]["video"][self.vcodec] + if self.range and self.range in profiles: + return profiles[self.range] + return profiles + + def get_react_context(self): + """ + Netflix uses a "BUILD_IDENTIFIER" value on some API's, e.g. the Shakti (metadata) API. + This value isn't given to the user through normal means so REGEX is needed. + It's obtained by grabbing the body of a logged-in netflix homepage. + The value changes often but doesn't often matter if it's only a bit out of date. + + It also uses a Client Version for various MPL calls. + + :returns: reactContext parsed json-loaded dictionary + """ + cache_loc = self.get_cache("web_data.json") + + if not os.path.isfile(cache_loc): + headers = { + 'accept': '*/*', + 'sec-fetch-mode': 'cors', + 'sec-fetch-dest': 'empty', + 'origin': 'https://www.netflix.com', + 'connection': 'keep-alive', + 'accept-encoding': 'gzip, deflate, br', + 'accept-language': 'en-US,en;q=0.9', + 'cache-control': 'no-cache', + 'sec-ch-ua': '"Microsoft Edge";v="116", "Chromium";v="116", "Not-A.Brand";v="24"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"', + 'sec-fetch-site': 'same-origin', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.46' + } + + src = self.session.get("https://www.netflix.com/browse", headers=headers).text + match = re.search(r"netflix\.reactContext = ({.+});</script><script>window\.", src, re.MULTILINE) + if not match: + raise self.log.exit(" - Failed to retrieve reactContext data, cookies might be outdated.") + react_context_raw = match.group(1) + react_context = json.loads(re.sub(r"\\x", r"\\u00", react_context_raw))["models"] + react_context["requestHeaders"]["data"] = { + re.sub(r"\B([A-Z])", r"-\1", k): str(v) for k, v in react_context["requestHeaders"]["data"].items() + } + react_context["abContext"]["data"]["headers"] = { + k: str(v) for k, v in react_context["abContext"]["data"]["headers"].items() + } + react_context["requestHeaders"]["data"] = { + k: str(v) for k, v in react_context["requestHeaders"]["data"].items() + } + #react_context["playerModel"]["data"]["config"]["core"]["initParams"]["clientVersion"] = ( + # react_context["playerModel"]["data"]["config"]["core"]["assets"]["core"].split("-")[-1][:-3] + #) + + os.makedirs(os.path.dirname(cache_loc), exist_ok=True) + with open(cache_loc, "w", encoding="utf-8") as fd: + fd.write(jsonpickle.encode(react_context)) + + return react_context + + with open(cache_loc, encoding="utf-8") as fd: + return jsonpickle.decode(fd.read()) + + def get_metadata(self, title_id): + """ + Obtain Metadata information about a title by it's ID. + :param title_id: Title's ID. + :returns: Title Metadata. + """ + + """ + # Wip non-working code for the newer shakti metadata replacement + metadata = self.session.post( + url=self.config["endpoints"]["website"].format( + build_id=self.react_context["serverDefs"]["data"]["BUILD_IDENTIFIER"] + ), + params={ + # features + "webp": self.react_context["browserInfo"]["data"]["features"]["webp"], + "drmSystem": self.config["configuration"]["drm_system"], + # truths + "isVolatileBillboardsEnabled": self.react_context["truths"]["data"]["volatileBillboardsEnabled"], + "routeAPIRequestsThroughFTL": self.react_context["truths"]["data"]["routeAPIRequestsThroughFTL"], + "isTop10Supported": self.react_context["truths"]["data"]["isTop10Supported"], + "categoryCraversEnabled": self.react_context["truths"]["data"]["categoryCraversEnabled"], + "hasVideoMerchInBob": self.react_context["truths"]["data"]["hasVideoMerchInBob"], + "persoInfoDensity": self.react_context["truths"]["data"]["enablePersoInfoDensityToggle"], + "contextAwareImages": self.react_context["truths"]["data"]["contextAwareImages"], + # ? + "falcor_server": "0.1.0", + "withSize": True, + "materialize": True, + "original_path": quote_plus( + f"/shakti/{self.react_context['serverDefs']['data']['BUILD_IDENTIFIER']}/pathEvaluator" + ) + }, + headers=dict( + **self.react_context["abContext"]["data"]["headers"], + **{ + "X-Netflix.Client.Request.Name": "ui/falcorUnclassified", + "X-Netflix.esn": self.react_context["esnGeneratorModel"]["data"]["esn"], + "x-netflix.nq.stack": self.react_context["serverDefs"]["data"]["stack"], + "x-netflix.request.client.user.guid": ( + self.react_context["memberContext"]["data"]["userInfo"]["guid"] + ) + }, + **self.react_context["requestHeaders"]["data"] + ), + data={ + "path": json.dumps([ + [ + "videos", + 70155547, + [ + "bobSupplementalMessage", + "bobSupplementalMessageIcon", + "bookmarkPosition", + "delivery", + "displayRuntime", + "evidence", + "hasSensitiveMetadata", + "interactiveBookmark", + "maturity", + "numSeasonsLabel", + "promoVideo", + "releaseYear", + "seasonCount", + "title", + "userRating", + "userRatingRequestId", + "watched" + ] + ], + [ + "videos", + 70155547, + "seasonList", + "current", + "summary" + ] + ]), + "authURL": self.react_context["memberContext"]["data"]["userInfo"]["authURL"] + } + ) + + print(metadata.headers) + print(metadata.text) + exit() + """ + + try: + metadata = self.session.get( + self.config["endpoints"]["metadata"].format(build_id=self.react_context['serverDefs']['data']['BUILD_IDENTIFIER']), + params={ + "movieid": title_id, + "drmSystem": self.config["configuration"]["drm_system"], + "isWatchlistEnabled": False, + "isShortformEnabled": False, + "isVolatileBillboardsEnabled": self.react_context["truths"]["data"]["volatileBillboardsEnabled"], + "languages": self.meta_lang + } + ).json() + except requests.HTTPError as e: + if e.response.status_code == 500: + self.log.warning( + " - Recieved a HTTP 500 error while getting metadata, deleting cached reactContext data" + ) + os.unlink(self.get_cache("web_data.json")) + return self.get_metadata(self, title_id) + raise + except json.JSONDecodeError: + raise self.log.exit(" - Failed to get metadata, title might not be available in your region.") + else: + if "status" in metadata and metadata["status"] == "error": + raise self.log.exit( + f" - Failed to get metadata, cookies might be expired. ({metadata['message']})" + ) + return metadata + + def get_manifest(self, title, video_profiles): + if isinstance(video_profiles, dict): + video_profiles = list(video_profiles.values()) + if self.quality == 720: + # NF only returns lower quality 720p streams if 1080p is also requested + video_profiles = [x for x in video_profiles if "l40" not in x] + audio_profiles = self.config["profiles"]["audio"] + if self.acodec: + audio_profiles = audio_profiles[self.acodec] + if isinstance(audio_profiles, dict): + audio_profiles = list(audio_profiles.values()) + profiles = sorted(set(flatten(as_list( + # as list then flatten in case any of these profiles are a list of lists + # list(set()) used to remove any potential duplicates + self.config["profiles"]["video"]["H264"]["BPL"], # always required for some reason + video_profiles, + audio_profiles, + self.config["profiles"]["subtitles"], + )))) + self.log.debug("Profiles:\n\t" + "\n\t".join(profiles)) + + params = {} + if self.cdm.device.type == DeviceTypes.CHROME: + params = { + "reqAttempt": 1, + "reqPriority": 0, + "reqName": "prefetch/manifest.xml", + "clienttype": "akira", + } + + manifest_data = { + 'version': 2, + 'url': '/manifest.xml', + "id": int(time.time()), + "esn": self.esn, + 'languages': ['en-US'], + 'uiVersion': 'shakti-va3fd86e3', + 'clientVersion': '6.0041.930.911', + 'params':{'type': 'standard', + 'viewableId': title.service_data.get("episodeId", title.service_data["id"]), + 'profiles': profiles, + 'flavor': 'STANDARD', + "drmType": self.config["configuration"]["drm_system"], + "drmVersion": self.config["configuration"]["drm_version"], + 'usePsshBox': True, + 'isBranching': False, + 'useHttpsStreams': False, + 'imageSubtitleHeight': 1080, + 'uiVersion': 'shakti-va3fd86e3', + 'clientVersion': '6.0041.930.911', + 'platform': '113.0.1774', + 'supportsPreReleasePin': True, + 'supportsWatermark': True, + 'showAllSubDubTracks': True, + 'titleSpecificData': {}, + "videoOutputInfo": [{ + # todo ; make this return valid, but "secure" values, maybe it helps + "type": "DigitalVideoOutputDescriptor", + "outputType": "unknown", + "supportedHdcpVersions": self.config["configuration"]["supported_hdcp_versions"], + "isHdcpEngaged": self.config["configuration"]["is_hdcp_engaged"] + }], + 'preferAssistiveAudio': False, + 'liveMetadataFormat': 'INDEXED_SEGMENT_TEMPLATE', + 'isNonMember': False, + 'osVersion': '10.0', + 'osName': 'windows', + 'desiredVmaf': 'plus_lts', + 'desiredSegmentVmaf': 'plus_lts', + 'requestSegmentVmaf': False, + 'challenge': self.config["payload_challenge"], + 'deviceSecurityLevel': '3000' + } + } + + self.log.debug(manifest_data) + + + _, payload_chunks = self.msl.send_message( + endpoint=self.config["endpoints"]["manifest.xml"], + params=params, + application_data=manifest_data, + userauthdata=self.userauthdata + ) + if "errorDetails" in payload_chunks: + raise Exception(f"Manifest call failed: {payload_chunks['errorDetails']}") + return payload_chunks + + def manifest_as_tracks(self, manifest): + # filter audio_tracks so that each stream is an entry instead of each track + manifest["audio_tracks"] = [x for y in [ + [dict(t, **d) for d in t["streams"]] + for t in manifest["audio_tracks"] + ] for x in y] + return Tracks( + # VIDEO + [VideoTrack( + id_=x["downloadable_id"], + source=self.ALIASES[0], + url=x["urls"][0]["url"], + # metadata + codec=x["content_profile"], + bitrate=x["bitrate"] * 1000, + width=x["res_w"], + height=x["res_h"], + fps=(float(x["framerate_value"]) / x["framerate_scale"]) if "framerate_value" in x else None, + # switches/options + needs_proxy=self.download_proxied, + needs_repack=False, + # decryption + encrypted=x["isDrm"], + pssh=Box.parse(base64.b64decode(manifest["video_tracks"][0]["drmHeader"]["bytes"])) if x[ + "isDrm"] else None, + kid=x["drmHeaderId"] if x["isDrm"] else None, + ) for x in manifest["video_tracks"][0]["streams"]], + # AUDIO + [AudioTrack( + id_=x["downloadable_id"], + source=self.ALIASES[0], + url=x["urls"][0]["url"], + # metadata + codec=x["content_profile"], + language=self.NF_LANG_MAP.get(x["language"], x["language"]), + bitrate=x["bitrate"] * 1000, + channels=x["channels"], + descriptive=x.get("rawTrackType", "").lower() == "assistive", + # switches/options + needs_proxy=self.download_proxied, + needs_repack=False, + # decryption + encrypted=x["isDrm"], + pssh=Box.parse(base64.b64decode(x["drmHeader"]["bytes"])) if x["isDrm"] else None, + kid=x.get("drmHeaderId") if x["isDrm"] else None, # TODO: haven't seen enc audio, needs testing + ) for x in manifest["audio_tracks"]], + # SUBTITLE + [TextTrack( + id_=list(x["downloadableIds"].values())[0], + source=self.ALIASES[0], + url=next(iter(next(iter(x["ttDownloadables"].values()))["downloadUrls"].values())), + # metadata + codec=next(iter(x["ttDownloadables"].keys())), + language=self.NF_LANG_MAP.get(x["language"], x["language"]), + forced=x["isForcedNarrative"], + # switches/options + needs_proxy=self.download_proxied, + # text track options + sdh=x["rawTrackType"] == "closedcaptions" + ) for x in manifest["timedtexttracks"] if not x["isNoneTrack"]] + ) + + @staticmethod + def get_original_language(manifest): + for language in manifest["audio_tracks"]: + if language["languageDescription"].endswith(" [Original]"): + return Language.get(language["language"]) + # e.g. get `en` from "A:1:1;2;en;0;|V:2:1;[...]" + return Language.get(manifest["defaultTrackOrderList"][0]["mediaId"].split(";")[2]) diff --git a/vinetrimmer/services/peacock.py b/vinetrimmer/services/peacock.py new file mode 100644 index 0000000..c312be2 --- /dev/null +++ b/vinetrimmer/services/peacock.py @@ -0,0 +1,421 @@ +import base64 +import hashlib +import hmac +import json +import os +import time +from datetime import datetime + +import click +import requests + +from vinetrimmer.objects import Title, Tracks +from vinetrimmer.services.BaseService import BaseService +from vinetrimmer.utils.regex import find + + +class Peacock(BaseService): + """ + Service code for NBC's Peacock streaming service (https://peacocktv.com). + + \b + Authorization: Cookies + Security: UHD@-- FHD@L3, doesn't care about releases. + + \b + Tips: - The library of contents can be viewed without logging in at https://www.peacocktv.com/stream/tv + See the footer for links to movies, news, etc. A US IP is required to view. + """ + + ALIASES = ["PCOK", "peacock"] + #GEOFENCE = ["us"] + TITLE_RE = [ + r"(?:https?://(?:www\.)?peacocktv\.com/watch/asset/|/?)(?P<id>movies/[a-z0-9/./-]+/[a-f0-9-]+)", + r"(?:https?://(?:www\.)?peacocktv\.com/watch/asset/|/?)(?P<id>tv/[a-z0-9-/.]+/\d+)", + r"(?:https?://(?:www\.)?peacocktv\.com/watch/asset/|/?)(?P<id>-/[a-z0-9-/.]+/\d+)", + r"(?:https?://(?:www\.)?peacocktv\.com/stream-tv/)?(?P<id>[a-z0-9-/.]+)", + ] + + @staticmethod + @click.command(name="Peacock", short_help="https://peacocktv.com") + @click.argument("title", type=str, required=False) + @click.option("-m", "--movie", is_flag=True, default=False, help="Title is a movie.") + @click.pass_context + def cli(ctx, **kwargs): + return Peacock(ctx, **kwargs) + + def __init__(self, ctx, title, movie): + super().__init__(ctx) + self.parse_title(ctx, title) + self.movie = movie + + self.profile = ctx.obj.profile + + self.service_config = None + self.hmac_key = None + self.tokens = None + self.license_api = None + self.license_bt = None + self.vcodec = ctx.parent.params["vcodec"] + self.vrange= ctx.parent.params["range_"] + + self.configure() + + def get_titles(self): + # Title is a slug, e.g. `/tv/the-office/4902514835143843112` or just `the-office` + + if "/" not in self.title: + r = self.session.get(self.config["endpoints"]["stream_tv"].format(title_id=self.title)) + self.title = find("/watch/asset(/[^']+)", r.text) + if not self.title: + raise self.log.exit(" - Title ID not found or invalid") + + if not self.title.startswith("/"): + self.title = f"/{self.title}" + + if self.title.startswith("/movies/"): + self.movie = True + + res = self.session.get( + url=self.config["endpoints"]["node"], + params={ + "slug": self.title, + "represent": "(items(items))" + }, + headers={ + "Accept": "*", + "Referer": f"https://www.peacocktv.com/watch/asset{self.title}", + "X-SkyOTT-Device": self.config["client"]["device"], + "X-SkyOTT-Platform": self.config["client"]["platform"], + "X-SkyOTT-Proposition": self.config["client"]["proposition"], + "X-SkyOTT-Provider": self.config["client"]["provider"], + "X-SkyOTT-Territory": self.config["client"]["territory"], + "X-SkyOTT-Language": "en" + } + ).json() + + if self.movie: + return Title( + id_=self.title, + type_=Title.Types.MOVIE, + name=res["attributes"]["title"], + year=res["attributes"]["year"], + source=self.ALIASES[0], + service_data=res, + ) + else: + titles = [] + for season in res["relationships"]["items"]["data"]: + for episode in season["relationships"]["items"]["data"]: + titles.append(episode) + return [Title( + id_=self.title, + type_=Title.Types.TV, + name=res["attributes"]["title"], + year=x["attributes"].get("year"), + season=x["attributes"].get("seasonNumber"), + episode=x["attributes"].get("episodeNumber"), + episode_name=x["attributes"].get("title"), + source=self.ALIASES[0], + service_data=x + ) for x in titles] + + def get_tracks(self, title): + supported_colour_spaces=["SDR"] + + if self.vrange == "HDR10": + self.log.info("Switched dynamic range to HDR10") + supported_colour_spaces=["HDR10"] + if self.vrange == "DV": + self.log.info("Switched dynamic range to DV") + supported_colour_spaces=["DolbyVision"] + content_id = title.service_data["attributes"]["formats"]["HD"]["contentId"] + variant_id = title.service_data["attributes"]["providerVariantId"] + + sky_headers = { + # order of these matter! + "X-SkyOTT-Agent": ".".join([ + self.config["client"]["proposition"].lower(), + self.config["client"]["device"].lower(), + self.config["client"]["platform"].lower() + ]), + "X-SkyOTT-PinOverride": "false", + "X-SkyOTT-Provider": self.config["client"]["provider"], + "X-SkyOTT-Territory": self.config["client"]["territory"], + "X-SkyOTT-UserToken": self.tokens["userToken"] + } + + body = json.dumps({ + "device": { + # maybe get these from the config endpoint? + "capabilities": [ + { + "protection": "PLAYREADY", + "container": "ISOBMFF", + "transport": "DASH", + "acodec": "AAC", + "vcodec": "H265" if self.vcodec == "H265" else "H264", + }, + { + "protection": "NONE", + "container": "ISOBMFF", + "transport": "DASH", + "acodec": "AAC", + "vcodec": "H265" if self.vcodec == "H265" else "H264", + } + ], + "maxVideoFormat": "UHD" if self.vcodec == "H265" else "HD", + "supportedColourSpaces": supported_colour_spaces, + "model": self.config["client"]["platform"], + "hdcpEnabled": "true" + }, + "client": { + "thirdParties": ["FREEWHEEL", "YOSPACE"] # CONVIVA + }, + "contentId": content_id, + "providerVariantId": variant_id, + "parentalControlPin": "null" + }, separators=(",", ":")) + + manifest = self.session.post( + url=self.config["endpoints"]["vod"], + data=body, + headers=dict(**sky_headers, **{ + "Accept": "application/vnd.playvod.v1+json", + "Content-Type": "application/vnd.playvod.v1+json", + "X-Sky-Signature": self.create_signature_header( + method="POST", + path="/video/playouts/vod", + sky_headers=sky_headers, + body=body, + timestamp=int(time.time()) + ) + }) + ).json() + if "errorCode" in manifest: + raise self.log.exit(f" - An error occurred: {manifest['description']} [{manifest['errorCode']}]") + + self.license_api = manifest["protection"]["licenceAcquisitionUrl"] + self.license_bt = manifest["protection"]["licenceToken"] + + tracks = Tracks.from_mpd( + url=manifest["asset"]["endpoints"][0]["url"], + session=self.session, + source=self.ALIASES[0] + ) + + if supported_colour_spaces == ["HDR10"]: + for track in tracks.videos: + track.hdr10 = True if supported_colour_spaces == ["HDR10"] else False + if supported_colour_spaces == ["DolbyVision"]: + for track in tracks.videos: + track.dolbyvison = True if supported_colour_spaces == ["DV"] else False + + for track in tracks: + track.needs_proxy = False + + for track in tracks.audios: + if track.language.territory == "AD": + # This is supposed to be Audio Description, not Andorra + track.language.territory = None + + return tracks + + def get_chapters(self, title): + return [] + + def license(self, challenge, **_): + request = self.session.post( + url=self.license_api, + headers={ + "Accept": "*", + "X-Sky-Signature": self.create_signature_header( + method="POST", + path="/" + self.license_api.split("://", 2)[1].split("/", 1)[1], + sky_headers={}, + body="", + timestamp=int(time.time()) + ) + }, + data=challenge # expects bytes + ) + return request.text + # Service specific functions + + def configure(self): + self.session.headers.update({"Origin": "https://www.peacocktv.com"}) + self.log.info("Getting Peacock Client configuration") + if self.config["client"]["platform"] != "PC": + self.service_config = self.session.get( + url=self.config["endpoints"]["config"].format( + territory=self.config["client"]["territory"], + provider=self.config["client"]["provider"], + proposition=self.config["client"]["proposition"], + device=self.config["client"]["platform"], + version=self.config["client"]["config_version"], + ) + ).json() + self.hmac_key = bytes(self.config["security"]["signature_hmac_key_v4"], "utf-8") + self.log.info("Getting Authorization Tokens") + self.tokens = self.get_tokens() + self.log.info("Verifying Authorization Tokens") + if not self.verify_tokens(): + raise self.log.exit(" - Failed! Cookies might be outdated.") + + @staticmethod + def calculate_sky_header_md5(headers): + if len(headers.items()) > 0: + headers_str = "\n".join(f"{x[0].lower()}: {x[1]}" for x in headers.items()) + "\n" + else: + headers_str = "{}" + return str(hashlib.md5(headers_str.encode()).hexdigest()) + + @staticmethod + def calculate_body_md5(body): + return str(hashlib.md5(body.encode()).hexdigest()) + + def calculate_signature(self, msg): + digest = hmac.new(self.hmac_key, bytes(msg, "utf-8"), hashlib.sha1).digest() + return str(base64.b64encode(digest), "utf-8") + + def create_signature_header(self, method, path, sky_headers, body, timestamp): + data = "\n".join([ + method.upper(), + path, + "", # important! + self.config["client"]["client_sdk"], + "1.0", + self.calculate_sky_header_md5(sky_headers), + str(timestamp), + self.calculate_body_md5(body) + ]) + "\n" + + signature_hmac = self.calculate_signature(data) + + return self.config["security"]["signature_format"].format( + client=self.config["client"]["client_sdk"], + signature=signature_hmac, + timestamp=timestamp + ) + + def get_tokens(self): + # Try to get cached tokens + tokens_cache_path = self.get_cache("tokens_{profile}_{id}.json".format( + profile=self.profile, + id=self.config["client"]["id"] + )) + if os.path.isfile(tokens_cache_path): + with open(tokens_cache_path, encoding="utf-8") as fd: + tokens = json.load(fd) + tokens_expiration = tokens.get("tokenExpiryTime", None) + if tokens_expiration and datetime.strptime(tokens_expiration, "%Y-%m-%dT%H:%M:%S.%fZ") > datetime.now(): + return tokens + + # Get all SkyOTT headers + sky_headers = { + # Order of these matters! + "X-SkyOTT-Agent": ".".join([ + self.config["client"]["proposition"], + self.config["client"]["device"], + self.config["client"]["platform"] + ]).lower(), + "X-SkyOTT-Device": self.config["client"]["device"], + "X-SkyOTT-Platform": self.config["client"]["platform"], + "X-SkyOTT-Proposition": self.config["client"]["proposition"], + "X-SkyOTT-Provider": self.config["client"]["provider"], + "X-SkyOTT-Territory": self.config["client"]["territory"] + } + + print(self.session.cookies) + + try: + # Call personas endpoint to get the accounts personaId + personas = self.session.get( + url=self.config["endpoints"]["personas"], + headers=dict(**sky_headers, **{ + "Accept": "application/vnd.persona.v1+json", + "Content-Type": "application/vnd.persona.v1+json", + "X-SkyOTT-TokenType": self.config["client"]["auth_scheme"] + }) + ).json() + except requests.HTTPError as e: + error = e.response.json() + if "message" in error and "code" in error: + error = f"{error['message']} [{error['code']}]" + if "bad credentials" in error.lower(): + error += ". Cookies may be expired or invalid." + raise self.log.exit(f" - Unable to get persona ID: {error}") + raise self.log.exit(f" - HTTP Error {e.response.status_code}: {e.response.reason}") + persona = personas["personas"][0]["personaId"] + + # Craft the body data that will be sent to the tokens endpoint, being minified and order matters! + body = json.dumps({ + "auth": { + "authScheme": self.config["client"]["auth_scheme"], + "authIssuer": self.config["client"]["auth_issuer"], + "provider": self.config["client"]["provider"], + "providerTerritory": self.config["client"]["territory"], + "proposition": self.config["client"]["proposition"], + "personaId": persona + }, + "device": { + "type": self.config["client"]["device"], + "platform": self.config["client"]["platform"], + "id": self.config["client"]["id"], + "drmDeviceId": self.config["client"]["drm_device_id"] + } + }, separators=(",", ":")) + + # Get the tokens + tokens = self.session.post( + url=self.config["endpoints"]["tokens"], + headers=dict(**sky_headers, **{ + "Accept": "application/vnd.tokens.v1+json", + "Content-Type": "application/vnd.tokens.v1+json", + "X-Sky-Signature": self.create_signature_header( + method="POST", + path="/auth/tokens", + sky_headers=sky_headers, + body=body, + timestamp=int(time.time()) + ) + }), + data=body + ).json() + + os.makedirs(os.path.dirname(tokens_cache_path), exist_ok=True) + with open(tokens_cache_path, "w", encoding="utf-8") as fd: + json.dump(tokens, fd) + + return tokens + + def verify_tokens(self): + """Verify the tokens by calling the /auth/users/me endpoint and seeing if it works""" + sky_headers = { + # order of these matter! + "X-SkyOTT-Device": self.config["client"]["device"], + "X-SkyOTT-Platform": self.config["client"]["platform"], + "X-SkyOTT-Proposition": self.config["client"]["proposition"], + "X-SkyOTT-Provider": self.config["client"]["provider"], + "X-SkyOTT-Territory": self.config["client"]["territory"], + "X-SkyOTT-UserToken": self.tokens["userToken"] + } + try: + self.session.get( + url=self.config["endpoints"]["me"], + headers=dict(**sky_headers, **{ + "Accept": "application/vnd.userinfo.v2+json", + "Content-Type": "application/vnd.userinfo.v2+json", + "X-Sky-Signature": self.create_signature_header( + method="GET", + path="/auth/users/me", + sky_headers=sky_headers, + body="", + timestamp=int(time.time()) + ) + }) + ) + except requests.HTTPError: + return False + else: + return True diff --git a/vinetrimmer/utils/AtomicSQL.py b/vinetrimmer/utils/AtomicSQL.py new file mode 100644 index 0000000..aa6bd3a --- /dev/null +++ b/vinetrimmer/utils/AtomicSQL.py @@ -0,0 +1,85 @@ +""" +AtomicSQL - Race-condition and Threading safe SQL Database Interface. +""" + +import os +import sqlite3 +import time +from threading import Lock + + +class AtomicSQL: + """ + Race-condition and Threading safe SQL Database Interface. + """ + + def __init__(self): + self.master_lock = Lock() # prevents race condition + self.db = {} # used to hold the database connections and commit changes and such + self.cursor = {} # used to execute queries and receive results + self.session_lock = {} # like master_lock, but per-session + + def load(self, connection): + """ + Store SQL Connection object and return a reference ticket. + :param connection: SQLite3 or pymysql Connection object. + :returns: Session ID in which the database connection is referenced with. + """ + self.master_lock.acquire() + try: + # obtain a unique cryptographically random session_id + session_id = None + while not session_id or session_id in self.db: + session_id = os.urandom(16) + self.db[session_id] = connection + self.cursor[session_id] = self.db[session_id].cursor() + self.session_lock[session_id] = Lock() + return session_id + finally: + self.master_lock.release() + + def safe_execute(self, session_id, action): + """ + Execute code on the Database Connection in a race-condition safe way. + :param session_id: Database Connection's Session ID. + :param action: Function or lambda in which to execute, it's provided `db` and `cursor` arguments. + :returns: Whatever `action` returns. + """ + if session_id not in self.db: + raise ValueError(f"Session ID {session_id!r} is invalid.") + self.master_lock.acquire() + self.session_lock[session_id].acquire() + try: + failures = 0 + while True: + try: + action( + db=self.db[session_id], + cursor=self.cursor[session_id] + ) + break + except sqlite3.OperationalError: + failures += 1 + delay = 3 * failures + print(f"AtomicSQL.safe_execute failed, retrying in {delay} seconds...") + time.sleep(delay) + if failures == 10: + raise ValueError("AtomicSQL.safe_execute failed too many time's. Aborting.") + return self.cursor[session_id] + finally: + self.session_lock[session_id].release() + self.master_lock.release() + + def commit(self, session_id): + """ + Commit changes to the Database Connection immediately. + This isn't necessary to be run every time you make changes, just ensure it's run + at least before termination. + :param session_id: Database Connection's Session ID. + :returns: True if it committed. + """ + self.safe_execute( + session_id, + lambda db, cursor: db.commit() + ) + return True # todo ; actually check if db.commit worked diff --git a/vinetrimmer/utils/BamSDK/__init__.py b/vinetrimmer/utils/BamSDK/__init__.py new file mode 100644 index 0000000..882d18c --- /dev/null +++ b/vinetrimmer/utils/BamSDK/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from vinetrimmer.utils.BamSDK.bamsdk import BamSdk diff --git a/vinetrimmer/utils/BamSDK/bamsdk.py b/vinetrimmer/utils/BamSDK/bamsdk.py new file mode 100644 index 0000000..3e40b0f --- /dev/null +++ b/vinetrimmer/utils/BamSDK/bamsdk.py @@ -0,0 +1,28 @@ +import requests + +from vinetrimmer.utils.BamSDK.services.account import account +from vinetrimmer.utils.BamSDK.services.bamIdentity import bamIdentity +from vinetrimmer.utils.BamSDK.services.content import content +from vinetrimmer.utils.BamSDK.services.device import device +from vinetrimmer.utils.BamSDK.services.drm import drm +from vinetrimmer.utils.BamSDK.services.media import media +from vinetrimmer.utils.BamSDK.services.session import session +from vinetrimmer.utils.BamSDK.services.token import token + + +class BamSdk: + def __init__(self, endpoint, session_=None): + self._session = session_ or requests.Session() + + self.config = self._session.get(endpoint).json() + self.application = self.config["application"] + self.commonHeaders = self.config["commonHeaders"] + + self.account = account(self.config["services"]["account"], self._session) + self.bamIdentity = bamIdentity(self.config["services"]["bamIdentity"], self._session) + self.content = content(self.config["services"]["content"], self._session) + self.device = device(self.config["services"]["device"], self._session) + self.drm = drm(self.config["services"]["drm"], self._session) + self.media = media(self.config["services"]["media"], self._session) + self.session = session(self.config["services"]["session"], self._session) + self.token = token(self.config["services"]["token"], self._session) diff --git a/vinetrimmer/utils/BamSDK/services/__init__.py b/vinetrimmer/utils/BamSDK/services/__init__.py new file mode 100644 index 0000000..0bdd32f --- /dev/null +++ b/vinetrimmer/utils/BamSDK/services/__init__.py @@ -0,0 +1,37 @@ +import requests + + +class Service: + def __init__(self, cfg, session=None): + self.session = session or requests.Session() + self.client = Client(cfg.get("client") or {}) + self.disabled = cfg.get("disabled") + self.extras = cfg.get("extras") + + +class Client: + def __init__(self, data): + self.baseUrl = data.get("baseUrl") + self.endpoints = {k: Endpoint(v) for k, v in (data.get("endpoints") or {}).items()} + self.extras = data.get("extras") or {} + + +class Endpoint: + def __init__(self, data): + self.headers = data.get("headers") or {} + self.href = data["href"] + self.method = data.get("method") or "GET" + self.templated = data.get("templated") or False + self.timeout = data.get("timeout") or 15 + self.ttl = data.get("ttl") or 0 + + # noinspection PyPep8Naming + def get_headers(self, accessToken=None, apiKey=None): + token = None + if accessToken: + token = {"accessToken": accessToken} + elif apiKey: + token = {"apiKey": apiKey} + if token: + self.headers.update({"Authorization": self.headers["Authorization"].format(**token)}) + return self.headers diff --git a/vinetrimmer/utils/BamSDK/services/account.py b/vinetrimmer/utils/BamSDK/services/account.py new file mode 100644 index 0000000..481efa2 --- /dev/null +++ b/vinetrimmer/utils/BamSDK/services/account.py @@ -0,0 +1,37 @@ +from requests import Request + +from vinetrimmer.utils.BamSDK.services import Service + + +# noinspection PyPep8Naming +class account(Service): + def createAccountGrant(self, json, access_token): + endpoint = self.client.endpoints["createAccountGrant"] + req = Request( + method=endpoint.method, + url=endpoint.href, + headers=endpoint.get_headers(accessToken=access_token), + json=json + ).prepare() + res = self.session.send(req) + return res.json() + + def getUserProfiles(self, access_token): + endpoint = self.client.endpoints["getUserProfiles"] + req = Request( + method=endpoint.method, + url=endpoint.href, + headers=endpoint.get_headers(accessToken=access_token) + ).prepare() + res = self.session.send(req) + return res.json() + + def setActiveUserProfile(self, profile_id, access_token): + endpoint = self.client.endpoints["setActiveUserProfile"] + req = Request( + method=endpoint.method, + url=endpoint.href.format(profileId=profile_id), + headers=endpoint.get_headers(accessToken=access_token) + ).prepare() + res = self.session.send(req) + return res.json() diff --git a/vinetrimmer/utils/BamSDK/services/bamIdentity.py b/vinetrimmer/utils/BamSDK/services/bamIdentity.py new file mode 100644 index 0000000..f635f8e --- /dev/null +++ b/vinetrimmer/utils/BamSDK/services/bamIdentity.py @@ -0,0 +1,20 @@ +from requests import Request + +from vinetrimmer.utils.BamSDK.services import Service + + +# noinspection PyPep8Naming +class bamIdentity(Service): + def identityLogin(self, email, password, access_token): + endpoint = self.client.endpoints["identityLogin"] + req = Request( + method=endpoint.method, + url=endpoint.href, + headers=endpoint.get_headers(accessToken=access_token), + json={ + "email": email, + "password": password + } + ).prepare() + res = self.session.send(req) + return res.json() diff --git a/vinetrimmer/utils/BamSDK/services/content.py b/vinetrimmer/utils/BamSDK/services/content.py new file mode 100644 index 0000000..42b049d --- /dev/null +++ b/vinetrimmer/utils/BamSDK/services/content.py @@ -0,0 +1,36 @@ +from requests import Request + +from vinetrimmer.utils.BamSDK.services import Service + + +# noinspection PyPep8Naming +class content(Service): + def getDmcEpisodes(self, region, season_id, page, access_token): + endpoint = self.client.endpoints["getDmcEpisodes"] + req = Request( + method=endpoint.method, + url=f"https://disney.content.edge.bamgrid.com/svc/content/DmcEpisodes/version/3.3/region/{region}/audience/false/maturity/1850/language/en/seasonId/{season_id}/pageSize/15/page/{page}", # noqa: E501 + headers=endpoint.get_headers(accessToken=access_token) + ).prepare() + res = self.session.send(req) + return res.json() + + def getDmcSeriesBundle(self, region, media_id, access_token): + endpoint = self.client.endpoints["getDmcSeriesBundle"] + req = Request( + method=endpoint.method, + url=f"https://disney.content.edge.bamgrid.com/svc/content/DmcSeriesBundle/version/3.3/region/{region}/audience/false/maturity/1850/language/en/encodedSeriesId/{media_id}", # noqa: E501 + headers=endpoint.get_headers(accessToken=access_token) + ).prepare() + res = self.session.send(req) + return res.json() + + def getDmcVideoBundle(self, region, media_id, access_token): + endpoint = self.client.endpoints["getDmcVideoBundle"] + req = Request( + method=endpoint.method, + url=f"https://disney.content.edge.bamgrid.com/svc/content/DmcVideoBundle/version/3.3/region/{region}/audience/false/maturity/1850/language/en/encodedFamilyId/{media_id}", # noqa: E501 + headers=endpoint.get_headers(accessToken=access_token) + ).prepare() + res = self.session.send(req) + return res.json() diff --git a/vinetrimmer/utils/BamSDK/services/device.py b/vinetrimmer/utils/BamSDK/services/device.py new file mode 100644 index 0000000..f4b6ff8 --- /dev/null +++ b/vinetrimmer/utils/BamSDK/services/device.py @@ -0,0 +1,24 @@ +from json import JSONDecodeError + +from requests import Request + +from vinetrimmer.utils.BamSDK.services import Service + + +# noinspection PyPep8Naming +class device(Service): + + def createDeviceGrant(self, json, api_key): + endpoint = self.client.endpoints["createDeviceGrant"] + req = Request( + method=endpoint.method, + url=endpoint.href, + headers=endpoint.get_headers(apiKey=api_key), + json=json + ).prepare() + res = self.session.send(req) + try: + data = res.json() + except JSONDecodeError: + raise Exception(f"An unexpected response occurred for bamsdk.createDeviceGrant: {res.text}") + return data diff --git a/vinetrimmer/utils/BamSDK/services/drm.py b/vinetrimmer/utils/BamSDK/services/drm.py new file mode 100644 index 0000000..412b7ef --- /dev/null +++ b/vinetrimmer/utils/BamSDK/services/drm.py @@ -0,0 +1,50 @@ +import json + +from requests import Request + +from vinetrimmer.utils.BamSDK.services import Service + + +# noinspection PyPep8Naming +class drm(Service): + def widevineCertificate(self): + endpoint = self.client.endpoints["widevineCertificate"] + req = Request( + method=endpoint.method, + url=endpoint.href, + headers=endpoint.headers + ).prepare() + res = self.session.send(req) + return res.content + + def widevineLicense(self, licence, access_token): + endpoint = self.client.endpoints["widevineLicense"] + req = Request( + method=endpoint.method, + url=endpoint.href, + headers=endpoint.get_headers(accessToken=access_token), + data=licence + ).prepare() + res = self.session.send(req) + try: + # if it's json content, then an error occurred + res = json.loads(res.text) + raise Exception(f"Failed to obtain license: {res}") + except json.JSONDecodeError: + return res.content + + def playreadyLicense(self, licence, access_token): + endpoint = self.client.endpoints["playReadyLicense"] + req = Request( + method=endpoint.method, + url=endpoint.href, + headers=endpoint.get_headers(accessToken=access_token), + data=licence + ).prepare() + res = self.session.send(req) + try: + # if it's json content, then an error occurred + res = json.loads(res.text) + raise Exception(f"Failed to obtain license: {res}") + except json.JSONDecodeError: + return res.content diff --git a/vinetrimmer/utils/BamSDK/services/media.py b/vinetrimmer/utils/BamSDK/services/media.py new file mode 100644 index 0000000..fa5265b --- /dev/null +++ b/vinetrimmer/utils/BamSDK/services/media.py @@ -0,0 +1,24 @@ +from requests import Request + +from vinetrimmer.utils.BamSDK.services import Service + + +# noinspection PyPep8Naming +class media(Service): + def __init__(self, cfg, session=None): + super().__init__(cfg, session) + self.uhd_allowed = self.extras["isUhdAllowed"] + self.default_scenario = self.extras["playbackScenarioDefault"] + self.scenarios = self.extras["playbackScenarios"] + self.restricted_scenario = self.extras["restrictedPlaybackScenario"] + self.security_requirements = self.extras["securityCheckRequirements"] + + def mediaPayload(self, media_id, scenario, access_token): + endpoint = self.client.endpoints["mediaPayload"] + req = Request( + method=endpoint.method, + url=f"{self.client.baseUrl}/media/{media_id}/scenarios/{scenario}", + headers=endpoint.get_headers(accessToken=access_token) + ).prepare() + res = self.session.send(req) + return res.json() diff --git a/vinetrimmer/utils/BamSDK/services/session.py b/vinetrimmer/utils/BamSDK/services/session.py new file mode 100644 index 0000000..4b71a68 --- /dev/null +++ b/vinetrimmer/utils/BamSDK/services/session.py @@ -0,0 +1,26 @@ +from requests import Request + +from vinetrimmer.utils.BamSDK.services import Service + + +# noinspection PyPep8Naming +class session(Service): + def getInfo(self, access_token): + endpoint = self.client.endpoints["getInfo"] + req = Request( + method=endpoint.method, + url=endpoint.href, + headers=endpoint.get_headers(accessToken=access_token) + ).prepare() + res = self.session.send(req) + return res.json() + + def getLocation(self, access_token): + endpoint = self.client.endpoints["getLocation"] + req = Request( + method=endpoint.method, + url=endpoint.href, + headers=endpoint.get_headers(accessToken=access_token) + ).prepare() + res = self.session.send(req) + return res.json() diff --git a/vinetrimmer/utils/BamSDK/services/token.py b/vinetrimmer/utils/BamSDK/services/token.py new file mode 100644 index 0000000..ae7b05c --- /dev/null +++ b/vinetrimmer/utils/BamSDK/services/token.py @@ -0,0 +1,21 @@ +from requests import Request + +from vinetrimmer.utils.BamSDK.services import Service + + +# noinspection PyPep8Naming +class token(Service): + def __init__(self, cfg, session=None): + super().__init__(cfg, session) + self.subject_tokens = self.extras["subjectTokenTypes"] + + def exchange(self, data, api_key): + endpoint = self.client.endpoints["exchange"] + req = Request( + method=endpoint.method, + url=endpoint.href, + headers=endpoint.get_headers(apiKey=api_key), + data=data + ).prepare() + res = self.session.send(req) + return res.json() diff --git a/vinetrimmer/utils/Logger.py b/vinetrimmer/utils/Logger.py new file mode 100644 index 0000000..c74366c --- /dev/null +++ b/vinetrimmer/utils/Logger.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import logging +import sys +from pathlib import Path +from typing import Any, IO, NoReturn, Optional, Union + +import coloredlogs + +LOG_FORMAT = "{asctime} [{levelname[0]}] {name} : {message}" +LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" +LOG_STYLE = "{" +LOG_FORMATTER = logging.Formatter(LOG_FORMAT, LOG_DATE_FORMAT, LOG_STYLE) + + +class Logger(logging.Logger): + def __init__(self, name: str = "root", level: int = logging.NOTSET, color: bool = True): + """Initialize the logger with a name and an optional level.""" + super().__init__(name, level) + if self.name == "root": + self.add_stream_handler() + if color: + self.install_color() + + def exit(self, msg: str, *args: Any, code: int = 1, **kwargs: Any) -> NoReturn: + """ + Log 'msg % args' with severity 'CRITICAL' and terminate the program + with a default exit code of 1. + + To pass exception information, use the keyword argument exc_info with + a true value, e.g. + + logger.exit("Houston, we have a %s", "major disaster", exc_info=1) + """ + self.critical(msg, *args, **kwargs) + sys.exit(code) + + def add_stream_handler(self, stream: Optional[IO[str]] = None) -> None: + """Add a stream handler to log. Stream defaults to stdout.""" + sh = logging.StreamHandler(stream) + sh.setFormatter(LOG_FORMATTER) + self.addHandler(sh) + + def add_file_handler(self, fp: Union[IO, Path, str]) -> None: + """Convenience alias func for add_stream_handler, deals with type of fp object input.""" + if not isinstance(fp, IO): + fp = Path(fp) + fp = fp.open("w", encoding="utf8") + self.add_stream_handler(fp) + + def install_color(self) -> None: + """Use coloredlogs to set up colors on the log output.""" + if self.level == logging.DEBUG: + coloredlogs.install(level=self.level, fmt=LOG_FORMAT, datefmt=LOG_DATE_FORMAT, style=LOG_STYLE) + coloredlogs.install(level=self.level, logger=self, fmt=LOG_FORMAT, datefmt=LOG_DATE_FORMAT, style=LOG_STYLE) + + +# Cache already used loggers to make sure their level is preserved +_loggers: dict[str, Logger] = {} + + +# noinspection PyPep8Naming +def getLogger(name: Optional[str] = None, level: int = logging.NOTSET) -> Logger: + name = name or "root" + _log = _loggers.get(name, Logger(name)) + _log.setLevel(level) + return _log diff --git a/vinetrimmer/utils/MSL/MSLKeys.py b/vinetrimmer/utils/MSL/MSLKeys.py new file mode 100644 index 0000000..2da255b --- /dev/null +++ b/vinetrimmer/utils/MSL/MSLKeys.py @@ -0,0 +1,10 @@ +from vinetrimmer.utils.MSL.MSLObject import MSLObject + + +class MSLKeys(MSLObject): + def __init__(self, encryption=None, sign=None, rsa=None, mastertoken=None, cdm_session=None): + self.encryption = encryption + self.sign = sign + self.rsa = rsa + self.mastertoken = mastertoken + self.cdm_session = cdm_session diff --git a/vinetrimmer/utils/MSL/MSLObject.py b/vinetrimmer/utils/MSL/MSLObject.py new file mode 100644 index 0000000..47264bb --- /dev/null +++ b/vinetrimmer/utils/MSL/MSLObject.py @@ -0,0 +1,6 @@ +import jsonpickle + + +class MSLObject: + def __repr__(self): + return "<{} {}>".format(self.__class__.__name__, jsonpickle.encode(self, unpicklable=False)) diff --git a/vinetrimmer/utils/MSL/__init__.py b/vinetrimmer/utils/MSL/__init__.py new file mode 100644 index 0000000..2eee0e7 --- /dev/null +++ b/vinetrimmer/utils/MSL/__init__.py @@ -0,0 +1,426 @@ +import base64 +import gzip +import json +import logging +import os +import random +import re +import sys +import time +import zlib +from datetime import datetime +from io import BytesIO +import ssl +import traceback +import jsonpickle +import requests +from Cryptodome.Cipher import AES, PKCS1_OAEP +from Cryptodome.Hash import HMAC, SHA256 +from Cryptodome.PublicKey import RSA +from Cryptodome.Random import get_random_bytes +from Cryptodome.Util import Padding + +from vinetrimmer.utils.MSL.MSLKeys import MSLKeys +from vinetrimmer.utils.MSL.schemes import EntityAuthenticationSchemes # noqa: F401 +from vinetrimmer.utils.MSL.schemes import KeyExchangeSchemes +from vinetrimmer.utils.MSL.schemes.EntityAuthentication import EntityAuthentication +from vinetrimmer.utils.MSL.schemes.KeyExchangeRequest import KeyExchangeRequest +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.poolmanager import PoolManager +from requests.packages.urllib3.util import ssl_ + +class MSL: + log = logging.getLogger("MSL") + + def __init__(self, session, endpoint, sender, keys, message_id, user_auth=None): + + CIPHERS = ( + "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:AES256-SHA" + ) + + class TlsAdapter(HTTPAdapter): + + def __init__(self, ssl_options=0, **kwargs): + self.ssl_options = ssl_options + super(TlsAdapter, self).__init__(**kwargs) + + def init_poolmanager(self, *pool_args, **pool_kwargs): + ctx = ssl_.create_urllib3_context(ciphers=CIPHERS, cert_reqs=ssl.CERT_REQUIRED, options=self.ssl_options) + self.poolmanager = PoolManager(*pool_args, + ssl_context=ctx, + **pool_kwargs) + + + session = requests.session() + adapter = TlsAdapter(ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1) + session.mount("https://", adapter) + self.session = session + self.endpoint = endpoint + self.sender = sender + self.keys = keys + self.user_auth = user_auth + self.message_id = message_id + + @classmethod + def handshake(cls, scheme, session, endpoint, sender, cdm=None, msl_keys_path=None): + message_id = random.randint(0, pow(2, 52)) + msl_keys = MSL.load_cache_data(msl_keys_path) + if msl_keys is not None: + cls.log.info("Using cached MSL data") + else: + msl_keys = MSLKeys() + if scheme != KeyExchangeSchemes.Widevine: + msl_keys.rsa = RSA.generate(2048) + + if not cdm: + raise cls.log.exit("- No cached data and no CDM specified") + + if not msl_keys_path: + raise cls.log.exit("- No cached data and no MSL key path specified") + + if scheme == KeyExchangeSchemes.Widevine: + msl_keys.cdm_session = cdm.open( + pssh=b"\x0A\x7A\x00\x6C\x38\x2B", + raw=True, + offline=True + ) + keyrequestdata = KeyExchangeRequest.Widevine( + keyrequest=cdm.get_license_challenge(msl_keys.cdm_session) + ) + else: + keyrequestdata = KeyExchangeRequest.AsymmetricWrapped( + keypairid="superKeyPair", + mechanism="JWK_RSA", + publickey=msl_keys.rsa.publickey().exportKey(format="DER") + ) + + data = jsonpickle.encode({ + "entityauthdata": EntityAuthentication.Unauthenticated(sender), + "headerdata": base64.b64encode(MSL.generate_msg_header( + message_id=message_id, + sender=sender, + is_handshake=True, + keyrequestdata=keyrequestdata + ).encode("utf-8")).decode("utf-8"), + "signature": "" + }, unpicklable=False) + data += json.dumps({ + "payload": base64.b64encode(json.dumps({ + "messageid": message_id, + "data": "", + "sequencenumber": 1, + "endofmsg": True + }).encode("utf-8")).decode("utf-8"), + "signature": "" + }) + + try: + r = session.post( + url=endpoint, + data=data + ) + except requests.HTTPError as e: + raise cls.log.exit(f"- Key exchange failed, response data is unexpected: {e.response.text}") + + key_exchange = r.json() # expecting no payloads, so this is fine + if "errordata" in key_exchange: + raise cls.log.exit("- Key exchange failed: " + json.loads(base64.b64decode( + key_exchange["errordata"] + ).decode())["errormsg"]) + + # parse the crypto keys + key_response_data = json.JSONDecoder().decode(base64.b64decode( + key_exchange["headerdata"] + ).decode("utf-8"))["keyresponsedata"] + + if key_response_data["scheme"] != str(scheme): + raise cls.log.exit("- Key exchange scheme mismatch occurred") + + key_data = key_response_data["keydata"] + if scheme == KeyExchangeSchemes.Widevine: + if "Remote" in cdm.device.__class__.__name__: + msl_keys.encryption, msl_keys.sign = cdm.device.exchange( + cdm.sessions[msl_keys.cdm_session], + license_res=key_data["cdmkeyresponse"], + enc_key_id=base64.b64decode(key_data["encryptionkeyid"]), + hmac_key_id=base64.b64decode(key_data["hmackeyid"]) + ) + cdm.parse_license(msl_keys.cdm_session, key_data["cdmkeyresponse"]) + else: + cdm.parse_license(msl_keys.cdm_session, key_data["cdmkeyresponse"]) + keys = cdm.get_keys(msl_keys.cdm_session) + msl_keys.encryption = MSL.get_widevine_key( + kid=base64.b64decode(key_data["encryptionkeyid"]), + keys=keys, + permissions=["AllowEncrypt", "AllowDecrypt"] + ) + msl_keys.sign = MSL.get_widevine_key( + kid=base64.b64decode(key_data["hmackeyid"]), + keys=keys, + permissions=["AllowSign", "AllowSignatureVerify"] + ) + else: + cipher_rsa = PKCS1_OAEP.new(msl_keys.rsa) + msl_keys.encryption = MSL.base64key_decode( + json.JSONDecoder().decode(cipher_rsa.decrypt( + base64.b64decode(key_data["encryptionkey"]) + ).decode("utf-8"))["k"] + ) + msl_keys.sign = MSL.base64key_decode( + json.JSONDecoder().decode(cipher_rsa.decrypt( + base64.b64decode(key_data["hmackey"]) + ).decode("utf-8"))["k"] + ) + msl_keys.mastertoken = key_response_data["mastertoken"] + + MSL.cache_keys(msl_keys, msl_keys_path) + cls.log.info("MSL handshake successful") + return cls( + session=session, + endpoint=endpoint, + sender=sender, + keys=msl_keys, + message_id=message_id + ) + + @staticmethod + def load_cache_data(msl_keys_path=None): + if not msl_keys_path or not os.path.exists(msl_keys_path): + return None + with open(msl_keys_path, encoding="utf-8") as fd: + msl_keys = jsonpickle.decode(fd.read()) + if msl_keys.rsa: + # noinspection PyTypeChecker + # expects RsaKey, but is a string, this is because jsonpickle can't pickle RsaKey object + # so as a workaround it exports to PEM, and then when reading, it imports that PEM back + # to an RsaKey :) + msl_keys.rsa = RSA.importKey(msl_keys.rsa) + # If it's expired or close to, return None as it's unusable + if msl_keys.mastertoken and ((datetime.utcfromtimestamp(int(json.JSONDecoder().decode( + base64.b64decode(msl_keys.mastertoken["tokendata"]).decode("utf-8") + )["expiration"])) - datetime.now()).total_seconds() / 60 / 60) < 10: + return None + return msl_keys + + @staticmethod + def cache_keys(msl_keys, msl_keys_path): + os.makedirs(os.path.dirname(msl_keys_path), exist_ok=True) + if msl_keys.rsa: + # jsonpickle can't pickle RsaKey objects :( + msl_keys.rsa = msl_keys.rsa.export_key() + with open(msl_keys_path, "w", encoding="utf-8") as fd: + fd.write(jsonpickle.encode(msl_keys)) + if msl_keys.rsa: + # re-import now + msl_keys.rsa = RSA.importKey(msl_keys.rsa) + + @staticmethod + def generate_msg_header(message_id, sender, is_handshake, userauthdata=None, keyrequestdata=None, + compression="GZIP"): + """ + The MSL header carries all MSL data used for entity and user authentication, message encryption + and verification, and service tokens. Portions of the MSL header are encrypted. + https://github.com/Netflix/msl/wiki/Messages#header-data + + :param message_id: number against which payload chunks are bound to protect against replay. + :param sender: ESN + :param is_handshake: This flag is set true if the message is a handshake message and will not include any + payload chunks. It will include keyrequestdata. + :param userauthdata: UserAuthData + :param keyrequestdata: KeyRequestData + :param compression: Supported compression algorithms. + + :return: The base64 encoded JSON String of the header + """ + header_data = { + "messageid": message_id, + "renewable": True, # MUST be True if is_handshake + "handshake": is_handshake, + "capabilities": { + "compressionalgos": [compression] if compression else [], + "languages": ["en-US"], # bcp-47 + "encoderformats": ["JSON"] + }, + "timestamp": int(time.time()), + # undocumented or unused: + "sender": sender, + "nonreplayable": False, + "recipient": "Netflix", + } + if userauthdata: + header_data["userauthdata"] = userauthdata + if keyrequestdata: + header_data["keyrequestdata"] = [keyrequestdata] + return jsonpickle.encode(header_data, unpicklable=False) + + @classmethod + def get_widevine_key(cls, kid, keys, permissions): + for key in keys: + if key.kid != kid: + continue + if key.type != "OPERATOR_SESSION": + cls.log.warning(f"Widevine Key Exchange: Wrong key type (not operator session) key {key}") + continue + if not set(permissions) <= set(key.permissions): + cls.log.warning(f"Widevine Key Exchange: Incorrect permissions, key {key}, needed perms {permissions}") + continue + return key.key + return None + + def send_message(self, endpoint, params, application_data, userauthdata=None): + message = self.create_message(application_data, userauthdata) + res = self.session.post(url=endpoint, data=message, params=params) + header, payload_data = self.parse_message(res.text) + if "errordata" in header: + raise self.log.exit( + "- MSL response message contains an error: {}".format( + json.loads(base64.b64decode(header["errordata"].encode("utf-8")).decode("utf-8")) + ) + ) + return header, payload_data + + def create_message(self, application_data, userauthdata=None): + self.message_id += 1 # new message must ue a new message id + + headerdata = self.encrypt(self.generate_msg_header( + message_id=self.message_id, + sender=self.sender, + is_handshake=False, + userauthdata=userauthdata + )) + + header = json.dumps({ + "headerdata": base64.b64encode(headerdata.encode("utf-8")).decode("utf-8"), + "signature": self.sign(headerdata).decode("utf-8"), + "mastertoken": self.keys.mastertoken + }) + + payload_chunks = [self.encrypt(json.dumps({ + "messageid": self.message_id, + "data": self.gzip_compress(json.dumps(application_data).encode("utf-8")).decode("utf-8"), + "compressionalgo": "GZIP", + "sequencenumber": 1, # todo ; use sequence_number from master token instead? + "endofmsg": True + }))] + + message = header + for payload_chunk in payload_chunks: + message += json.dumps({ + "payload": base64.b64encode(payload_chunk.encode("utf-8")).decode("utf-8"), + "signature": self.sign(payload_chunk).decode("utf-8") + }) + + return message + + def decrypt_payload_chunks(self, payload_chunks): + """ + Decrypt and extract data from payload chunks + + :param payload_chunks: List of payload chunks + :return: json object + """ + raw_data = "" + + for payload_chunk in payload_chunks: + # todo ; verify signature of payload_chunk["signature"] against payload_chunk["payload"] + # expecting base64-encoded json string + payload_chunk = json.loads(base64.b64decode(payload_chunk["payload"]).decode("utf-8")) + # decrypt the payload + payload_decrypted = AES.new( + key=self.keys.encryption, + mode=AES.MODE_CBC, + iv=base64.b64decode(payload_chunk["iv"]) + ).decrypt(base64.b64decode(payload_chunk["ciphertext"])) + payload_decrypted = Padding.unpad(payload_decrypted, 16) + payload_decrypted = json.loads(payload_decrypted.decode("utf-8")) + # decode and uncompress data if compressed + payload_data = base64.b64decode(payload_decrypted["data"]) + if payload_decrypted.get("compressionalgo") == "GZIP": + payload_data = zlib.decompress(payload_data, 16 + zlib.MAX_WBITS) + raw_data += payload_data.decode("utf-8") + + data = json.loads(raw_data) + if "error" in data: + error = data["error"] + error_display = error.get("display") + error_detail = re.sub(r" \(E3-[^)]+\)", "", error.get("detail", "")) + + #if error_display: + # self.log.critical(f"- {error_display}") + #if error_detail: + # self.log.critical(f"- {error_detail}") + + if not (error_display or error_detail): + self.log.critical(f"- {error}") + + sys.exit(1) + + return data["result"] + + def parse_message(self, message): + """ + Parse an MSL message into a header and list of payload chunks + + :param message: MSL message + :returns: a 2-item tuple containing message and list of payload chunks if available + """ + parsed_message = json.loads("[{}]".format(message.replace("}{", "},{"))) + + header = parsed_message[0] + encrypted_payload_chunks = parsed_message[1:] if len(parsed_message) > 1 else [] + if encrypted_payload_chunks: + payload_chunks = self.decrypt_payload_chunks(encrypted_payload_chunks) + else: + payload_chunks = {} + + return header, payload_chunks + + @staticmethod + def gzip_compress(data): + out = BytesIO() + with gzip.GzipFile(fileobj=out, mode="w") as fd: + fd.write(data) + return base64.b64encode(out.getvalue()) + + @staticmethod + def base64key_decode(payload): + length = len(payload) % 4 + if length == 2: + payload += "==" + elif length == 3: + payload += "=" + elif length != 0: + raise ValueError("Invalid base64 string") + return base64.urlsafe_b64decode(payload.encode("utf-8")) + + def encrypt(self, plaintext): + """ + Encrypt the given Plaintext with the encryption key + :param plaintext: + :return: Serialized JSON String of the encryption Envelope + """ + iv = get_random_bytes(16) + return json.dumps({ + "ciphertext": base64.b64encode( + AES.new( + self.keys.encryption, + AES.MODE_CBC, + iv + ).encrypt( + Padding.pad(plaintext.encode("utf-8"), 16) + ) + ).decode("utf-8"), + "keyid": "{}_{}".format(self.sender, json.loads( + base64.b64decode(self.keys.mastertoken["tokendata"]).decode("utf-8") + )["sequencenumber"]), + "sha256": "AA==", + "iv": base64.b64encode(iv).decode("utf-8") + }) + + def sign(self, text): + """ + Calculates the HMAC signature for the given text with the current sign key and SHA256 + :param text: + :return: Base64 encoded signature + """ + return base64.b64encode(HMAC.new(self.keys.sign, text.encode("utf-8"), SHA256).digest()) diff --git a/vinetrimmer/utils/MSL/schemes/EntityAuthentication.py b/vinetrimmer/utils/MSL/schemes/EntityAuthentication.py new file mode 100644 index 0000000..2bbd9b8 --- /dev/null +++ b/vinetrimmer/utils/MSL/schemes/EntityAuthentication.py @@ -0,0 +1,59 @@ +from vinetrimmer.utils.MSL import EntityAuthenticationSchemes +from vinetrimmer.utils.MSL.MSLObject import MSLObject + + +# noinspection PyPep8Naming +class EntityAuthentication(MSLObject): + def __init__(self, scheme, authdata): + """ + Data used to identify and authenticate the entity associated with a message. + https://github.com/Netflix/msl/wiki/Entity-Authentication-%28Configuration%29 + + :param scheme: Entity Authentication Scheme identifier + :param authdata: Entity Authentication data + """ + self.scheme = str(scheme) + self.authdata = authdata + + @classmethod + def Unauthenticated(cls, identity): + """ + The unauthenticated entity authentication scheme does not provide encryption or authentication and only + identifies the entity. Therefore entity identities can be harvested and spoofed. The benefit of this + authentication scheme is that the entity has control over its identity. This may be useful if the identity is + derived from or related to other data, or if retaining the identity is desired across state resets or in the + event of MSL errors requiring entity re-authentication. + """ + return cls( + scheme=EntityAuthenticationSchemes.Unauthenticated, + authdata={"identity": identity} + ) + + @classmethod + def Widevine(cls, devtype, keyrequest): + """ + The Widevine entity authentication scheme is used by devices with the Widevine CDM. It does not provide + encryption or authentication and only identifies the entity. Therefore entity identities can be harvested + and spoofed. The entity identity is composed from the provided device type and Widevine key request data. The + Widevine CDM properties can be extracted from the key request data. + + When coupled with the Widevine key exchange scheme, the entity identity can be cryptographically validated by + comparing the entity authentication key request data against the key exchange key request data. + + Note that the local entity will not know its entity identity when using this scheme. + + > Devtype + + An arbitrary value identifying the device type the local entity wishes to assume. The data inside the Widevine + key request may be optionally used to validate the claimed device type. + + :param devtype: Local entity device type + :param keyrequest: Widevine key request + """ + return cls( + scheme=EntityAuthenticationSchemes.Widevine, + authdata={ + "devtype": devtype, + "keyrequest": keyrequest + } + ) diff --git a/vinetrimmer/utils/MSL/schemes/KeyExchangeRequest.py b/vinetrimmer/utils/MSL/schemes/KeyExchangeRequest.py new file mode 100644 index 0000000..8b94295 --- /dev/null +++ b/vinetrimmer/utils/MSL/schemes/KeyExchangeRequest.py @@ -0,0 +1,80 @@ +import base64 + +from vinetrimmer.utils.MSL import KeyExchangeSchemes +from vinetrimmer.utils.MSL.MSLObject import MSLObject + + +# noinspection PyPep8Naming +class KeyExchangeRequest(MSLObject): + def __init__(self, scheme, keydata): + """ + Session key exchange data from a requesting entity. + https://github.com/Netflix/msl/wiki/Key-Exchange-%28Configuration%29 + + :param scheme: Key Exchange Scheme identifier + :param keydata: Key Request data + """ + self.scheme = str(scheme) + self.keydata = keydata + + @classmethod + def AsymmetricWrapped(cls, keypairid, mechanism, publickey): + """ + Asymmetric wrapped key exchange uses a generated ephemeral asymmetric key pair for key exchange. It will + typically be used when there is no other data or keys from which to base secure key exchange. + + This mechanism provides perfect forward secrecy but does not guarantee that session keys will only be available + to the requesting entity if the requesting MSL stack has been modified to perform the operation on behalf of a + third party. + + > Key Pair ID + + The key pair ID is included as a sanity check. + + > Mechanism & Public Key + + The following mechanisms are associated public key formats are currently supported. + + Field Public Key Format Description + RSA SPKI RSA-OAEP encrypt/decrypt + ECC SPKI ECIES encrypt/decrypt + JWEJS_RSA SPKI RSA-OAEP JSON Web Encryption JSON Serialization + JWE_RSA SPKI RSA-OAEP JSON Web Encryption Compact Serialization + JWK_RSA SPKI RSA-OAEP JSON Web Key + JWK_RSAES SPKI RSA PKCS#1 JSON Web Key + + :param keypairid: key pair ID + :param mechanism: asymmetric key type + :param publickey: public key + """ + return cls( + scheme=KeyExchangeSchemes.AsymmetricWrapped, + keydata={ + "keypairid": keypairid, + "mechanism": mechanism, + "publickey": base64.b64encode(publickey).decode("utf-8") + } + ) + + @classmethod + def Widevine(cls, keyrequest): + """ + Google Widevine provides a secure key exchange mechanism. When requested the Widevine component will issue a + one-time use key request. The Widevine server library can be used to authenticate the request and return + randomly generated symmetric keys in a protected key response bound to the request and Widevine client library. + The key response also specifies the key identities, types and their permitted usage. + + The Widevine key request also contains a model identifier and a unique device identifier with an expectation of + long-term persistence. These values are available from the Widevine client library and can be retrieved from + the key request by the Widevine server library. + + The Widevine client library will protect the returned keys from inspection or misuse. + + :param keyrequest: Base64-encoded Widevine CDM license challenge (PSSH: b'\x0A\x7A\x00\x6C\x38\x2B') + """ + if not isinstance(keyrequest, str): + keyrequest = base64.b64encode(keyrequest).decode() + return cls( + scheme=KeyExchangeSchemes.Widevine, + keydata={"keyrequest": keyrequest} + ) diff --git a/vinetrimmer/utils/MSL/schemes/UserAuthentication.py b/vinetrimmer/utils/MSL/schemes/UserAuthentication.py new file mode 100644 index 0000000..a7f052f --- /dev/null +++ b/vinetrimmer/utils/MSL/schemes/UserAuthentication.py @@ -0,0 +1,59 @@ +from vinetrimmer.utils.MSL.MSLObject import MSLObject +from vinetrimmer.utils.MSL.schemes import UserAuthenticationSchemes + + +# noinspection PyPep8Naming +class UserAuthentication(MSLObject): + def __init__(self, scheme, authdata): + """ + Data used to identify and authenticate the user associated with a message. + https://github.com/Netflix/msl/wiki/User-Authentication-%28Configuration%29 + + :param scheme: User Authentication Scheme identifier + :param authdata: User Authentication data + """ + self.scheme = str(scheme) + self.authdata = authdata + + @classmethod + def EmailPassword(cls, email, password): + """ + Email and password is a standard user authentication scheme in wide use. + + :param email: user email address + :param password: user password + """ + return cls( + scheme=UserAuthenticationSchemes.EmailPassword, + authdata={ + "email": email, + "password": password + } + ) + + @classmethod + def NetflixIDCookies(cls, netflixid, securenetflixid): + """ + Netflix ID HTTP cookies are used when the user has previously logged in to a web site. Possession of the + cookies serves as proof of user identity, in the same manner as they do when communicating with the web site. + + The Netflix ID cookie and Secure Netflix ID cookie are HTTP cookies issued by the Netflix web site after + subscriber login. The Netflix ID cookie is encrypted and identifies the subscriber and analogous to a + subscriber’s username. The Secure Netflix ID cookie is tied to a Netflix ID cookie and only sent over HTTPS + and analogous to a subscriber’s password. + + In some cases the Netflix ID and Secure Netflix ID cookies will be unavailable to the MSL stack or application. + If either or both of the Netflix ID or Secure Netflix ID cookies are absent in the above data structure the + HTTP cookie headers will be queried for it; this is only acceptable when HTTPS is used as the underlying + transport protocol. + + :param netflixid: Netflix ID cookie + :param securenetflixid: Secure Netflix ID cookie + """ + return cls( + scheme=UserAuthenticationSchemes.NetflixIDCookies, + authdata={ + "netflixid": netflixid, + "securenetflixid": securenetflixid + } + ) diff --git a/vinetrimmer/utils/MSL/schemes/__init__.py b/vinetrimmer/utils/MSL/schemes/__init__.py new file mode 100644 index 0000000..a1f61d7 --- /dev/null +++ b/vinetrimmer/utils/MSL/schemes/__init__.py @@ -0,0 +1,24 @@ +from enum import Enum + + +class Scheme(Enum): + def __str__(self): + return str(self.value) + + +class EntityAuthenticationSchemes(Scheme): + """https://github.com/Netflix/msl/wiki/Entity-Authentication-%28Configuration%29""" + Unauthenticated = "NONE" + Widevine = "WIDEVINE" + + +class UserAuthenticationSchemes(Scheme): + """https://github.com/Netflix/msl/wiki/User-Authentication-%28Configuration%29""" + EmailPassword = "EMAIL_PASSWORD" + NetflixIDCookies = "NETFLIXID" + + +class KeyExchangeSchemes(Scheme): + """https://github.com/Netflix/msl/wiki/Key-Exchange-%28Configuration%29""" + AsymmetricWrapped = "ASYMMETRIC_WRAPPED" + Widevine = "WIDEVINE" diff --git a/vinetrimmer/utils/__init__.py b/vinetrimmer/utils/__init__.py new file mode 100644 index 0000000..2de2c35 --- /dev/null +++ b/vinetrimmer/utils/__init__.py @@ -0,0 +1,55 @@ +from langcodes import Language, closest_match + +from vinetrimmer.constants import LANGUAGE_MAX_DISTANCE +from vinetrimmer.vendor.pymp4.parser import Box + + +def get_boxes(data, box_type, as_bytes=False): + """Scan a byte array for a wanted box, then parse and yield each find.""" + # using slicing to get to the wanted box is done because parsing the entire box and recursively + # scanning through each box and its children often wouldn't scan far enough to reach the wanted box. + # since it doesnt care what child box the wanted box is from, this works fine. + if not isinstance(data, (bytes, bytearray)): + raise ValueError("data must be bytes") + while True: + try: + index = data.index(box_type) + except ValueError: + break + if index < 0: + break + if index > 4: + index -= 4 # size is before box type and is 4 bytes long + data = data[index:] + try: + box = Box.parse(data) + except IOError: + # TODO: Does this miss any data we may need? + break + if as_bytes: + box = Box.build(box) + yield box + + +def is_close_match(language, languages): + if not (language and languages and all(languages)): + return False + languages = list(map(str, [x for x in languages if x])) + return closest_match(language, languages)[1] <= LANGUAGE_MAX_DISTANCE + + +def get_closest_match(language, languages): + match, distance = closest_match(language, list(map(str, languages))) + if distance > LANGUAGE_MAX_DISTANCE: + return None + return Language.get(match) + + +def try_get(obj, func): + try: + return func(obj) + except (AttributeError, IndexError, KeyError, TypeError): + return None + +def short_hash(input): + return base_encode(int(md5(input).hexdigest(), 16)) diff --git a/vinetrimmer/utils/adobepass.py b/vinetrimmer/utils/adobepass.py new file mode 100644 index 0000000..7a11e7d --- /dev/null +++ b/vinetrimmer/utils/adobepass.py @@ -0,0 +1,19 @@ +import os +from abc import ABC + +from yt_dlp import YoutubeDL +from yt_dlp.extractor.adobepass import AdobePassIE + + +class AdobePassVT(AdobePassIE, ABC): + def __init__(self, credential, get_cache): + super().__init__( + YoutubeDL( + { + "ap_mso": credential.extra, # See yt_dlp.extractor.adobepass for supported MSO providers + "ap_username": credential.username, + "ap_password": credential.password, + "cachedir": os.path.realpath(get_cache("adobepass")), + } + ) + ) diff --git a/vinetrimmer/utils/click.py b/vinetrimmer/utils/click.py new file mode 100644 index 0000000..eb11e12 --- /dev/null +++ b/vinetrimmer/utils/click.py @@ -0,0 +1,214 @@ +import importlib +import logging +import re + +import click + +from vinetrimmer import services +from vinetrimmer.services.BaseService import BaseService +from vinetrimmer.utils.collections import as_list + + +log = logging.getLogger("click") + + +class ContextData: + def __init__(self, config, vaults, cdm, profile=None, cookies=None, credentials=None): + self.config = config + self.vaults = vaults + self.cdm = cdm + self.profile = profile + self.cookies = cookies + self.credentials = credentials + + +class AliasedGroup(click.Group): + def get_command(self, ctx, cmd_name): + rv = click.Group.get_command(self, ctx, cmd_name) + if rv is not None: + setattr(services, cmd_name, getattr( + importlib.import_module(f"vinetrimmer.services.{cmd_name.lower()}"), cmd_name + )) + return rv + + for key, aliases in services.SERVICE_MAP.items(): + if cmd_name.lower() in map(str.lower, aliases): + setattr(services, key, getattr(importlib.import_module(f"vinetrimmer.services.{key.lower()}"), key)) + return click.Group.get_command(self, ctx, key) + + service = services.get_service_key(cmd_name) + + if not service: + title_id = None + + for x in dir(services): + x = getattr(services, x) + + if isinstance(x, type) and issubclass(x, BaseService) and x != BaseService: + title_re = as_list(getattr(x, "TITLE_RE", [])) + for regex in title_re: + m = re.search(regex, cmd_name) + if m and m.group().startswith(("http://", "https://", "urn:")): + title_id = m.group("id") + break + + if title_id: + ctx.params["service_name"] = x.__name__ + setattr(services, x.__name__, getattr( + importlib.import_module(f"vinetrimmer.services.{x.__name__.lower()}"), x.__name__ + )) + importlib.import_module(f"vinetrimmer.services.{x.__name__.lower()}") + ctx.params["title"] = cmd_name + return click.Group.get_command(self, ctx, x.__name__) + + if not title_id: + raise log.exit(" - Unable to guess service from title ID") + + def list_commands(self, ctx): + return sorted(self.commands, key=str.casefold) + + +def _choice(ctx, param, value, value_map): + if value is None: + return None + + if value.lower() in value_map: + return value_map[value.lower()] + else: + valid_values = {x: None for x in value_map.values()} + valid_values = ", ".join(repr(x) for x in valid_values) + ctx.fail(f"Invalid value for {param.name!r}: {value!r} is not one of {valid_values}.") + + +def acodec_param(ctx, param, value): + return _choice(ctx, param, value, { + "aac": "AAC", + "ac3": "AC3", + "ac-3": "AC3", + "dd": "AC3", + "ec3": "EC3", + "ec-3": "EC3", + "eac3": "EC3", + "e-ac3": "EC3", + "e-ac-3": "EC3", + "dd+": "EC3", + "ddp": "EC3", + "vorb": "VORB", + "vorbis": "VORB", + "opus": "OPUS", + }) + + +def language_param(ctx, param, value): + if isinstance(value, list): + return value + if not value: + return [] + return re.split(r"\s*[,;]\s*", value) + + +def quality_param(ctx, param, value): + if not value: + return None + + if value.lower() == "sd": + return "SD" + + if value.lower() == "4k": + return 2160 + + try: + return int(value.lower().rstrip("p")) + except TypeError: + ctx.fail( + f"expected string for int() conversion, got {value!r} of type {value.__class__.__name__}", + param, + ctx + ) + except ValueError: + ctx.fail(f"{value!r} is not a valid integer", param, ctx) + + +def range_param(ctx, param, value): + return _choice(ctx, param, value, { + "sdr": "SDR", + "hdr": "HDR10", + "hdr10": "HDR10", + "hlg": "HLG", + "dv": "DV", + "dovi": "DV", + }) + + +def vcodec_param(ctx, param, value): + return _choice(ctx, param, value, { + "h264": "H264", + "avc": "H264", + "h265": "H265", + "hevc": "H265", + "vp9": "VP9", + "av1": "AV1", + }) + + +def wanted_param(ctx, param, value): + MIN_EPISODE = 0 + MAX_EPISODE = 9999 + + def parse_tokens(*tokens): + """ + Parse multiple tokens or ranged tokens as '{s}x{e}' strings. + + Supports exclusioning by putting a `-` before the token. + + Example: + >>> parse_tokens("S01E01") + ["1x1"] + >>> parse_tokens("S02E01", "S02E03-S02E05") + ["2x1", "2x3", "2x4", "2x5"] + >>> parse_tokens("S01-S05", "-S03", "-S02E01") + ["1x0", "1x1", ..., "2x0", (...), "2x2", (...), "4x0", ..., "5x0", ...] + """ + if len(tokens) == 0: + return [] + computed = [] + exclusions = [] + for token in tokens: + exclude = token.startswith("-") + if exclude: + token = token[1:] + parsed = [ + re.match(r"^S(?P<season>\d+)(E(?P<episode>\d+))?$", x, re.IGNORECASE) + for x in re.split(r"[:-]", token) + ] + if len(parsed) > 2: + ctx.fail(f"Invalid token, only a left and right range is acceptable: {token}") + if len(parsed) == 1: + parsed.append(parsed[0]) + if any(x is None for x in parsed): + ctx.fail(f"Invalid token, syntax error occurred: {token}") + from_season, from_episode = [ + int(v) if v is not None else MIN_EPISODE + for k, v in parsed[0].groupdict().items() if parsed[0] + ] + to_season, to_episode = [ + int(v) if v is not None else MAX_EPISODE + for k, v in parsed[1].groupdict().items() if parsed[1] + ] + if from_season > to_season: + ctx.fail(f"Invalid range, left side season cannot be bigger than right side season: {token}") + if from_season == to_season and from_episode > to_episode: + ctx.fail(f"Invalid range, left side episode cannot be bigger than right side episode: {token}") + for s in range(from_season, to_season + 1): + for e in range( + from_episode if s == from_season else 0, + (MAX_EPISODE if s < to_season else to_episode) + 1 + ): + (computed if not exclude else exclusions).append(f"{s}x{e}") + for exclusion in exclusions: + if exclusion in computed: + computed.remove(exclusion) + return list(set(computed)) + + if value: + return parse_tokens(*re.split(r"\s*[,;]\s*", value)) diff --git a/vinetrimmer/utils/collections.py b/vinetrimmer/utils/collections.py new file mode 100644 index 0000000..1f86138 --- /dev/null +++ b/vinetrimmer/utils/collections.py @@ -0,0 +1,52 @@ +import itertools +from typing import Iterable, Sequence + + +def as_lists(*args): + """Convert any input objects to list objects.""" + for item in args: + yield item if isinstance(item, list) else [item] + + +def as_list(*args): + """ + Convert any input objects to a single merged list object. + + Example: + >>> as_list('foo', ['buzz', 'bizz'], 'bazz', 'bozz', ['bar'], ['bur']) + ['foo', 'buzz', 'bizz', 'bazz', 'bozz', 'bar', 'bur'] + """ + if args == (None,): + return [] + return list(itertools.chain.from_iterable(as_lists(*args))) + + +def flatten(items, ignore_types=str): + """ + Flatten items recursively. + + Example: + >>> list(flatten(["foo", [["bar", ["buzz", [""]], "bee"]]])) + ['foo', 'bar', 'buzz', '', 'bee'] + >>> list(flatten("foo")) + ['foo'] + >>> list(flatten({1}, set)) + [{1}] + """ + if isinstance(items, (Iterable, Sequence)) and not isinstance(items, ignore_types): + for i in items: + yield from flatten(i, ignore_types) + else: + yield items + + +def merge_dict(*dicts): + """Recursively merge dicts into dest in-place.""" + dest = dicts[0] + for d in dicts[1:]: + for key, value in d.items(): + if isinstance(value, dict): + node = dest.setdefault(key, {}) + merge_dict(node, value) + else: + dest[key] = value diff --git a/vinetrimmer/utils/drmtoday.py b/vinetrimmer/utils/drmtoday.py new file mode 100644 index 0000000..59983b9 --- /dev/null +++ b/vinetrimmer/utils/drmtoday.py @@ -0,0 +1,25 @@ +# Most of these were taken from the old player JS. +# 40002 was inferred based on tests with actual keys. +DRMTODAY_RESPONSE_CODES = { + "00000": "Success", + "01000": "General Internal Error", + "02000": "General Request Error", + "03000": "General Request Authentication Error", + "30000": "General DRM Error", + "40000": "General Widevine Modular Error", + "40001": "Widevine Device Certificate Revocation", + "40002": "Widevine Device Certificate Serial Number Revocation", + "41000": "General Widevine Classic Error", + "42000": "General PlayReady Error", + "43000": "General FairPlay Error", + "44000": "General OMA Error", + "44001": "OMA Device Registration Failed", + "45000": "General CDRM Error", + "45001": "CDRM Device Registration Failed", + "70000": "General Output Protection Error", + "70001": "All keys filtered by EOP settings", + "80000": "General CSL Error", + "80001": "Too many concurrent streams", + "90000": "General GBL Error", + "90001": "License delivery prohibited in your region" +} diff --git a/vinetrimmer/utils/gen_esn.py b/vinetrimmer/utils/gen_esn.py new file mode 100644 index 0000000..e01e112 --- /dev/null +++ b/vinetrimmer/utils/gen_esn.py @@ -0,0 +1,55 @@ +from datetime import datetime, timedelta +import os +import logging +import random + + +log = logging.getLogger("NF-ESN") + +def chrome_esn_generator(): + + ESN_GEN = "".join(random.choice("0123456789ABCDEF") for _ in range(30)) + esn_file = '.esn' + + def gen_file(): + with open(esn_file, 'w') as file: + file.write(f'NFCDIE-03-{ESN_GEN}') + + if not os.path.isfile(esn_file): + log.warning("Generating a new Chrome ESN") + gen_file() + + file_datetime = datetime.fromtimestamp(os.path.getmtime(esn_file)) + time_diff = datetime.now() - file_datetime + if time_diff > timedelta(hours=6): + log.warning("Old ESN detected, Generating a new Chrome ESN") + gen_file() + + with open(esn_file, 'r') as f: + esn = f.read() + + return esn + +def edge_esn_generator(): + + ESN_GEN = "".join(random.choice("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") for _ in range(24)) + esn_file = '.esn' + + def gen_file(): + with open(esn_file, 'w') as file: + file.write(f'NFCDIE-03-{ESN_GEN}1111') + + if not os.path.isfile(esn_file): + log.warning("Generating a new Edge ESN") + gen_file() + + file_datetime = datetime.fromtimestamp(os.path.getmtime(esn_file)) + time_diff = datetime.now() - file_datetime + if time_diff > timedelta(hours=6): + log.warning("Old ESN detected, Generating a new Edge ESN") + gen_file() + + with open(esn_file, 'r') as f: + esn = f.read() + + return esn \ No newline at end of file diff --git a/vinetrimmer/utils/io.py b/vinetrimmer/utils/io.py new file mode 100644 index 0000000..855703c --- /dev/null +++ b/vinetrimmer/utils/io.py @@ -0,0 +1,301 @@ +import asyncio +import contextlib +import logging +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import httpx +import pproxy +import requests +import yaml + +from vinetrimmer import config +from vinetrimmer.utils.collections import as_list + + +def load_yaml(path): + if not os.path.isfile(path): + return {} + with open(path) as fd: + return yaml.safe_load(fd) + + +_ip_info = None + + +def get_ip_info(session=None, fresh=False): + """Use extreme-ip-lookup.com to get IP location information.""" + global _ip_info + + if fresh or not _ip_info: + # alternatives: http://www.geoplugin.net/json.gp, http://ip-api.com/json/, https://extreme-ip-lookup.com/json + # _ip_info = (session or httpx).get("https://ip-api.com/json/").json() + _ip_info = (session or httpx).get("http://ip-api.com/json/").json() + + return _ip_info + + +@contextlib.asynccontextmanager +async def start_pproxy(host, port, username, password): + rerouted_proxy = "http://localhost:8081" + server = pproxy.Server(rerouted_proxy) + remote = pproxy.Connection(f"http+ssl://{host}:{port}#{username}:{password}") + handler = await server.start_server(dict(rserver=[remote])) + try: + yield rerouted_proxy + finally: + handler.close() + await handler.wait_closed() + + +def download_range(url, count, start=0, proxy=None): + """Download n bytes without using the Range header due to support issues.""" + # TODO: Can this be done with Aria2c? + executable = shutil.which("curl") + if not executable: + raise EnvironmentError("Track needs curl to download a chunk of data but wasn't found...") + + arguments = [ + executable, + "-s", # use -s instead of --no-progress-meter due to version requirements + "-L", # follow redirects, e.g. http->https + "--proxy-insecure", # disable SSL verification of proxy + "--output", "-", # output to stdout + "--url", url + ] + if proxy: + arguments.extend(["--proxy", proxy]) + + curl = subprocess.Popen( + arguments, + stdout=subprocess.PIPE, + stderr=open(os.devnull, "wb"), + shell=False + ) + buffer = b'' + location = -1 + while len(buffer) < count: + stdout = curl.stdout + data = b'' + if stdout: + data = stdout.read(1) + if len(data) > 0: + location += len(data) + if location >= start: + buffer += data + else: + if curl.poll() is not None: + break + curl.kill() # stop downloading + return buffer + + +async def aria2c(uri, out, headers=None, proxy=None): + """ + Downloads file(s) using Aria2(c). + + Parameters: + uri: URL to download. If uri is a list of urls, they will be downloaded and + concatenated into one file. + out: The output file path to save to. + headers: Headers to apply on aria2c. + proxy: Proxy to apply on aria2c. + """ + executable = shutil.which("aria2c") or shutil.which("aria2") + if not executable: + raise EnvironmentError("Aria2c executable not found...") + + arguments = [ + executable, + "-c", # Continue downloading a partially downloaded file + "--remote-time", # Retrieve timestamp of the remote file from the and apply if available + "-o", os.path.basename(out), # The file name of the downloaded file, relative to -d + "-x", "16", # The maximum number of connections to one server for each download + "-j", "16", # The maximum number of parallel downloads for every static (HTTP/FTP) URL + "-s", "16", # Download a file using N connections. + "--allow-overwrite=true", + "--auto-file-renaming=false", + "--retry-wait", "5", # Set the seconds to wait between retries. + "--max-tries", "15", + "--max-file-not-found", "15", + "--summary-interval", "0", + "--file-allocation", "none" if sys.platform == "win32" else "falloc", + "--console-log-level", "warn", + "--download-result", "hide" + ] + + for option, value in config.config.aria2c.items(): + arguments.append(f"--{option.replace('_', '-')}={value}") + + for header, value in (headers or {}).items(): + if header.lower() == "accept-encoding": + # we cannot set an allowed encoding, or it will return compressed + # and the code is not set up to uncompress the data + continue + arguments.extend(["--header", f"{header}: {value}"]) + + segmented = isinstance(uri, list) + segments_dir = f"{out}_segments" + + if segmented: + uri = "\n".join([ + f"{url}\n" + f"\tdir={segments_dir}\n" + f"\tout={i:08}.mp4" + for i, url in enumerate(uri) + ]) + + if proxy: + arguments.append("--all-proxy") + if proxy.lower().startswith("https://"): + auth, hostname = proxy[8:].split("@") + async with start_pproxy(*hostname.split(":"), *auth.split(":")) as pproxy_: + arguments.extend([pproxy_, "-d"]) + if segmented: + arguments.extend([segments_dir, "-i-"]) + proc = await asyncio.create_subprocess_exec(*arguments, stdin=subprocess.PIPE) + await proc.communicate(as_list(uri)[0].encode("utf-8")) + else: + arguments.extend([os.path.dirname(out), uri]) + proc = await asyncio.create_subprocess_exec(*arguments) + await proc.communicate() + else: + arguments.append(proxy) + + try: + if segmented: + subprocess.run( + arguments + ["-d", segments_dir, "-i-"], + input=as_list(uri)[0], + encoding="utf-8", + check=True + ) + else: + subprocess.run( + arguments + ["-d", os.path.dirname(out), uri], + check=True + ) + except subprocess.CalledProcessError: + raise ValueError("Aria2c failed too many times, aborting") + + if segmented: + # merge the segments together + with open(out, "wb") as ofd: + for file in sorted(os.listdir(segments_dir)): + file = os.path.join(segments_dir, file) + with open(file, "rb") as ifd: + data = ifd.read() + # Apple TV+ needs this done to fix audio decryption + data = re.sub(b"(tfhd\x00\x02\x00\x1a\x00\x00\x00\x01\x00\x00\x00)\x02", b"\\g<1>\x01", data) + ofd.write(data) + os.unlink(file) + os.rmdir(segments_dir) + + print() + + +async def saldl(uri, out, headers=None, proxy=None): + if headers: + headers.update({k: v for k, v in headers.items() if k.lower() != "accept-encoding"}) + + executable = shutil.which("saldl") or shutil.which("saldl-win64") or shutil.which("saldl-win32") + if not executable: + raise EnvironmentError("Saldl executable not found...") + + arguments = [ + executable, + # "--no-status", + "--skip-TLS-verification", + "--resume", + "--merge-in-order", + "-c8", + "--auto-size", "1", + "-D", os.path.dirname(out), + "-o", os.path.basename(out), + ] + + if headers: + arguments.extend([ + "--custom-headers", + "\r\n".join([f"{k}: {v}" for k, v in headers.items()]) + ]) + + if proxy: + arguments.extend(["--proxy", proxy]) + + if isinstance(uri, list): + raise ValueError("Saldl code does not yet support multiple uri (e.g. segmented) downloads.") + arguments.append(uri) + + try: + subprocess.run(arguments, check=True) + except subprocess.CalledProcessError: + raise ValueError("Saldl failed too many times, aborting") + + print() + + +async def m3u8dl(uri: str, out: str, track): + executable = shutil.which("N_m3u8DL-RE") or shutil.which("m3u8DL") + if not executable: + raise EnvironmentError("N_m3u8DL-RE executable not found...") + + ffmpeg_binary = shutil.which("ffmpeg") + + arguments = [ + executable, + uri, + "--save-dir", os.path.dirname(out), + "--tmp-dir", os.path.dirname(out), + "--save-name", os.path.basename(out).replace(".mp4", ""), + "--write-meta-json", "False", + "--log-level", "ERROR", + "--thread-count", "96", + "--download-retry-count", "8", + "--ffmpeg-binary-path", ffmpeg_binary, + "--binary-merge", + "--decryption-engine", "SHAKA_PACKAGER", + "--http-request-timeout", "8", + "--live-real-time-merge" + ] + + if track.__class__.__name__ == "VideoTrack": + if track.height: + arguments.extend([ + "-sv", f"res='{track.height}*':codec='{track.codec}':for=best" + ]) + else: + arguments.extend([ + "-sv", "best" + ]) + + arguments.extend([ + "-da", "all", + "-ds", "all", + ]) + elif track.__class__.__name__ == "AudioTrack": + if track.language: + arguments.extend([ + "-sa", f"lang='{track.language}':for=best" + ]) + else: + arguments.extend([ + "-sa", "best" + ]) + + arguments.extend([ + "-dv", "all", + "-ds", "all", + "-M", "format=mp4:muxer=ffmpeg", + ]) + else: + raise ValueError(f"{track.__class__.__name__} not supported yet!") + + try: + p = subprocess.run(arguments, check=True) + except subprocess.CalledProcessError: + raise ValueError("N_m3u8DL-RE failed too many times, aborting") + print() diff --git a/vinetrimmer/utils/pyhulu.py b/vinetrimmer/utils/pyhulu.py new file mode 100644 index 0000000..330e44c --- /dev/null +++ b/vinetrimmer/utils/pyhulu.py @@ -0,0 +1,104 @@ +import base64 +import hashlib +import logging +import random + +import pyhulu + + +class Device: # pylint: disable=too-few-public-methods + """Data class used for containing device attributes.""" + + def __init__(self, device_code, device_key): + self.device_code = str(device_code) + + if isinstance(device_key, str): + self.device_key = bytes.fromhex(device_key) + else: + self.device_key = device_key + + if len(self.device_code) != 3: + raise ValueError("Invalid device code length") + + if len(self.device_key) != 16: + raise ValueError("Invalid device key length") + + def __repr__(self): + return "<Device device_code={}, device_key={}>".format( + self.device_code, + base64.b64encode(self.device_key).decode("utf-8") + ) + + +class HuluClient(pyhulu.HuluClient): + def __init__(self, device, session, version=1, **kwargs): + self.logger = logging.getLogger(__name__) + self.device = device + self.session = session + self.version = version or 1 + self.extra_playlist_params = kwargs + + self.session_key, self.server_key = self.get_session_key() + + def load_playlist(self, video_id): + """ + load_playlist() + + Method to get a playlist containing the MPD + and license URL for the provided video ID and return it + + @param video_id: String of the video ID to get a playlist for + + @return: Dict of decrypted playlist response + """ + params = { + "device_identifier": hashlib.md5().hexdigest().upper(), + "deejay_device_id": int(self.device.device_code), + "version": self.version, + "content_eab_id": video_id, + "rv": random.randrange(100000, 1000000), + "kv": self.server_key + } + params.update(self.extra_playlist_params) + + r = self.session.post("https://play.hulu.com/v6/playlist", json=params) + ciphertext = self.__get_ciphertext(r.text, params) + + return self.decrypt_response(self.session_key, ciphertext) + + def get_session_key(self): + """ + get_session_key() + + Method to do a Hulu config request and calculate + the session key against device key and current server key + + @return: Session key in bytes, and the config key ID. + """ + random_value = random.randrange(100000, 1000000) + nonce = hashlib.md5(",".join([ + self.device.device_key.hex(), + self.device.device_code, + str(self.version), + str(random_value) + ]).encode("utf-8")).hexdigest() + + payload = { + "rv": random_value, + "mozart_version": "1", + "region": "US", + "version": self.version, + "device": self.device.device_code, + "encrypted_nonce": nonce + } + + r = self.session.post("https://play.hulu.com/config", data=payload) + ciphertext = self.__get_ciphertext(r.text, payload) + + config = self.decrypt_response(self.device.device_key, ciphertext) + + derived_key_array = bytearray() + for device_byte, server_byte in zip(self.device.device_key, bytes.fromhex(config["key"])): + derived_key_array.append(device_byte ^ server_byte) + + return bytes(derived_key_array), config["key_id"] diff --git a/vinetrimmer/utils/regex.py b/vinetrimmer/utils/regex.py new file mode 100644 index 0000000..fcdb44a --- /dev/null +++ b/vinetrimmer/utils/regex.py @@ -0,0 +1,10 @@ +import re + + +def find(pattern, string, group=None): + if group: + m = re.search(pattern, string) + if m: + return m.group(group) + else: + return next(iter(re.findall(pattern, string)), None) diff --git a/vinetrimmer/utils/subprocess.py b/vinetrimmer/utils/subprocess.py new file mode 100644 index 0000000..152e9f9 --- /dev/null +++ b/vinetrimmer/utils/subprocess.py @@ -0,0 +1,29 @@ +import json +import subprocess + + +def ffprobe(uri): + """Use ffprobe on the provided data to get stream information.""" + args = [ + "ffprobe", + "-v", "quiet", + "-of", "json", + "-show_streams" + ] + if isinstance(uri, str): + args.extend([ + "-f", "lavfi", + "-i", "movie={}[out+subcc]".format(uri.replace("\\", '/').replace(":", "\\\\:")) + ]) + elif isinstance(uri, bytes): + args.append("pipe:") + try: + ff = subprocess.run( + args, + input=uri if isinstance(uri, bytes) else None, + check=True, + capture_output=True + ) + except subprocess.CalledProcessError: + return {} + return json.loads(ff.stdout.decode("utf-8")) diff --git a/vinetrimmer/utils/ttml2ssa.py b/vinetrimmer/utils/ttml2ssa.py new file mode 100644 index 0000000..32290f2 --- /dev/null +++ b/vinetrimmer/utils/ttml2ssa.py @@ -0,0 +1,1177 @@ +# encoding: utf-8 +# +# -------------------------------------------- +# based on https://github.com/yuppity/ttml2srt +# -------------------------------------------- +# SPDX-License-Identifier: LGPL-2.1-or-later + +from __future__ import unicode_literals, absolute_import, division + +import re +import io +import os.path +import json +from collections import OrderedDict +from copy import deepcopy +import tempfile + +try: + from defusedxml import minidom # type: ignore +except: + from xml.dom import minidom + + +class TimestampConverter(object): + + def __init__(self, frame_rate=23.976, tick_rate=1): + self.tick_rate = tick_rate + self.frame_rate = frame_rate + + def timeexpr_to_ms(self, *args): + return self._timeexpr_to_ms(*args) + + def _timeexpr_to_ms(self, time_expr): + """Use the given time expression to get a matching conversion method + to overwrite self.timeexpr_to_ms() with. + """ + + self.timeexpr_to_ms = self.determine_ms_convfn(time_expr) + return self.timeexpr_to_ms(time_expr) + + def _hhmmss_to_ms(self, hh, mm, ss): + return hh * 3600 * 1000 + mm * 60 * 1000 + ss * 1000 + + def subrip_to_ms(self, timestamp): + """Desconstruct SubRip timecode down to milliseconds + """ + + hh, mm, ss, ms = re.split(r'[:,]', timestamp) + return int(int(hh) * 3.6e6 + int(mm) * 60000 + int(ss) * 1000 + int(ms)) + + def _metric_to_ms(self, metric_multiplier, metric_value): + return int(metric_multiplier * metric_value) + + def _ms_to_hhmmssms(self, ms): + hh = int(ms / 3.6e6) + mm = int((ms % 3.6e6) / 60000) + ss = int((ms % 60000) / 1000) + ms = int(ms % 1000) + + return hh, mm, ss, ms + + def ms_to_subrip(self, ms): + """Build SubRip timecode from milliseconds + """ + + hh, mm, ss, ms = self._ms_to_hhmmssms(ms) + return '{:02d}:{:02d}:{:02d},{:03d}'.format(hh, mm, ss, ms) + + def ms_to_ssa(self, ms): + """Build SSA/ASS timecode from milliseconds + """ + + hh, mm, ss, ms = self._ms_to_hhmmssms(ms) + return '{:01d}:{:02d}:{:02d}.{:02d}'.format(hh, mm, ss, int(ms / 10)) + + def frames_to_ms(self, frames): + """Convert frame count to ms + """ + + return int(int(frames) * (1000 / self.frame_rate)) + + def offset_frames_to_ms(self, time): + """Convert offset-time expression with f metric to milliseconds. + """ + + frames = float(time[:-1]) + return int(int(frames) * (1000 / self.frame_rate)) + + def offset_ticks_to_ms(self, time): + """Convert offset-time expression with t metric to milliseconds. + """ + + ticks = int(time[:-1]) + seconds = 1.0 / self.tick_rate + return (seconds * ticks) * 1000 + + def offset_hours_to_ms(self, time): + """Convert offset-time expression with h metric to milliseconds. + """ + + hours = float(time[:-1]) + return self._metric_to_ms(3.6e6, hours) + + def offset_minutes_to_ms(self, time): + """Convert offset-time expression with m metric to milliseconds. + """ + + return self._metric_to_ms(60 * 1000, float(time[:-1])) + + def offset_seconds_to_ms(self, time): + """Convert offset-time expression with s metric to milliseconds. + """ + + seconds = float(time[:-1]) + return self._metric_to_ms(1000, seconds) + + def offset_ms_to_ms(self, time): + """Convert offset-time expression with ms metric to milliseconds. + """ + + ms = int(time[:-2]) + return ms + + def fraction_timestamp_to_ms(self, timestamp): + """Convert hh:mm:ss.fraction to milliseconds + """ + + hh, mm, ss, fraction = re.split(r'[:.]', timestamp) + hh, mm, ss = [int(i) for i in (hh, mm, ss)] + # Resolution beyond ms is useless for our purposes + ms = int(fraction[:3]) + + return self._hhmmss_to_ms(hh, mm, ss) + ms + + def frame_timestamp_to_ms(self, timestamp): + """Convert hh:mm:ss:frames to milliseconds + + Will handle hh:mm:ss:frames.sub-frames by discarding the sub-frame part + """ + + hh, mm, ss, frames = [int(i) for i in timestamp.split('.')[0].split(':')] + hhmmss_ms = self._hhmmss_to_ms(hh, mm, ss) + ms = self.frames_to_ms(frames) + return hhmmss_ms + ms + + def determine_ms_convfn(self, time_expr): + """Determine approriate ms conversion fn to pass the time expression to. + + Args: + time_exrp (str): TTML time expression + + Return: + Conversion method (callable) + + Strips the time expression of digits and uses the resulting string as + a key to a dict of conversion methods. + """ + + # Map time expression delimiters to conversion methods. Saves + # us from having to exec multibranch code on each line but assumes all + # time expressions to be of the same form. + time_expr_fns = { + + # clock-time, no frames or fraction + # Example(s): "00:02:23" + '::': self.frame_timestamp_to_ms, + + # clock-time, frames + # Example(s): "00:02:23:12", "00:02:23:12.222" + ':::': self.frame_timestamp_to_ms, + ':::.': self.frame_timestamp_to_ms, + + # clock-time, fraction + # Example(s): "00:02:23.283" + '::.': self.fraction_timestamp_to_ms, + + # offset-time, hour metric + # Example(s): "1h", "1.232837372637h" + 'h': self.offset_hours_to_ms, + '.h': self.offset_hours_to_ms, + + # offset-time, minute metric + # Example(s): "1m", "13.72986323m" + 'm': self.offset_minutes_to_ms, + '.m': self.offset_minutes_to_ms, + + # offset-time, second metric + # Example(s): "1s", "113.2312312s" + 's': self.offset_seconds_to_ms, + '.s': self.offset_seconds_to_ms, + + # offset-time, millisecond metric + # Example(s): "1ms", "1000.1231231231223ms" + 'ms': self.offset_ms_to_ms, + '.ms': self.offset_ms_to_ms, + + # offset-time, frame metric + # Example(s): "100f" + 'f': self.offset_frames_to_ms, + '.f': self.offset_frames_to_ms, + + # offset-time, tick metric + # Example(s): "19298323t" + 't': self.offset_ticks_to_ms, + '.t': self.offset_ticks_to_ms, + + } + + try: + delims = ''.join([i for i in time_expr if not i.isdigit()]) + return time_expr_fns[delims] + except KeyError: + raise NotImplementedError( + 'Unknown timestamp format ("{}")'.format(time_expr)) + + +class Ttml2Ssa(object): + + VERSION = '0.3.8' + + TIME_BASES = [ + 'media', + 'smpte', + ] + + SCALE = { + 'NTSC2PAL' : 23.976/25, + 'PAL2NTSC' : 25/23.976, + 'NTSC2FILM' : 23.976/24, + 'PAL2FILM' : 25/24, + 'FILM2NTSC' : 24/23.976, + 'FILM2PAL' : 24/25 + } + + TOP_MARKER = '{\\an8}' + + def __init__(self, shift=0, source_fps=23.976, scale_factor=1, subtitle_language=None): + self.shift = shift + self.source_fps = source_fps + self.subtitle_language = subtitle_language + self.scale_factor = scale_factor + self.ssa_timestamp_min_sep = 200 + self.use_cosmetic_filter = True + self.use_language_filter = True + self.fix_amazon_errors = True + + self.allow_italics = True + self.allow_top_pos = True + + self.allow_timestamp_manipulation = True + self.fix_timestamp_collisions = True + self.fix_duplicated_entries = True + + try: + self.cache_directory = tempfile.gettempdir() # Fails on Android + self.cache_downloaded_subtitles = True + except: + self.cache_directory = None + self.cache_downloaded_subtitles = False + + self._styles = {} + self._italic_style_ids = [] + self._top_regions_ids = [] + + self._allowed_style_attrs = ( + 'color', + 'fontStyle', + 'fontWeight', + ) + + ## This variable stores the language ID from the xml file. + # But it may not exist or it may be wrong. + self.lang = None + + self.ssa_style = OrderedDict([ + ('Fontname', 'Arial'), + ('Fontsize', 50), + ('PrimaryColour', '&H00EEEEEE'), + ('SecondaryColour', '&H000000FF'), + ('BackColour', '&H40000000'), + ('OutlineColour', '&H00000000'), + ('Bold', 0), + ('Italic', 0), + ('Underline', 0), + ('Alignment', 2), + ('BorderStyle', 1), + ('Outline', 2), + ('Shadow', 3), + ('MarginL', 0), + ('MarginR', 0), + ('MarginV', 40), + ('StrikeOut', 0), + ('ScaleX', 100), + ('ScaleY', 100), + ('Spacing', 0), + ('Angle', 0), + ('Encoding', 1) + ]) + self.ssa_playresx = 1280 + self.ssa_playresy = 720 + + self.entries = [] + + def set_video_aspect_ratio(self, ratio): + """ Adjust the SSA options PlaResX and PlayRexY according to the aspect ratio of the video """ + self.ssa_playresy = int(self.ssa_playresx / ratio) + + def parse_subtitle_file(self, filename, file_encoding=None): + """Read and parse a subtitle file. + If the file has the vtt or srt extension it will be parsed as a vtt. Otherwise it will be parsed as ttml. + The result is stored in the `entries` list, as begin (ms), end (ms), text, position. + """ + + extension = os.path.splitext(filename)[1].lower() + if extension == ".srt" or extension == ".vtt": + self.parse_vtt_file(filename, file_encoding) + else: + self.parse_ttml_file(filename, file_encoding) + + def parse_ttml_file(self, filename, file_encoding=None): + """Read and parse a ttml/xml/dfxp file. + The result is stored in the `entries` list, as begin (ms), end (ms), text, position. + """ + + doc = self._read_file(filename, file_encoding) + self.parse_ttml_from_string(doc.encode('utf-8')) + + def parse_ttml_from_string(self, doc): + """Read and parse a ttml/xml/dfxp subtitle from a string. + The result is stored in the `entries` list, as begin (ms), end (ms), text, position. + """ + + def extract_rate(s): + try: + m = s.split(' ') + return int(m[0]) / int(m[1]) + except: + return 1 + + del self.entries [:] + self._tc = TimestampConverter() + + ttml_dom = minidom.parseString(doc) + self._encoding = ttml_dom.encoding + + if self._encoding and self._encoding.lower() not in ['utf8', 'utf-8']: + # Don't bother with subtitles that aren't utf-8 encoded + # but assume utf-8 when the encoding attr is missing + raise NotImplementedError('Source is not utf-8 encoded') + + # Get the root tt element (assume the file contains + # a single subtitle document) + tt_element = ttml_dom.getElementsByTagNameNS('*', 'tt')[0] + + # Extract doc language + # https://tools.ietf.org/html/rfc4646#section-2.1 + language_tag = tt_element.getAttribute('xml:lang') or '' + self.lang = re.split(r'\s+', language_tag.strip())[0].split('-')[0] + + # Store TT parameters as instance vars (in camel case) + opttime = {} + for ttp_name, defval, convfn in ( + # (tt param, default val, fn to process the str) + ('frameRate', 0, lambda x: float(x)), + ('tickRate', 0, lambda x: int(x)), + ('timeBase', 'media', lambda x: x), + ('clockMode', '', lambda x: x), + #('frameRateMultiplier', 1, lambda x: int(x)), + ('frameRateMultiplier', 1, lambda x: extract_rate(x)), + ('subFrameRate', 1, lambda x: int(x)), + ('markerMode', '', lambda x: x), + ('dropMode', '', lambda x: x), + ): + ttp_val = getattr( + tt_element.attributes.get('ttp:' + ttp_name), 'value', defval) + opttime[Ttml2Ssa._snake_to_camel(ttp_name)] = convfn(ttp_val) + + if opttime['time_base'] not in Ttml2Ssa.TIME_BASES: + raise NotImplementedError('No support for "{}" time base'.format( + opttime['time_base'])) + + # Set effective tick rate as per + # https://www.w3.org/TR/ttml1/#parameter-attribute-tickRate + # This will obviously only be made use of if we encounter offset-time + # expressions that have the tick metric. + self._tc.tick_rate = opttime['tick_rate'] + if not opttime['tick_rate'] and opttime['frame_rate']: + self._tc.tick_rate = int(opttime['frame_rate'] * opttime['sub_frame_rate']) + elif not opttime['tick_rate']: + self._tc.tick_rate = 1 + + # Set FPS to source_fps if no TT param + self._tc.frame_rate = opttime['frame_rate'] or self.source_fps + + # Grab <style>s + # https://www.w3.org/TR/ttml1/#styling-attribute-vocabulary + for styles_container in ttml_dom.getElementsByTagName('styling'): + for style in styles_container.getElementsByTagName('style'): + style_id = getattr( + style.attributes.get('xml:id', {}), 'value', None) + if not style_id: + continue + self._styles[style_id] = self._get_tt_style_attrs(style, True) + if self._styles[style_id]['font_style'] == 'italic': + self._italic_style_ids.append(style_id) + + # Grab top regions + for layout_container in ttml_dom.getElementsByTagName('layout'): + for region in layout_container.getElementsByTagName('region'): + region_id = getattr( + region.attributes.get('xml:id', {}), 'value', None) + if region_id: + # Case 1: displayAlign is in layout -> region + if region.getAttribute('tts:displayAlign') == 'before': + self._top_regions_ids.append(region_id) + # Case 2: displayAlign is in layout -> region -> style + for style in region.getElementsByTagName('style'): + if style.getAttribute('tts:displayAlign') == 'before': + self._top_regions_ids.append(region_id) + + # Get em <p>s. + # + # CAUTION: This is very naive and will fail us when the TTML + # document contains multiple local time contexts with their own + # offsets, or even just a single context with an offset other + # than zero. + lines = [i for i in ttml_dom.getElementsByTagNameNS('*', 'p') \ + if 'begin' in i.attributes.keys()] + + for p in lines: + entry = {} + ms_begin, ms_end, text, position = self._process_parag(p) + entry['ms_begin'] = ms_begin + entry['ms_end'] = ms_end + entry['text'] = text + entry['position'] = position + self.entries.append(entry) + + self._apply_options() + + def _apply_options(self): + if self.scale_factor != 1: + self._scale_timestamps(self.scale_factor) + + if self.shift: + self._shift_timestamps(self.shift) + + if self.fix_duplicated_entries: + self.entries = self._remove_duplicated(self.entries) + + # Sort and fix timestamps + self.entries = sorted(self.entries, key=lambda x: x['ms_begin']) + if self.allow_timestamp_manipulation and self.fix_timestamp_collisions: + self.entries = self._sequalize(self.entries) + + if self.use_cosmetic_filter: + self._cosmetic_filter() + + if self.use_language_filter: + self._language_fix_filter() + + def _get_tt_style_attrs(self, node, in_head=False): + """Extract node's style attributes + + Node can be a style definition element or a content element (<p>). + + Attributes are filtered against :attr:`Ttml2Ssa._allowed_style_attrs` + and returned as a dict whose keys are attribute names camel cased. + """ + + style = {} + for attr_name in self._allowed_style_attrs: + tts = 'tts:' + attr_name + attr_name = Ttml2Ssa._snake_to_camel(attr_name) + style[attr_name] = node.getAttribute(tts) or '' + if not in_head: + style['style_id'] = node.getAttribute('style') + return style + + + def _extract_dialogue(self, nodes, styles=[]): + """Extract text content and styling attributes from <p> elements. + + Args: + nodes (xml.dom.minidom.Node): List of <p> elements + styles (list): List of style signifiers that should be + applied to each node + + Return: + List of SRT paragraphs (strings) + """ + + dialogue = [] + + for node in nodes: + _styles = [] + + if node.nodeType == node.TEXT_NODE: + format_str = '{}' + + # Take the liberty to make a few stylistic choices. We don't + # want too many leading spaces or any unnecessary new lines + text = re.sub(r'^\s{4,}', '', node.nodeValue.replace('\n', '')) + + for style in styles: + format_str = '{ot}{f}{et}'.format( + et='</{}>'.format(style), + ot='<{}>'.format(style), + f=format_str) + + dialogue.append(format_str.format(text)) + + elif node.localName == 'br': + dialogue.append('\n') + + # Checks for italics for now but shouldn't be too much work to + # support bold text or colors + elif node.localName == 'span': + style_attrs = self._get_tt_style_attrs(node) + inline_italic = style_attrs['font_style'] == 'italic' + assoc_italic = style_attrs['style_id'] in self._italic_style_ids + if inline_italic or assoc_italic or node.parentNode.getAttribute('style') == 'AmazonDefaultStyle': + _styles.append('i') + + if node.hasChildNodes(): + dialogue += self._extract_dialogue(node.childNodes, _styles) + + return ''.join(dialogue) + + def _process_parag(self, paragraph): + """Extract begin and end attrs, and text content of <p> element. + + Args: + paragragh (xml.dom.minidom.Element): <p> element. + + Returns: + Tuple containing + begin in ms, + end in ms, + text content in Subrip (SRT) format, + position (top or bottom) where the text should appear + """ + + begin = paragraph.attributes['begin'].value + end = paragraph.attributes['end'].value + + ms_begin = self._tc.timeexpr_to_ms(begin) + ms_end = self._tc.timeexpr_to_ms(end) + + dialogue = self._extract_dialogue(paragraph.childNodes) + + # Trim lines and remove empty lines + new_text = "" + for line in dialogue.splitlines(): + line = line.strip() + if line: + if new_text: new_text += "\n" + new_text += line + dialogue = new_text + + position = 'top' if paragraph.getAttribute('region') in self._top_regions_ids else 'bottom' + + return ms_begin, ms_end, dialogue, position + + + def parse_vtt_file(self, filename, file_encoding=None): + """Read and parse a vtt/srt file. + The result is stored in the `entries` list, as begin (ms), end (ms), text, position. + """ + + vtt = self._read_file(filename, file_encoding) + self.parse_vtt_from_string(vtt) + + def parse_vtt_from_string(self, vtt): + """Read and parse a vtt/srt subtitle from a string. + The result is stored in the `entries` list, as begin (ms), end (ms), text, position. + """ + + def unescape_text(text): + try: + # Python 2 + from HTMLParser import HTMLParser + htmlparser = HTMLParser() + except ImportError: + # Python 3 + import html + htmlparser = html + no_escape_list = [('‎', '<lrm>'), ('‏', '<rlm>')] + for c in no_escape_list: + text = text.replace(c[0], c[1]) + text = htmlparser.unescape(text) + for c in no_escape_list: + text = text.replace(c[1], c[0]) + return text + + del self.entries [:] + self._tc = TimestampConverter() + + lines = vtt.splitlines() + i = 0 + while i < len(lines): + line = lines[i].strip() + i += 1 + #m = re.match(r'(?P<t1>\d{2}:\d{2}:\d{2}[\.,]\d{3})\s-->\s(?P<t2>\d{2}:\d{2}:\d{2}[\.,]\d{3})(?:.*(line:(?P<pos>[0-9.]+?))%)?', line) + m = re.match(r'(?P<t1>(\d{2}:)?\d{2}:\d{2}[\.,]\d{3})\s-->\s(?P<t2>(\d{2}:)?\d{2}:\d{2}[\.,]\d{3})(?:.*(line:(?P<pos>[0-9.]+?))%)?', line) + if m: + time1 = m.group('t1').replace(',', '.') + time2 = m.group('t2').replace(',', '.') + if len(time1) == 9: time1 = "00:" + time1 + if len(time2) == 9: time2 = "00:" + time2 + entry = {} + entry['ms_begin'] = self._tc.timeexpr_to_ms(time1) + entry['ms_end'] = self._tc.timeexpr_to_ms(time2) + entry['position'] = 'top' if m.group('pos') and float(m.group('pos')) < 50 else 'bottom' + text = "" + while i < len(lines): + line = lines[i].strip() + + # Remove <c> </c> tags + line = re.sub('</??c.*?>', '', line) + + i += 1 + if line: + if text: text += "\n" + text += line + else: + break + entry['text'] = unescape_text(text) + self.entries.append(entry) + self._apply_options() + + def generate_srt(self): + """Return a string with the generated subtitle document in srt format.""" + + srt_format_str = '{}\r\n{} --> {}\r\n{}\r\n\r\n' + res = '' + entry_count = 1 + for entry in self.entries: + text = entry['text'].replace("\n", "\r\n") + + if not self.allow_italics: + text = re.sub(r'<i>|</i>', '', text) + + # Remove <c> </c> tags + text = re.sub('</??c.*?>', '', text) + + if self.allow_top_pos and entry['position'] == 'top': + text = Ttml2Ssa.TOP_MARKER + text + + res += srt_format_str.format(entry_count, \ + self._tc.ms_to_subrip(entry['ms_begin']), \ + self._tc.ms_to_subrip(entry['ms_end']), \ + text) + entry_count += 1 + return res + + def generate_vtt(self): + """Return a string with the generated subtitle document in vtt format.""" + + vtt_format_str = '{} --> {} {}\n{}\n\n' + res = 'WEBVTT\n\n' + + for entry in self.entries: + text = entry['text'].replace('\r', '') + + if not self.allow_italics: + text = re.sub(r'<i>|</i>', '', text) + + # Remove <c> </c> tags + text = re.sub('</??c.*?>', '', text) + + pos_str = 'line:90%,end' + if self.allow_top_pos and entry['position'] == 'top': + pos_str = 'line:10%,start' + + res += vtt_format_str.format(self._tc.ms_to_subrip(entry['ms_begin']).replace(',','.'), \ + self._tc.ms_to_subrip(entry['ms_end']).replace(',','.'), \ + pos_str, text) + return res + + def _paragraphs_to_ssa(self, timestamp_min_sep=200): + def fix_timestamps_separation(entries, timestamp_min_sep): + for i in range(len(entries)): + if i == 0: continue + diff = entries[i]['ms_begin'] - entries[i-1]['ms_end'] + if diff < timestamp_min_sep: + s = round((timestamp_min_sep - diff) / 2) + entries[i]['ms_begin'] += s + entries[i-1]['ms_end'] -= s + if entries[i-1]['ms_end'] < 0: entries[i-1]['ms_end'] = 0 + + entries = deepcopy(self.entries) + if self.allow_timestamp_manipulation and timestamp_min_sep > 0: + fix_timestamps_separation(entries, timestamp_min_sep) + + ssa_format_str = 'Dialogue: 0,{},{},Default,,0,0,0,,{}\r\n' + res = "" + for entry in entries: + text = entry['text'] + if not self.allow_italics: + text = re.sub(r'<i>|</i>', '', text) + + for tag in [('\n', '\\\\N'), + ('<i.*?>', '{\\\\i1}'), ('</i>', '{\\\\i0}'), + ('<b.*?>', '{\\\\b1}'), ('</b>', '{\\\\b0}'), + ('<u.*?>', '{\\\\u1}'), ('</u>', '{\\\\u0}'), + ('<.*?>', '')]: + text = re.sub(tag[0], tag[1], text) + + if self.allow_top_pos and entry['position'] == 'top': + text = Ttml2Ssa.TOP_MARKER + text + + res += ssa_format_str.format(self._tc.ms_to_ssa(entry['ms_begin']), self._tc.ms_to_ssa(entry['ms_end']), text) + return res + + def generate_ssa(self): + """Return a string with the generated subtitle document in ssa format.""" + + res = "[Script Info]\r\n" \ + "ScriptType: v4.00+\r\n" \ + "Collisions: Normal\r\n" \ + "PlayDepth: 0\r\n" \ + "PlayResX: {}\r\n" \ + "PlayResY: {}\r\n" \ + "ScaledBorderAndShadow: yes\r\n\r\n" \ + "[V4+ Styles]\r\n" \ + "Format: Name,{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}\r\n" \ + "Style: Default,{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}\r\n\r\n" \ + "[Events]\r\n" \ + "Format: Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text\r\n" \ + .format(self.ssa_playresx, self.ssa_playresy, \ + *list(self.ssa_style.keys()) + list(self.ssa_style.values())) + + res += self._paragraphs_to_ssa(self.ssa_timestamp_min_sep) + return res + + def _shift_timestamps(self, milliseconds): + self._printinfo("Shifting {} milliseconds".format(milliseconds)) + for entry in self.entries: + entry['ms_begin'] += milliseconds + entry['ms_end'] += milliseconds + + def _scale_timestamps(self, multiplier): + self._printinfo("Scale factor: {}".format(multiplier)) + for entry in self.entries: + entry['ms_begin'] *= multiplier + entry['ms_end'] *= multiplier + + def _cosmetic_filter(self): + total_count = 0 + for entry in self.entries: + number_of_lines = len(entry['text'].splitlines()) + + entry['text'], n_changes = re.subn('—', '-', entry['text']) + total_count += n_changes + entry['text'], n_changes = re.subn('―', '-', entry['text']) + total_count += n_changes + entry['text'], n_changes = re.subn('–', '-', entry['text']) + total_count += n_changes + + # Sometimes, in amazon subtitles, the line break is missing when the 2nd line starts with '<i>-' + if self.fix_amazon_errors and number_of_lines == 1: + entry['text'], n_changes = re.subn(r'(\S)<i>-', r'\1\n<i>-', entry['text']) + total_count += n_changes + + # Add an space between '-' and the first word + entry['text'], n_changes = re.subn(r'^(<i>|</i>|)-(\S)', r'\1- \2', entry['text'], flags=re.MULTILINE) + total_count += n_changes + + # Add missing '-' in the first line + if re.match(r'^(?!(-)|<i>-).*?\n(-|<i>-)', entry['text']): + entry['text'] = '- ' + entry['text'] + total_count += 1 + + # If there's only one line and it starts with '-', remove it + if number_of_lines == 1 and entry['text'].count('-') == 1 and \ + (entry['text'].startswith('- ') or entry['text'].startswith('<i>- ')): + entry['text'] = entry['text'].replace('- ', '') + total_count += 1 + + self._printinfo("Cosmetic changes: {}".format(total_count)) + + def _language_fix_filter(self): + lang = self.subtitle_language or self.lang + es_replacements = [('\xA8', '¿'), ('\xAD', '¡'), ('ń', 'ñ')] + total_count = 0 + for entry in self.entries: + if lang == 'es': + for rep in es_replacements: + total_count += entry['text'].count(rep[0]) + entry['text'] = entry['text'].replace(rep[0], rep[1]) + if lang == 'ar': + from unicodedata import lookup + # Netflix (vtt) + if not '‎' in entry['text'] and not '‏' in entry['text']: + # Amazon + entry['text'], n_changes = re.subn(r'^(?!{}|{})'.format(lookup('RIGHT-TO-LEFT MARK'), lookup('RIGHT-TO-LEFT EMBEDDING')), lookup('RIGHT-TO-LEFT EMBEDDING'), entry['text'], flags=re.MULTILINE) + total_count += n_changes + total_count += entry['text'].count('?') + total_count += entry['text'].count(',') + entry['text'] = entry['text'].replace('?', '؟').replace(',', '،') + + # Netflix (vtt) + if '‎' in entry['text'] or '‏' in entry['text']: + from unicodedata import lookup + entry['text'] = entry['text'].replace('‎', lookup('LEFT-TO-RIGHT EMBEDDING')) + entry['text'] = entry['text'].replace('‏', lookup('RIGHT-TO-LEFT EMBEDDING')) + + self._printinfo("Replacements for language '{}': {}".format(lang, total_count)) + + def _sequalize(self, entries): + """ Combine parallel paragraphs """ + + total_count = 0 + res = [] + + for i in range(len(entries)): + if i > 0 and entries[i]['ms_begin'] < entries[i-1]['ms_end']: + entry = res.pop() + if entries[i]['ms_end'] > entries[i-1]['ms_end']: + entry['ms_end'] = entries[i]['ms_end'] + entry['text'] += '\n' + entries[i]['text'] + res.append(entry) + total_count += 1 + else: + res.append(entries[i]) + + if total_count: + self._printinfo("Sequalized entries: {}".format(total_count)) + + return res + + def _remove_duplicated(self, entries): + """ Remove duplicated lines """ + + total_count = 0 + res = [] + + for i in range(len(entries)): + if i > 0 and (entries[i]['text'] == entries[i-1]['text']) and \ + ((entries[i]['ms_begin'] == entries[i-1]['ms_begin'] and entries[i]['ms_end'] == entries[i-1]['ms_end']) or \ + (entries[i]['ms_begin'] == entries[i-1]['ms_end'])): + res[-1]['ms_end'] = entries[i]['ms_end'] + total_count += 1 + else: + res.append(entries[i]) + + if total_count: + self._printinfo("Duplicated entries removed: {}".format(total_count)) + + return res + + def _printinfo(self, text): + print(text) + + def write2file(self, output): + """Write subtitle to file + + It will be saved as ssa, srt or vtt according to the output file extension. + """ + + extension = os.path.splitext(output)[1].lower() + output_encoding = 'utf-8-sig' + + if extension == '.ssa' or extension == '.ass': + res = self.generate_ssa() + elif extension == '.vtt': + res = self.generate_vtt() + output_encoding = 'utf-8' + else: + res = self.generate_srt() + + with io.open(output, 'w', encoding=output_encoding, newline='') as handle: + handle.write(res) + + def _read_file(self, filename, encoding=None): + """ Try to read the file using the supplied encoding (if any), utf-8 and latin-1 """ + + contents = "" + + encodings = ['utf-8', 'latin-1'] + if encoding: + encodings.insert(0, encoding) + + for enc in encodings: + try: + self._printinfo("Opening file {} with encoding {}".format(filename, enc)) + with io.open(filename, 'r', encoding=enc) as handle: + contents = handle.read() + break + except UnicodeDecodeError: + self._printinfo("Error opening {}".format(filename)) + + return contents + + def string_to_color(self, text): + text = text.upper() + if text.startswith('#'): text = text[1:] + color_names = { + # In BBGGRR + 'WHITE': 'FFFFFF', + 'BLANCO': 'FFFFFF', + 'GRAY': '808080', + 'GREY': '808080', + 'GRIS': '808080', + 'YELLOW': '00FFFF', + 'AMARILLO': '00FFFF', + 'RED': '0000FF', + 'ROJO': '0000FF', + 'GREEN': '00FF00', + 'VERDE': '00FF00', + 'BLUE': 'FF0000', + 'AZUL': 'FF0000', + 'BROWN': '2A2AA5', + 'MARRON': '2A2AA5', + 'BLACK': '000000', + 'NEGRO': '000000' + } + if text in color_names: + text = color_names[text] + + try: + number = int(text, base=16) + except: + self._printinfo('Warning: color {} is not recognized'.format(text)) + number = 0xffffff # White + + hex_number = "&H" + format(number, '08x').upper() + return hex_number + + @staticmethod + def _snake_to_camel(s): + camel = '' + for c in s: + d = ord(c) + if d < 91 and d > 64: + camel += '_' + c.lower() + else: + camel += c + return camel + + @staticmethod + def parse_m3u8_from_string(m3u8): + """ Parse a m3u8 from a string a return a list of the segments """ + + segments = [] + + lines = m3u8.splitlines() + duration = 0 + discontinuity = False + for line in lines: + m = re.match(r'#EXTINF:([0-9.]+),', line) + if m: + duration = float(m.group(1)) + if line.startswith('#EXT-X-DISCONTINUITY'): + discontinuity = True + if not line.startswith('#'): + segment = {} + segment['url'] = line + segment['duration'] = duration + segment['discontinuity'] = discontinuity + segments.append(segment) + duration = 0 + discontinuity = False + + return segments + + def download_m3u8_subtitle(self, url): + """ Download all segments from a m3u8 file and joins them together. + Return a string with the subtitle and a list of the segments. + """ + + import requests + + baseurl = os.path.dirname(url) + self._printinfo('Downloading {}'.format(url)) + #self._printinfo('baseurl: {}'.format(baseurl)) + r = requests.get(url, allow_redirects=True) + segments = Ttml2Ssa.parse_m3u8_from_string(r.content.decode('utf-8')) + #self._printinfo('segments: {}'.format(json.dumps(segments, sort_keys=True, indent=4))) + self._printinfo('segments: {}'.format(json.dumps(segments))) + + res = '' + for segment in segments: + url = baseurl +'/'+ segment['url'] + self._printinfo('Downloading segment: {}'.format(os.path.basename(url))) + r = requests.get(url, allow_redirects=True) + res += r.content.decode('utf-8') + + return res, segments + + def download_m3u8_disney(self, url): + """ Similar to download_m3u8_subtitle but specific for Disney+ + Download all segments from a m3u8 file and joins them together. + Return a string with the subtitle and the offset (in milliseconds) + that must be added to the timestamps. + """ + + if self.cache_downloaded_subtitles and self.cache_directory: + vtt, offset = self._load_vtt_from_cache(url) + if vtt: + return vtt, offset + + vtt, segments = self.download_m3u8_subtitle(url) + offset = 0 + if len(segments) > 1 and segments[1]['discontinuity']: + offset = segments[0]['duration'] * 1000 + self._printinfo("offset: {}".format(offset)) + + if self.cache_downloaded_subtitles and self.cache_directory: + self._save_vtt_to_cache(url, vtt, offset) + + return vtt, offset + + def _cache_filename(self, url): + import hashlib + id = re.sub(r'(?:https|http)://.*?/', '', url) + self._printinfo('cache id: {}'.format(id)) + md5sum = hashlib.md5(id.encode('utf-8')).hexdigest() + return '{}{}{}.json'.format(self.cache_directory, os.path.sep, md5sum) + + def _save_vtt_to_cache(self, url, vtt, offset): + filename = self._cache_filename(url) + self._printinfo('Saving {}'.format(filename)) + + data = {} + data['data'] = vtt + data['offset'] = offset + + with io.open(filename, 'w', encoding='utf-8') as handle: + handle.write(json.dumps(data, ensure_ascii=False)) + + def _load_vtt_from_cache(self, url): + filename = self._cache_filename(url) + if os.path.exists(filename): + self._printinfo('Loading {}'.format(filename)) + with io.open(filename, 'r', encoding='utf-8') as handle: + data = json.loads(handle.read()) + return data['data'], data['offset'] + return '', 0 + + @staticmethod + def get_subtitle_list_from_m3u8_string(doc, language_list=None, allow_forced=True, allow_non_forced=True, baseurl='', sort=True): + """ Parse a m3u8 file, look for subtitles and return a list of them """ + + def lang_allowed(lang, lang_list): + if not lang_list: + return True + + lang = lang.lower() + + for l in lang_list: + if lang.startswith(l.lower()): + return True + + return False + + sub_list = [] + lines = doc.splitlines() + tag = '#EXT-X-MEDIA:TYPE=SUBTITLES,' + for line in lines: + if line.startswith(tag): + sub = {} + sub['lang'] = '' + sub['name'] = '' + sub['forced'] = False + sub['url'] = '' + line = line.replace(tag, '') + params = line.split(',') + + for param in params: + if '=' in param: + name, value = param.split('=', 1) + value = value.replace('"', '') + if name == 'LANGUAGE': sub['lang'] = value + elif name == 'NAME': sub['name'] = value + elif name == 'FORCED' and value == 'YES': sub['forced'] = True + elif name == 'URI': sub['url'] = baseurl + value + + if sub['url'] and sub['name'] and sub['lang']: + sub['impaired'] = 'CC' in sub['name'] + sub['filename'] = '{}{}{}'.format(sub['lang'], '.[CC]' if sub['impaired'] else '', '.forced' if sub['forced'] else '') + if lang_allowed(sub['lang'], language_list) and ((allow_forced and sub['forced']) or (allow_non_forced and not sub['forced'])): + sub_list.append(sub) + + if sort: + sub_list = sorted(sub_list, key=lambda x: x['lang'].replace('-419', '-lat') +" "+ str(int(x['forced']))) + + return sub_list + + def get_subtitle_list_from_m3u8_url(self, url, language_list=None, allow_forced=True, allow_non_forced=True): + """ Download the m3u8 file from the url, look for subtitles in the file and return a list of them """ + + import requests + self._printinfo('Downloading {}'.format(url)) + baseurl = os.path.dirname(url) + '/' + r = requests.get(url, allow_redirects=True) + sub_list = Ttml2Ssa.get_subtitle_list_from_m3u8_string(r.content.decode('utf-8'), language_list, allow_forced, allow_non_forced, baseurl) + return sub_list + + +class Ttml2SsaAddon(Ttml2Ssa): + def __init__(self, shift=0, source_fps=23.976, scale_factor=1, subtitle_language=None): + super(Ttml2SsaAddon, self).__init__(shift, source_fps, scale_factor, subtitle_language) + self.addon = Ttml2SsaAddon._addon() + + try: # Kodi >= 19 + from xbmcvfs import translatePath + except ImportError: # Kodi 18 + from xbmc import translatePath + + self.cache_directory = translatePath(self.addon.getAddonInfo('profile')) + "subtitles" + os.sep + self._printinfo("Cache directory: {}".format(self.cache_directory)) + if not os.path.exists(os.path.dirname(self.cache_directory)): + os.makedirs(os.path.dirname(self.cache_directory)) + self.cache_downloaded_subtitles = True + + self._load_settings() + + def _load_settings(self): + self.ssa_style["Fontname"] = self.addon.getSetting('fontname') + self.ssa_style["Fontsize"] = self.addon.getSettingInt('fontsize') + self.ssa_style["PrimaryColour"] = self.string_to_color(self.addon.getSetting('primarycolor')) + self.ssa_style["BackColour"] = self.string_to_color(self.addon.getSetting('backcolor')) + self.ssa_style["OutlineColour"] = self.string_to_color(self.addon.getSetting('outlinecolor')) + self.ssa_style["BorderStyle"] = 1 if self.addon.getSettingInt('borderstyle') == 0 else 3 + self.ssa_style["Outline"] = self.addon.getSettingInt('outline') + self.ssa_style["Shadow"] = self.addon.getSettingInt('shadow') + self.ssa_style["Bold"] = -1 if self.addon.getSettingBool('bold') else 0 + self.ssa_style["Italic"] = -1 if self.addon.getSettingBool('italic') else 0 + self.ssa_style["MarginL"] = self.addon.getSettingInt('marginl') + self.ssa_style["MarginR"] = self.addon.getSettingInt('marginr') + self.ssa_style["MarginV"] = self.addon.getSettingInt('marginv') + self.use_cosmetic_filter = self.addon.getSettingBool('cosmetic_filter') + self.use_language_filter = self.addon.getSettingBool('language_filter') + self.fix_amazon_errors = self.addon.getSettingBool('fix_amazon') + self.cache_downloaded_subtitles = self.addon.getSettingBool('cache_downloaded') + self.ssa_timestamp_min_sep = self.addon.getSettingInt('min_sep') + self.allow_italics = self.addon.getSettingBool('allow_italics') + self.allow_top_pos = self.addon.getSettingBool('allow_top_pos') + self.allow_timestamp_manipulation = self.addon.getSettingBool('timestamp manipulation') + self.fix_timestamp_collisions = self.addon.getSettingBool('fix_collisions') + self.fix_duplicated_entries = False + self._printinfo("Subtitle type: {}".format(self.subtitle_type())) + self._printinfo("SSA style: {}".format(self.ssa_style)) + self._printinfo("Cosmetic filter: {}".format("yes" if self.use_cosmetic_filter else "no")) + self._printinfo("Language filter: {}".format("yes" if self.use_language_filter else "no")) + self._printinfo("Fix Amazon errors: {}".format("yes" if self.fix_amazon_errors else "no")) + self._printinfo("Cache downloaded subtitles: {}".format("yes" if self.cache_downloaded_subtitles else "no")) + self._printinfo("Timestamp minimum separation: {}".format(self.ssa_timestamp_min_sep)) + + def subtitle_type(self): + """ Return the user's preferred subtitle type. + Posible values: srt, ssa, both + """ + + return Ttml2SsaAddon.subtitle_type() + + @staticmethod + def _addon(): + import xbmcaddon + return xbmcaddon.Addon('script.module.ttml2ssa') + + @staticmethod + def subtitle_type(): + """ Return the user's preferred subtitle type. + Posible values: srt, ssa, both + """ + + addon = Ttml2SsaAddon._addon() + return ['ssa', 'srt', 'both'][addon.getSettingInt('subtitle_type')] + + def _printinfo(self, text): + """ Print info in the kodi log """ + + import xbmc + xbmc.log("Ttml2Ssa: {}".format(text), xbmc.LOGINFO) diff --git a/vinetrimmer/utils/widevine/__init__.py b/vinetrimmer/utils/widevine/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vinetrimmer/utils/widevine/cdm.py b/vinetrimmer/utils/widevine/cdm.py new file mode 100644 index 0000000..defde29 --- /dev/null +++ b/vinetrimmer/utils/widevine/cdm.py @@ -0,0 +1,103 @@ +from uuid import UUID + +from Cryptodome.Random import get_random_bytes, random + +from vinetrimmer.utils.widevine.device import LocalDevice +from vinetrimmer.utils.widevine.session import Session + +import requests +import json +import base64 + + +class Cdm: + #system_id = b"\xed\xef\x8b\xa9\x79\xd6\x4a\xce\xa3\xc8\x27\xdc\xd5\x1d\x21\xed" + system_id = b"\x9a\x04\xf0\x79\x98\x40\x42\x86\xab\x92\xe6\x5b\xe0\x88\x5f\x95" + uuid = UUID(bytes=system_id) + urn = f"urn:uuid:{uuid}" + service_certificate_challenge = b"\x08\x04" + common_privacy_cert = ("CAUSxwUKwQIIAxIQFwW5F8wSBIaLBjM6L3cqjBiCtIKSBSKOAjCCAQoCggEBAJntWzsyfateJO/DtiqVtZhSCtW8y" + "zdQPgZFuBTYdrjfQFEEQa2M462xG7iMTnJaXkqeB5UpHVhYQCOn4a8OOKkSeTkwCGELbxWMh4x+Ib/7/up34QGeHl" + "eB6KRfRiY9FOYOgFioYHrc4E+shFexN6jWfM3rM3BdmDoh+07svUoQykdJDKR+ql1DghjduvHK3jOS8T1v+2RC/TH" + "hv0CwxgTRxLpMlSCkv5fuvWCSmvzu9Vu69WTi0Ods18Vcc6CCuZYSC4NZ7c4kcHCCaA1vZ8bYLErF8xNEkKdO7Dev" + "Sy8BDFnoKEPiWC8La59dsPxebt9k+9MItHEbzxJQAZyfWgkCAwEAAToUbGljZW5zZS53aWRldmluZS5jb20SgAOuN" + "HMUtag1KX8nE4j7e7jLUnfSSYI83dHaMLkzOVEes8y96gS5RLknwSE0bv296snUE5F+bsF2oQQ4RgpQO8GVK5uk5M" + "4PxL/CCpgIqq9L/NGcHc/N9XTMrCjRtBBBbPneiAQwHL2zNMr80NQJeEI6ZC5UYT3wr8+WykqSSdhV5Cs6cD7xdn9" + "qm9Nta/gr52u/DLpP3lnSq8x2/rZCR7hcQx+8pSJmthn8NpeVQ/ypy727+voOGlXnVaPHvOZV+WRvWCq5z3CqCLl5" + "+Gf2Ogsrf9s2LFvE7NVV2FvKqcWTw4PIV9Sdqrd+QLeFHd/SSZiAjjWyWOddeOrAyhb3BHMEwg2T7eTo/xxvF+YkP" + "j89qPwXCYcOxF+6gjomPwzvofcJOxkJkoMmMzcFBDopvab5tDQsyN9UPLGhGC98X/8z8QSQ+spbJTYLdgFenFoGq4" + "7gLwDS6NWYYQSqzE3Udf2W7pzk4ybyG4PHBYV3s4cyzdq8amvtE/sNSdOKReuHpfQ=") + + def __init__(self, device): + """Create a Widevine Content Decryption Module using a specific devices data.""" + self.sessions = {} + self.device = device + + def open(self, pssh, raw=False, offline=False): + """ + Open a CDM session with the specified pssh box. + Multiple sessions can be active at the same time. + + Parameters: + pssh: PSSH Data, either a full WidevineCencHeader or a full mp4 pssh box. + raw: If the PSSH Data is incomplete, e.g. NF Key Exchange, set this to True. + offline: 'OFFLINE' License Type field value. + + Returns: + New Session ID. + """ + session_id = self.create_session_id(self.device) + self.sessions[session_id] = Session(session_id, pssh, raw, offline) + return session_id + + def close(self, session_id): + """ + Close a CDM session. + :param session_id: Session to close. + :returns: True if Successful. + """ + if self.is_session_open(session_id): + self.sessions.pop(session_id) + return True + return False + + def is_session_open(self, session_id): + return session_id in self.sessions + + def set_service_certificate(self, session_id, certificate): + if not self.is_session_open(session_id): + raise ValueError(f"There's no session with the id [{session_id!r}]...") + return self.device.set_service_certificate(self.sessions[session_id], certificate) + + def get_license_challenge(self, session_id): + if not self.is_session_open(session_id): + raise ValueError(f"There's no session with the id [{session_id!r}]...") + return self.device.get_license_challenge(self.sessions[session_id]) + + def parse_license(self, session_id, license_res): + if not self.is_session_open(session_id): + raise ValueError(f"There's no session with the id [{session_id!r}]...") + return self.device.parse_license(self.sessions[session_id], license_res) + + def get_keys(self, session_id, content_only=False): + if not self.is_session_open(session_id): + raise ValueError(f"There's no session with the id [{session_id!r}]...") + keys = self.sessions[session_id].keys + if content_only: + return [x for x in keys if x.type == "CONTENT"] + return keys + + @staticmethod + def create_session_id(device): + if device.type == LocalDevice.Types.ANDROID: + session_id = "{hex:16X}{counter}".format( + hex=random.getrandbits(64), + counter="01" # counter, this resets regularly so it's fine to use 01 + ) + session_id.ljust(32, "0") # pad to 16 bytes (32 chars) + return session_id.encode("ascii") + if device.type == LocalDevice.Types.CHROME: + return get_random_bytes(16) + if device.type == LocalDevice.Types.PLAYREADY: + return get_random_bytes(16) + raise ValueError(f"Device Type {device.type.name} is not implemented") diff --git a/vinetrimmer/utils/widevine/device.py b/vinetrimmer/utils/widevine/device.py new file mode 100644 index 0000000..88e1684 --- /dev/null +++ b/vinetrimmer/utils/widevine/device.py @@ -0,0 +1,433 @@ +import base64 +import json +import os +import random +import struct +import time +import sys +from abc import ABC, abstractmethod +from enum import Enum + +import requests +import validators +from construct import BitStruct, Bytes, Const, Container +from construct import Enum as CEnum +from construct import Flag, If, Int8ub, Int16ub, Optional, Padded, Padding, Struct, this +from Cryptodome.Cipher import AES, PKCS1_OAEP +from Cryptodome.Hash import CMAC, HMAC, SHA1, SHA256 +from Cryptodome.PublicKey import RSA +from Cryptodome.Random import get_random_bytes +from Cryptodome.Signature import pss +from Cryptodome.Util import Padding as CPadding +from protobuf3.message import DecodeError + +from vinetrimmer.utils.widevine.key import Key +from vinetrimmer.utils.widevine.protos import widevine_pb2 as widevine +from vinetrimmer.vendor.pymp4.parser import Box + +try: + import cdmapi + cdmapi_supported = True +except ImportError: + cdmapi_supported = False + + +class BaseDevice(ABC): + class Types(Enum): + CHROME = 1 + ANDROID = 2 + PLAYREADY = 3 + + def __repr__(self): + return "{name}({items})".format( + name=self.__class__.__name__, + items=", ".join([f"{k}={repr(v)}" for k, v in self.__dict__.items()]) + ) + + @abstractmethod + def set_service_certificate(self, session, certificate): + """ + Applies a service certificate to the device. + This would be used for devices that wish to use Privacy Mode. + It's akin to SSL/TLS in that it adds another layer of protection on the data itself from MiTM attacks. + Chrome device_type keys beyond 906 require a Verified Media Path (VMP), which in turn requires a service + certificate to be set (Privacy Mode). + """ + + @abstractmethod + def get_license_challenge(self, session): + """ + Get a license challenge (SignedLicenseRequest) to send to a service API. + + Returns: + Base64-encoded SignedLicenseRequest (as bytes). + """ + + @abstractmethod + def parse_license(self, session, license_res): + """Parse license response data, derive keys.""" + + +class LocalDevice(BaseDevice): + WidevineDeviceStruct = Struct( + "signature" / Const(b"WVD"), + "version" / Int8ub, + "type" / CEnum( + Int8ub, + **{t.name: t.value for t in BaseDevice.Types} + ), + "security_level" / Int8ub, + "flags" / Padded(1, Optional(BitStruct( + Padding(7), + "send_key_control_nonce" / Flag + ))), + "private_key_len" / Int16ub, + "private_key" / Bytes(this.private_key_len), + "client_id_len" / Int16ub, + "client_id" / Bytes(this.client_id_len), + "vmp_len" / Optional(Int16ub), + "vmp" / If(this.vmp_len, Optional(Bytes(this.vmp_len))) + ) + WidevineDeviceStructVersion = 1 # latest version supported + + def __init__(self, *_, type, security_level, flags, private_key, client_id, vmp=None, **__): + """ + This is the device key data that is needed for the CDM (Content Decryption Module). + + Parameters: + type: Device Type + security_level: Security level from 1 (highest ranking) to 3 (lowest ranking) + flags: Extra flags + private_key: Device Private Key + client_id: Device Client Identification Blob + vmp: Verified Media Path (VMP) File Hashes Blob + + Flags: + send_key_control_nonce: Setting this to `true` will set a random int between 1 and 2^31 under + `KeyControlNonce` on the License Request Challenge. + """ + # *_,*__ is to ignore unwanted args, like signature and version from the struct. + # `type` param is shadowing a built-in (not great) but required to match with the struct + self.type = self.Types[type] if isinstance(type, str) else type + self.security_level = security_level + self.flags = flags + self.private_key = RSA.importKey(private_key) if private_key else None + self.client_id = widevine.ClientIdentification() + try: + self.client_id.ParseFromString(client_id) + except DecodeError: + raise ValueError("client_id could not be parsed as a ClientIdentification") + self.vmp = widevine.FileHashes() + if vmp: + try: + self.vmp.ParseFromString(vmp) + except DecodeError: + raise ValueError("Verified Media Path (VMP) could not be parsed as FileHashes") + # noinspection PyProtectedMember + self.client_id._FileHashes.CopyFrom(self.vmp) + + self.sessions = {} + + # shorthands + self.system_id = None + if self.client_id: + # noinspection PyProtectedMember + self.system_id = self.client_id.Token._DeviceCertificate.SystemId + + @classmethod + def load(cls, uri, session=None): + if isinstance(uri, bytes): + # direct data + return cls(**cls.WidevineDeviceStruct.parse(uri)) + elif validators.url(uri): + # remote url + return cls(**cls.WidevineDeviceStruct.parse((session or requests).get(uri).content)) + else: + # local file + with open(uri, "rb") as fd: + return cls(**cls.WidevineDeviceStruct.parse_stream(fd)) + + @classmethod + def from_dir(cls, d): + with open(os.path.join(d, "wv.json")) as fd: + config = json.load(fd) + + try: + with open(os.path.join(d, "device_private_key"), "rb") as fd: + private_key = fd.read() + except FileNotFoundError: + private_key = None + + with open(os.path.join(d, "device_client_id_blob"), "rb") as fd: + client_id = fd.read() + + try: + with open(os.path.join(d, "device_vmp_blob"), "rb") as fd: + vmp = fd.read() + except FileNotFoundError: + vmp = None + + return cls( + type=getattr(cls.Types, config["session_id_type"].upper()), + security_level=config["security_level"], + flags={ + "send_key_control_nonce": config.get("send_key_control_nonce", config["session_id_type"] == "android"), + }, + private_key=private_key, + client_id=client_id, + vmp=vmp, + ) + + def dumpb(self): + private_key = self.private_key.export_key("DER") if self.private_key else None + return self.WidevineDeviceStruct.build(dict( + version=self.WidevineDeviceStructVersion, + type=self.type.value, + security_level=self.security_level, + flags=self.flags, + private_key_len=len(private_key) if private_key else 0, + private_key=private_key, + client_id_len=len(self.client_id.SerializeToString()) if self.client_id else 0, + client_id=self.client_id.SerializeToString() if self.client_id else None, + vmp_len=len(self.vmp.SerializeToString()) if self.vmp else 0, + vmp=self.vmp.SerializeToString() if self.vmp else None + )) + + def dump(self, path): + with open(path, "wb") as fd: + fd.write(self.dumpb()) + + def set_service_certificate(self, session, certificate): + if isinstance(certificate, str): + certificate = base64.b64decode(certificate) # assuming base64 + + signed_message = widevine.SignedMessage() + try: + signed_message.ParseFromString(certificate) + except DecodeError: + raise ValueError("Certificate could not be parsed as a SignedMessage") + + signed_device_certificate = widevine.SignedDeviceCertificate() + try: + signed_device_certificate.ParseFromString(signed_message.Msg) + except DecodeError: + raise ValueError("Certificate's message could not be parsed as a SignedDeviceCertificate") + + session.signed_device_certificate = signed_device_certificate + session.privacy_mode = True + + return True + + def get_license_challenge(self, session): + if not self.client_id: + raise ValueError("No client identification blob is available for this device.") + if not self.private_key and not cdmapi_supported: + raise ValueError("No device private key is available for this device and cdmapi is not installed.") + + license_request = None + + if session.raw: + # raw pssh will be treated as bytes and not parsed + license_request = widevine.SignedLicenseRequestRaw() + license_request.Type = widevine.SignedLicenseRequestRaw.MessageType.Value("LICENSE_REQUEST") + license_request.Msg.ContentId.CencId.Pssh = session.cenc_header # bytes, init_data + else: + license_request = widevine.SignedLicenseRequest() + license_request.Type = widevine.SignedLicenseRequest.MessageType.Value("LICENSE_REQUEST") + license_request.Msg.ContentId.CencId.Pssh.CopyFrom(session.cenc_header) # init_data + + license_type = "OFFLINE" if session.offline else "DEFAULT" + license_request.Msg.ContentId.CencId.LicenseType = widevine.LicenseType.Value(license_type) + license_request.Msg.ContentId.CencId.RequestId = session.session_id + license_request.Msg.Type = widevine.LicenseRequest.RequestType.Value("NEW") + license_request.Msg.RequestTime = int(time.time()) + license_request.Msg.ProtocolVersion = widevine.ProtocolVersion.Value("VERSION_2_1") + + if self.flags and self.flags.get("send_key_control_nonce"): + license_request.Msg.KeyControlNonce = random.randrange(1, 2 ** 31) + + if session.privacy_mode: + cid_aes_key = get_random_bytes(16) + cid_iv = get_random_bytes(16) + + enc_client_id = widevine.EncryptedClientIdentification() + if not session.signed_device_certificate: + raise ValueError("Missing signed_device_certificate") + enc_client_id.ServiceId = session.signed_device_certificate._DeviceCertificate.ServiceId.decode() + enc_client_id.ServiceCertificateSerialNumber = ( + session.signed_device_certificate._DeviceCertificate.SerialNumber + ) + enc_client_id.EncryptedClientId = AES.new(cid_aes_key, AES.MODE_CBC, cid_iv).encrypt( + CPadding.pad(self.client_id.SerializeToString(), 16) + ) + + enc_client_id.EncryptedClientIdIv = cid_iv + enc_client_id.EncryptedPrivacyKey = PKCS1_OAEP.new( + RSA.importKey(session.signed_device_certificate._DeviceCertificate.PublicKey) + ).encrypt(cid_aes_key) + + license_request.Msg.EncryptedClientId.CopyFrom(enc_client_id) + else: + license_request.Msg.ClientId.CopyFrom(self.client_id) + + if cdmapi_supported and not self.private_key: + data = SHA1.new(license_request.Msg.SerializeToString()) + em = (pss._EMSA_PSS_ENCODE(data, 2047, get_random_bytes, lambda x, y: pss.MGF1(x, y, data), 20)).hex() + sig = cdmapi.encrypt(em) + license_request.Signature = bytes.fromhex(sig) + else: + license_request.Signature = pss.new(self.private_key).sign( + SHA1.new(license_request.Msg.SerializeToString()) + ) + + session.license_request = license_request + + return session.license_request.SerializeToString() + + def parse_license(self, session, license_res): + if not session.license_request: + raise ValueError("No license request for the session was created. Create one first.") + + if isinstance(license_res, str): + license_res = base64.b64decode(license_res) + + signed_license = widevine.SignedLicense() + try: + signed_license.ParseFromString(license_res) + except DecodeError: + raise ValueError(f"Failed to parse license_res {license_res!r} as SignedLicense") + session.signed_license = signed_license + + def get_auth_keys(*i, k, b): + if len(i) > 1: + return b"".join([get_auth_keys(x, k=k, b=b) for x in i]) + c = CMAC.new(k, ciphermod=AES) + c.update(struct.pack("B", i[0]) + b) + return c.digest() + + license_req_msg = session.license_request.Msg.SerializeToString() + enc_key_base = b"ENCRYPTION\000%b\0\0\0\x80" % license_req_msg + auth_key_base = b"AUTHENTICATION\0%b\0\0\2\0" % license_req_msg + + if cdmapi_supported and not self.private_key: + session.session_key = bytes.fromhex(cdmapi.decrypt(session.signed_license.SessionKey.hex())) + else: + session.session_key = PKCS1_OAEP.new(self.private_key).decrypt(session.signed_license.SessionKey) + session.derived_keys["enc"] = get_auth_keys(1, k=session.session_key, b=enc_key_base) + session.derived_keys["auth_1"] = get_auth_keys(1, 2, k=session.session_key, b=auth_key_base) + session.derived_keys["auth_2"] = get_auth_keys(3, 4, k=session.session_key, b=auth_key_base) + + lic_hmac = HMAC.new(session.derived_keys["auth_1"], digestmod=SHA256) + lic_hmac.update(session.signed_license.Msg.SerializeToString()) + if lic_hmac.digest() != session.signed_license.Signature: + raise ValueError("SignedLicense Signature doesn't match its Message") + + for key in session.signed_license.Msg.Key: + key_type = widevine.License.KeyContainer.KeyType.Name(key.Type) + permissions = [] + if key_type == "OPERATOR_SESSION": + for (descriptor, value) in key._OperatorSessionKeyPermissions.ListFields(): + if value == 1: + permissions.append(descriptor.name) + session.keys.append(Key( + kid=key.Id if key.Id else key_type.encode("utf-8"), + key_type=key_type, + key=CPadding.unpad(AES.new(session.derived_keys["enc"], AES.MODE_CBC, iv=key.Iv).decrypt(key.Key), 16), + permissions=permissions + )) + + return True + + +class RemoteDevice(BaseDevice): + def __init__(self, *_, name, host, username, key, device=None, type, system_id, security_level, **__): + self.type = self.Types[type] if isinstance(type, str) else type + self.system_id = system_id + self.security_level = security_level + self.name = name + self.host = host + self.username = username + self.key = key + self.device = device + + self.sessions = {} + + self.api_session_id = None + + def set_service_certificate(self, session, certificate): + if isinstance(certificate, bytes): + certificate = base64.b64encode(certificate).decode() + + # certificate needs to be base64 to be sent off to the API. + # it needs to intentionally be kept as base64 encoded SignedMessage. + + session.signed_device_certificate = certificate + session.privacy_mode = True + + return True + + def get_license_challenge(self, session): + #return('<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><AcquireLicense xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols"><challenge><Challenge xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols/messages"><LA xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols" Id="SignedData" xml:space="preserve"><Version>1</Version><ContentHeader><WRMHEADER xmlns="http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader" version="4.0.0.0"><DATA><PROTECTINFO><KEYLEN>16</KEYLEN><ALGID>AESCTR</ALGID></PROTECTINFO><KID>4tPGZGh65UKHjc+Zx8+s9Q==</KID><CHECKSUM>rP8FLDWRTIU=</CHECKSUM><LA_URL>https://prls.atv-ps.amazon.com/cdp</LA_URL></DATA></WRMHEADER></ContentHeader><CLIENTINFO><CLIENTVERSION>4.0.0.5102</CLIENTVERSION></CLIENTINFO><RevocationLists><RevListInfo><ListID>ioydTlK2p0WXkWklprR5Hw==</ListID><Version>13</Version></RevListInfo><RevListInfo><ListID>Ef/RUojT3U6Ct2jqTCChbA==</ListID><Version>72</Version></RevListInfo></RevocationLists><CustomData>None</CustomData><LicenseNonce>i13r4hPvZeeNks0pIXYjUw==</LicenseNonce><ClientTime>1707691589</ClientTime><EncryptedData xmlns="http://www.w3.org/2001/04/xmlenc#" Type="http://www.w3.org/2001/04/xmlenc#Element"><EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"></EncryptionMethod><KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#"><EncryptionMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#ecc256"></EncryptionMethod><KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><KeyName>WMRMServer</KeyName></KeyInfo><CipherData><CipherValue>prLz7zZX0d/9uJpAXn/SHOCbrPcZMG341omz6kKVkTGIUZxP6ceCsoOjA/sMaUBovzSG7RSt2hz8wRsg6azCAsokZUlUJ22UQ8ptfhVCfZyoXxelsnzG5rkWP5TH9Ncfq5FC8qY/KOrNhSFT3WorvRKmtVVH/fQn4ZQx0EpOHpY=</CipherValue></CipherData></EncryptedKey></KeyInfo><CipherData><CipherValue>pxH279BJ9tWMaaUdFxHK1EodQreyoKgIyLEM7a8lYSAybmGVxNuRCKed990nXJIg8WpOh3a2hB08Ygh3geyGTcfWFc1oU/egsTPCd5pqO6uAuazfv5/i1XNvenCMbLO6blASvCV3vF6U9IHThTQaCza9OM/6wnbNsv8A4FZdFx1fzMTFT5p4I4ff7VL4Hc6SLCLOlfs3h7tlZKZSHmP1TQLKGdCj1a3n+chEXlnHVMgkxc2VKMYFoT8fOaK9k8kY4rjVCi4Ss912qRtVYd6qMFrdd8kNNfL6ikSG+LkfjCEMyPbnu+xqFtM5uHzRxJyv42G9eoJdDxVSeg64/ubDE5j5b0mrvD4HrxDoXdi15EZvfti7pnCXcoJCMYHwAugmjTPCT7jTHRVrkvu+ubWcvxAWyBhLjD+MNWdZkv5j2a/3vUxIbbUP1vOn8mi6qX5gLTj1io31Vx+lHzLZ+pjd1dvhXX2JMpZSc9oYnyG9KBaRX2sqlhd0NU8K8whWqNfanwTB5Ppp9IPE9e1wf8GYGJ0PVW9ZX6YP1qm8ND0ROb9nw4ayoQy+axIDJlENqfcY2emeBmHKXQ+vHGeSgw5iCH+BKPxEV1kiu2QSTIgErLinX23O14RkBAFlIgjoaDPiuvYeZqKDqQMHIgIN+rwTJZLlIJYjF8Gh6elft8m5MXwNBVWm2WQqn/F55L5lREw2TM31h7aMWq+ryzSsPBSKncUY45qHuL4u9YhVNvLIRw5XPZivtppkSd07CrqLNeG2jZQIvtD/3IPaQfLBiZd/mJ4gA+O8vIa2rUbKjJWnO/Ahn9PVLrAgZXMyFUCzURi+hGBkOoYCAR4Wk2pAV8z+U2UEjKtgaegblbvnAkULjrxwdWi21eH37/a42P97lxLomEIj0wSYcVTq3OG4zpzhGj/sPuUDvV1T891a+RcGkgCWgRxhKER2mMRkvqdtdl7KHGDtRLwikwtj9K5YkWXj8+4l7J3BERgkycSihQ+m6aI3E1vk625lOLpgQ9DNgcV+jlOGFl2EqNdEn5kVo2koNm6MJn0Z7FMLFOQClbTRaORJjorSGqmShZOHB8dgUZH/iIRiAn4DBwrKMyKEWBSsnQxMSXV9mgEJsFU2aWVuAfSSJloZ/WFeItx/BSIdPXBlpPY89BlKmZ1fZXCP4Nyv7KgGAsHmZXDrLJYQL75BbObxRB7GajXZBlO8gNpxeRnlFkB20duujUr7FjT4sJuviGHmBiy3zzyYERBigWopmE2ynXLDnvp8pZeY2zjDgtdWpIxWpmKOuIWzAVEtrygQOCTb+rqH/7cZ2lSh9PLO/D7EC2D34sllBuhdYWWviBScDFySq5xI7sObZYlVLmxiGP3uslblZ27oizciZT6IYbl3e7zh4luQBw+DmODpSjQICSce6E18n0B1EdR47y5MDJIH8yGcyp81iAYgZfNDA2LMRQ4LMS3E0RvEPqbTsOI9OQB6smQ2Y51hEEPTGiCyVqb83ewktmK1x37QCxneIPENtUmiGzU3pMKThkpZj+kU6Ys+Y96pguN2/H1DvypugcKBmKdwbVJO7AraGSkcfppqr7eQ6j8xAfL3FMr+uTmiTlJBxyvFXkdvpMSnVfMpfV7kt4QimfHNeqLGqMZkiacCzQjAMiZVQeE2c1v8NPZfH2cMJRigJTAX1nDaSewjFlrFEnpUjkFJToivGm2JOjfLr14LWvIiHVMxPTpkK9t8PvKo/sVqVKL2jhklZs9pz07AFKbgU8/UjdMFMM2OiwEY7ZGFvabrCLE+6dIDZvv7jX3ORrMm7ei+uAr9AnnjnP3BVOjuX9DFvP8iLGw6KfcgJMtmtO+2MP3Fv2A4wWkk8LHfhvg6nw841BpRDhkcp/zM68Sds/qX+zHgWLL/2qKHbFheKaZ/NSBrvBaDfCpJ24pMu2AIfaGsHDaNP0EB1L8ruYpzc65Pkmo3vhejbmSgSFvWWfRtVO92wGYRrEx0DazDcYM2wpRzfs2aqlu4Nlfd+D5rHsearMfc/Br3Ku0lcTlnUFwocqdpXXH65RijTdw0UuizZzmlWlui++8crMiMMIYPyiToAA0PQZOUiGalMINoVRRRKpdF+0i8GDG6EKDD5dgTa7dhRqXGaLX46t4U0AbuZ3utbrPBTN6kAL1J49kVzaUoENY+P0tTolQMbY1VQgDa/rntzjD0KaACoD+hQsN4smr8SGYrDqkeJN25EAoqhjR93Qni+J6nFDH7kzvC5b0TQIHaM4XWrHpHuZ4nI9Y9cMO9/Xa5+qax7M7JCoL2i3Q0YkF+lhOVdiCOEN4NHOEkU8KvRigtRH5fAst0P/pXktQR/vuIvIzvIMnGSMXWRhqnnVWtqwm/DPhdIuow4FuAgAsJSIn73uoxq3VsS0Y605QJh9pCV9N5y5sBqH+9+kDzPlLvQWAvo5dRdr0Z7egrnwZXPeVwmc1HpwrzAfNLcA6jaeucucRXLCPIkbtLnLZRPoFcFXburGZw6bqwaOYLmNRjs/kNspDkyyr+r90VoModX6MSG3vqX5JbcsS1+ul4AUUbxsAxMwTU3QmAoLAC77Wg9LRCObHBCoWyJhDwg9yWqdy35Exh49lIghAeGvKD102CtSfykNgJr77+gTjP6sKnaidmZBnujlJZvXux84her5uL9aujUN8FIfnsa2zj/Sjtqrc33hQvMdPEpqW7pO7rv77jY/Saj90kvXumj1eqj/tCESrY3nKnBQTDP5ebLJq8IvFF1ICmIVy1g8bLgVC3jJ2pjIQZCH+znyNxAJ1G0/gIhPQOWmtqHKeIG2/epcALcDwMF3+DQdku3hXuNtezHzjEN9wZlBoLCOI5WRPVDuUsACJ0BCEFKwDvKIrNXTuOCU3uPBCMcDnDdnTLtHl1ll+DkU7KAhVla4G3NG9X61m/N3AZz70C6yM+2GTLgy47dwvOCQEaR6hG8ZmVz+TlZ5QRbCP7afRHJgdBlqbslJurSkbQlvwlLxZQN/ox3MMcWNE0xbBMwwMtBe4Cg4P7mEdZWn5jzipmeeD3VGFIccOHqVuK4k/dwd77HXw9+BJur8RxHGkmDr8oBZxsn1TlZIfEW2DLZ5ardvBM0Q1uDp5x5jnwea7bFxQ2dvcfZZeSR+JTXugtgrAbQ/pj78BMCN0wM5SiYAg/XvvPPbdCkr/bJjdzonWeHmoklu5t4DL3y8yrcpIIcBXggOhJRjfK/Sg/BaWHoQT6AJdQmg+1vqgEJsHKtSzPO06I8RkL1Tebn0/XwhpkkKhVBpmwzxZM3QBlleysaVdiV9cACWA/q8/qLtdxQqfMihNfgj0nPDCG2skJKTPMN9Z5NIXchiCmAtKQVa2cLmNeB9Zvep03oFN9c8CPiYo4F/pf2Q1+dqZY0jlSa9sgiPEJk37iX5XiRrZoiRxI5TFWw0jCd6f2C2EIBgNRWggn1mnOonIYM4i6VsBXmSCPX1oQMtVmWWLIyDxCgWmvQVW50GT2LhHDiV7uSREzV2A=</CipherValue></CipherData></EncryptedData></LA><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod><SignatureMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#ecdsa-sha256"></SignatureMethod><Reference URI="#SignedData"><DigestMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#sha256"></DigestMethod><DigestValue>o1ak/VXS+vlmffC29izf63s98NAVMvv7y2MeTc3mYYE=</DigestValue></Reference></SignedInfo><SignatureValue>AJOs5DhdBEDdG9Qa3olWlzwL/K/CDB5YJnQmjYYF2PXYUiRTLk2b9Ewi5xLED+3dag4FeDMvs0qsILnFYjAGnw==</SignatureValue><KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><KeyValue><ECCKeyValue><PublicKey>d9e5CGy2ffr8bJaGBzk60Ho3y7LcIqydUgbNEEvqSoz+Uwe8kVLv50eXwzfJjY23gSmK15t6lU388dvQ9u9C2w==</PublicKey></ECCKeyValue></KeyValue></KeyInfo></Signature></Challenge></challenge></AcquireLicense></soap:Body></soap:Envelope>') + for i in range(40, 0, -1): + sys.stdout.write(f"\rRate limiting getting keys: {i:3}") + sys.stdout.flush() + time.sleep(1) + sys.stdout.write("\rGetting key!") + sys.stdout.flush() + pssh = session.pssh + if isinstance(pssh, Container): + pssh = Box.build(pssh) + if isinstance(pssh, bytes): + pssh = base64.b64encode(pssh).decode() + + res = self.session(f"{self.host}/challenge", {"pssh": pssh, "device_name": self.device}, {'x-api-key': self.key, 'x-api-username': self.username}) + + self.api_session_id = res["session_id"] + + return res["challenge"] + + def parse_license(self, session, license_res): + if isinstance(license_res, bytes): + license_res = base64.b64encode(license_res).decode() + + license_res = base64.b64decode(license_res).decode() + + + res = self.session(f"{self.host}/keys", {"license": license_res, "pssh": session.pssh}, {'x-api-key': self.key, 'x-api-username': self.username}) + + for key_pair in res["keys"].split(";"): # Split by a delimiter (e.g., ";") if there are multiple key-value pairs + kid, key = key_pair.split(":") # Split each key-value pair into kid and key + session.keys.append(Key(kid=kid, key_type="CONTENT", key=key)) + + return True + + def exchange(self, session, license_res, enc_key_id, hmac_key_id): + if isinstance(license_res, bytes): + license_res = base64.b64encode(license_res).decode() + if isinstance(enc_key_id, bytes): + enc_key_id = base64.b64encode(enc_key_id).decode() + if isinstance(hmac_key_id, bytes): + hmac_key_id = base64.b64encode(hmac_key_id).decode() + res = self.session("GetKeysX", { + "cdmkeyresponse": license_res, + "encryptionkeyid": enc_key_id, + "hmackeyid": hmac_key_id, + "session_id": self.api_session_id + }) + return base64.b64decode(res["encryption_key"]), base64.b64decode(res["sign_key"]) + + def session(self, address, json, headers=None): + res = requests.post( + address, + json=json, + headers=headers + ) + + data = res.json() + + #print(data) + + if res.status_code != 200: + raise ValueError(f"CDM API returned an error: {res['status_code']} - {res['message']}") + + return data diff --git a/vinetrimmer/utils/widevine/key.py b/vinetrimmer/utils/widevine/key.py new file mode 100644 index 0000000..e14f218 --- /dev/null +++ b/vinetrimmer/utils/widevine/key.py @@ -0,0 +1,12 @@ +class Key: + def __init__(self, kid, key_type, key, permissions=None): + self.kid = kid + self.type = key_type + self.key = key + self.permissions = permissions or [] + + def __repr__(self): + return "{name}({items})".format( + name=self.__class__.__name__, + items=", ".join([f"{k}={repr(v)}" for k, v in self.__dict__.items()]) + ) diff --git a/vinetrimmer/utils/widevine/keybox.py b/vinetrimmer/utils/widevine/keybox.py new file mode 100644 index 0000000..853109b --- /dev/null +++ b/vinetrimmer/utils/widevine/keybox.py @@ -0,0 +1,45 @@ +import struct + +from crccheck.crc import Crc32Mpeg2 + + +class Keybox: + def __init__(self, data): + length = len(data) + if length not in (128, 132): + raise ValueError(f"Invalid keybox length: {length}. Should be 128 or 132 bytes") + + if length == 128: # QSEE style keybox + if data[0x80:0x84] != b"LVL1": + raise ValueError("QSEE style keybox does not end in bytes 'LVL1'") + data = data[0:0x80] + + if data[0x78:0x7C] != b"kbox": + raise ValueError("Invalid keybox magic") + + body_crc = Crc32Mpeg2.calc(data[:0x7C]) + body_crc_expected = struct.unpack(">L", data[0x7C:0x7C + 4])[0] + if body_crc_expected != body_crc: + raise ValueError(f"Keybox CRC is bad. Expected: 0x{body_crc_expected:08X}. Computed: 0x{body_crc:08X}") + + self.stable_id = data[0x00:0x20] # aka device ID + self.device_aes_key = data[0x20:0x30] + self.device_id = data[0x30:0x78] # device id sent to google, possibly flags + system_id + encrypted + + # known fields + self.flags, self.system_id = struct.unpack(">L", self.device_id[0:8])[:2] + + def __str__(self): + return f"{self.stable_id.decode('utf-8').strip()} ({self.system_id})" + + def __repr__(self): + return "{name}({items})".format( + name=self.__class__.__name__, + items=", ".join([f"{k}={repr(v)}" for k, v in self.__dict__.items()]) + ) + + @classmethod + def load(cls, file): + """Load Keybox from a file path.""" + with open(file, "rb", encoding="utf-8") as fd: + return cls(fd.read()) diff --git a/vinetrimmer/utils/widevine/protos/__init__.py b/vinetrimmer/utils/widevine/protos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vinetrimmer/utils/widevine/protos/alt.proto b/vinetrimmer/utils/widevine/protos/alt.proto new file mode 100644 index 0000000..d5be514 --- /dev/null +++ b/vinetrimmer/utils/widevine/protos/alt.proto @@ -0,0 +1,418 @@ +syntax = "proto2"; + +package video_widevine; + +option java_package = "com.google.video.widevine.protos"; +option optimize_for = LITE_RUNTIME; + +enum LicenseType { + STREAMING = 1; + OFFLINE = 2; +} + +enum ProtocolVersion { + VERSION_2_0 = 20; + VERSION_2_1 = 21; +} + +message LicenseIdentification { + optional bytes request_id = 1; + optional bytes session_id = 2; + optional bytes purchase_id = 3; + optional .video_widevine.LicenseType type = 4; + optional int32 version = 5; + optional bytes provider_session_token = 6; +} + +message License { + message Policy { + optional bool can_play = 1 [default = false]; + optional bool can_persist = 2 [default = false]; + optional bool can_renew = 3 [default = false]; + optional int64 rental_duration_seconds = 4 [default = 0]; + optional int64 playback_duration_seconds = 5 [default = 0]; + optional int64 license_duration_seconds = 6 [default = 0]; + optional int64 renewal_recovery_duration_seconds = 7 [default = 0]; + optional string renewal_server_url = 8; + optional int64 renewal_delay_seconds = 9 [default = 0]; + optional int64 renewal_retry_interval_seconds = 10 [default = 0]; + optional bool renew_with_usage = 11 [default = false]; + optional bool always_include_client_id = 12 [default = false]; + optional int64 play_start_grace_period_seconds = 13 [default = 0]; + optional bool soft_enforce_playback_duration = 14 [default = false]; + } + message KeyContainer { + message KeyControl { + optional bytes key_control_block = 1; + optional bytes iv = 2; + } + message OutputProtection { + enum HDCP { + HDCP_NONE = 0; + HDCP_V1 = 1; + HDCP_V2 = 2; + HDCP_V2_1 = 3; + HDCP_V2_2 = 4; + HDCP_NO_DIGITAL_OUTPUT = 255; + } + enum CGMS { + CGMS_NONE = 42; + COPY_FREE = 0; + COPY_ONCE = 2; + COPY_NEVER = 3; + } + optional .video_widevine.License.KeyContainer.OutputProtection.HDCP hdcp = 1 [default = HDCP_NONE]; + optional .video_widevine.License.KeyContainer.OutputProtection.CGMS cgms_flags = 2 [default = CGMS_NONE]; + } + message VideoResolutionConstraint { + optional uint32 min_resolution_pixels = 1; + optional uint32 max_resolution_pixels = 2; + optional .video_widevine.License.KeyContainer.OutputProtection required_protection = 3; + } + message OperatorSessionKeyPermissions { + optional bool allow_encrypt = 1 [default = false]; + optional bool allow_decrypt = 2 [default = false]; + optional bool allow_sign = 3 [default = false]; + optional bool allow_signature_verify = 4 [default = false]; + } + enum KeyType { + SIGNING = 1; + CONTENT = 2; + KEY_CONTROL = 3; + OPERATOR_SESSION = 4; + SUB_SESSION = 5; + } + enum SecurityLevel { + SW_SECURE_CRYPTO = 1; + SW_SECURE_DECODE = 2; + HW_SECURE_CRYPTO = 3; + HW_SECURE_DECODE = 4; + HW_SECURE_ALL = 5; + } + optional bytes id = 1; + optional bytes iv = 2; + optional bytes key = 3; + optional .video_widevine.License.KeyContainer.KeyType type = 4; + optional .video_widevine.License.KeyContainer.SecurityLevel level = 5 [default = SW_SECURE_CRYPTO]; + optional .video_widevine.License.KeyContainer.OutputProtection required_protection = 6; + optional .video_widevine.License.KeyContainer.OutputProtection requested_protection = 7; + optional .video_widevine.License.KeyContainer.KeyControl key_control = 8; + optional .video_widevine.License.KeyContainer.OperatorSessionKeyPermissions operator_session_key_permissions = 9; + repeated .video_widevine.License.KeyContainer.VideoResolutionConstraint video_resolution_constraints = 10; + optional bool anti_rollback_usage_table = 11 [default = false]; + optional string track_label = 12; + } + optional .video_widevine.LicenseIdentification id = 1; + optional .video_widevine.License.Policy policy = 2; + repeated .video_widevine.License.KeyContainer key = 3; + optional int64 license_start_time = 4; + optional bool remote_attestation_verified = 5 [default = false]; + optional bytes provider_client_token = 6; + optional uint32 protection_scheme = 7; + optional bytes srm_requirement = 8; + optional bytes srm_update = 9; +} + +message LicenseRequest { + message ContentIdentification { + message CencDeprecated { + repeated bytes pssh = 1; + optional .video_widevine.LicenseType license_type = 2; + optional bytes request_id = 3; + } + message WebmDeprecated { + optional bytes header = 1; + optional .video_widevine.LicenseType license_type = 2; + optional bytes request_id = 3; + } + message ExistingLicense { + optional .video_widevine.LicenseIdentification license_id = 1; + optional int64 seconds_since_started = 2; + optional int64 seconds_since_last_played = 3; + optional bytes session_usage_table_entry = 4; + } + message InitData { + enum InitDataType { + CENC = 1; + WEBM = 2; + } + optional .video_widevine.LicenseRequest.ContentIdentification.InitData.InitDataType init_data_type = 1 [default = CENC]; + optional bytes init_data = 2; + optional .video_widevine.LicenseType license_type = 3; + optional bytes request_id = 4; + } + optional .video_widevine.LicenseRequest.ContentIdentification.CencDeprecated cenc_id_deprecated = 1; + optional .video_widevine.LicenseRequest.ContentIdentification.WebmDeprecated webm_id_deprecated = 2; + optional .video_widevine.LicenseRequest.ContentIdentification.ExistingLicense existing_license = 3; + optional .video_widevine.LicenseRequest.ContentIdentification.InitData init_data = 4; + } + message SubSessionData { + optional string sub_session_key_id = 1; + optional uint32 nonce = 2; + optional string track_label = 3; + } + enum RequestType { + NEW = 1; + RENEWAL = 2; + RELEASE = 3; + } + optional .video_widevine.ClientIdentification client_id = 1; + optional .video_widevine.LicenseRequest.ContentIdentification content_id = 2; + optional .video_widevine.LicenseRequest.RequestType type = 3; + optional int64 request_time = 4; + optional bytes key_control_nonce_deprecated = 5; + optional .video_widevine.ProtocolVersion protocol_version = 6 [default = VERSION_2_0]; + optional uint32 key_control_nonce = 7; + optional .video_widevine.EncryptedClientIdentification encrypted_client_id = 8; + repeated .video_widevine.LicenseRequest.SubSessionData sub_session_data = 9; +} + +message LicenseError { + enum Error { + INVALID_DRM_DEVICE_CERTIFICATE = 1; + REVOKED_DRM_DEVICE_CERTIFICATE = 2; + SERVICE_UNAVAILABLE = 3; + } + optional .video_widevine.LicenseError.Error error_code = 1; +} + +message MetricData { + message TypeValue { + optional .video_widevine.MetricData.MetricType type = 1; + optional int64 value = 2 [default = 0]; + } + enum MetricType { + LATENCY = 1; + TIMESTAMP = 2; + } + optional string stage_name = 1; + repeated .video_widevine.MetricData.TypeValue metric_data = 2; +} + +message RemoteAttestation { + optional .video_widevine.EncryptedClientIdentification certificate = 1; + optional bytes salt = 2; + optional bytes signature = 3; +} + +message SignedMessage { + enum MessageType { + LICENSE_REQUEST = 1; + LICENSE = 2; + ERROR_RESPONSE = 3; + SERVICE_CERTIFICATE_REQUEST = 4; + SERVICE_CERTIFICATE = 5; + } + optional .video_widevine.SignedMessage.MessageType type = 1; + optional bytes msg = 2; + optional bytes signature = 3; + optional bytes session_key = 4; + optional .video_widevine.RemoteAttestation remote_attestation = 5; + repeated .video_widevine.MetricData metric_data = 6; +} + +message SignedLicenseRequest { + optional .video_widevine.SignedMessage.MessageType type = 1; + optional .video_widevine.LicenseRequest msg = 2; + optional bytes signature = 3; + optional bytes session_key = 4; + optional .video_widevine.RemoteAttestation remote_attestation = 5; + repeated .video_widevine.MetricData metric_data = 6; +} + +message SignedLicense { + optional .video_widevine.SignedMessage.MessageType type = 1; + optional .video_widevine.License msg = 2; + optional bytes signature = 3; + optional bytes session_key = 4; + optional .video_widevine.RemoteAttestation remote_attestation = 5; + repeated .video_widevine.MetricData metric_data = 6; +} + +message GroupKeys { + message GroupKeyData { + optional string track_type = 1; + optional bytes key = 2; + } + enum GroupLicenseVersion { + GROUP_LICENSE_VERSION_1 = 0; + GROUP_LICENSE_VERSION_2 = 1; + } + repeated .video_widevine.License.KeyContainer key = 1 [deprecated = true]; + optional bytes group_id = 2; + optional .video_widevine.GroupKeys.GroupLicenseVersion version = 3 [default = GROUP_LICENSE_VERSION_1]; + repeated .video_widevine.GroupKeys.GroupKeyData key_data = 4; +} + +message ProvisioningOptions { + enum CertificateType { + WIDEVINE_DRM = 0; + X509 = 1; + } + optional .video_widevine.ProvisioningOptions.CertificateType certificate_type = 1 [default = WIDEVINE_DRM]; + optional string certificate_authority = 2; +} + +message ProvisioningRequest { + optional .video_widevine.ClientIdentification client_id = 1; + optional .video_widevine.EncryptedClientIdentification encrypted_client_id = 5; + optional bytes nonce = 2; + optional .video_widevine.ProvisioningOptions options = 3; + optional bytes stable_id = 4; + optional bytes provider_id = 6; + optional bytes spoid = 7; +} + +message ProvisioningResponse { + optional bytes device_rsa_key = 1; + optional bytes device_rsa_key_iv = 2; + optional bytes device_certificate = 3; + optional bytes nonce = 4; + optional bytes wrapping_key = 5; +} + +message SignedProvisioningMessage { + enum ProtocolVersion { + VERSION_2 = 2; + VERSION_3 = 3; + } + optional bytes message = 1; + optional bytes signature = 2; + optional .video_widevine.SignedProvisioningMessage.ProtocolVersion protocol_version = 3 [default = VERSION_2]; +} + +message ClientIdentification { + message NameValue { + optional string name = 1; + optional string value = 2; + } + message ClientCapabilities { + enum HdcpVersion { + HDCP_NONE = 0; + HDCP_V1 = 1; + HDCP_V2 = 2; + HDCP_V2_1 = 3; + HDCP_V2_2 = 4; + HDCP_NO_DIGITAL_OUTPUT = 255; + } + enum CertificateKeyType { + RSA_2048 = 0; + RSA_3072 = 1; + } + optional bool client_token = 1 [default = false]; + optional bool session_token = 2 [default = false]; + optional bool video_resolution_constraints = 3 [default = false]; + optional .video_widevine.ClientIdentification.ClientCapabilities.HdcpVersion max_hdcp_version = 4 [default = HDCP_NONE]; + optional uint32 oem_crypto_api_version = 5; + optional bool anti_rollback_usage_table = 6 [default = false]; + optional uint32 srm_version = 7; + optional bool can_update_srm = 8 [default = false]; + repeated .video_widevine.ClientIdentification.ClientCapabilities.CertificateKeyType supported_certificate_key_type = 9; + } + enum TokenType { + KEYBOX = 0; + DRM_DEVICE_CERTIFICATE = 1; + REMOTE_ATTESTATION_CERTIFICATE = 2; + OEM_DEVICE_CERTIFICATE = 3; + } + optional .video_widevine.ClientIdentification.TokenType type = 1 [default = KEYBOX]; + optional bytes token = 2; + repeated .video_widevine.ClientIdentification.NameValue client_info = 3; + optional bytes provider_client_token = 4; + optional uint32 license_counter = 5; + optional .video_widevine.ClientIdentification.ClientCapabilities client_capabilities = 6; + optional bytes vmp_data = 7; +} + +message EncryptedClientIdentification { + optional string provider_id = 1; + optional bytes service_certificate_serial_number = 2; + optional bytes encrypted_client_id = 3; + optional bytes encrypted_client_id_iv = 4; + optional bytes encrypted_privacy_key = 5; +} + +message DrmDeviceCertificate { + enum CertificateType { + ROOT = 0; + DRM_INTERMEDIATE = 1; + DRM_USER_DEVICE = 2; + SERVICE = 3; + PROVISIONER = 4; + } + optional .video_widevine.DrmDeviceCertificate.CertificateType type = 1; + optional bytes serial_number = 2; + optional uint32 creation_time_seconds = 3; + optional bytes public_key = 4; + optional uint32 system_id = 5; + optional bool test_device_deprecated = 6 [deprecated = true]; + optional string provider_id = 7; +} + +message DeviceCertificateStatus { + enum Status { + VALID = 0; + REVOKED = 1; + } + optional bytes drm_serial_number = 1; + optional .video_widevine.DeviceCertificateStatus.Status status = 2 [default = VALID]; + optional .video_widevine.ProvisionedDeviceInfo device_info = 4; + optional bytes oem_serial_number = 5; +} + +message DeviceCertificateStatusList { + optional uint32 creation_time_seconds = 1; + repeated .video_widevine.DeviceCertificateStatus certificate_status = 2; +} + +message SignedCertificateStatusList { + optional bytes certificate_status_list = 1; + optional bytes signature = 2; +} + +message ProvisionedDeviceInfo { + enum WvSecurityLevel { + LEVEL_UNSPECIFIED = 0; + LEVEL_1 = 1; + LEVEL_2 = 2; + LEVEL_3 = 3; + } + optional uint32 system_id = 1; + optional string soc = 2; + optional string manufacturer = 3; + optional string model = 4; + optional string device_type = 5; + optional uint32 model_year = 6; + optional .video_widevine.ProvisionedDeviceInfo.WvSecurityLevel security_level = 7 [default = LEVEL_UNSPECIFIED]; + optional bool test_device = 8 [default = false]; +} + +message SubLicense { + optional string sub_session_key_id = 1; + optional bytes key_msg = 2; +} + +message WidevinePsshData { + enum Algorithm { + UNENCRYPTED = 0; + AESCTR = 1; + } + optional .video_widevine.WidevinePsshData.Algorithm algorithm = 1; + repeated bytes key_id = 2; + optional string provider = 3; + optional bytes content_id = 4; + optional string track_type_deprecated = 5; + optional string policy = 6 [deprecated = true]; + optional uint32 crypto_period_index = 7; + optional bytes grouped_license = 8; + optional uint32 protection_scheme = 9; + optional uint32 crypto_period_seconds = 10; + repeated .video_widevine.SubLicense sub_licenses = 11; + optional string group_master_key_id = 12; +} + +message SignedDrmDeviceCertificate { + optional bytes drm_certificate = 1; + optional bytes signature = 2; + optional .video_widevine.SignedDrmDeviceCertificate signer = 3; +} diff --git a/vinetrimmer/utils/widevine/protos/widevine.proto b/vinetrimmer/utils/widevine/protos/widevine.proto new file mode 100644 index 0000000..6c14b96 --- /dev/null +++ b/vinetrimmer/utils/widevine/protos/widevine.proto @@ -0,0 +1,536 @@ +syntax = "proto2"; + +// from x86 (partial), most of it from the ARM version: +message ClientIdentification { + enum TokenType { + KEYBOX = 0; + DEVICE_CERTIFICATE = 1; + REMOTE_ATTESTATION_CERTIFICATE = 2; + } + message NameValue { + required string Name = 1; + required string Value = 2; + } + message ClientCapabilities { + enum HdcpVersion { + HDCP_NONE = 0; + HDCP_V1 = 1; + HDCP_V2 = 2; + HDCP_V2_1 = 3; + HDCP_V2_2 = 4; + } + enum CertificateKeyType { + RSA_2048 = 0; + RSA_3072 = 1; + } + optional uint32 ClientToken = 1; + optional uint32 SessionToken = 2; + optional uint32 VideoResolutionConstraints = 3; + optional HdcpVersion MaxHdcpVersion = 4; + optional uint32 OemCryptoApiVersion = 5; + optional uint32 AntiRollbackUsageTable = 6; + optional uint32 SrmVersion = 7; + optional bool CanUpdateSrm = 8 [default = false]; + repeated CertificateKeyType SupportedCertificateKeyType = 9; + } + required TokenType Type = 1; + //optional bytes Token = 2; // by default the client treats this as blob, but it's usually a DeviceCertificate, so for usefulness sake, I'm replacing it with this one: + optional SignedDeviceCertificate Token = 2; // use this when parsing, "bytes" when building a client id blob + repeated NameValue ClientInfo = 3; + optional bytes ProviderClientToken = 4; + optional uint32 LicenseCounter = 5; + optional ClientCapabilities _ClientCapabilities = 6; // how should we deal with duped names? will have to look at proto docs later + optional FileHashes _FileHashes = 7; // vmp blob goes here +} + +message ClientIdentificationRaw { + enum TokenType { + KEYBOX = 0; + DEVICE_CERTIFICATE = 1; + REMOTE_ATTESTATION_CERTIFICATE = 2; + } + message NameValue { + required string Name = 1; + required string Value = 2; + } + message ClientCapabilities { + enum HdcpVersion { + HDCP_NONE = 0; + HDCP_V1 = 1; + HDCP_V2 = 2; + HDCP_V2_1 = 3; + HDCP_V2_2 = 4; + } + enum CertificateKeyType { + RSA_2048 = 0; + RSA_3072 = 1; + } + optional uint32 ClientToken = 1; + optional uint32 SessionToken = 2; + optional uint32 VideoResolutionConstraints = 3; + optional HdcpVersion MaxHdcpVersion = 4; + optional uint32 OemCryptoApiVersion = 5; + optional uint32 AntiRollbackUsageTable = 6; + optional uint32 SrmVersion = 7; + optional bool CanUpdateSrm = 8 [default = false]; + repeated CertificateKeyType SupportedCertificateKeyType = 9; + } + required TokenType Type = 1; + optional bytes Token = 2; // by default the client treats this as blob, but it's usually a DeviceCertificate, so for usefulness sake, I'm replacing it with this one: + //optional SignedDeviceCertificate Token = 2; // use this when parsing, "bytes" when building a client id blob + repeated NameValue ClientInfo = 3; + optional bytes ProviderClientToken = 4; + optional uint32 LicenseCounter = 5; + optional ClientCapabilities _ClientCapabilities = 6; // how should we deal with duped names? will have to look at proto docs later + optional FileHashes _FileHashes = 7; // vmp blob goes here +} + +message DeviceCertificate { + enum CertificateType { + ROOT = 0; + INTERMEDIATE = 1; + USER_DEVICE = 2; + SERVICE = 3; + } + required CertificateType Type = 1; // the compiled code reused this as ProvisionedDeviceInfo.WvSecurityLevel, however that is incorrect (compiler aliased it as they're both identical as a structure) + optional bytes SerialNumber = 2; + optional uint32 CreationTimeSeconds = 3; + optional bytes PublicKey = 4; + optional uint32 SystemId = 5; + optional uint32 TestDeviceDeprecated = 6; // is it bool or int? + optional bytes ServiceId = 7; // service URL for service certificates +} + +// missing some references, +message DeviceCertificateStatus { + enum CertificateStatus { + VALID = 0; + REVOKED = 1; + } + optional bytes SerialNumber = 1; + optional CertificateStatus Status = 2; + optional ProvisionedDeviceInfo DeviceInfo = 4; // where is 3? is it deprecated? +} + +message DeviceCertificateStatusList { + optional uint32 CreationTimeSeconds = 1; + repeated DeviceCertificateStatus CertificateStatus = 2; +} + +message EncryptedClientIdentification { + required string ServiceId = 1; + optional bytes ServiceCertificateSerialNumber = 2; + required bytes EncryptedClientId = 3; + required bytes EncryptedClientIdIv = 4; + required bytes EncryptedPrivacyKey = 5; +} + +// todo: fill (for this top-level type, it might be impossible/difficult) +enum LicenseType { + ZERO = 0; + DEFAULT = 1; // 1 is STREAMING/temporary license; on recent versions may go up to 3 (latest x86); it might be persist/don't persist type, unconfirmed + OFFLINE = 2; +} + +// todo: fill (for this top-level type, it might be impossible/difficult) +// this is just a guess because these globals got lost, but really, do we need more? +enum ProtocolVersion { + VERSION_2_0 = 20; + VERSION_2_1 = 21; +} + + +message LicenseIdentification { + optional bytes RequestId = 1; + optional bytes SessionId = 2; + optional bytes PurchaseId = 3; + optional LicenseType Type = 4; + optional uint32 Version = 5; + optional bytes ProviderSessionToken = 6; +} + + +message License { + message Policy { + optional bool CanPlay = 1; // changed from uint32 to bool + optional bool CanPersist = 2; + optional bool CanRenew = 3; + optional uint32 RentalDurationSeconds = 4; + optional uint32 PlaybackDurationSeconds = 5; + optional uint32 LicenseDurationSeconds = 6; + optional uint32 RenewalRecoveryDurationSeconds = 7; + optional string RenewalServerUrl = 8; + optional uint32 RenewalDelaySeconds = 9; + optional uint32 RenewalRetryIntervalSeconds = 10; + optional bool RenewWithUsage = 11; // was uint32 + } + message KeyContainer { + enum KeyType { + SIGNING = 1; + CONTENT = 2; + KEY_CONTROL = 3; + OPERATOR_SESSION = 4; + } + enum SecurityLevel { + SW_SECURE_CRYPTO = 1; + SW_SECURE_DECODE = 2; + HW_SECURE_CRYPTO = 3; + HW_SECURE_DECODE = 4; + HW_SECURE_ALL = 5; + } + message OutputProtection { + enum CGMS { + COPY_FREE = 0; + COPY_ONCE = 2; + COPY_NEVER = 3; + CGMS_NONE = 0x2A; // PC default! + } + optional ClientIdentification.ClientCapabilities.HdcpVersion Hdcp = 1; // it's most likely a copy of Hdcp version available here, but compiler optimized it away + optional CGMS CgmsFlags = 2; + } + message KeyControl { + required bytes KeyControlBlock = 1; // what is this? + required bytes Iv = 2; + } + message OperatorSessionKeyPermissions { + optional uint32 AllowEncrypt = 1; + optional uint32 AllowDecrypt = 2; + optional uint32 AllowSign = 3; + optional uint32 AllowSignatureVerify = 4; + } + message VideoResolutionConstraint { + optional uint32 MinResolutionPixels = 1; + optional uint32 MaxResolutionPixels = 2; + optional OutputProtection RequiredProtection = 3; + } + optional bytes Id = 1; + optional bytes Iv = 2; + optional bytes Key = 3; + optional KeyType Type = 4; + optional SecurityLevel Level = 5; + optional OutputProtection RequiredProtection = 6; + optional OutputProtection RequestedProtection = 7; + optional KeyControl _KeyControl = 8; // duped names, etc + optional OperatorSessionKeyPermissions _OperatorSessionKeyPermissions = 9; // duped names, etc + repeated VideoResolutionConstraint VideoResolutionConstraints = 10; + } + optional LicenseIdentification Id = 1; + optional Policy _Policy = 2; // duped names, etc + repeated KeyContainer Key = 3; + optional uint32 LicenseStartTime = 4; + optional uint32 RemoteAttestationVerified = 5; // bool? + optional bytes ProviderClientToken = 6; + // there might be more, check with newer versions (I see field 7-8 in a lic) + // this appeared in latest x86: + optional uint32 ProtectionScheme = 7; // type unconfirmed fully, but it's likely as WidevineCencHeader describesit (fourcc) +} + +message LicenseError { + enum Error { + INVALID_DEVICE_CERTIFICATE = 1; + REVOKED_DEVICE_CERTIFICATE = 2; + SERVICE_UNAVAILABLE = 3; + } + //LicenseRequest.RequestType ErrorCode; // clang mismatch + optional Error ErrorCode = 1; +} + +message LicenseRequest { + message ContentIdentification { + message CENC { + //optional bytes Pssh = 1; // the client's definition is opaque, it doesn't care about the contents, but the PSSH has a clear definition that is understood and requested by the server, thus I'll replace it with: + optional WidevineCencHeader Pssh = 1; + optional LicenseType LicenseType = 2; // unfortunately the LicenseType symbols are not present, acceptable value seems to only be 1 (is this persist/don't persist? look into it!) + optional bytes RequestId = 3; + } + message WebM { + optional bytes Header = 1; // identical to CENC, aside from PSSH and the parent field number used + optional LicenseType LicenseType = 2; + optional bytes RequestId = 3; + } + message ExistingLicense { + optional LicenseIdentification LicenseId = 1; + optional uint32 SecondsSinceStarted = 2; + optional uint32 SecondsSinceLastPlayed = 3; + optional bytes SessionUsageTableEntry = 4; // interesting! try to figure out the connection between the usage table blob and KCB! + } + optional CENC CencId = 1; + optional WebM WebmId = 2; + optional ExistingLicense License = 3; + } + enum RequestType { + NEW = 1; + RENEWAL = 2; + RELEASE = 3; + } + optional ClientIdentification ClientId = 1; + optional ContentIdentification ContentId = 2; + optional RequestType Type = 3; + optional uint32 RequestTime = 4; + optional bytes KeyControlNonceDeprecated = 5; + optional ProtocolVersion ProtocolVersion = 6; // lacking symbols for this + optional uint32 KeyControlNonce = 7; + optional EncryptedClientIdentification EncryptedClientId = 8; +} + +// raw pssh hack +message LicenseRequestRaw { + message ContentIdentification { + message CENC { + optional bytes Pssh = 1; // the client's definition is opaque, it doesn't care about the contents, but the PSSH has a clear definition that is understood and requested by the server, thus I'll replace it with: + //optional WidevineCencHeader Pssh = 1; + optional LicenseType LicenseType = 2; // unfortunately the LicenseType symbols are not present, acceptable value seems to only be 1 (is this persist/don't persist? look into it!) + optional bytes RequestId = 3; + } + message WebM { + optional bytes Header = 1; // identical to CENC, aside from PSSH and the parent field number used + optional LicenseType LicenseType = 2; + optional bytes RequestId = 3; + } + message ExistingLicense { + optional LicenseIdentification LicenseId = 1; + optional uint32 SecondsSinceStarted = 2; + optional uint32 SecondsSinceLastPlayed = 3; + optional bytes SessionUsageTableEntry = 4; // interesting! try to figure out the connection between the usage table blob and KCB! + } + optional CENC CencId = 1; + optional WebM WebmId = 2; + optional ExistingLicense License = 3; + } + enum RequestType { + NEW = 1; + RENEWAL = 2; + RELEASE = 3; + } + optional ClientIdentification ClientId = 1; + optional ContentIdentification ContentId = 2; + optional RequestType Type = 3; + optional uint32 RequestTime = 4; + optional bytes KeyControlNonceDeprecated = 5; + optional ProtocolVersion ProtocolVersion = 6; // lacking symbols for this + optional uint32 KeyControlNonce = 7; + optional EncryptedClientIdentification EncryptedClientId = 8; +} + + +message ProvisionedDeviceInfo { + enum WvSecurityLevel { + LEVEL_UNSPECIFIED = 0; + LEVEL_1 = 1; + LEVEL_2 = 2; + LEVEL_3 = 3; + } + optional uint32 SystemId = 1; + optional string Soc = 2; + optional string Manufacturer = 3; + optional string Model = 4; + optional string DeviceType = 5; + optional uint32 ModelYear = 6; + optional WvSecurityLevel SecurityLevel = 7; + optional uint32 TestDevice = 8; // bool? +} + + +message ProvisioningOptions { + enum CertificateType { + WIDEVINE_DRM = 0; + X509 = 1; + } + optional CertificateType certificate_type = 1 [default = WIDEVINE_DRM]; + optional string certificate_authority = 2; +} + +message ProvisioningRequest { + optional ClientIdentificationRaw client_id = 1; + optional EncryptedClientIdentification encrypted_client_id = 5; + optional bytes nonce = 2; + optional ProvisioningOptions options = 3; + optional bytes stable_id = 4; + optional bytes provider_id = 6; + optional bytes spoid = 7; +} + +// todo: fill +message ProvisioningResponse { + optional bytes device_rsa_key = 1; + optional bytes device_rsa_key_iv = 2; + optional bytes device_certificate = 3; + optional bytes nonce = 4; + optional bytes wrapping_key = 5; +} + +message RemoteAttestation { + optional EncryptedClientIdentification Certificate = 1; + optional string Salt = 2; + optional string Signature = 3; +} + +// todo: fill +message SessionInit { +} + +// todo: fill +message SessionState { +} + +// todo: fill +message SignedCertificateStatusList { +} + +message SignedDeviceCertificate { + + //optional bytes DeviceCertificate = 1; // again, they use a buffer where it's supposed to be a message, so we'll replace it with what it really is: + optional DeviceCertificate _DeviceCertificate = 1; // how should we deal with duped names? will have to look at proto docs later + optional bytes Signature = 2; + optional SignedDeviceCertificate Signer = 3; +} + + +// todo: may be outdated +message SignedProvisioningMessage { + optional bytes message = 1; + optional bytes signature = 2; + optional ProtocolVersion protocol_version = 3 [default = VERSION_2_0]; +} + +// the root of all messages, from either server or client +message SignedMessage { + enum MessageType { + LICENSE_REQUEST = 1; + LICENSE = 2; + ERROR_RESPONSE = 3; + SERVICE_CERTIFICATE_REQUEST = 4; + SERVICE_CERTIFICATE = 5; + } + optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel + optional bytes Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present + // for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE + optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now) + optional bytes SessionKey = 4; // often RSA wrapped for licenses + optional RemoteAttestation RemoteAttestation = 5; +} + + + +// This message is copied from google's docs, not reversed: +message WidevineCencHeader { + enum Algorithm { + UNENCRYPTED = 0; + AESCTR = 1; + }; + optional Algorithm algorithm = 1; + repeated bytes key_id = 2; + + // Content provider name. + optional string provider = 3; + + // A content identifier, specified by content provider. + optional bytes content_id = 4; + + // Track type. Acceptable values are SD, HD and AUDIO. Used to + // differentiate content keys used by an asset. + optional string track_type_deprecated = 5; + + // The name of a registered policy to be used for this asset. + optional string policy = 6; + + // Crypto period index, for media using key rotation. + optional uint32 crypto_period_index = 7; + + // Optional protected context for group content. The grouped_license is a + // serialized SignedMessage. + optional bytes grouped_license = 8; + + // Protection scheme identifying the encryption algorithm. + // Represented as one of the following 4CC values: + // 'cenc' (AESCTR), 'cbc1' (AESCBC), + // 'cens' (AESCTR subsample), 'cbcs' (AESCBC subsample). + optional uint32 protection_scheme = 9; + + // Optional. For media using key rotation, this represents the duration + // of each crypto period in seconds. + optional uint32 crypto_period_seconds = 10; +} + + +// remove these when using it outside of protoc: + +// from here on, it's just for testing, these messages don't exist in the binaries, I'm adding them to avoid detecting type programmatically +message SignedLicenseRequest { + enum MessageType { + LICENSE_REQUEST = 1; + LICENSE = 2; + ERROR_RESPONSE = 3; + SERVICE_CERTIFICATE_REQUEST = 4; + SERVICE_CERTIFICATE = 5; + } + optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel + optional LicenseRequest Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present + // for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE + optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now) + optional bytes SessionKey = 4; // often RSA wrapped for licenses + optional RemoteAttestation RemoteAttestation = 5; +} + +// hack +message SignedLicenseRequestRaw { + enum MessageType { + LICENSE_REQUEST = 1; + LICENSE = 2; + ERROR_RESPONSE = 3; + SERVICE_CERTIFICATE_REQUEST = 4; + SERVICE_CERTIFICATE = 5; + } + optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel + optional LicenseRequestRaw Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present + // for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE + optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now) + optional bytes SessionKey = 4; // often RSA wrapped for licenses + optional RemoteAttestation RemoteAttestation = 5; +} + + +message SignedLicense { + enum MessageType { + LICENSE_REQUEST = 1; + LICENSE = 2; + ERROR_RESPONSE = 3; + SERVICE_CERTIFICATE_REQUEST = 4; + SERVICE_CERTIFICATE = 5; + } + optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel + optional License Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present + // for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE + optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now) + optional bytes SessionKey = 4; // often RSA wrapped for licenses + optional RemoteAttestation RemoteAttestation = 5; +} + +message SignedServiceCertificate { + enum MessageType { + LICENSE_REQUEST = 1; + LICENSE = 2; + ERROR_RESPONSE = 3; + SERVICE_CERTIFICATE_REQUEST = 4; + SERVICE_CERTIFICATE = 5; + } + optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel + optional SignedDeviceCertificate Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present + // for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE + optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now) + optional bytes SessionKey = 4; // often RSA wrapped for licenses + optional RemoteAttestation RemoteAttestation = 5; +} + +//vmp support +message FileHashes { + message Signature { + optional string filename = 1; + optional bool test_signing = 2; //0 - release, 1 - testing + optional bytes SHA512Hash = 3; + optional bool main_exe = 4; //0 for dlls, 1 for exe, this is field 3 in file + optional bytes signature = 5; + } + optional bytes signer = 1; + repeated Signature signatures = 2; +} diff --git a/vinetrimmer/utils/widevine/protos/widevine_pb2.py b/vinetrimmer/utils/widevine/protos/widevine_pb2.py new file mode 100644 index 0000000..73c5400 --- /dev/null +++ b/vinetrimmer/utils/widevine/protos/widevine_pb2.py @@ -0,0 +1,3848 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: widevine.proto + +import sys + +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from protobuf3 import descriptor as _descriptor +from protobuf3 import message +from protobuf3 import reflection as _reflection +from protobuf3 import symbol_database as _symbol_database +from protobuf3.internal import enum_type_wrapper + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='widevine.proto', + package='', + syntax='proto2', + serialized_options=None, + serialized_pb=_b('\n\x0fwv_proto2.proto\"\xcc\x07\n\x14\x43lientIdentification\x12-\n\x04Type\x18\x01 \x02(\x0e\x32\x1f.ClientIdentification.TokenType\x12\'\n\x05Token\x18\x02 \x01(\x0b\x32\x18.SignedDeviceCertificate\x12\x33\n\nClientInfo\x18\x03 \x03(\x0b\x32\x1f.ClientIdentification.NameValue\x12\x1b\n\x13ProviderClientToken\x18\x04 \x01(\x0c\x12\x16\n\x0eLicenseCounter\x18\x05 \x01(\r\x12\x45\n\x13_ClientCapabilities\x18\x06 \x01(\x0b\x32(.ClientIdentification.ClientCapabilities\x12 \n\x0b_FileHashes\x18\x07 \x01(\x0b\x32\x0b.FileHashes\x1a(\n\tNameValue\x12\x0c\n\x04Name\x18\x01 \x02(\t\x12\r\n\x05Value\x18\x02 \x02(\t\x1a\x89\x04\n\x12\x43lientCapabilities\x12\x13\n\x0b\x43lientToken\x18\x01 \x01(\r\x12\x14\n\x0cSessionToken\x18\x02 \x01(\r\x12\"\n\x1aVideoResolutionConstraints\x18\x03 \x01(\r\x12L\n\x0eMaxHdcpVersion\x18\x04 \x01(\x0e\x32\x34.ClientIdentification.ClientCapabilities.HdcpVersion\x12\x1b\n\x13OemCryptoApiVersion\x18\x05 \x01(\r\x12\x1e\n\x16\x41ntiRollbackUsageTable\x18\x06 \x01(\r\x12\x12\n\nSrmVersion\x18\x07 \x01(\r\x12\x1b\n\x0c\x43\x61nUpdateSrm\x18\x08 \x01(\x08:\x05\x66\x61lse\x12`\n\x1bSupportedCertificateKeyType\x18\t \x03(\x0e\x32;.ClientIdentification.ClientCapabilities.CertificateKeyType\"T\n\x0bHdcpVersion\x12\r\n\tHDCP_NONE\x10\x00\x12\x0b\n\x07HDCP_V1\x10\x01\x12\x0b\n\x07HDCP_V2\x10\x02\x12\r\n\tHDCP_V2_1\x10\x03\x12\r\n\tHDCP_V2_2\x10\x04\"0\n\x12\x43\x65rtificateKeyType\x12\x0c\n\x08RSA_2048\x10\x00\x12\x0c\n\x08RSA_3072\x10\x01\"S\n\tTokenType\x12\n\n\x06KEYBOX\x10\x00\x12\x16\n\x12\x44\x45VICE_CERTIFICATE\x10\x01\x12\"\n\x1eREMOTE_ATTESTATION_CERTIFICATE\x10\x02\"\xc4\x07\n\x17\x43lientIdentificationRaw\x12\x30\n\x04Type\x18\x01 \x02(\x0e\x32\".ClientIdentificationRaw.TokenType\x12\r\n\x05Token\x18\x02 \x01(\x0c\x12\x36\n\nClientInfo\x18\x03 \x03(\x0b\x32\".ClientIdentificationRaw.NameValue\x12\x1b\n\x13ProviderClientToken\x18\x04 \x01(\x0c\x12\x16\n\x0eLicenseCounter\x18\x05 \x01(\r\x12H\n\x13_ClientCapabilities\x18\x06 \x01(\x0b\x32+.ClientIdentificationRaw.ClientCapabilities\x12 \n\x0b_FileHashes\x18\x07 \x01(\x0b\x32\x0b.FileHashes\x1a(\n\tNameValue\x12\x0c\n\x04Name\x18\x01 \x02(\t\x12\r\n\x05Value\x18\x02 \x02(\t\x1a\x8f\x04\n\x12\x43lientCapabilities\x12\x13\n\x0b\x43lientToken\x18\x01 \x01(\r\x12\x14\n\x0cSessionToken\x18\x02 \x01(\r\x12\"\n\x1aVideoResolutionConstraints\x18\x03 \x01(\r\x12O\n\x0eMaxHdcpVersion\x18\x04 \x01(\x0e\x32\x37.ClientIdentificationRaw.ClientCapabilities.HdcpVersion\x12\x1b\n\x13OemCryptoApiVersion\x18\x05 \x01(\r\x12\x1e\n\x16\x41ntiRollbackUsageTable\x18\x06 \x01(\r\x12\x12\n\nSrmVersion\x18\x07 \x01(\r\x12\x1b\n\x0c\x43\x61nUpdateSrm\x18\x08 \x01(\x08:\x05\x66\x61lse\x12\x63\n\x1bSupportedCertificateKeyType\x18\t \x03(\x0e\x32>.ClientIdentificationRaw.ClientCapabilities.CertificateKeyType\"T\n\x0bHdcpVersion\x12\r\n\tHDCP_NONE\x10\x00\x12\x0b\n\x07HDCP_V1\x10\x01\x12\x0b\n\x07HDCP_V2\x10\x02\x12\r\n\tHDCP_V2_1\x10\x03\x12\r\n\tHDCP_V2_2\x10\x04\"0\n\x12\x43\x65rtificateKeyType\x12\x0c\n\x08RSA_2048\x10\x00\x12\x0c\n\x08RSA_3072\x10\x01\"S\n\tTokenType\x12\n\n\x06KEYBOX\x10\x00\x12\x16\n\x12\x44\x45VICE_CERTIFICATE\x10\x01\x12\"\n\x1eREMOTE_ATTESTATION_CERTIFICATE\x10\x02\"\x9b\x02\n\x11\x44\x65viceCertificate\x12\x30\n\x04Type\x18\x01 \x02(\x0e\x32\".DeviceCertificate.CertificateType\x12\x14\n\x0cSerialNumber\x18\x02 \x01(\x0c\x12\x1b\n\x13\x43reationTimeSeconds\x18\x03 \x01(\r\x12\x11\n\tPublicKey\x18\x04 \x01(\x0c\x12\x10\n\x08SystemId\x18\x05 \x01(\r\x12\x1c\n\x14TestDeviceDeprecated\x18\x06 \x01(\r\x12\x11\n\tServiceId\x18\x07 \x01(\x0c\"K\n\x0f\x43\x65rtificateType\x12\x08\n\x04ROOT\x10\x00\x12\x10\n\x0cINTERMEDIATE\x10\x01\x12\x0f\n\x0bUSER_DEVICE\x10\x02\x12\x0b\n\x07SERVICE\x10\x03\"\xc4\x01\n\x17\x44\x65viceCertificateStatus\x12\x14\n\x0cSerialNumber\x18\x01 \x01(\x0c\x12:\n\x06Status\x18\x02 \x01(\x0e\x32*.DeviceCertificateStatus.CertificateStatus\x12*\n\nDeviceInfo\x18\x04 \x01(\x0b\x32\x16.ProvisionedDeviceInfo\"+\n\x11\x43\x65rtificateStatus\x12\t\n\x05VALID\x10\x00\x12\x0b\n\x07REVOKED\x10\x01\"o\n\x1b\x44\x65viceCertificateStatusList\x12\x1b\n\x13\x43reationTimeSeconds\x18\x01 \x01(\r\x12\x33\n\x11\x43\x65rtificateStatus\x18\x02 \x03(\x0b\x32\x18.DeviceCertificateStatus\"\xaf\x01\n\x1d\x45ncryptedClientIdentification\x12\x11\n\tServiceId\x18\x01 \x02(\t\x12&\n\x1eServiceCertificateSerialNumber\x18\x02 \x01(\x0c\x12\x19\n\x11\x45ncryptedClientId\x18\x03 \x02(\x0c\x12\x1b\n\x13\x45ncryptedClientIdIv\x18\x04 \x02(\x0c\x12\x1b\n\x13\x45ncryptedPrivacyKey\x18\x05 \x02(\x0c\"\x9c\x01\n\x15LicenseIdentification\x12\x11\n\tRequestId\x18\x01 \x01(\x0c\x12\x11\n\tSessionId\x18\x02 \x01(\x0c\x12\x12\n\nPurchaseId\x18\x03 \x01(\x0c\x12\x1a\n\x04Type\x18\x04 \x01(\x0e\x32\x0c.LicenseType\x12\x0f\n\x07Version\x18\x05 \x01(\r\x12\x1c\n\x14ProviderSessionToken\x18\x06 \x01(\x0c\"\xa1\x0e\n\x07License\x12\"\n\x02Id\x18\x01 \x01(\x0b\x32\x16.LicenseIdentification\x12 \n\x07_Policy\x18\x02 \x01(\x0b\x32\x0f.License.Policy\x12\"\n\x03Key\x18\x03 \x03(\x0b\x32\x15.License.KeyContainer\x12\x18\n\x10LicenseStartTime\x18\x04 \x01(\r\x12!\n\x19RemoteAttestationVerified\x18\x05 \x01(\r\x12\x1b\n\x13ProviderClientToken\x18\x06 \x01(\x0c\x12\x18\n\x10ProtectionScheme\x18\x07 \x01(\r\x1a\xbb\x02\n\x06Policy\x12\x0f\n\x07\x43\x61nPlay\x18\x01 \x01(\x08\x12\x12\n\nCanPersist\x18\x02 \x01(\x08\x12\x10\n\x08\x43\x61nRenew\x18\x03 \x01(\x08\x12\x1d\n\x15RentalDurationSeconds\x18\x04 \x01(\r\x12\x1f\n\x17PlaybackDurationSeconds\x18\x05 \x01(\r\x12\x1e\n\x16LicenseDurationSeconds\x18\x06 \x01(\r\x12&\n\x1eRenewalRecoveryDurationSeconds\x18\x07 \x01(\r\x12\x18\n\x10RenewalServerUrl\x18\x08 \x01(\t\x12\x1b\n\x13RenewalDelaySeconds\x18\t \x01(\r\x12#\n\x1bRenewalRetryIntervalSeconds\x18\n \x01(\r\x12\x16\n\x0eRenewWithUsage\x18\x0b \x01(\x08\x1a\xf9\t\n\x0cKeyContainer\x12\n\n\x02Id\x18\x01 \x01(\x0c\x12\n\n\x02Iv\x18\x02 \x01(\x0c\x12\x0b\n\x03Key\x18\x03 \x01(\x0c\x12+\n\x04Type\x18\x04 \x01(\x0e\x32\x1d.License.KeyContainer.KeyType\x12\x32\n\x05Level\x18\x05 \x01(\x0e\x32#.License.KeyContainer.SecurityLevel\x12\x42\n\x12RequiredProtection\x18\x06 \x01(\x0b\x32&.License.KeyContainer.OutputProtection\x12\x43\n\x13RequestedProtection\x18\x07 \x01(\x0b\x32&.License.KeyContainer.OutputProtection\x12\x35\n\x0b_KeyControl\x18\x08 \x01(\x0b\x32 .License.KeyContainer.KeyControl\x12[\n\x1e_OperatorSessionKeyPermissions\x18\t \x01(\x0b\x32\x33.License.KeyContainer.OperatorSessionKeyPermissions\x12S\n\x1aVideoResolutionConstraints\x18\n \x03(\x0b\x32/.License.KeyContainer.VideoResolutionConstraint\x1a\xdb\x01\n\x10OutputProtection\x12\x42\n\x04Hdcp\x18\x01 \x01(\x0e\x32\x34.ClientIdentification.ClientCapabilities.HdcpVersion\x12>\n\tCgmsFlags\x18\x02 \x01(\x0e\x32+.License.KeyContainer.OutputProtection.CGMS\"C\n\x04\x43GMS\x12\r\n\tCOPY_FREE\x10\x00\x12\r\n\tCOPY_ONCE\x10\x02\x12\x0e\n\nCOPY_NEVER\x10\x03\x12\r\n\tCGMS_NONE\x10*\x1a\x31\n\nKeyControl\x12\x17\n\x0fKeyControlBlock\x18\x01 \x02(\x0c\x12\n\n\x02Iv\x18\x02 \x02(\x0c\x1a|\n\x1dOperatorSessionKeyPermissions\x12\x14\n\x0c\x41llowEncrypt\x18\x01 \x01(\r\x12\x14\n\x0c\x41llowDecrypt\x18\x02 \x01(\r\x12\x11\n\tAllowSign\x18\x03 \x01(\r\x12\x1c\n\x14\x41llowSignatureVerify\x18\x04 \x01(\r\x1a\x99\x01\n\x19VideoResolutionConstraint\x12\x1b\n\x13MinResolutionPixels\x18\x01 \x01(\r\x12\x1b\n\x13MaxResolutionPixels\x18\x02 \x01(\r\x12\x42\n\x12RequiredProtection\x18\x03 \x01(\x0b\x32&.License.KeyContainer.OutputProtection\"J\n\x07KeyType\x12\x0b\n\x07SIGNING\x10\x01\x12\x0b\n\x07\x43ONTENT\x10\x02\x12\x0f\n\x0bKEY_CONTROL\x10\x03\x12\x14\n\x10OPERATOR_SESSION\x10\x04\"z\n\rSecurityLevel\x12\x14\n\x10SW_SECURE_CRYPTO\x10\x01\x12\x14\n\x10SW_SECURE_DECODE\x10\x02\x12\x14\n\x10HW_SECURE_CRYPTO\x10\x03\x12\x14\n\x10HW_SECURE_DECODE\x10\x04\x12\x11\n\rHW_SECURE_ALL\x10\x05\"\x98\x01\n\x0cLicenseError\x12&\n\tErrorCode\x18\x01 \x01(\x0e\x32\x13.LicenseError.Error\"`\n\x05\x45rror\x12\x1e\n\x1aINVALID_DEVICE_CERTIFICATE\x10\x01\x12\x1e\n\x1aREVOKED_DEVICE_CERTIFICATE\x10\x02\x12\x17\n\x13SERVICE_UNAVAILABLE\x10\x03\"\xac\x07\n\x0eLicenseRequest\x12\'\n\x08\x43lientId\x18\x01 \x01(\x0b\x32\x15.ClientIdentification\x12\x38\n\tContentId\x18\x02 \x01(\x0b\x32%.LicenseRequest.ContentIdentification\x12)\n\x04Type\x18\x03 \x01(\x0e\x32\x1b.LicenseRequest.RequestType\x12\x13\n\x0bRequestTime\x18\x04 \x01(\r\x12!\n\x19KeyControlNonceDeprecated\x18\x05 \x01(\x0c\x12)\n\x0fProtocolVersion\x18\x06 \x01(\x0e\x32\x10.ProtocolVersion\x12\x17\n\x0fKeyControlNonce\x18\x07 \x01(\r\x12\x39\n\x11\x45ncryptedClientId\x18\x08 \x01(\x0b\x32\x1e.EncryptedClientIdentification\x1a\xa2\x04\n\x15\x43ontentIdentification\x12:\n\x06\x43\x65ncId\x18\x01 \x01(\x0b\x32*.LicenseRequest.ContentIdentification.CENC\x12:\n\x06WebmId\x18\x02 \x01(\x0b\x32*.LicenseRequest.ContentIdentification.WebM\x12\x46\n\x07License\x18\x03 \x01(\x0b\x32\x35.LicenseRequest.ContentIdentification.ExistingLicense\x1a_\n\x04\x43\x45NC\x12!\n\x04Pssh\x18\x01 \x01(\x0b\x32\x13.WidevineCencHeader\x12!\n\x0bLicenseType\x18\x02 \x01(\x0e\x32\x0c.LicenseType\x12\x11\n\tRequestId\x18\x03 \x01(\x0c\x1aL\n\x04WebM\x12\x0e\n\x06Header\x18\x01 \x01(\x0c\x12!\n\x0bLicenseType\x18\x02 \x01(\x0e\x32\x0c.LicenseType\x12\x11\n\tRequestId\x18\x03 \x01(\x0c\x1a\x99\x01\n\x0f\x45xistingLicense\x12)\n\tLicenseId\x18\x01 \x01(\x0b\x32\x16.LicenseIdentification\x12\x1b\n\x13SecondsSinceStarted\x18\x02 \x01(\r\x12\x1e\n\x16SecondsSinceLastPlayed\x18\x03 \x01(\r\x12\x1e\n\x16SessionUsageTableEntry\x18\x04 \x01(\x0c\"0\n\x0bRequestType\x12\x07\n\x03NEW\x10\x01\x12\x0b\n\x07RENEWAL\x10\x02\x12\x0b\n\x07RELEASE\x10\x03\"\xa9\x07\n\x11LicenseRequestRaw\x12\'\n\x08\x43lientId\x18\x01 \x01(\x0b\x32\x15.ClientIdentification\x12;\n\tContentId\x18\x02 \x01(\x0b\x32(.LicenseRequestRaw.ContentIdentification\x12,\n\x04Type\x18\x03 \x01(\x0e\x32\x1e.LicenseRequestRaw.RequestType\x12\x13\n\x0bRequestTime\x18\x04 \x01(\r\x12!\n\x19KeyControlNonceDeprecated\x18\x05 \x01(\x0c\x12)\n\x0fProtocolVersion\x18\x06 \x01(\x0e\x32\x10.ProtocolVersion\x12\x17\n\x0fKeyControlNonce\x18\x07 \x01(\r\x12\x39\n\x11\x45ncryptedClientId\x18\x08 \x01(\x0b\x32\x1e.EncryptedClientIdentification\x1a\x96\x04\n\x15\x43ontentIdentification\x12=\n\x06\x43\x65ncId\x18\x01 \x01(\x0b\x32-.LicenseRequestRaw.ContentIdentification.CENC\x12=\n\x06WebmId\x18\x02 \x01(\x0b\x32-.LicenseRequestRaw.ContentIdentification.WebM\x12I\n\x07License\x18\x03 \x01(\x0b\x32\x38.LicenseRequestRaw.ContentIdentification.ExistingLicense\x1aJ\n\x04\x43\x45NC\x12\x0c\n\x04Pssh\x18\x01 \x01(\x0c\x12!\n\x0bLicenseType\x18\x02 \x01(\x0e\x32\x0c.LicenseType\x12\x11\n\tRequestId\x18\x03 \x01(\x0c\x1aL\n\x04WebM\x12\x0e\n\x06Header\x18\x01 \x01(\x0c\x12!\n\x0bLicenseType\x18\x02 \x01(\x0e\x32\x0c.LicenseType\x12\x11\n\tRequestId\x18\x03 \x01(\x0c\x1a\x99\x01\n\x0f\x45xistingLicense\x12)\n\tLicenseId\x18\x01 \x01(\x0b\x32\x16.LicenseIdentification\x12\x1b\n\x13SecondsSinceStarted\x18\x02 \x01(\r\x12\x1e\n\x16SecondsSinceLastPlayed\x18\x03 \x01(\r\x12\x1e\n\x16SessionUsageTableEntry\x18\x04 \x01(\x0c\"0\n\x0bRequestType\x12\x07\n\x03NEW\x10\x01\x12\x0b\n\x07RENEWAL\x10\x02\x12\x0b\n\x07RELEASE\x10\x03\"\xa6\x02\n\x15ProvisionedDeviceInfo\x12\x10\n\x08SystemId\x18\x01 \x01(\r\x12\x0b\n\x03Soc\x18\x02 \x01(\t\x12\x14\n\x0cManufacturer\x18\x03 \x01(\t\x12\r\n\x05Model\x18\x04 \x01(\t\x12\x12\n\nDeviceType\x18\x05 \x01(\t\x12\x11\n\tModelYear\x18\x06 \x01(\r\x12=\n\rSecurityLevel\x18\x07 \x01(\x0e\x32&.ProvisionedDeviceInfo.WvSecurityLevel\x12\x12\n\nTestDevice\x18\x08 \x01(\r\"O\n\x0fWvSecurityLevel\x12\x15\n\x11LEVEL_UNSPECIFIED\x10\x00\x12\x0b\n\x07LEVEL_1\x10\x01\x12\x0b\n\x07LEVEL_2\x10\x02\x12\x0b\n\x07LEVEL_3\x10\x03\"\xb1\x01\n\x13ProvisioningOptions\x12L\n\x10\x63\x65rtificate_type\x18\x01 \x01(\x0e\x32$.ProvisioningOptions.CertificateType:\x0cWIDEVINE_DRM\x12\x1d\n\x15\x63\x65rtificate_authority\x18\x02 \x01(\t\"-\n\x0f\x43\x65rtificateType\x12\x10\n\x0cWIDEVINE_DRM\x10\x00\x12\x08\n\x04X509\x10\x01\"\xec\x01\n\x13ProvisioningRequest\x12+\n\tclient_id\x18\x01 \x01(\x0b\x32\x18.ClientIdentificationRaw\x12;\n\x13\x65ncrypted_client_id\x18\x05 \x01(\x0b\x32\x1e.EncryptedClientIdentification\x12\r\n\x05nonce\x18\x02 \x01(\x0c\x12%\n\x07options\x18\x03 \x01(\x0b\x32\x14.ProvisioningOptions\x12\x11\n\tstable_id\x18\x04 \x01(\x0c\x12\x13\n\x0bprovider_id\x18\x06 \x01(\x0c\x12\r\n\x05spoid\x18\x07 \x01(\x0c\"\x8a\x01\n\x14ProvisioningResponse\x12\x16\n\x0e\x64\x65vice_rsa_key\x18\x01 \x01(\x0c\x12\x19\n\x11\x64\x65vice_rsa_key_iv\x18\x02 \x01(\x0c\x12\x1a\n\x12\x64\x65vice_certificate\x18\x03 \x01(\x0c\x12\r\n\x05nonce\x18\x04 \x01(\x0c\x12\x14\n\x0cwrapping_key\x18\x05 \x01(\x0c\"i\n\x11RemoteAttestation\x12\x33\n\x0b\x43\x65rtificate\x18\x01 \x01(\x0b\x32\x1e.EncryptedClientIdentification\x12\x0c\n\x04Salt\x18\x02 \x01(\t\x12\x11\n\tSignature\x18\x03 \x01(\t\"\r\n\x0bSessionInit\"\x0e\n\x0cSessionState\"\x1d\n\x1bSignedCertificateStatusList\"\x86\x01\n\x17SignedDeviceCertificate\x12.\n\x12_DeviceCertificate\x18\x01 \x01(\x0b\x32\x12.DeviceCertificate\x12\x11\n\tSignature\x18\x02 \x01(\x0c\x12(\n\x06Signer\x18\x03 \x01(\x0b\x32\x18.SignedDeviceCertificate\"x\n\x19SignedProvisioningMessage\x12\x0f\n\x07message\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12\x37\n\x10protocol_version\x18\x03 \x01(\x0e\x32\x10.ProtocolVersion:\x0bVERSION_2_0\"\x9b\x02\n\rSignedMessage\x12(\n\x04Type\x18\x01 \x01(\x0e\x32\x1a.SignedMessage.MessageType\x12\x0b\n\x03Msg\x18\x02 \x01(\x0c\x12\x11\n\tSignature\x18\x03 \x01(\x0c\x12\x12\n\nSessionKey\x18\x04 \x01(\x0c\x12-\n\x11RemoteAttestation\x18\x05 \x01(\x0b\x32\x12.RemoteAttestation\"}\n\x0bMessageType\x12\x13\n\x0fLICENSE_REQUEST\x10\x01\x12\x0b\n\x07LICENSE\x10\x02\x12\x12\n\x0e\x45RROR_RESPONSE\x10\x03\x12\x1f\n\x1bSERVICE_CERTIFICATE_REQUEST\x10\x04\x12\x17\n\x13SERVICE_CERTIFICATE\x10\x05\"\xc5\x02\n\x12WidevineCencHeader\x12\x30\n\talgorithm\x18\x01 \x01(\x0e\x32\x1d.WidevineCencHeader.Algorithm\x12\x0e\n\x06key_id\x18\x02 \x03(\x0c\x12\x10\n\x08provider\x18\x03 \x01(\t\x12\x12\n\ncontent_id\x18\x04 \x01(\x0c\x12\x1d\n\x15track_type_deprecated\x18\x05 \x01(\t\x12\x0e\n\x06policy\x18\x06 \x01(\t\x12\x1b\n\x13\x63rypto_period_index\x18\x07 \x01(\r\x12\x17\n\x0fgrouped_license\x18\x08 \x01(\x0c\x12\x19\n\x11protection_scheme\x18\t \x01(\r\x12\x1d\n\x15\x63rypto_period_seconds\x18\n \x01(\r\"(\n\tAlgorithm\x12\x0f\n\x0bUNENCRYPTED\x10\x00\x12\n\n\x06\x41\x45SCTR\x10\x01\"\xba\x02\n\x14SignedLicenseRequest\x12/\n\x04Type\x18\x01 \x01(\x0e\x32!.SignedLicenseRequest.MessageType\x12\x1c\n\x03Msg\x18\x02 \x01(\x0b\x32\x0f.LicenseRequest\x12\x11\n\tSignature\x18\x03 \x01(\x0c\x12\x12\n\nSessionKey\x18\x04 \x01(\x0c\x12-\n\x11RemoteAttestation\x18\x05 \x01(\x0b\x32\x12.RemoteAttestation\"}\n\x0bMessageType\x12\x13\n\x0fLICENSE_REQUEST\x10\x01\x12\x0b\n\x07LICENSE\x10\x02\x12\x12\n\x0e\x45RROR_RESPONSE\x10\x03\x12\x1f\n\x1bSERVICE_CERTIFICATE_REQUEST\x10\x04\x12\x17\n\x13SERVICE_CERTIFICATE\x10\x05\"\xc3\x02\n\x17SignedLicenseRequestRaw\x12\x32\n\x04Type\x18\x01 \x01(\x0e\x32$.SignedLicenseRequestRaw.MessageType\x12\x1f\n\x03Msg\x18\x02 \x01(\x0b\x32\x12.LicenseRequestRaw\x12\x11\n\tSignature\x18\x03 \x01(\x0c\x12\x12\n\nSessionKey\x18\x04 \x01(\x0c\x12-\n\x11RemoteAttestation\x18\x05 \x01(\x0b\x32\x12.RemoteAttestation\"}\n\x0bMessageType\x12\x13\n\x0fLICENSE_REQUEST\x10\x01\x12\x0b\n\x07LICENSE\x10\x02\x12\x12\n\x0e\x45RROR_RESPONSE\x10\x03\x12\x1f\n\x1bSERVICE_CERTIFICATE_REQUEST\x10\x04\x12\x17\n\x13SERVICE_CERTIFICATE\x10\x05\"\xa5\x02\n\rSignedLicense\x12(\n\x04Type\x18\x01 \x01(\x0e\x32\x1a.SignedLicense.MessageType\x12\x15\n\x03Msg\x18\x02 \x01(\x0b\x32\x08.License\x12\x11\n\tSignature\x18\x03 \x01(\x0c\x12\x12\n\nSessionKey\x18\x04 \x01(\x0c\x12-\n\x11RemoteAttestation\x18\x05 \x01(\x0b\x32\x12.RemoteAttestation\"}\n\x0bMessageType\x12\x13\n\x0fLICENSE_REQUEST\x10\x01\x12\x0b\n\x07LICENSE\x10\x02\x12\x12\n\x0e\x45RROR_RESPONSE\x10\x03\x12\x1f\n\x1bSERVICE_CERTIFICATE_REQUEST\x10\x04\x12\x17\n\x13SERVICE_CERTIFICATE\x10\x05\"\xcb\x02\n\x18SignedServiceCertificate\x12\x33\n\x04Type\x18\x01 \x01(\x0e\x32%.SignedServiceCertificate.MessageType\x12%\n\x03Msg\x18\x02 \x01(\x0b\x32\x18.SignedDeviceCertificate\x12\x11\n\tSignature\x18\x03 \x01(\x0c\x12\x12\n\nSessionKey\x18\x04 \x01(\x0c\x12-\n\x11RemoteAttestation\x18\x05 \x01(\x0b\x32\x12.RemoteAttestation\"}\n\x0bMessageType\x12\x13\n\x0fLICENSE_REQUEST\x10\x01\x12\x0b\n\x07LICENSE\x10\x02\x12\x12\n\x0e\x45RROR_RESPONSE\x10\x03\x12\x1f\n\x1bSERVICE_CERTIFICATE_REQUEST\x10\x04\x12\x17\n\x13SERVICE_CERTIFICATE\x10\x05\"\xb5\x01\n\nFileHashes\x12\x0e\n\x06signer\x18\x01 \x01(\x0c\x12)\n\nsignatures\x18\x02 \x03(\x0b\x32\x15.FileHashes.Signature\x1al\n\tSignature\x12\x10\n\x08\x66ilename\x18\x01 \x01(\t\x12\x14\n\x0ctest_signing\x18\x02 \x01(\x08\x12\x12\n\nSHA512Hash\x18\x03 \x01(\x0c\x12\x10\n\x08main_exe\x18\x04 \x01(\x08\x12\x11\n\tsignature\x18\x05 \x01(\x0c*1\n\x0bLicenseType\x12\x08\n\x04ZERO\x10\x00\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x01\x12\x0b\n\x07OFFLINE\x10\x02*3\n\x0fProtocolVersion\x12\x0f\n\x0bVERSION_2_0\x10\x14\x12\x0f\n\x0bVERSION_2_1\x10\x15') +) + +_LICENSETYPE = _descriptor.EnumDescriptor( + name='LicenseType', + full_name='LicenseType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='ZERO', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='DEFAULT', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='OFFLINE', index=2, number=2, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=10118, + serialized_end=10167, +) +_sym_db.RegisterEnumDescriptor(_LICENSETYPE) + +LicenseType = enum_type_wrapper.EnumTypeWrapper(_LICENSETYPE) +_PROTOCOLVERSION = _descriptor.EnumDescriptor( + name='ProtocolVersion', + full_name='ProtocolVersion', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='VERSION_2_0', index=0, number=20, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='VERSION_2_1', index=1, number=21, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=10169, + serialized_end=10220, +) +_sym_db.RegisterEnumDescriptor(_PROTOCOLVERSION) + +ProtocolVersion = enum_type_wrapper.EnumTypeWrapper(_PROTOCOLVERSION) +ZERO = 0 +DEFAULT = 1 +OFFLINE = 2 +VERSION_2_0 = 20 +VERSION_2_1 = 21 + + +_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_HDCPVERSION = _descriptor.EnumDescriptor( + name='HdcpVersion', + full_name='ClientIdentification.ClientCapabilities.HdcpVersion', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='HDCP_NONE', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='HDCP_V1', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='HDCP_V2', index=2, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='HDCP_V2_1', index=3, number=3, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='HDCP_V2_2', index=4, number=4, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=773, + serialized_end=857, +) +_sym_db.RegisterEnumDescriptor(_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_HDCPVERSION) + +_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_CERTIFICATEKEYTYPE = _descriptor.EnumDescriptor( + name='CertificateKeyType', + full_name='ClientIdentification.ClientCapabilities.CertificateKeyType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='RSA_2048', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='RSA_3072', index=1, number=1, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=859, + serialized_end=907, +) +_sym_db.RegisterEnumDescriptor(_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_CERTIFICATEKEYTYPE) + +_CLIENTIDENTIFICATION_TOKENTYPE = _descriptor.EnumDescriptor( + name='TokenType', + full_name='ClientIdentification.TokenType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='KEYBOX', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='DEVICE_CERTIFICATE', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='REMOTE_ATTESTATION_CERTIFICATE', index=2, number=2, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=909, + serialized_end=992, +) +_sym_db.RegisterEnumDescriptor(_CLIENTIDENTIFICATION_TOKENTYPE) + +_CLIENTIDENTIFICATIONRAW_CLIENTCAPABILITIES_HDCPVERSION = _descriptor.EnumDescriptor( + name='HdcpVersion', + full_name='ClientIdentificationRaw.ClientCapabilities.HdcpVersion', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='HDCP_NONE', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='HDCP_V1', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='HDCP_V2', index=2, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='HDCP_V2_1', index=3, number=3, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='HDCP_V2_2', index=4, number=4, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=773, + serialized_end=857, +) +_sym_db.RegisterEnumDescriptor(_CLIENTIDENTIFICATIONRAW_CLIENTCAPABILITIES_HDCPVERSION) + +_CLIENTIDENTIFICATIONRAW_CLIENTCAPABILITIES_CERTIFICATEKEYTYPE = _descriptor.EnumDescriptor( + name='CertificateKeyType', + full_name='ClientIdentificationRaw.ClientCapabilities.CertificateKeyType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='RSA_2048', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='RSA_3072', index=1, number=1, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=859, + serialized_end=907, +) +_sym_db.RegisterEnumDescriptor(_CLIENTIDENTIFICATIONRAW_CLIENTCAPABILITIES_CERTIFICATEKEYTYPE) + +_CLIENTIDENTIFICATIONRAW_TOKENTYPE = _descriptor.EnumDescriptor( + name='TokenType', + full_name='ClientIdentificationRaw.TokenType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='KEYBOX', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='DEVICE_CERTIFICATE', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='REMOTE_ATTESTATION_CERTIFICATE', index=2, number=2, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=909, + serialized_end=992, +) +_sym_db.RegisterEnumDescriptor(_CLIENTIDENTIFICATIONRAW_TOKENTYPE) + +_DEVICECERTIFICATE_CERTIFICATETYPE = _descriptor.EnumDescriptor( + name='CertificateType', + full_name='DeviceCertificate.CertificateType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='ROOT', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='INTERMEDIATE', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='USER_DEVICE', index=2, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SERVICE', index=3, number=3, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=2170, + serialized_end=2245, +) +_sym_db.RegisterEnumDescriptor(_DEVICECERTIFICATE_CERTIFICATETYPE) + +_DEVICECERTIFICATESTATUS_CERTIFICATESTATUS = _descriptor.EnumDescriptor( + name='CertificateStatus', + full_name='DeviceCertificateStatus.CertificateStatus', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='VALID', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='REVOKED', index=1, number=1, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=2401, + serialized_end=2444, +) +_sym_db.RegisterEnumDescriptor(_DEVICECERTIFICATESTATUS_CERTIFICATESTATUS) + +_LICENSE_KEYCONTAINER_OUTPUTPROTECTION_CGMS = _descriptor.EnumDescriptor( + name='CGMS', + full_name='License.KeyContainer.OutputProtection.CGMS', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='COPY_FREE', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='COPY_ONCE', index=1, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='COPY_NEVER', index=2, number=3, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='CGMS_NONE', index=3, number=42, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=4122, + serialized_end=4189, +) +_sym_db.RegisterEnumDescriptor(_LICENSE_KEYCONTAINER_OUTPUTPROTECTION_CGMS) + +_LICENSE_KEYCONTAINER_KEYTYPE = _descriptor.EnumDescriptor( + name='KeyType', + full_name='License.KeyContainer.KeyType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='SIGNING', index=0, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='CONTENT', index=1, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='KEY_CONTROL', index=2, number=3, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='OPERATOR_SESSION', index=3, number=4, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=4524, + serialized_end=4598, +) +_sym_db.RegisterEnumDescriptor(_LICENSE_KEYCONTAINER_KEYTYPE) + +_LICENSE_KEYCONTAINER_SECURITYLEVEL = _descriptor.EnumDescriptor( + name='SecurityLevel', + full_name='License.KeyContainer.SecurityLevel', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='SW_SECURE_CRYPTO', index=0, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SW_SECURE_DECODE', index=1, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='HW_SECURE_CRYPTO', index=2, number=3, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='HW_SECURE_DECODE', index=3, number=4, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='HW_SECURE_ALL', index=4, number=5, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=4600, + serialized_end=4722, +) +_sym_db.RegisterEnumDescriptor(_LICENSE_KEYCONTAINER_SECURITYLEVEL) + +_LICENSEERROR_ERROR = _descriptor.EnumDescriptor( + name='Error', + full_name='LicenseError.Error', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='INVALID_DEVICE_CERTIFICATE', index=0, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='REVOKED_DEVICE_CERTIFICATE', index=1, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SERVICE_UNAVAILABLE', index=2, number=3, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=4781, + serialized_end=4877, +) +_sym_db.RegisterEnumDescriptor(_LICENSEERROR_ERROR) + +_LICENSEREQUEST_REQUESTTYPE = _descriptor.EnumDescriptor( + name='RequestType', + full_name='LicenseRequest.RequestType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='NEW', index=0, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='RENEWAL', index=1, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='RELEASE', index=2, number=3, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=5772, + serialized_end=5820, +) +_sym_db.RegisterEnumDescriptor(_LICENSEREQUEST_REQUESTTYPE) + +_LICENSEREQUESTRAW_REQUESTTYPE = _descriptor.EnumDescriptor( + name='RequestType', + full_name='LicenseRequestRaw.RequestType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='NEW', index=0, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='RENEWAL', index=1, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='RELEASE', index=2, number=3, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=5772, + serialized_end=5820, +) +_sym_db.RegisterEnumDescriptor(_LICENSEREQUESTRAW_REQUESTTYPE) + +_PROVISIONEDDEVICEINFO_WVSECURITYLEVEL = _descriptor.EnumDescriptor( + name='WvSecurityLevel', + full_name='ProvisionedDeviceInfo.WvSecurityLevel', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='LEVEL_UNSPECIFIED', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='LEVEL_1', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='LEVEL_2', index=2, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='LEVEL_3', index=3, number=3, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=6978, + serialized_end=7057, +) +_sym_db.RegisterEnumDescriptor(_PROVISIONEDDEVICEINFO_WVSECURITYLEVEL) + +_PROVISIONINGOPTIONS_CERTIFICATETYPE = _descriptor.EnumDescriptor( + name='CertificateType', + full_name='ProvisioningOptions.CertificateType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='WIDEVINE_DRM', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='X509', index=1, number=1, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=7192, + serialized_end=7237, +) +_sym_db.RegisterEnumDescriptor(_PROVISIONINGOPTIONS_CERTIFICATETYPE) + +_SIGNEDMESSAGEmessageTYPE = _descriptor.EnumDescriptor( + name='MessageType', + full_name='SignedMessage.MessageType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='LICENSE_REQUEST', index=0, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='LICENSE', index=1, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='ERROR_RESPONSE', index=2, number=3, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SERVICE_CERTIFICATE_REQUEST', index=3, number=4, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SERVICE_CERTIFICATE', index=4, number=5, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=8206, + serialized_end=8331, +) +_sym_db.RegisterEnumDescriptor(_SIGNEDMESSAGEmessageTYPE) + +_WIDEVINECENCHEADER_ALGORITHM = _descriptor.EnumDescriptor( + name='Algorithm', + full_name='WidevineCencHeader.Algorithm', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='UNENCRYPTED', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='AESCTR', index=1, number=1, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=8619, + serialized_end=8659, +) +_sym_db.RegisterEnumDescriptor(_WIDEVINECENCHEADER_ALGORITHM) + +_SIGNEDLICENSEREQUESTmessageTYPE = _descriptor.EnumDescriptor( + name='MessageType', + full_name='SignedLicenseRequest.MessageType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='LICENSE_REQUEST', index=0, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='LICENSE', index=1, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='ERROR_RESPONSE', index=2, number=3, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SERVICE_CERTIFICATE_REQUEST', index=3, number=4, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SERVICE_CERTIFICATE', index=4, number=5, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=8206, + serialized_end=8331, +) +_sym_db.RegisterEnumDescriptor(_SIGNEDLICENSEREQUESTmessageTYPE) + +_SIGNEDLICENSEREQUESTRAWmessageTYPE = _descriptor.EnumDescriptor( + name='MessageType', + full_name='SignedLicenseRequestRaw.MessageType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='LICENSE_REQUEST', index=0, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='LICENSE', index=1, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='ERROR_RESPONSE', index=2, number=3, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SERVICE_CERTIFICATE_REQUEST', index=3, number=4, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SERVICE_CERTIFICATE', index=4, number=5, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=8206, + serialized_end=8331, +) +_sym_db.RegisterEnumDescriptor(_SIGNEDLICENSEREQUESTRAWmessageTYPE) + +_SIGNEDLICENSEmessageTYPE = _descriptor.EnumDescriptor( + name='MessageType', + full_name='SignedLicense.MessageType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='LICENSE_REQUEST', index=0, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='LICENSE', index=1, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='ERROR_RESPONSE', index=2, number=3, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SERVICE_CERTIFICATE_REQUEST', index=3, number=4, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SERVICE_CERTIFICATE', index=4, number=5, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=8206, + serialized_end=8331, +) +_sym_db.RegisterEnumDescriptor(_SIGNEDLICENSEmessageTYPE) + +_SIGNEDSERVICECERTIFICATEmessageTYPE = _descriptor.EnumDescriptor( + name='MessageType', + full_name='SignedServiceCertificate.MessageType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='LICENSE_REQUEST', index=0, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='LICENSE', index=1, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='ERROR_RESPONSE', index=2, number=3, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SERVICE_CERTIFICATE_REQUEST', index=3, number=4, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SERVICE_CERTIFICATE', index=4, number=5, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=8206, + serialized_end=8331, +) +_sym_db.RegisterEnumDescriptor(_SIGNEDSERVICECERTIFICATEmessageTYPE) + + +_CLIENTIDENTIFICATION_NAMEVALUE = _descriptor.Descriptor( + name='NameValue', + full_name='ClientIdentification.NameValue', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='Name', full_name='ClientIdentification.NameValue.Name', index=0, + number=1, type=9, cpp_type=9, label=2, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Value', full_name='ClientIdentification.NameValue.Value', index=1, + number=2, type=9, cpp_type=9, label=2, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=343, + serialized_end=383, +) + +_CLIENTIDENTIFICATION_CLIENTCAPABILITIES = _descriptor.Descriptor( + name='ClientCapabilities', + full_name='ClientIdentification.ClientCapabilities', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='ClientToken', full_name='ClientIdentification.ClientCapabilities.ClientToken', index=0, + number=1, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SessionToken', full_name='ClientIdentification.ClientCapabilities.SessionToken', index=1, + number=2, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='VideoResolutionConstraints', full_name='ClientIdentification.ClientCapabilities.VideoResolutionConstraints', index=2, + number=3, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='MaxHdcpVersion', full_name='ClientIdentification.ClientCapabilities.MaxHdcpVersion', index=3, + number=4, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='OemCryptoApiVersion', full_name='ClientIdentification.ClientCapabilities.OemCryptoApiVersion', index=4, + number=5, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='AntiRollbackUsageTable', full_name='ClientIdentification.ClientCapabilities.AntiRollbackUsageTable', index=5, + number=6, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SrmVersion', full_name='ClientIdentification.ClientCapabilities.SrmVersion', index=6, + number=7, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='CanUpdateSrm', full_name='ClientIdentification.ClientCapabilities.CanUpdateSrm', index=7, + number=8, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SupportedCertificateKeyType', full_name='ClientIdentification.ClientCapabilities.SupportedCertificateKeyType', index=8, + number=9, type=14, cpp_type=8, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _CLIENTIDENTIFICATION_CLIENTCAPABILITIES_HDCPVERSION, + _CLIENTIDENTIFICATION_CLIENTCAPABILITIES_CERTIFICATEKEYTYPE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=386, + serialized_end=907, +) + +_CLIENTIDENTIFICATION = _descriptor.Descriptor( + name='ClientIdentification', + full_name='ClientIdentification', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='Type', full_name='ClientIdentification.Type', index=0, + number=1, type=14, cpp_type=8, label=2, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Token', full_name='ClientIdentification.Token', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ClientInfo', full_name='ClientIdentification.ClientInfo', index=2, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ProviderClientToken', full_name='ClientIdentification.ProviderClientToken', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='LicenseCounter', full_name='ClientIdentification.LicenseCounter', index=4, + number=5, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='_ClientCapabilities', full_name='ClientIdentification._ClientCapabilities', index=5, + number=6, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='_FileHashes', full_name='ClientIdentification._FileHashes', index=6, + number=7, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_CLIENTIDENTIFICATION_NAMEVALUE, _CLIENTIDENTIFICATION_CLIENTCAPABILITIES, ], + enum_types=[ + _CLIENTIDENTIFICATION_TOKENTYPE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=20, + serialized_end=992, +) + + +_CLIENTIDENTIFICATIONRAW_NAMEVALUE = _descriptor.Descriptor( + name='NameValue', + full_name='ClientIdentificationRaw.NameValue', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='Name', full_name='ClientIdentificationRaw.NameValue.Name', index=0, + number=1, type=9, cpp_type=9, label=2, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Value', full_name='ClientIdentificationRaw.NameValue.Value', index=1, + number=2, type=9, cpp_type=9, label=2, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=343, + serialized_end=383, +) + +_CLIENTIDENTIFICATIONRAW_CLIENTCAPABILITIES = _descriptor.Descriptor( + name='ClientCapabilities', + full_name='ClientIdentificationRaw.ClientCapabilities', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='ClientToken', full_name='ClientIdentificationRaw.ClientCapabilities.ClientToken', index=0, + number=1, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SessionToken', full_name='ClientIdentificationRaw.ClientCapabilities.SessionToken', index=1, + number=2, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='VideoResolutionConstraints', full_name='ClientIdentificationRaw.ClientCapabilities.VideoResolutionConstraints', index=2, + number=3, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='MaxHdcpVersion', full_name='ClientIdentificationRaw.ClientCapabilities.MaxHdcpVersion', index=3, + number=4, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='OemCryptoApiVersion', full_name='ClientIdentificationRaw.ClientCapabilities.OemCryptoApiVersion', index=4, + number=5, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='AntiRollbackUsageTable', full_name='ClientIdentificationRaw.ClientCapabilities.AntiRollbackUsageTable', index=5, + number=6, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SrmVersion', full_name='ClientIdentificationRaw.ClientCapabilities.SrmVersion', index=6, + number=7, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='CanUpdateSrm', full_name='ClientIdentificationRaw.ClientCapabilities.CanUpdateSrm', index=7, + number=8, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SupportedCertificateKeyType', full_name='ClientIdentificationRaw.ClientCapabilities.SupportedCertificateKeyType', index=8, + number=9, type=14, cpp_type=8, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _CLIENTIDENTIFICATIONRAW_CLIENTCAPABILITIES_HDCPVERSION, + _CLIENTIDENTIFICATIONRAW_CLIENTCAPABILITIES_CERTIFICATEKEYTYPE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1347, + serialized_end=1874, +) + +_CLIENTIDENTIFICATIONRAW = _descriptor.Descriptor( + name='ClientIdentificationRaw', + full_name='ClientIdentificationRaw', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='Type', full_name='ClientIdentificationRaw.Type', index=0, + number=1, type=14, cpp_type=8, label=2, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Token', full_name='ClientIdentificationRaw.Token', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ClientInfo', full_name='ClientIdentificationRaw.ClientInfo', index=2, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ProviderClientToken', full_name='ClientIdentificationRaw.ProviderClientToken', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='LicenseCounter', full_name='ClientIdentificationRaw.LicenseCounter', index=4, + number=5, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='_ClientCapabilities', full_name='ClientIdentificationRaw._ClientCapabilities', index=5, + number=6, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='_FileHashes', full_name='ClientIdentificationRaw._FileHashes', index=6, + number=7, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_CLIENTIDENTIFICATIONRAW_NAMEVALUE, _CLIENTIDENTIFICATIONRAW_CLIENTCAPABILITIES, ], + enum_types=[ + _CLIENTIDENTIFICATIONRAW_TOKENTYPE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=995, + serialized_end=1959, +) + + +_DEVICECERTIFICATE = _descriptor.Descriptor( + name='DeviceCertificate', + full_name='DeviceCertificate', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='Type', full_name='DeviceCertificate.Type', index=0, + number=1, type=14, cpp_type=8, label=2, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SerialNumber', full_name='DeviceCertificate.SerialNumber', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='CreationTimeSeconds', full_name='DeviceCertificate.CreationTimeSeconds', index=2, + number=3, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='PublicKey', full_name='DeviceCertificate.PublicKey', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SystemId', full_name='DeviceCertificate.SystemId', index=4, + number=5, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='TestDeviceDeprecated', full_name='DeviceCertificate.TestDeviceDeprecated', index=5, + number=6, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ServiceId', full_name='DeviceCertificate.ServiceId', index=6, + number=7, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _DEVICECERTIFICATE_CERTIFICATETYPE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1962, + serialized_end=2245, +) + + +_DEVICECERTIFICATESTATUS = _descriptor.Descriptor( + name='DeviceCertificateStatus', + full_name='DeviceCertificateStatus', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='SerialNumber', full_name='DeviceCertificateStatus.SerialNumber', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Status', full_name='DeviceCertificateStatus.Status', index=1, + number=2, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='DeviceInfo', full_name='DeviceCertificateStatus.DeviceInfo', index=2, + number=4, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _DEVICECERTIFICATESTATUS_CERTIFICATESTATUS, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2248, + serialized_end=2444, +) + + +_DEVICECERTIFICATESTATUSLIST = _descriptor.Descriptor( + name='DeviceCertificateStatusList', + full_name='DeviceCertificateStatusList', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='CreationTimeSeconds', full_name='DeviceCertificateStatusList.CreationTimeSeconds', index=0, + number=1, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='CertificateStatus', full_name='DeviceCertificateStatusList.CertificateStatus', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2446, + serialized_end=2557, +) + + +_ENCRYPTEDCLIENTIDENTIFICATION = _descriptor.Descriptor( + name='EncryptedClientIdentification', + full_name='EncryptedClientIdentification', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='ServiceId', full_name='EncryptedClientIdentification.ServiceId', index=0, + number=1, type=9, cpp_type=9, label=2, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ServiceCertificateSerialNumber', full_name='EncryptedClientIdentification.ServiceCertificateSerialNumber', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='EncryptedClientId', full_name='EncryptedClientIdentification.EncryptedClientId', index=2, + number=3, type=12, cpp_type=9, label=2, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='EncryptedClientIdIv', full_name='EncryptedClientIdentification.EncryptedClientIdIv', index=3, + number=4, type=12, cpp_type=9, label=2, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='EncryptedPrivacyKey', full_name='EncryptedClientIdentification.EncryptedPrivacyKey', index=4, + number=5, type=12, cpp_type=9, label=2, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2560, + serialized_end=2735, +) + + +_LICENSEIDENTIFICATION = _descriptor.Descriptor( + name='LicenseIdentification', + full_name='LicenseIdentification', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='RequestId', full_name='LicenseIdentification.RequestId', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SessionId', full_name='LicenseIdentification.SessionId', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='PurchaseId', full_name='LicenseIdentification.PurchaseId', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Type', full_name='LicenseIdentification.Type', index=3, + number=4, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Version', full_name='LicenseIdentification.Version', index=4, + number=5, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ProviderSessionToken', full_name='LicenseIdentification.ProviderSessionToken', index=5, + number=6, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2738, + serialized_end=2894, +) + + +_LICENSE_POLICY = _descriptor.Descriptor( + name='Policy', + full_name='License.Policy', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='CanPlay', full_name='License.Policy.CanPlay', index=0, + number=1, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='CanPersist', full_name='License.Policy.CanPersist', index=1, + number=2, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='CanRenew', full_name='License.Policy.CanRenew', index=2, + number=3, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RentalDurationSeconds', full_name='License.Policy.RentalDurationSeconds', index=3, + number=4, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='PlaybackDurationSeconds', full_name='License.Policy.PlaybackDurationSeconds', index=4, + number=5, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='LicenseDurationSeconds', full_name='License.Policy.LicenseDurationSeconds', index=5, + number=6, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RenewalRecoveryDurationSeconds', full_name='License.Policy.RenewalRecoveryDurationSeconds', index=6, + number=7, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RenewalServerUrl', full_name='License.Policy.RenewalServerUrl', index=7, + number=8, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RenewalDelaySeconds', full_name='License.Policy.RenewalDelaySeconds', index=8, + number=9, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RenewalRetryIntervalSeconds', full_name='License.Policy.RenewalRetryIntervalSeconds', index=9, + number=10, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RenewWithUsage', full_name='License.Policy.RenewWithUsage', index=10, + number=11, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=3131, + serialized_end=3446, +) + +_LICENSE_KEYCONTAINER_OUTPUTPROTECTION = _descriptor.Descriptor( + name='OutputProtection', + full_name='License.KeyContainer.OutputProtection', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='Hdcp', full_name='License.KeyContainer.OutputProtection.Hdcp', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='CgmsFlags', full_name='License.KeyContainer.OutputProtection.CgmsFlags', index=1, + number=2, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _LICENSE_KEYCONTAINER_OUTPUTPROTECTION_CGMS, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=3970, + serialized_end=4189, +) + +_LICENSE_KEYCONTAINER_KEYCONTROL = _descriptor.Descriptor( + name='KeyControl', + full_name='License.KeyContainer.KeyControl', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='KeyControlBlock', full_name='License.KeyContainer.KeyControl.KeyControlBlock', index=0, + number=1, type=12, cpp_type=9, label=2, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Iv', full_name='License.KeyContainer.KeyControl.Iv', index=1, + number=2, type=12, cpp_type=9, label=2, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=4191, + serialized_end=4240, +) + +_LICENSE_KEYCONTAINER_OPERATORSESSIONKEYPERMISSIONS = _descriptor.Descriptor( + name='OperatorSessionKeyPermissions', + full_name='License.KeyContainer.OperatorSessionKeyPermissions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='AllowEncrypt', full_name='License.KeyContainer.OperatorSessionKeyPermissions.AllowEncrypt', index=0, + number=1, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='AllowDecrypt', full_name='License.KeyContainer.OperatorSessionKeyPermissions.AllowDecrypt', index=1, + number=2, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='AllowSign', full_name='License.KeyContainer.OperatorSessionKeyPermissions.AllowSign', index=2, + number=3, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='AllowSignatureVerify', full_name='License.KeyContainer.OperatorSessionKeyPermissions.AllowSignatureVerify', index=3, + number=4, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=4242, + serialized_end=4366, +) + +_LICENSE_KEYCONTAINER_VIDEORESOLUTIONCONSTRAINT = _descriptor.Descriptor( + name='VideoResolutionConstraint', + full_name='License.KeyContainer.VideoResolutionConstraint', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='MinResolutionPixels', full_name='License.KeyContainer.VideoResolutionConstraint.MinResolutionPixels', index=0, + number=1, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='MaxResolutionPixels', full_name='License.KeyContainer.VideoResolutionConstraint.MaxResolutionPixels', index=1, + number=2, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RequiredProtection', full_name='License.KeyContainer.VideoResolutionConstraint.RequiredProtection', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=4369, + serialized_end=4522, +) + +_LICENSE_KEYCONTAINER = _descriptor.Descriptor( + name='KeyContainer', + full_name='License.KeyContainer', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='Id', full_name='License.KeyContainer.Id', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Iv', full_name='License.KeyContainer.Iv', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Key', full_name='License.KeyContainer.Key', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Type', full_name='License.KeyContainer.Type', index=3, + number=4, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Level', full_name='License.KeyContainer.Level', index=4, + number=5, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RequiredProtection', full_name='License.KeyContainer.RequiredProtection', index=5, + number=6, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RequestedProtection', full_name='License.KeyContainer.RequestedProtection', index=6, + number=7, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='_KeyControl', full_name='License.KeyContainer._KeyControl', index=7, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='_OperatorSessionKeyPermissions', full_name='License.KeyContainer._OperatorSessionKeyPermissions', index=8, + number=9, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='VideoResolutionConstraints', full_name='License.KeyContainer.VideoResolutionConstraints', index=9, + number=10, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_LICENSE_KEYCONTAINER_OUTPUTPROTECTION, _LICENSE_KEYCONTAINER_KEYCONTROL, _LICENSE_KEYCONTAINER_OPERATORSESSIONKEYPERMISSIONS, _LICENSE_KEYCONTAINER_VIDEORESOLUTIONCONSTRAINT, ], + enum_types=[ + _LICENSE_KEYCONTAINER_KEYTYPE, + _LICENSE_KEYCONTAINER_SECURITYLEVEL, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=3449, + serialized_end=4722, +) + +_LICENSE = _descriptor.Descriptor( + name='License', + full_name='License', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='Id', full_name='License.Id', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='_Policy', full_name='License._Policy', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Key', full_name='License.Key', index=2, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='LicenseStartTime', full_name='License.LicenseStartTime', index=3, + number=4, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RemoteAttestationVerified', full_name='License.RemoteAttestationVerified', index=4, + number=5, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ProviderClientToken', full_name='License.ProviderClientToken', index=5, + number=6, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ProtectionScheme', full_name='License.ProtectionScheme', index=6, + number=7, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_LICENSE_POLICY, _LICENSE_KEYCONTAINER, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2897, + serialized_end=4722, +) + + +_LICENSEERROR = _descriptor.Descriptor( + name='LicenseError', + full_name='LicenseError', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='ErrorCode', full_name='LicenseError.ErrorCode', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _LICENSEERROR_ERROR, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=4725, + serialized_end=4877, +) + + +_LICENSEREQUEST_CONTENTIDENTIFICATION_CENC = _descriptor.Descriptor( + name='CENC', + full_name='LicenseRequest.ContentIdentification.CENC', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='Pssh', full_name='LicenseRequest.ContentIdentification.CENC.Pssh', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='LicenseType', full_name='LicenseRequest.ContentIdentification.CENC.LicenseType', index=1, + number=2, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RequestId', full_name='LicenseRequest.ContentIdentification.CENC.RequestId', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=5441, + serialized_end=5536, +) + +_LICENSEREQUEST_CONTENTIDENTIFICATION_WEBM = _descriptor.Descriptor( + name='WebM', + full_name='LicenseRequest.ContentIdentification.WebM', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='Header', full_name='LicenseRequest.ContentIdentification.WebM.Header', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='LicenseType', full_name='LicenseRequest.ContentIdentification.WebM.LicenseType', index=1, + number=2, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RequestId', full_name='LicenseRequest.ContentIdentification.WebM.RequestId', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=5538, + serialized_end=5614, +) + +_LICENSEREQUEST_CONTENTIDENTIFICATION_EXISTINGLICENSE = _descriptor.Descriptor( + name='ExistingLicense', + full_name='LicenseRequest.ContentIdentification.ExistingLicense', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='LicenseId', full_name='LicenseRequest.ContentIdentification.ExistingLicense.LicenseId', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SecondsSinceStarted', full_name='LicenseRequest.ContentIdentification.ExistingLicense.SecondsSinceStarted', index=1, + number=2, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SecondsSinceLastPlayed', full_name='LicenseRequest.ContentIdentification.ExistingLicense.SecondsSinceLastPlayed', index=2, + number=3, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SessionUsageTableEntry', full_name='LicenseRequest.ContentIdentification.ExistingLicense.SessionUsageTableEntry', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=5617, + serialized_end=5770, +) + +_LICENSEREQUEST_CONTENTIDENTIFICATION = _descriptor.Descriptor( + name='ContentIdentification', + full_name='LicenseRequest.ContentIdentification', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='CencId', full_name='LicenseRequest.ContentIdentification.CencId', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='WebmId', full_name='LicenseRequest.ContentIdentification.WebmId', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='License', full_name='LicenseRequest.ContentIdentification.License', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_LICENSEREQUEST_CONTENTIDENTIFICATION_CENC, _LICENSEREQUEST_CONTENTIDENTIFICATION_WEBM, _LICENSEREQUEST_CONTENTIDENTIFICATION_EXISTINGLICENSE, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=5224, + serialized_end=5770, +) + +_LICENSEREQUEST = _descriptor.Descriptor( + name='LicenseRequest', + full_name='LicenseRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='ClientId', full_name='LicenseRequest.ClientId', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ContentId', full_name='LicenseRequest.ContentId', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Type', full_name='LicenseRequest.Type', index=2, + number=3, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RequestTime', full_name='LicenseRequest.RequestTime', index=3, + number=4, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='KeyControlNonceDeprecated', full_name='LicenseRequest.KeyControlNonceDeprecated', index=4, + number=5, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ProtocolVersion', full_name='LicenseRequest.ProtocolVersion', index=5, + number=6, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=20, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='KeyControlNonce', full_name='LicenseRequest.KeyControlNonce', index=6, + number=7, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='EncryptedClientId', full_name='LicenseRequest.EncryptedClientId', index=7, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_LICENSEREQUEST_CONTENTIDENTIFICATION, ], + enum_types=[ + _LICENSEREQUEST_REQUESTTYPE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=4880, + serialized_end=5820, +) + + +_LICENSEREQUESTRAW_CONTENTIDENTIFICATION_CENC = _descriptor.Descriptor( + name='CENC', + full_name='LicenseRequestRaw.ContentIdentification.CENC', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='Pssh', full_name='LicenseRequestRaw.ContentIdentification.CENC.Pssh', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='LicenseType', full_name='LicenseRequestRaw.ContentIdentification.CENC.LicenseType', index=1, + number=2, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RequestId', full_name='LicenseRequestRaw.ContentIdentification.CENC.RequestId', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=6402, + serialized_end=6476, +) + +_LICENSEREQUESTRAW_CONTENTIDENTIFICATION_WEBM = _descriptor.Descriptor( + name='WebM', + full_name='LicenseRequestRaw.ContentIdentification.WebM', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='Header', full_name='LicenseRequestRaw.ContentIdentification.WebM.Header', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='LicenseType', full_name='LicenseRequestRaw.ContentIdentification.WebM.LicenseType', index=1, + number=2, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RequestId', full_name='LicenseRequestRaw.ContentIdentification.WebM.RequestId', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=5538, + serialized_end=5614, +) + +_LICENSEREQUESTRAW_CONTENTIDENTIFICATION_EXISTINGLICENSE = _descriptor.Descriptor( + name='ExistingLicense', + full_name='LicenseRequestRaw.ContentIdentification.ExistingLicense', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='LicenseId', full_name='LicenseRequestRaw.ContentIdentification.ExistingLicense.LicenseId', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SecondsSinceStarted', full_name='LicenseRequestRaw.ContentIdentification.ExistingLicense.SecondsSinceStarted', index=1, + number=2, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SecondsSinceLastPlayed', full_name='LicenseRequestRaw.ContentIdentification.ExistingLicense.SecondsSinceLastPlayed', index=2, + number=3, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SessionUsageTableEntry', full_name='LicenseRequestRaw.ContentIdentification.ExistingLicense.SessionUsageTableEntry', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=5617, + serialized_end=5770, +) + +_LICENSEREQUESTRAW_CONTENTIDENTIFICATION = _descriptor.Descriptor( + name='ContentIdentification', + full_name='LicenseRequestRaw.ContentIdentification', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='CencId', full_name='LicenseRequestRaw.ContentIdentification.CencId', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='WebmId', full_name='LicenseRequestRaw.ContentIdentification.WebmId', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='License', full_name='LicenseRequestRaw.ContentIdentification.License', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_LICENSEREQUESTRAW_CONTENTIDENTIFICATION_CENC, _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_WEBM, _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_EXISTINGLICENSE, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=6176, + serialized_end=6710, +) + +_LICENSEREQUESTRAW = _descriptor.Descriptor( + name='LicenseRequestRaw', + full_name='LicenseRequestRaw', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='ClientId', full_name='LicenseRequestRaw.ClientId', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ContentId', full_name='LicenseRequestRaw.ContentId', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Type', full_name='LicenseRequestRaw.Type', index=2, + number=3, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RequestTime', full_name='LicenseRequestRaw.RequestTime', index=3, + number=4, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='KeyControlNonceDeprecated', full_name='LicenseRequestRaw.KeyControlNonceDeprecated', index=4, + number=5, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ProtocolVersion', full_name='LicenseRequestRaw.ProtocolVersion', index=5, + number=6, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=20, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='KeyControlNonce', full_name='LicenseRequestRaw.KeyControlNonce', index=6, + number=7, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='EncryptedClientId', full_name='LicenseRequestRaw.EncryptedClientId', index=7, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_LICENSEREQUESTRAW_CONTENTIDENTIFICATION, ], + enum_types=[ + _LICENSEREQUESTRAW_REQUESTTYPE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=5823, + serialized_end=6760, +) + + +_PROVISIONEDDEVICEINFO = _descriptor.Descriptor( + name='ProvisionedDeviceInfo', + full_name='ProvisionedDeviceInfo', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='SystemId', full_name='ProvisionedDeviceInfo.SystemId', index=0, + number=1, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Soc', full_name='ProvisionedDeviceInfo.Soc', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Manufacturer', full_name='ProvisionedDeviceInfo.Manufacturer', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Model', full_name='ProvisionedDeviceInfo.Model', index=3, + number=4, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='DeviceType', full_name='ProvisionedDeviceInfo.DeviceType', index=4, + number=5, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ModelYear', full_name='ProvisionedDeviceInfo.ModelYear', index=5, + number=6, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SecurityLevel', full_name='ProvisionedDeviceInfo.SecurityLevel', index=6, + number=7, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='TestDevice', full_name='ProvisionedDeviceInfo.TestDevice', index=7, + number=8, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _PROVISIONEDDEVICEINFO_WVSECURITYLEVEL, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=6763, + serialized_end=7057, +) + + +_PROVISIONINGOPTIONS = _descriptor.Descriptor( + name='ProvisioningOptions', + full_name='ProvisioningOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='certificate_type', full_name='ProvisioningOptions.certificate_type', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='certificate_authority', full_name='ProvisioningOptions.certificate_authority', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _PROVISIONINGOPTIONS_CERTIFICATETYPE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=7060, + serialized_end=7237, +) + + +_PROVISIONINGREQUEST = _descriptor.Descriptor( + name='ProvisioningRequest', + full_name='ProvisioningRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='client_id', full_name='ProvisioningRequest.client_id', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='encrypted_client_id', full_name='ProvisioningRequest.encrypted_client_id', index=1, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='nonce', full_name='ProvisioningRequest.nonce', index=2, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='options', full_name='ProvisioningRequest.options', index=3, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='stable_id', full_name='ProvisioningRequest.stable_id', index=4, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='provider_id', full_name='ProvisioningRequest.provider_id', index=5, + number=6, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='spoid', full_name='ProvisioningRequest.spoid', index=6, + number=7, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=7240, + serialized_end=7476, +) + + +_PROVISIONINGRESPONSE = _descriptor.Descriptor( + name='ProvisioningResponse', + full_name='ProvisioningResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='device_rsa_key', full_name='ProvisioningResponse.device_rsa_key', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='device_rsa_key_iv', full_name='ProvisioningResponse.device_rsa_key_iv', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='device_certificate', full_name='ProvisioningResponse.device_certificate', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='nonce', full_name='ProvisioningResponse.nonce', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='wrapping_key', full_name='ProvisioningResponse.wrapping_key', index=4, + number=5, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=7479, + serialized_end=7617, +) + + +_REMOTEATTESTATION = _descriptor.Descriptor( + name='RemoteAttestation', + full_name='RemoteAttestation', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='Certificate', full_name='RemoteAttestation.Certificate', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Salt', full_name='RemoteAttestation.Salt', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Signature', full_name='RemoteAttestation.Signature', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=7619, + serialized_end=7724, +) + + +_SESSIONINIT = _descriptor.Descriptor( + name='SessionInit', + full_name='SessionInit', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=7726, + serialized_end=7739, +) + + +_SESSIONSTATE = _descriptor.Descriptor( + name='SessionState', + full_name='SessionState', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=7741, + serialized_end=7755, +) + + +_SIGNEDCERTIFICATESTATUSLIST = _descriptor.Descriptor( + name='SignedCertificateStatusList', + full_name='SignedCertificateStatusList', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=7757, + serialized_end=7786, +) + + +_SIGNEDDEVICECERTIFICATE = _descriptor.Descriptor( + name='SignedDeviceCertificate', + full_name='SignedDeviceCertificate', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='_DeviceCertificate', full_name='SignedDeviceCertificate._DeviceCertificate', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Signature', full_name='SignedDeviceCertificate.Signature', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Signer', full_name='SignedDeviceCertificate.Signer', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=7789, + serialized_end=7923, +) + + +_SIGNEDPROVISIONINGMESSAGE = _descriptor.Descriptor( + name='SignedProvisioningMessage', + full_name='SignedProvisioningMessage', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='message', full_name='SignedProvisioningMessage.message', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='signature', full_name='SignedProvisioningMessage.signature', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='protocol_version', full_name='SignedProvisioningMessage.protocol_version', index=2, + number=3, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=20, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=7925, + serialized_end=8045, +) + + +_SIGNEDMESSAGE = _descriptor.Descriptor( + name='SignedMessage', + full_name='SignedMessage', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='Type', full_name='SignedMessage.Type', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Msg', full_name='SignedMessage.Msg', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Signature', full_name='SignedMessage.Signature', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SessionKey', full_name='SignedMessage.SessionKey', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RemoteAttestation', full_name='SignedMessage.RemoteAttestation', index=4, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _SIGNEDMESSAGEmessageTYPE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=8048, + serialized_end=8331, +) + + +_WIDEVINECENCHEADER = _descriptor.Descriptor( + name='WidevineCencHeader', + full_name='WidevineCencHeader', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='algorithm', full_name='WidevineCencHeader.algorithm', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='key_id', full_name='WidevineCencHeader.key_id', index=1, + number=2, type=12, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='provider', full_name='WidevineCencHeader.provider', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='content_id', full_name='WidevineCencHeader.content_id', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='track_type_deprecated', full_name='WidevineCencHeader.track_type_deprecated', index=4, + number=5, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='policy', full_name='WidevineCencHeader.policy', index=5, + number=6, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='crypto_period_index', full_name='WidevineCencHeader.crypto_period_index', index=6, + number=7, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='grouped_license', full_name='WidevineCencHeader.grouped_license', index=7, + number=8, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='protection_scheme', full_name='WidevineCencHeader.protection_scheme', index=8, + number=9, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='crypto_period_seconds', full_name='WidevineCencHeader.crypto_period_seconds', index=9, + number=10, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _WIDEVINECENCHEADER_ALGORITHM, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=8334, + serialized_end=8659, +) + + +_SIGNEDLICENSEREQUEST = _descriptor.Descriptor( + name='SignedLicenseRequest', + full_name='SignedLicenseRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='Type', full_name='SignedLicenseRequest.Type', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Msg', full_name='SignedLicenseRequest.Msg', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Signature', full_name='SignedLicenseRequest.Signature', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SessionKey', full_name='SignedLicenseRequest.SessionKey', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RemoteAttestation', full_name='SignedLicenseRequest.RemoteAttestation', index=4, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _SIGNEDLICENSEREQUESTmessageTYPE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=8662, + serialized_end=8976, +) + + +_SIGNEDLICENSEREQUESTRAW = _descriptor.Descriptor( + name='SignedLicenseRequestRaw', + full_name='SignedLicenseRequestRaw', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='Type', full_name='SignedLicenseRequestRaw.Type', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Msg', full_name='SignedLicenseRequestRaw.Msg', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Signature', full_name='SignedLicenseRequestRaw.Signature', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SessionKey', full_name='SignedLicenseRequestRaw.SessionKey', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RemoteAttestation', full_name='SignedLicenseRequestRaw.RemoteAttestation', index=4, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _SIGNEDLICENSEREQUESTRAWmessageTYPE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=8979, + serialized_end=9302, +) + + +_SIGNEDLICENSE = _descriptor.Descriptor( + name='SignedLicense', + full_name='SignedLicense', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='Type', full_name='SignedLicense.Type', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Msg', full_name='SignedLicense.Msg', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Signature', full_name='SignedLicense.Signature', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SessionKey', full_name='SignedLicense.SessionKey', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RemoteAttestation', full_name='SignedLicense.RemoteAttestation', index=4, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _SIGNEDLICENSEmessageTYPE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=9305, + serialized_end=9598, +) + + +_SIGNEDSERVICECERTIFICATE = _descriptor.Descriptor( + name='SignedServiceCertificate', + full_name='SignedServiceCertificate', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='Type', full_name='SignedServiceCertificate.Type', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Msg', full_name='SignedServiceCertificate.Msg', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='Signature', full_name='SignedServiceCertificate.Signature', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SessionKey', full_name='SignedServiceCertificate.SessionKey', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='RemoteAttestation', full_name='SignedServiceCertificate.RemoteAttestation', index=4, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _SIGNEDSERVICECERTIFICATEmessageTYPE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=9601, + serialized_end=9932, +) + + +_FILEHASHES_SIGNATURE = _descriptor.Descriptor( + name='Signature', + full_name='FileHashes.Signature', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='filename', full_name='FileHashes.Signature.filename', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='test_signing', full_name='FileHashes.Signature.test_signing', index=1, + number=2, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='SHA512Hash', full_name='FileHashes.Signature.SHA512Hash', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='main_exe', full_name='FileHashes.Signature.main_exe', index=3, + number=4, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='signature', full_name='FileHashes.Signature.signature', index=4, + number=5, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=10008, + serialized_end=10116, +) + +_FILEHASHES = _descriptor.Descriptor( + name='FileHashes', + full_name='FileHashes', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='signer', full_name='FileHashes.signer', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='signatures', full_name='FileHashes.signatures', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[_FILEHASHES_SIGNATURE, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=9935, + serialized_end=10116, +) + +_CLIENTIDENTIFICATION_NAMEVALUE.containing_type = _CLIENTIDENTIFICATION +_CLIENTIDENTIFICATION_CLIENTCAPABILITIES.fields_by_name['MaxHdcpVersion'].enum_type = _CLIENTIDENTIFICATION_CLIENTCAPABILITIES_HDCPVERSION +_CLIENTIDENTIFICATION_CLIENTCAPABILITIES.fields_by_name['SupportedCertificateKeyType'].enum_type = _CLIENTIDENTIFICATION_CLIENTCAPABILITIES_CERTIFICATEKEYTYPE +_CLIENTIDENTIFICATION_CLIENTCAPABILITIES.containing_type = _CLIENTIDENTIFICATION +_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_HDCPVERSION.containing_type = _CLIENTIDENTIFICATION_CLIENTCAPABILITIES +_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_CERTIFICATEKEYTYPE.containing_type = _CLIENTIDENTIFICATION_CLIENTCAPABILITIES +_CLIENTIDENTIFICATION.fields_by_name['Type'].enum_type = _CLIENTIDENTIFICATION_TOKENTYPE +_CLIENTIDENTIFICATION.fields_by_name['Token'].message_type = _SIGNEDDEVICECERTIFICATE +_CLIENTIDENTIFICATION.fields_by_name['ClientInfo'].message_type = _CLIENTIDENTIFICATION_NAMEVALUE +_CLIENTIDENTIFICATION.fields_by_name['_ClientCapabilities'].message_type = _CLIENTIDENTIFICATION_CLIENTCAPABILITIES +_CLIENTIDENTIFICATION.fields_by_name['_FileHashes'].message_type = _FILEHASHES +_CLIENTIDENTIFICATION_TOKENTYPE.containing_type = _CLIENTIDENTIFICATION +_CLIENTIDENTIFICATIONRAW_NAMEVALUE.containing_type = _CLIENTIDENTIFICATIONRAW +_CLIENTIDENTIFICATIONRAW_CLIENTCAPABILITIES.fields_by_name['MaxHdcpVersion'].enum_type = _CLIENTIDENTIFICATIONRAW_CLIENTCAPABILITIES_HDCPVERSION +_CLIENTIDENTIFICATIONRAW_CLIENTCAPABILITIES.fields_by_name['SupportedCertificateKeyType'].enum_type = _CLIENTIDENTIFICATIONRAW_CLIENTCAPABILITIES_CERTIFICATEKEYTYPE +_CLIENTIDENTIFICATIONRAW_CLIENTCAPABILITIES.containing_type = _CLIENTIDENTIFICATIONRAW +_CLIENTIDENTIFICATIONRAW_CLIENTCAPABILITIES_HDCPVERSION.containing_type = _CLIENTIDENTIFICATIONRAW_CLIENTCAPABILITIES +_CLIENTIDENTIFICATIONRAW_CLIENTCAPABILITIES_CERTIFICATEKEYTYPE.containing_type = _CLIENTIDENTIFICATIONRAW_CLIENTCAPABILITIES +_CLIENTIDENTIFICATIONRAW.fields_by_name['Type'].enum_type = _CLIENTIDENTIFICATIONRAW_TOKENTYPE +_CLIENTIDENTIFICATIONRAW.fields_by_name['ClientInfo'].message_type = _CLIENTIDENTIFICATIONRAW_NAMEVALUE +_CLIENTIDENTIFICATIONRAW.fields_by_name['_ClientCapabilities'].message_type = _CLIENTIDENTIFICATIONRAW_CLIENTCAPABILITIES +_CLIENTIDENTIFICATIONRAW.fields_by_name['_FileHashes'].message_type = _FILEHASHES +_CLIENTIDENTIFICATIONRAW_TOKENTYPE.containing_type = _CLIENTIDENTIFICATIONRAW +_DEVICECERTIFICATE.fields_by_name['Type'].enum_type = _DEVICECERTIFICATE_CERTIFICATETYPE +_DEVICECERTIFICATE_CERTIFICATETYPE.containing_type = _DEVICECERTIFICATE +_DEVICECERTIFICATESTATUS.fields_by_name['Status'].enum_type = _DEVICECERTIFICATESTATUS_CERTIFICATESTATUS +_DEVICECERTIFICATESTATUS.fields_by_name['DeviceInfo'].message_type = _PROVISIONEDDEVICEINFO +_DEVICECERTIFICATESTATUS_CERTIFICATESTATUS.containing_type = _DEVICECERTIFICATESTATUS +_DEVICECERTIFICATESTATUSLIST.fields_by_name['CertificateStatus'].message_type = _DEVICECERTIFICATESTATUS +_LICENSEIDENTIFICATION.fields_by_name['Type'].enum_type = _LICENSETYPE +_LICENSE_POLICY.containing_type = _LICENSE +_LICENSE_KEYCONTAINER_OUTPUTPROTECTION.fields_by_name['Hdcp'].enum_type = _CLIENTIDENTIFICATION_CLIENTCAPABILITIES_HDCPVERSION +_LICENSE_KEYCONTAINER_OUTPUTPROTECTION.fields_by_name['CgmsFlags'].enum_type = _LICENSE_KEYCONTAINER_OUTPUTPROTECTION_CGMS +_LICENSE_KEYCONTAINER_OUTPUTPROTECTION.containing_type = _LICENSE_KEYCONTAINER +_LICENSE_KEYCONTAINER_OUTPUTPROTECTION_CGMS.containing_type = _LICENSE_KEYCONTAINER_OUTPUTPROTECTION +_LICENSE_KEYCONTAINER_KEYCONTROL.containing_type = _LICENSE_KEYCONTAINER +_LICENSE_KEYCONTAINER_OPERATORSESSIONKEYPERMISSIONS.containing_type = _LICENSE_KEYCONTAINER +_LICENSE_KEYCONTAINER_VIDEORESOLUTIONCONSTRAINT.fields_by_name['RequiredProtection'].message_type = _LICENSE_KEYCONTAINER_OUTPUTPROTECTION +_LICENSE_KEYCONTAINER_VIDEORESOLUTIONCONSTRAINT.containing_type = _LICENSE_KEYCONTAINER +_LICENSE_KEYCONTAINER.fields_by_name['Type'].enum_type = _LICENSE_KEYCONTAINER_KEYTYPE +_LICENSE_KEYCONTAINER.fields_by_name['Level'].enum_type = _LICENSE_KEYCONTAINER_SECURITYLEVEL +_LICENSE_KEYCONTAINER.fields_by_name['RequiredProtection'].message_type = _LICENSE_KEYCONTAINER_OUTPUTPROTECTION +_LICENSE_KEYCONTAINER.fields_by_name['RequestedProtection'].message_type = _LICENSE_KEYCONTAINER_OUTPUTPROTECTION +_LICENSE_KEYCONTAINER.fields_by_name['_KeyControl'].message_type = _LICENSE_KEYCONTAINER_KEYCONTROL +_LICENSE_KEYCONTAINER.fields_by_name['_OperatorSessionKeyPermissions'].message_type = _LICENSE_KEYCONTAINER_OPERATORSESSIONKEYPERMISSIONS +_LICENSE_KEYCONTAINER.fields_by_name['VideoResolutionConstraints'].message_type = _LICENSE_KEYCONTAINER_VIDEORESOLUTIONCONSTRAINT +_LICENSE_KEYCONTAINER.containing_type = _LICENSE +_LICENSE_KEYCONTAINER_KEYTYPE.containing_type = _LICENSE_KEYCONTAINER +_LICENSE_KEYCONTAINER_SECURITYLEVEL.containing_type = _LICENSE_KEYCONTAINER +_LICENSE.fields_by_name['Id'].message_type = _LICENSEIDENTIFICATION +_LICENSE.fields_by_name['_Policy'].message_type = _LICENSE_POLICY +_LICENSE.fields_by_name['Key'].message_type = _LICENSE_KEYCONTAINER +_LICENSEERROR.fields_by_name['ErrorCode'].enum_type = _LICENSEERROR_ERROR +_LICENSEERROR_ERROR.containing_type = _LICENSEERROR +_LICENSEREQUEST_CONTENTIDENTIFICATION_CENC.fields_by_name['Pssh'].message_type = _WIDEVINECENCHEADER +_LICENSEREQUEST_CONTENTIDENTIFICATION_CENC.fields_by_name['LicenseType'].enum_type = _LICENSETYPE +_LICENSEREQUEST_CONTENTIDENTIFICATION_CENC.containing_type = _LICENSEREQUEST_CONTENTIDENTIFICATION +_LICENSEREQUEST_CONTENTIDENTIFICATION_WEBM.fields_by_name['LicenseType'].enum_type = _LICENSETYPE +_LICENSEREQUEST_CONTENTIDENTIFICATION_WEBM.containing_type = _LICENSEREQUEST_CONTENTIDENTIFICATION +_LICENSEREQUEST_CONTENTIDENTIFICATION_EXISTINGLICENSE.fields_by_name['LicenseId'].message_type = _LICENSEIDENTIFICATION +_LICENSEREQUEST_CONTENTIDENTIFICATION_EXISTINGLICENSE.containing_type = _LICENSEREQUEST_CONTENTIDENTIFICATION +_LICENSEREQUEST_CONTENTIDENTIFICATION.fields_by_name['CencId'].message_type = _LICENSEREQUEST_CONTENTIDENTIFICATION_CENC +_LICENSEREQUEST_CONTENTIDENTIFICATION.fields_by_name['WebmId'].message_type = _LICENSEREQUEST_CONTENTIDENTIFICATION_WEBM +_LICENSEREQUEST_CONTENTIDENTIFICATION.fields_by_name['License'].message_type = _LICENSEREQUEST_CONTENTIDENTIFICATION_EXISTINGLICENSE +_LICENSEREQUEST_CONTENTIDENTIFICATION.containing_type = _LICENSEREQUEST +_LICENSEREQUEST.fields_by_name['ClientId'].message_type = _CLIENTIDENTIFICATION +_LICENSEREQUEST.fields_by_name['ContentId'].message_type = _LICENSEREQUEST_CONTENTIDENTIFICATION +_LICENSEREQUEST.fields_by_name['Type'].enum_type = _LICENSEREQUEST_REQUESTTYPE +_LICENSEREQUEST.fields_by_name['ProtocolVersion'].enum_type = _PROTOCOLVERSION +_LICENSEREQUEST.fields_by_name['EncryptedClientId'].message_type = _ENCRYPTEDCLIENTIDENTIFICATION +_LICENSEREQUEST_REQUESTTYPE.containing_type = _LICENSEREQUEST +_LICENSEREQUESTRAW_CONTENTIDENTIFICATION_CENC.fields_by_name['LicenseType'].enum_type = _LICENSETYPE +_LICENSEREQUESTRAW_CONTENTIDENTIFICATION_CENC.containing_type = _LICENSEREQUESTRAW_CONTENTIDENTIFICATION +_LICENSEREQUESTRAW_CONTENTIDENTIFICATION_WEBM.fields_by_name['LicenseType'].enum_type = _LICENSETYPE +_LICENSEREQUESTRAW_CONTENTIDENTIFICATION_WEBM.containing_type = _LICENSEREQUESTRAW_CONTENTIDENTIFICATION +_LICENSEREQUESTRAW_CONTENTIDENTIFICATION_EXISTINGLICENSE.fields_by_name['LicenseId'].message_type = _LICENSEIDENTIFICATION +_LICENSEREQUESTRAW_CONTENTIDENTIFICATION_EXISTINGLICENSE.containing_type = _LICENSEREQUESTRAW_CONTENTIDENTIFICATION +_LICENSEREQUESTRAW_CONTENTIDENTIFICATION.fields_by_name['CencId'].message_type = _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_CENC +_LICENSEREQUESTRAW_CONTENTIDENTIFICATION.fields_by_name['WebmId'].message_type = _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_WEBM +_LICENSEREQUESTRAW_CONTENTIDENTIFICATION.fields_by_name['License'].message_type = _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_EXISTINGLICENSE +_LICENSEREQUESTRAW_CONTENTIDENTIFICATION.containing_type = _LICENSEREQUESTRAW +_LICENSEREQUESTRAW.fields_by_name['ClientId'].message_type = _CLIENTIDENTIFICATION +_LICENSEREQUESTRAW.fields_by_name['ContentId'].message_type = _LICENSEREQUESTRAW_CONTENTIDENTIFICATION +_LICENSEREQUESTRAW.fields_by_name['Type'].enum_type = _LICENSEREQUESTRAW_REQUESTTYPE +_LICENSEREQUESTRAW.fields_by_name['ProtocolVersion'].enum_type = _PROTOCOLVERSION +_LICENSEREQUESTRAW.fields_by_name['EncryptedClientId'].message_type = _ENCRYPTEDCLIENTIDENTIFICATION +_LICENSEREQUESTRAW_REQUESTTYPE.containing_type = _LICENSEREQUESTRAW +_PROVISIONEDDEVICEINFO.fields_by_name['SecurityLevel'].enum_type = _PROVISIONEDDEVICEINFO_WVSECURITYLEVEL +_PROVISIONEDDEVICEINFO_WVSECURITYLEVEL.containing_type = _PROVISIONEDDEVICEINFO +_PROVISIONINGOPTIONS.fields_by_name['certificate_type'].enum_type = _PROVISIONINGOPTIONS_CERTIFICATETYPE +_PROVISIONINGOPTIONS_CERTIFICATETYPE.containing_type = _PROVISIONINGOPTIONS +_PROVISIONINGREQUEST.fields_by_name['client_id'].message_type = _CLIENTIDENTIFICATIONRAW +_PROVISIONINGREQUEST.fields_by_name['encrypted_client_id'].message_type = _ENCRYPTEDCLIENTIDENTIFICATION +_PROVISIONINGREQUEST.fields_by_name['options'].message_type = _PROVISIONINGOPTIONS +_REMOTEATTESTATION.fields_by_name['Certificate'].message_type = _ENCRYPTEDCLIENTIDENTIFICATION +_SIGNEDDEVICECERTIFICATE.fields_by_name['_DeviceCertificate'].message_type = _DEVICECERTIFICATE +_SIGNEDDEVICECERTIFICATE.fields_by_name['Signer'].message_type = _SIGNEDDEVICECERTIFICATE +_SIGNEDPROVISIONINGMESSAGE.fields_by_name['protocol_version'].enum_type = _PROTOCOLVERSION +_SIGNEDMESSAGE.fields_by_name['Type'].enum_type = _SIGNEDMESSAGEmessageTYPE +_SIGNEDMESSAGE.fields_by_name['RemoteAttestation'].message_type = _REMOTEATTESTATION +_SIGNEDMESSAGEmessageTYPE.containing_type = _SIGNEDMESSAGE +_WIDEVINECENCHEADER.fields_by_name['algorithm'].enum_type = _WIDEVINECENCHEADER_ALGORITHM +_WIDEVINECENCHEADER_ALGORITHM.containing_type = _WIDEVINECENCHEADER +_SIGNEDLICENSEREQUEST.fields_by_name['Type'].enum_type = _SIGNEDLICENSEREQUESTmessageTYPE +_SIGNEDLICENSEREQUEST.fields_by_name['Msg'].message_type = _LICENSEREQUEST +_SIGNEDLICENSEREQUEST.fields_by_name['RemoteAttestation'].message_type = _REMOTEATTESTATION +_SIGNEDLICENSEREQUESTmessageTYPE.containing_type = _SIGNEDLICENSEREQUEST +_SIGNEDLICENSEREQUESTRAW.fields_by_name['Type'].enum_type = _SIGNEDLICENSEREQUESTRAWmessageTYPE +_SIGNEDLICENSEREQUESTRAW.fields_by_name['Msg'].message_type = _LICENSEREQUESTRAW +_SIGNEDLICENSEREQUESTRAW.fields_by_name['RemoteAttestation'].message_type = _REMOTEATTESTATION +_SIGNEDLICENSEREQUESTRAWmessageTYPE.containing_type = _SIGNEDLICENSEREQUESTRAW +_SIGNEDLICENSE.fields_by_name['Type'].enum_type = _SIGNEDLICENSEmessageTYPE +_SIGNEDLICENSE.fields_by_name['Msg'].message_type = _LICENSE +_SIGNEDLICENSE.fields_by_name['RemoteAttestation'].message_type = _REMOTEATTESTATION +_SIGNEDLICENSEmessageTYPE.containing_type = _SIGNEDLICENSE +_SIGNEDSERVICECERTIFICATE.fields_by_name['Type'].enum_type = _SIGNEDSERVICECERTIFICATEmessageTYPE +_SIGNEDSERVICECERTIFICATE.fields_by_name['Msg'].message_type = _SIGNEDDEVICECERTIFICATE +_SIGNEDSERVICECERTIFICATE.fields_by_name['RemoteAttestation'].message_type = _REMOTEATTESTATION +_SIGNEDSERVICECERTIFICATEmessageTYPE.containing_type = _SIGNEDSERVICECERTIFICATE +_FILEHASHES_SIGNATURE.containing_type = _FILEHASHES +_FILEHASHES.fields_by_name['signatures'].message_type = _FILEHASHES_SIGNATURE +DESCRIPTOR.message_types_by_name['ClientIdentification'] = _CLIENTIDENTIFICATION +DESCRIPTOR.message_types_by_name['ClientIdentificationRaw'] = _CLIENTIDENTIFICATIONRAW +DESCRIPTOR.message_types_by_name['DeviceCertificate'] = _DEVICECERTIFICATE +DESCRIPTOR.message_types_by_name['DeviceCertificateStatus'] = _DEVICECERTIFICATESTATUS +DESCRIPTOR.message_types_by_name['DeviceCertificateStatusList'] = _DEVICECERTIFICATESTATUSLIST +DESCRIPTOR.message_types_by_name['EncryptedClientIdentification'] = _ENCRYPTEDCLIENTIDENTIFICATION +DESCRIPTOR.message_types_by_name['LicenseIdentification'] = _LICENSEIDENTIFICATION +DESCRIPTOR.message_types_by_name['License'] = _LICENSE +DESCRIPTOR.message_types_by_name['LicenseError'] = _LICENSEERROR +DESCRIPTOR.message_types_by_name['LicenseRequest'] = _LICENSEREQUEST +DESCRIPTOR.message_types_by_name['LicenseRequestRaw'] = _LICENSEREQUESTRAW +DESCRIPTOR.message_types_by_name['ProvisionedDeviceInfo'] = _PROVISIONEDDEVICEINFO +DESCRIPTOR.message_types_by_name['ProvisioningOptions'] = _PROVISIONINGOPTIONS +DESCRIPTOR.message_types_by_name['ProvisioningRequest'] = _PROVISIONINGREQUEST +DESCRIPTOR.message_types_by_name['ProvisioningResponse'] = _PROVISIONINGRESPONSE +DESCRIPTOR.message_types_by_name['RemoteAttestation'] = _REMOTEATTESTATION +DESCRIPTOR.message_types_by_name['SessionInit'] = _SESSIONINIT +DESCRIPTOR.message_types_by_name['SessionState'] = _SESSIONSTATE +DESCRIPTOR.message_types_by_name['SignedCertificateStatusList'] = _SIGNEDCERTIFICATESTATUSLIST +DESCRIPTOR.message_types_by_name['SignedDeviceCertificate'] = _SIGNEDDEVICECERTIFICATE +DESCRIPTOR.message_types_by_name['SignedProvisioningMessage'] = _SIGNEDPROVISIONINGMESSAGE +DESCRIPTOR.message_types_by_name['SignedMessage'] = _SIGNEDMESSAGE +DESCRIPTOR.message_types_by_name['WidevineCencHeader'] = _WIDEVINECENCHEADER +DESCRIPTOR.message_types_by_name['SignedLicenseRequest'] = _SIGNEDLICENSEREQUEST +DESCRIPTOR.message_types_by_name['SignedLicenseRequestRaw'] = _SIGNEDLICENSEREQUESTRAW +DESCRIPTOR.message_types_by_name['SignedLicense'] = _SIGNEDLICENSE +DESCRIPTOR.message_types_by_name['SignedServiceCertificate'] = _SIGNEDSERVICECERTIFICATE +DESCRIPTOR.message_types_by_name['FileHashes'] = _FILEHASHES +DESCRIPTOR.enum_types_by_name['LicenseType'] = _LICENSETYPE +DESCRIPTOR.enum_types_by_name['ProtocolVersion'] = _PROTOCOLVERSION +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +ClientIdentification = _reflection.GeneratedProtocolMessageType('ClientIdentification', (message.Message,), dict( + + NameValue = _reflection.GeneratedProtocolMessageType('NameValue', (message.Message,), dict( + DESCRIPTOR = _CLIENTIDENTIFICATION_NAMEVALUE, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:ClientIdentification.NameValue) + )) + , + + ClientCapabilities = _reflection.GeneratedProtocolMessageType('ClientCapabilities', (message.Message,), dict( + DESCRIPTOR = _CLIENTIDENTIFICATION_CLIENTCAPABILITIES, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:ClientIdentification.ClientCapabilities) + )) + , + DESCRIPTOR = _CLIENTIDENTIFICATION, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:ClientIdentification) + )) +_sym_db.RegisterMessage(ClientIdentification) +_sym_db.RegisterMessage(ClientIdentification.NameValue) +_sym_db.RegisterMessage(ClientIdentification.ClientCapabilities) + +ClientIdentificationRaw = _reflection.GeneratedProtocolMessageType('ClientIdentificationRaw', (message.Message,), dict( + + NameValue = _reflection.GeneratedProtocolMessageType('NameValue', (message.Message,), dict( + DESCRIPTOR = _CLIENTIDENTIFICATIONRAW_NAMEVALUE, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:ClientIdentificationRaw.NameValue) + )) + , + + ClientCapabilities = _reflection.GeneratedProtocolMessageType('ClientCapabilities', (message.Message,), dict( + DESCRIPTOR = _CLIENTIDENTIFICATIONRAW_CLIENTCAPABILITIES, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:ClientIdentificationRaw.ClientCapabilities) + )) + , + DESCRIPTOR = _CLIENTIDENTIFICATIONRAW, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:ClientIdentificationRaw) + )) +_sym_db.RegisterMessage(ClientIdentificationRaw) +_sym_db.RegisterMessage(ClientIdentificationRaw.NameValue) +_sym_db.RegisterMessage(ClientIdentificationRaw.ClientCapabilities) + +DeviceCertificate = _reflection.GeneratedProtocolMessageType('DeviceCertificate', (message.Message,), dict( + DESCRIPTOR = _DEVICECERTIFICATE, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:DeviceCertificate) + )) +_sym_db.RegisterMessage(DeviceCertificate) + +DeviceCertificateStatus = _reflection.GeneratedProtocolMessageType('DeviceCertificateStatus', (message.Message,), dict( + DESCRIPTOR = _DEVICECERTIFICATESTATUS, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:DeviceCertificateStatus) + )) +_sym_db.RegisterMessage(DeviceCertificateStatus) + +DeviceCertificateStatusList = _reflection.GeneratedProtocolMessageType('DeviceCertificateStatusList', (message.Message,), dict( + DESCRIPTOR = _DEVICECERTIFICATESTATUSLIST, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:DeviceCertificateStatusList) + )) +_sym_db.RegisterMessage(DeviceCertificateStatusList) + +EncryptedClientIdentification = _reflection.GeneratedProtocolMessageType('EncryptedClientIdentification', (message.Message,), dict( + DESCRIPTOR = _ENCRYPTEDCLIENTIDENTIFICATION, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:EncryptedClientIdentification) + )) +_sym_db.RegisterMessage(EncryptedClientIdentification) + +LicenseIdentification = _reflection.GeneratedProtocolMessageType('LicenseIdentification', (message.Message,), dict( + DESCRIPTOR = _LICENSEIDENTIFICATION, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:LicenseIdentification) + )) +_sym_db.RegisterMessage(LicenseIdentification) + +License = _reflection.GeneratedProtocolMessageType('License', (message.Message,), dict( + + Policy = _reflection.GeneratedProtocolMessageType('Policy', (message.Message,), dict( + DESCRIPTOR = _LICENSE_POLICY, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:License.Policy) + )) + , + + KeyContainer = _reflection.GeneratedProtocolMessageType('KeyContainer', (message.Message,), dict( + + OutputProtection = _reflection.GeneratedProtocolMessageType('OutputProtection', (message.Message,), dict( + DESCRIPTOR = _LICENSE_KEYCONTAINER_OUTPUTPROTECTION, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:License.KeyContainer.OutputProtection) + )) + , + + KeyControl = _reflection.GeneratedProtocolMessageType('KeyControl', (message.Message,), dict( + DESCRIPTOR = _LICENSE_KEYCONTAINER_KEYCONTROL, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:License.KeyContainer.KeyControl) + )) + , + + OperatorSessionKeyPermissions = _reflection.GeneratedProtocolMessageType('OperatorSessionKeyPermissions', (message.Message,), dict( + DESCRIPTOR = _LICENSE_KEYCONTAINER_OPERATORSESSIONKEYPERMISSIONS, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:License.KeyContainer.OperatorSessionKeyPermissions) + )) + , + + VideoResolutionConstraint = _reflection.GeneratedProtocolMessageType('VideoResolutionConstraint', (message.Message,), dict( + DESCRIPTOR = _LICENSE_KEYCONTAINER_VIDEORESOLUTIONCONSTRAINT, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:License.KeyContainer.VideoResolutionConstraint) + )) + , + DESCRIPTOR = _LICENSE_KEYCONTAINER, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:License.KeyContainer) + )) + , + DESCRIPTOR = _LICENSE, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:License) + )) +_sym_db.RegisterMessage(License) +_sym_db.RegisterMessage(License.Policy) +_sym_db.RegisterMessage(License.KeyContainer) +_sym_db.RegisterMessage(License.KeyContainer.OutputProtection) +_sym_db.RegisterMessage(License.KeyContainer.KeyControl) +_sym_db.RegisterMessage(License.KeyContainer.OperatorSessionKeyPermissions) +_sym_db.RegisterMessage(License.KeyContainer.VideoResolutionConstraint) + +LicenseError = _reflection.GeneratedProtocolMessageType('LicenseError', (message.Message,), dict( + DESCRIPTOR = _LICENSEERROR, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:LicenseError) + )) +_sym_db.RegisterMessage(LicenseError) + +LicenseRequest = _reflection.GeneratedProtocolMessageType('LicenseRequest', (message.Message,), dict( + + ContentIdentification = _reflection.GeneratedProtocolMessageType('ContentIdentification', (message.Message,), dict( + + CENC = _reflection.GeneratedProtocolMessageType('CENC', (message.Message,), dict( + DESCRIPTOR = _LICENSEREQUEST_CONTENTIDENTIFICATION_CENC, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:LicenseRequest.ContentIdentification.CENC) + )) + , + + WebM = _reflection.GeneratedProtocolMessageType('WebM', (message.Message,), dict( + DESCRIPTOR = _LICENSEREQUEST_CONTENTIDENTIFICATION_WEBM, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:LicenseRequest.ContentIdentification.WebM) + )) + , + + ExistingLicense = _reflection.GeneratedProtocolMessageType('ExistingLicense', (message.Message,), dict( + DESCRIPTOR = _LICENSEREQUEST_CONTENTIDENTIFICATION_EXISTINGLICENSE, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:LicenseRequest.ContentIdentification.ExistingLicense) + )) + , + DESCRIPTOR = _LICENSEREQUEST_CONTENTIDENTIFICATION, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:LicenseRequest.ContentIdentification) + )) + , + DESCRIPTOR = _LICENSEREQUEST, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:LicenseRequest) + )) +_sym_db.RegisterMessage(LicenseRequest) +_sym_db.RegisterMessage(LicenseRequest.ContentIdentification) +_sym_db.RegisterMessage(LicenseRequest.ContentIdentification.CENC) +_sym_db.RegisterMessage(LicenseRequest.ContentIdentification.WebM) +_sym_db.RegisterMessage(LicenseRequest.ContentIdentification.ExistingLicense) + +LicenseRequestRaw = _reflection.GeneratedProtocolMessageType('LicenseRequestRaw', (message.Message,), dict( + + ContentIdentification = _reflection.GeneratedProtocolMessageType('ContentIdentification', (message.Message,), dict( + + CENC = _reflection.GeneratedProtocolMessageType('CENC', (message.Message,), dict( + DESCRIPTOR = _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_CENC, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:LicenseRequestRaw.ContentIdentification.CENC) + )) + , + + WebM = _reflection.GeneratedProtocolMessageType('WebM', (message.Message,), dict( + DESCRIPTOR = _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_WEBM, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:LicenseRequestRaw.ContentIdentification.WebM) + )) + , + + ExistingLicense = _reflection.GeneratedProtocolMessageType('ExistingLicense', (message.Message,), dict( + DESCRIPTOR = _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_EXISTINGLICENSE, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:LicenseRequestRaw.ContentIdentification.ExistingLicense) + )) + , + DESCRIPTOR = _LICENSEREQUESTRAW_CONTENTIDENTIFICATION, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:LicenseRequestRaw.ContentIdentification) + )) + , + DESCRIPTOR = _LICENSEREQUESTRAW, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:LicenseRequestRaw) + )) +_sym_db.RegisterMessage(LicenseRequestRaw) +_sym_db.RegisterMessage(LicenseRequestRaw.ContentIdentification) +_sym_db.RegisterMessage(LicenseRequestRaw.ContentIdentification.CENC) +_sym_db.RegisterMessage(LicenseRequestRaw.ContentIdentification.WebM) +_sym_db.RegisterMessage(LicenseRequestRaw.ContentIdentification.ExistingLicense) + +ProvisionedDeviceInfo = _reflection.GeneratedProtocolMessageType('ProvisionedDeviceInfo', (message.Message,), dict( + DESCRIPTOR = _PROVISIONEDDEVICEINFO, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:ProvisionedDeviceInfo) + )) +_sym_db.RegisterMessage(ProvisionedDeviceInfo) + +ProvisioningOptions = _reflection.GeneratedProtocolMessageType('ProvisioningOptions', (message.Message,), dict( + DESCRIPTOR = _PROVISIONINGOPTIONS, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:ProvisioningOptions) + )) +_sym_db.RegisterMessage(ProvisioningOptions) + +ProvisioningRequest = _reflection.GeneratedProtocolMessageType('ProvisioningRequest', (message.Message,), dict( + DESCRIPTOR = _PROVISIONINGREQUEST, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:ProvisioningRequest) + )) +_sym_db.RegisterMessage(ProvisioningRequest) + +ProvisioningResponse = _reflection.GeneratedProtocolMessageType('ProvisioningResponse', (message.Message,), dict( + DESCRIPTOR = _PROVISIONINGRESPONSE, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:ProvisioningResponse) + )) +_sym_db.RegisterMessage(ProvisioningResponse) + +RemoteAttestation = _reflection.GeneratedProtocolMessageType('RemoteAttestation', (message.Message,), dict( + DESCRIPTOR = _REMOTEATTESTATION, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:RemoteAttestation) + )) +_sym_db.RegisterMessage(RemoteAttestation) + +SessionInit = _reflection.GeneratedProtocolMessageType('SessionInit', (message.Message,), dict( + DESCRIPTOR = _SESSIONINIT, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:SessionInit) + )) +_sym_db.RegisterMessage(SessionInit) + +SessionState = _reflection.GeneratedProtocolMessageType('SessionState', (message.Message,), dict( + DESCRIPTOR = _SESSIONSTATE, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:SessionState) + )) +_sym_db.RegisterMessage(SessionState) + +SignedCertificateStatusList = _reflection.GeneratedProtocolMessageType('SignedCertificateStatusList', (message.Message,), dict( + DESCRIPTOR = _SIGNEDCERTIFICATESTATUSLIST, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:SignedCertificateStatusList) + )) +_sym_db.RegisterMessage(SignedCertificateStatusList) + +SignedDeviceCertificate = _reflection.GeneratedProtocolMessageType('SignedDeviceCertificate', (message.Message,), dict( + DESCRIPTOR = _SIGNEDDEVICECERTIFICATE, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:SignedDeviceCertificate) + )) +_sym_db.RegisterMessage(SignedDeviceCertificate) + +SignedProvisioningMessage = _reflection.GeneratedProtocolMessageType('SignedProvisioningMessage', (message.Message,), dict( + DESCRIPTOR = _SIGNEDPROVISIONINGMESSAGE, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:SignedProvisioningMessage) + )) +_sym_db.RegisterMessage(SignedProvisioningMessage) + +SignedMessage = _reflection.GeneratedProtocolMessageType('SignedMessage', (message.Message,), dict( + DESCRIPTOR = _SIGNEDMESSAGE, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:SignedMessage) + )) +_sym_db.RegisterMessage(SignedMessage) + +WidevineCencHeader = _reflection.GeneratedProtocolMessageType('WidevineCencHeader', (message.Message,), dict( + DESCRIPTOR = _WIDEVINECENCHEADER, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:WidevineCencHeader) + )) +_sym_db.RegisterMessage(WidevineCencHeader) + +SignedLicenseRequest = _reflection.GeneratedProtocolMessageType('SignedLicenseRequest', (message.Message,), dict( + DESCRIPTOR = _SIGNEDLICENSEREQUEST, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:SignedLicenseRequest) + )) +_sym_db.RegisterMessage(SignedLicenseRequest) + +SignedLicenseRequestRaw = _reflection.GeneratedProtocolMessageType('SignedLicenseRequestRaw', (message.Message,), dict( + DESCRIPTOR = _SIGNEDLICENSEREQUESTRAW, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:SignedLicenseRequestRaw) + )) +_sym_db.RegisterMessage(SignedLicenseRequestRaw) + +SignedLicense = _reflection.GeneratedProtocolMessageType('SignedLicense', (message.Message,), dict( + DESCRIPTOR = _SIGNEDLICENSE, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:SignedLicense) + )) +_sym_db.RegisterMessage(SignedLicense) + +SignedServiceCertificate = _reflection.GeneratedProtocolMessageType('SignedServiceCertificate', (message.Message,), dict( + DESCRIPTOR = _SIGNEDSERVICECERTIFICATE, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:SignedServiceCertificate) + )) +_sym_db.RegisterMessage(SignedServiceCertificate) + +FileHashes = _reflection.GeneratedProtocolMessageType('FileHashes', (message.Message,), dict( + + Signature = _reflection.GeneratedProtocolMessageType('Signature', (message.Message,), dict( + DESCRIPTOR = _FILEHASHES_SIGNATURE, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:FileHashes.Signature) + )) + , + DESCRIPTOR = _FILEHASHES, + __module__ = 'widevine' + # @@protoc_insertion_point(class_scope:FileHashes) + )) +_sym_db.RegisterMessage(FileHashes) +_sym_db.RegisterMessage(FileHashes.Signature) + + +# @@protoc_insertion_point(module_scope) diff --git a/vinetrimmer/utils/widevine/session.py b/vinetrimmer/utils/widevine/session.py new file mode 100644 index 0000000..abf0710 --- /dev/null +++ b/vinetrimmer/utils/widevine/session.py @@ -0,0 +1,56 @@ +import base64 + +from construct import Container + +from vinetrimmer.utils.widevine.protos import widevine_pb2 as widevine +from vinetrimmer.vendor.pymp4.parser import Box + + +class Session: + def __init__(self, session_id, pssh, raw, offline): + if not session_id: + raise ValueError("A session_id must be provided...") + if not pssh: + raise ValueError("A PSSH Box must be provided...") + self.session_id = session_id + self.pssh = pssh + self.cenc_header = pssh + self.offline = offline + self.raw = raw + self.session_key = None + self.derived_keys = { + "enc": None, + "auth_1": None, + "auth_2": None + } + self.license_request = None + self.signed_license = None + self.signed_device_certificate = None + self.privacy_mode = False + self.keys = [] + + def __repr__(self): + return "{name}({items})".format( + name=self.__class__.__name__, + items=", ".join([f"{k}={repr(v)}" for k, v in self.__dict__.items()]) + ) + + @staticmethod + def parse_pssh_box(pssh): + """ + Parse a PSSH box's init_data into a WidevineCencHeader. + + Parameters: + pssh: A pssh box as str (base64), bytes, or a PSSH Box Container. + + Returns: + The init_data parsed as a WidevineCencHeader. + """ + # if isinstance(pssh, str): + # pssh = base64.b64decode(pssh) + # if not isinstance(pssh, Container): + # pssh = Box.parse(pssh) + # cenc_header = widevine.WidevineCencHeader() + # cenc_header.ParseFromString(pssh.init_data) + # return cenc_header + return False diff --git a/vinetrimmer/utils/widevine/vmp.py b/vinetrimmer/utils/widevine/vmp.py new file mode 100644 index 0000000..9fbd125 --- /dev/null +++ b/vinetrimmer/utils/widevine/vmp.py @@ -0,0 +1,113 @@ +from abc import ABC, abstractmethod + +try: + # this was tested to work with protobuf 3, but it's an internal API (any varint decoder might work) + from protobuf3.internal.decoder import _DecodeVarint as _di +except ImportError: + # this is generic and does not depend on pb internals, + # however it will decode "larger" possible numbers than pb decoder which has them fixed + def leb128_decode(buffer, pos, limit=64): + result = 0 + shift = 0 + while True: + b = buffer[pos] + pos += 1 + result |= ((b & 0x7F) << shift) + if not b & 0x80: + return result, pos + shift += 7 + if shift > limit: + raise Exception("integer too large, shift: {}".format(shift)) + + _di = leb128_decode + + +class FromFileMixin(ABC): + @abstractmethod + def __init__(self, buf): + ... + + @classmethod + def from_file(cls, filename): + """Load given a filename""" + with open(filename, "rb") as fd: + return fd.read() + + +# the signatures use a format internally similar to +# protobuf's encoding, but without wire types +class VariableReader(FromFileMixin): + """Protobuf-like encoding reader""" + + def __init__(self, buf): + self.buf = buf + self.pos = 0 + self.size = len(buf) + + def read_int(self): + """Read a variable length integer""" + # _DecodeVarint will take care of out of range errors + val, nextpos = _di(self.buf, self.pos) + self.pos = nextpos + return val + + def read_bytes_raw(self, size): + """Read size bytes""" + b = self.buf[self.pos:self.pos + size] + self.pos += size + return b + + def read_bytes(self): + """Read a bytes object""" + size = self.read_int() + return self.read_bytes_raw(size) + + def is_end(self): + return self.size == self.pos + + +class TaggedReader(VariableReader): + """Tagged reader, needed for implementing a Widevine signature reader""" + + def read_tag(self): + """Read a tagged buffer""" + return self.read_int(), self.read_bytes() + + def read_all_tags(self, max_tag=3): + tags = {} + while not self.is_end(): + tag, bytes_ = self.read_tag() + if tag > max_tag: + raise IndexError("tag out of bound: got {}, max {}".format(tag, max_tag)) + + tags[tag] = bytes_ + return tags + + +class WidevineSignatureReader(FromFileMixin): + """Parses a Widevine .sig signature file.""" + + SIGNER_TAG = 1 + SIGNATURE_TAG = 2 + ISMAINEXE_TAG = 3 + + def __init__(self, buf): + reader = TaggedReader(buf) + self.version = reader.read_int() + if self.version != 0: + raise Exception("Unsupported signature format version {}".format(self.version)) + self.tags = reader.read_all_tags() + + self.signer = self.tags[self.SIGNER_TAG] + self.signature = self.tags[self.SIGNATURE_TAG] + + extra = self.tags[self.ISMAINEXE_TAG] + if len(extra) != 1 or (extra[0] > 1): + raise Exception(f"Unexpected 'ismainexe' field value (not '\\x00' or '\\x01'), please check: {extra!r}") + + self.mainexe = bool(extra[0]) + + @classmethod + def get_tags(cls, filename): + """Return a dictionary of each tag in the signature file""" + return cls.from_file(filename).tags diff --git a/vinetrimmer/utils/xml.py b/vinetrimmer/utils/xml.py new file mode 100644 index 0000000..6aa8b51 --- /dev/null +++ b/vinetrimmer/utils/xml.py @@ -0,0 +1,24 @@ +from typing import Union + +from lxml import etree +from lxml.etree import ElementTree + + +def load_xml(xml: Union[str, bytes]) -> ElementTree: + """Safely parse XML data to an ElementTree, without namespaces in tags.""" + if not isinstance(xml, bytes): + xml = xml.encode("utf8") + root = etree.fromstring(xml) + for elem in root.getiterator(): + if not hasattr(elem.tag, "find"): + # e.g. comment elements + continue + elem.tag = etree.QName(elem).localname + for name, value in elem.attrib.items(): + local_name = etree.QName(name).localname + if local_name == name: + continue + del elem.attrib[name] + elem.attrib[local_name] = value + etree.cleanup_namespaces(root) + return root \ No newline at end of file diff --git a/vinetrimmer/utils/xmltodict.py b/vinetrimmer/utils/xmltodict.py new file mode 100644 index 0000000..098f627 --- /dev/null +++ b/vinetrimmer/utils/xmltodict.py @@ -0,0 +1,522 @@ +#!/usr/bin/env python +"Makes working with XML feel like you are working with JSON" + +from xml.parsers import expat +from xml.sax.saxutils import XMLGenerator +from xml.sax.xmlreader import AttributesImpl +from io import StringIO + +_dict = dict +import platform +if tuple(map(int, platform.python_version_tuple()[:2])) < (3, 7): + from collections import OrderedDict as _dict + +from inspect import isgenerator + +__author__ = 'Martin Blech' +__version__ = "0.14.2" +__license__ = 'MIT' + + +class ParsingInterrupted(Exception): + pass + + +class _DictSAXHandler: + def __init__(self, + item_depth=0, + item_callback=lambda *args: True, + xml_attribs=True, + attr_prefix='@', + cdata_key='#text', + force_cdata=False, + cdata_separator='', + postprocessor=None, + dict_constructor=_dict, + strip_whitespace=True, + namespace_separator=':', + namespaces=None, + force_list=None, + comment_key='#comment'): + self.path = [] + self.stack = [] + self.data = [] + self.item = None + self.item_depth = item_depth + self.xml_attribs = xml_attribs + self.item_callback = item_callback + self.attr_prefix = attr_prefix + self.cdata_key = cdata_key + self.force_cdata = force_cdata + self.cdata_separator = cdata_separator + self.postprocessor = postprocessor + self.dict_constructor = dict_constructor + self.strip_whitespace = strip_whitespace + self.namespace_separator = namespace_separator + self.namespaces = namespaces + self.namespace_declarations = dict_constructor() + self.force_list = force_list + self.comment_key = comment_key + + def _build_name(self, full_name): + if self.namespaces is None: + return full_name + i = full_name.rfind(self.namespace_separator) + if i == -1: + return full_name + namespace, name = full_name[:i], full_name[i+1:] + try: + short_namespace = self.namespaces[namespace] + except KeyError: + short_namespace = namespace + if not short_namespace: + return name + else: + return self.namespace_separator.join((short_namespace, name)) + + def _attrs_to_dict(self, attrs): + if isinstance(attrs, dict): + return attrs + return self.dict_constructor(zip(attrs[0::2], attrs[1::2])) + + def startNamespaceDecl(self, prefix, uri): + self.namespace_declarations[prefix or ''] = uri + + def startElement(self, full_name, attrs): + name = self._build_name(full_name) + attrs = self._attrs_to_dict(attrs) + if attrs and self.namespace_declarations: + attrs['xmlns'] = self.namespace_declarations + self.namespace_declarations = self.dict_constructor() + self.path.append((name, attrs or None)) + if len(self.path) >= self.item_depth: + self.stack.append((self.item, self.data)) + if self.xml_attribs: + attr_entries = [] + for key, value in attrs.items(): + key = self.attr_prefix+self._build_name(key) + if self.postprocessor: + entry = self.postprocessor(self.path, key, value) + else: + entry = (key, value) + if entry: + attr_entries.append(entry) + attrs = self.dict_constructor(attr_entries) + else: + attrs = None + self.item = attrs or None + self.data = [] + + def endElement(self, full_name): + name = self._build_name(full_name) + if len(self.path) == self.item_depth: + item = self.item + if item is None: + item = (None if not self.data + else self.cdata_separator.join(self.data)) + + should_continue = self.item_callback(self.path, item) + if not should_continue: + raise ParsingInterrupted + if self.stack: + data = (None if not self.data + else self.cdata_separator.join(self.data)) + item = self.item + self.item, self.data = self.stack.pop() + if self.strip_whitespace and data: + data = data.strip() or None + if data and self.force_cdata and item is None: + item = self.dict_constructor() + if item is not None: + if data: + self.push_data(item, self.cdata_key, data) + self.item = self.push_data(self.item, name, item) + else: + self.item = self.push_data(self.item, name, data) + else: + self.item = None + self.data = [] + self.path.pop() + + def characters(self, data): + if not self.data: + self.data = [data] + else: + self.data.append(data) + + def comments(self, data): + if self.strip_whitespace: + data = data.strip() + self.item = self.push_data(self.item, self.comment_key, data) + + def push_data(self, item, key, data): + if self.postprocessor is not None: + result = self.postprocessor(self.path, key, data) + if result is None: + return item + key, data = result + if item is None: + item = self.dict_constructor() + try: + value = item[key] + if isinstance(value, list): + value.append(data) + else: + item[key] = [value, data] + except KeyError: + if self._should_force_list(key, data): + item[key] = [data] + else: + item[key] = data + return item + + def _should_force_list(self, key, value): + if not self.force_list: + return False + if isinstance(self.force_list, bool): + return self.force_list + try: + return key in self.force_list + except TypeError: + return self.force_list(self.path[:-1], key, value) + + +def parse(xml_input, encoding=None, expat=expat, process_namespaces=False, + namespace_separator=':', disable_entities=True, process_comments=False, **kwargs): + """Parse the given XML input and convert it into a dictionary. + + `xml_input` can either be a `string`, a file-like object, or a generator of strings. + + If `xml_attribs` is `True`, element attributes are put in the dictionary + among regular child elements, using `@` as a prefix to avoid collisions. If + set to `False`, they are just ignored. + + Simple example:: + + >>> import xmltodict + >>> doc = xmltodict.parse(\"\"\" + ... <a prop="x"> + ... <b>1</b> + ... <b>2</b> + ... </a> + ... \"\"\") + >>> doc['a']['@prop'] + u'x' + >>> doc['a']['b'] + [u'1', u'2'] + + If `item_depth` is `0`, the function returns a dictionary for the root + element (default behavior). Otherwise, it calls `item_callback` every time + an item at the specified depth is found and returns `None` in the end + (streaming mode). + + The callback function receives two parameters: the `path` from the document + root to the item (name-attribs pairs), and the `item` (dict). If the + callback's return value is false-ish, parsing will be stopped with the + :class:`ParsingInterrupted` exception. + + Streaming example:: + + >>> def handle(path, item): + ... print('path:%s item:%s' % (path, item)) + ... return True + ... + >>> xmltodict.parse(\"\"\" + ... <a prop="x"> + ... <b>1</b> + ... <b>2</b> + ... </a>\"\"\", item_depth=2, item_callback=handle) + path:[(u'a', {u'prop': u'x'}), (u'b', None)] item:1 + path:[(u'a', {u'prop': u'x'}), (u'b', None)] item:2 + + The optional argument `postprocessor` is a function that takes `path`, + `key` and `value` as positional arguments and returns a new `(key, value)` + pair where both `key` and `value` may have changed. Usage example:: + + >>> def postprocessor(path, key, value): + ... try: + ... return key + ':int', int(value) + ... except (ValueError, TypeError): + ... return key, value + >>> xmltodict.parse('<a><b>1</b><b>2</b><b>x</b></a>', + ... postprocessor=postprocessor) + {'a': {'b:int': [1, 2], 'b': 'x'}} + + You can pass an alternate version of `expat` (such as `defusedexpat`) by + using the `expat` parameter. E.g: + + >>> import defusedexpat + >>> xmltodict.parse('<a>hello</a>', expat=defusedexpat.pyexpat) + {'a': 'hello'} + + You can use the force_list argument to force lists to be created even + when there is only a single child of a given level of hierarchy. The + force_list argument is a tuple of keys. If the key for a given level + of hierarchy is in the force_list argument, that level of hierarchy + will have a list as a child (even if there is only one sub-element). + The index_keys operation takes precedence over this. This is applied + after any user-supplied postprocessor has already run. + + For example, given this input: + <servers> + <server> + <name>host1</name> + <os>Linux</os> + <interfaces> + <interface> + <name>em0</name> + <ip_address>10.0.0.1</ip_address> + </interface> + </interfaces> + </server> + </servers> + + If called with force_list=('interface',), it will produce + this dictionary: + {'servers': + {'server': + {'name': 'host1', + 'os': 'Linux'}, + 'interfaces': + {'interface': + [ {'name': 'em0', 'ip_address': '10.0.0.1' } ] } } } + + `force_list` can also be a callable that receives `path`, `key` and + `value`. This is helpful in cases where the logic that decides whether + a list should be forced is more complex. + + + If `process_comment` is `True` then comment will be added with comment_key + (default=`'#comment'`) to then tag which contains comment + + For example, given this input: + <a> + <b> + <!-- b comment --> + <c> + <!-- c comment --> + 1 + </c> + <d>2</d> + </b> + </a> + + If called with process_comment=True, it will produce + this dictionary: + 'a': { + 'b': { + '#comment': 'b comment', + 'c': { + + '#comment': 'c comment', + '#text': '1', + }, + 'd': '2', + }, + } + """ + handler = _DictSAXHandler(namespace_separator=namespace_separator, + **kwargs) + if isinstance(xml_input, str): + encoding = encoding or 'utf-8' + xml_input = xml_input.encode(encoding) + if not process_namespaces: + namespace_separator = None + parser = expat.ParserCreate( + encoding, + namespace_separator + ) + try: + parser.ordered_attributes = True + except AttributeError: + # Jython's expat does not support ordered_attributes + pass + parser.StartNamespaceDeclHandler = handler.startNamespaceDecl + parser.StartElementHandler = handler.startElement + parser.EndElementHandler = handler.endElement + parser.CharacterDataHandler = handler.characters + if process_comments: + parser.CommentHandler = handler.comments + parser.buffer_text = True + if disable_entities: + try: + # Attempt to disable DTD in Jython's expat parser (Xerces-J). + feature = "http://apache.org/xml/features/disallow-doctype-decl" + parser._reader.setFeature(feature, True) + except AttributeError: + # For CPython / expat parser. + # Anything not handled ends up here and entities aren't expanded. + parser.DefaultHandler = lambda x: None + # Expects an integer return; zero means failure -> expat.ExpatError. + parser.ExternalEntityRefHandler = lambda *x: 1 + if hasattr(xml_input, 'read'): + parser.ParseFile(xml_input) + elif isgenerator(xml_input): + for chunk in xml_input: + parser.Parse(chunk, False) + parser.Parse(b'', True) + else: + parser.Parse(xml_input, True) + return handler.item + + +def _process_namespace(name, namespaces, ns_sep=':', attr_prefix='@'): + if not namespaces: + return name + try: + ns, name = name.rsplit(ns_sep, 1) + except ValueError: + pass + else: + ns_res = namespaces.get(ns.strip(attr_prefix)) + name = '{}{}{}{}'.format( + attr_prefix if ns.startswith(attr_prefix) else '', + ns_res, ns_sep, name) if ns_res else name + return name + + +def _emit(key, value, content_handler, + attr_prefix='@', + cdata_key='#text', + depth=0, + preprocessor=None, + pretty=False, + newl='\n', + indent='\t', + namespace_separator=':', + namespaces=None, + full_document=True, + expand_iter=None): + key = _process_namespace(key, namespaces, namespace_separator, attr_prefix) + if preprocessor is not None: + result = preprocessor(key, value) + if result is None: + return + key, value = result + if not hasattr(value, '__iter__') or isinstance(value, (str, dict)): + value = [value] + for index, v in enumerate(value): + if full_document and depth == 0 and index > 0: + raise ValueError('document with multiple roots') + if v is None: + v = _dict() + elif isinstance(v, bool): + v = 'true' if v else 'false' + elif not isinstance(v, (dict, str)): + if expand_iter and hasattr(v, '__iter__'): + v = _dict(((expand_iter, v),)) + else: + v = str(v) + if isinstance(v, str): + v = _dict(((cdata_key, v),)) + cdata = None + attrs = _dict() + children = [] + for ik, iv in v.items(): + if ik == cdata_key: + cdata = iv + continue + if ik.startswith(attr_prefix): + ik = _process_namespace(ik, namespaces, namespace_separator, + attr_prefix) + if ik == '@xmlns' and isinstance(iv, dict): + for k, v in iv.items(): + attr = 'xmlns{}'.format(f':{k}' if k else '') + attrs[attr] = str(v) + continue + if not isinstance(iv, str): + iv = str(iv) + attrs[ik[len(attr_prefix):]] = iv + continue + children.append((ik, iv)) + if isinstance(indent, int): + indent = ' ' * indent + if pretty: + content_handler.ignorableWhitespace(depth * indent) + content_handler.startElement(key, AttributesImpl(attrs)) + if pretty and children: + content_handler.ignorableWhitespace(newl) + for child_key, child_value in children: + _emit(child_key, child_value, content_handler, + attr_prefix, cdata_key, depth+1, preprocessor, + pretty, newl, indent, namespaces=namespaces, + namespace_separator=namespace_separator, + expand_iter=expand_iter) + if cdata is not None: + content_handler.characters(cdata) + if pretty and children: + content_handler.ignorableWhitespace(depth * indent) + content_handler.endElement(key) + if pretty and depth: + content_handler.ignorableWhitespace(newl) + + +def unparse(input_dict, output=None, encoding='utf-8', full_document=True, + short_empty_elements=False, + **kwargs): + """Emit an XML document for the given `input_dict` (reverse of `parse`). + + The resulting XML document is returned as a string, but if `output` (a + file-like object) is specified, it is written there instead. + + Dictionary keys prefixed with `attr_prefix` (default=`'@'`) are interpreted + as XML node attributes, whereas keys equal to `cdata_key` + (default=`'#text'`) are treated as character data. + + The `pretty` parameter (default=`False`) enables pretty-printing. In this + mode, lines are terminated with `'\n'` and indented with `'\t'`, but this + can be customized with the `newl` and `indent` parameters. + + """ + if full_document and len(input_dict) != 1: + raise ValueError('Document must have exactly one root.') + must_return = False + if output is None: + output = StringIO() + must_return = True + if short_empty_elements: + content_handler = XMLGenerator(output, encoding, True) + else: + content_handler = XMLGenerator(output, encoding) + if full_document: + content_handler.startDocument() + for key, value in input_dict.items(): + _emit(key, value, content_handler, full_document=full_document, + **kwargs) + if full_document: + content_handler.endDocument() + if must_return: + value = output.getvalue() + try: # pragma no cover + value = value.decode(encoding) + except AttributeError: # pragma no cover + pass + return value + + +if __name__ == '__main__': # pragma: no cover + import sys + import marshal + try: + stdin = sys.stdin.buffer + stdout = sys.stdout.buffer + except AttributeError: + stdin = sys.stdin + stdout = sys.stdout + + (item_depth,) = sys.argv[1:] + item_depth = int(item_depth) + + def handle_item(path, item): + marshal.dump((path, item), stdout) + return True + + try: + root = parse(stdin, + item_depth=item_depth, + item_callback=handle_item, + dict_constructor=dict) + if item_depth == 0: + handle_item([], root) + except KeyboardInterrupt: + pass diff --git a/vinetrimmer/vendor/BamSDK/__init__.py b/vinetrimmer/vendor/BamSDK/__init__.py new file mode 100644 index 0000000..83a017b --- /dev/null +++ b/vinetrimmer/vendor/BamSDK/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from vinetrimmer.vendor.BamSDK.bamsdk import BamSdk diff --git a/vinetrimmer/vendor/BamSDK/bamsdk.py b/vinetrimmer/vendor/BamSDK/bamsdk.py new file mode 100644 index 0000000..46df42e --- /dev/null +++ b/vinetrimmer/vendor/BamSDK/bamsdk.py @@ -0,0 +1,28 @@ +import requests + +from vinetrimmer.vendor.BamSDK.services.account import account +from vinetrimmer.vendor.BamSDK.services.bamIdentity import bamIdentity +from vinetrimmer.vendor.BamSDK.services.content import content +from vinetrimmer.vendor.BamSDK.services.device import device +from vinetrimmer.vendor.BamSDK.services.drm import drm +from vinetrimmer.vendor.BamSDK.services.media import media +from vinetrimmer.vendor.BamSDK.services.session import session +from vinetrimmer.vendor.BamSDK.services.token import token + + +class BamSdk: + def __init__(self, endpoint, session_=None): + self._session = session_ or requests.Session() + + self.config = self._session.get(endpoint).json() + self.application = self.config["application"] + self.commonHeaders = self.config["commonHeaders"] + + self.account = account(self.config["services"]["account"], self._session) + self.bamIdentity = bamIdentity(self.config["services"]["bamIdentity"], self._session) + self.content = content(self.config["services"]["content"], self._session) + self.device = device(self.config["services"]["device"], self._session) + self.drm = drm(self.config["services"]["drm"], self._session) + self.media = media(self.config["services"]["media"], self._session) + self.session = session(self.config["services"]["session"], self._session) + self.token = token(self.config["services"]["token"], self._session) diff --git a/vinetrimmer/vendor/BamSDK/services/__init__.py b/vinetrimmer/vendor/BamSDK/services/__init__.py new file mode 100644 index 0000000..0bdd32f --- /dev/null +++ b/vinetrimmer/vendor/BamSDK/services/__init__.py @@ -0,0 +1,37 @@ +import requests + + +class Service: + def __init__(self, cfg, session=None): + self.session = session or requests.Session() + self.client = Client(cfg.get("client") or {}) + self.disabled = cfg.get("disabled") + self.extras = cfg.get("extras") + + +class Client: + def __init__(self, data): + self.baseUrl = data.get("baseUrl") + self.endpoints = {k: Endpoint(v) for k, v in (data.get("endpoints") or {}).items()} + self.extras = data.get("extras") or {} + + +class Endpoint: + def __init__(self, data): + self.headers = data.get("headers") or {} + self.href = data["href"] + self.method = data.get("method") or "GET" + self.templated = data.get("templated") or False + self.timeout = data.get("timeout") or 15 + self.ttl = data.get("ttl") or 0 + + # noinspection PyPep8Naming + def get_headers(self, accessToken=None, apiKey=None): + token = None + if accessToken: + token = {"accessToken": accessToken} + elif apiKey: + token = {"apiKey": apiKey} + if token: + self.headers.update({"Authorization": self.headers["Authorization"].format(**token)}) + return self.headers diff --git a/vinetrimmer/vendor/BamSDK/services/account.py b/vinetrimmer/vendor/BamSDK/services/account.py new file mode 100644 index 0000000..be348d7 --- /dev/null +++ b/vinetrimmer/vendor/BamSDK/services/account.py @@ -0,0 +1,37 @@ +from requests import Request + +from vinetrimmer.vendor.BamSDK.services import Service + + +# noinspection PyPep8Naming +class account(Service): + def createAccountGrant(self, json, access_token): + endpoint = self.client.endpoints["createAccountGrant"] + req = Request( + method=endpoint.method, + url=endpoint.href, + headers=endpoint.get_headers(accessToken=access_token), + json=json + ).prepare() + res = self.session.send(req) + return res.json() + + def getUserProfiles(self, access_token): + endpoint = self.client.endpoints["getUserProfiles"] + req = Request( + method=endpoint.method, + url=endpoint.href, + headers=endpoint.get_headers(accessToken=access_token) + ).prepare() + res = self.session.send(req) + return res.json() + + def setActiveUserProfile(self, profile_id, access_token): + endpoint = self.client.endpoints["setActiveUserProfile"] + req = Request( + method=endpoint.method, + url=endpoint.href.format(profileId=profile_id), + headers=endpoint.get_headers(accessToken=access_token) + ).prepare() + res = self.session.send(req) + return res.json() diff --git a/vinetrimmer/vendor/BamSDK/services/bamIdentity.py b/vinetrimmer/vendor/BamSDK/services/bamIdentity.py new file mode 100644 index 0000000..43697b1 --- /dev/null +++ b/vinetrimmer/vendor/BamSDK/services/bamIdentity.py @@ -0,0 +1,20 @@ +from requests import Request + +from vinetrimmer.vendor.BamSDK.services import Service + + +# noinspection PyPep8Naming +class bamIdentity(Service): + def identityLogin(self, email, password, access_token): + endpoint = self.client.endpoints["identityLogin"] + req = Request( + method=endpoint.method, + url=endpoint.href, + headers=endpoint.get_headers(accessToken=access_token), + json={ + "email": email, + "password": password + } + ).prepare() + res = self.session.send(req) + return res.json() diff --git a/vinetrimmer/vendor/BamSDK/services/content.py b/vinetrimmer/vendor/BamSDK/services/content.py new file mode 100644 index 0000000..ba45fc8 --- /dev/null +++ b/vinetrimmer/vendor/BamSDK/services/content.py @@ -0,0 +1,36 @@ +from requests import Request + +from vinetrimmer.utils.BamSDK.services import Service + + +# noinspection PyPep8Naming +class content(Service): + def getDmcEpisodes(self, region, season_id, page, access_token): + endpoint = self.client.endpoints["getDmcEpisodes"] + req = Request( + method=endpoint.method, + url=f"https://star.content.edge.bamgrid.com/svc/content/DmcEpisodes/version/3.3/region/{region}/audience/k-false,l-true/maturity/1899/language/en/seasonId/{season_id}/pageSize/15/page/{page}", # noqa: E501 + headers=endpoint.get_headers(accessToken=access_token) + ).prepare() + res = self.session.send(req) + return res.json() + + def getDmcSeriesBundle(self, region, media_id, access_token): + endpoint = self.client.endpoints["getDmcSeriesBundle"] + req = Request( + method=endpoint.method, + url=f"https://star.content.edge.bamgrid.com/svc/content/DmcSeriesBundle/version/3.3/region/{region}/audience/k-false,l-true/maturity/1899/language/en/encodedSeriesId/{media_id}", # noqa: E501 + headers=endpoint.get_headers(accessToken=access_token) + ).prepare() + res = self.session.send(req) + return res.json() + + def getDmcVideoBundle(self, region, media_id, access_token): + endpoint = self.client.endpoints["getDmcVideoBundle"] + req = Request( + method=endpoint.method, + url=f"https://star.content.edge.bamgrid.com/svc/content/DmcVideoBundle/version/3.3/region/{region}/audience/k-false,l-true/maturity/1899/language/en/encodedFamilyId/{media_id}", # noqa: E501 + headers=endpoint.get_headers(accessToken=access_token) + ).prepare() + res = self.session.send(req) + return res.json() diff --git a/vinetrimmer/vendor/BamSDK/services/device.py b/vinetrimmer/vendor/BamSDK/services/device.py new file mode 100644 index 0000000..b20fca2 --- /dev/null +++ b/vinetrimmer/vendor/BamSDK/services/device.py @@ -0,0 +1,24 @@ +from json import JSONDecodeError + +from requests import Request + +from vinetrimmer.vendor.BamSDK.services import Service + + +# noinspection PyPep8Naming +class device(Service): + + def createDeviceGrant(self, json, api_key): + endpoint = self.client.endpoints["createDeviceGrant"] + req = Request( + method=endpoint.method, + url=endpoint.href, + headers=endpoint.get_headers(apiKey=api_key), + json=json + ).prepare() + res = self.session.send(req) + try: + data = res.json() + except JSONDecodeError: + raise Exception(f"An unexpected response occurred for bamsdk.createDeviceGrant: {res.text}") + return data diff --git a/vinetrimmer/vendor/BamSDK/services/drm.py b/vinetrimmer/vendor/BamSDK/services/drm.py new file mode 100644 index 0000000..21d231e --- /dev/null +++ b/vinetrimmer/vendor/BamSDK/services/drm.py @@ -0,0 +1,34 @@ +import json + +from requests import Request + +from vinetrimmer.vendor.BamSDK.services import Service + + +# noinspection PyPep8Naming +class drm(Service): + def widevineCertificate(self): + endpoint = self.client.endpoints["widevineCertificate"] + req = Request( + method=endpoint.method, + url=endpoint.href, + headers=endpoint.headers + ).prepare() + res = self.session.send(req) + return res.content + + def widevineLicense(self, licence, access_token): + endpoint = self.client.endpoints["widevineLicense"] + req = Request( + method=endpoint.method, + url=endpoint.href, + headers=endpoint.get_headers(accessToken=access_token), + data=licence + ).prepare() + res = self.session.send(req) + try: + # if it's json content, then an error occurred + res = json.loads(res.text) + raise Exception(f"Failed to obtain license: {res}") + except json.JSONDecodeError: + return res.content diff --git a/vinetrimmer/vendor/BamSDK/services/media.py b/vinetrimmer/vendor/BamSDK/services/media.py new file mode 100644 index 0000000..ae49905 --- /dev/null +++ b/vinetrimmer/vendor/BamSDK/services/media.py @@ -0,0 +1,24 @@ +from requests import Request + +from vinetrimmer.vendor.BamSDK.services import Service + + +# noinspection PyPep8Naming +class media(Service): + def __init__(self, cfg, session=None): + super().__init__(cfg, session) + self.uhd_allowed = self.extras["isUhdAllowed"] + self.default_scenario = self.extras["playbackScenarioDefault"] + self.scenarios = self.extras["playbackScenarios"] + self.restricted_scenario = self.extras["restrictedPlaybackScenario"] + self.security_requirements = self.extras["securityCheckRequirements"] + + def mediaPayload(self, media_id, scenario, access_token): + endpoint = self.client.endpoints["mediaPayload"] + req = Request( + method=endpoint.method, + url=f"{self.client.baseUrl}/media/{media_id}/scenarios/{scenario}", + headers=endpoint.get_headers(accessToken=access_token) + ).prepare() + res = self.session.send(req) + return res.json() diff --git a/vinetrimmer/vendor/BamSDK/services/session.py b/vinetrimmer/vendor/BamSDK/services/session.py new file mode 100644 index 0000000..12682e5 --- /dev/null +++ b/vinetrimmer/vendor/BamSDK/services/session.py @@ -0,0 +1,26 @@ +from requests import Request + +from vinetrimmer.vendor.BamSDK.services import Service + + +# noinspection PyPep8Naming +class session(Service): + def getInfo(self, access_token): + endpoint = self.client.endpoints["getInfo"] + req = Request( + method=endpoint.method, + url=endpoint.href, + headers=endpoint.get_headers(accessToken=access_token) + ).prepare() + res = self.session.send(req) + return res.json() + + def getLocation(self, access_token): + endpoint = self.client.endpoints["getLocation"] + req = Request( + method=endpoint.method, + url=endpoint.href, + headers=endpoint.get_headers(accessToken=access_token) + ).prepare() + res = self.session.send(req) + return res.json() diff --git a/vinetrimmer/vendor/BamSDK/services/token.py b/vinetrimmer/vendor/BamSDK/services/token.py new file mode 100644 index 0000000..c629136 --- /dev/null +++ b/vinetrimmer/vendor/BamSDK/services/token.py @@ -0,0 +1,21 @@ +from requests import Request + +from vinetrimmer.vendor.BamSDK.services import Service + + +# noinspection PyPep8Naming +class token(Service): + def __init__(self, cfg, session=None): + super().__init__(cfg, session) + self.subject_tokens = self.extras["subjectTokenTypes"] + + def exchange(self, data, api_key): + endpoint = self.client.endpoints["exchange"] + req = Request( + method=endpoint.method, + url=endpoint.href, + headers=endpoint.get_headers(apiKey=api_key), + data=data + ).prepare() + res = self.session.send(req) + return res.json() diff --git a/vinetrimmer/vendor/h2/__init__.py b/vinetrimmer/vendor/h2/__init__.py new file mode 100644 index 0000000..00f22d9 --- /dev/null +++ b/vinetrimmer/vendor/h2/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +""" +h2 +~~ + +A HTTP/2 implementation. +""" +__version__ = '2.6.2' diff --git a/vinetrimmer/vendor/h2/config.py b/vinetrimmer/vendor/h2/config.py new file mode 100644 index 0000000..f3c6df7 --- /dev/null +++ b/vinetrimmer/vendor/h2/config.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +""" +h2/config +~~~~~~~~~ + +Objects for controlling the configuration of the HTTP/2 stack. +""" + + +class _BooleanConfigOption(object): + """ + Descriptor for handling a boolean config option. This will block + attempts to set boolean config options to non-bools. + """ + def __init__(self, name): + self.name = name + self.attr_name = '_%s' % self.name + + def __get__(self, instance, owner): + return getattr(instance, self.attr_name) + + def __set__(self, instance, value): + if not isinstance(value, bool): + raise ValueError("%s must be a bool" % self.name) + setattr(instance, self.attr_name, value) + + +class DummyLogger(object): + """ + An Logger object that does not actual logging, hence a DummyLogger. + + For the class the log operation is merely a no-op. The intent is to avoid + conditionals being sprinkled throughout the hyper-h2 code for calls to + logging functions when no logger is passed into the corresponding object. + """ + def __init__(self, *vargs): + pass + + def debug(self, *vargs, **kwargs): + """ + No-op logging. Only level needed for now. + """ + pass + + +class H2Configuration(object): + """ + An object that controls the way a single HTTP/2 connection behaves. + + This object allows the users to customize behaviour. In particular, it + allows users to enable or disable optional features, or to otherwise handle + various unusual behaviours. + + This object has very little behaviour of its own: it mostly just ensures + that configuration is self-consistent. + + :param client_side: Whether this object is to be used on the client side of + a connection, or on the server side. Affects the logic used by the + state machine, the default settings values, the allowable stream IDs, + and several other properties. Defaults to ``True``. + :type client_side: ``bool`` + + :param header_encoding: Controls whether the headers emitted by this object + in events are transparently decoded to ``unicode`` strings, and what + encoding is used to do that decoding. For historical reasons, this + defaults to ``'utf-8'``. To prevent the decoding of headers (that is, + to force them to be returned as bytestrings), this can be set to + ``False`` or the empty string. + :type header_encoding: ``str``, ``False``, or ``None`` + + :param validate_outbound_headers: Controls whether the headers emitted + by this object are validated against the rules in RFC 7540. + Disabling this setting will cause outbound header validation to + be skipped, and allow the object to emit headers that may be illegal + according to RFC 7540. Defaults to ``True``. + :type validate_outbound_headers: ``bool`` + + :param normalize_outbound_headers: Controls whether the headers emitted + by this object are normalized before sending. Disabling this setting + will cause outbound header normalization to be skipped, and allow + the object to emit headers that may be illegal according to + RFC 7540. Defaults to ``True``. + :type normalize_outbound_headers: ``bool`` + + :param validate_inbound_headers: Controls whether the headers received + by this object are validated against the rules in RFC 7540. + Disabling this setting will cause inbound header validation to + be skipped, and allow the object to receive headers that may be illegal + according to RFC 7540. Defaults to ``True``. + :type validate_inbound_headers: ``bool`` + + :param logger: A logger that conforms to the requirements for this module, + those being no I/O and no context switches, which is needed in order + to run in asynchronous operation. + + .. versionadded:: 2.6.0 + + :type logger: ``logging.Logger`` + """ + client_side = _BooleanConfigOption('client_side') + validate_outbound_headers = _BooleanConfigOption( + 'validate_outbound_headers' + ) + normalize_outbound_headers = _BooleanConfigOption( + 'normalize_outbound_headers' + ) + validate_inbound_headers = _BooleanConfigOption( + 'validate_inbound_headers' + ) + + def __init__(self, + client_side=True, + header_encoding='utf-8', + validate_outbound_headers=True, + normalize_outbound_headers=True, + validate_inbound_headers=True, + logger=None): + self.client_side = client_side + self.header_encoding = header_encoding + self.validate_outbound_headers = validate_outbound_headers + self.normalize_outbound_headers = normalize_outbound_headers + self.validate_inbound_headers = validate_inbound_headers + self.logger = logger or DummyLogger(__name__) + + @property + def header_encoding(self): + """ + Controls whether the headers emitted by this object in events are + transparently decoded to ``unicode`` strings, and what encoding is used + to do that decoding. For historical reasons, this defaults to + ``'utf-8'``. To prevent the decoding of headers (that is, to force them + to be returned as bytestrings), this can be set to ``False`` or the + empty string. + """ + return self._header_encoding + + @header_encoding.setter + def header_encoding(self, value): + """ + Enforces constraints on the value of header encoding. + """ + if not isinstance(value, (bool, str, type(None))): + raise ValueError("header_encoding must be bool, string, or None") + if value is True: + raise ValueError("header_encoding cannot be True") + self._header_encoding = value diff --git a/vinetrimmer/vendor/h2/connection.py b/vinetrimmer/vendor/h2/connection.py new file mode 100644 index 0000000..f839e16 --- /dev/null +++ b/vinetrimmer/vendor/h2/connection.py @@ -0,0 +1,2074 @@ +# -*- coding: utf-8 -*- +""" +h2/connection +~~~~~~~~~~~~~ + +An implementation of a HTTP/2 connection. +""" +import base64 + +from enum import Enum, IntEnum + +from vinetrimmer.vendor.hyperframe.exceptions import InvalidPaddingError +from vinetrimmer.vendor.hyperframe.frame import ( + GoAwayFrame, WindowUpdateFrame, HeadersFrame, DataFrame, PingFrame, + PushPromiseFrame, SettingsFrame, RstStreamFrame, PriorityFrame, + ContinuationFrame, AltSvcFrame +) +from vinetrimmer.vendor.hpack.hpack import Encoder, Decoder +from vinetrimmer.vendor.hpack.exceptions import HPACKError + +from .config import H2Configuration +from .errors import ErrorCodes, _error_code_from_int +from .events import ( + WindowUpdated, RemoteSettingsChanged, PingAcknowledged, + SettingsAcknowledged, ConnectionTerminated, PriorityUpdated, + AlternativeServiceAvailable, +) +from .exceptions import ( + ProtocolError, NoSuchStreamError, FlowControlError, FrameTooLargeError, + TooManyStreamsError, StreamClosedError, StreamIDTooLowError, + NoAvailableStreamIDError, RFC1122Error, DenialOfServiceError +) +from .frame_buffer import FrameBuffer +from .settings import Settings, SettingCodes +from .stream import H2Stream, StreamClosedBy +from .utilities import guard_increment_window +from .windows import WindowManager + +try: + from vinetrimmer.vendor.hpack.exceptions import OversizedHeaderListError +except ImportError: # Platform-specific: HPACK < 2.3.0 + # If the exception doesn't exist, it cannot possibly be thrown. Define a + # placeholder name, but don't otherwise worry about it. + class OversizedHeaderListError(Exception): + pass + + +try: + from vinetrimmer.vendor.hyperframe.frame import ExtensionFrame +except ImportError: # Platform-specific: Hyperframe < 5.0.0 + # If the frame doesn't exist, that's just fine: we'll define it ourselves + # and the method will just never be called. + class ExtensionFrame(object): + pass + + +class ConnectionState(Enum): + IDLE = 0 + CLIENT_OPEN = 1 + SERVER_OPEN = 2 + CLOSED = 3 + + +class ConnectionInputs(Enum): + SEND_HEADERS = 0 + SEND_PUSH_PROMISE = 1 + SEND_DATA = 2 + SEND_GOAWAY = 3 + SEND_WINDOW_UPDATE = 4 + SEND_PING = 5 + SEND_SETTINGS = 6 + SEND_RST_STREAM = 7 + SEND_PRIORITY = 8 + RECV_HEADERS = 9 + RECV_PUSH_PROMISE = 10 + RECV_DATA = 11 + RECV_GOAWAY = 12 + RECV_WINDOW_UPDATE = 13 + RECV_PING = 14 + RECV_SETTINGS = 15 + RECV_RST_STREAM = 16 + RECV_PRIORITY = 17 + SEND_ALTERNATIVE_SERVICE = 18 # Added in 2.3.0 + RECV_ALTERNATIVE_SERVICE = 19 # Added in 2.3.0 + + +class AllowedStreamIDs(IntEnum): + EVEN = 0 + ODD = 1 + + +class H2ConnectionStateMachine(object): + """ + A single HTTP/2 connection state machine. + + This state machine, while defined in its own class, is logically part of + the H2Connection class also defined in this file. The state machine itself + maintains very little state directly, instead focusing entirely on managing + state transitions. + """ + # For the purposes of this state machine we treat HEADERS and their + # associated CONTINUATION frames as a single jumbo frame. The protocol + # allows/requires this by preventing other frames from being interleved in + # between HEADERS/CONTINUATION frames. + # + # The _transitions dictionary contains a mapping of tuples of + # (state, input) to tuples of (side_effect_function, end_state). This map + # contains all allowed transitions: anything not in this map is invalid + # and immediately causes a transition to ``closed``. + + _transitions = { + # State: idle + (ConnectionState.IDLE, ConnectionInputs.SEND_HEADERS): + (None, ConnectionState.CLIENT_OPEN), + (ConnectionState.IDLE, ConnectionInputs.RECV_HEADERS): + (None, ConnectionState.SERVER_OPEN), + (ConnectionState.IDLE, ConnectionInputs.SEND_SETTINGS): + (None, ConnectionState.IDLE), + (ConnectionState.IDLE, ConnectionInputs.RECV_SETTINGS): + (None, ConnectionState.IDLE), + (ConnectionState.IDLE, ConnectionInputs.SEND_WINDOW_UPDATE): + (None, ConnectionState.IDLE), + (ConnectionState.IDLE, ConnectionInputs.RECV_WINDOW_UPDATE): + (None, ConnectionState.IDLE), + (ConnectionState.IDLE, ConnectionInputs.SEND_PING): + (None, ConnectionState.IDLE), + (ConnectionState.IDLE, ConnectionInputs.RECV_PING): + (None, ConnectionState.IDLE), + (ConnectionState.IDLE, ConnectionInputs.SEND_GOAWAY): + (None, ConnectionState.CLOSED), + (ConnectionState.IDLE, ConnectionInputs.RECV_GOAWAY): + (None, ConnectionState.CLOSED), + (ConnectionState.IDLE, ConnectionInputs.SEND_PRIORITY): + (None, ConnectionState.IDLE), + (ConnectionState.IDLE, ConnectionInputs.RECV_PRIORITY): + (None, ConnectionState.IDLE), + (ConnectionState.IDLE, ConnectionInputs.SEND_ALTERNATIVE_SERVICE): + (None, ConnectionState.SERVER_OPEN), + (ConnectionState.IDLE, ConnectionInputs.RECV_ALTERNATIVE_SERVICE): + (None, ConnectionState.CLIENT_OPEN), + + # State: open, client side. + (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_HEADERS): + (None, ConnectionState.CLIENT_OPEN), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_DATA): + (None, ConnectionState.CLIENT_OPEN), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_GOAWAY): + (None, ConnectionState.CLOSED), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_WINDOW_UPDATE): + (None, ConnectionState.CLIENT_OPEN), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_PING): + (None, ConnectionState.CLIENT_OPEN), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_SETTINGS): + (None, ConnectionState.CLIENT_OPEN), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_PRIORITY): + (None, ConnectionState.CLIENT_OPEN), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_HEADERS): + (None, ConnectionState.CLIENT_OPEN), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_PUSH_PROMISE): + (None, ConnectionState.CLIENT_OPEN), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_DATA): + (None, ConnectionState.CLIENT_OPEN), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_GOAWAY): + (None, ConnectionState.CLOSED), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_WINDOW_UPDATE): + (None, ConnectionState.CLIENT_OPEN), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_PING): + (None, ConnectionState.CLIENT_OPEN), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_SETTINGS): + (None, ConnectionState.CLIENT_OPEN), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_RST_STREAM): + (None, ConnectionState.CLIENT_OPEN), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_RST_STREAM): + (None, ConnectionState.CLIENT_OPEN), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_PRIORITY): + (None, ConnectionState.CLIENT_OPEN), + (ConnectionState.CLIENT_OPEN, + ConnectionInputs.RECV_ALTERNATIVE_SERVICE): + (None, ConnectionState.CLIENT_OPEN), + + # State: open, server side. + (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_HEADERS): + (None, ConnectionState.SERVER_OPEN), + (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_PUSH_PROMISE): + (None, ConnectionState.SERVER_OPEN), + (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_DATA): + (None, ConnectionState.SERVER_OPEN), + (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_GOAWAY): + (None, ConnectionState.CLOSED), + (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_WINDOW_UPDATE): + (None, ConnectionState.SERVER_OPEN), + (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_PING): + (None, ConnectionState.SERVER_OPEN), + (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_SETTINGS): + (None, ConnectionState.SERVER_OPEN), + (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_PRIORITY): + (None, ConnectionState.SERVER_OPEN), + (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_HEADERS): + (None, ConnectionState.SERVER_OPEN), + (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_DATA): + (None, ConnectionState.SERVER_OPEN), + (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_GOAWAY): + (None, ConnectionState.CLOSED), + (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_WINDOW_UPDATE): + (None, ConnectionState.SERVER_OPEN), + (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_PING): + (None, ConnectionState.SERVER_OPEN), + (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_SETTINGS): + (None, ConnectionState.SERVER_OPEN), + (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_PRIORITY): + (None, ConnectionState.SERVER_OPEN), + (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_RST_STREAM): + (None, ConnectionState.SERVER_OPEN), + (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_RST_STREAM): + (None, ConnectionState.SERVER_OPEN), + (ConnectionState.SERVER_OPEN, + ConnectionInputs.SEND_ALTERNATIVE_SERVICE): + (None, ConnectionState.SERVER_OPEN), + (ConnectionState.SERVER_OPEN, + ConnectionInputs.RECV_ALTERNATIVE_SERVICE): + (None, ConnectionState.SERVER_OPEN), + + # State: closed + (ConnectionState.CLOSED, ConnectionInputs.SEND_GOAWAY): + (None, ConnectionState.CLOSED), + (ConnectionState.CLOSED, ConnectionInputs.RECV_GOAWAY): + (None, ConnectionState.CLOSED), + } + + def __init__(self): + self.state = ConnectionState.IDLE + + def process_input(self, input_): + """ + Process a specific input in the state machine. + """ + if not isinstance(input_, ConnectionInputs): + raise ValueError("Input must be an instance of ConnectionInputs") + + try: + func, target_state = self._transitions[(self.state, input_)] + except KeyError: + old_state = self.state + self.state = ConnectionState.CLOSED + raise ProtocolError( + "Invalid input %s in state %s" % (input_, old_state) + ) + else: + self.state = target_state + if func is not None: # pragma: no cover + return func() + + return [] + + +class H2Connection(object): + """ + A low-level HTTP/2 connection object. This handles building and receiving + frames and maintains both connection and per-stream state for all streams + on this connection. + + This wraps a HTTP/2 Connection state machine implementation, ensuring that + frames can only be sent/received when the connection is in a valid state. + It also builds stream state machines on demand to ensure that the + constraints of those state machines are met as well. Attempts to create + frames that cannot be sent will raise a ``ProtocolError``. + + .. versionchanged:: 2.3.0 + Added the ``header_encoding`` keyword argument. + + .. versionchanged:: 2.5.0 + Added the ``config`` keyword argument. Deprecated the ``client_side`` + and ``header_encoding`` parameters. + + :param client_side: Whether this object is to be used on the client side of + a connection, or on the server side. Affects the logic used by the + state machine, the default settings values, the allowable stream IDs, + and several other properties. Defaults to ``True``. + + .. deprecated:: 2.5.0 + + :type client_side: ``bool`` + + :param header_encoding: Controls whether the headers emitted by this object + in events are transparently decoded to ``unicode`` strings, and what + encoding is used to do that decoding. For historical reason, this + defaults to ``'utf-8'``. To prevent the decoding of headers (that is, + to force them to be returned as bytestrings), this can be set to + ``False`` or the empty string. + + .. deprecated:: 2.5.0 + + :type header_encoding: ``str`` or ``False`` + + :param config: The configuration for the HTTP/2 connection. If provided, + supersedes the deprecated ``client_side`` and ``header_encoding`` + values. + + .. versionadded:: 2.5.0 + + :type config: :class:`H2Configuration <h2.config.H2Configuration>` + """ + # The initial maximum outbound frame size. This can be changed by receiving + # a settings frame. + DEFAULT_MAX_OUTBOUND_FRAME_SIZE = 65535 + + # The initial maximum inbound frame size. This is somewhat arbitrarily + # chosen. + DEFAULT_MAX_INBOUND_FRAME_SIZE = 2**24 + + # The highest acceptable stream ID. + HIGHEST_ALLOWED_STREAM_ID = 2**31 - 1 + + # The largest acceptable window increment. + MAX_WINDOW_INCREMENT = 2**31 - 1 + + # The initial default value of SETTINGS_MAX_HEADER_LIST_SIZE. + DEFAULT_MAX_HEADER_LIST_SIZE = 2**16 + + def __init__(self, client_side=True, header_encoding='utf-8', config=None): + self.state_machine = H2ConnectionStateMachine() + self.streams = {} + self.highest_inbound_stream_id = 0 + self.highest_outbound_stream_id = 0 + self.encoder = Encoder() + self.decoder = Decoder() + + # This won't always actually do anything: for versions of HPACK older + # than 2.3.0 it does nothing. However, we have to try! + self.decoder.max_header_list_size = self.DEFAULT_MAX_HEADER_LIST_SIZE + + #: The configuration for this HTTP/2 connection object. + #: + #: .. versionadded:: 2.5.0 + self.config = config + if self.config is None: + self.config = H2Configuration( + client_side=client_side, + header_encoding=header_encoding, + ) + + # Objects that store settings, including defaults. + # + # We set the MAX_CONCURRENT_STREAMS value to 100 because its default is + # unbounded, and that's a dangerous default because it allows + # essentially unbounded resources to be allocated regardless of how + # they will be used. 100 should be suitable for the average + # application. This default obviously does not apply to the remote + # peer's settings: the remote peer controls them! + # + # We also set MAX_HEADER_LIST_SIZE to a reasonable value. This is to + # advertise our defence against CVE-2016-6581. However, not all + # versions of HPACK will let us do it. That's ok: we should at least + # suggest that we're not vulnerable. + self.local_settings = Settings( + client=self.config.client_side, + initial_values={ + SettingCodes.MAX_CONCURRENT_STREAMS: 100, + SettingCodes.MAX_HEADER_LIST_SIZE: + self.DEFAULT_MAX_HEADER_LIST_SIZE, + } + ) + self.remote_settings = Settings(client=not self.config.client_side) + + # The curent value of the connection flow control windows on the + # connection. + self.outbound_flow_control_window = ( + self.remote_settings.initial_window_size + ) + + #: The maximum size of a frame that can be emitted by this peer, in + #: bytes. + self.max_outbound_frame_size = self.remote_settings.max_frame_size + + #: The maximum size of a frame that can be received by this peer, in + #: bytes. + self.max_inbound_frame_size = self.local_settings.max_frame_size + + # Buffer for incoming data. + self.incoming_buffer = FrameBuffer(server=not self.config.client_side) + + # A private variable to store a sequence of received header frames + # until completion. + self._header_frames = [] + + # Data that needs to be sent. + self._data_to_send = b'' + + # Keeps track of how streams are closed. + # Used to ensure that we don't blow up in the face of frames that were + # in flight when a RST_STREAM was sent. + # Also used to determine whether we should consider a frame received + # while a stream is closed as either a stream error or a connection + # error. + self._closed_streams = {} + + # The flow control window manager for the connection. + self._inbound_flow_control_window_manager = WindowManager( + max_window_size=self.local_settings.initial_window_size + ) + + # When in doubt use dict-dispatch. + self._frame_dispatch_table = { + HeadersFrame: self._receive_headers_frame, + PushPromiseFrame: self._receive_push_promise_frame, + SettingsFrame: self._receive_settings_frame, + DataFrame: self._receive_data_frame, + WindowUpdateFrame: self._receive_window_update_frame, + PingFrame: self._receive_ping_frame, + RstStreamFrame: self._receive_rst_stream_frame, + PriorityFrame: self._receive_priority_frame, + GoAwayFrame: self._receive_goaway_frame, + ContinuationFrame: self._receive_naked_continuation, + AltSvcFrame: self._receive_alt_svc_frame, + ExtensionFrame: self._receive_unknown_frame + } + + def _prepare_for_sending(self, frames): + if not frames: + return + self._data_to_send += b''.join(f.serialize() for f in frames) + assert all(f.body_len <= self.max_outbound_frame_size for f in frames) + + def _open_streams(self, remainder): + """ + A common method of counting number of open streams. Returns the number + of streams that are open *and* that have (stream ID % 2) == remainder. + While it iterates, also deletes any closed streams. + """ + count = 0 + to_delete = [] + + for stream_id, stream in self.streams.items(): + if stream.open and (stream_id % 2 == remainder): + count += 1 + elif stream.closed: + to_delete.append(stream_id) + + for stream_id in to_delete: + stream = self.streams.pop(stream_id) + self._closed_streams[stream_id] = stream.closed_by + + return count + + @property + def open_outbound_streams(self): + """ + The current number of open outbound streams. + """ + outbound_numbers = int(self.config.client_side) + return self._open_streams(outbound_numbers) + + @property + def open_inbound_streams(self): + """ + The current number of open inbound streams. + """ + inbound_numbers = int(not self.config.client_side) + return self._open_streams(inbound_numbers) + + @property + def header_encoding(self): + """ + Controls whether the headers emitted by this object in events are + transparently decoded to ``unicode`` strings, and what encoding is used + to do that decoding. For historical reason, this defaults to + ``'utf-8'``. To prevent the decoding of headers (that is, to force them + to be returned as bytestrings), this can be set to ``False`` or the + empty string. + + .. versionadded:: 2.3.0 + + .. deprecated:: 2.5.0 + Use :data:`config <h2.connection.H2Connection.config>` instead. + """ + return self.config.header_encoding + + @header_encoding.setter + def header_encoding(self, value): + """ + Setter for header encoding config value. + """ + self.config.header_encoding = value + + @property + def client_side(self): + """ + Whether this object is to be used on the client side of a connection, + or on the server side. Affects the logic used by the state machine, the + default settings values, the allowable stream IDs, and several other + properties. Defaults to ``True``. + + .. deprecated:: 2.5.0 + Use :data:`config <h2.connection.H2Connection.config>` instead. + """ + return self.config.client_side + + @property + def inbound_flow_control_window(self): + """ + The size of the inbound flow control window for the connection. This is + rarely publicly useful: instead, use :meth:`remote_flow_control_window + <h2.connection.H2Connection.remote_flow_control_window>`. This + shortcut is largely present to provide a shortcut to this data. + """ + return self._inbound_flow_control_window_manager.current_window_size + + def _begin_new_stream(self, stream_id, allowed_ids): + """ + Initiate a new stream. + + .. versionchanged:: 2.0.0 + Removed this function from the public API. + + :param stream_id: The ID of the stream to open. + :param allowed_ids: What kind of stream ID is allowed. + """ + self.config.logger.debug( + "Attempting to initiate stream ID %d", stream_id + ) + outbound = self._stream_id_is_outbound(stream_id) + highest_stream_id = ( + self.highest_outbound_stream_id if outbound else + self.highest_inbound_stream_id + ) + + if stream_id <= highest_stream_id: + raise StreamIDTooLowError(stream_id, highest_stream_id) + + if (stream_id % 2) != int(allowed_ids): + raise ProtocolError( + "Invalid stream ID for peer." + ) + + s = H2Stream( + stream_id, + config=self.config, + inbound_window_size=self.local_settings.initial_window_size, + outbound_window_size=self.remote_settings.initial_window_size + ) + self.config.logger.debug("Stream ID %d created", stream_id) + s.max_inbound_frame_size = self.max_inbound_frame_size + s.max_outbound_frame_size = self.max_outbound_frame_size + + self.streams[stream_id] = s + self.config.logger.debug("Current streams: %s", self.streams.keys()) + + if outbound: + self.highest_outbound_stream_id = stream_id + else: + self.highest_inbound_stream_id = stream_id + + return s + + def initiate_connection(self): + """ + Provides any data that needs to be sent at the start of the connection. + Must be called for both clients and servers. + """ + self.config.logger.debug("Initializing connection") + self.state_machine.process_input(ConnectionInputs.SEND_SETTINGS) + if self.config.client_side: + preamble = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' + else: + preamble = b'' + + f = SettingsFrame(0) + for setting, value in self.local_settings.items(): + f.settings[setting] = value + self.config.logger.debug( + "Send Settings frame: %s", self.local_settings + ) + + self._data_to_send += preamble + f.serialize() + + def initiate_upgrade_connection(self, settings_header=None): + """ + Call to initialise the connection object for use with an upgraded + HTTP/2 connection (i.e. a connection negotiated using the + ``Upgrade: h2c`` HTTP header). + + This method differs from :meth:`initiate_connection + <h2.connection.H2Connection.initiate_connection>` in several ways. + Firstly, it handles the additional SETTINGS frame that is sent in the + ``HTTP2-Settings`` header field. When called on a client connection, + this method will return a bytestring that the caller can put in the + ``HTTP2-Settings`` field they send on their initial request. When + called on a server connection, the user **must** provide the value they + received from the client in the ``HTTP2-Settings`` header field to the + ``settings_header`` argument, which will be used appropriately. + + Additionally, this method sets up stream 1 in a half-closed state + appropriate for this side of the connection, to reflect the fact that + the request is already complete. + + Finally, this method also prepares the appropriate preamble to be sent + after the upgrade. + + .. versionadded:: 2.3.0 + + :param settings_header: (optional, server-only): The value of the + ``HTTP2-Settings`` header field received from the client. + :type settings_header: ``bytes`` + + :returns: For clients, a bytestring to put in the ``HTTP2-Settings``. + For servers, returns nothing. + :rtype: ``bytes`` or ``None`` + """ + self.config.logger.debug( + "Upgrade connection. Current settings: %s", self.local_settings + ) + + frame_data = None + # Begin by getting the preamble in place. + self.initiate_connection() + + if self.config.client_side: + f = SettingsFrame(0) + for setting, value in self.local_settings.items(): + f.settings[setting] = value + + frame_data = f.serialize_body() + frame_data = base64.urlsafe_b64encode(frame_data) + elif settings_header: + # We have a settings header from the client. This needs to be + # applied, but we want to throw away the ACK. We do this by + # inserting the data into a Settings frame and then passing it to + # the state machine, but ignoring the return value. + settings_header = base64.urlsafe_b64decode(settings_header) + f = SettingsFrame(0) + f.parse_body(settings_header) + self._receive_settings_frame(f) + + # Set up appropriate state. Stream 1 in a half-closed state: + # half-closed(local) for clients, half-closed(remote) for servers. + # Additionally, we need to set up the Connection state machine. + connection_input = ( + ConnectionInputs.SEND_HEADERS if self.config.client_side + else ConnectionInputs.RECV_HEADERS + ) + self.config.logger.debug("Process input %s", connection_input) + self.state_machine.process_input(connection_input) + + # Set up stream 1. + self._begin_new_stream(stream_id=1, allowed_ids=AllowedStreamIDs.ODD) + self.streams[1].upgrade(self.config.client_side) + return frame_data + + def _get_or_create_stream(self, stream_id, allowed_ids): + """ + Gets a stream by its stream ID. Will create one if one does not already + exist. Use allowed_ids to circumvent the usual stream ID rules for + clients and servers. + + .. versionchanged:: 2.0.0 + Removed this function from the public API. + """ + try: + return self.streams[stream_id] + except KeyError: + return self._begin_new_stream(stream_id, allowed_ids) + + def _get_stream_by_id(self, stream_id): + """ + Gets a stream by its stream ID. Raises NoSuchStreamError if the stream + ID does not correspond to a known stream and is higher than the current + maximum: raises if it is lower than the current maximum. + + .. versionchanged:: 2.0.0 + Removed this function from the public API. + """ + try: + return self.streams[stream_id] + except KeyError: + outbound = self._stream_id_is_outbound(stream_id) + highest_stream_id = ( + self.highest_outbound_stream_id if outbound else + self.highest_inbound_stream_id + ) + + if stream_id > highest_stream_id: + raise NoSuchStreamError(stream_id) + else: + raise StreamClosedError(stream_id) + + def get_next_available_stream_id(self): + """ + Returns an integer suitable for use as the stream ID for the next + stream created by this endpoint. For server endpoints, this stream ID + will be even. For client endpoints, this stream ID will be odd. If no + stream IDs are available, raises :class:`NoAvailableStreamIDError + <h2.exceptions.NoAvailableStreamIDError>`. + + .. warning:: The return value from this function does not change until + the stream ID has actually been used by sending or pushing + headers on that stream. For that reason, it should be + called as close as possible to the actual use of the + stream ID. + + .. versionadded:: 2.0.0 + + :raises: :class:`NoAvailableStreamIDError + <h2.exceptions.NoAvailableStreamIDError>` + :returns: The next free stream ID this peer can use to initiate a + stream. + :rtype: ``int`` + """ + # No streams have been opened yet, so return the lowest allowed stream + # ID. + if not self.highest_outbound_stream_id: + next_stream_id = 1 if self.config.client_side else 2 + else: + next_stream_id = self.highest_outbound_stream_id + 2 + self.config.logger.debug( + "Next available stream ID %d", next_stream_id + ) + if next_stream_id > self.HIGHEST_ALLOWED_STREAM_ID: + raise NoAvailableStreamIDError("Exhausted allowed stream IDs") + + return next_stream_id + + def send_headers(self, stream_id, headers, end_stream=False, + priority_weight=None, priority_depends_on=None, + priority_exclusive=None): + """ + Send headers on a given stream. + + This function can be used to send request or response headers: the kind + that are sent depends on whether this connection has been opened as a + client or server connection, and whether the stream was opened by the + remote peer or not. + + If this is a client connection, calling ``send_headers`` will send the + headers as a request. It will also implicitly open the stream being + used. If this is a client connection and ``send_headers`` has *already* + been called, this will send trailers instead. + + If this is a server connection, calling ``send_headers`` will send the + headers as a response. It is a protocol error for a server to open a + stream by sending headers. If this is a server connection and + ``send_headers`` has *already* been called, this will send trailers + instead. + + When acting as a server, you may call ``send_headers`` any number of + times allowed by the following rules, in this order: + + - zero or more times with ``(':status', '1XX')`` (where ``1XX`` is a + placeholder for any 100-level status code). + - once with any other status header. + - zero or one time for trailers. + + That is, you are allowed to send as many informational responses as you + like, followed by one complete response and zero or one HTTP trailer + blocks. + + Clients may send one or two header blocks: one request block, and + optionally one trailer block. + + If it is important to send HPACK "never indexed" header fields (as + defined in `RFC 7451 Section 7.1.3 + <https://tools.ietf.org/html/rfc7541#section-7.1.3>`_), the user may + instead provide headers using the HPACK library's :class:`HeaderTuple + <hpack:hpack.HeaderTuple>` and :class:`NeverIndexedHeaderTuple + <hpack:hpack.NeverIndexedHeaderTuple>` objects. + + This method also allows users to prioritize the stream immediately, + by sending priority information on the HEADERS frame directly. To do + this, any one of ``priority_weight``, ``priority_depends_on``, or + ``priority_exclusive`` must be set to a value that is not ``None``. For + more information on the priority fields, see :meth:`prioritize + <h2.connection.H2Connection.prioritize>`. + + .. warning:: In HTTP/2, it is mandatory that all the HTTP/2 special + headers (that is, ones whose header keys begin with ``:``) appear + at the start of the header block, before any normal headers. + If you pass a dictionary to the ``headers`` parameter, it is + unlikely that they will iterate in that order, and your connection + may fail. For this reason, passing a ``dict`` to ``headers`` is + *deprecated*, and will be removed in 3.0. + + .. versionchanged:: 2.3.0 + Added support for using :class:`HeaderTuple + <hpack:hpack.HeaderTuple>` objects to store headers. + + .. versionchanged:: 2.4.0 + Added the ability to provide priority keyword arguments: + ``priority_weight``, ``priority_depends_on``, and + ``priority_exclusive``. + + :param stream_id: The stream ID to send the headers on. If this stream + does not currently exist, it will be created. + :type stream_id: ``int`` + + :param headers: The request/response headers to send. + :type headers: An iterable of two tuples of bytestrings or + :class:`HeaderTuple <hpack:hpack.HeaderTuple>` objects. + + :param end_stream: Whether this headers frame should end the stream + immediately (that is, whether no more data will be sent after this + frame). Defaults to ``False``. + :type end_stream: ``bool`` + + :param priority_weight: Sets the priority weight of the stream. See + :meth:`prioritize <h2.connection.H2Connection.prioritize>` for more + about how this field works. Defaults to ``None``, which means that + no priority information will be sent. + :type priority_weight: ``int`` or ``None`` + + :param priority_depends_on: Sets which stream this one depends on for + priority purposes. See :meth:`prioritize + <h2.connection.H2Connection.prioritize>` for more about how this + field works. Defaults to ``None``, which means that no priority + information will be sent. + :type priority_depends_on: ``int`` or ``None`` + + :param priority_exclusive: Sets whether this stream exclusively depends + on the stream given in ``priority_depends_on`` for priority + purposes. See :meth:`prioritize + <h2.connection.H2Connection.prioritize>` for more about how this + field workds. Defaults to ``None``, which means that no priority + information will be sent. + :type priority_depends_on: ``bool`` or ``None`` + + :returns: Nothing + """ + self.config.logger.debug( + "Send headers on stream ID %d", stream_id + ) + + # Check we can open the stream. + if stream_id not in self.streams: + max_open_streams = self.remote_settings.max_concurrent_streams + if (self.open_outbound_streams + 1) > max_open_streams: + raise TooManyStreamsError( + "Max outbound streams is %d, %d open" % + (max_open_streams, self.open_outbound_streams) + ) + + self.state_machine.process_input(ConnectionInputs.SEND_HEADERS) + stream = self._get_or_create_stream( + stream_id, AllowedStreamIDs(self.config.client_side) + ) + frames = stream.send_headers( + headers, self.encoder, end_stream + ) + + # We may need to send priority information. + priority_present = ( + (priority_weight is not None) or + (priority_depends_on is not None) or + (priority_exclusive is not None) + ) + + if priority_present: + if not self.config.client_side: + raise RFC1122Error("Servers SHOULD NOT prioritize streams.") + + headers_frame = frames[0] + headers_frame.flags.add('PRIORITY') + frames[0] = _add_frame_priority( + headers_frame, + priority_weight, + priority_depends_on, + priority_exclusive + ) + + self._prepare_for_sending(frames) + + def send_data(self, stream_id, data, end_stream=False, pad_length=None): + """ + Send data on a given stream. + + This method does no breaking up of data: if the data is larger than the + value returned by :meth:`local_flow_control_window + <h2.connection.H2Connection.local_flow_control_window>` for this stream + then a :class:`FlowControlError <h2.exceptions.FlowControlError>` will + be raised. If the data is larger than :data:`max_outbound_frame_size + <h2.connection.H2Connection.max_outbound_frame_size>` then a + :class:`FrameTooLargeError <h2.exceptions.FrameTooLargeError>` will be + raised. + + Hyper-h2 does this to avoid buffering the data internally. If the user + has more data to send than hyper-h2 will allow, consider breaking it up + and buffering it externally. + + :param stream_id: The ID of the stream on which to send the data. + :type stream_id: ``int`` + :param data: The data to send on the stream. + :type data: ``bytes`` + :param end_stream: (optional) Whether this is the last data to be sent + on the stream. Defaults to ``False``. + :type end_stream: ``bool`` + :param pad_length: (optional) Length of the padding to apply to the + data frame. Defaults to ``None`` for no use of padding. Note that + a value of ``0`` results in padding of length ``0`` + (with the "padding" flag set on the frame). + + .. versionadded:: 2.6.0 + + :type pad_length: ``int`` + :returns: Nothing + """ + self.config.logger.debug( + "Send data on stream ID %d with len %d", stream_id, len(data) + ) + frame_size = len(data) + if pad_length is not None: + if not isinstance(pad_length, int): + raise TypeError("pad_length must be an int") + if pad_length < 0 or pad_length > 255: + raise ValueError("pad_length must be within range: [0, 255]") + # Account for padding bytes plus the 1-byte padding length field. + frame_size += pad_length + 1 + self.config.logger.debug( + "Frame size on stream ID %d is %d", stream_id, frame_size + ) + + if frame_size > self.local_flow_control_window(stream_id): + raise FlowControlError( + "Cannot send %d bytes, flow control window is %d." % + (frame_size, self.local_flow_control_window(stream_id)) + ) + elif frame_size > self.max_outbound_frame_size: + raise FrameTooLargeError( + "Cannot send frame size %d, max frame size is %d" % + (frame_size, self.max_outbound_frame_size) + ) + + self.state_machine.process_input(ConnectionInputs.SEND_DATA) + frames = self.streams[stream_id].send_data( + data, end_stream, pad_length=pad_length + ) + + self._prepare_for_sending(frames) + + self.outbound_flow_control_window -= frame_size + self.config.logger.debug( + "Outbound flow control window size is %d", + self.outbound_flow_control_window + ) + assert self.outbound_flow_control_window >= 0 + + def end_stream(self, stream_id): + """ + Cleanly end a given stream. + + This method ends a stream by sending an empty DATA frame on that stream + with the ``END_STREAM`` flag set. + + :param stream_id: The ID of the stream to end. + :type stream_id: ``int`` + :returns: Nothing + """ + self.config.logger.debug("End stream ID %d", stream_id) + self.state_machine.process_input(ConnectionInputs.SEND_DATA) + frames = self.streams[stream_id].end_stream() + self._prepare_for_sending(frames) + + def increment_flow_control_window(self, increment, stream_id=None): + """ + Increment a flow control window, optionally for a single stream. Allows + the remote peer to send more data. + + .. versionchanged:: 2.0.0 + Rejects attempts to increment the flow control window by out of + range values with a ``ValueError``. + + :param increment: The amount to increment the flow control window by. + :type increment: ``int`` + :param stream_id: (optional) The ID of the stream that should have its + flow control window opened. If not present or ``None``, the + connection flow control window will be opened instead. + :type stream_id: ``int`` or ``None`` + :returns: Nothing + :raises: ``ValueError`` + """ + if not (1 <= increment <= self.MAX_WINDOW_INCREMENT): + raise ValueError( + "Flow control increment must be between 1 and %d" % + self.MAX_WINDOW_INCREMENT + ) + + self.state_machine.process_input(ConnectionInputs.SEND_WINDOW_UPDATE) + + if stream_id is not None: + stream = self.streams[stream_id] + frames = stream.increase_flow_control_window( + increment + ) + else: + self._inbound_flow_control_window_manager.window_opened(increment) + f = WindowUpdateFrame(0) + f.window_increment = increment + frames = [f] + + self.config.logger.debug( + "Increase stream ID %d flow control window by %d", + stream_id, increment + ) + self._prepare_for_sending(frames) + + def push_stream(self, stream_id, promised_stream_id, request_headers): + """ + Push a response to the client by sending a PUSH_PROMISE frame. + + If it is important to send HPACK "never indexed" header fields (as + defined in `RFC 7451 Section 7.1.3 + <https://tools.ietf.org/html/rfc7541#section-7.1.3>`_), the user may + instead provide headers using the HPACK library's :class:`HeaderTuple + <hpack:hpack.HeaderTuple>` and :class:`NeverIndexedHeaderTuple + <hpack:hpack.NeverIndexedHeaderTuple>` objects. + + :param stream_id: The ID of the stream that this push is a response to. + :type stream_id: ``int`` + :param promised_stream_id: The ID of the stream that the pushed + response will be sent on. + :type promised_stream_id: ``int`` + :param request_headers: The headers of the request that the pushed + response will be responding to. + :type request_headers: An iterable of two tuples of bytestrings or + :class:`HeaderTuple <hpack:hpack.HeaderTuple>` objects. + :returns: Nothing + """ + self.config.logger.debug( + "Send Push Promise frame on stream ID %d", stream_id + ) + + if not self.remote_settings.enable_push: + raise ProtocolError("Remote peer has disabled stream push") + + self.state_machine.process_input(ConnectionInputs.SEND_PUSH_PROMISE) + stream = self._get_stream_by_id(stream_id) + + # We need to prevent users pushing streams in response to streams that + # they themselves have already pushed: see #163 and RFC 7540 § 6.6. The + # easiest way to do that is to assert that the stream_id is not even: + # this shortcut works because only servers can push and the state + # machine will enforce this. + if (stream_id % 2) == 0: + raise ProtocolError("Cannot recursively push streams.") + + new_stream = self._begin_new_stream( + promised_stream_id, AllowedStreamIDs.EVEN + ) + self.streams[promised_stream_id] = new_stream + + frames = stream.push_stream_in_band( + promised_stream_id, request_headers, self.encoder + ) + new_frames = new_stream.locally_pushed() + self._prepare_for_sending(frames + new_frames) + + def ping(self, opaque_data): + """ + Send a PING frame. + + :param opaque_data: A bytestring of length 8 that will be sent in the + PING frame. + :returns: Nothing + """ + self.config.logger.debug("Send Ping frame") + + if not isinstance(opaque_data, bytes) or len(opaque_data) != 8: + raise ValueError("Invalid value for ping data: %r" % opaque_data) + + self.state_machine.process_input(ConnectionInputs.SEND_PING) + f = PingFrame(0) + f.opaque_data = opaque_data + self._prepare_for_sending([f]) + + def reset_stream(self, stream_id, error_code=0): + """ + Reset a stream. + + This method forcibly closes a stream by sending a RST_STREAM frame for + a given stream. This is not a graceful closure. To gracefully end a + stream, try the :meth:`end_stream + <h2.connection.H2Connection.end_stream>` method. + + :param stream_id: The ID of the stream to reset. + :type stream_id: ``int`` + :param error_code: (optional) The error code to use to reset the + stream. Defaults to :data:`ErrorCodes.NO_ERROR + <h2.errors.ErrorCodes.NO_ERROR>`. + :type error_code: ``int`` + :returns: Nothing + """ + self.config.logger.debug("Reset stream ID %d", stream_id) + self.state_machine.process_input(ConnectionInputs.SEND_RST_STREAM) + stream = self._get_stream_by_id(stream_id) + frames = stream.reset_stream(error_code) + self._prepare_for_sending(frames) + + def close_connection(self, error_code=0, additional_data=None, + last_stream_id=None): + + """ + Close a connection, emitting a GOAWAY frame. + + .. versionchanged:: 2.4.0 + Added ``additional_data`` and ``last_stream_id`` arguments. + + :param error_code: (optional) The error code to send in the GOAWAY + frame. + :param additional_data: (optional) Additional debug data indicating + a reason for closing the connection. Must be a bytestring. + :param last_stream_id: (optional) The last stream which was processed + by the sender. Defaults to ``highest_inbound_stream_id``. + :returns: Nothing + """ + self.config.logger.debug("Close connection") + self.state_machine.process_input(ConnectionInputs.SEND_GOAWAY) + + # Additional_data must be bytes + if additional_data is not None: + assert isinstance(additional_data, bytes) + + if last_stream_id is None: + last_stream_id = self.highest_inbound_stream_id + + f = GoAwayFrame( + stream_id=0, + last_stream_id=last_stream_id, + error_code=error_code, + additional_data=(additional_data or b'') + ) + self._prepare_for_sending([f]) + + def update_settings(self, new_settings): + """ + Update the local settings. This will prepare and emit the appropriate + SETTINGS frame. + + :param new_settings: A dictionary of {setting: new value} + """ + self.config.logger.debug( + "Update connection settings to %s", new_settings + ) + self.state_machine.process_input(ConnectionInputs.SEND_SETTINGS) + self.local_settings.update(new_settings) + s = SettingsFrame(0) + s.settings = new_settings + self._prepare_for_sending([s]) + + def advertise_alternative_service(self, + field_value, + origin=None, + stream_id=None): + """ + Notify a client about an available Alternative Service. + + An Alternative Service is defined in `RFC 7838 + <https://tools.ietf.org/html/rfc7838>`_. An Alternative Service + notification informs a client that a given origin is also available + elsewhere. + + Alternative Services can be advertised in two ways. Firstly, they can + be advertised explicitly: that is, a server can say "origin X is also + available at Y". To advertise like this, set the ``origin`` argument + and not the ``stream_id`` argument. Alternatively, they can be + advertised implicitly: that is, a server can say "the origin you're + contacting on stream X is also available at Y". To advertise like this, + set the ``stream_id`` argument and not the ``origin`` argument. + + The explicit method of advertising can be done as long as the + connection is active. The implicit method can only be done after the + client has sent the request headers and before the server has sent the + response headers: outside of those points, Hyper-h2 will forbid sending + the Alternative Service advertisement by raising a ProtocolError. + + The ``field_value`` parameter is specified in RFC 7838. Hyper-h2 does + not validate or introspect this argument: the user is required to + ensure that it's well-formed. ``field_value`` corresponds to RFC 7838's + "Alternative Service Field Value". + + .. note:: It is strongly preferred to use the explicit method of + advertising Alternative Services. The implicit method of + advertising Alternative Services has a number of subtleties + and can lead to inconsistencies between the server and + client. Hyper-h2 allows both mechanisms, but caution is + strongly advised. + + .. versionadded:: 2.3.0 + + :param field_value: The RFC 7838 Alternative Service Field Value. This + argument is not introspected by Hyper-h2: the user is responsible + for ensuring that it is well-formed. + :type field_value: ``bytes`` + + :param origin: The origin/authority to which the Alternative Service + being advertised applies. Must not be provided at the same time as + ``stream_id``. + :type origin: ``bytes`` or ``None`` + + :param stream_id: The ID of the stream which was sent to the authority + for which this Alternative Service advertisement applies. Must not + be provided at the same time as ``origin``. + :type stream_id: ``int`` or ``None`` + + :returns: Nothing. + """ + if not isinstance(field_value, bytes): + raise ValueError("Field must be bytestring.") + + if origin is not None and stream_id is not None: + raise ValueError("Must not provide both origin and stream_id") + + self.state_machine.process_input( + ConnectionInputs.SEND_ALTERNATIVE_SERVICE + ) + + if origin is not None: + # This ALTSVC is sent on stream zero. + f = AltSvcFrame(stream_id=0) + f.origin = origin + f.field = field_value + frames = [f] + else: + stream = self._get_stream_by_id(stream_id) + frames = stream.advertise_alternative_service(field_value) + + self._prepare_for_sending(frames) + + def prioritize(self, stream_id, weight=None, depends_on=None, + exclusive=None): + """ + Notify a server about the priority of a stream. + + Stream priorities are a form of guidance to a remote server: they + inform the server about how important a given response is, so that the + server may allocate its resources (e.g. bandwidth, CPU time, etc.) + accordingly. This exists to allow clients to ensure that the most + important data arrives earlier, while less important data does not + starve out the more important data. + + Stream priorities are explained in depth in `RFC 7540 Section 5.3 + <https://tools.ietf.org/html/rfc7540#section-5.3>`_. + + This method updates the priority information of a single stream. It may + be called well before a stream is actively in use, or well after a + stream is closed. + + .. warning:: RFC 7540 allows for servers to change the priority of + streams. However, hyper-h2 **does not** allow server + stacks to do this. This is because most clients do not + adequately know how to respond when provided conflicting + priority information, and relatively little utility is + provided by making that functionality available. + + .. note:: hyper-h2 **does not** maintain any information about the + RFC 7540 priority tree. That means that hyper-h2 does not + prevent incautious users from creating invalid priority + trees, particularly by creating priority loops. While some + basic error checking is provided by hyper-h2, users are + strongly recommended to understand their prioritisation + strategies before using the priority tools here. + + .. note:: Priority information is strictly advisory. Servers are + allowed to disregard it entirely. Avoid relying on the idea + that your priority signaling will definitely be obeyed. + + .. versionadded:: 2.4.0 + + :param stream_id: The ID of the stream to prioritize. + :type stream_id: ``int`` + + :param weight: The weight to give the stream. Defaults to ``16``, the + default weight of any stream. May be any value between ``1`` and + ``256`` inclusive. The relative weight of a stream indicates what + proportion of available resources will be allocated to that + stream. + :type weight: ``int`` + + :param depends_on: The ID of the stream on which this stream depends. + This stream will only be progressed if it is impossible to + progress the parent stream (the one on which this one depends). + Passing the value ``0`` means that this stream does not depend on + any other. Defaults to ``0``. + :type depends_on: ``int`` + + :param exclusive: Whether this stream is an exclusive dependency of its + "parent" stream (i.e. the stream given by ``depends_on``). If a + stream is an exclusive dependency of another, that means that all + previously-set children of the parent are moved to become children + of the new exclusively-dependent stream. Defaults to ``False``. + :type exclusive: ``bool`` + """ + if not self.config.client_side: + raise RFC1122Error("Servers SHOULD NOT prioritize streams.") + + self.state_machine.process_input( + ConnectionInputs.SEND_PRIORITY + ) + + frame = PriorityFrame(stream_id) + frame = _add_frame_priority(frame, weight, depends_on, exclusive) + + self._prepare_for_sending([frame]) + + def local_flow_control_window(self, stream_id): + """ + Returns the maximum amount of data that can be sent on stream + ``stream_id``. + + This value will never be larger than the total data that can be sent on + the connection: even if the given stream allows more data, the + connection window provides a logical maximum to the amount of data that + can be sent. + + The maximum data that can be sent in a single data frame on a stream + is either this value, or the maximum frame size, whichever is + *smaller*. + + :param stream_id: The ID of the stream whose flow control window is + being queried. + :type stream_id: ``int`` + :returns: The amount of data in bytes that can be sent on the stream + before the flow control window is exhausted. + :rtype: ``int`` + """ + stream = self._get_stream_by_id(stream_id) + return min( + self.outbound_flow_control_window, + stream.outbound_flow_control_window + ) + + def remote_flow_control_window(self, stream_id): + """ + Returns the maximum amount of data the remote peer can send on stream + ``stream_id``. + + This value will never be larger than the total data that can be sent on + the connection: even if the given stream allows more data, the + connection window provides a logical maximum to the amount of data that + can be sent. + + The maximum data that can be sent in a single data frame on a stream + is either this value, or the maximum frame size, whichever is + *smaller*. + + :param stream_id: The ID of the stream whose flow control window is + being queried. + :type stream_id: ``int`` + :returns: The amount of data in bytes that can be received on the + stream before the flow control window is exhausted. + :rtype: ``int`` + """ + stream = self._get_stream_by_id(stream_id) + return min( + self.inbound_flow_control_window, + stream.inbound_flow_control_window + ) + + def acknowledge_received_data(self, acknowledged_size, stream_id): + """ + Inform the :class:`H2Connection <h2.connection.H2Connection>` that a + certain number of flow-controlled bytes have been processed, and that + the space should be handed back to the remote peer at an opportune + time. + + .. versionadded:: 2.5.0 + + :param acknowledged_size: The total *flow-controlled size* of the data + that has been processed. Note that this must include the amount of + padding that was sent with that data. + :type acknowledged_size: ``int`` + :param stream_id: The ID of the stream on which this data was received. + :type stream_id: ``int`` + :returns: Nothing + :rtype: ``None`` + """ + self.config.logger.debug( + "Ack received data on stream ID %d with size %d", + stream_id, acknowledged_size + ) + if stream_id <= 0: + raise ValueError( + "Stream ID %d is not valid for acknowledge_received_data" % + stream_id + ) + if acknowledged_size < 0: + raise ValueError("Cannot acknowledge negative data") + + frames = [] + + conn_manager = self._inbound_flow_control_window_manager + conn_increment = conn_manager.process_bytes(acknowledged_size) + if conn_increment: + f = WindowUpdateFrame(0) + f.window_increment = conn_increment + frames.append(f) + + try: + stream = self._get_stream_by_id(stream_id) + except StreamClosedError: + # The stream is already gone. We're not worried about incrementing + # the window in this case. + pass + else: + # No point incrementing the windows of closed streams. + if stream.open: + frames.extend( + stream.acknowledge_received_data(acknowledged_size) + ) + + self._prepare_for_sending(frames) + + def data_to_send(self, amt=None): + """ + Returns some data for sending out of the internal data buffer. + + This method is analogous to ``read`` on a file-like object, but it + doesn't block. Instead, it returns as much data as the user asks for, + or less if that much data is not available. It does not perform any + I/O, and so uses a different name. + + :param amt: (optional) The maximum amount of data to return. If not + set, or set to ``None``, will return as much data as possible. + :type amt: ``int`` + :returns: A bytestring containing the data to send on the wire. + :rtype: ``bytes`` + """ + if amt is None: + data = self._data_to_send + self._data_to_send = b'' + return data + else: + data = self._data_to_send[:amt] + self._data_to_send = self._data_to_send[amt:] + return data + + def clear_outbound_data_buffer(self): + """ + Clears the outbound data buffer, such that if this call was immediately + followed by a call to + :meth:`data_to_send <h2.connection.H2Connection.data_to_send>`, that + call would return no data. + + This method should not normally be used, but is made available to avoid + exposing implementation details. + """ + self._data_to_send = b'' + + def _acknowledge_settings(self): + """ + Acknowledge settings that have been received. + + .. versionchanged:: 2.0.0 + Removed from public API, removed useless ``event`` parameter, made + automatic. + + :returns: Nothing + """ + self.state_machine.process_input(ConnectionInputs.SEND_SETTINGS) + + changes = self.remote_settings.acknowledge() + + if SettingCodes.INITIAL_WINDOW_SIZE in changes: + setting = changes[SettingCodes.INITIAL_WINDOW_SIZE] + self._flow_control_change_from_settings( + setting.original_value, + setting.new_value, + ) + + # HEADER_TABLE_SIZE changes by the remote part affect our encoder: cf. + # RFC 7540 Section 6.5.2. + if SettingCodes.HEADER_TABLE_SIZE in changes: + setting = changes[SettingCodes.HEADER_TABLE_SIZE] + self.encoder.header_table_size = setting.new_value + + if SettingCodes.MAX_FRAME_SIZE in changes: + setting = changes[SettingCodes.MAX_FRAME_SIZE] + self.max_outbound_frame_size = setting.new_value + for stream in self.streams.values(): + stream.max_outbound_frame_size = setting.new_value + + f = SettingsFrame(0) + f.flags.add('ACK') + return [f] + + def _flow_control_change_from_settings(self, old_value, new_value): + """ + Update flow control windows in response to a change in the value of + SETTINGS_INITIAL_WINDOW_SIZE. + + When this setting is changed, it automatically updates all flow control + windows by the delta in the settings values. Note that it does not + increment the *connection* flow control window, per section 6.9.2 of + RFC 7540. + """ + delta = new_value - old_value + + for stream in self.streams.values(): + stream.outbound_flow_control_window = guard_increment_window( + stream.outbound_flow_control_window, + delta + ) + + def _inbound_flow_control_change_from_settings(self, old_value, new_value): + """ + Update remote flow control windows in response to a change in the value + of SETTINGS_INITIAL_WINDOW_SIZE. + + When this setting is changed, it automatically updates all remote flow + control windows by the delta in the settings values. + """ + delta = new_value - old_value + + for stream in self.streams.values(): + stream._inbound_flow_control_change_from_settings(delta) + + def receive_data(self, data): + """ + Pass some received HTTP/2 data to the connection for handling. + + :param data: The data received from the remote peer on the network. + :type data: ``bytes`` + :returns: A list of events that the remote peer triggered by sending + this data. + """ + self.config.logger.debug( + "Process received data on connection. Received data: %r", data + ) + + events = [] + self.incoming_buffer.add_data(data) + self.incoming_buffer.max_frame_size = self.max_inbound_frame_size + + try: + for frame in self.incoming_buffer: + events.extend(self._receive_frame(frame)) + except InvalidPaddingError: + self._terminate_connection(ErrorCodes.PROTOCOL_ERROR) + raise ProtocolError("Received frame with invalid padding.") + except ProtocolError as e: + # For whatever reason, receiving the frame caused a protocol error. + # We should prepare to emit a GoAway frame before throwing the + # exception up further. No need for an event: the exception will + # do fine. + self._terminate_connection(e.error_code) + raise + + return events + + def _receive_frame(self, frame): + """ + Handle a frame received on the connection. + + .. versionchanged:: 2.0.0 + Removed from the public API. + """ + try: + # I don't love using __class__ here, maybe reconsider it. + frames, events = self._frame_dispatch_table[frame.__class__](frame) + except StreamClosedError as e: + # If the stream was closed by RST_STREAM, we just send a RST_STREAM + # to the remote peer. Otherwise, this is a connection error, and so + # we will re-raise to trigger one. + if self._stream_is_closed_by_reset(e.stream_id): + f = RstStreamFrame(e.stream_id) + f.error_code = e.error_code + self._prepare_for_sending([f]) + events = e._events + else: + raise + except StreamIDTooLowError as e: + # The stream ID seems invalid. This may happen when the closed + # stream has been cleaned up, or when the remote peer has opened a + # new stream with a higher stream ID than this one, forcing it + # closed implicitly. + # + # Check how the stream was closed: depending on the mechanism, it + # is either a stream error or a connection error. + if self._stream_is_closed_by_reset(e.stream_id): + # Closed by RST_STREAM is a stream error. + f = RstStreamFrame(e.stream_id) + f.error_code = ErrorCodes.STREAM_CLOSED + self._prepare_for_sending([f]) + events = [] + elif self._stream_is_closed_by_end(e.stream_id): + # Closed by END_STREAM is a connection error. + raise StreamClosedError(e.stream_id) + else: + # Closed implicitly, also a connection error, but of type + # PROTOCOL_ERROR. + raise + else: + self._prepare_for_sending(frames) + + return events + + def _terminate_connection(self, error_code): + """ + Terminate the connection early. Used in error handling blocks to send + GOAWAY frames. + """ + f = GoAwayFrame(0) + f.last_stream_id = self.highest_inbound_stream_id + f.error_code = error_code + self.state_machine.process_input(ConnectionInputs.SEND_GOAWAY) + self._prepare_for_sending([f]) + + def _receive_headers_frame(self, frame): + """ + Receive a headers frame on the connection. + """ + # If necessary, check we can open the stream. Also validate that the + # stream ID is valid. + if frame.stream_id not in self.streams: + max_open_streams = self.local_settings.max_concurrent_streams + if (self.open_inbound_streams + 1) > max_open_streams: + raise TooManyStreamsError( + "Max outbound streams is %d, %d open" % + (max_open_streams, self.open_outbound_streams) + ) + + # Let's decode the headers. We handle headers as bytes internally up + # until we hang them off the event, at which point we may optionally + # convert them to unicode. + headers = _decode_headers(self.decoder, frame.data) + + events = self.state_machine.process_input( + ConnectionInputs.RECV_HEADERS + ) + stream = self._get_or_create_stream( + frame.stream_id, AllowedStreamIDs(not self.config.client_side) + ) + frames, stream_events = stream.receive_headers( + headers, + 'END_STREAM' in frame.flags, + self.config.header_encoding + ) + + if 'PRIORITY' in frame.flags: + p_frames, p_events = self._receive_priority_frame(frame) + stream_events[0].priority_updated = p_events[0] + stream_events.extend(p_events) + assert not p_frames + + return frames, events + stream_events + + def _receive_push_promise_frame(self, frame): + """ + Receive a push-promise frame on the connection. + """ + if not self.local_settings.enable_push: + raise ProtocolError("Received pushed stream") + + pushed_headers = _decode_headers(self.decoder, frame.data) + + events = self.state_machine.process_input( + ConnectionInputs.RECV_PUSH_PROMISE + ) + + try: + stream = self._get_stream_by_id(frame.stream_id) + except NoSuchStreamError: + # We need to check if the parent stream was reset by us. If it was + # then we presume that the PUSH_PROMISE was in flight when we reset + # the parent stream. Rather than accept the new stream, just reset + # it. + # + # If this was closed naturally, however, we should call this a + # PROTOCOL_ERROR: pushing a stream on a naturally closed stream is + # a real problem because it creates a brand new stream that the + # remote peer now believes exists. + if (self._stream_closed_by(frame.stream_id) == + StreamClosedBy.SEND_RST_STREAM): + f = RstStreamFrame(frame.promised_stream_id) + f.error_code = ErrorCodes.REFUSED_STREAM + return [f], events + + raise ProtocolError("Attempted to push on closed stream.") + + # We need to prevent peers pushing streams in response to streams that + # they themselves have already pushed: see #163 and RFC 7540 § 6.6. The + # easiest way to do that is to assert that the stream_id is not even: + # this shortcut works because only servers can push and the state + # machine will enforce this. + if (frame.stream_id % 2) == 0: + raise ProtocolError("Cannot recursively push streams.") + + try: + frames, stream_events = stream.receive_push_promise_in_band( + frame.promised_stream_id, + pushed_headers, + self.config.header_encoding, + ) + except StreamClosedError: + # The parent stream was reset by us, so we presume that + # PUSH_PROMISE was in flight when we reset the parent stream. + # So we just reset the new stream. + f = RstStreamFrame(frame.promised_stream_id) + f.error_code = ErrorCodes.REFUSED_STREAM + return [f], events + + new_stream = self._begin_new_stream( + frame.promised_stream_id, AllowedStreamIDs.EVEN + ) + self.streams[frame.promised_stream_id] = new_stream + new_stream.remotely_pushed(pushed_headers) + + return frames, events + stream_events + + def _receive_data_frame(self, frame): + """ + Receive a data frame on the connection. + """ + flow_controlled_length = frame.flow_controlled_length + + events = self.state_machine.process_input( + ConnectionInputs.RECV_DATA + ) + self._inbound_flow_control_window_manager.window_consumed( + flow_controlled_length + ) + stream = self._get_stream_by_id(frame.stream_id) + frames, stream_events = stream.receive_data( + frame.data, + 'END_STREAM' in frame.flags, + flow_controlled_length + ) + return frames, events + stream_events + + def _receive_settings_frame(self, frame): + """ + Receive a SETTINGS frame on the connection. + """ + events = self.state_machine.process_input( + ConnectionInputs.RECV_SETTINGS + ) + + # This is an ack of the local settings. + if 'ACK' in frame.flags: + changed_settings = self._local_settings_acked() + ack_event = SettingsAcknowledged() + ack_event.changed_settings = changed_settings + events.append(ack_event) + return [], events + + # Add the new settings. + self.remote_settings.update(frame.settings) + events.append( + RemoteSettingsChanged.from_settings( + self.remote_settings, frame.settings + ) + ) + frames = self._acknowledge_settings() + + return frames, events + + def _receive_window_update_frame(self, frame): + """ + Receive a WINDOW_UPDATE frame on the connection. + """ + # Validate the frame. + if not (1 <= frame.window_increment <= self.MAX_WINDOW_INCREMENT): + raise ProtocolError( + "Flow control increment must be between 1 and %d, received %d" + % (self.MAX_WINDOW_INCREMENT, frame.window_increment) + ) + + events = self.state_machine.process_input( + ConnectionInputs.RECV_WINDOW_UPDATE + ) + + if frame.stream_id: + stream = self._get_stream_by_id(frame.stream_id) + frames, stream_events = stream.receive_window_update( + frame.window_increment + ) + else: + # Increment our local flow control window. + self.outbound_flow_control_window = guard_increment_window( + self.outbound_flow_control_window, + frame.window_increment + ) + + # FIXME: Should we split this into one event per active stream? + window_updated_event = WindowUpdated() + window_updated_event.stream_id = 0 + window_updated_event.delta = frame.window_increment + stream_events = [window_updated_event] + frames = [] + + return frames, events + stream_events + + def _receive_ping_frame(self, frame): + """ + Receive a PING frame on the connection. + """ + events = self.state_machine.process_input( + ConnectionInputs.RECV_PING + ) + flags = [] + + if 'ACK' in frame.flags: + evt = PingAcknowledged() + evt.ping_data = frame.opaque_data + events.append(evt) + else: + f = PingFrame(0) + f.flags = {'ACK'} + f.opaque_data = frame.opaque_data + flags.append(f) + + return flags, events + + def _receive_rst_stream_frame(self, frame): + """ + Receive a RST_STREAM frame on the connection. + """ + events = self.state_machine.process_input( + ConnectionInputs.RECV_RST_STREAM + ) + try: + stream = self._get_stream_by_id(frame.stream_id) + except NoSuchStreamError: + # The stream is missing. That's ok, we just do nothing here. + stream_frames = [] + stream_events = [] + else: + stream_frames, stream_events = stream.stream_reset(frame) + + return stream_frames, events + stream_events + + def _receive_priority_frame(self, frame): + """ + Receive a PRIORITY frame on the connection. + """ + events = self.state_machine.process_input( + ConnectionInputs.RECV_PRIORITY + ) + + event = PriorityUpdated() + event.stream_id = frame.stream_id + event.depends_on = frame.depends_on + event.exclusive = frame.exclusive + + # Weight is an integer between 1 and 256, but the byte only allows + # 0 to 255: add one. + event.weight = frame.stream_weight + 1 + + # A stream may not depend on itself. + if event.depends_on == frame.stream_id: + raise ProtocolError( + "Stream %d may not depend on itself" % frame.stream_id + ) + events.append(event) + + return [], events + + def _receive_goaway_frame(self, frame): + """ + Receive a GOAWAY frame on the connection. + """ + events = self.state_machine.process_input( + ConnectionInputs.RECV_GOAWAY + ) + + # Clear the outbound data buffer: we cannot send further data now. + self.clear_outbound_data_buffer() + + # Fire an appropriate ConnectionTerminated event. + new_event = ConnectionTerminated() + new_event.error_code = _error_code_from_int(frame.error_code) + new_event.last_stream_id = frame.last_stream_id + new_event.additional_data = (frame.additional_data + if frame.additional_data else None) + events.append(new_event) + + return [], events + + def _receive_naked_continuation(self, frame): + """ + A naked CONTINUATION frame has been received. This is always an error, + but the type of error it is depends on the state of the stream and must + transition the state of the stream, so we need to pass it to the + appropriate stream. + """ + stream = self._get_stream_by_id(frame.stream_id) + stream.receive_continuation() + assert False, "Should not be reachable" + + def _receive_alt_svc_frame(self, frame): + """ + An ALTSVC frame has been received. This frame, specified in RFC 7838, + is used to advertise alternative places where the same service can be + reached. + + This frame can optionally be received either on a stream or on stream + 0, and its semantics are different in each case. + """ + events = self.state_machine.process_input( + ConnectionInputs.RECV_ALTERNATIVE_SERVICE + ) + frames = [] + + if frame.stream_id: + # Given that it makes no sense to receive ALTSVC on a stream + # before that stream has been opened with a HEADERS frame, the + # ALTSVC frame cannot create a stream. If the stream is not + # present, we simply ignore the frame. + try: + stream = self._get_stream_by_id(frame.stream_id) + except (NoSuchStreamError, StreamClosedError): + pass + else: + stream_frames, stream_events = stream.receive_alt_svc(frame) + frames.extend(stream_frames) + events.extend(stream_events) + else: + # This frame is sent on stream 0. The origin field on the frame + # must be present, though if it isn't it's not a ProtocolError + # (annoyingly), we just need to ignore it. + if not frame.origin: + return frames, events + + # If we're a server, we want to ignore this (RFC 7838 says so). + if not self.config.client_side: + return frames, events + + event = AlternativeServiceAvailable() + event.origin = frame.origin + event.field_value = frame.field + events.append(event) + + return frames, events + + def _receive_unknown_frame(self, frame): + """ + We have received a frame that we do not understand. This is almost + certainly an extension frame, though it's impossible to be entirely + sure. + + RFC 7540 § 5.5 says that we MUST ignore unknown frame types: so we + do. + """ + # All we do here is log. + self.config.logger.debug( + "Received unknown extension frame (ID %d)", frame.stream_id + ) + return [], [] + + def _local_settings_acked(self): + """ + Handle the local settings being ACKed, update internal state. + """ + changes = self.local_settings.acknowledge() + + if SettingCodes.INITIAL_WINDOW_SIZE in changes: + setting = changes[SettingCodes.INITIAL_WINDOW_SIZE] + self._inbound_flow_control_change_from_settings( + setting.original_value, + setting.new_value, + ) + + if SettingCodes.MAX_HEADER_LIST_SIZE in changes: + setting = changes[SettingCodes.MAX_HEADER_LIST_SIZE] + self.decoder.max_header_list_size = setting.new_value + + if SettingCodes.MAX_FRAME_SIZE in changes: + setting = changes[SettingCodes.MAX_FRAME_SIZE] + self.max_inbound_frame_size = setting.new_value + + if SettingCodes.HEADER_TABLE_SIZE in changes: + setting = changes[SettingCodes.HEADER_TABLE_SIZE] + # This is safe across all hpack versions: some versions just won't + # respect it. + self.decoder.max_allowed_table_size = setting.new_value + + return changes + + def _stream_id_is_outbound(self, stream_id): + """ + Returns ``True`` if the stream ID corresponds to an outbound stream + (one initiated by this peer), returns ``False`` otherwise. + """ + return (stream_id % 2 == int(self.config.client_side)) + + def _stream_closed_by(self, stream_id): + """ + Returns how the stream was closed. + + The return value will be either a member of + ``h2.stream.StreamClosedBy`` or ``None``. If ``None``, the stream was + closed implicitly by the peer opening a stream with a higher stream ID + before opening this one. + """ + if stream_id in self.streams: + return self.streams[stream_id].closed_by + if stream_id in self._closed_streams: + return self._closed_streams[stream_id] + return None + + def _stream_is_closed_by_reset(self, stream_id): + """ + Returns ``True`` if the stream was closed by sending or receiving a + RST_STREAM frame. Returns ``False`` otherwise. + """ + return self._stream_closed_by(stream_id) in ( + StreamClosedBy.RECV_RST_STREAM, StreamClosedBy.SEND_RST_STREAM + ) + + def _stream_is_closed_by_end(self, stream_id): + """ + Returns ``True`` if the stream was closed by sending or receiving an + END_STREAM flag in a HEADERS or DATA frame. Returns ``False`` + otherwise. + """ + return self._stream_closed_by(stream_id) in ( + StreamClosedBy.RECV_END_STREAM, StreamClosedBy.SEND_END_STREAM + ) + + +def _add_frame_priority(frame, weight=None, depends_on=None, exclusive=None): + """ + Adds priority data to a given frame. Does not change any flags set on that + frame: if the caller is adding priority information to a HEADERS frame they + must set that themselves. + + This method also deliberately sets defaults for anything missing. + + This method validates the input values. + """ + # A stream may not depend on itself. + if depends_on == frame.stream_id: + raise ProtocolError( + "Stream %d may not depend on itself" % frame.stream_id + ) + + # Weight must be between 1 and 256. + if weight is not None: + if weight > 256 or weight < 1: + raise ProtocolError( + "Weight must be between 1 and 256, not %d" % weight + ) + else: + # Weight is an integer between 1 and 256, but the byte only allows + # 0 to 255: subtract one. + weight -= 1 + + # Set defaults for anything not provided. + weight = weight if weight is not None else 15 + depends_on = depends_on if depends_on is not None else 0 + exclusive = exclusive if exclusive is not None else False + + frame.stream_weight = weight + frame.depends_on = depends_on + frame.exclusive = exclusive + + return frame + + +def _decode_headers(decoder, encoded_header_block): + """ + Decode a HPACK-encoded header block, translating HPACK exceptions into + sensible hyper-h2 errors. + + This only ever returns bytestring headers: hyper-h2 may emit them as + unicode later, but internally it processes them as bytestrings only. + """ + try: + return decoder.decode(encoded_header_block, raw=True) + except OversizedHeaderListError as e: + # This is a symptom of a HPACK bomb attack: the user has + # disregarded our requirements on how large a header block we'll + # accept. + raise DenialOfServiceError("Oversized header block: %s" % e) + except (HPACKError, IndexError, TypeError, UnicodeDecodeError) as e: + # We should only need HPACKError here, but versions of HPACK older + # than 2.1.0 throw all three others as well. For maximum + # compatibility, catch all of them. + raise ProtocolError("Error decoding header block: %s" % e) diff --git a/vinetrimmer/vendor/h2/errors.py b/vinetrimmer/vendor/h2/errors.py new file mode 100644 index 0000000..1198b3b --- /dev/null +++ b/vinetrimmer/vendor/h2/errors.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +""" +h2/errors +~~~~~~~~~~~~~~~~~~~ + +Global error code registry containing the established HTTP/2 error codes. + +The current registry is available at: +https://tools.ietf.org/html/rfc7540#section-11.4 +""" +import enum + + +class ErrorCodes(enum.IntEnum): + """ + All known HTTP/2 error codes. + + .. versionadded:: 2.5.0 + """ + #: Graceful shutdown. + NO_ERROR = 0x0 + + #: Protocol error detected. + PROTOCOL_ERROR = 0x1 + + #: Implementation fault. + INTERNAL_ERROR = 0x2 + + #: Flow-control limits exceeded. + FLOW_CONTROL_ERROR = 0x3 + + #: Settings not acknowledged. + SETTINGS_TIMEOUT = 0x4 + + #: Frame received for closed stream. + STREAM_CLOSED = 0x5 + + #: Frame size incorrect. + FRAME_SIZE_ERROR = 0x6 + + #: Stream not processed. + REFUSED_STREAM = 0x7 + + #: Stream cancelled. + CANCEL = 0x8 + + #: Compression state not updated. + COMPRESSION_ERROR = 0x9 + + #: TCP connection error for CONNECT method. + CONNECT_ERROR = 0xa + + #: Processing capacity exceeded. + ENHANCE_YOUR_CALM = 0xb + + #: Negotiated TLS parameters not acceptable. + INADEQUATE_SECURITY = 0xc + + #: Use HTTP/1.1 for the request. + HTTP_1_1_REQUIRED = 0xd + + +def _error_code_from_int(code): + """ + Given an integer error code, returns either one of :class:`ErrorCodes + <h2.errors.ErrorCodes>` or, if not present in the known set of codes, + returns the integer directly. + """ + try: + return ErrorCodes(code) + except ValueError: + return code + + +#: Graceful shutdown. +#: +#: .. deprecated:: 2.5.0 +#: Deprecated in favour of :class:`ErrorCodes.NO_ERROR +#: <h2.errors.ErrorCodes.NO_ERROR>`. +NO_ERROR = ErrorCodes.NO_ERROR + +#: Protocol error detected. +#: +#: .. deprecated:: 2.5.0 +#: Deprecated in favour of :class:`ErrorCodes.PROTOCOL_ERROR +#: <h2.errors.ErrorCodes.PROTOCOL_ERROR>`. +PROTOCOL_ERROR = ErrorCodes.PROTOCOL_ERROR + +#: Implementation fault. +#: +#: .. deprecated:: 2.5.0 +#: Deprecated in favour of :class:`ErrorCodes.INTERNAL_ERROR +#: <h2.errors.ErrorCodes.INTERNAL_ERROR>`. +INTERNAL_ERROR = ErrorCodes.INTERNAL_ERROR + +#: Flow-control limits exceeded. +#: +#: .. deprecated:: 2.5.0 +#: Deprecated in favour of :class:`ErrorCodes.FLOW_CONTROL_ERROR +#: <h2.errors.ErrorCodes.FLOW_CONTROL_ERROR>`. +FLOW_CONTROL_ERROR = ErrorCodes.FLOW_CONTROL_ERROR + +#: Settings not acknowledged. +#: +#: .. deprecated:: 2.5.0 +#: Deprecated in favour of :class:`ErrorCodes.SETTINGS_TIMEOUT +#: <h2.errors.ErrorCodes.SETTINGS_TIMEOUT>`. +SETTINGS_TIMEOUT = ErrorCodes.SETTINGS_TIMEOUT + +#: Frame received for closed stream. +#: +#: .. deprecated:: 2.5.0 +#: Deprecated in favour of :class:`ErrorCodes.STREAM_CLOSED +#: <h2.errors.ErrorCodes.STREAM_CLOSED>`. +STREAM_CLOSED = ErrorCodes.STREAM_CLOSED + +#: Frame size incorrect. +#: +#: .. deprecated:: 2.5.0 +#: Deprecated in favour of :class:`ErrorCodes.FRAME_SIZE_ERROR +#: <h2.errors.ErrorCodes.FRAME_SIZE_ERROR>`. +FRAME_SIZE_ERROR = ErrorCodes.FRAME_SIZE_ERROR + +#: Stream not processed. +#: +#: .. deprecated:: 2.5.0 +#: Deprecated in favour of :class:`ErrorCodes.REFUSED_STREAM +#: <h2.errors.ErrorCodes.REFUSED_STREAM>`. +REFUSED_STREAM = ErrorCodes.REFUSED_STREAM + +#: Stream cancelled. +#: +#: .. deprecated:: 2.5.0 +#: Deprecated in favour of :class:`ErrorCodes.CANCEL +#: <h2.errors.ErrorCodes.CANCEL>`. +CANCEL = ErrorCodes.CANCEL + +#: Compression state not updated. +#: +#: .. deprecated:: 2.5.0 +#: Deprecated in favour of :class:`ErrorCodes.COMPRESSION_ERROR +#: <h2.errors.ErrorCodes.COMPRESSION_ERROR>`. +COMPRESSION_ERROR = ErrorCodes.COMPRESSION_ERROR + +#: TCP connection error for CONNECT method. +#: +#: .. deprecated:: 2.5.0 +#: Deprecated in favour of :class:`ErrorCodes.CONNECT_ERROR +#: <h2.errors.ErrorCodes.CONNECT_ERROR>`. +CONNECT_ERROR = ErrorCodes.CONNECT_ERROR + +#: Processing capacity exceeded. +#: +#: .. deprecated:: 2.5.0 +#: Deprecated in favour of :class:`ErrorCodes.ENHANCE_YOUR_CALM +#: <h2.errors.ErrorCodes.ENHANCE_YOUR_CALM>`. +ENHANCE_YOUR_CALM = ErrorCodes.ENHANCE_YOUR_CALM + +#: Negotiated TLS parameters not acceptable. +#: +#: .. deprecated:: 2.5.0 +#: Deprecated in favour of :class:`ErrorCodes.INADEQUATE_SECURITY +#: <h2.errors.ErrorCodes.INADEQUATE_SECURITY>`. +INADEQUATE_SECURITY = ErrorCodes.INADEQUATE_SECURITY + +#: Use HTTP/1.1 for the request. +#: +#: .. deprecated:: 2.5.0 +#: Deprecated in favour of :class:`ErrorCodes.HTTP_1_1_REQUIRED +#: <h2.errors.ErrorCodes.HTTP_1_1_REQUIRED>`. +HTTP_1_1_REQUIRED = ErrorCodes.HTTP_1_1_REQUIRED + +#: All known HTTP/2 error codes. +#: +#: .. deprecated:: 2.5.0 +#: Deprecated in favour of :class:`ErrorCodes <h2.errors.ErrorCodes>`. +H2_ERRORS = list(ErrorCodes) + +__all__ = ['H2_ERRORS', 'NO_ERROR', 'PROTOCOL_ERROR', 'INTERNAL_ERROR', + 'FLOW_CONTROL_ERROR', 'SETTINGS_TIMEOUT', 'STREAM_CLOSED', + 'FRAME_SIZE_ERROR', 'REFUSED_STREAM', 'CANCEL', 'COMPRESSION_ERROR', + 'CONNECT_ERROR', 'ENHANCE_YOUR_CALM', 'INADEQUATE_SECURITY', + 'HTTP_1_1_REQUIRED', 'ErrorCodes'] diff --git a/vinetrimmer/vendor/h2/events.py b/vinetrimmer/vendor/h2/events.py new file mode 100644 index 0000000..55f2021 --- /dev/null +++ b/vinetrimmer/vendor/h2/events.py @@ -0,0 +1,597 @@ +# -*- coding: utf-8 -*- +""" +h2/events +~~~~~~~~~ + +Defines Event types for HTTP/2. + +Events are returned by the H2 state machine to allow implementations to keep +track of events triggered by receiving data. Each time data is provided to the +H2 state machine it processes the data and returns a list of Event objects. +""" +import binascii + +from .settings import ChangedSetting, _setting_code_from_int + + +class Event(object): + """ + Base class for h2 events. + """ + pass + + +class RequestReceived(Event): + """ + The RequestReceived event is fired whenever request headers are received. + This event carries the HTTP headers for the given request and the stream ID + of the new stream. + + .. versionchanged:: 2.3.0 + Changed the type of ``headers`` to :class:`HeaderTuple + <hpack:hpack.HeaderTuple>`. This has no effect on current users. + + .. versionchanged:: 2.4.0 + Added ``stream_ended`` and ``priority_updated`` properties. + """ + def __init__(self): + #: The Stream ID for the stream this request was made on. + self.stream_id = None + + #: The request headers. + self.headers = None + + #: If this request also ended the stream, the associated + #: :class:`StreamEnded <h2.events.StreamEnded>` event will be available + #: here. + #: + #: .. versionadded:: 2.4.0 + self.stream_ended = None + + #: If this request also had associated priority information, the + #: associated :class:`PriorityUpdated <h2.events.PriorityUpdated>` + #: event will be available here. + #: + #: .. versionadded:: 2.4.0 + self.priority_updated = None + + def __repr__(self): + return "<RequestReceived stream_id:%s, headers:%s>" % ( + self.stream_id, self.headers + ) + + +class ResponseReceived(Event): + """ + The ResponseReceived event is fired whenever response headers are received. + This event carries the HTTP headers for the given response and the stream + ID of the new stream. + + .. versionchanged:: 2.3.0 + Changed the type of ``headers`` to :class:`HeaderTuple + <hpack:hpack.HeaderTuple>`. This has no effect on current users. + + .. versionchanged:: 2.4.0 + Added ``stream_ended`` and ``priority_updated`` properties. + """ + def __init__(self): + #: The Stream ID for the stream this response was made on. + self.stream_id = None + + #: The response headers. + self.headers = None + + #: If this response also ended the stream, the associated + #: :class:`StreamEnded <h2.events.StreamEnded>` event will be available + #: here. + #: + #: .. versionadded:: 2.4.0 + self.stream_ended = None + + #: If this response also had associated priority information, the + #: associated :class:`PriorityUpdated <h2.events.PriorityUpdated>` + #: event will be available here. + #: + #: .. versionadded:: 2.4.0 + self.priority_updated = None + + def __repr__(self): + return "<ResponseReceived stream_id:%s, headers:%s>" % ( + self.stream_id, self.headers + ) + + +class TrailersReceived(Event): + """ + The TrailersReceived event is fired whenever trailers are received on a + stream. Trailers are a set of headers sent after the body of the + request/response, and are used to provide information that wasn't known + ahead of time (e.g. content-length). This event carries the HTTP header + fields that form the trailers and the stream ID of the stream on which they + were received. + + .. versionchanged:: 2.3.0 + Changed the type of ``headers`` to :class:`HeaderTuple + <hpack:hpack.HeaderTuple>`. This has no effect on current users. + + .. versionchanged:: 2.4.0 + Added ``stream_ended`` and ``priority_updated`` properties. + """ + def __init__(self): + #: The Stream ID for the stream on which these trailers were received. + self.stream_id = None + + #: The trailers themselves. + self.headers = None + + #: Trailers always end streams. This property has the associated + #: :class:`StreamEnded <h2.events.StreamEnded>` in it. + #: + #: .. versionadded:: 2.4.0 + self.stream_ended = None + + #: If the trailers also set associated priority information, the + #: associated :class:`PriorityUpdated <h2.events.PriorityUpdated>` + #: event will be available here. + #: + #: .. versionadded:: 2.4.0 + self.priority_updated = None + + def __repr__(self): + return "<TrailersReceived stream_id:%s, headers:%s>" % ( + self.stream_id, self.headers + ) + + +class _HeadersSent(Event): + """ + The _HeadersSent event is fired whenever headers are sent. + + This is an internal event, used to determine validation steps on + outgoing header blocks. + """ + pass + + +class _ResponseSent(_HeadersSent): + """ + The _ResponseSent event is fired whenever response headers are sent + on a stream. + + This is an internal event, used to determine validation steps on + outgoing header blocks. + """ + pass + + +class _RequestSent(_HeadersSent): + """ + The _RequestSent event is fired whenever request headers are sent + on a stream. + + This is an internal event, used to determine validation steps on + outgoing header blocks. + """ + pass + + +class _TrailersSent(_HeadersSent): + """ + The _TrailersSent event is fired whenever trailers are sent on a + stream. Trailers are a set of headers sent after the body of the + request/response, and are used to provide information that wasn't known + ahead of time (e.g. content-length). + + This is an internal event, used to determine validation steps on + outgoing header blocks. + """ + pass + + +class _PushedRequestSent(_HeadersSent): + """ + The _PushedRequestSent event is fired whenever pushed request headers are + sent. + + This is an internal event, used to determine validation steps on outgoing + header blocks. + """ + pass + + +class InformationalResponseReceived(Event): + """ + The InformationalResponseReceived event is fired when an informational + response (that is, one whose status code is a 1XX code) is received from + the remote peer. + + The remote peer may send any number of these, from zero upwards. These + responses are most commonly sent in response to requests that have the + ``expect: 100-continue`` header field present. Most users can safely + ignore this event unless you are intending to use the + ``expect: 100-continue`` flow, or are for any reason expecting a different + 1XX status code. + + .. versionadded:: 2.2.0 + + .. versionchanged:: 2.3.0 + Changed the type of ``headers`` to :class:`HeaderTuple + <hpack:hpack.HeaderTuple>`. This has no effect on current users. + + .. versionchanged:: 2.4.0 + Added ``priority_updated`` property. + """ + def __init__(self): + #: The Stream ID for the stream this informational response was made + #: on. + self.stream_id = None + + #: The headers for this informational response. + self.headers = None + + #: If this response also had associated priority information, the + #: associated :class:`PriorityUpdated <h2.events.PriorityUpdated>` + #: event will be available here. + #: + #: .. versionadded:: 2.4.0 + self.priority_updated = None + + def __repr__(self): + return "<InformationalResponseReceived stream_id:%s, headers:%s>" % ( + self.stream_id, self.headers + ) + + +class DataReceived(Event): + """ + The DataReceived event is fired whenever data is received on a stream from + the remote peer. The event carries the data itself, and the stream ID on + which the data was received. + + .. versionchanged:: 2.4.0 + Added ``stream_ended`` property. + """ + def __init__(self): + #: The Stream ID for the stream this data was received on. + self.stream_id = None + + #: The data itself. + self.data = None + + #: The amount of data received that counts against the flow control + #: window. Note that padding counts against the flow control window, so + #: when adjusting flow control you should always use this field rather + #: than ``len(data)``. + self.flow_controlled_length = None + + #: If this data chunk also completed the stream, the associated + #: :class:`StreamEnded <h2.events.StreamEnded>` event will be available + #: here. + #: + #: .. versionadded:: 2.4.0 + self.stream_ended = None + + def __repr__(self): + return ( + "<DataReceived stream_id:%s, " + "flow_controlled_length:%s, " + "data:%s>" % ( + self.stream_id, + self.flow_controlled_length, + _bytes_representation(self.data[:20]), + ) + ) + + +class WindowUpdated(Event): + """ + The WindowUpdated event is fired whenever a flow control window changes + size. HTTP/2 defines flow control windows for connections and streams: this + event fires for both connections and streams. The event carries the ID of + the stream to which it applies (set to zero if the window update applies to + the connection), and the delta in the window size. + """ + def __init__(self): + #: The Stream ID of the stream whose flow control window was changed. + #: May be ``0`` if the connection window was changed. + self.stream_id = None + + #: The window delta. + self.delta = None + + def __repr__(self): + return "<WindowUpdated stream_id:%s, delta:%s>" % ( + self.stream_id, self.delta + ) + + +class RemoteSettingsChanged(Event): + """ + The RemoteSettingsChanged event is fired whenever the remote peer changes + its settings. It contains a complete inventory of changed settings, + including their previous values. + + In HTTP/2, settings changes need to be acknowledged. hyper-h2 automatically + acknowledges settings changes for efficiency. However, it is possible that + the caller may not be happy with the changed setting. + + When this event is received, the caller should confirm that the new + settings are acceptable. If they are not acceptable, the user should close + the connection with the error code :data:`PROTOCOL_ERROR + <h2.errors.ErrorCodes.PROTOCOL_ERROR>`. + + .. versionchanged:: 2.0.0 + Prior to this version the user needed to acknowledge settings changes. + This is no longer the case: hyper-h2 now automatically acknowledges + them. + """ + def __init__(self): + #: A dictionary of setting byte to + #: :class:`ChangedSetting <h2.settings.ChangedSetting>`, representing + #: the changed settings. + self.changed_settings = {} + + @classmethod + def from_settings(cls, old_settings, new_settings): + """ + Build a RemoteSettingsChanged event from a set of changed settings. + + :param old_settings: A complete collection of old settings, in the form + of a dictionary of ``{setting: value}``. + :param new_settings: All the changed settings and their new values, in + the form of a dictionary of ``{setting: value}``. + """ + e = cls() + for setting, new_value in new_settings.items(): + setting = _setting_code_from_int(setting) + original_value = old_settings.get(setting) + change = ChangedSetting(setting, original_value, new_value) + e.changed_settings[setting] = change + + return e + + def __repr__(self): + return "<RemoteSettingsChanged changed_settings:{%s}>" % ( + ", ".join(repr(cs) for cs in self.changed_settings.values()), + ) + + +class PingAcknowledged(Event): + """ + The PingAcknowledged event is fired whenever a user-emitted PING is + acknowledged. This contains the data in the ACK'ed PING, allowing the + user to correlate PINGs and calculate RTT. + """ + def __init__(self): + #: The data included on the ping. + self.ping_data = None + + def __repr__(self): + return "<PingAcknowledged ping_data:%s>" % ( + _bytes_representation(self.ping_data), + ) + + +class StreamEnded(Event): + """ + The StreamEnded event is fired whenever a stream is ended by a remote + party. The stream may not be fully closed if it has not been closed + locally, but no further data or headers should be expected on that stream. + """ + def __init__(self): + #: The Stream ID of the stream that was closed. + self.stream_id = None + + def __repr__(self): + return "<StreamEnded stream_id:%s>" % self.stream_id + + +class StreamReset(Event): + """ + The StreamReset event is fired in two situations. The first is when the + remote party forcefully resets the stream. The second is when the remote + party has made a protocol error which only affects a single stream. In this + case, Hyper-h2 will terminate the stream early and return this event. + + .. versionchanged:: 2.0.0 + This event is now fired when Hyper-h2 automatically resets a stream. + """ + def __init__(self): + #: The Stream ID of the stream that was reset. + self.stream_id = None + + #: The error code given. Either one of :class:`ErrorCodes + #: <h2.errors.ErrorCodes>` or ``int`` + self.error_code = None + + #: Whether the remote peer sent a RST_STREAM or we did. + self.remote_reset = True + + def __repr__(self): + return "<StreamReset stream_id:%s, error_code:%s, remote_reset:%s>" % ( + self.stream_id, self.error_code, self.remote_reset + ) + + +class PushedStreamReceived(Event): + """ + The PushedStreamReceived event is fired whenever a pushed stream has been + received from a remote peer. The event carries on it the new stream ID, the + ID of the parent stream, and the request headers pushed by the remote peer. + """ + def __init__(self): + #: The Stream ID of the stream created by the push. + self.pushed_stream_id = None + + #: The Stream ID of the stream that the push is related to. + self.parent_stream_id = None + + #: The request headers, sent by the remote party in the push. + self.headers = None + + def __repr__(self): + return ( + "<PushedStreamReceived pushed_stream_id:%s, parent_stream_id:%s, " + "headers:%s>" % ( + self.pushed_stream_id, + self.parent_stream_id, + self.headers, + ) + ) + + +class SettingsAcknowledged(Event): + """ + The SettingsAcknowledged event is fired whenever a settings ACK is received + from the remote peer. The event carries on it the settings that were + acknowedged, in the same format as + :class:`h2.events.RemoteSettingsChanged`. + """ + def __init__(self): + #: A dictionary of setting byte to + #: :class:`ChangedSetting <h2.settings.ChangedSetting>`, representing + #: the changed settings. + self.changed_settings = {} + + def __repr__(self): + return "<SettingsAcknowledged changed_settings:{%s}>" % ( + ", ".join(repr(cs) for cs in self.changed_settings.values()), + ) + + +class PriorityUpdated(Event): + """ + The PriorityUpdated event is fired whenever a stream sends updated priority + information. This can occur when the stream is opened, or at any time + during the stream lifetime. + + This event is purely advisory, and does not need to be acted on. + + .. versionadded:: 2.0.0 + """ + def __init__(self): + #: The ID of the stream whose priority information is being updated. + self.stream_id = None + + #: The new stream weight. May be the same as the original stream + #: weight. An integer between 1 and 256. + self.weight = None + + #: The stream ID this stream now depends on. May be ``0``. + self.depends_on = None + + #: Whether the stream *exclusively* depends on the parent stream. If it + #: does, this stream should inherit the current children of its new + #: parent. + self.exclusive = None + + def __repr__(self): + return ( + "<PriorityUpdated stream_id:%s, weight:%s, depends_on:%s, " + "exclusive:%s>" % ( + self.stream_id, + self.weight, + self.depends_on, + self.exclusive + ) + ) + + +class ConnectionTerminated(Event): + """ + The ConnectionTerminated event is fired when a connection is torn down by + the remote peer using a GOAWAY frame. Once received, no further action may + be taken on the connection: a new connection must be established. + """ + def __init__(self): + #: The error code cited when tearing down the connection. Should be + #: one of :class:`ErrorCodes <h2.errors.ErrorCodes>`, but may not be if + #: unknown HTTP/2 extensions are being used. + self.error_code = None + + #: The stream ID of the last stream the remote peer saw. This can + #: provide an indication of what data, if any, never reached the remote + #: peer and so can safely be resent. + self.last_stream_id = None + + #: Additional debug data that can be appended to GOAWAY frame. + self.additional_data = None + + def __repr__(self): + return ( + "<ConnectionTerminated error_code:%s, last_stream_id:%s, " + "additional_data:%s>" % ( + self.error_code, + self.last_stream_id, + _bytes_representation( + self.additional_data[:20] + if self.additional_data else None) + ) + ) + + +class AlternativeServiceAvailable(Event): + """ + The AlternativeServiceAvailable event is fired when the remote peer + advertises an `RFC 7838 <https://tools.ietf.org/html/rfc7838>`_ Alternative + Service using an ALTSVC frame. + + This event always carries the origin to which the ALTSVC information + applies. That origin is either supplied by the server directly, or inferred + by hyper-h2 from the ``:authority`` pseudo-header field that was sent by + the user when initiating a given stream. + + This event also carries what RFC 7838 calls the "Alternative Service Field + Value", which is formatted like a HTTP header field and contains the + relevant alternative service information. Hyper-h2 does not parse or in any + way modify that information: the user is required to do that. + + This event can only be fired on the client end of a connection. + + .. versionadded:: 2.3.0 + """ + def __init__(self): + #: The origin to which the alternative service field value applies. + #: This field is either supplied by the server directly, or inferred by + #: hyper-h2 from the ``:authority`` pseudo-header field that was sent + #: by the user when initiating the stream on which the frame was + #: received. + self.origin = None + + #: The ALTSVC field value. This contains information about the HTTP + #: alternative service being advertised by the server. Hyper-h2 does + #: not parse this field: it is left exactly as sent by the server. The + #: structure of the data in this field is given by `RFC 7838 Section 3 + #: <https://tools.ietf.org/html/rfc7838#section-3>`_. + self.field_value = None + + def __repr__(self): + return ( + "<AlternativeServiceAvailable origin:%s, field_value:%s>" % ( + self.origin.decode('utf-8', 'ignore'), + self.field_value.decode('utf-8', 'ignore'), + ) + ) + + +def _bytes_representation(data): + """ + Converts a bytestring into something that is safe to print on all Python + platforms. + + This function is relatively expensive, so it should not be called on the + mainline of the code. It's safe to use in things like object repr methods + though. + """ + if data is None: + return None + + hex = binascii.hexlify(data) + + # This is moderately clever: on all Python versions hexlify returns a byte + # string. On Python 3 we want an actual string, so we just check whether + # that's what we have. + if not isinstance(hex, str): # pragma: no cover + hex = hex.decode('ascii') + + return hex diff --git a/vinetrimmer/vendor/h2/exceptions.py b/vinetrimmer/vendor/h2/exceptions.py new file mode 100644 index 0000000..f597975 --- /dev/null +++ b/vinetrimmer/vendor/h2/exceptions.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +""" +h2/exceptions +~~~~~~~~~~~~~ + +Exceptions for the HTTP/2 module. +""" +import vinetrimmer.vendor.h2.errors + + +class H2Error(Exception): + """ + The base class for all exceptions for the HTTP/2 module. + """ + + +class ProtocolError(H2Error): + """ + An action was attempted in violation of the HTTP/2 protocol. + """ + #: The error code corresponds to this kind of Protocol Error. + error_code = vinetrimmer.vendor.h2.errors.ErrorCodes.PROTOCOL_ERROR + + +class FrameTooLargeError(ProtocolError): + """ + The frame that we tried to send or that we received was too large. + """ + #: This error code that corresponds to this kind of Protocol Error. + error_code = vinetrimmer.vendor.h2.errors.ErrorCodes.FRAME_SIZE_ERROR + + +class FrameDataMissingError(ProtocolError): + """ + The frame that we received is missing some data. + + .. versionadded:: 2.0.0 + """ + #: The error code that corresponds to this kind of Protocol Error + error_code = vinetrimmer.vendor.h2.errors.ErrorCodes.FRAME_SIZE_ERROR + + +class TooManyStreamsError(ProtocolError): + """ + An attempt was made to open a stream that would lead to too many concurrent + streams. + """ + pass + + +class FlowControlError(ProtocolError): + """ + An attempted action violates flow control constraints. + """ + #: The error code that corresponds to this kind of + #: :class:`ProtocolError <h2.exceptions.ProtocolError>` + error_code = vinetrimmer.vendor.h2.errors.ErrorCodes.FLOW_CONTROL_ERROR + + +class StreamIDTooLowError(ProtocolError): + """ + An attempt was made to open a stream that had an ID that is lower than the + highest ID we have seen on this connection. + """ + def __init__(self, stream_id, max_stream_id): + #: The ID of the stream that we attempted to open. + self.stream_id = stream_id + + #: The current highest-seen stream ID. + self.max_stream_id = max_stream_id + + def __str__(self): + return "StreamIDTooLowError: %d is lower than %d" % ( + self.stream_id, self.max_stream_id + ) + + +class NoAvailableStreamIDError(ProtocolError): + """ + There are no available stream IDs left to the connection. All stream IDs + have been exhausted. + + .. versionadded:: 2.0.0 + """ + pass + + +class NoSuchStreamError(ProtocolError): + """ + A stream-specific action referenced a stream that does not exist. + + .. versionchanged:: 2.0.0 + Became a subclass of :class:`ProtocolError + <h2.exceptions.ProtocolError>` + """ + def __init__(self, stream_id): + #: The stream ID that corresponds to the non-existent stream. + self.stream_id = stream_id + + +class StreamClosedError(NoSuchStreamError): + """ + A more specific form of + :class:`NoSuchStreamError <h2.exceptions.NoSuchStreamError>`. Indicates + that the stream has since been closed, and that all state relating to that + stream has been removed. + """ + def __init__(self, stream_id): + #: The stream ID that corresponds to the nonexistent stream. + self.stream_id = stream_id + + #: The relevant HTTP/2 error code. + self.error_code = vinetrimmer.vendor.h2.errors.ErrorCodes.STREAM_CLOSED + + # Any events that internal code may need to fire. Not relevant to + # external users that may receive a StreamClosedError. + self._events = [] + + +class InvalidSettingsValueError(ProtocolError, ValueError): + """ + An attempt was made to set an invalid Settings value. + + .. versionadded:: 2.0.0 + """ + def __init__(self, msg, error_code): + super(InvalidSettingsValueError, self).__init__(msg) + self.error_code = error_code + + +class InvalidBodyLengthError(ProtocolError): + """ + The remote peer sent more or less data that the Content-Length header + indicated. + + .. versionadded:: 2.0.0 + """ + def __init__(self, expected, actual): + self.expected_length = expected + self.actual_length = actual + + def __str__(self): + return "InvalidBodyLengthError: Expected %d bytes, received %d" % ( + self.expected_length, self.actual_length + ) + + +class UnsupportedFrameError(ProtocolError, KeyError): + """ + The remote peer sent a frame that is unsupported in this context. + + .. versionadded:: 2.1.0 + """ + # TODO: Remove the KeyError in 3.0.0 + pass + + +class RFC1122Error(H2Error): + """ + Emitted when users attempt to do something that is literally allowed by the + relevant RFC, but is sufficiently ill-defined that it's unwise to allow + users to actually do it. + + While there is some disagreement about whether or not we should be liberal + in what accept, it is a truth universally acknowledged that we should be + conservative in what emit. + + .. versionadded:: 2.4.0 + """ + # shazow says I'm going to regret naming the exception this way. If that + # turns out to be true, TELL HIM NOTHING. + pass + + +class DenialOfServiceError(ProtocolError): + """ + Emitted when the remote peer exhibits a behaviour that is likely to be an + attempt to perform a Denial of Service attack on the implementation. This + is a form of ProtocolError that carries a different error code, and allows + more easy detection of this kind of behaviour. + + .. versionadded:: 2.5.0 + """ + #: The error code that corresponds to this kind of + #: :class:`ProtocolError <h2.exceptions.ProtocolError>` + error_code = vinetrimmer.vendor.h2.errors.ErrorCodes.ENHANCE_YOUR_CALM diff --git a/vinetrimmer/vendor/h2/frame_buffer.py b/vinetrimmer/vendor/h2/frame_buffer.py new file mode 100644 index 0000000..b622894 --- /dev/null +++ b/vinetrimmer/vendor/h2/frame_buffer.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- +""" +h2/frame_buffer +~~~~~~~~~~~~~~~ + +A data structure that provides a way to iterate over a byte buffer in terms of +frames. +""" +from vinetrimmer.vendor.hyperframe.exceptions import UnknownFrameError, InvalidFrameError +from vinetrimmer.vendor.hyperframe.frame import ( + Frame, HeadersFrame, ContinuationFrame, PushPromiseFrame +) + +from .exceptions import ( + ProtocolError, FrameTooLargeError, FrameDataMissingError +) + +# To avoid a DOS attack based on sending loads of continuation frames, we limit +# the maximum number we're perpared to receive. In this case, we'll set the +# limit to 64, which means the largest encoded header block we can receive by +# default is 262144 bytes long, and the largest possible *at all* is 1073741760 +# bytes long. +# +# This value seems reasonable for now, but in future we may want to evaluate +# making it configurable. +CONTINUATION_BACKLOG = 64 + + +class FrameBuffer(object): + """ + This is a data structure that expects to act as a buffer for HTTP/2 data + that allows iteraton in terms of H2 frames. + """ + def __init__(self, server=False): + self.data = b'' + self.max_frame_size = 0 + self._preamble = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' if server else b'' + self._preamble_len = len(self._preamble) + self._headers_buffer = [] + + def add_data(self, data): + """ + Add more data to the frame buffer. + + :param data: A bytestring containing the byte buffer. + """ + if self._preamble_len: + data_len = len(data) + of_which_preamble = min(self._preamble_len, data_len) + + if self._preamble[:of_which_preamble] != data[:of_which_preamble]: + raise ProtocolError("Invalid HTTP/2 preamble.") + + data = data[of_which_preamble:] + self._preamble_len -= of_which_preamble + self._preamble = self._preamble[of_which_preamble:] + + self.data += data + + def _parse_frame_header(self, data): + """ + Parses the frame header from the data. Either returns a tuple of + (frame, length), or throws an exception. The returned frame may be None + if the frame is of unknown type. + """ + try: + frame, length = Frame.parse_frame_header(data[:9]) + except UnknownFrameError as e: # Platform-specific: Hyperframe < 5.0 + # Here we do something a bit odd. We want to consume the frame data + # as consistently as possible, but we also don't ever want to yield + # None. Instead, we make sure that, if there is no frame, we + # recurse into ourselves. + # This can only happen now on older versions of hyperframe. + # TODO: Remove in 3.0 + length = e.length + frame = None + except ValueError as e: + # The frame header is invalid. This is a ProtocolError + raise ProtocolError("Invalid frame header received: %s" % str(e)) + + return frame, length + + def _validate_frame_length(self, length): + """ + Confirm that the frame is an appropriate length. + """ + if length > self.max_frame_size: + raise FrameTooLargeError( + "Received overlong frame: length %d, max %d" % + (length, self.max_frame_size) + ) + + def _update_header_buffer(self, f): + """ + Updates the internal header buffer. Returns a frame that should replace + the current one. May throw exceptions if this frame is invalid. + """ + # Check if we're in the middle of a headers block. If we are, this + # frame *must* be a CONTINUATION frame with the same stream ID as the + # leading HEADERS or PUSH_PROMISE frame. Anything else is a + # ProtocolError. If the frame *is* valid, append it to the header + # buffer. + if self._headers_buffer: + stream_id = self._headers_buffer[0].stream_id + valid_frame = ( + f is not None and + isinstance(f, ContinuationFrame) and + f.stream_id == stream_id + ) + if not valid_frame: + raise ProtocolError("Invalid frame during header block.") + + # Append the frame to the buffer. + self._headers_buffer.append(f) + if len(self._headers_buffer) > CONTINUATION_BACKLOG: + raise ProtocolError("Too many continuation frames received.") + + # If this is the end of the header block, then we want to build a + # mutant HEADERS frame that's massive. Use the original one we got, + # then set END_HEADERS and set its data appopriately. If it's not + # the end of the block, lose the current frame: we can't yield it. + if 'END_HEADERS' in f.flags: + f = self._headers_buffer[0] + f.flags.add('END_HEADERS') + f.data = b''.join(x.data for x in self._headers_buffer) + self._headers_buffer = [] + else: + f = None + elif (isinstance(f, (HeadersFrame, PushPromiseFrame)) and + 'END_HEADERS' not in f.flags): + # This is the start of a headers block! Save the frame off and then + # act like we didn't receive one. + self._headers_buffer.append(f) + f = None + + return f + + # The methods below support the iterator protocol. + def __iter__(self): + return self + + def next(self): # Python 2 + # First, check that we have enough data to successfully parse the + # next frame header. If not, bail. Otherwise, parse it. + if len(self.data) < 9: + raise StopIteration() + + try: + f, length = self._parse_frame_header(self.data) + except InvalidFrameError: # pragma: no cover + raise ProtocolError("Received frame with invalid frame header.") + + # Next, check that we have enough length to parse the frame body. If + # not, bail, leaving the frame header data in the buffer for next time. + if len(self.data) < length + 9: + raise StopIteration() + + # Confirm the frame has an appropriate length. + self._validate_frame_length(length) + + # Don't try to parse the body if we didn't get a frame we know about: + # there's nothing we can do with it anyway. + if f is not None: + try: + f.parse_body(memoryview(self.data[9:9+length])) + except InvalidFrameError: + raise FrameDataMissingError("Frame data missing or invalid") + + # At this point, as we know we'll use or discard the entire frame, we + # can update the data. + self.data = self.data[9+length:] + + # Pass the frame through the header buffer. + f = self._update_header_buffer(f) + + # If we got a frame we didn't understand or shouldn't yield, rather + # than return None it'd be better if we just tried to get the next + # frame in the sequence instead. Recurse back into ourselves to do + # that. This is safe because the amount of work we have to do here is + # strictly bounded by the length of the buffer. + return f if f is not None else self.next() + + def __next__(self): # Python 3 + return self.next() diff --git a/vinetrimmer/vendor/h2/settings.py b/vinetrimmer/vendor/h2/settings.py new file mode 100644 index 0000000..f7fa18d --- /dev/null +++ b/vinetrimmer/vendor/h2/settings.py @@ -0,0 +1,376 @@ +# -*- coding: utf-8 -*- +""" +h2/settings +~~~~~~~~~~~ + +This module contains a HTTP/2 settings object. This object provides a simple +API for manipulating HTTP/2 settings, keeping track of both the current active +state of the settings and the unacknowledged future values of the settings. +""" +import collections +import enum + +from vinetrimmer.vendor.hyperframe.frame import SettingsFrame + +from vinetrimmer.vendor.h2.errors import ErrorCodes +from vinetrimmer.vendor.h2.exceptions import InvalidSettingsValueError + + +class SettingCodes(enum.IntEnum): + """ + All known HTTP/2 setting codes. + + .. versionadded:: 2.6.0 + """ + + #: Allows the sender to inform the remote endpoint of the maximum size of + #: the header compression table used to decode header blocks, in octets. + HEADER_TABLE_SIZE = SettingsFrame.HEADER_TABLE_SIZE + + #: This setting can be used to disable server push. To disable server push + #: on a client, set this to 0. + ENABLE_PUSH = SettingsFrame.ENABLE_PUSH + + #: Indicates the maximum number of concurrent streams that the sender will + #: allow. + MAX_CONCURRENT_STREAMS = SettingsFrame.MAX_CONCURRENT_STREAMS + + #: Indicates the sender's initial window size (in octets) for stream-level + #: flow control. + INITIAL_WINDOW_SIZE = SettingsFrame.INITIAL_WINDOW_SIZE + + try: # Platform-specific: Hyperframe < 4.0.0 + _max_frame_size = SettingsFrame.SETTINGS_MAX_FRAME_SIZE + except AttributeError: # Platform-specific: Hyperframe >= 4.0.0 + _max_frame_size = SettingsFrame.MAX_FRAME_SIZE + + #: Indicates the size of the largest frame payload that the sender is + #: willing to receive, in octets. + MAX_FRAME_SIZE = _max_frame_size + + try: # Platform-specific: Hyperframe < 4.0.0 + _max_header_list_size = SettingsFrame.SETTINGS_MAX_HEADER_LIST_SIZE + except AttributeError: # Platform-specific: Hyperframe >= 4.0.0 + _max_header_list_size = SettingsFrame.MAX_HEADER_LIST_SIZE + + #: This advisory setting informs a peer of the maximum size of header list + #: that the sender is prepared to accept, in octets. The value is based on + #: the uncompressed size of header fields, including the length of the name + #: and value in octets plus an overhead of 32 octets for each header field. + MAX_HEADER_LIST_SIZE = _max_header_list_size + + +def _setting_code_from_int(code): + """ + Given an integer setting code, returns either one of :class:`SettingCodes + <h2.settings.SettingCodes>` or, if not present in the known set of codes, + returns the integer directly. + """ + try: + return SettingCodes(code) + except ValueError: + return code + + +# Aliases for all the settings values. + +#: Allows the sender to inform the remote endpoint of the maximum size of the +#: header compression table used to decode header blocks, in octets. +#: +#: .. deprecated:: 2.6.0 +#: Deprecated in favour of :data:`SettingCodes.HEADER_TABLE_SIZE +#: <h2.settings.SettingCodes.HEADER_TABLE_SIZE>`. +HEADER_TABLE_SIZE = SettingCodes.HEADER_TABLE_SIZE + +#: This setting can be used to disable server push. To disable server push on +#: a client, set this to 0. +#: +#: .. deprecated:: 2.6.0 +#: Deprecated in favour of :data:`SettingCodes.ENABLE_PUSH +#: <h2.settings.SettingCodes.ENABLE_PUSH>`. +ENABLE_PUSH = SettingCodes.ENABLE_PUSH + +#: Indicates the maximum number of concurrent streams that the sender will +#: allow. +#: +#: .. deprecated:: 2.6.0 +#: Deprecated in favour of :data:`SettingCodes.MAX_CONCURRENT_STREAMS +#: <h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS>`. +MAX_CONCURRENT_STREAMS = SettingCodes.MAX_CONCURRENT_STREAMS + +#: Indicates the sender's initial window size (in octets) for stream-level flow +#: control. +#: +#: .. deprecated:: 2.6.0 +#: Deprecated in favour of :data:`SettingCodes.INITIAL_WINDOW_SIZE +#: <h2.settings.SettingCodes.INITIAL_WINDOW_SIZE>`. +INITIAL_WINDOW_SIZE = SettingCodes.INITIAL_WINDOW_SIZE + +#: Indicates the size of the largest frame payload that the sender is willing +#: to receive, in octets. +#: +#: .. deprecated:: 2.6.0 +#: Deprecated in favour of :data:`SettingCodes.MAX_FRAME_SIZE +#: <h2.settings.SettingCodes.MAX_FRAME_SIZE>`. +MAX_FRAME_SIZE = SettingCodes.MAX_FRAME_SIZE + +#: This advisory setting informs a peer of the maximum size of header list that +#: the sender is prepared to accept, in octets. The value is based on the +#: uncompressed size of header fields, including the length of the name and +#: value in octets plus an overhead of 32 octets for each header field. +#: +#: .. deprecated:: 2.6.0 +#: Deprecated in favour of :data:`SettingCodes.MAX_HEADER_LIST_SIZE +#: <h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE>`. +MAX_HEADER_LIST_SIZE = SettingCodes.MAX_HEADER_LIST_SIZE + + +class ChangedSetting: + + def __init__(self, setting, original_value, new_value): + #: The setting code given. Either one of :class:`SettingCodes + #: <h2.settings.SettingCodes>` or ``int`` + #: + #: .. versionchanged:: 2.6.0 + self.setting = setting + + #: The original value before being changed. + self.original_value = original_value + + #: The new value after being changed. + self.new_value = new_value + + def __repr__(self): + return ( + "ChangedSetting(setting=%s, original_value=%s, " + "new_value=%s)" + ) % ( + self.setting, + self.original_value, + self.new_value + ) + + +class Settings(collections.abc.MutableMapping): + """ + An object that encapsulates HTTP/2 settings state. + + HTTP/2 Settings are a complex beast. Each party, remote and local, has its + own settings and a view of the other party's settings. When a settings + frame is emitted by a peer it cannot assume that the new settings values + are in place until the remote peer acknowledges the setting. In principle, + multiple settings changes can be "in flight" at the same time, all with + different values. + + This object encapsulates this mess. It provides a dict-like interface to + settings, which return the *current* values of the settings in question. + Additionally, it keeps track of the stack of proposed values: each time an + acknowledgement is sent/received, it updates the current values with the + stack of proposed values. On top of all that, it validates the values to + make sure they're allowed, and raises :class:`InvalidSettingsValueError + <h2.exceptions.InvalidSettingsValueError>` if they are not. + + Finally, this object understands what the default values of the HTTP/2 + settings are, and sets those defaults appropriately. + + .. versionchanged:: 2.2.0 + Added the ``initial_values`` parameter. + + .. versionchanged:: 2.5.0 + Added the ``max_header_list_size`` property. + + :param client: (optional) Whether these settings should be defaulted for a + client implementation or a server implementation. Defaults to ``True``. + :type client: ``bool`` + :param initial_values: (optional) Any initial values the user would like + set, rather than RFC 7540's defaults. + :type initial_vales: ``MutableMapping`` + """ + def __init__(self, client=True, initial_values=None): + # Backing object for the settings. This is a dictionary of + # (setting: [list of values]), where the first value in the list is the + # current value of the setting. Strictly this doesn't use lists but + # instead uses collections.deque to avoid repeated memory allocations. + # + # This contains the default values for HTTP/2. + self._settings = { + SettingCodes.HEADER_TABLE_SIZE: collections.deque([4096]), + SettingCodes.ENABLE_PUSH: collections.deque([int(client)]), + SettingCodes.INITIAL_WINDOW_SIZE: collections.deque([65535]), + SettingCodes.MAX_FRAME_SIZE: collections.deque([16384]), + } + if initial_values is not None: + for key, value in initial_values.items(): + invalid = _validate_setting(key, value) + if invalid: + raise InvalidSettingsValueError( + "Setting %d has invalid value %d" % (key, value), + error_code=invalid + ) + self._settings[key] = collections.deque([value]) + + def acknowledge(self): + """ + The settings have been acknowledged, either by the user (remote + settings) or by the remote peer (local settings). + + :returns: A dict of {setting: ChangedSetting} that were applied. + """ + changed_settings = {} + + # If there is more than one setting in the list, we have a setting + # value outstanding. Update them. + for k, v in self._settings.items(): + if len(v) > 1: + old_setting = v.popleft() + new_setting = v[0] + changed_settings[k] = ChangedSetting( + k, old_setting, new_setting + ) + + return changed_settings + + # Provide easy-access to well known settings. + @property + def header_table_size(self): + """ + The current value of the :data:`HEADER_TABLE_SIZE + <h2.settings.SettingCodes.HEADER_TABLE_SIZE>` setting. + """ + return self[SettingCodes.HEADER_TABLE_SIZE] + + @header_table_size.setter + def header_table_size(self, value): + self[SettingCodes.HEADER_TABLE_SIZE] = value + + @property + def enable_push(self): + """ + The current value of the :data:`ENABLE_PUSH + <h2.settings.SettingCodes.ENABLE_PUSH>` setting. + """ + return self[SettingCodes.ENABLE_PUSH] + + @enable_push.setter + def enable_push(self, value): + self[SettingCodes.ENABLE_PUSH] = value + + @property + def initial_window_size(self): + """ + The current value of the :data:`INITIAL_WINDOW_SIZE + <h2.settings.SettingCodes.INITIAL_WINDOW_SIZE>` setting. + """ + return self[SettingCodes.INITIAL_WINDOW_SIZE] + + @initial_window_size.setter + def initial_window_size(self, value): + self[SettingCodes.INITIAL_WINDOW_SIZE] = value + + @property + def max_frame_size(self): + """ + The current value of the :data:`MAX_FRAME_SIZE + <h2.settings.SettingCodes.MAX_FRAME_SIZE>` setting. + """ + return self[SettingCodes.MAX_FRAME_SIZE] + + @max_frame_size.setter + def max_frame_size(self, value): + self[SettingCodes.MAX_FRAME_SIZE] = value + + @property + def max_concurrent_streams(self): + """ + The current value of the :data:`MAX_CONCURRENT_STREAMS + <h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS>` setting. + """ + return self.get(SettingCodes.MAX_CONCURRENT_STREAMS, 2**32+1) + + @max_concurrent_streams.setter + def max_concurrent_streams(self, value): + self[SettingCodes.MAX_CONCURRENT_STREAMS] = value + + @property + def max_header_list_size(self): + """ + The current value of the :data:`MAX_HEADER_LIST_SIZE + <h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE>` setting. If not set, + returns ``None``, which means unlimited. + + .. versionadded:: 2.5.0 + """ + return self.get(SettingCodes.MAX_HEADER_LIST_SIZE, None) + + @max_header_list_size.setter + def max_header_list_size(self, value): + self[SettingCodes.MAX_HEADER_LIST_SIZE] = value + + # Implement the MutableMapping API. + def __getitem__(self, key): + val = self._settings[key][0] + + # Things that were created when a setting was received should stay + # KeyError'd. + if val is None: + raise KeyError + + return val + + def __setitem__(self, key, value): + invalid = _validate_setting(key, value) + if invalid: + raise InvalidSettingsValueError( + "Setting %d has invalid value %d" % (key, value), + error_code=invalid + ) + + try: + items = self._settings[key] + except KeyError: + items = collections.deque([None]) + self._settings[key] = items + + items.append(value) + + def __delitem__(self, key): + del self._settings[key] + + def __iter__(self): + return self._settings.__iter__() + + def __len__(self): + return len(self._settings) + + def __eq__(self, other): + if isinstance(other, Settings): + return self._settings == other._settings + else: + return NotImplemented + + def __ne__(self, other): + if isinstance(other, Settings): + return not self == other + else: + return NotImplemented + + +def _validate_setting(setting, value): + """ + Confirms that a specific setting has a well-formed value. If the setting is + invalid, returns an error code. Otherwise, returns 0 (NO_ERROR). + """ + if setting == SettingCodes.ENABLE_PUSH: + if value not in (0, 1): + return ErrorCodes.PROTOCOL_ERROR + elif setting == SettingCodes.INITIAL_WINDOW_SIZE: + if not 0 <= value <= 2147483647: # 2^31 - 1 + return ErrorCodes.FLOW_CONTROL_ERROR + elif setting == SettingCodes.MAX_FRAME_SIZE: + if not 16384 <= value <= 16777215: # 2^14 and 2^24 - 1 + return ErrorCodes.PROTOCOL_ERROR + elif setting == SettingCodes.MAX_HEADER_LIST_SIZE: + if value < 0: + return ErrorCodes.PROTOCOL_ERROR + + return 0 diff --git a/vinetrimmer/vendor/h2/stream.py b/vinetrimmer/vendor/h2/stream.py new file mode 100644 index 0000000..cce869d --- /dev/null +++ b/vinetrimmer/vendor/h2/stream.py @@ -0,0 +1,1399 @@ +# -*- coding: utf-8 -*- +""" +h2/stream +~~~~~~~~~ + +An implementation of a HTTP/2 stream. +""" +import warnings + +from enum import Enum, IntEnum +from vinetrimmer.vendor.hpack import HeaderTuple +from vinetrimmer.vendor.hyperframe.frame import ( + HeadersFrame, ContinuationFrame, DataFrame, WindowUpdateFrame, + RstStreamFrame, PushPromiseFrame, AltSvcFrame +) + +from .errors import ErrorCodes, _error_code_from_int +from .events import ( + RequestReceived, ResponseReceived, DataReceived, WindowUpdated, + StreamEnded, PushedStreamReceived, StreamReset, TrailersReceived, + InformationalResponseReceived, AlternativeServiceAvailable, + _ResponseSent, _RequestSent, _TrailersSent, _PushedRequestSent +) +from .exceptions import ( + ProtocolError, StreamClosedError, InvalidBodyLengthError, FlowControlError +) +from .utilities import ( + guard_increment_window, is_informational_response, authority_from_headers, + validate_headers, validate_outbound_headers, normalize_outbound_headers, + HeaderValidationFlags, extract_method_header +) +from .windows import WindowManager + + +class StreamState(IntEnum): + IDLE = 0 + RESERVED_REMOTE = 1 + RESERVED_LOCAL = 2 + OPEN = 3 + HALF_CLOSED_REMOTE = 4 + HALF_CLOSED_LOCAL = 5 + CLOSED = 6 + + +class StreamInputs(Enum): + SEND_HEADERS = 0 + SEND_PUSH_PROMISE = 1 + SEND_RST_STREAM = 2 + SEND_DATA = 3 + SEND_WINDOW_UPDATE = 4 + SEND_END_STREAM = 5 + RECV_HEADERS = 6 + RECV_PUSH_PROMISE = 7 + RECV_RST_STREAM = 8 + RECV_DATA = 9 + RECV_WINDOW_UPDATE = 10 + RECV_END_STREAM = 11 + RECV_CONTINUATION = 12 # Added in 2.0.0 + SEND_INFORMATIONAL_HEADERS = 13 # Added in 2.2.0 + RECV_INFORMATIONAL_HEADERS = 14 # Added in 2.2.0 + SEND_ALTERNATIVE_SERVICE = 15 # Added in 2.3.0 + RECV_ALTERNATIVE_SERVICE = 16 # Added in 2.3.0 + UPGRADE_CLIENT = 17 # Added 2.3.0 + UPGRADE_SERVER = 18 # Added 2.3.0 + + +class StreamClosedBy(Enum): + SEND_END_STREAM = 0 + RECV_END_STREAM = 1 + SEND_RST_STREAM = 2 + RECV_RST_STREAM = 3 + + +# This array is initialized once, and is indexed by the stream states above. +# It indicates whether a stream in the given state is open. The reason we do +# this is that we potentially check whether a stream in a given state is open +# quite frequently: given that we check so often, we should do so in the +# fastest and most performant way possible. +STREAM_OPEN = [False for _ in range(0, len(StreamState))] +STREAM_OPEN[StreamState.OPEN] = True +STREAM_OPEN[StreamState.HALF_CLOSED_LOCAL] = True +STREAM_OPEN[StreamState.HALF_CLOSED_REMOTE] = True + + +class H2StreamStateMachine(object): + """ + A single HTTP/2 stream state machine. + + This stream object implements basically the state machine described in + RFC 7540 section 5.1. + + :param stream_id: The stream ID of this stream. This is stored primarily + for logging purposes. + """ + def __init__(self, stream_id): + self.state = StreamState.IDLE + self.stream_id = stream_id + + #: Whether this peer is the client side of this stream. + self.client = None + + # Whether trailers have been sent/received on this stream or not. + self.headers_sent = None + self.trailers_sent = None + self.headers_received = None + self.trailers_received = None + + # How the stream was closed. One of StreamClosedBy. + self.stream_closed_by = None + + def process_input(self, input_): + """ + Process a specific input in the state machine. + """ + if not isinstance(input_, StreamInputs): + raise ValueError("Input must be an instance of StreamInputs") + + try: + func, target_state = _transitions[(self.state, input_)] + except KeyError: + old_state = self.state + self.state = StreamState.CLOSED + raise ProtocolError( + "Invalid input %s in state %s" % (input_, old_state) + ) + else: + previous_state = self.state + self.state = target_state + if func is not None: + try: + return func(self, previous_state) + except ProtocolError: + self.state = StreamState.CLOSED + raise + except AssertionError as e: # pragma: no cover + self.state = StreamState.CLOSED + raise ProtocolError(e) + + return [] + + def request_sent(self, previous_state): + """ + Fires when a request is sent. + """ + self.client = True + self.headers_sent = True + event = _RequestSent() + + return [event] + + def response_sent(self, previous_state): + """ + Fires when something that should be a response is sent. This 'response' + may actually be trailers. + """ + if not self.headers_sent: + if self.client is True or self.client is None: + raise ProtocolError("Client cannot send responses.") + self.headers_sent = True + event = _ResponseSent() + else: + assert not self.trailers_sent + self.trailers_sent = True + event = _TrailersSent() + + return [event] + + def request_received(self, previous_state): + """ + Fires when a request is received. + """ + assert not self.headers_received + assert not self.trailers_received + + self.client = False + self.headers_received = True + event = RequestReceived() + + event.stream_id = self.stream_id + return [event] + + def response_received(self, previous_state): + """ + Fires when a response is received. Also disambiguates between responses + and trailers. + """ + if not self.headers_received: + assert self.client is True + self.headers_received = True + event = ResponseReceived() + else: + assert not self.trailers_received + self.trailers_received = True + event = TrailersReceived() + + event.stream_id = self.stream_id + return [event] + + def data_received(self, previous_state): + """ + Fires when data is received. + """ + event = DataReceived() + event.stream_id = self.stream_id + return [event] + + def window_updated(self, previous_state): + """ + Fires when a window update frame is received. + """ + event = WindowUpdated() + event.stream_id = self.stream_id + return [event] + + def stream_half_closed(self, previous_state): + """ + Fires when an END_STREAM flag is received in the OPEN state, + transitioning this stream to a HALF_CLOSED_REMOTE state. + """ + event = StreamEnded() + event.stream_id = self.stream_id + return [event] + + def stream_ended(self, previous_state): + """ + Fires when a stream is cleanly ended. + """ + self.stream_closed_by = StreamClosedBy.RECV_END_STREAM + event = StreamEnded() + event.stream_id = self.stream_id + return [event] + + def stream_reset(self, previous_state): + """ + Fired when a stream is forcefully reset. + """ + self.stream_closed_by = StreamClosedBy.RECV_RST_STREAM + event = StreamReset() + event.stream_id = self.stream_id + return [event] + + def send_new_pushed_stream(self, previous_state): + """ + Fires on the newly pushed stream, when pushed by the local peer. + + No event here, but definitionally this peer must be a server. + """ + assert self.client is None + self.client = False + self.headers_received = True + return [] + + def recv_new_pushed_stream(self, previous_state): + """ + Fires on the newly pushed stream, when pushed by the remote peer. + + No event here, but definitionally this peer must be a client. + """ + assert self.client is None + self.client = True + self.headers_sent = True + return [] + + def send_push_promise(self, previous_state): + """ + Fires on the already-existing stream when a PUSH_PROMISE frame is sent. + We may only send PUSH_PROMISE frames if we're a server. + """ + if self.client is True: + raise ProtocolError("Cannot push streams from client peers.") + + event = _PushedRequestSent() + return [event] + + def recv_push_promise(self, previous_state): + """ + Fires on the already-existing stream when a PUSH_PROMISE frame is + received. We may only receive PUSH_PROMISE frames if we're a client. + + Fires a PushedStreamReceived event. + """ + if not self.client: + if self.client is None: # pragma: no cover + msg = "Idle streams cannot receive pushes" + else: # pragma: no cover + msg = "Cannot receive pushed streams as a server" + raise ProtocolError(msg) + + event = PushedStreamReceived() + event.parent_stream_id = self.stream_id + return [event] + + def send_end_stream(self, previous_state): + """ + Called when an attempt is made to send END_STREAM in the + HALF_CLOSED_REMOTE state. + """ + self.stream_closed_by = StreamClosedBy.SEND_END_STREAM + + def send_reset_stream(self, previous_state): + """ + Called when an attempt is made to send RST_STREAM in a non-closed + stream state. + """ + self.stream_closed_by = StreamClosedBy.SEND_RST_STREAM + + def reset_stream_on_error(self, previous_state): + """ + Called when we need to forcefully emit another RST_STREAM frame on + behalf of the state machine. + + If this is the first time we've done this, we should also hang an event + off the StreamClosedError so that the user can be informed. We know + it's the first time we've done this if the stream is currently in a + state other than CLOSED. + """ + self.stream_closed_by = StreamClosedBy.SEND_RST_STREAM + + error = StreamClosedError(self.stream_id) + + event = StreamReset() + event.stream_id = self.stream_id + event.error_code = ErrorCodes.STREAM_CLOSED + event.remote_reset = False + error._events = [event] + raise error + + def recv_on_closed_stream(self, previous_state): + """ + Called when an unexpected frame is received on an already-closed + stream. + + An endpoint that receives an unexpected frame should treat it as + a stream error or connection error with type STREAM_CLOSED, depending + on the specific frame. The error handling is done at a higher level: + this just raises the appropriate error. + """ + raise StreamClosedError(self.stream_id) + + def send_on_closed_stream(self, previous_state): + """ + Called when an attempt is made to send data on an already-closed + stream. + + This essentially overrides the standard logic by throwing a + more-specific error: StreamClosedError. This is a ProtocolError, so it + matches the standard API of the state machine, but provides more detail + to the user. + """ + raise StreamClosedError(self.stream_id) + + def recv_push_on_closed_stream(self, previous_state): + """ + Called when a PUSH_PROMISE frame is received on a full stop + stream. + + If the stream was closed by us sending a RST_STREAM frame, then we + presume that the PUSH_PROMISE was in flight when we reset the parent + stream. Rathen than accept the new stream, we just reset it. + Otherwise, we should call this a PROTOCOL_ERROR: pushing a stream on a + naturally closed stream is a real problem because it creates a brand + new stream that the remote peer now believes exists. + """ + assert self.stream_closed_by is not None + + if self.stream_closed_by == StreamClosedBy.SEND_RST_STREAM: + raise StreamClosedError(self.stream_id) + else: + raise ProtocolError("Attempted to push on closed stream.") + + def send_push_on_closed_stream(self, previous_state): + """ + Called when an attempt is made to push on an already-closed stream. + + This essentially overrides the standard logic by providing a more + useful error message. It's necessary because simply indicating that the + stream is closed is not enough: there is now a new stream that is not + allowed to be there. The only recourse is to tear the whole connection + down. + """ + raise ProtocolError("Attempted to push on closed stream.") + + def window_on_closed_stream(self, previous_state): + """ + Called when a WINDOW_UPDATE frame is received on an already-closed + stream. + + If we sent an END_STREAM frame, we just ignore the frame, as instructed + in RFC 7540 Section 5.1. Technically we should eventually consider + WINDOW_UPDATE in this state an error, but we don't have access to a + clock so we just always allow it. If we closed the stream for any other + reason, we behave as we do for receiving any other frame on a closed + stream. + """ + assert self.stream_closed_by is not None + + if self.stream_closed_by == StreamClosedBy.SEND_END_STREAM: + return [] + return self.recv_on_closed_stream(previous_state) + + def reset_on_closed_stream(self, previous_state): + """ + Called when a RST_STREAM frame is received on an already-closed stream. + + If we sent an END_STREAM frame, we just ignore the frame, as instructed + in RFC 7540 Section 5.1. Technically we should eventually consider + RST_STREAM in this state an error, but we don't have access to a clock + so we just always allow it. If we closed the stream for any other + reason, we behave as we do for receiving any other frame on a closed + stream. + """ + assert self.stream_closed_by is not None + + if self.stream_closed_by is StreamClosedBy.SEND_END_STREAM: + return [] + return self.recv_on_closed_stream(previous_state) + + def send_informational_response(self, previous_state): + """ + Called when an informational header block is sent (that is, a block + where the :status header has a 1XX value). + + Only enforces that these are sent *before* final headers are sent. + """ + if self.headers_sent: + raise ProtocolError("Information response after final response") + + event = _ResponseSent() + return [event] + + def recv_informational_response(self, previous_state): + """ + Called when an informational header block is received (that is, a block + where the :status header has a 1XX value). + """ + if self.headers_received: + raise ProtocolError("Informational response after final response") + + event = InformationalResponseReceived() + event.stream_id = self.stream_id + return [event] + + def recv_alt_svc(self, previous_state): + """ + Called when receiving an ALTSVC frame. + + RFC 7838 allows us to receive ALTSVC frames at any stream state, which + is really absurdly overzealous. For that reason, we want to limit the + states in which we can actually receive it. It's really only sensible + to receive it after we've sent our own headers and before the server + has sent its header block: the server can't guarantee that we have any + state around after it completes its header block, and the server + doesn't know what origin we're talking about before we've sent ours. + + For that reason, this function applies a few extra checks on both state + and some of the little state variables we keep around. If those suggest + an unreasonable situation for the ALTSVC frame to have been sent in, + we quietly ignore it (as RFC 7838 suggests). + + This function is also *not* always called by the state machine. In some + states (IDLE, RESERVED_LOCAL, CLOSED) we don't bother to call it, + because we know the frame cannot be valid in that state (IDLE because + the server cannot know what origin the stream applies to, CLOSED + because the server cannot assume we still have state around, + RESERVED_LOCAL because by definition if we're in the RESERVED_LOCAL + state then *we* are the server). + """ + # Servers can't receive ALTSVC frames, but RFC 7838 tells us to ignore + # them. + if self.client is False: + return [] + + # If we've received the response headers from the server they can't + # guarantee we still have any state around. Other implementations + # (like nghttp2) ignore ALTSVC in this state, so we will too. + if self.headers_received: + return [] + + # Otherwise, this is a sensible enough frame to have received. Return + # the event and let it get populated. + return [AlternativeServiceAvailable()] + + def send_alt_svc(self, previous_state): + """ + Called when sending an ALTSVC frame on this stream. + + For consistency with the restrictions we apply on receiving ALTSVC + frames in ``recv_alt_svc``, we want to restrict when users can send + ALTSVC frames to the situations when we ourselves would accept them. + + That means: when we are a server, when we have received the request + headers, and when we have not yet sent our own response headers. + """ + # We should not send ALTSVC after we've sent response headers, as the + # client may have disposed of its state. + if self.headers_sent: + raise ProtocolError( + "Cannot send ALTSVC after sending response headers." + ) + + return + + +# STATE MACHINE +# +# The stream state machine is defined here to avoid the need to allocate it +# repeatedly for each stream. It cannot be defined in the stream class because +# it needs to be able to reference the callbacks defined on the class, but +# because Python's scoping rules are weird the class object is not actually in +# scope during the body of the class object. +# +# For the sake of clarity, we reproduce the RFC 7540 state machine here: +# +# +--------+ +# send PP | | recv PP +# ,--------| idle |--------. +# / | | \ +# v +--------+ v +# +----------+ | +----------+ +# | | | send H / | | +# ,------| reserved | | recv H | reserved |------. +# | | (local) | | | (remote) | | +# | +----------+ v +----------+ | +# | | +--------+ | | +# | | recv ES | | send ES | | +# | send H | ,-------| open |-------. | recv H | +# | | / | | \ | | +# | v v +--------+ v v | +# | +----------+ | +----------+ | +# | | half | | | half | | +# | | closed | | send R / | closed | | +# | | (remote) | | recv R | (local) | | +# | +----------+ | +----------+ | +# | | | | | +# | | send ES / | recv ES / | | +# | | send R / v send R / | | +# | | recv R +--------+ recv R | | +# | send R / `----------->| |<-----------' send R / | +# | recv R | closed | recv R | +# `----------------------->| |<----------------------' +# +--------+ +# +# send: endpoint sends this frame +# recv: endpoint receives this frame +# +# H: HEADERS frame (with implied CONTINUATIONs) +# PP: PUSH_PROMISE frame (with implied CONTINUATIONs) +# ES: END_STREAM flag +# R: RST_STREAM frame +# +# For the purposes of this state machine we treat HEADERS and their +# associated CONTINUATION frames as a single jumbo frame. The protocol +# allows/requires this by preventing other frames from being interleved in +# between HEADERS/CONTINUATION frames. However, if a CONTINUATION frame is +# received without a prior HEADERS frame, it *will* be passed to this state +# machine. The state machine should always reject that frame, either as an +# invalid transition or because the stream is closed. +# +# There is a confusing relationship around PUSH_PROMISE frames. The state +# machine above considers them to be frames belonging to the new stream, +# which is *somewhat* true. However, they are sent with the stream ID of +# their related stream, and are only sendable in some cases. +# For this reason, our state machine implementation below allows for +# PUSH_PROMISE frames both in the IDLE state (as in the diagram), but also +# in the OPEN, HALF_CLOSED_LOCAL, and HALF_CLOSED_REMOTE states. +# Essentially, for hyper-h2, PUSH_PROMISE frames are effectively sent on +# two streams. +# +# The _transitions dictionary contains a mapping of tuples of +# (state, input) to tuples of (side_effect_function, end_state). This +# map contains all allowed transitions: anything not in this map is +# invalid and immediately causes a transition to ``closed``. +_transitions = { + # State: idle + (StreamState.IDLE, StreamInputs.SEND_HEADERS): + (H2StreamStateMachine.request_sent, StreamState.OPEN), + (StreamState.IDLE, StreamInputs.RECV_HEADERS): + (H2StreamStateMachine.request_received, StreamState.OPEN), + (StreamState.IDLE, StreamInputs.RECV_DATA): + (H2StreamStateMachine.reset_stream_on_error, StreamState.CLOSED), + (StreamState.IDLE, StreamInputs.SEND_PUSH_PROMISE): + (H2StreamStateMachine.send_new_pushed_stream, + StreamState.RESERVED_LOCAL), + (StreamState.IDLE, StreamInputs.RECV_PUSH_PROMISE): + (H2StreamStateMachine.recv_new_pushed_stream, + StreamState.RESERVED_REMOTE), + (StreamState.IDLE, StreamInputs.RECV_ALTERNATIVE_SERVICE): + (None, StreamState.IDLE), + (StreamState.IDLE, StreamInputs.UPGRADE_CLIENT): + (H2StreamStateMachine.request_sent, StreamState.HALF_CLOSED_LOCAL), + (StreamState.IDLE, StreamInputs.UPGRADE_SERVER): + (H2StreamStateMachine.request_received, + StreamState.HALF_CLOSED_REMOTE), + + # State: reserved local + (StreamState.RESERVED_LOCAL, StreamInputs.SEND_HEADERS): + (H2StreamStateMachine.response_sent, StreamState.HALF_CLOSED_REMOTE), + (StreamState.RESERVED_LOCAL, StreamInputs.RECV_DATA): + (H2StreamStateMachine.reset_stream_on_error, StreamState.CLOSED), + (StreamState.RESERVED_LOCAL, StreamInputs.SEND_WINDOW_UPDATE): + (None, StreamState.RESERVED_LOCAL), + (StreamState.RESERVED_LOCAL, StreamInputs.RECV_WINDOW_UPDATE): + (H2StreamStateMachine.window_updated, StreamState.RESERVED_LOCAL), + (StreamState.RESERVED_LOCAL, StreamInputs.SEND_RST_STREAM): + (H2StreamStateMachine.send_reset_stream, StreamState.CLOSED), + (StreamState.RESERVED_LOCAL, StreamInputs.RECV_RST_STREAM): + (H2StreamStateMachine.stream_reset, StreamState.CLOSED), + (StreamState.RESERVED_LOCAL, StreamInputs.SEND_ALTERNATIVE_SERVICE): + (H2StreamStateMachine.send_alt_svc, StreamState.RESERVED_LOCAL), + (StreamState.RESERVED_LOCAL, StreamInputs.RECV_ALTERNATIVE_SERVICE): + (None, StreamState.RESERVED_LOCAL), + + # State: reserved remote + (StreamState.RESERVED_REMOTE, StreamInputs.RECV_HEADERS): + (H2StreamStateMachine.response_received, + StreamState.HALF_CLOSED_LOCAL), + (StreamState.RESERVED_REMOTE, StreamInputs.RECV_DATA): + (H2StreamStateMachine.reset_stream_on_error, StreamState.CLOSED), + (StreamState.RESERVED_REMOTE, StreamInputs.SEND_WINDOW_UPDATE): + (None, StreamState.RESERVED_REMOTE), + (StreamState.RESERVED_REMOTE, StreamInputs.RECV_WINDOW_UPDATE): + (H2StreamStateMachine.window_updated, StreamState.RESERVED_REMOTE), + (StreamState.RESERVED_REMOTE, StreamInputs.SEND_RST_STREAM): + (H2StreamStateMachine.send_reset_stream, StreamState.CLOSED), + (StreamState.RESERVED_REMOTE, StreamInputs.RECV_RST_STREAM): + (H2StreamStateMachine.stream_reset, StreamState.CLOSED), + (StreamState.RESERVED_REMOTE, StreamInputs.RECV_ALTERNATIVE_SERVICE): + (H2StreamStateMachine.recv_alt_svc, StreamState.RESERVED_REMOTE), + + # State: open + (StreamState.OPEN, StreamInputs.SEND_HEADERS): + (H2StreamStateMachine.response_sent, StreamState.OPEN), + (StreamState.OPEN, StreamInputs.RECV_HEADERS): + (H2StreamStateMachine.response_received, StreamState.OPEN), + (StreamState.OPEN, StreamInputs.SEND_DATA): + (None, StreamState.OPEN), + (StreamState.OPEN, StreamInputs.RECV_DATA): + (H2StreamStateMachine.data_received, StreamState.OPEN), + (StreamState.OPEN, StreamInputs.SEND_END_STREAM): + (None, StreamState.HALF_CLOSED_LOCAL), + (StreamState.OPEN, StreamInputs.RECV_END_STREAM): + (H2StreamStateMachine.stream_half_closed, + StreamState.HALF_CLOSED_REMOTE), + (StreamState.OPEN, StreamInputs.SEND_WINDOW_UPDATE): + (None, StreamState.OPEN), + (StreamState.OPEN, StreamInputs.RECV_WINDOW_UPDATE): + (H2StreamStateMachine.window_updated, StreamState.OPEN), + (StreamState.OPEN, StreamInputs.SEND_RST_STREAM): + (H2StreamStateMachine.send_reset_stream, StreamState.CLOSED), + (StreamState.OPEN, StreamInputs.RECV_RST_STREAM): + (H2StreamStateMachine.stream_reset, StreamState.CLOSED), + (StreamState.OPEN, StreamInputs.SEND_PUSH_PROMISE): + (H2StreamStateMachine.send_push_promise, StreamState.OPEN), + (StreamState.OPEN, StreamInputs.RECV_PUSH_PROMISE): + (H2StreamStateMachine.recv_push_promise, StreamState.OPEN), + (StreamState.OPEN, StreamInputs.SEND_INFORMATIONAL_HEADERS): + (H2StreamStateMachine.send_informational_response, StreamState.OPEN), + (StreamState.OPEN, StreamInputs.RECV_INFORMATIONAL_HEADERS): + (H2StreamStateMachine.recv_informational_response, StreamState.OPEN), + (StreamState.OPEN, StreamInputs.SEND_ALTERNATIVE_SERVICE): + (H2StreamStateMachine.send_alt_svc, StreamState.OPEN), + (StreamState.OPEN, StreamInputs.RECV_ALTERNATIVE_SERVICE): + (H2StreamStateMachine.recv_alt_svc, StreamState.OPEN), + + # State: half-closed remote + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_HEADERS): + (H2StreamStateMachine.response_sent, StreamState.HALF_CLOSED_REMOTE), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_HEADERS): + (H2StreamStateMachine.reset_stream_on_error, StreamState.CLOSED), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_DATA): + (None, StreamState.HALF_CLOSED_REMOTE), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_DATA): + (H2StreamStateMachine.reset_stream_on_error, StreamState.CLOSED), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_END_STREAM): + (H2StreamStateMachine.send_end_stream, StreamState.CLOSED), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_WINDOW_UPDATE): + (None, StreamState.HALF_CLOSED_REMOTE), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_WINDOW_UPDATE): + (H2StreamStateMachine.window_updated, StreamState.HALF_CLOSED_REMOTE), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_RST_STREAM): + (H2StreamStateMachine.send_reset_stream, StreamState.CLOSED), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_RST_STREAM): + (H2StreamStateMachine.stream_reset, StreamState.CLOSED), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_PUSH_PROMISE): + (H2StreamStateMachine.send_push_promise, + StreamState.HALF_CLOSED_REMOTE), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_PUSH_PROMISE): + (H2StreamStateMachine.reset_stream_on_error, StreamState.CLOSED), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_INFORMATIONAL_HEADERS): + (H2StreamStateMachine.send_informational_response, + StreamState.HALF_CLOSED_REMOTE), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_ALTERNATIVE_SERVICE): + (H2StreamStateMachine.send_alt_svc, StreamState.HALF_CLOSED_REMOTE), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_ALTERNATIVE_SERVICE): + (H2StreamStateMachine.recv_alt_svc, StreamState.HALF_CLOSED_REMOTE), + + # State: half-closed local + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_HEADERS): + (H2StreamStateMachine.response_received, + StreamState.HALF_CLOSED_LOCAL), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_DATA): + (H2StreamStateMachine.data_received, StreamState.HALF_CLOSED_LOCAL), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_END_STREAM): + (H2StreamStateMachine.stream_ended, StreamState.CLOSED), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.SEND_WINDOW_UPDATE): + (None, StreamState.HALF_CLOSED_LOCAL), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_WINDOW_UPDATE): + (H2StreamStateMachine.window_updated, StreamState.HALF_CLOSED_LOCAL), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.SEND_RST_STREAM): + (H2StreamStateMachine.send_reset_stream, StreamState.CLOSED), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_RST_STREAM): + (H2StreamStateMachine.stream_reset, StreamState.CLOSED), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_PUSH_PROMISE): + (H2StreamStateMachine.recv_push_promise, + StreamState.HALF_CLOSED_LOCAL), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_INFORMATIONAL_HEADERS): + (H2StreamStateMachine.recv_informational_response, + StreamState.HALF_CLOSED_LOCAL), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.SEND_ALTERNATIVE_SERVICE): + (H2StreamStateMachine.send_alt_svc, StreamState.HALF_CLOSED_LOCAL), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_ALTERNATIVE_SERVICE): + (H2StreamStateMachine.recv_alt_svc, StreamState.HALF_CLOSED_LOCAL), + + # State: closed + (StreamState.CLOSED, StreamInputs.RECV_END_STREAM): + (None, StreamState.CLOSED), + (StreamState.CLOSED, StreamInputs.RECV_ALTERNATIVE_SERVICE): + (None, StreamState.CLOSED), + + # RFC 7540 Section 5.1 defines how the end point should react when + # receiving a frame on a closed stream with the following statements: + # + # > An endpoint that receives any frame other than PRIORITY after receiving + # > a RST_STREAM MUST treat that as a stream error of type STREAM_CLOSED. + # > An endpoint that receives any frames after receiving a frame with the + # > END_STREAM flag set MUST treat that as a connection error of type + # > STREAM_CLOSED. + (StreamState.CLOSED, StreamInputs.RECV_HEADERS): + (H2StreamStateMachine.recv_on_closed_stream, StreamState.CLOSED), + (StreamState.CLOSED, StreamInputs.RECV_DATA): + (H2StreamStateMachine.recv_on_closed_stream, StreamState.CLOSED), + + # > WINDOW_UPDATE or RST_STREAM frames can be received in this state + # > for a short period after a DATA or HEADERS frame containing a + # > END_STREAM flag is sent. + (StreamState.CLOSED, StreamInputs.RECV_WINDOW_UPDATE): + (H2StreamStateMachine.window_on_closed_stream, StreamState.CLOSED), + (StreamState.CLOSED, StreamInputs.RECV_RST_STREAM): + (H2StreamStateMachine.reset_on_closed_stream, StreamState.CLOSED), + + # > A receiver MUST treat the receipt of a PUSH_PROMISE on a stream that is + # > neither "open" nor "half-closed (local)" as a connection error of type + # > PROTOCOL_ERROR. + (StreamState.CLOSED, StreamInputs.RECV_PUSH_PROMISE): + (H2StreamStateMachine.recv_push_on_closed_stream, StreamState.CLOSED), + + # Also, users should be forbidden from sending on closed streams. + (StreamState.CLOSED, StreamInputs.SEND_HEADERS): + (H2StreamStateMachine.send_on_closed_stream, StreamState.CLOSED), + (StreamState.CLOSED, StreamInputs.SEND_PUSH_PROMISE): + (H2StreamStateMachine.send_push_on_closed_stream, StreamState.CLOSED), + (StreamState.CLOSED, StreamInputs.SEND_RST_STREAM): + (H2StreamStateMachine.send_on_closed_stream, StreamState.CLOSED), + (StreamState.CLOSED, StreamInputs.SEND_DATA): + (H2StreamStateMachine.send_on_closed_stream, StreamState.CLOSED), + (StreamState.CLOSED, StreamInputs.SEND_WINDOW_UPDATE): + (H2StreamStateMachine.send_on_closed_stream, StreamState.CLOSED), + (StreamState.CLOSED, StreamInputs.SEND_END_STREAM): + (H2StreamStateMachine.send_on_closed_stream, StreamState.CLOSED), +} + + +class H2Stream(object): + """ + A low-level HTTP/2 stream object. This handles building and receiving + frames and maintains per-stream state. + + This wraps a HTTP/2 Stream state machine implementation, ensuring that + frames can only be sent/received when the stream is in a valid state. + Attempts to create frames that cannot be sent will raise a + ``ProtocolError``. + """ + def __init__(self, + stream_id, + config, + inbound_window_size, + outbound_window_size): + self.state_machine = H2StreamStateMachine(stream_id) + self.stream_id = stream_id + self.max_outbound_frame_size = None + self.request_method = None + + # The curent value of the outbound stream flow control window + self.outbound_flow_control_window = outbound_window_size + + # The flow control manager. + self._inbound_window_manager = WindowManager(inbound_window_size) + + # The expected content length, if any. + self._expected_content_length = None + + # The actual received content length. Always tracked. + self._actual_content_length = 0 + + # The authority we believe this stream belongs to. + self._authority = None + + # The configuration for this stream. + self.config = config + + def __repr__(self): + return "<%s id:%d state:%r>" % ( + type(self).__name__, + self.stream_id, + self.state_machine.state + ) + + @property + def inbound_flow_control_window(self): + """ + The size of the inbound flow control window for the stream. This is + rarely publicly useful: instead, use :meth:`remote_flow_control_window + <h2.stream.H2Stream.remote_flow_control_window>`. This shortcut is + largely present to provide a shortcut to this data. + """ + return self._inbound_window_manager.current_window_size + + @property + def open(self): + """ + Whether the stream is 'open' in any sense: that is, whether it counts + against the number of concurrent streams. + """ + # RFC 7540 Section 5.1.2 defines 'open' for this purpose to mean either + # the OPEN state or either of the HALF_CLOSED states. Perplexingly, + # this excludes the reserved states. + # For more detail on why we're doing this in this slightly weird way, + # see the comment on ``STREAM_OPEN`` at the top of the file. + return STREAM_OPEN[self.state_machine.state] + + @property + def closed(self): + """ + Whether the stream is closed. + """ + return self.state_machine.state == StreamState.CLOSED + + @property + def closed_by(self): + """ + Returns how the stream was closed, as one of StreamClosedBy. + """ + return self.state_machine.stream_closed_by + + def upgrade(self, client_side): + """ + Called by the connection to indicate that this stream is the initial + request/response of an upgraded connection. Places the stream into an + appropriate state. + """ + self.config.logger.debug("Upgrading %r", self) + + assert self.stream_id == 1 + input_ = ( + StreamInputs.UPGRADE_CLIENT if client_side + else StreamInputs.UPGRADE_SERVER + ) + + # This may return events, we deliberately don't want them. + self.state_machine.process_input(input_) + return + + def send_headers(self, headers, encoder, end_stream=False): + """ + Returns a list of HEADERS/CONTINUATION frames to emit as either headers + or trailers. + """ + self.config.logger.debug("Send headers %s on %r", headers, self) + # Convert headers to two-tuples. + # FIXME: The fallback for dictionary headers is to be removed in 3.0. + try: + headers = headers.items() + warnings.warn( + "Implicit conversion of dictionaries to two-tuples for " + "headers is deprecated and will be removed in 3.0.", + DeprecationWarning + ) + except AttributeError: + headers = headers + + # Because encoding headers makes an irreversible change to the header + # compression context, we make the state transition before we encode + # them. + + # First, check if we're a client. If we are, no problem: if we aren't, + # we need to scan the header block to see if this is an informational + # response. + input_ = StreamInputs.SEND_HEADERS + if ((not self.state_machine.client) and + is_informational_response(headers)): + if end_stream: + raise ProtocolError( + "Cannot set END_STREAM on informational responses." + ) + + input_ = StreamInputs.SEND_INFORMATIONAL_HEADERS + + events = self.state_machine.process_input(input_) + + hf = HeadersFrame(self.stream_id) + hdr_validation_flags = self._build_hdr_validation_flags(events) + frames = self._build_headers_frames( + headers, encoder, hf, hdr_validation_flags + ) + + if end_stream: + # Not a bug: the END_STREAM flag is valid on the initial HEADERS + # frame, not the CONTINUATION frames that follow. + self.state_machine.process_input(StreamInputs.SEND_END_STREAM) + frames[0].flags.add('END_STREAM') + + if self.state_machine.trailers_sent and not end_stream: + raise ProtocolError("Trailers must have END_STREAM set.") + + if self.state_machine.client and self._authority is None: + self._authority = authority_from_headers(headers) + + # store request method for _initialize_content_length + self.request_method = extract_method_header(headers) + + return frames + + def push_stream_in_band(self, related_stream_id, headers, encoder): + """ + Returns a list of PUSH_PROMISE/CONTINUATION frames to emit as a pushed + stream header. Called on the stream that has the PUSH_PROMISE frame + sent on it. + """ + self.config.logger.debug("Push stream %r", self) + + # Because encoding headers makes an irreversible change to the header + # compression context, we make the state transition *first*. + + events = self.state_machine.process_input( + StreamInputs.SEND_PUSH_PROMISE + ) + + ppf = PushPromiseFrame(self.stream_id) + ppf.promised_stream_id = related_stream_id + hdr_validation_flags = self._build_hdr_validation_flags(events) + frames = self._build_headers_frames( + headers, encoder, ppf, hdr_validation_flags + ) + + return frames + + def locally_pushed(self): + """ + Mark this stream as one that was pushed by this peer. Must be called + immediately after initialization. Sends no frames, simply updates the + state machine. + """ + # This does not trigger any events. + events = self.state_machine.process_input( + StreamInputs.SEND_PUSH_PROMISE + ) + assert not events + return [] + + def send_data(self, data, end_stream=False, pad_length=None): + """ + Prepare some data frames. Optionally end the stream. + + .. warning:: Does not perform flow control checks. + """ + self.config.logger.debug( + "Send data on %r with end stream set to %s", self, end_stream + ) + + self.state_machine.process_input(StreamInputs.SEND_DATA) + + df = DataFrame(self.stream_id) + df.data = data + if end_stream: + self.state_machine.process_input(StreamInputs.SEND_END_STREAM) + df.flags.add('END_STREAM') + if pad_length is not None: + df.flags.add('PADDED') + df.pad_length = pad_length + + # Subtract flow_controlled_length to account for possible padding + self.outbound_flow_control_window -= df.flow_controlled_length + assert self.outbound_flow_control_window >= 0 + + return [df] + + def end_stream(self): + """ + End a stream without sending data. + """ + self.config.logger.debug("End stream %r", self) + + self.state_machine.process_input(StreamInputs.SEND_END_STREAM) + df = DataFrame(self.stream_id) + df.flags.add('END_STREAM') + return [df] + + def advertise_alternative_service(self, field_value): + """ + Advertise an RFC 7838 alternative service. The semantics of this are + better documented in the ``H2Connection`` class. + """ + self.config.logger.debug( + "Advertise alternative service of %r for %r", field_value, self + ) + self.state_machine.process_input(StreamInputs.SEND_ALTERNATIVE_SERVICE) + asf = AltSvcFrame(self.stream_id) + asf.field = field_value + return [asf] + + def increase_flow_control_window(self, increment): + """ + Increase the size of the flow control window for the remote side. + """ + self.config.logger.debug( + "Increase flow control window for %r by %d", + self, increment + ) + self.state_machine.process_input(StreamInputs.SEND_WINDOW_UPDATE) + self._inbound_window_manager.window_opened(increment) + + wuf = WindowUpdateFrame(self.stream_id) + wuf.window_increment = increment + return [wuf] + + def receive_push_promise_in_band(self, + promised_stream_id, + headers, + header_encoding): + """ + Receives a push promise frame sent on this stream, pushing a remote + stream. This is called on the stream that has the PUSH_PROMISE sent + on it. + """ + self.config.logger.debug( + "Receive Push Promise on %r for remote stream %d", + self, promised_stream_id + ) + events = self.state_machine.process_input( + StreamInputs.RECV_PUSH_PROMISE + ) + events[0].pushed_stream_id = promised_stream_id + + if self.config.validate_inbound_headers: + hdr_validation_flags = self._build_hdr_validation_flags(events) + headers = validate_headers(headers, hdr_validation_flags) + + if header_encoding: + headers = list(_decode_headers(headers, header_encoding)) + events[0].headers = headers + return [], events + + def remotely_pushed(self, pushed_headers): + """ + Mark this stream as one that was pushed by the remote peer. Must be + called immediately after initialization. Sends no frames, simply + updates the state machine. + """ + self.config.logger.debug("%r pushed by remote peer", self) + events = self.state_machine.process_input( + StreamInputs.RECV_PUSH_PROMISE + ) + self._authority = authority_from_headers(pushed_headers) + return [], events + + def receive_headers(self, headers, end_stream, header_encoding): + """ + Receive a set of headers (or trailers). + """ + if is_informational_response(headers): + if end_stream: + raise ProtocolError( + "Cannot set END_STREAM on informational responses" + ) + input_ = StreamInputs.RECV_INFORMATIONAL_HEADERS + else: + input_ = StreamInputs.RECV_HEADERS + + events = self.state_machine.process_input(input_) + + if end_stream: + es_events = self.state_machine.process_input( + StreamInputs.RECV_END_STREAM + ) + events[0].stream_ended = es_events[0] + events += es_events + + self._initialize_content_length(headers) + + if isinstance(events[0], TrailersReceived): + if not end_stream: + raise ProtocolError("Trailers must have END_STREAM set") + + if self.config.validate_inbound_headers: + hdr_validation_flags = self._build_hdr_validation_flags(events) + headers = validate_headers(headers, hdr_validation_flags) + + if header_encoding: + headers = list(_decode_headers(headers, header_encoding)) + + events[0].headers = headers + return [], events + + def receive_data(self, data, end_stream, flow_control_len): + """ + Receive some data. + """ + self.config.logger.debug( + "Receive data on %r with end stream %s and flow control length " + "set to %d", self, end_stream, flow_control_len + ) + events = self.state_machine.process_input(StreamInputs.RECV_DATA) + self._inbound_window_manager.window_consumed(flow_control_len) + self._track_content_length(len(data), end_stream) + + if end_stream: + es_events = self.state_machine.process_input( + StreamInputs.RECV_END_STREAM + ) + events[0].stream_ended = es_events[0] + events.extend(es_events) + + events[0].data = data + events[0].flow_controlled_length = flow_control_len + return [], events + + def receive_window_update(self, increment): + """ + Handle a WINDOW_UPDATE increment. + """ + self.config.logger.debug( + "Receive Window Update on %r for increment of %d", + self, increment + ) + events = self.state_machine.process_input( + StreamInputs.RECV_WINDOW_UPDATE + ) + frames = [] + + # If we encounter a problem with incrementing the flow control window, + # this should be treated as a *stream* error, not a *connection* error. + # That means we need to catch the error and forcibly close the stream. + if events: + events[0].delta = increment + try: + self.outbound_flow_control_window = guard_increment_window( + self.outbound_flow_control_window, + increment + ) + except FlowControlError: + # Ok, this is bad. We're going to need to perform a local + # reset. + event = StreamReset() + event.stream_id = self.stream_id + event.error_code = ErrorCodes.FLOW_CONTROL_ERROR + event.remote_reset = False + + events = [event] + frames = self.reset_stream(event.error_code) + + return frames, events + + def receive_continuation(self): + """ + A naked CONTINUATION frame has been received. This is always an error, + but the type of error it is depends on the state of the stream and must + transition the state of the stream, so we need to handle it. + """ + self.config.logger.debug("Receive Continuation frame on %r", self) + self.state_machine.process_input( + StreamInputs.RECV_CONTINUATION + ) + assert False, "Should not be reachable" + + def receive_alt_svc(self, frame): + """ + An Alternative Service frame was received on the stream. This frame + inherits the origin associated with this stream. + """ + self.config.logger.debug( + "Receive Alternative Service frame on stream %r", self + ) + + # If the origin is present, RFC 7838 says we have to ignore it. + if frame.origin: + return [], [] + + events = self.state_machine.process_input( + StreamInputs.RECV_ALTERNATIVE_SERVICE + ) + + # There are lots of situations where we want to ignore the ALTSVC + # frame. If we need to pay attention, we'll have an event and should + # fill it out. + if events: + assert isinstance(events[0], AlternativeServiceAvailable) + events[0].origin = self._authority + events[0].field_value = frame.field + + return [], events + + def reset_stream(self, error_code=0): + """ + Close the stream locally. Reset the stream with an error code. + """ + self.config.logger.debug( + "Local reset %r with error code: %d", self, error_code + ) + self.state_machine.process_input(StreamInputs.SEND_RST_STREAM) + + rsf = RstStreamFrame(self.stream_id) + rsf.error_code = error_code + return [rsf] + + def stream_reset(self, frame): + """ + Handle a stream being reset remotely. + """ + self.config.logger.debug( + "Remote reset %r with error code: %d", self, frame.error_code + ) + events = self.state_machine.process_input(StreamInputs.RECV_RST_STREAM) + + if events: + # We don't fire an event if this stream is already closed. + events[0].error_code = _error_code_from_int(frame.error_code) + + return [], events + + def acknowledge_received_data(self, acknowledged_size): + """ + The user has informed us that they've processed some amount of data + that was received on this stream. Pass that to the window manager and + potentially return some WindowUpdate frames. + """ + self.config.logger.debug( + "Acknowledge received data with size %d on %r", + acknowledged_size, self + ) + increment = self._inbound_window_manager.process_bytes( + acknowledged_size + ) + if increment: + f = WindowUpdateFrame(self.stream_id) + f.window_increment = increment + return [f] + + return [] + + def _build_hdr_validation_flags(self, events): + """ + Constructs a set of header validation flags for use when normalizing + and validating header blocks. + """ + is_trailer = isinstance( + events[0], (_TrailersSent, TrailersReceived) + ) + is_response_header = isinstance( + events[0], + ( + _ResponseSent, + ResponseReceived, + InformationalResponseReceived + ) + ) + is_push_promise = isinstance( + events[0], (PushedStreamReceived, _PushedRequestSent) + ) + + return HeaderValidationFlags( + is_client=self.state_machine.client, + is_trailer=is_trailer, + is_response_header=is_response_header, + is_push_promise=is_push_promise, + ) + + def _build_headers_frames(self, + headers, + encoder, + first_frame, + hdr_validation_flags): + """ + Helper method to build headers or push promise frames. + """ + # We need to lowercase the header names, and to ensure that secure + # header fields are kept out of compression contexts. + if self.config.normalize_outbound_headers: + headers = normalize_outbound_headers( + headers, hdr_validation_flags + ) + if self.config.validate_outbound_headers: + headers = validate_outbound_headers( + headers, hdr_validation_flags + ) + + encoded_headers = encoder.encode(headers) + + # Slice into blocks of max_outbound_frame_size. Be careful with this: + # it only works right because we never send padded frames or priority + # information on the frames. Revisit this if we do. + header_blocks = [ + encoded_headers[i:i+self.max_outbound_frame_size] + for i in range( + 0, len(encoded_headers), self.max_outbound_frame_size + ) + ] + + frames = [] + first_frame.data = header_blocks[0] + frames.append(first_frame) + + for block in header_blocks[1:]: + cf = ContinuationFrame(self.stream_id) + cf.data = block + frames.append(cf) + + frames[-1].flags.add('END_HEADERS') + return frames + + def _initialize_content_length(self, headers): + """ + Checks the headers for a content-length header and initializes the + _expected_content_length field from it. It's not an error for no + Content-Length header to be present. + """ + if self.request_method == b'HEAD': + self._expected_content_length = 0 + return + + for n, v in headers: + if n == b'content-length': + try: + self._expected_content_length = int(v, 10) + except ValueError: + raise ProtocolError( + "Invalid content-length header: %s" % v + ) + + return + + def _track_content_length(self, length, end_stream): + """ + Update the expected content length in response to data being received. + Validates that the appropriate amount of data is sent. Always updates + the received data, but only validates the length against the + content-length header if one was sent. + + :param length: The length of the body chunk received. + :param end_stream: If this is the last body chunk received. + """ + self._actual_content_length += length + actual = self._actual_content_length + expected = self._expected_content_length + + if expected is not None: + if expected < actual: + raise InvalidBodyLengthError(expected, actual) + + if end_stream and expected != actual: + raise InvalidBodyLengthError(expected, actual) + + def _inbound_flow_control_change_from_settings(self, delta): + """ + We changed SETTINGS_INITIAL_WINDOW_SIZE, which means we need to + update the target window size for flow control. For our flow control + strategy, this means we need to do two things: we need to adjust the + current window size, but we also need to set the target maximum window + size to the new value. + """ + new_max_size = self._inbound_window_manager.max_window_size + delta + self._inbound_window_manager.window_opened(delta) + self._inbound_window_manager.max_window_size = new_max_size + + +def _decode_headers(headers, encoding): + """ + Given an iterable of header two-tuples and an encoding, decodes those + headers using that encoding while preserving the type of the header tuple. + This ensures that the use of ``HeaderTuple`` is preserved. + """ + for header in headers: + # This function expects to work on decoded headers, which are always + # HeaderTuple objects. + assert isinstance(header, HeaderTuple) + + name, value = header + name = name.decode(encoding) + value = value.decode(encoding) + yield header.__class__(name, value) diff --git a/vinetrimmer/vendor/h2/utilities.py b/vinetrimmer/vendor/h2/utilities.py new file mode 100644 index 0000000..e8ee014 --- /dev/null +++ b/vinetrimmer/vendor/h2/utilities.py @@ -0,0 +1,585 @@ +# -*- coding: utf-8 -*- +""" +h2/utilities +~~~~~~~~~~~~ + +Utility functions that do not belong in a separate module. +""" +import collections +import re +from string import whitespace +import sys + +from vinetrimmer.vendor.hpack import HeaderTuple, NeverIndexedHeaderTuple + +from .exceptions import ProtocolError, FlowControlError + +UPPER_RE = re.compile(b"[A-Z]") + +# A set of headers that are hop-by-hop or connection-specific and thus +# forbidden in HTTP/2. This list comes from RFC 7540 § 8.1.2.2. +CONNECTION_HEADERS = frozenset([ + b'connection', u'connection', + b'proxy-connection', u'proxy-connection', + b'keep-alive', u'keep-alive', + b'transfer-encoding', u'transfer-encoding', + b'upgrade', u'upgrade', +]) + + +_ALLOWED_PSEUDO_HEADER_FIELDS = frozenset([ + b':method', u':method', + b':scheme', u':scheme', + b':authority', u':authority', + b':path', u':path', + b':status', u':status', +]) + + +_SECURE_HEADERS = frozenset([ + # May have basic credentials which are vulnerable to dictionary attacks. + b'authorization', u'authorization', + b'proxy-authorization', u'proxy-authorization', +]) + + +_REQUEST_ONLY_HEADERS = frozenset([ + b':scheme', u':scheme', + b':path', u':path', + b':authority', u':authority', + b':method', u':method' +]) + + +_RESPONSE_ONLY_HEADERS = frozenset([b':status', u':status']) + + +if sys.version_info[0] == 2: # Python 2.X + _WHITESPACE = frozenset(whitespace) +else: # Python 3.3+ + _WHITESPACE = frozenset(map(ord, whitespace)) + + +def _secure_headers(headers, hdr_validation_flags): + """ + Certain headers are at risk of being attacked during the header compression + phase, and so need to be kept out of header compression contexts. This + function automatically transforms certain specific headers into HPACK + never-indexed fields to ensure they don't get added to header compression + contexts. + + This function currently implements two rules: + + - 'authorization' and 'proxy-authorization' fields are automatically made + never-indexed. + - Any 'cookie' header field shorter than 20 bytes long is made + never-indexed. + + These fields are the most at-risk. These rules are inspired by Firefox + and nghttp2. + """ + for header in headers: + if header[0] in _SECURE_HEADERS: + yield NeverIndexedHeaderTuple(*header) + elif header[0] in (b'cookie', u'cookie') and len(header[1]) < 20: + yield NeverIndexedHeaderTuple(*header) + else: + yield header + + +def extract_method_header(headers): + """ + Extracts the request method from the headers list. + """ + for k, v in headers: + if k in (b':method', u':method'): + if not isinstance(v, bytes): + return v.encode('utf-8') + else: + return v + + +def is_informational_response(headers): + """ + Searches a header block for a :status header to confirm that a given + collection of headers are an informational response. Assumes the header + block is well formed: that is, that the HTTP/2 special headers are first + in the block, and so that it can stop looking when it finds the first + header field whose name does not begin with a colon. + + :param headers: The HTTP/2 header block. + :returns: A boolean indicating if this is an informational response. + """ + for n, v in headers: + if isinstance(n, bytes): + sigil = b':' + status = b':status' + informational_start = b'1' + else: + sigil = u':' + status = u':status' + informational_start = u'1' + + # If we find a non-special header, we're done here: stop looping. + if not n.startswith(sigil): + return False + + # This isn't the status header, bail. + if n != status: + continue + + # If the first digit is a 1, we've got informational headers. + return v.startswith(informational_start) + + +def guard_increment_window(current, increment): + """ + Increments a flow control window, guarding against that window becoming too + large. + + :param current: The current value of the flow control window. + :param increment: The increment to apply to that window. + :returns: The new value of the window. + :raises: ``FlowControlError`` + """ + # The largest value the flow control window may take. + LARGEST_FLOW_CONTROL_WINDOW = 2**31 - 1 + + new_size = current + increment + + if new_size > LARGEST_FLOW_CONTROL_WINDOW: + raise FlowControlError( + "May not increment flow control window past %d" % + LARGEST_FLOW_CONTROL_WINDOW + ) + + return new_size + + +def authority_from_headers(headers): + """ + Given a header set, searches for the authority header and returns the + value. + + Note that this doesn't terminate early, so should only be called if the + headers are for a client request. Otherwise, will loop over the entire + header set, which is potentially unwise. + + :param headers: The HTTP header set. + :returns: The value of the authority header, or ``None``. + :rtype: ``bytes`` or ``None``. + """ + for n, v in headers: + # This gets run against headers that come both from HPACK and from the + # user, so we may have unicode floating around in here. We only want + # bytes. + if n in (b':authority', u':authority'): + return v.encode('utf-8') if not isinstance(v, bytes) else v + + return None + + +# Flags used by the validate_headers pipeline to determine which checks +# should be applied to a given set of headers. +HeaderValidationFlags = collections.namedtuple( + 'HeaderValidationFlags', + ['is_client', 'is_trailer', 'is_response_header', 'is_push_promise'] +) + + +def validate_headers(headers, hdr_validation_flags): + """ + Validates a header sequence against a set of constraints from RFC 7540. + + :param headers: The HTTP header set. + :param hdr_validation_flags: An instance of HeaderValidationFlags. + """ + # This validation logic is built on a sequence of generators that are + # iterated over to provide the final header list. This reduces some of the + # overhead of doing this checking. However, it's worth noting that this + # checking remains somewhat expensive, and attempts should be made wherever + # possible to reduce the time spent doing them. + # + # For example, we avoid tuple upacking in loops because it represents a + # fixed cost that we don't want to spend, instead indexing into the header + # tuples. + headers = _reject_uppercase_header_fields( + headers, hdr_validation_flags + ) + headers = _reject_surrounding_whitespace( + headers, hdr_validation_flags + ) + headers = _reject_te( + headers, hdr_validation_flags + ) + headers = _reject_connection_header( + headers, hdr_validation_flags + ) + headers = _reject_pseudo_header_fields( + headers, hdr_validation_flags + ) + headers = _check_host_authority_header( + headers, hdr_validation_flags + ) + headers = _check_path_header(headers, hdr_validation_flags) + + return list(headers) + + +def _reject_uppercase_header_fields(headers, hdr_validation_flags): + """ + Raises a ProtocolError if any uppercase character is found in a header + block. + """ + for header in headers: + if UPPER_RE.search(header[0]): + raise ProtocolError( + "Received uppercase header name %s." % header[0]) + yield header + + +def _reject_surrounding_whitespace(headers, hdr_validation_flags): + """ + Raises a ProtocolError if any header name or value is surrounded by + whitespace characters. + """ + # For compatibility with RFC 7230 header fields, we need to allow the field + # value to be an empty string. This is ludicrous, but technically allowed. + # The field name may not be empty, though, so we can safely assume that it + # must have at least one character in it and throw exceptions if it + # doesn't. + for header in headers: + if header[0][0] in _WHITESPACE or header[0][-1] in _WHITESPACE: + raise ProtocolError( + "Received header name surrounded by whitespace %r" % header[0]) + if header[1] and ((header[1][0] in _WHITESPACE) or + (header[1][-1] in _WHITESPACE)): + raise ProtocolError( + "Received header value surrounded by whitespace %r" % header[1] + ) + yield header + + +def _reject_te(headers, hdr_validation_flags): + """ + Raises a ProtocolError if the TE header is present in a header block and + its value is anything other than "trailers". + """ + for header in headers: + if header[0] in (b'te', u'te'): + if header[1].lower() not in (b'trailers', u'trailers'): + raise ProtocolError( + "Invalid value for Transfer-Encoding header: %s" % + header[1] + ) + + yield header + + +def _reject_connection_header(headers, hdr_validation_flags): + """ + Raises a ProtocolError if the Connection header is present in a header + block. + """ + for header in headers: + if header[0] in CONNECTION_HEADERS: + raise ProtocolError( + "Connection-specific header field present: %s." % header[0] + ) + + yield header + + +def _custom_startswith(test_string, bytes_prefix, unicode_prefix): + """ + Given a string that might be a bytestring or a Unicode string, + return True if it starts with the appropriate prefix. + """ + if isinstance(test_string, bytes): + return test_string.startswith(bytes_prefix) + else: + return test_string.startswith(unicode_prefix) + + +def _assert_header_in_set(string_header, bytes_header, header_set): + """ + Given a set of header names, checks whether the string or byte version of + the header name is present. Raises a Protocol error with the appropriate + error if it's missing. + """ + if not (string_header in header_set or bytes_header in header_set): + raise ProtocolError( + "Header block missing mandatory %s header" % string_header + ) + + +def _reject_pseudo_header_fields(headers, hdr_validation_flags): + """ + Raises a ProtocolError if duplicate pseudo-header fields are found in a + header block or if a pseudo-header field appears in a block after an + ordinary header field. + + Raises a ProtocolError if pseudo-header fields are found in trailers. + """ + seen_pseudo_header_fields = set() + seen_regular_header = False + + for header in headers: + if _custom_startswith(header[0], b':', u':'): + if header[0] in seen_pseudo_header_fields: + raise ProtocolError( + "Received duplicate pseudo-header field %s" % header[0] + ) + + seen_pseudo_header_fields.add(header[0]) + + if seen_regular_header: + raise ProtocolError( + "Received pseudo-header field out of sequence: %s" % + header[0] + ) + + if header[0] not in _ALLOWED_PSEUDO_HEADER_FIELDS: + raise ProtocolError( + "Received custom pseudo-header field %s" % header[0] + ) + + else: + seen_regular_header = True + + yield header + + # Check the pseudo-headers we got to confirm they're acceptable. + _check_pseudo_header_field_acceptability( + seen_pseudo_header_fields, hdr_validation_flags + ) + + +def _check_pseudo_header_field_acceptability(pseudo_headers, + hdr_validation_flags): + """ + Given the set of pseudo-headers present in a header block and the + validation flags, confirms that RFC 7540 allows them. + """ + # Pseudo-header fields MUST NOT appear in trailers - RFC 7540 § 8.1.2.1 + if hdr_validation_flags.is_trailer and pseudo_headers: + raise ProtocolError( + "Received pseudo-header in trailer %s" % pseudo_headers + ) + + # If ':status' pseudo-header is not there in a response header, reject it. + # Similarly, if ':path', ':method', or ':scheme' are not there in a request + # header, reject it. Additionally, if a response contains any request-only + # headers or vice-versa, reject it. + # Relevant RFC section: RFC 7540 § 8.1.2.4 + # https://tools.ietf.org/html/rfc7540#section-8.1.2.4 + if hdr_validation_flags.is_response_header: + _assert_header_in_set(u':status', b':status', pseudo_headers) + invalid_response_headers = pseudo_headers & _REQUEST_ONLY_HEADERS + if invalid_response_headers: + raise ProtocolError( + "Encountered request-only headers %s" % + invalid_response_headers + ) + elif (not hdr_validation_flags.is_response_header and + not hdr_validation_flags.is_trailer): + # This is a request, so we need to have seen :path, :method, and + # :scheme. + _assert_header_in_set(u':path', b':path', pseudo_headers) + _assert_header_in_set(u':method', b':method', pseudo_headers) + _assert_header_in_set(u':scheme', b':scheme', pseudo_headers) + invalid_request_headers = pseudo_headers & _RESPONSE_ONLY_HEADERS + if invalid_request_headers: + raise ProtocolError( + "Encountered response-only headers %s" % + invalid_request_headers + ) + + +def _validate_host_authority_header(headers): + """ + Given the :authority and Host headers from a request block that isn't + a trailer, check that: + 1. At least one of these headers is set. + 2. If both headers are set, they match. + + :param headers: The HTTP header set. + :raises: ``ProtocolError`` + """ + # We use None as a sentinel value. Iterate over the list of headers, + # and record the value of these headers (if present). We don't need + # to worry about receiving duplicate :authority headers, as this is + # enforced by the _reject_pseudo_header_fields() pipeline. + # + # TODO: We should also guard against receiving duplicate Host headers, + # and against sending duplicate headers. + authority_header_val = None + host_header_val = None + + for header in headers: + if header[0] in (b':authority', u':authority'): + authority_header_val = header[1] + elif header[0] in (b'host', u'host'): + host_header_val = header[1] + + yield header + + # If we have not-None values for these variables, then we know we saw + # the corresponding header. + authority_present = (authority_header_val is not None) + host_present = (host_header_val is not None) + + # It is an error for a request header block to contain neither + # an :authority header nor a Host header. + if not authority_present and not host_present: + raise ProtocolError( + "Request header block does not have an :authority or Host header." + ) + + # If we receive both headers, they should definitely match. + if authority_present and host_present: + if authority_header_val != host_header_val: + raise ProtocolError( + "Request header block has mismatched :authority and " + "Host headers: %r / %r" + % (authority_header_val, host_header_val) + ) + + +def _check_host_authority_header(headers, hdr_validation_flags): + """ + Raises a ProtocolError if a header block arrives that does not contain an + :authority or a Host header, or if a header block contains both fields, + but their values do not match. + """ + # We only expect to see :authority and Host headers on request header + # blocks that aren't trailers, so skip this validation if this is a + # response header or we're looking at trailer blocks. + skip_validation = ( + hdr_validation_flags.is_response_header or + hdr_validation_flags.is_trailer + ) + if skip_validation: + return headers + + return _validate_host_authority_header(headers) + + +def _check_path_header(headers, hdr_validation_flags): + """ + Raise a ProtocolError if a header block arrives or is sent that contains an + empty :path header. + """ + def inner(): + for header in headers: + if header[0] in (b':path', u':path'): + if not header[1]: + raise ProtocolError("An empty :path header is forbidden") + + yield header + + # We only expect to see :authority and Host headers on request header + # blocks that aren't trailers, so skip this validation if this is a + # response header or we're looking at trailer blocks. + skip_validation = ( + hdr_validation_flags.is_response_header or + hdr_validation_flags.is_trailer + ) + if skip_validation: + return headers + else: + return inner() + + +def _lowercase_header_names(headers, hdr_validation_flags): + """ + Given an iterable of header two-tuples, rebuilds that iterable with the + header names lowercased. This generator produces tuples that preserve the + original type of the header tuple for tuple and any ``HeaderTuple``. + """ + for header in headers: + if isinstance(header, HeaderTuple): + yield header.__class__(header[0].lower(), header[1]) + else: + yield (header[0].lower(), header[1]) + + +def _strip_surrounding_whitespace(headers, hdr_validation_flags): + """ + Given an iterable of header two-tuples, strip both leading and trailing + whitespace from both header names and header values. This generator + produces tuples that preserve the original type of the header tuple for + tuple and any ``HeaderTuple``. + """ + for header in headers: + if isinstance(header, HeaderTuple): + yield header.__class__(header[0].strip(), header[1].strip()) + else: + yield (header[0].strip(), header[1].strip()) + + +def _strip_connection_headers(headers, hdr_validation_flags): + """ + Strip any connection headers as per RFC7540 § 8.1.2.2. + """ + for header in headers: + if header[0] not in CONNECTION_HEADERS: + yield header + + +def _check_sent_host_authority_header(headers, hdr_validation_flags): + """ + Raises an InvalidHeaderBlockError if we try to send a header block + that does not contain an :authority or a Host header, or if + the header block contains both fields, but their values do not match. + """ + # We only expect to see :authority and Host headers on request header + # blocks that aren't trailers, so skip this validation if this is a + # response header or we're looking at trailer blocks. + skip_validation = ( + hdr_validation_flags.is_response_header or + hdr_validation_flags.is_trailer + ) + if skip_validation: + return headers + + return _validate_host_authority_header(headers) + + +def normalize_outbound_headers(headers, hdr_validation_flags): + """ + Normalizes a header sequence that we are about to send. + + :param headers: The HTTP header set. + :param hdr_validation_flags: An instance of HeaderValidationFlags. + """ + headers = _lowercase_header_names(headers, hdr_validation_flags) + headers = _strip_surrounding_whitespace(headers, hdr_validation_flags) + headers = _strip_connection_headers(headers, hdr_validation_flags) + headers = _secure_headers(headers, hdr_validation_flags) + + return headers + + +def validate_outbound_headers(headers, hdr_validation_flags): + """ + Validates and normalizes a header sequence that we are about to send. + + :param headers: The HTTP header set. + :param hdr_validation_flags: An instance of HeaderValidationFlags. + """ + headers = _reject_te( + headers, hdr_validation_flags + ) + headers = _reject_connection_header( + headers, hdr_validation_flags + ) + headers = _reject_pseudo_header_fields( + headers, hdr_validation_flags + ) + headers = _check_sent_host_authority_header( + headers, hdr_validation_flags + ) + headers = _check_path_header(headers, hdr_validation_flags) + + return headers diff --git a/vinetrimmer/vendor/h2/windows.py b/vinetrimmer/vendor/h2/windows.py new file mode 100644 index 0000000..6656975 --- /dev/null +++ b/vinetrimmer/vendor/h2/windows.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +""" +h2/windows +~~~~~~~~~~ + +Defines tools for managing HTTP/2 flow control windows. + +The objects defined in this module are used to automatically manage HTTP/2 +flow control windows. Specifically, they keep track of what the size of the +window is, how much data has been consumed from that window, and how much data +the user has already used. It then implements a basic algorithm that attempts +to manage the flow control window without user input, trying to ensure that it +does not emit too many WINDOW_UPDATE frames. +""" +from __future__ import division + +from .exceptions import FlowControlError + + +# The largest acceptable value for a HTTP/2 flow control window. +LARGEST_FLOW_CONTROL_WINDOW = 2**31 - 1 + + +class WindowManager(object): + """ + A basic HTTP/2 window manager. + + :param max_window_size: The maximum size of the flow control window. + :type max_window_size: ``int`` + """ + def __init__(self, max_window_size): + assert max_window_size <= LARGEST_FLOW_CONTROL_WINDOW + self.max_window_size = max_window_size + self.current_window_size = max_window_size + self._bytes_processed = 0 + + def window_consumed(self, size): + """ + We have received a certain number of bytes from the remote peer. This + necessarily shrinks the flow control window! + + :param size: The number of flow controlled bytes we received from the + remote peer. + :type size: ``int`` + :returns: Nothing. + :rtype: ``None`` + """ + self.current_window_size -= size + if self.current_window_size < 0: + raise FlowControlError("Flow control window shrunk below 0") + + def window_opened(self, size): + """ + The flow control window has been incremented, either because of manual + flow control management or because of the user changing the flow + control settings. This can have the effect of increasing what we + consider to be the "maximum" flow control window size. + + This does not increase our view of how many bytes have been processed, + only of how much space is in the window. + + :param size: The increment to the flow control window we received. + :type size: ``int`` + :returns: Nothing + :rtype: ``None`` + """ + self.current_window_size += size + + if self.current_window_size > LARGEST_FLOW_CONTROL_WINDOW: + raise FlowControlError( + "Flow control window mustn't exceed %d" % + LARGEST_FLOW_CONTROL_WINDOW + ) + + if self.current_window_size > self.max_window_size: + self.max_window_size = self.current_window_size + + def process_bytes(self, size): + """ + The application has informed us that it has processed a certain number + of bytes. This may cause us to want to emit a window update frame. If + we do want to emit a window update frame, this method will return the + number of bytes that we should increment the window by. + + :param size: The number of flow controlled bytes that the application + has processed. + :type size: ``int`` + :returns: The number of bytes to increment the flow control window by, + or ``None``. + :rtype: ``int`` or ``None`` + """ + self._bytes_processed += size + return self._maybe_update_window() + + def _maybe_update_window(self): + """ + Run the algorithm. + + Our current algorithm can be described like this. + + 1. If no bytes have been processed, we immediately return 0. There is + no meaningful way for us to hand space in the window back to the + remote peer, so let's not even try. + 2. If there is no space in the flow control window, and we have + processed at least 1024 bytes (or 1/4 of the window, if the window + is smaller), we will emit a window update frame. This is to avoid + the risk of blocking a stream altogether. + 3. If there is space in the flow control window, and we have processed + at least 1/2 of the window worth of bytes, we will emit a window + update frame. This is to minimise the number of window update frames + we have to emit. + + In a healthy system with large flow control windows, this will + irregularly emit WINDOW_UPDATE frames. This prevents us starving the + connection by emitting eleventy bajillion WINDOW_UPDATE frames, + especially in situations where the remote peer is sending a lot of very + small DATA frames. + """ + # TODO: Can the window be smaller than 1024 bytes? If not, we can + # streamline this algorithm. + if not self._bytes_processed: + return None + + max_increment = (self.max_window_size - self.current_window_size) + increment = 0 + + # Note that, even though we may increment less than _bytes_processed, + # we still want to set it to zero whenever we emit an increment. This + # is because we'll always increment up to the maximum we can. + if (self.current_window_size == 0) and ( + self._bytes_processed > min(1024, self.max_window_size // 4)): + increment = min(self._bytes_processed, max_increment) + self._bytes_processed = 0 + elif self._bytes_processed >= (self.max_window_size // 2): + increment = min(self._bytes_processed, max_increment) + self._bytes_processed = 0 + + self.current_window_size += increment + return increment diff --git a/vinetrimmer/vendor/hpack/__init__.py b/vinetrimmer/vendor/hpack/__init__.py new file mode 100644 index 0000000..22edde2 --- /dev/null +++ b/vinetrimmer/vendor/hpack/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +""" +hpack +~~~~~ + +HTTP/2 header encoding for Python. +""" +from .hpack import Encoder, Decoder +from .struct import HeaderTuple, NeverIndexedHeaderTuple +from .exceptions import ( + HPACKError, HPACKDecodingError, InvalidTableIndex, OversizedHeaderListError +) + +__all__ = [ + 'Encoder', 'Decoder', 'HPACKError', 'HPACKDecodingError', + 'InvalidTableIndex', 'HeaderTuple', 'NeverIndexedHeaderTuple', + 'OversizedHeaderListError' +] + +__version__ = '3.0.0' diff --git a/vinetrimmer/vendor/hpack/compat.py b/vinetrimmer/vendor/hpack/compat.py new file mode 100644 index 0000000..4fcaad4 --- /dev/null +++ b/vinetrimmer/vendor/hpack/compat.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +""" +hpack/compat +~~~~~~~~~~~~ + +Normalizes the Python 2/3 API for internal use. +""" +import sys + + +_ver = sys.version_info +is_py2 = _ver[0] == 2 +is_py3 = _ver[0] == 3 + +if is_py2: + def to_byte(char): + return ord(char) + + def decode_hex(b): + return b.decode('hex') + + def to_bytes(b): + if isinstance(b, memoryview): + return b.tobytes() + else: + return bytes(b) + + unicode = unicode # noqa + bytes = str + +elif is_py3: + def to_byte(char): + return char + + def decode_hex(b): + return bytes.fromhex(b) + + def to_bytes(b): + return bytes(b) + + unicode = str + bytes = bytes diff --git a/vinetrimmer/vendor/hpack/exceptions.py b/vinetrimmer/vendor/hpack/exceptions.py new file mode 100644 index 0000000..571ba98 --- /dev/null +++ b/vinetrimmer/vendor/hpack/exceptions.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +""" +hyper/http20/exceptions +~~~~~~~~~~~~~~~~~~~~~~~ + +This defines exceptions used in the HTTP/2 portion of hyper. +""" + + +class HPACKError(Exception): + """ + The base class for all ``hpack`` exceptions. + """ + pass + + +class HPACKDecodingError(HPACKError): + """ + An error has been encountered while performing HPACK decoding. + """ + pass + + +class InvalidTableIndex(HPACKDecodingError): + """ + An invalid table index was received. + """ + pass + + +class OversizedHeaderListError(HPACKDecodingError): + """ + A header list that was larger than we allow has been received. This may be + a DoS attack. + + .. versionadded:: 2.3.0 + """ + pass + + +class InvalidTableSizeError(HPACKDecodingError): + """ + An attempt was made to change the decoder table size to a value larger than + allowed, or the list was shrunk and the remote peer didn't shrink their + table size. + + .. versionadded:: 3.0.0 + """ + pass diff --git a/vinetrimmer/vendor/hpack/hpack.py b/vinetrimmer/vendor/hpack/hpack.py new file mode 100644 index 0000000..57e8c7f --- /dev/null +++ b/vinetrimmer/vendor/hpack/hpack.py @@ -0,0 +1,630 @@ +# -*- coding: utf-8 -*- +""" +hpack/hpack +~~~~~~~~~~~ + +Implements the HPACK header compression algorithm as detailed by the IETF. +""" +import logging + +from .table import HeaderTable, table_entry_size +from .compat import to_byte, to_bytes +from .exceptions import ( + HPACKDecodingError, OversizedHeaderListError, InvalidTableSizeError +) +from .huffman import HuffmanEncoder +from .huffman_constants import ( + REQUEST_CODES, REQUEST_CODES_LENGTH +) +from .huffman_table import decode_huffman +from .struct import HeaderTuple, NeverIndexedHeaderTuple + +log = logging.getLogger(__name__) + +INDEX_NONE = b'\x00' +INDEX_NEVER = b'\x10' +INDEX_INCREMENTAL = b'\x40' + +# Precompute 2^i for 1-8 for use in prefix calcs. +# Zero index is not used but there to save a subtraction +# as prefix numbers are not zero indexed. +_PREFIX_BIT_MAX_NUMBERS = [(2 ** i) - 1 for i in range(9)] + +try: # pragma: no cover + basestring = basestring +except NameError: # pragma: no cover + basestring = (str, bytes) + + +# We default the maximum header list we're willing to accept to 64kB. That's a +# lot of headers, but if applications want to raise it they can do. +DEFAULT_MAX_HEADER_LIST_SIZE = 2 ** 16 + + +def _unicode_if_needed(header, raw): + """ + Provides a header as a unicode string if raw is False, otherwise returns + it as a bytestring. + """ + name = to_bytes(header[0]) + value = to_bytes(header[1]) + if not raw: + name = name.decode('utf-8') + value = value.decode('utf-8') + return header.__class__(name, value) + + +def encode_integer(integer, prefix_bits): + """ + This encodes an integer according to the wacky integer encoding rules + defined in the HPACK spec. + """ + log.debug("Encoding %d with %d bits", integer, prefix_bits) + + if integer < 0: + raise ValueError( + "Can only encode positive integers, got %s" % integer + ) + + if prefix_bits < 1 or prefix_bits > 8: + raise ValueError( + "Prefix bits must be between 1 and 8, got %s" % prefix_bits + ) + + max_number = _PREFIX_BIT_MAX_NUMBERS[prefix_bits] + + if integer < max_number: + return bytearray([integer]) # Seriously? + else: + elements = [max_number] + integer -= max_number + + while integer >= 128: + elements.append((integer & 127) + 128) + integer >>= 7 + + elements.append(integer) + + return bytearray(elements) + + +def decode_integer(data, prefix_bits): + """ + This decodes an integer according to the wacky integer encoding rules + defined in the HPACK spec. Returns a tuple of the decoded integer and the + number of bytes that were consumed from ``data`` in order to get that + integer. + """ + if prefix_bits < 1 or prefix_bits > 8: + raise ValueError( + "Prefix bits must be between 1 and 8, got %s" % prefix_bits + ) + + max_number = _PREFIX_BIT_MAX_NUMBERS[prefix_bits] + index = 1 + shift = 0 + mask = (0xFF >> (8 - prefix_bits)) + + try: + number = to_byte(data[0]) & mask + if number == max_number: + while True: + next_byte = to_byte(data[index]) + index += 1 + + if next_byte >= 128: + number += (next_byte - 128) << shift + else: + number += next_byte << shift + break + shift += 7 + + except IndexError: + raise HPACKDecodingError( + "Unable to decode HPACK integer representation from %r" % data + ) + + log.debug("Decoded %d, consumed %d bytes", number, index) + + return number, index + + +def _dict_to_iterable(header_dict): + """ + This converts a dictionary to an iterable of two-tuples. This is a + HPACK-specific function becuase it pulls "special-headers" out first and + then emits them. + """ + assert isinstance(header_dict, dict) + keys = sorted( + header_dict.keys(), + key=lambda k: not _to_bytes(k).startswith(b':') + ) + for key in keys: + yield key, header_dict[key] + + +def _to_bytes(string): + """ + Convert string to bytes. + """ + if not isinstance(string, basestring): # pragma: no cover + string = str(string) + + return string if isinstance(string, bytes) else string.encode('utf-8') + + +class Encoder(object): + """ + An HPACK encoder object. This object takes HTTP headers and emits encoded + HTTP/2 header blocks. + """ + + def __init__(self): + self.header_table = HeaderTable() + self.huffman_coder = HuffmanEncoder( + REQUEST_CODES, REQUEST_CODES_LENGTH + ) + self.table_size_changes = [] + + @property + def header_table_size(self): + """ + Controls the size of the HPACK header table. + """ + return self.header_table.maxsize + + @header_table_size.setter + def header_table_size(self, value): + self.header_table.maxsize = value + if self.header_table.resized: + self.table_size_changes.append(value) + + def encode(self, headers, huffman=True): + """ + Takes a set of headers and encodes them into a HPACK-encoded header + block. + + :param headers: The headers to encode. Must be either an iterable of + tuples, an iterable of :class:`HeaderTuple + <hpack.struct.HeaderTuple>`, or a ``dict``. + + If an iterable of tuples, the tuples may be either + two-tuples or three-tuples. If they are two-tuples, the + tuples must be of the format ``(name, value)``. If they + are three-tuples, they must be of the format + ``(name, value, sensitive)``, where ``sensitive`` is a + boolean value indicating whether the header should be + added to header tables anywhere. If not present, + ``sensitive`` defaults to ``False``. + + If an iterable of :class:`HeaderTuple + <hpack.struct.HeaderTuple>`, the tuples must always be + two-tuples. Instead of using ``sensitive`` as a third + tuple entry, use :class:`NeverIndexedHeaderTuple + <hpack.struct.NeverIndexedHeaderTuple>` to request that + the field never be indexed. + + .. warning:: HTTP/2 requires that all special headers + (headers whose names begin with ``:`` characters) + appear at the *start* of the header block. While + this method will ensure that happens for ``dict`` + subclasses, callers using any other iterable of + tuples **must** ensure they place their special + headers at the start of the iterable. + + For efficiency reasons users should prefer to use + iterables of two-tuples: fixing the ordering of + dictionary headers is an expensive operation that + should be avoided if possible. + + :param huffman: (optional) Whether to Huffman-encode any header sent as + a literal value. Except for use when debugging, it is + recommended that this be left enabled. + + :returns: A bytestring containing the HPACK-encoded header block. + """ + # Transforming the headers into a header block is a procedure that can + # be modeled as a chain or pipe. First, the headers are encoded. This + # encoding can be done a number of ways. If the header name-value pair + # are already in the header table we can represent them using the + # indexed representation: the same is true if they are in the static + # table. Otherwise, a literal representation will be used. + log.debug("HPACK encoding %s", headers) + header_block = [] + + # Turn the headers into a list of tuples if possible. This is the + # natural way to interact with them in HPACK. Because dictionaries are + # un-ordered, we need to make sure we grab the "special" headers first. + if isinstance(headers, dict): + headers = _dict_to_iterable(headers) + + # Before we begin, if the header table size has been changed we need + # to signal all changes since last emission appropriately. + if self.header_table.resized: + header_block.append(self._encode_table_size_change()) + self.header_table.resized = False + + # Add each header to the header block + for header in headers: + sensitive = False + if isinstance(header, HeaderTuple): + sensitive = not header.indexable + elif len(header) > 2: + sensitive = header[2] + + header = (_to_bytes(header[0]), _to_bytes(header[1])) + header_block.append(self.add(header, sensitive, huffman)) + + header_block = b''.join(header_block) + + log.debug("Encoded header block to %s", header_block) + + return header_block + + def add(self, to_add, sensitive, huffman=False): + """ + This function takes a header key-value tuple and serializes it. + """ + log.debug("Adding %s to the header table", to_add) + + name, value = to_add + + # Set our indexing mode + indexbit = INDEX_INCREMENTAL if not sensitive else INDEX_NEVER + + # Search for a matching header in the header table. + match = self.header_table.search(name, value) + + if match is None: + # Not in the header table. Encode using the literal syntax, + # and add it to the header table. + encoded = self._encode_literal(name, value, indexbit, huffman) + if not sensitive: + self.header_table.add(name, value) + return encoded + + # The header is in the table, break out the values. If we matched + # perfectly, we can use the indexed representation: otherwise we + # can use the indexed literal. + index, name, perfect = match + + if perfect: + # Indexed representation. + encoded = self._encode_indexed(index) + else: + # Indexed literal. We are going to add header to the + # header table unconditionally. It is a future todo to + # filter out headers which are known to be ineffective for + # indexing since they just take space in the table and + # pushed out other valuable headers. + encoded = self._encode_indexed_literal( + index, value, indexbit, huffman + ) + if not sensitive: + self.header_table.add(name, value) + + return encoded + + def _encode_indexed(self, index): + """ + Encodes a header using the indexed representation. + """ + field = encode_integer(index, 7) + field[0] |= 0x80 # we set the top bit + return bytes(field) + + def _encode_literal(self, name, value, indexbit, huffman=False): + """ + Encodes a header with a literal name and literal value. If ``indexing`` + is True, the header will be added to the header table: otherwise it + will not. + """ + if huffman: + name = self.huffman_coder.encode(name) + value = self.huffman_coder.encode(value) + + name_len = encode_integer(len(name), 7) + value_len = encode_integer(len(value), 7) + + if huffman: + name_len[0] |= 0x80 + value_len[0] |= 0x80 + + return b''.join( + [indexbit, bytes(name_len), name, bytes(value_len), value] + ) + + def _encode_indexed_literal(self, index, value, indexbit, huffman=False): + """ + Encodes a header with an indexed name and a literal value and performs + incremental indexing. + """ + if indexbit != INDEX_INCREMENTAL: + prefix = encode_integer(index, 4) + else: + prefix = encode_integer(index, 6) + + prefix[0] |= ord(indexbit) + + if huffman: + value = self.huffman_coder.encode(value) + + value_len = encode_integer(len(value), 7) + + if huffman: + value_len[0] |= 0x80 + + return b''.join([bytes(prefix), bytes(value_len), value]) + + def _encode_table_size_change(self): + """ + Produces the encoded form of all header table size change context + updates. + """ + block = b'' + for size_bytes in self.table_size_changes: + size_bytes = encode_integer(size_bytes, 5) + size_bytes[0] |= 0x20 + block += bytes(size_bytes) + self.table_size_changes = [] + return block + + +class Decoder(object): + """ + An HPACK decoder object. + + .. versionchanged:: 2.3.0 + Added ``max_header_list_size`` argument. + + :param max_header_list_size: The maximum decompressed size we will allow + for any single header block. This is a protection against DoS attacks + that attempt to force the application to expand a relatively small + amount of data into a really large header list, allowing enormous + amounts of memory to be allocated. + + If this amount of data is exceeded, a `OversizedHeaderListError + <hpack.OversizedHeaderListError>` exception will be raised. At this + point the connection should be shut down, as the HPACK state will no + longer be useable. + + Defaults to 64kB. + :type max_header_list_size: ``int`` + """ + def __init__(self, max_header_list_size=DEFAULT_MAX_HEADER_LIST_SIZE): + self.header_table = HeaderTable() + + #: The maximum decompressed size we will allow for any single header + #: block. This is a protection against DoS attacks that attempt to + #: force the application to expand a relatively small amount of data + #: into a really large header list, allowing enormous amounts of memory + #: to be allocated. + #: + #: If this amount of data is exceeded, a `OversizedHeaderListError + #: <hpack.OversizedHeaderListError>` exception will be raised. At this + #: point the connection should be shut down, as the HPACK state will no + #: longer be usable. + #: + #: Defaults to 64kB. + #: + #: .. versionadded:: 2.3.0 + self.max_header_list_size = max_header_list_size + + #: Maximum allowed header table size. + #: + #: A HTTP/2 implementation should set this to the most recent value of + #: SETTINGS_HEADER_TABLE_SIZE that it sent *and has received an ACK + #: for*. Once this setting is set, the actual header table size will be + #: checked at the end of each decoding run and whenever it is changed, + #: to confirm that it fits in this size. + self.max_allowed_table_size = self.header_table.maxsize + + @property + def header_table_size(self): + """ + Controls the size of the HPACK header table. + """ + return self.header_table.maxsize + + @header_table_size.setter + def header_table_size(self, value): + self.header_table.maxsize = value + + def decode(self, data, raw=False): + """ + Takes an HPACK-encoded header block and decodes it into a header set. + + :param data: A bytestring representing a complete HPACK-encoded header + block. + :param raw: (optional) Whether to return the headers as tuples of raw + byte strings or to decode them as UTF-8 before returning + them. The default value is False, which returns tuples of + Unicode strings + :returns: A list of two-tuples of ``(name, value)`` representing the + HPACK-encoded headers, in the order they were decoded. + :raises HPACKDecodingError: If an error is encountered while decoding + the header block. + """ + log.debug("Decoding %s", data) + + data_mem = memoryview(data) + headers = [] + data_len = len(data) + inflated_size = 0 + current_index = 0 + + while current_index < data_len: + # Work out what kind of header we're decoding. + # If the high bit is 1, it's an indexed field. + current = to_byte(data[current_index]) + indexed = True if current & 0x80 else False + + # Otherwise, if the second-highest bit is 1 it's a field that does + # alter the header table. + literal_index = True if current & 0x40 else False + + # Otherwise, if the third-highest bit is 1 it's an encoding context + # update. + encoding_update = True if current & 0x20 else False + + if indexed: + header, consumed = self._decode_indexed( + data_mem[current_index:] + ) + elif literal_index: + # It's a literal header that does affect the header table. + header, consumed = self._decode_literal_index( + data_mem[current_index:] + ) + elif encoding_update: + # It's an update to the encoding context. These are forbidden + # in a header block after any actual header. + if headers: + raise HPACKDecodingError( + "Table size update not at the start of the block" + ) + consumed = self._update_encoding_context( + data_mem[current_index:] + ) + header = None + else: + # It's a literal header that does not affect the header table. + header, consumed = self._decode_literal_no_index( + data_mem[current_index:] + ) + + if header: + headers.append(header) + inflated_size += table_entry_size(*header) + + if inflated_size > self.max_header_list_size: + raise OversizedHeaderListError( + "A header list larger than %d has been received" % + self.max_header_list_size + ) + + current_index += consumed + + # Confirm that the table size is lower than the maximum. We do this + # here to ensure that we catch when the max has been *shrunk* and the + # remote peer hasn't actually done that. + self._assert_valid_table_size() + + try: + return [_unicode_if_needed(h, raw) for h in headers] + except UnicodeDecodeError: + raise HPACKDecodingError("Unable to decode headers as UTF-8.") + + def _assert_valid_table_size(self): + """ + Check that the table size set by the encoder is lower than the maximum + we expect to have. + """ + if self.header_table_size > self.max_allowed_table_size: + raise InvalidTableSizeError( + "Encoder did not shrink table size to within the max" + ) + + def _update_encoding_context(self, data): + """ + Handles a byte that updates the encoding context. + """ + # We've been asked to resize the header table. + new_size, consumed = decode_integer(data, 5) + if new_size > self.max_allowed_table_size: + raise InvalidTableSizeError( + "Encoder exceeded max allowable table size" + ) + self.header_table_size = new_size + return consumed + + def _decode_indexed(self, data): + """ + Decodes a header represented using the indexed representation. + """ + index, consumed = decode_integer(data, 7) + header = HeaderTuple(*self.header_table.get_by_index(index)) + log.debug("Decoded %s, consumed %d", header, consumed) + return header, consumed + + def _decode_literal_no_index(self, data): + return self._decode_literal(data, False) + + def _decode_literal_index(self, data): + return self._decode_literal(data, True) + + def _decode_literal(self, data, should_index): + """ + Decodes a header represented with a literal. + """ + total_consumed = 0 + + # When should_index is true, if the low six bits of the first byte are + # nonzero, the header name is indexed. + # When should_index is false, if the low four bits of the first byte + # are nonzero the header name is indexed. + if should_index: + indexed_name = to_byte(data[0]) & 0x3F + name_len = 6 + not_indexable = False + else: + high_byte = to_byte(data[0]) + indexed_name = high_byte & 0x0F + name_len = 4 + not_indexable = high_byte & 0x10 + + if indexed_name: + # Indexed header name. + index, consumed = decode_integer(data, name_len) + name = self.header_table.get_by_index(index)[0] + + total_consumed = consumed + length = 0 + else: + # Literal header name. The first byte was consumed, so we need to + # move forward. + data = data[1:] + + length, consumed = decode_integer(data, 7) + name = data[consumed:consumed + length] + if len(name) != length: + raise HPACKDecodingError("Truncated header block") + + if to_byte(data[0]) & 0x80: + name = decode_huffman(name) + total_consumed = consumed + length + 1 # Since we moved forward 1. + + data = data[consumed + length:] + + # The header value is definitely length-based. + length, consumed = decode_integer(data, 7) + value = data[consumed:consumed + length] + if len(value) != length: + raise HPACKDecodingError("Truncated header block") + + if to_byte(data[0]) & 0x80: + value = decode_huffman(value) + + # Updated the total consumed length. + total_consumed += length + consumed + + # If we have been told never to index the header field, encode that in + # the tuple we use. + if not_indexable: + header = NeverIndexedHeaderTuple(name, value) + else: + header = HeaderTuple(name, value) + + # If we've been asked to index this, add it to the header table. + if should_index: + self.header_table.add(name, value) + + log.debug( + "Decoded %s, total consumed %d bytes, indexed %s", + header, + total_consumed, + should_index + ) + + return header, total_consumed diff --git a/vinetrimmer/vendor/hpack/hpack_compat.py b/vinetrimmer/vendor/hpack/hpack_compat.py new file mode 100644 index 0000000..e5a76d7 --- /dev/null +++ b/vinetrimmer/vendor/hpack/hpack_compat.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +""" +hpack/hpack_compat +~~~~~~~~~~~~~~~~~~ + +Provides an abstraction layer over two HPACK implementations. + +This module has a pure-Python greenfield HPACK implementation that can be used +on all Python platforms. However, this implementation is both slower and more +memory-hungry than could be achieved with a C-language version. Additionally, +nghttp2's HPACK implementation currently achieves better compression ratios +than hyper's in almost all benchmarks. + +For those who care about efficiency and speed in HPACK, this module allows you +to use nghttp2's HPACK implementation instead of ours. This module detects +whether the nghttp2 bindings are installed, and if they are it wraps them in +a hpack-compatible API and uses them instead of its own. If not, it falls back +to the built-in Python bindings. +""" +import logging +from .hpack import _to_bytes + +log = logging.getLogger(__name__) + +# Attempt to import nghttp2. +try: + import nghttp2 + USE_NGHTTP2 = True + log.debug("Using nghttp2's HPACK implementation.") +except ImportError: + USE_NGHTTP2 = False + log.debug("Using our pure-Python HPACK implementation.") + +if USE_NGHTTP2: # noqa + class Encoder(object): + """ + An HPACK encoder object. This object takes HTTP headers and emits + encoded HTTP/2 header blocks. + """ + def __init__(self): + self._e = nghttp2.HDDeflater() + + @property + def header_table_size(self): + """ + Returns the header table size. For the moment this isn't + useful, so we don't use it. + """ + raise NotImplementedError() + + @header_table_size.setter + def header_table_size(self, value): + log.debug("Setting header table size to %d", value) + self._e.change_table_size(value) + + def encode(self, headers, huffman=True): + """ + Encode the headers. The huffman parameter has no effect, it is + simply present for compatibility. + """ + log.debug("HPACK encoding %s", headers) + + # Turn the headers into a list of tuples if possible. This is the + # natural way to interact with them in HPACK. + if isinstance(headers, dict): + headers = headers.items() + + # Next, walk across the headers and turn them all into bytestrings. + headers = [(_to_bytes(n), _to_bytes(v)) for n, v in headers] + + # Now, let nghttp2 do its thing. + header_block = self._e.deflate(headers) + + return header_block + + class Decoder(object): + """ + An HPACK decoder object. + """ + def __init__(self): + self._d = nghttp2.HDInflater() + + @property + def header_table_size(self): + """ + Returns the header table size. For the moment this isn't + useful, so we don't use it. + """ + raise NotImplementedError() + + @header_table_size.setter + def header_table_size(self, value): + log.debug("Setting header table size to %d", value) + self._d.change_table_size(value) + + def decode(self, data): + """ + Takes an HPACK-encoded header block and decodes it into a header + set. + """ + log.debug("Decoding %s", data) + + headers = self._d.inflate(data) + return [(n.decode('utf-8'), v.decode('utf-8')) for n, v in headers] +else: + # Grab the built-in encoder and decoder. + from .hpack import Encoder, Decoder # noqa diff --git a/vinetrimmer/vendor/hpack/huffman.py b/vinetrimmer/vendor/hpack/huffman.py new file mode 100644 index 0000000..159569c --- /dev/null +++ b/vinetrimmer/vendor/hpack/huffman.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +""" +hpack/huffman_decoder +~~~~~~~~~~~~~~~~~~~~~ + +An implementation of a bitwise prefix tree specially built for decoding +Huffman-coded content where we already know the Huffman table. +""" +from .compat import to_byte, decode_hex + + +class HuffmanEncoder(object): + """ + Encodes a string according to the Huffman encoding table defined in the + HPACK specification. + """ + def __init__(self, huffman_code_list, huffman_code_list_lengths): + self.huffman_code_list = huffman_code_list + self.huffman_code_list_lengths = huffman_code_list_lengths + + def encode(self, bytes_to_encode): + """ + Given a string of bytes, encodes them according to the HPACK Huffman + specification. + """ + # If handed the empty string, just immediately return. + if not bytes_to_encode: + return b'' + + final_num = 0 + final_int_len = 0 + + # Turn each byte into its huffman code. These codes aren't necessarily + # octet aligned, so keep track of how far through an octet we are. To + # handle this cleanly, just use a single giant integer. + for char in bytes_to_encode: + byte = to_byte(char) + bin_int_len = self.huffman_code_list_lengths[byte] + bin_int = self.huffman_code_list[byte] & ( + 2 ** (bin_int_len + 1) - 1 + ) + final_num <<= bin_int_len + final_num |= bin_int + final_int_len += bin_int_len + + # Pad out to an octet with ones. + bits_to_be_padded = (8 - (final_int_len % 8)) % 8 + final_num <<= bits_to_be_padded + final_num |= (1 << bits_to_be_padded) - 1 + + # Convert the number to hex and strip off the leading '0x' and the + # trailing 'L', if present. + final_num = hex(final_num)[2:].rstrip('L') + + # If this is odd, prepend a zero. + final_num = '0' + final_num if len(final_num) % 2 != 0 else final_num + + # This number should have twice as many digits as bytes. If not, we're + # missing some leading zeroes. Work out how many bytes we want and how + # many digits we have, then add the missing zero digits to the front. + total_bytes = (final_int_len + bits_to_be_padded) // 8 + expected_digits = total_bytes * 2 + + if len(final_num) != expected_digits: + missing_digits = expected_digits - len(final_num) + final_num = ('0' * missing_digits) + final_num + + return decode_hex(final_num) diff --git a/vinetrimmer/vendor/hpack/huffman_constants.py b/vinetrimmer/vendor/hpack/huffman_constants.py new file mode 100644 index 0000000..c2b3bb2 --- /dev/null +++ b/vinetrimmer/vendor/hpack/huffman_constants.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- +""" +hpack/huffman_constants +~~~~~~~~~~~~~~~~~~~~~~~ + +Defines the constant Huffman table. This takes up an upsetting amount of space, +but c'est la vie. +""" + +REQUEST_CODES = [ + 0x1ff8, + 0x7fffd8, + 0xfffffe2, + 0xfffffe3, + 0xfffffe4, + 0xfffffe5, + 0xfffffe6, + 0xfffffe7, + 0xfffffe8, + 0xffffea, + 0x3ffffffc, + 0xfffffe9, + 0xfffffea, + 0x3ffffffd, + 0xfffffeb, + 0xfffffec, + 0xfffffed, + 0xfffffee, + 0xfffffef, + 0xffffff0, + 0xffffff1, + 0xffffff2, + 0x3ffffffe, + 0xffffff3, + 0xffffff4, + 0xffffff5, + 0xffffff6, + 0xffffff7, + 0xffffff8, + 0xffffff9, + 0xffffffa, + 0xffffffb, + 0x14, + 0x3f8, + 0x3f9, + 0xffa, + 0x1ff9, + 0x15, + 0xf8, + 0x7fa, + 0x3fa, + 0x3fb, + 0xf9, + 0x7fb, + 0xfa, + 0x16, + 0x17, + 0x18, + 0x0, + 0x1, + 0x2, + 0x19, + 0x1a, + 0x1b, + 0x1c, + 0x1d, + 0x1e, + 0x1f, + 0x5c, + 0xfb, + 0x7ffc, + 0x20, + 0xffb, + 0x3fc, + 0x1ffa, + 0x21, + 0x5d, + 0x5e, + 0x5f, + 0x60, + 0x61, + 0x62, + 0x63, + 0x64, + 0x65, + 0x66, + 0x67, + 0x68, + 0x69, + 0x6a, + 0x6b, + 0x6c, + 0x6d, + 0x6e, + 0x6f, + 0x70, + 0x71, + 0x72, + 0xfc, + 0x73, + 0xfd, + 0x1ffb, + 0x7fff0, + 0x1ffc, + 0x3ffc, + 0x22, + 0x7ffd, + 0x3, + 0x23, + 0x4, + 0x24, + 0x5, + 0x25, + 0x26, + 0x27, + 0x6, + 0x74, + 0x75, + 0x28, + 0x29, + 0x2a, + 0x7, + 0x2b, + 0x76, + 0x2c, + 0x8, + 0x9, + 0x2d, + 0x77, + 0x78, + 0x79, + 0x7a, + 0x7b, + 0x7ffe, + 0x7fc, + 0x3ffd, + 0x1ffd, + 0xffffffc, + 0xfffe6, + 0x3fffd2, + 0xfffe7, + 0xfffe8, + 0x3fffd3, + 0x3fffd4, + 0x3fffd5, + 0x7fffd9, + 0x3fffd6, + 0x7fffda, + 0x7fffdb, + 0x7fffdc, + 0x7fffdd, + 0x7fffde, + 0xffffeb, + 0x7fffdf, + 0xffffec, + 0xffffed, + 0x3fffd7, + 0x7fffe0, + 0xffffee, + 0x7fffe1, + 0x7fffe2, + 0x7fffe3, + 0x7fffe4, + 0x1fffdc, + 0x3fffd8, + 0x7fffe5, + 0x3fffd9, + 0x7fffe6, + 0x7fffe7, + 0xffffef, + 0x3fffda, + 0x1fffdd, + 0xfffe9, + 0x3fffdb, + 0x3fffdc, + 0x7fffe8, + 0x7fffe9, + 0x1fffde, + 0x7fffea, + 0x3fffdd, + 0x3fffde, + 0xfffff0, + 0x1fffdf, + 0x3fffdf, + 0x7fffeb, + 0x7fffec, + 0x1fffe0, + 0x1fffe1, + 0x3fffe0, + 0x1fffe2, + 0x7fffed, + 0x3fffe1, + 0x7fffee, + 0x7fffef, + 0xfffea, + 0x3fffe2, + 0x3fffe3, + 0x3fffe4, + 0x7ffff0, + 0x3fffe5, + 0x3fffe6, + 0x7ffff1, + 0x3ffffe0, + 0x3ffffe1, + 0xfffeb, + 0x7fff1, + 0x3fffe7, + 0x7ffff2, + 0x3fffe8, + 0x1ffffec, + 0x3ffffe2, + 0x3ffffe3, + 0x3ffffe4, + 0x7ffffde, + 0x7ffffdf, + 0x3ffffe5, + 0xfffff1, + 0x1ffffed, + 0x7fff2, + 0x1fffe3, + 0x3ffffe6, + 0x7ffffe0, + 0x7ffffe1, + 0x3ffffe7, + 0x7ffffe2, + 0xfffff2, + 0x1fffe4, + 0x1fffe5, + 0x3ffffe8, + 0x3ffffe9, + 0xffffffd, + 0x7ffffe3, + 0x7ffffe4, + 0x7ffffe5, + 0xfffec, + 0xfffff3, + 0xfffed, + 0x1fffe6, + 0x3fffe9, + 0x1fffe7, + 0x1fffe8, + 0x7ffff3, + 0x3fffea, + 0x3fffeb, + 0x1ffffee, + 0x1ffffef, + 0xfffff4, + 0xfffff5, + 0x3ffffea, + 0x7ffff4, + 0x3ffffeb, + 0x7ffffe6, + 0x3ffffec, + 0x3ffffed, + 0x7ffffe7, + 0x7ffffe8, + 0x7ffffe9, + 0x7ffffea, + 0x7ffffeb, + 0xffffffe, + 0x7ffffec, + 0x7ffffed, + 0x7ffffee, + 0x7ffffef, + 0x7fffff0, + 0x3ffffee, + 0x3fffffff, +] + +REQUEST_CODES_LENGTH = [ + 13, 23, 28, 28, 28, 28, 28, 28, 28, 24, 30, 28, 28, 30, 28, 28, + 28, 28, 28, 28, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 6, 10, 10, 12, 13, 6, 8, 11, 10, 10, 8, 11, 8, 6, 6, 6, + 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 15, 6, 12, 10, + 13, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 8, 13, 19, 13, 14, 6, + 15, 5, 6, 5, 6, 5, 6, 6, 6, 5, 7, 7, 6, 6, 6, 5, + 6, 7, 6, 5, 5, 6, 7, 7, 7, 7, 7, 15, 11, 14, 13, 28, + 20, 22, 20, 20, 22, 22, 22, 23, 22, 23, 23, 23, 23, 23, 24, 23, + 24, 24, 22, 23, 24, 23, 23, 23, 23, 21, 22, 23, 22, 23, 23, 24, + 22, 21, 20, 22, 22, 23, 23, 21, 23, 22, 22, 24, 21, 22, 23, 23, + 21, 21, 22, 21, 23, 22, 23, 23, 20, 22, 22, 22, 23, 22, 22, 23, + 26, 26, 20, 19, 22, 23, 22, 25, 26, 26, 26, 27, 27, 26, 24, 25, + 19, 21, 26, 27, 27, 26, 27, 24, 21, 21, 26, 26, 28, 27, 27, 27, + 20, 24, 20, 21, 22, 21, 21, 23, 22, 22, 25, 25, 24, 24, 26, 23, + 26, 27, 26, 26, 27, 27, 27, 27, 27, 28, 27, 27, 27, 27, 27, 26, + 30, +] diff --git a/vinetrimmer/vendor/hpack/huffman_table.py b/vinetrimmer/vendor/hpack/huffman_table.py new file mode 100644 index 0000000..c199ef5 --- /dev/null +++ b/vinetrimmer/vendor/hpack/huffman_table.py @@ -0,0 +1,4739 @@ +# -*- coding: utf-8 -*- +""" +hpack/huffman_table +~~~~~~~~~~~~~~~~~~~ + +This implementation of a Huffman decoding table for HTTP/2 is essentially a +Python port of the work originally done for nghttp2's Huffman decoding. For +this reason, while this file is made available under the MIT license as is the +rest of this module, this file is undoubtedly a derivative work of the nghttp2 +file ``nghttp2_hd_huffman_data.c``, obtained from +https://github.com/tatsuhiro-t/nghttp2/ at commit +d2b55ad1a245e1d1964579fa3fac36ebf3939e72. That work is made available under +the Apache 2.0 license under the following terms: + + Copyright (c) 2013 Tatsuhiro Tsujikawa + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +The essence of this approach is that it builds a finite state machine out of +4-bit nibbles of Huffman coded data. The input function passes 4 bits worth of +data to the state machine each time, which uses those 4 bits of data along with +the current accumulated state data to process the data given. + +For the sake of efficiency, the in-memory representation of the states, +transitions, and result values of the state machine are represented as a long +list containing three-tuples. This list is enormously long, and viewing it as +an in-memory representation is not very clear, but it is laid out here in a way +that is intended to be *somewhat* more clear. + +Essentially, the list is structured as 256 collections of 16 entries (one for +each nibble) of three-tuples. Each collection is called a "node", and the +zeroth collection is called the "root node". The state machine tracks one +value: the "state" byte. + +For each nibble passed to the state machine, it first multiplies the "state" +byte by 16 and adds the numerical value of the nibble. This number is the index +into the large flat list. + +The three-tuple that is found by looking up that index consists of three +values: + +- a new state value, used for subsequent decoding +- a collection of flags, used to determine whether data is emitted or whether + the state machine is complete. +- the byte value to emit, assuming that emitting a byte is required. + +The flags are consulted, if necessary a byte is emitted, and then the next +nibble is used. This continues until the state machine believes it has +completely Huffman-decoded the data. + +This approach has relatively little indirection, and therefore performs +relatively well, particularly on implementations like PyPy where the cost of +loops at the Python-level is not too expensive. The total number of loop +iterations is 4x the number of bytes passed to the decoder. +""" +from .exceptions import HPACKDecodingError + + +# This defines the state machine "class" at the top of the file. The reason we +# do this is to keep the terrifing monster state table at the *bottom* of the +# file so you don't have to actually *look* at the damn thing. +def decode_huffman(huffman_string): + """ + Given a bytestring of Huffman-encoded data for HPACK, returns a bytestring + of the decompressed data. + """ + if not huffman_string: + return b'' + + state = 0 + flags = 0 + decoded_bytes = bytearray() + + # Perversely, bytearrays are a lot more convenient across Python 2 and + # Python 3 because they behave *the same way* on both platforms. Given that + # we really do want numerical bytes when we iterate here, let's use a + # bytearray. + huffman_string = bytearray(huffman_string) + + # This loop is unrolled somewhat. Because we use a nibble, not a byte, we + # need to handle each nibble twice. We unroll that: it makes the loop body + # a bit longer, but that's ok. + for input_byte in huffman_string: + index = (state * 16) + (input_byte >> 4) + state, flags, output_byte = HUFFMAN_TABLE[index] + + if flags & HUFFMAN_FAIL: + raise HPACKDecodingError("Invalid Huffman String") + + if flags & HUFFMAN_EMIT_SYMBOL: + decoded_bytes.append(output_byte) + + index = (state * 16) + (input_byte & 0x0F) + state, flags, output_byte = HUFFMAN_TABLE[index] + + if flags & HUFFMAN_FAIL: + raise HPACKDecodingError("Invalid Huffman String") + + if flags & HUFFMAN_EMIT_SYMBOL: + decoded_bytes.append(output_byte) + + if not (flags & HUFFMAN_COMPLETE): + raise HPACKDecodingError("Incomplete Huffman string") + + return bytes(decoded_bytes) + + +# Some decoder flags to control state transitions. +HUFFMAN_COMPLETE = 1 +HUFFMAN_EMIT_SYMBOL = (1 << 1) +HUFFMAN_FAIL = (1 << 2) + +# This is the monster table. Avert your eyes, children. +HUFFMAN_TABLE = [ + # Node 0 (Root Node, never emits symbols.) + (4, 0, 0), + (5, 0, 0), + (7, 0, 0), + (8, 0, 0), + (11, 0, 0), + (12, 0, 0), + (16, 0, 0), + (19, 0, 0), + (25, 0, 0), + (28, 0, 0), + (32, 0, 0), + (35, 0, 0), + (42, 0, 0), + (49, 0, 0), + (57, 0, 0), + (64, HUFFMAN_COMPLETE, 0), + + # Node 1 + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 48), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 49), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 50), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 97), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 99), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 101), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 105), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 111), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 115), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 116), + (13, 0, 0), + (14, 0, 0), + (17, 0, 0), + (18, 0, 0), + (20, 0, 0), + (21, 0, 0), + + # Node 2 + (1, HUFFMAN_EMIT_SYMBOL, 48), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 48), + (1, HUFFMAN_EMIT_SYMBOL, 49), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 49), + (1, HUFFMAN_EMIT_SYMBOL, 50), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 50), + (1, HUFFMAN_EMIT_SYMBOL, 97), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 97), + (1, HUFFMAN_EMIT_SYMBOL, 99), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 99), + (1, HUFFMAN_EMIT_SYMBOL, 101), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 101), + (1, HUFFMAN_EMIT_SYMBOL, 105), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 105), + (1, HUFFMAN_EMIT_SYMBOL, 111), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 111), + + # Node 3 + (2, HUFFMAN_EMIT_SYMBOL, 48), + (9, HUFFMAN_EMIT_SYMBOL, 48), + (23, HUFFMAN_EMIT_SYMBOL, 48), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 48), + (2, HUFFMAN_EMIT_SYMBOL, 49), + (9, HUFFMAN_EMIT_SYMBOL, 49), + (23, HUFFMAN_EMIT_SYMBOL, 49), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 49), + (2, HUFFMAN_EMIT_SYMBOL, 50), + (9, HUFFMAN_EMIT_SYMBOL, 50), + (23, HUFFMAN_EMIT_SYMBOL, 50), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 50), + (2, HUFFMAN_EMIT_SYMBOL, 97), + (9, HUFFMAN_EMIT_SYMBOL, 97), + (23, HUFFMAN_EMIT_SYMBOL, 97), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 97), + + # Node 4 + (3, HUFFMAN_EMIT_SYMBOL, 48), + (6, HUFFMAN_EMIT_SYMBOL, 48), + (10, HUFFMAN_EMIT_SYMBOL, 48), + (15, HUFFMAN_EMIT_SYMBOL, 48), + (24, HUFFMAN_EMIT_SYMBOL, 48), + (31, HUFFMAN_EMIT_SYMBOL, 48), + (41, HUFFMAN_EMIT_SYMBOL, 48), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 48), + (3, HUFFMAN_EMIT_SYMBOL, 49), + (6, HUFFMAN_EMIT_SYMBOL, 49), + (10, HUFFMAN_EMIT_SYMBOL, 49), + (15, HUFFMAN_EMIT_SYMBOL, 49), + (24, HUFFMAN_EMIT_SYMBOL, 49), + (31, HUFFMAN_EMIT_SYMBOL, 49), + (41, HUFFMAN_EMIT_SYMBOL, 49), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 49), + + # Node 5 + (3, HUFFMAN_EMIT_SYMBOL, 50), + (6, HUFFMAN_EMIT_SYMBOL, 50), + (10, HUFFMAN_EMIT_SYMBOL, 50), + (15, HUFFMAN_EMIT_SYMBOL, 50), + (24, HUFFMAN_EMIT_SYMBOL, 50), + (31, HUFFMAN_EMIT_SYMBOL, 50), + (41, HUFFMAN_EMIT_SYMBOL, 50), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 50), + (3, HUFFMAN_EMIT_SYMBOL, 97), + (6, HUFFMAN_EMIT_SYMBOL, 97), + (10, HUFFMAN_EMIT_SYMBOL, 97), + (15, HUFFMAN_EMIT_SYMBOL, 97), + (24, HUFFMAN_EMIT_SYMBOL, 97), + (31, HUFFMAN_EMIT_SYMBOL, 97), + (41, HUFFMAN_EMIT_SYMBOL, 97), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 97), + + # Node 6 + (2, HUFFMAN_EMIT_SYMBOL, 99), + (9, HUFFMAN_EMIT_SYMBOL, 99), + (23, HUFFMAN_EMIT_SYMBOL, 99), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 99), + (2, HUFFMAN_EMIT_SYMBOL, 101), + (9, HUFFMAN_EMIT_SYMBOL, 101), + (23, HUFFMAN_EMIT_SYMBOL, 101), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 101), + (2, HUFFMAN_EMIT_SYMBOL, 105), + (9, HUFFMAN_EMIT_SYMBOL, 105), + (23, HUFFMAN_EMIT_SYMBOL, 105), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 105), + (2, HUFFMAN_EMIT_SYMBOL, 111), + (9, HUFFMAN_EMIT_SYMBOL, 111), + (23, HUFFMAN_EMIT_SYMBOL, 111), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 111), + + # Node 7 + (3, HUFFMAN_EMIT_SYMBOL, 99), + (6, HUFFMAN_EMIT_SYMBOL, 99), + (10, HUFFMAN_EMIT_SYMBOL, 99), + (15, HUFFMAN_EMIT_SYMBOL, 99), + (24, HUFFMAN_EMIT_SYMBOL, 99), + (31, HUFFMAN_EMIT_SYMBOL, 99), + (41, HUFFMAN_EMIT_SYMBOL, 99), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 99), + (3, HUFFMAN_EMIT_SYMBOL, 101), + (6, HUFFMAN_EMIT_SYMBOL, 101), + (10, HUFFMAN_EMIT_SYMBOL, 101), + (15, HUFFMAN_EMIT_SYMBOL, 101), + (24, HUFFMAN_EMIT_SYMBOL, 101), + (31, HUFFMAN_EMIT_SYMBOL, 101), + (41, HUFFMAN_EMIT_SYMBOL, 101), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 101), + + # Node 8 + (3, HUFFMAN_EMIT_SYMBOL, 105), + (6, HUFFMAN_EMIT_SYMBOL, 105), + (10, HUFFMAN_EMIT_SYMBOL, 105), + (15, HUFFMAN_EMIT_SYMBOL, 105), + (24, HUFFMAN_EMIT_SYMBOL, 105), + (31, HUFFMAN_EMIT_SYMBOL, 105), + (41, HUFFMAN_EMIT_SYMBOL, 105), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 105), + (3, HUFFMAN_EMIT_SYMBOL, 111), + (6, HUFFMAN_EMIT_SYMBOL, 111), + (10, HUFFMAN_EMIT_SYMBOL, 111), + (15, HUFFMAN_EMIT_SYMBOL, 111), + (24, HUFFMAN_EMIT_SYMBOL, 111), + (31, HUFFMAN_EMIT_SYMBOL, 111), + (41, HUFFMAN_EMIT_SYMBOL, 111), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 111), + + # Node 9 + (1, HUFFMAN_EMIT_SYMBOL, 115), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 115), + (1, HUFFMAN_EMIT_SYMBOL, 116), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 116), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 32), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 37), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 45), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 46), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 47), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 51), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 52), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 53), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 54), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 55), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 56), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 57), + + # Node 10 + (2, HUFFMAN_EMIT_SYMBOL, 115), + (9, HUFFMAN_EMIT_SYMBOL, 115), + (23, HUFFMAN_EMIT_SYMBOL, 115), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 115), + (2, HUFFMAN_EMIT_SYMBOL, 116), + (9, HUFFMAN_EMIT_SYMBOL, 116), + (23, HUFFMAN_EMIT_SYMBOL, 116), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 116), + (1, HUFFMAN_EMIT_SYMBOL, 32), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 32), + (1, HUFFMAN_EMIT_SYMBOL, 37), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 37), + (1, HUFFMAN_EMIT_SYMBOL, 45), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 45), + (1, HUFFMAN_EMIT_SYMBOL, 46), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 46), + + # Node 11 + (3, HUFFMAN_EMIT_SYMBOL, 115), + (6, HUFFMAN_EMIT_SYMBOL, 115), + (10, HUFFMAN_EMIT_SYMBOL, 115), + (15, HUFFMAN_EMIT_SYMBOL, 115), + (24, HUFFMAN_EMIT_SYMBOL, 115), + (31, HUFFMAN_EMIT_SYMBOL, 115), + (41, HUFFMAN_EMIT_SYMBOL, 115), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 115), + (3, HUFFMAN_EMIT_SYMBOL, 116), + (6, HUFFMAN_EMIT_SYMBOL, 116), + (10, HUFFMAN_EMIT_SYMBOL, 116), + (15, HUFFMAN_EMIT_SYMBOL, 116), + (24, HUFFMAN_EMIT_SYMBOL, 116), + (31, HUFFMAN_EMIT_SYMBOL, 116), + (41, HUFFMAN_EMIT_SYMBOL, 116), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 116), + + # Node 12 + (2, HUFFMAN_EMIT_SYMBOL, 32), + (9, HUFFMAN_EMIT_SYMBOL, 32), + (23, HUFFMAN_EMIT_SYMBOL, 32), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 32), + (2, HUFFMAN_EMIT_SYMBOL, 37), + (9, HUFFMAN_EMIT_SYMBOL, 37), + (23, HUFFMAN_EMIT_SYMBOL, 37), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 37), + (2, HUFFMAN_EMIT_SYMBOL, 45), + (9, HUFFMAN_EMIT_SYMBOL, 45), + (23, HUFFMAN_EMIT_SYMBOL, 45), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 45), + (2, HUFFMAN_EMIT_SYMBOL, 46), + (9, HUFFMAN_EMIT_SYMBOL, 46), + (23, HUFFMAN_EMIT_SYMBOL, 46), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 46), + + # Node 13 + (3, HUFFMAN_EMIT_SYMBOL, 32), + (6, HUFFMAN_EMIT_SYMBOL, 32), + (10, HUFFMAN_EMIT_SYMBOL, 32), + (15, HUFFMAN_EMIT_SYMBOL, 32), + (24, HUFFMAN_EMIT_SYMBOL, 32), + (31, HUFFMAN_EMIT_SYMBOL, 32), + (41, HUFFMAN_EMIT_SYMBOL, 32), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 32), + (3, HUFFMAN_EMIT_SYMBOL, 37), + (6, HUFFMAN_EMIT_SYMBOL, 37), + (10, HUFFMAN_EMIT_SYMBOL, 37), + (15, HUFFMAN_EMIT_SYMBOL, 37), + (24, HUFFMAN_EMIT_SYMBOL, 37), + (31, HUFFMAN_EMIT_SYMBOL, 37), + (41, HUFFMAN_EMIT_SYMBOL, 37), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 37), + + # Node 14 + (3, HUFFMAN_EMIT_SYMBOL, 45), + (6, HUFFMAN_EMIT_SYMBOL, 45), + (10, HUFFMAN_EMIT_SYMBOL, 45), + (15, HUFFMAN_EMIT_SYMBOL, 45), + (24, HUFFMAN_EMIT_SYMBOL, 45), + (31, HUFFMAN_EMIT_SYMBOL, 45), + (41, HUFFMAN_EMIT_SYMBOL, 45), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 45), + (3, HUFFMAN_EMIT_SYMBOL, 46), + (6, HUFFMAN_EMIT_SYMBOL, 46), + (10, HUFFMAN_EMIT_SYMBOL, 46), + (15, HUFFMAN_EMIT_SYMBOL, 46), + (24, HUFFMAN_EMIT_SYMBOL, 46), + (31, HUFFMAN_EMIT_SYMBOL, 46), + (41, HUFFMAN_EMIT_SYMBOL, 46), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 46), + + # Node 15 + (1, HUFFMAN_EMIT_SYMBOL, 47), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 47), + (1, HUFFMAN_EMIT_SYMBOL, 51), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 51), + (1, HUFFMAN_EMIT_SYMBOL, 52), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 52), + (1, HUFFMAN_EMIT_SYMBOL, 53), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 53), + (1, HUFFMAN_EMIT_SYMBOL, 54), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 54), + (1, HUFFMAN_EMIT_SYMBOL, 55), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 55), + (1, HUFFMAN_EMIT_SYMBOL, 56), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 56), + (1, HUFFMAN_EMIT_SYMBOL, 57), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 57), + + # Node 16 + (2, HUFFMAN_EMIT_SYMBOL, 47), + (9, HUFFMAN_EMIT_SYMBOL, 47), + (23, HUFFMAN_EMIT_SYMBOL, 47), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 47), + (2, HUFFMAN_EMIT_SYMBOL, 51), + (9, HUFFMAN_EMIT_SYMBOL, 51), + (23, HUFFMAN_EMIT_SYMBOL, 51), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 51), + (2, HUFFMAN_EMIT_SYMBOL, 52), + (9, HUFFMAN_EMIT_SYMBOL, 52), + (23, HUFFMAN_EMIT_SYMBOL, 52), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 52), + (2, HUFFMAN_EMIT_SYMBOL, 53), + (9, HUFFMAN_EMIT_SYMBOL, 53), + (23, HUFFMAN_EMIT_SYMBOL, 53), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 53), + + # Node 17 + (3, HUFFMAN_EMIT_SYMBOL, 47), + (6, HUFFMAN_EMIT_SYMBOL, 47), + (10, HUFFMAN_EMIT_SYMBOL, 47), + (15, HUFFMAN_EMIT_SYMBOL, 47), + (24, HUFFMAN_EMIT_SYMBOL, 47), + (31, HUFFMAN_EMIT_SYMBOL, 47), + (41, HUFFMAN_EMIT_SYMBOL, 47), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 47), + (3, HUFFMAN_EMIT_SYMBOL, 51), + (6, HUFFMAN_EMIT_SYMBOL, 51), + (10, HUFFMAN_EMIT_SYMBOL, 51), + (15, HUFFMAN_EMIT_SYMBOL, 51), + (24, HUFFMAN_EMIT_SYMBOL, 51), + (31, HUFFMAN_EMIT_SYMBOL, 51), + (41, HUFFMAN_EMIT_SYMBOL, 51), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 51), + + # Node 18 + (3, HUFFMAN_EMIT_SYMBOL, 52), + (6, HUFFMAN_EMIT_SYMBOL, 52), + (10, HUFFMAN_EMIT_SYMBOL, 52), + (15, HUFFMAN_EMIT_SYMBOL, 52), + (24, HUFFMAN_EMIT_SYMBOL, 52), + (31, HUFFMAN_EMIT_SYMBOL, 52), + (41, HUFFMAN_EMIT_SYMBOL, 52), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 52), + (3, HUFFMAN_EMIT_SYMBOL, 53), + (6, HUFFMAN_EMIT_SYMBOL, 53), + (10, HUFFMAN_EMIT_SYMBOL, 53), + (15, HUFFMAN_EMIT_SYMBOL, 53), + (24, HUFFMAN_EMIT_SYMBOL, 53), + (31, HUFFMAN_EMIT_SYMBOL, 53), + (41, HUFFMAN_EMIT_SYMBOL, 53), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 53), + + # Node 19 + (2, HUFFMAN_EMIT_SYMBOL, 54), + (9, HUFFMAN_EMIT_SYMBOL, 54), + (23, HUFFMAN_EMIT_SYMBOL, 54), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 54), + (2, HUFFMAN_EMIT_SYMBOL, 55), + (9, HUFFMAN_EMIT_SYMBOL, 55), + (23, HUFFMAN_EMIT_SYMBOL, 55), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 55), + (2, HUFFMAN_EMIT_SYMBOL, 56), + (9, HUFFMAN_EMIT_SYMBOL, 56), + (23, HUFFMAN_EMIT_SYMBOL, 56), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 56), + (2, HUFFMAN_EMIT_SYMBOL, 57), + (9, HUFFMAN_EMIT_SYMBOL, 57), + (23, HUFFMAN_EMIT_SYMBOL, 57), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 57), + + # Node 20 + (3, HUFFMAN_EMIT_SYMBOL, 54), + (6, HUFFMAN_EMIT_SYMBOL, 54), + (10, HUFFMAN_EMIT_SYMBOL, 54), + (15, HUFFMAN_EMIT_SYMBOL, 54), + (24, HUFFMAN_EMIT_SYMBOL, 54), + (31, HUFFMAN_EMIT_SYMBOL, 54), + (41, HUFFMAN_EMIT_SYMBOL, 54), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 54), + (3, HUFFMAN_EMIT_SYMBOL, 55), + (6, HUFFMAN_EMIT_SYMBOL, 55), + (10, HUFFMAN_EMIT_SYMBOL, 55), + (15, HUFFMAN_EMIT_SYMBOL, 55), + (24, HUFFMAN_EMIT_SYMBOL, 55), + (31, HUFFMAN_EMIT_SYMBOL, 55), + (41, HUFFMAN_EMIT_SYMBOL, 55), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 55), + + # Node 21 + (3, HUFFMAN_EMIT_SYMBOL, 56), + (6, HUFFMAN_EMIT_SYMBOL, 56), + (10, HUFFMAN_EMIT_SYMBOL, 56), + (15, HUFFMAN_EMIT_SYMBOL, 56), + (24, HUFFMAN_EMIT_SYMBOL, 56), + (31, HUFFMAN_EMIT_SYMBOL, 56), + (41, HUFFMAN_EMIT_SYMBOL, 56), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 56), + (3, HUFFMAN_EMIT_SYMBOL, 57), + (6, HUFFMAN_EMIT_SYMBOL, 57), + (10, HUFFMAN_EMIT_SYMBOL, 57), + (15, HUFFMAN_EMIT_SYMBOL, 57), + (24, HUFFMAN_EMIT_SYMBOL, 57), + (31, HUFFMAN_EMIT_SYMBOL, 57), + (41, HUFFMAN_EMIT_SYMBOL, 57), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 57), + + # Node 22 + (26, 0, 0), + (27, 0, 0), + (29, 0, 0), + (30, 0, 0), + (33, 0, 0), + (34, 0, 0), + (36, 0, 0), + (37, 0, 0), + (43, 0, 0), + (46, 0, 0), + (50, 0, 0), + (53, 0, 0), + (58, 0, 0), + (61, 0, 0), + (65, 0, 0), + (68, HUFFMAN_COMPLETE, 0), + + # Node 23 + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 61), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 65), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 95), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 98), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 100), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 102), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 103), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 104), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 108), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 109), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 110), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 112), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 114), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 117), + (38, 0, 0), + (39, 0, 0), + + # Node 24 + (1, HUFFMAN_EMIT_SYMBOL, 61), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 61), + (1, HUFFMAN_EMIT_SYMBOL, 65), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 65), + (1, HUFFMAN_EMIT_SYMBOL, 95), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 95), + (1, HUFFMAN_EMIT_SYMBOL, 98), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 98), + (1, HUFFMAN_EMIT_SYMBOL, 100), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 100), + (1, HUFFMAN_EMIT_SYMBOL, 102), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 102), + (1, HUFFMAN_EMIT_SYMBOL, 103), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 103), + (1, HUFFMAN_EMIT_SYMBOL, 104), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 104), + + # Node 25 + (2, HUFFMAN_EMIT_SYMBOL, 61), + (9, HUFFMAN_EMIT_SYMBOL, 61), + (23, HUFFMAN_EMIT_SYMBOL, 61), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 61), + (2, HUFFMAN_EMIT_SYMBOL, 65), + (9, HUFFMAN_EMIT_SYMBOL, 65), + (23, HUFFMAN_EMIT_SYMBOL, 65), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 65), + (2, HUFFMAN_EMIT_SYMBOL, 95), + (9, HUFFMAN_EMIT_SYMBOL, 95), + (23, HUFFMAN_EMIT_SYMBOL, 95), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 95), + (2, HUFFMAN_EMIT_SYMBOL, 98), + (9, HUFFMAN_EMIT_SYMBOL, 98), + (23, HUFFMAN_EMIT_SYMBOL, 98), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 98), + + # Node 26 + (3, HUFFMAN_EMIT_SYMBOL, 61), + (6, HUFFMAN_EMIT_SYMBOL, 61), + (10, HUFFMAN_EMIT_SYMBOL, 61), + (15, HUFFMAN_EMIT_SYMBOL, 61), + (24, HUFFMAN_EMIT_SYMBOL, 61), + (31, HUFFMAN_EMIT_SYMBOL, 61), + (41, HUFFMAN_EMIT_SYMBOL, 61), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 61), + (3, HUFFMAN_EMIT_SYMBOL, 65), + (6, HUFFMAN_EMIT_SYMBOL, 65), + (10, HUFFMAN_EMIT_SYMBOL, 65), + (15, HUFFMAN_EMIT_SYMBOL, 65), + (24, HUFFMAN_EMIT_SYMBOL, 65), + (31, HUFFMAN_EMIT_SYMBOL, 65), + (41, HUFFMAN_EMIT_SYMBOL, 65), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 65), + + # Node 27 + (3, HUFFMAN_EMIT_SYMBOL, 95), + (6, HUFFMAN_EMIT_SYMBOL, 95), + (10, HUFFMAN_EMIT_SYMBOL, 95), + (15, HUFFMAN_EMIT_SYMBOL, 95), + (24, HUFFMAN_EMIT_SYMBOL, 95), + (31, HUFFMAN_EMIT_SYMBOL, 95), + (41, HUFFMAN_EMIT_SYMBOL, 95), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 95), + (3, HUFFMAN_EMIT_SYMBOL, 98), + (6, HUFFMAN_EMIT_SYMBOL, 98), + (10, HUFFMAN_EMIT_SYMBOL, 98), + (15, HUFFMAN_EMIT_SYMBOL, 98), + (24, HUFFMAN_EMIT_SYMBOL, 98), + (31, HUFFMAN_EMIT_SYMBOL, 98), + (41, HUFFMAN_EMIT_SYMBOL, 98), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 98), + + # Node 28 + (2, HUFFMAN_EMIT_SYMBOL, 100), + (9, HUFFMAN_EMIT_SYMBOL, 100), + (23, HUFFMAN_EMIT_SYMBOL, 100), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 100), + (2, HUFFMAN_EMIT_SYMBOL, 102), + (9, HUFFMAN_EMIT_SYMBOL, 102), + (23, HUFFMAN_EMIT_SYMBOL, 102), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 102), + (2, HUFFMAN_EMIT_SYMBOL, 103), + (9, HUFFMAN_EMIT_SYMBOL, 103), + (23, HUFFMAN_EMIT_SYMBOL, 103), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 103), + (2, HUFFMAN_EMIT_SYMBOL, 104), + (9, HUFFMAN_EMIT_SYMBOL, 104), + (23, HUFFMAN_EMIT_SYMBOL, 104), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 104), + + # Node 29 + (3, HUFFMAN_EMIT_SYMBOL, 100), + (6, HUFFMAN_EMIT_SYMBOL, 100), + (10, HUFFMAN_EMIT_SYMBOL, 100), + (15, HUFFMAN_EMIT_SYMBOL, 100), + (24, HUFFMAN_EMIT_SYMBOL, 100), + (31, HUFFMAN_EMIT_SYMBOL, 100), + (41, HUFFMAN_EMIT_SYMBOL, 100), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 100), + (3, HUFFMAN_EMIT_SYMBOL, 102), + (6, HUFFMAN_EMIT_SYMBOL, 102), + (10, HUFFMAN_EMIT_SYMBOL, 102), + (15, HUFFMAN_EMIT_SYMBOL, 102), + (24, HUFFMAN_EMIT_SYMBOL, 102), + (31, HUFFMAN_EMIT_SYMBOL, 102), + (41, HUFFMAN_EMIT_SYMBOL, 102), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 102), + + # Node 30 + (3, HUFFMAN_EMIT_SYMBOL, 103), + (6, HUFFMAN_EMIT_SYMBOL, 103), + (10, HUFFMAN_EMIT_SYMBOL, 103), + (15, HUFFMAN_EMIT_SYMBOL, 103), + (24, HUFFMAN_EMIT_SYMBOL, 103), + (31, HUFFMAN_EMIT_SYMBOL, 103), + (41, HUFFMAN_EMIT_SYMBOL, 103), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 103), + (3, HUFFMAN_EMIT_SYMBOL, 104), + (6, HUFFMAN_EMIT_SYMBOL, 104), + (10, HUFFMAN_EMIT_SYMBOL, 104), + (15, HUFFMAN_EMIT_SYMBOL, 104), + (24, HUFFMAN_EMIT_SYMBOL, 104), + (31, HUFFMAN_EMIT_SYMBOL, 104), + (41, HUFFMAN_EMIT_SYMBOL, 104), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 104), + + # Node 31 + (1, HUFFMAN_EMIT_SYMBOL, 108), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 108), + (1, HUFFMAN_EMIT_SYMBOL, 109), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 109), + (1, HUFFMAN_EMIT_SYMBOL, 110), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 110), + (1, HUFFMAN_EMIT_SYMBOL, 112), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 112), + (1, HUFFMAN_EMIT_SYMBOL, 114), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 114), + (1, HUFFMAN_EMIT_SYMBOL, 117), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 117), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 58), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 66), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 67), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 68), + + # Node 32 + (2, HUFFMAN_EMIT_SYMBOL, 108), + (9, HUFFMAN_EMIT_SYMBOL, 108), + (23, HUFFMAN_EMIT_SYMBOL, 108), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 108), + (2, HUFFMAN_EMIT_SYMBOL, 109), + (9, HUFFMAN_EMIT_SYMBOL, 109), + (23, HUFFMAN_EMIT_SYMBOL, 109), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 109), + (2, HUFFMAN_EMIT_SYMBOL, 110), + (9, HUFFMAN_EMIT_SYMBOL, 110), + (23, HUFFMAN_EMIT_SYMBOL, 110), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 110), + (2, HUFFMAN_EMIT_SYMBOL, 112), + (9, HUFFMAN_EMIT_SYMBOL, 112), + (23, HUFFMAN_EMIT_SYMBOL, 112), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 112), + + # Node 33 + (3, HUFFMAN_EMIT_SYMBOL, 108), + (6, HUFFMAN_EMIT_SYMBOL, 108), + (10, HUFFMAN_EMIT_SYMBOL, 108), + (15, HUFFMAN_EMIT_SYMBOL, 108), + (24, HUFFMAN_EMIT_SYMBOL, 108), + (31, HUFFMAN_EMIT_SYMBOL, 108), + (41, HUFFMAN_EMIT_SYMBOL, 108), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 108), + (3, HUFFMAN_EMIT_SYMBOL, 109), + (6, HUFFMAN_EMIT_SYMBOL, 109), + (10, HUFFMAN_EMIT_SYMBOL, 109), + (15, HUFFMAN_EMIT_SYMBOL, 109), + (24, HUFFMAN_EMIT_SYMBOL, 109), + (31, HUFFMAN_EMIT_SYMBOL, 109), + (41, HUFFMAN_EMIT_SYMBOL, 109), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 109), + + # Node 34 + (3, HUFFMAN_EMIT_SYMBOL, 110), + (6, HUFFMAN_EMIT_SYMBOL, 110), + (10, HUFFMAN_EMIT_SYMBOL, 110), + (15, HUFFMAN_EMIT_SYMBOL, 110), + (24, HUFFMAN_EMIT_SYMBOL, 110), + (31, HUFFMAN_EMIT_SYMBOL, 110), + (41, HUFFMAN_EMIT_SYMBOL, 110), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 110), + (3, HUFFMAN_EMIT_SYMBOL, 112), + (6, HUFFMAN_EMIT_SYMBOL, 112), + (10, HUFFMAN_EMIT_SYMBOL, 112), + (15, HUFFMAN_EMIT_SYMBOL, 112), + (24, HUFFMAN_EMIT_SYMBOL, 112), + (31, HUFFMAN_EMIT_SYMBOL, 112), + (41, HUFFMAN_EMIT_SYMBOL, 112), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 112), + + # Node 35 + (2, HUFFMAN_EMIT_SYMBOL, 114), + (9, HUFFMAN_EMIT_SYMBOL, 114), + (23, HUFFMAN_EMIT_SYMBOL, 114), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 114), + (2, HUFFMAN_EMIT_SYMBOL, 117), + (9, HUFFMAN_EMIT_SYMBOL, 117), + (23, HUFFMAN_EMIT_SYMBOL, 117), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 117), + (1, HUFFMAN_EMIT_SYMBOL, 58), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 58), + (1, HUFFMAN_EMIT_SYMBOL, 66), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 66), + (1, HUFFMAN_EMIT_SYMBOL, 67), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 67), + (1, HUFFMAN_EMIT_SYMBOL, 68), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 68), + + # Node 36 + (3, HUFFMAN_EMIT_SYMBOL, 114), + (6, HUFFMAN_EMIT_SYMBOL, 114), + (10, HUFFMAN_EMIT_SYMBOL, 114), + (15, HUFFMAN_EMIT_SYMBOL, 114), + (24, HUFFMAN_EMIT_SYMBOL, 114), + (31, HUFFMAN_EMIT_SYMBOL, 114), + (41, HUFFMAN_EMIT_SYMBOL, 114), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 114), + (3, HUFFMAN_EMIT_SYMBOL, 117), + (6, HUFFMAN_EMIT_SYMBOL, 117), + (10, HUFFMAN_EMIT_SYMBOL, 117), + (15, HUFFMAN_EMIT_SYMBOL, 117), + (24, HUFFMAN_EMIT_SYMBOL, 117), + (31, HUFFMAN_EMIT_SYMBOL, 117), + (41, HUFFMAN_EMIT_SYMBOL, 117), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 117), + + # Node 37 + (2, HUFFMAN_EMIT_SYMBOL, 58), + (9, HUFFMAN_EMIT_SYMBOL, 58), + (23, HUFFMAN_EMIT_SYMBOL, 58), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 58), + (2, HUFFMAN_EMIT_SYMBOL, 66), + (9, HUFFMAN_EMIT_SYMBOL, 66), + (23, HUFFMAN_EMIT_SYMBOL, 66), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 66), + (2, HUFFMAN_EMIT_SYMBOL, 67), + (9, HUFFMAN_EMIT_SYMBOL, 67), + (23, HUFFMAN_EMIT_SYMBOL, 67), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 67), + (2, HUFFMAN_EMIT_SYMBOL, 68), + (9, HUFFMAN_EMIT_SYMBOL, 68), + (23, HUFFMAN_EMIT_SYMBOL, 68), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 68), + + # Node 38 + (3, HUFFMAN_EMIT_SYMBOL, 58), + (6, HUFFMAN_EMIT_SYMBOL, 58), + (10, HUFFMAN_EMIT_SYMBOL, 58), + (15, HUFFMAN_EMIT_SYMBOL, 58), + (24, HUFFMAN_EMIT_SYMBOL, 58), + (31, HUFFMAN_EMIT_SYMBOL, 58), + (41, HUFFMAN_EMIT_SYMBOL, 58), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 58), + (3, HUFFMAN_EMIT_SYMBOL, 66), + (6, HUFFMAN_EMIT_SYMBOL, 66), + (10, HUFFMAN_EMIT_SYMBOL, 66), + (15, HUFFMAN_EMIT_SYMBOL, 66), + (24, HUFFMAN_EMIT_SYMBOL, 66), + (31, HUFFMAN_EMIT_SYMBOL, 66), + (41, HUFFMAN_EMIT_SYMBOL, 66), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 66), + + # Node 39 + (3, HUFFMAN_EMIT_SYMBOL, 67), + (6, HUFFMAN_EMIT_SYMBOL, 67), + (10, HUFFMAN_EMIT_SYMBOL, 67), + (15, HUFFMAN_EMIT_SYMBOL, 67), + (24, HUFFMAN_EMIT_SYMBOL, 67), + (31, HUFFMAN_EMIT_SYMBOL, 67), + (41, HUFFMAN_EMIT_SYMBOL, 67), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 67), + (3, HUFFMAN_EMIT_SYMBOL, 68), + (6, HUFFMAN_EMIT_SYMBOL, 68), + (10, HUFFMAN_EMIT_SYMBOL, 68), + (15, HUFFMAN_EMIT_SYMBOL, 68), + (24, HUFFMAN_EMIT_SYMBOL, 68), + (31, HUFFMAN_EMIT_SYMBOL, 68), + (41, HUFFMAN_EMIT_SYMBOL, 68), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 68), + + # Node 40 + (44, 0, 0), + (45, 0, 0), + (47, 0, 0), + (48, 0, 0), + (51, 0, 0), + (52, 0, 0), + (54, 0, 0), + (55, 0, 0), + (59, 0, 0), + (60, 0, 0), + (62, 0, 0), + (63, 0, 0), + (66, 0, 0), + (67, 0, 0), + (69, 0, 0), + (72, HUFFMAN_COMPLETE, 0), + + # Node 41 + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 69), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 70), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 71), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 72), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 73), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 74), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 75), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 76), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 77), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 78), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 79), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 80), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 81), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 82), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 83), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 84), + + # Node 42 + (1, HUFFMAN_EMIT_SYMBOL, 69), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 69), + (1, HUFFMAN_EMIT_SYMBOL, 70), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 70), + (1, HUFFMAN_EMIT_SYMBOL, 71), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 71), + (1, HUFFMAN_EMIT_SYMBOL, 72), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 72), + (1, HUFFMAN_EMIT_SYMBOL, 73), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 73), + (1, HUFFMAN_EMIT_SYMBOL, 74), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 74), + (1, HUFFMAN_EMIT_SYMBOL, 75), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 75), + (1, HUFFMAN_EMIT_SYMBOL, 76), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 76), + + # Node 43 + (2, HUFFMAN_EMIT_SYMBOL, 69), + (9, HUFFMAN_EMIT_SYMBOL, 69), + (23, HUFFMAN_EMIT_SYMBOL, 69), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 69), + (2, HUFFMAN_EMIT_SYMBOL, 70), + (9, HUFFMAN_EMIT_SYMBOL, 70), + (23, HUFFMAN_EMIT_SYMBOL, 70), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 70), + (2, HUFFMAN_EMIT_SYMBOL, 71), + (9, HUFFMAN_EMIT_SYMBOL, 71), + (23, HUFFMAN_EMIT_SYMBOL, 71), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 71), + (2, HUFFMAN_EMIT_SYMBOL, 72), + (9, HUFFMAN_EMIT_SYMBOL, 72), + (23, HUFFMAN_EMIT_SYMBOL, 72), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 72), + + # Node 44 + (3, HUFFMAN_EMIT_SYMBOL, 69), + (6, HUFFMAN_EMIT_SYMBOL, 69), + (10, HUFFMAN_EMIT_SYMBOL, 69), + (15, HUFFMAN_EMIT_SYMBOL, 69), + (24, HUFFMAN_EMIT_SYMBOL, 69), + (31, HUFFMAN_EMIT_SYMBOL, 69), + (41, HUFFMAN_EMIT_SYMBOL, 69), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 69), + (3, HUFFMAN_EMIT_SYMBOL, 70), + (6, HUFFMAN_EMIT_SYMBOL, 70), + (10, HUFFMAN_EMIT_SYMBOL, 70), + (15, HUFFMAN_EMIT_SYMBOL, 70), + (24, HUFFMAN_EMIT_SYMBOL, 70), + (31, HUFFMAN_EMIT_SYMBOL, 70), + (41, HUFFMAN_EMIT_SYMBOL, 70), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 70), + + # Node 45 + (3, HUFFMAN_EMIT_SYMBOL, 71), + (6, HUFFMAN_EMIT_SYMBOL, 71), + (10, HUFFMAN_EMIT_SYMBOL, 71), + (15, HUFFMAN_EMIT_SYMBOL, 71), + (24, HUFFMAN_EMIT_SYMBOL, 71), + (31, HUFFMAN_EMIT_SYMBOL, 71), + (41, HUFFMAN_EMIT_SYMBOL, 71), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 71), + (3, HUFFMAN_EMIT_SYMBOL, 72), + (6, HUFFMAN_EMIT_SYMBOL, 72), + (10, HUFFMAN_EMIT_SYMBOL, 72), + (15, HUFFMAN_EMIT_SYMBOL, 72), + (24, HUFFMAN_EMIT_SYMBOL, 72), + (31, HUFFMAN_EMIT_SYMBOL, 72), + (41, HUFFMAN_EMIT_SYMBOL, 72), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 72), + + # Node 46 + (2, HUFFMAN_EMIT_SYMBOL, 73), + (9, HUFFMAN_EMIT_SYMBOL, 73), + (23, HUFFMAN_EMIT_SYMBOL, 73), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 73), + (2, HUFFMAN_EMIT_SYMBOL, 74), + (9, HUFFMAN_EMIT_SYMBOL, 74), + (23, HUFFMAN_EMIT_SYMBOL, 74), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 74), + (2, HUFFMAN_EMIT_SYMBOL, 75), + (9, HUFFMAN_EMIT_SYMBOL, 75), + (23, HUFFMAN_EMIT_SYMBOL, 75), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 75), + (2, HUFFMAN_EMIT_SYMBOL, 76), + (9, HUFFMAN_EMIT_SYMBOL, 76), + (23, HUFFMAN_EMIT_SYMBOL, 76), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 76), + + # Node 47 + (3, HUFFMAN_EMIT_SYMBOL, 73), + (6, HUFFMAN_EMIT_SYMBOL, 73), + (10, HUFFMAN_EMIT_SYMBOL, 73), + (15, HUFFMAN_EMIT_SYMBOL, 73), + (24, HUFFMAN_EMIT_SYMBOL, 73), + (31, HUFFMAN_EMIT_SYMBOL, 73), + (41, HUFFMAN_EMIT_SYMBOL, 73), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 73), + (3, HUFFMAN_EMIT_SYMBOL, 74), + (6, HUFFMAN_EMIT_SYMBOL, 74), + (10, HUFFMAN_EMIT_SYMBOL, 74), + (15, HUFFMAN_EMIT_SYMBOL, 74), + (24, HUFFMAN_EMIT_SYMBOL, 74), + (31, HUFFMAN_EMIT_SYMBOL, 74), + (41, HUFFMAN_EMIT_SYMBOL, 74), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 74), + + # Node 48 + (3, HUFFMAN_EMIT_SYMBOL, 75), + (6, HUFFMAN_EMIT_SYMBOL, 75), + (10, HUFFMAN_EMIT_SYMBOL, 75), + (15, HUFFMAN_EMIT_SYMBOL, 75), + (24, HUFFMAN_EMIT_SYMBOL, 75), + (31, HUFFMAN_EMIT_SYMBOL, 75), + (41, HUFFMAN_EMIT_SYMBOL, 75), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 75), + (3, HUFFMAN_EMIT_SYMBOL, 76), + (6, HUFFMAN_EMIT_SYMBOL, 76), + (10, HUFFMAN_EMIT_SYMBOL, 76), + (15, HUFFMAN_EMIT_SYMBOL, 76), + (24, HUFFMAN_EMIT_SYMBOL, 76), + (31, HUFFMAN_EMIT_SYMBOL, 76), + (41, HUFFMAN_EMIT_SYMBOL, 76), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 76), + + # Node 49 + (1, HUFFMAN_EMIT_SYMBOL, 77), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 77), + (1, HUFFMAN_EMIT_SYMBOL, 78), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 78), + (1, HUFFMAN_EMIT_SYMBOL, 79), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 79), + (1, HUFFMAN_EMIT_SYMBOL, 80), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 80), + (1, HUFFMAN_EMIT_SYMBOL, 81), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 81), + (1, HUFFMAN_EMIT_SYMBOL, 82), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 82), + (1, HUFFMAN_EMIT_SYMBOL, 83), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 83), + (1, HUFFMAN_EMIT_SYMBOL, 84), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 84), + + # Node 50 + (2, HUFFMAN_EMIT_SYMBOL, 77), + (9, HUFFMAN_EMIT_SYMBOL, 77), + (23, HUFFMAN_EMIT_SYMBOL, 77), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 77), + (2, HUFFMAN_EMIT_SYMBOL, 78), + (9, HUFFMAN_EMIT_SYMBOL, 78), + (23, HUFFMAN_EMIT_SYMBOL, 78), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 78), + (2, HUFFMAN_EMIT_SYMBOL, 79), + (9, HUFFMAN_EMIT_SYMBOL, 79), + (23, HUFFMAN_EMIT_SYMBOL, 79), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 79), + (2, HUFFMAN_EMIT_SYMBOL, 80), + (9, HUFFMAN_EMIT_SYMBOL, 80), + (23, HUFFMAN_EMIT_SYMBOL, 80), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 80), + + # Node 51 + (3, HUFFMAN_EMIT_SYMBOL, 77), + (6, HUFFMAN_EMIT_SYMBOL, 77), + (10, HUFFMAN_EMIT_SYMBOL, 77), + (15, HUFFMAN_EMIT_SYMBOL, 77), + (24, HUFFMAN_EMIT_SYMBOL, 77), + (31, HUFFMAN_EMIT_SYMBOL, 77), + (41, HUFFMAN_EMIT_SYMBOL, 77), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 77), + (3, HUFFMAN_EMIT_SYMBOL, 78), + (6, HUFFMAN_EMIT_SYMBOL, 78), + (10, HUFFMAN_EMIT_SYMBOL, 78), + (15, HUFFMAN_EMIT_SYMBOL, 78), + (24, HUFFMAN_EMIT_SYMBOL, 78), + (31, HUFFMAN_EMIT_SYMBOL, 78), + (41, HUFFMAN_EMIT_SYMBOL, 78), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 78), + + # Node 52 + (3, HUFFMAN_EMIT_SYMBOL, 79), + (6, HUFFMAN_EMIT_SYMBOL, 79), + (10, HUFFMAN_EMIT_SYMBOL, 79), + (15, HUFFMAN_EMIT_SYMBOL, 79), + (24, HUFFMAN_EMIT_SYMBOL, 79), + (31, HUFFMAN_EMIT_SYMBOL, 79), + (41, HUFFMAN_EMIT_SYMBOL, 79), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 79), + (3, HUFFMAN_EMIT_SYMBOL, 80), + (6, HUFFMAN_EMIT_SYMBOL, 80), + (10, HUFFMAN_EMIT_SYMBOL, 80), + (15, HUFFMAN_EMIT_SYMBOL, 80), + (24, HUFFMAN_EMIT_SYMBOL, 80), + (31, HUFFMAN_EMIT_SYMBOL, 80), + (41, HUFFMAN_EMIT_SYMBOL, 80), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 80), + + # Node 53 + (2, HUFFMAN_EMIT_SYMBOL, 81), + (9, HUFFMAN_EMIT_SYMBOL, 81), + (23, HUFFMAN_EMIT_SYMBOL, 81), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 81), + (2, HUFFMAN_EMIT_SYMBOL, 82), + (9, HUFFMAN_EMIT_SYMBOL, 82), + (23, HUFFMAN_EMIT_SYMBOL, 82), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 82), + (2, HUFFMAN_EMIT_SYMBOL, 83), + (9, HUFFMAN_EMIT_SYMBOL, 83), + (23, HUFFMAN_EMIT_SYMBOL, 83), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 83), + (2, HUFFMAN_EMIT_SYMBOL, 84), + (9, HUFFMAN_EMIT_SYMBOL, 84), + (23, HUFFMAN_EMIT_SYMBOL, 84), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 84), + + # Node 54 + (3, HUFFMAN_EMIT_SYMBOL, 81), + (6, HUFFMAN_EMIT_SYMBOL, 81), + (10, HUFFMAN_EMIT_SYMBOL, 81), + (15, HUFFMAN_EMIT_SYMBOL, 81), + (24, HUFFMAN_EMIT_SYMBOL, 81), + (31, HUFFMAN_EMIT_SYMBOL, 81), + (41, HUFFMAN_EMIT_SYMBOL, 81), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 81), + (3, HUFFMAN_EMIT_SYMBOL, 82), + (6, HUFFMAN_EMIT_SYMBOL, 82), + (10, HUFFMAN_EMIT_SYMBOL, 82), + (15, HUFFMAN_EMIT_SYMBOL, 82), + (24, HUFFMAN_EMIT_SYMBOL, 82), + (31, HUFFMAN_EMIT_SYMBOL, 82), + (41, HUFFMAN_EMIT_SYMBOL, 82), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 82), + + # Node 55 + (3, HUFFMAN_EMIT_SYMBOL, 83), + (6, HUFFMAN_EMIT_SYMBOL, 83), + (10, HUFFMAN_EMIT_SYMBOL, 83), + (15, HUFFMAN_EMIT_SYMBOL, 83), + (24, HUFFMAN_EMIT_SYMBOL, 83), + (31, HUFFMAN_EMIT_SYMBOL, 83), + (41, HUFFMAN_EMIT_SYMBOL, 83), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 83), + (3, HUFFMAN_EMIT_SYMBOL, 84), + (6, HUFFMAN_EMIT_SYMBOL, 84), + (10, HUFFMAN_EMIT_SYMBOL, 84), + (15, HUFFMAN_EMIT_SYMBOL, 84), + (24, HUFFMAN_EMIT_SYMBOL, 84), + (31, HUFFMAN_EMIT_SYMBOL, 84), + (41, HUFFMAN_EMIT_SYMBOL, 84), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 84), + + # Node 56 + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 85), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 86), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 87), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 89), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 106), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 107), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 113), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 118), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 119), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 120), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 121), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 122), + (70, 0, 0), + (71, 0, 0), + (73, 0, 0), + (74, HUFFMAN_COMPLETE, 0), + + # Node 57 + (1, HUFFMAN_EMIT_SYMBOL, 85), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 85), + (1, HUFFMAN_EMIT_SYMBOL, 86), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 86), + (1, HUFFMAN_EMIT_SYMBOL, 87), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 87), + (1, HUFFMAN_EMIT_SYMBOL, 89), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 89), + (1, HUFFMAN_EMIT_SYMBOL, 106), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 106), + (1, HUFFMAN_EMIT_SYMBOL, 107), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 107), + (1, HUFFMAN_EMIT_SYMBOL, 113), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 113), + (1, HUFFMAN_EMIT_SYMBOL, 118), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 118), + + # Node 58 + (2, HUFFMAN_EMIT_SYMBOL, 85), + (9, HUFFMAN_EMIT_SYMBOL, 85), + (23, HUFFMAN_EMIT_SYMBOL, 85), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 85), + (2, HUFFMAN_EMIT_SYMBOL, 86), + (9, HUFFMAN_EMIT_SYMBOL, 86), + (23, HUFFMAN_EMIT_SYMBOL, 86), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 86), + (2, HUFFMAN_EMIT_SYMBOL, 87), + (9, HUFFMAN_EMIT_SYMBOL, 87), + (23, HUFFMAN_EMIT_SYMBOL, 87), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 87), + (2, HUFFMAN_EMIT_SYMBOL, 89), + (9, HUFFMAN_EMIT_SYMBOL, 89), + (23, HUFFMAN_EMIT_SYMBOL, 89), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 89), + + # Node 59 + (3, HUFFMAN_EMIT_SYMBOL, 85), + (6, HUFFMAN_EMIT_SYMBOL, 85), + (10, HUFFMAN_EMIT_SYMBOL, 85), + (15, HUFFMAN_EMIT_SYMBOL, 85), + (24, HUFFMAN_EMIT_SYMBOL, 85), + (31, HUFFMAN_EMIT_SYMBOL, 85), + (41, HUFFMAN_EMIT_SYMBOL, 85), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 85), + (3, HUFFMAN_EMIT_SYMBOL, 86), + (6, HUFFMAN_EMIT_SYMBOL, 86), + (10, HUFFMAN_EMIT_SYMBOL, 86), + (15, HUFFMAN_EMIT_SYMBOL, 86), + (24, HUFFMAN_EMIT_SYMBOL, 86), + (31, HUFFMAN_EMIT_SYMBOL, 86), + (41, HUFFMAN_EMIT_SYMBOL, 86), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 86), + + # Node 60 + (3, HUFFMAN_EMIT_SYMBOL, 87), + (6, HUFFMAN_EMIT_SYMBOL, 87), + (10, HUFFMAN_EMIT_SYMBOL, 87), + (15, HUFFMAN_EMIT_SYMBOL, 87), + (24, HUFFMAN_EMIT_SYMBOL, 87), + (31, HUFFMAN_EMIT_SYMBOL, 87), + (41, HUFFMAN_EMIT_SYMBOL, 87), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 87), + (3, HUFFMAN_EMIT_SYMBOL, 89), + (6, HUFFMAN_EMIT_SYMBOL, 89), + (10, HUFFMAN_EMIT_SYMBOL, 89), + (15, HUFFMAN_EMIT_SYMBOL, 89), + (24, HUFFMAN_EMIT_SYMBOL, 89), + (31, HUFFMAN_EMIT_SYMBOL, 89), + (41, HUFFMAN_EMIT_SYMBOL, 89), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 89), + + # Node 61 + (2, HUFFMAN_EMIT_SYMBOL, 106), + (9, HUFFMAN_EMIT_SYMBOL, 106), + (23, HUFFMAN_EMIT_SYMBOL, 106), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 106), + (2, HUFFMAN_EMIT_SYMBOL, 107), + (9, HUFFMAN_EMIT_SYMBOL, 107), + (23, HUFFMAN_EMIT_SYMBOL, 107), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 107), + (2, HUFFMAN_EMIT_SYMBOL, 113), + (9, HUFFMAN_EMIT_SYMBOL, 113), + (23, HUFFMAN_EMIT_SYMBOL, 113), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 113), + (2, HUFFMAN_EMIT_SYMBOL, 118), + (9, HUFFMAN_EMIT_SYMBOL, 118), + (23, HUFFMAN_EMIT_SYMBOL, 118), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 118), + + # Node 62 + (3, HUFFMAN_EMIT_SYMBOL, 106), + (6, HUFFMAN_EMIT_SYMBOL, 106), + (10, HUFFMAN_EMIT_SYMBOL, 106), + (15, HUFFMAN_EMIT_SYMBOL, 106), + (24, HUFFMAN_EMIT_SYMBOL, 106), + (31, HUFFMAN_EMIT_SYMBOL, 106), + (41, HUFFMAN_EMIT_SYMBOL, 106), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 106), + (3, HUFFMAN_EMIT_SYMBOL, 107), + (6, HUFFMAN_EMIT_SYMBOL, 107), + (10, HUFFMAN_EMIT_SYMBOL, 107), + (15, HUFFMAN_EMIT_SYMBOL, 107), + (24, HUFFMAN_EMIT_SYMBOL, 107), + (31, HUFFMAN_EMIT_SYMBOL, 107), + (41, HUFFMAN_EMIT_SYMBOL, 107), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 107), + + # Node 63 + (3, HUFFMAN_EMIT_SYMBOL, 113), + (6, HUFFMAN_EMIT_SYMBOL, 113), + (10, HUFFMAN_EMIT_SYMBOL, 113), + (15, HUFFMAN_EMIT_SYMBOL, 113), + (24, HUFFMAN_EMIT_SYMBOL, 113), + (31, HUFFMAN_EMIT_SYMBOL, 113), + (41, HUFFMAN_EMIT_SYMBOL, 113), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 113), + (3, HUFFMAN_EMIT_SYMBOL, 118), + (6, HUFFMAN_EMIT_SYMBOL, 118), + (10, HUFFMAN_EMIT_SYMBOL, 118), + (15, HUFFMAN_EMIT_SYMBOL, 118), + (24, HUFFMAN_EMIT_SYMBOL, 118), + (31, HUFFMAN_EMIT_SYMBOL, 118), + (41, HUFFMAN_EMIT_SYMBOL, 118), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 118), + + # Node 64 + (1, HUFFMAN_EMIT_SYMBOL, 119), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 119), + (1, HUFFMAN_EMIT_SYMBOL, 120), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 120), + (1, HUFFMAN_EMIT_SYMBOL, 121), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 121), + (1, HUFFMAN_EMIT_SYMBOL, 122), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 122), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 38), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 42), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 44), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 59), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 88), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 90), + (75, 0, 0), + (78, 0, 0), + + # Node 65 + (2, HUFFMAN_EMIT_SYMBOL, 119), + (9, HUFFMAN_EMIT_SYMBOL, 119), + (23, HUFFMAN_EMIT_SYMBOL, 119), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 119), + (2, HUFFMAN_EMIT_SYMBOL, 120), + (9, HUFFMAN_EMIT_SYMBOL, 120), + (23, HUFFMAN_EMIT_SYMBOL, 120), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 120), + (2, HUFFMAN_EMIT_SYMBOL, 121), + (9, HUFFMAN_EMIT_SYMBOL, 121), + (23, HUFFMAN_EMIT_SYMBOL, 121), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 121), + (2, HUFFMAN_EMIT_SYMBOL, 122), + (9, HUFFMAN_EMIT_SYMBOL, 122), + (23, HUFFMAN_EMIT_SYMBOL, 122), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 122), + + # Node 66 + (3, HUFFMAN_EMIT_SYMBOL, 119), + (6, HUFFMAN_EMIT_SYMBOL, 119), + (10, HUFFMAN_EMIT_SYMBOL, 119), + (15, HUFFMAN_EMIT_SYMBOL, 119), + (24, HUFFMAN_EMIT_SYMBOL, 119), + (31, HUFFMAN_EMIT_SYMBOL, 119), + (41, HUFFMAN_EMIT_SYMBOL, 119), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 119), + (3, HUFFMAN_EMIT_SYMBOL, 120), + (6, HUFFMAN_EMIT_SYMBOL, 120), + (10, HUFFMAN_EMIT_SYMBOL, 120), + (15, HUFFMAN_EMIT_SYMBOL, 120), + (24, HUFFMAN_EMIT_SYMBOL, 120), + (31, HUFFMAN_EMIT_SYMBOL, 120), + (41, HUFFMAN_EMIT_SYMBOL, 120), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 120), + + # Node 67 + (3, HUFFMAN_EMIT_SYMBOL, 121), + (6, HUFFMAN_EMIT_SYMBOL, 121), + (10, HUFFMAN_EMIT_SYMBOL, 121), + (15, HUFFMAN_EMIT_SYMBOL, 121), + (24, HUFFMAN_EMIT_SYMBOL, 121), + (31, HUFFMAN_EMIT_SYMBOL, 121), + (41, HUFFMAN_EMIT_SYMBOL, 121), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 121), + (3, HUFFMAN_EMIT_SYMBOL, 122), + (6, HUFFMAN_EMIT_SYMBOL, 122), + (10, HUFFMAN_EMIT_SYMBOL, 122), + (15, HUFFMAN_EMIT_SYMBOL, 122), + (24, HUFFMAN_EMIT_SYMBOL, 122), + (31, HUFFMAN_EMIT_SYMBOL, 122), + (41, HUFFMAN_EMIT_SYMBOL, 122), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 122), + + # Node 68 + (1, HUFFMAN_EMIT_SYMBOL, 38), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 38), + (1, HUFFMAN_EMIT_SYMBOL, 42), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 42), + (1, HUFFMAN_EMIT_SYMBOL, 44), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 44), + (1, HUFFMAN_EMIT_SYMBOL, 59), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 59), + (1, HUFFMAN_EMIT_SYMBOL, 88), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 88), + (1, HUFFMAN_EMIT_SYMBOL, 90), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 90), + (76, 0, 0), + (77, 0, 0), + (79, 0, 0), + (81, 0, 0), + + # Node 69 + (2, HUFFMAN_EMIT_SYMBOL, 38), + (9, HUFFMAN_EMIT_SYMBOL, 38), + (23, HUFFMAN_EMIT_SYMBOL, 38), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 38), + (2, HUFFMAN_EMIT_SYMBOL, 42), + (9, HUFFMAN_EMIT_SYMBOL, 42), + (23, HUFFMAN_EMIT_SYMBOL, 42), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 42), + (2, HUFFMAN_EMIT_SYMBOL, 44), + (9, HUFFMAN_EMIT_SYMBOL, 44), + (23, HUFFMAN_EMIT_SYMBOL, 44), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 44), + (2, HUFFMAN_EMIT_SYMBOL, 59), + (9, HUFFMAN_EMIT_SYMBOL, 59), + (23, HUFFMAN_EMIT_SYMBOL, 59), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 59), + + # Node 70 + (3, HUFFMAN_EMIT_SYMBOL, 38), + (6, HUFFMAN_EMIT_SYMBOL, 38), + (10, HUFFMAN_EMIT_SYMBOL, 38), + (15, HUFFMAN_EMIT_SYMBOL, 38), + (24, HUFFMAN_EMIT_SYMBOL, 38), + (31, HUFFMAN_EMIT_SYMBOL, 38), + (41, HUFFMAN_EMIT_SYMBOL, 38), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 38), + (3, HUFFMAN_EMIT_SYMBOL, 42), + (6, HUFFMAN_EMIT_SYMBOL, 42), + (10, HUFFMAN_EMIT_SYMBOL, 42), + (15, HUFFMAN_EMIT_SYMBOL, 42), + (24, HUFFMAN_EMIT_SYMBOL, 42), + (31, HUFFMAN_EMIT_SYMBOL, 42), + (41, HUFFMAN_EMIT_SYMBOL, 42), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 42), + + # Node 71 + (3, HUFFMAN_EMIT_SYMBOL, 44), + (6, HUFFMAN_EMIT_SYMBOL, 44), + (10, HUFFMAN_EMIT_SYMBOL, 44), + (15, HUFFMAN_EMIT_SYMBOL, 44), + (24, HUFFMAN_EMIT_SYMBOL, 44), + (31, HUFFMAN_EMIT_SYMBOL, 44), + (41, HUFFMAN_EMIT_SYMBOL, 44), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 44), + (3, HUFFMAN_EMIT_SYMBOL, 59), + (6, HUFFMAN_EMIT_SYMBOL, 59), + (10, HUFFMAN_EMIT_SYMBOL, 59), + (15, HUFFMAN_EMIT_SYMBOL, 59), + (24, HUFFMAN_EMIT_SYMBOL, 59), + (31, HUFFMAN_EMIT_SYMBOL, 59), + (41, HUFFMAN_EMIT_SYMBOL, 59), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 59), + + # Node 72 + (2, HUFFMAN_EMIT_SYMBOL, 88), + (9, HUFFMAN_EMIT_SYMBOL, 88), + (23, HUFFMAN_EMIT_SYMBOL, 88), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 88), + (2, HUFFMAN_EMIT_SYMBOL, 90), + (9, HUFFMAN_EMIT_SYMBOL, 90), + (23, HUFFMAN_EMIT_SYMBOL, 90), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 90), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 33), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 34), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 40), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 41), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 63), + (80, 0, 0), + (82, 0, 0), + (84, 0, 0), + + # Node 73 + (3, HUFFMAN_EMIT_SYMBOL, 88), + (6, HUFFMAN_EMIT_SYMBOL, 88), + (10, HUFFMAN_EMIT_SYMBOL, 88), + (15, HUFFMAN_EMIT_SYMBOL, 88), + (24, HUFFMAN_EMIT_SYMBOL, 88), + (31, HUFFMAN_EMIT_SYMBOL, 88), + (41, HUFFMAN_EMIT_SYMBOL, 88), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 88), + (3, HUFFMAN_EMIT_SYMBOL, 90), + (6, HUFFMAN_EMIT_SYMBOL, 90), + (10, HUFFMAN_EMIT_SYMBOL, 90), + (15, HUFFMAN_EMIT_SYMBOL, 90), + (24, HUFFMAN_EMIT_SYMBOL, 90), + (31, HUFFMAN_EMIT_SYMBOL, 90), + (41, HUFFMAN_EMIT_SYMBOL, 90), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 90), + + # Node 74 + (1, HUFFMAN_EMIT_SYMBOL, 33), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 33), + (1, HUFFMAN_EMIT_SYMBOL, 34), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 34), + (1, HUFFMAN_EMIT_SYMBOL, 40), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 40), + (1, HUFFMAN_EMIT_SYMBOL, 41), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 41), + (1, HUFFMAN_EMIT_SYMBOL, 63), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 63), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 39), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 43), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 124), + (83, 0, 0), + (85, 0, 0), + (88, 0, 0), + + # Node 75 + (2, HUFFMAN_EMIT_SYMBOL, 33), + (9, HUFFMAN_EMIT_SYMBOL, 33), + (23, HUFFMAN_EMIT_SYMBOL, 33), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 33), + (2, HUFFMAN_EMIT_SYMBOL, 34), + (9, HUFFMAN_EMIT_SYMBOL, 34), + (23, HUFFMAN_EMIT_SYMBOL, 34), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 34), + (2, HUFFMAN_EMIT_SYMBOL, 40), + (9, HUFFMAN_EMIT_SYMBOL, 40), + (23, HUFFMAN_EMIT_SYMBOL, 40), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 40), + (2, HUFFMAN_EMIT_SYMBOL, 41), + (9, HUFFMAN_EMIT_SYMBOL, 41), + (23, HUFFMAN_EMIT_SYMBOL, 41), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 41), + + # Node 76 + (3, HUFFMAN_EMIT_SYMBOL, 33), + (6, HUFFMAN_EMIT_SYMBOL, 33), + (10, HUFFMAN_EMIT_SYMBOL, 33), + (15, HUFFMAN_EMIT_SYMBOL, 33), + (24, HUFFMAN_EMIT_SYMBOL, 33), + (31, HUFFMAN_EMIT_SYMBOL, 33), + (41, HUFFMAN_EMIT_SYMBOL, 33), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 33), + (3, HUFFMAN_EMIT_SYMBOL, 34), + (6, HUFFMAN_EMIT_SYMBOL, 34), + (10, HUFFMAN_EMIT_SYMBOL, 34), + (15, HUFFMAN_EMIT_SYMBOL, 34), + (24, HUFFMAN_EMIT_SYMBOL, 34), + (31, HUFFMAN_EMIT_SYMBOL, 34), + (41, HUFFMAN_EMIT_SYMBOL, 34), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 34), + + # Node 77 + (3, HUFFMAN_EMIT_SYMBOL, 40), + (6, HUFFMAN_EMIT_SYMBOL, 40), + (10, HUFFMAN_EMIT_SYMBOL, 40), + (15, HUFFMAN_EMIT_SYMBOL, 40), + (24, HUFFMAN_EMIT_SYMBOL, 40), + (31, HUFFMAN_EMIT_SYMBOL, 40), + (41, HUFFMAN_EMIT_SYMBOL, 40), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 40), + (3, HUFFMAN_EMIT_SYMBOL, 41), + (6, HUFFMAN_EMIT_SYMBOL, 41), + (10, HUFFMAN_EMIT_SYMBOL, 41), + (15, HUFFMAN_EMIT_SYMBOL, 41), + (24, HUFFMAN_EMIT_SYMBOL, 41), + (31, HUFFMAN_EMIT_SYMBOL, 41), + (41, HUFFMAN_EMIT_SYMBOL, 41), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 41), + + # Node 78 + (2, HUFFMAN_EMIT_SYMBOL, 63), + (9, HUFFMAN_EMIT_SYMBOL, 63), + (23, HUFFMAN_EMIT_SYMBOL, 63), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 63), + (1, HUFFMAN_EMIT_SYMBOL, 39), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 39), + (1, HUFFMAN_EMIT_SYMBOL, 43), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 43), + (1, HUFFMAN_EMIT_SYMBOL, 124), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 124), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 35), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 62), + (86, 0, 0), + (87, 0, 0), + (89, 0, 0), + (90, 0, 0), + + # Node 79 + (3, HUFFMAN_EMIT_SYMBOL, 63), + (6, HUFFMAN_EMIT_SYMBOL, 63), + (10, HUFFMAN_EMIT_SYMBOL, 63), + (15, HUFFMAN_EMIT_SYMBOL, 63), + (24, HUFFMAN_EMIT_SYMBOL, 63), + (31, HUFFMAN_EMIT_SYMBOL, 63), + (41, HUFFMAN_EMIT_SYMBOL, 63), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 63), + (2, HUFFMAN_EMIT_SYMBOL, 39), + (9, HUFFMAN_EMIT_SYMBOL, 39), + (23, HUFFMAN_EMIT_SYMBOL, 39), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 39), + (2, HUFFMAN_EMIT_SYMBOL, 43), + (9, HUFFMAN_EMIT_SYMBOL, 43), + (23, HUFFMAN_EMIT_SYMBOL, 43), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 43), + + # Node 80 + (3, HUFFMAN_EMIT_SYMBOL, 39), + (6, HUFFMAN_EMIT_SYMBOL, 39), + (10, HUFFMAN_EMIT_SYMBOL, 39), + (15, HUFFMAN_EMIT_SYMBOL, 39), + (24, HUFFMAN_EMIT_SYMBOL, 39), + (31, HUFFMAN_EMIT_SYMBOL, 39), + (41, HUFFMAN_EMIT_SYMBOL, 39), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 39), + (3, HUFFMAN_EMIT_SYMBOL, 43), + (6, HUFFMAN_EMIT_SYMBOL, 43), + (10, HUFFMAN_EMIT_SYMBOL, 43), + (15, HUFFMAN_EMIT_SYMBOL, 43), + (24, HUFFMAN_EMIT_SYMBOL, 43), + (31, HUFFMAN_EMIT_SYMBOL, 43), + (41, HUFFMAN_EMIT_SYMBOL, 43), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 43), + + # Node 81 + (2, HUFFMAN_EMIT_SYMBOL, 124), + (9, HUFFMAN_EMIT_SYMBOL, 124), + (23, HUFFMAN_EMIT_SYMBOL, 124), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 124), + (1, HUFFMAN_EMIT_SYMBOL, 35), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 35), + (1, HUFFMAN_EMIT_SYMBOL, 62), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 62), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 0), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 36), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 64), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 91), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 93), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 126), + (91, 0, 0), + (92, 0, 0), + + # Node 82 + (3, HUFFMAN_EMIT_SYMBOL, 124), + (6, HUFFMAN_EMIT_SYMBOL, 124), + (10, HUFFMAN_EMIT_SYMBOL, 124), + (15, HUFFMAN_EMIT_SYMBOL, 124), + (24, HUFFMAN_EMIT_SYMBOL, 124), + (31, HUFFMAN_EMIT_SYMBOL, 124), + (41, HUFFMAN_EMIT_SYMBOL, 124), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 124), + (2, HUFFMAN_EMIT_SYMBOL, 35), + (9, HUFFMAN_EMIT_SYMBOL, 35), + (23, HUFFMAN_EMIT_SYMBOL, 35), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 35), + (2, HUFFMAN_EMIT_SYMBOL, 62), + (9, HUFFMAN_EMIT_SYMBOL, 62), + (23, HUFFMAN_EMIT_SYMBOL, 62), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 62), + + # Node 83 + (3, HUFFMAN_EMIT_SYMBOL, 35), + (6, HUFFMAN_EMIT_SYMBOL, 35), + (10, HUFFMAN_EMIT_SYMBOL, 35), + (15, HUFFMAN_EMIT_SYMBOL, 35), + (24, HUFFMAN_EMIT_SYMBOL, 35), + (31, HUFFMAN_EMIT_SYMBOL, 35), + (41, HUFFMAN_EMIT_SYMBOL, 35), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 35), + (3, HUFFMAN_EMIT_SYMBOL, 62), + (6, HUFFMAN_EMIT_SYMBOL, 62), + (10, HUFFMAN_EMIT_SYMBOL, 62), + (15, HUFFMAN_EMIT_SYMBOL, 62), + (24, HUFFMAN_EMIT_SYMBOL, 62), + (31, HUFFMAN_EMIT_SYMBOL, 62), + (41, HUFFMAN_EMIT_SYMBOL, 62), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 62), + + # Node 84 + (1, HUFFMAN_EMIT_SYMBOL, 0), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 0), + (1, HUFFMAN_EMIT_SYMBOL, 36), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 36), + (1, HUFFMAN_EMIT_SYMBOL, 64), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 64), + (1, HUFFMAN_EMIT_SYMBOL, 91), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 91), + (1, HUFFMAN_EMIT_SYMBOL, 93), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 93), + (1, HUFFMAN_EMIT_SYMBOL, 126), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 126), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 94), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 125), + (93, 0, 0), + (94, 0, 0), + + # Node 85 + (2, HUFFMAN_EMIT_SYMBOL, 0), + (9, HUFFMAN_EMIT_SYMBOL, 0), + (23, HUFFMAN_EMIT_SYMBOL, 0), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 0), + (2, HUFFMAN_EMIT_SYMBOL, 36), + (9, HUFFMAN_EMIT_SYMBOL, 36), + (23, HUFFMAN_EMIT_SYMBOL, 36), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 36), + (2, HUFFMAN_EMIT_SYMBOL, 64), + (9, HUFFMAN_EMIT_SYMBOL, 64), + (23, HUFFMAN_EMIT_SYMBOL, 64), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 64), + (2, HUFFMAN_EMIT_SYMBOL, 91), + (9, HUFFMAN_EMIT_SYMBOL, 91), + (23, HUFFMAN_EMIT_SYMBOL, 91), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 91), + + # Node 86 + (3, HUFFMAN_EMIT_SYMBOL, 0), + (6, HUFFMAN_EMIT_SYMBOL, 0), + (10, HUFFMAN_EMIT_SYMBOL, 0), + (15, HUFFMAN_EMIT_SYMBOL, 0), + (24, HUFFMAN_EMIT_SYMBOL, 0), + (31, HUFFMAN_EMIT_SYMBOL, 0), + (41, HUFFMAN_EMIT_SYMBOL, 0), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 0), + (3, HUFFMAN_EMIT_SYMBOL, 36), + (6, HUFFMAN_EMIT_SYMBOL, 36), + (10, HUFFMAN_EMIT_SYMBOL, 36), + (15, HUFFMAN_EMIT_SYMBOL, 36), + (24, HUFFMAN_EMIT_SYMBOL, 36), + (31, HUFFMAN_EMIT_SYMBOL, 36), + (41, HUFFMAN_EMIT_SYMBOL, 36), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 36), + + # Node 87 + (3, HUFFMAN_EMIT_SYMBOL, 64), + (6, HUFFMAN_EMIT_SYMBOL, 64), + (10, HUFFMAN_EMIT_SYMBOL, 64), + (15, HUFFMAN_EMIT_SYMBOL, 64), + (24, HUFFMAN_EMIT_SYMBOL, 64), + (31, HUFFMAN_EMIT_SYMBOL, 64), + (41, HUFFMAN_EMIT_SYMBOL, 64), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 64), + (3, HUFFMAN_EMIT_SYMBOL, 91), + (6, HUFFMAN_EMIT_SYMBOL, 91), + (10, HUFFMAN_EMIT_SYMBOL, 91), + (15, HUFFMAN_EMIT_SYMBOL, 91), + (24, HUFFMAN_EMIT_SYMBOL, 91), + (31, HUFFMAN_EMIT_SYMBOL, 91), + (41, HUFFMAN_EMIT_SYMBOL, 91), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 91), + + # Node 88 + (2, HUFFMAN_EMIT_SYMBOL, 93), + (9, HUFFMAN_EMIT_SYMBOL, 93), + (23, HUFFMAN_EMIT_SYMBOL, 93), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 93), + (2, HUFFMAN_EMIT_SYMBOL, 126), + (9, HUFFMAN_EMIT_SYMBOL, 126), + (23, HUFFMAN_EMIT_SYMBOL, 126), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 126), + (1, HUFFMAN_EMIT_SYMBOL, 94), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 94), + (1, HUFFMAN_EMIT_SYMBOL, 125), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 125), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 60), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 96), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 123), + (95, 0, 0), + + # Node 89 + (3, HUFFMAN_EMIT_SYMBOL, 93), + (6, HUFFMAN_EMIT_SYMBOL, 93), + (10, HUFFMAN_EMIT_SYMBOL, 93), + (15, HUFFMAN_EMIT_SYMBOL, 93), + (24, HUFFMAN_EMIT_SYMBOL, 93), + (31, HUFFMAN_EMIT_SYMBOL, 93), + (41, HUFFMAN_EMIT_SYMBOL, 93), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 93), + (3, HUFFMAN_EMIT_SYMBOL, 126), + (6, HUFFMAN_EMIT_SYMBOL, 126), + (10, HUFFMAN_EMIT_SYMBOL, 126), + (15, HUFFMAN_EMIT_SYMBOL, 126), + (24, HUFFMAN_EMIT_SYMBOL, 126), + (31, HUFFMAN_EMIT_SYMBOL, 126), + (41, HUFFMAN_EMIT_SYMBOL, 126), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 126), + + # Node 90 + (2, HUFFMAN_EMIT_SYMBOL, 94), + (9, HUFFMAN_EMIT_SYMBOL, 94), + (23, HUFFMAN_EMIT_SYMBOL, 94), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 94), + (2, HUFFMAN_EMIT_SYMBOL, 125), + (9, HUFFMAN_EMIT_SYMBOL, 125), + (23, HUFFMAN_EMIT_SYMBOL, 125), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 125), + (1, HUFFMAN_EMIT_SYMBOL, 60), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 60), + (1, HUFFMAN_EMIT_SYMBOL, 96), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 96), + (1, HUFFMAN_EMIT_SYMBOL, 123), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 123), + (96, 0, 0), + (110, 0, 0), + + # Node 91 + (3, HUFFMAN_EMIT_SYMBOL, 94), + (6, HUFFMAN_EMIT_SYMBOL, 94), + (10, HUFFMAN_EMIT_SYMBOL, 94), + (15, HUFFMAN_EMIT_SYMBOL, 94), + (24, HUFFMAN_EMIT_SYMBOL, 94), + (31, HUFFMAN_EMIT_SYMBOL, 94), + (41, HUFFMAN_EMIT_SYMBOL, 94), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 94), + (3, HUFFMAN_EMIT_SYMBOL, 125), + (6, HUFFMAN_EMIT_SYMBOL, 125), + (10, HUFFMAN_EMIT_SYMBOL, 125), + (15, HUFFMAN_EMIT_SYMBOL, 125), + (24, HUFFMAN_EMIT_SYMBOL, 125), + (31, HUFFMAN_EMIT_SYMBOL, 125), + (41, HUFFMAN_EMIT_SYMBOL, 125), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 125), + + # Node 92 + (2, HUFFMAN_EMIT_SYMBOL, 60), + (9, HUFFMAN_EMIT_SYMBOL, 60), + (23, HUFFMAN_EMIT_SYMBOL, 60), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 60), + (2, HUFFMAN_EMIT_SYMBOL, 96), + (9, HUFFMAN_EMIT_SYMBOL, 96), + (23, HUFFMAN_EMIT_SYMBOL, 96), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 96), + (2, HUFFMAN_EMIT_SYMBOL, 123), + (9, HUFFMAN_EMIT_SYMBOL, 123), + (23, HUFFMAN_EMIT_SYMBOL, 123), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 123), + (97, 0, 0), + (101, 0, 0), + (111, 0, 0), + (133, 0, 0), + + # Node 93 + (3, HUFFMAN_EMIT_SYMBOL, 60), + (6, HUFFMAN_EMIT_SYMBOL, 60), + (10, HUFFMAN_EMIT_SYMBOL, 60), + (15, HUFFMAN_EMIT_SYMBOL, 60), + (24, HUFFMAN_EMIT_SYMBOL, 60), + (31, HUFFMAN_EMIT_SYMBOL, 60), + (41, HUFFMAN_EMIT_SYMBOL, 60), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 60), + (3, HUFFMAN_EMIT_SYMBOL, 96), + (6, HUFFMAN_EMIT_SYMBOL, 96), + (10, HUFFMAN_EMIT_SYMBOL, 96), + (15, HUFFMAN_EMIT_SYMBOL, 96), + (24, HUFFMAN_EMIT_SYMBOL, 96), + (31, HUFFMAN_EMIT_SYMBOL, 96), + (41, HUFFMAN_EMIT_SYMBOL, 96), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 96), + + # Node 94 + (3, HUFFMAN_EMIT_SYMBOL, 123), + (6, HUFFMAN_EMIT_SYMBOL, 123), + (10, HUFFMAN_EMIT_SYMBOL, 123), + (15, HUFFMAN_EMIT_SYMBOL, 123), + (24, HUFFMAN_EMIT_SYMBOL, 123), + (31, HUFFMAN_EMIT_SYMBOL, 123), + (41, HUFFMAN_EMIT_SYMBOL, 123), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 123), + (98, 0, 0), + (99, 0, 0), + (102, 0, 0), + (105, 0, 0), + (112, 0, 0), + (119, 0, 0), + (134, 0, 0), + (153, 0, 0), + + # Node 95 + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 92), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 195), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 208), + (100, 0, 0), + (103, 0, 0), + (104, 0, 0), + (106, 0, 0), + (107, 0, 0), + (113, 0, 0), + (116, 0, 0), + (120, 0, 0), + (126, 0, 0), + (135, 0, 0), + (142, 0, 0), + (154, 0, 0), + (169, 0, 0), + + # Node 96 + (1, HUFFMAN_EMIT_SYMBOL, 92), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 92), + (1, HUFFMAN_EMIT_SYMBOL, 195), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 195), + (1, HUFFMAN_EMIT_SYMBOL, 208), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 208), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 128), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 130), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 131), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 162), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 184), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 194), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 224), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 226), + (108, 0, 0), + (109, 0, 0), + + # Node 97 + (2, HUFFMAN_EMIT_SYMBOL, 92), + (9, HUFFMAN_EMIT_SYMBOL, 92), + (23, HUFFMAN_EMIT_SYMBOL, 92), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 92), + (2, HUFFMAN_EMIT_SYMBOL, 195), + (9, HUFFMAN_EMIT_SYMBOL, 195), + (23, HUFFMAN_EMIT_SYMBOL, 195), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 195), + (2, HUFFMAN_EMIT_SYMBOL, 208), + (9, HUFFMAN_EMIT_SYMBOL, 208), + (23, HUFFMAN_EMIT_SYMBOL, 208), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 208), + (1, HUFFMAN_EMIT_SYMBOL, 128), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 128), + (1, HUFFMAN_EMIT_SYMBOL, 130), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 130), + + # Node 98 + (3, HUFFMAN_EMIT_SYMBOL, 92), + (6, HUFFMAN_EMIT_SYMBOL, 92), + (10, HUFFMAN_EMIT_SYMBOL, 92), + (15, HUFFMAN_EMIT_SYMBOL, 92), + (24, HUFFMAN_EMIT_SYMBOL, 92), + (31, HUFFMAN_EMIT_SYMBOL, 92), + (41, HUFFMAN_EMIT_SYMBOL, 92), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 92), + (3, HUFFMAN_EMIT_SYMBOL, 195), + (6, HUFFMAN_EMIT_SYMBOL, 195), + (10, HUFFMAN_EMIT_SYMBOL, 195), + (15, HUFFMAN_EMIT_SYMBOL, 195), + (24, HUFFMAN_EMIT_SYMBOL, 195), + (31, HUFFMAN_EMIT_SYMBOL, 195), + (41, HUFFMAN_EMIT_SYMBOL, 195), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 195), + + # Node 99 + (3, HUFFMAN_EMIT_SYMBOL, 208), + (6, HUFFMAN_EMIT_SYMBOL, 208), + (10, HUFFMAN_EMIT_SYMBOL, 208), + (15, HUFFMAN_EMIT_SYMBOL, 208), + (24, HUFFMAN_EMIT_SYMBOL, 208), + (31, HUFFMAN_EMIT_SYMBOL, 208), + (41, HUFFMAN_EMIT_SYMBOL, 208), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 208), + (2, HUFFMAN_EMIT_SYMBOL, 128), + (9, HUFFMAN_EMIT_SYMBOL, 128), + (23, HUFFMAN_EMIT_SYMBOL, 128), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 128), + (2, HUFFMAN_EMIT_SYMBOL, 130), + (9, HUFFMAN_EMIT_SYMBOL, 130), + (23, HUFFMAN_EMIT_SYMBOL, 130), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 130), + + # Node 100 + (3, HUFFMAN_EMIT_SYMBOL, 128), + (6, HUFFMAN_EMIT_SYMBOL, 128), + (10, HUFFMAN_EMIT_SYMBOL, 128), + (15, HUFFMAN_EMIT_SYMBOL, 128), + (24, HUFFMAN_EMIT_SYMBOL, 128), + (31, HUFFMAN_EMIT_SYMBOL, 128), + (41, HUFFMAN_EMIT_SYMBOL, 128), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 128), + (3, HUFFMAN_EMIT_SYMBOL, 130), + (6, HUFFMAN_EMIT_SYMBOL, 130), + (10, HUFFMAN_EMIT_SYMBOL, 130), + (15, HUFFMAN_EMIT_SYMBOL, 130), + (24, HUFFMAN_EMIT_SYMBOL, 130), + (31, HUFFMAN_EMIT_SYMBOL, 130), + (41, HUFFMAN_EMIT_SYMBOL, 130), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 130), + + # Node 101 + (1, HUFFMAN_EMIT_SYMBOL, 131), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 131), + (1, HUFFMAN_EMIT_SYMBOL, 162), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 162), + (1, HUFFMAN_EMIT_SYMBOL, 184), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 184), + (1, HUFFMAN_EMIT_SYMBOL, 194), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 194), + (1, HUFFMAN_EMIT_SYMBOL, 224), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 224), + (1, HUFFMAN_EMIT_SYMBOL, 226), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 226), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 153), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 161), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 167), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 172), + + # Node 102 + (2, HUFFMAN_EMIT_SYMBOL, 131), + (9, HUFFMAN_EMIT_SYMBOL, 131), + (23, HUFFMAN_EMIT_SYMBOL, 131), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 131), + (2, HUFFMAN_EMIT_SYMBOL, 162), + (9, HUFFMAN_EMIT_SYMBOL, 162), + (23, HUFFMAN_EMIT_SYMBOL, 162), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 162), + (2, HUFFMAN_EMIT_SYMBOL, 184), + (9, HUFFMAN_EMIT_SYMBOL, 184), + (23, HUFFMAN_EMIT_SYMBOL, 184), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 184), + (2, HUFFMAN_EMIT_SYMBOL, 194), + (9, HUFFMAN_EMIT_SYMBOL, 194), + (23, HUFFMAN_EMIT_SYMBOL, 194), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 194), + + # Node 103 + (3, HUFFMAN_EMIT_SYMBOL, 131), + (6, HUFFMAN_EMIT_SYMBOL, 131), + (10, HUFFMAN_EMIT_SYMBOL, 131), + (15, HUFFMAN_EMIT_SYMBOL, 131), + (24, HUFFMAN_EMIT_SYMBOL, 131), + (31, HUFFMAN_EMIT_SYMBOL, 131), + (41, HUFFMAN_EMIT_SYMBOL, 131), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 131), + (3, HUFFMAN_EMIT_SYMBOL, 162), + (6, HUFFMAN_EMIT_SYMBOL, 162), + (10, HUFFMAN_EMIT_SYMBOL, 162), + (15, HUFFMAN_EMIT_SYMBOL, 162), + (24, HUFFMAN_EMIT_SYMBOL, 162), + (31, HUFFMAN_EMIT_SYMBOL, 162), + (41, HUFFMAN_EMIT_SYMBOL, 162), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 162), + + # Node 104 + (3, HUFFMAN_EMIT_SYMBOL, 184), + (6, HUFFMAN_EMIT_SYMBOL, 184), + (10, HUFFMAN_EMIT_SYMBOL, 184), + (15, HUFFMAN_EMIT_SYMBOL, 184), + (24, HUFFMAN_EMIT_SYMBOL, 184), + (31, HUFFMAN_EMIT_SYMBOL, 184), + (41, HUFFMAN_EMIT_SYMBOL, 184), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 184), + (3, HUFFMAN_EMIT_SYMBOL, 194), + (6, HUFFMAN_EMIT_SYMBOL, 194), + (10, HUFFMAN_EMIT_SYMBOL, 194), + (15, HUFFMAN_EMIT_SYMBOL, 194), + (24, HUFFMAN_EMIT_SYMBOL, 194), + (31, HUFFMAN_EMIT_SYMBOL, 194), + (41, HUFFMAN_EMIT_SYMBOL, 194), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 194), + + # Node 105 + (2, HUFFMAN_EMIT_SYMBOL, 224), + (9, HUFFMAN_EMIT_SYMBOL, 224), + (23, HUFFMAN_EMIT_SYMBOL, 224), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 224), + (2, HUFFMAN_EMIT_SYMBOL, 226), + (9, HUFFMAN_EMIT_SYMBOL, 226), + (23, HUFFMAN_EMIT_SYMBOL, 226), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 226), + (1, HUFFMAN_EMIT_SYMBOL, 153), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 153), + (1, HUFFMAN_EMIT_SYMBOL, 161), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 161), + (1, HUFFMAN_EMIT_SYMBOL, 167), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 167), + (1, HUFFMAN_EMIT_SYMBOL, 172), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 172), + + # Node 106 + (3, HUFFMAN_EMIT_SYMBOL, 224), + (6, HUFFMAN_EMIT_SYMBOL, 224), + (10, HUFFMAN_EMIT_SYMBOL, 224), + (15, HUFFMAN_EMIT_SYMBOL, 224), + (24, HUFFMAN_EMIT_SYMBOL, 224), + (31, HUFFMAN_EMIT_SYMBOL, 224), + (41, HUFFMAN_EMIT_SYMBOL, 224), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 224), + (3, HUFFMAN_EMIT_SYMBOL, 226), + (6, HUFFMAN_EMIT_SYMBOL, 226), + (10, HUFFMAN_EMIT_SYMBOL, 226), + (15, HUFFMAN_EMIT_SYMBOL, 226), + (24, HUFFMAN_EMIT_SYMBOL, 226), + (31, HUFFMAN_EMIT_SYMBOL, 226), + (41, HUFFMAN_EMIT_SYMBOL, 226), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 226), + + # Node 107 + (2, HUFFMAN_EMIT_SYMBOL, 153), + (9, HUFFMAN_EMIT_SYMBOL, 153), + (23, HUFFMAN_EMIT_SYMBOL, 153), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 153), + (2, HUFFMAN_EMIT_SYMBOL, 161), + (9, HUFFMAN_EMIT_SYMBOL, 161), + (23, HUFFMAN_EMIT_SYMBOL, 161), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 161), + (2, HUFFMAN_EMIT_SYMBOL, 167), + (9, HUFFMAN_EMIT_SYMBOL, 167), + (23, HUFFMAN_EMIT_SYMBOL, 167), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 167), + (2, HUFFMAN_EMIT_SYMBOL, 172), + (9, HUFFMAN_EMIT_SYMBOL, 172), + (23, HUFFMAN_EMIT_SYMBOL, 172), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 172), + + # Node 108 + (3, HUFFMAN_EMIT_SYMBOL, 153), + (6, HUFFMAN_EMIT_SYMBOL, 153), + (10, HUFFMAN_EMIT_SYMBOL, 153), + (15, HUFFMAN_EMIT_SYMBOL, 153), + (24, HUFFMAN_EMIT_SYMBOL, 153), + (31, HUFFMAN_EMIT_SYMBOL, 153), + (41, HUFFMAN_EMIT_SYMBOL, 153), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 153), + (3, HUFFMAN_EMIT_SYMBOL, 161), + (6, HUFFMAN_EMIT_SYMBOL, 161), + (10, HUFFMAN_EMIT_SYMBOL, 161), + (15, HUFFMAN_EMIT_SYMBOL, 161), + (24, HUFFMAN_EMIT_SYMBOL, 161), + (31, HUFFMAN_EMIT_SYMBOL, 161), + (41, HUFFMAN_EMIT_SYMBOL, 161), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 161), + + # Node 109 + (3, HUFFMAN_EMIT_SYMBOL, 167), + (6, HUFFMAN_EMIT_SYMBOL, 167), + (10, HUFFMAN_EMIT_SYMBOL, 167), + (15, HUFFMAN_EMIT_SYMBOL, 167), + (24, HUFFMAN_EMIT_SYMBOL, 167), + (31, HUFFMAN_EMIT_SYMBOL, 167), + (41, HUFFMAN_EMIT_SYMBOL, 167), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 167), + (3, HUFFMAN_EMIT_SYMBOL, 172), + (6, HUFFMAN_EMIT_SYMBOL, 172), + (10, HUFFMAN_EMIT_SYMBOL, 172), + (15, HUFFMAN_EMIT_SYMBOL, 172), + (24, HUFFMAN_EMIT_SYMBOL, 172), + (31, HUFFMAN_EMIT_SYMBOL, 172), + (41, HUFFMAN_EMIT_SYMBOL, 172), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 172), + + # Node 110 + (114, 0, 0), + (115, 0, 0), + (117, 0, 0), + (118, 0, 0), + (121, 0, 0), + (123, 0, 0), + (127, 0, 0), + (130, 0, 0), + (136, 0, 0), + (139, 0, 0), + (143, 0, 0), + (146, 0, 0), + (155, 0, 0), + (162, 0, 0), + (170, 0, 0), + (180, 0, 0), + + # Node 111 + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 176), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 177), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 179), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 209), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 216), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 217), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 227), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 229), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 230), + (122, 0, 0), + (124, 0, 0), + (125, 0, 0), + (128, 0, 0), + (129, 0, 0), + (131, 0, 0), + (132, 0, 0), + + # Node 112 + (1, HUFFMAN_EMIT_SYMBOL, 176), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 176), + (1, HUFFMAN_EMIT_SYMBOL, 177), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 177), + (1, HUFFMAN_EMIT_SYMBOL, 179), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 179), + (1, HUFFMAN_EMIT_SYMBOL, 209), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 209), + (1, HUFFMAN_EMIT_SYMBOL, 216), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 216), + (1, HUFFMAN_EMIT_SYMBOL, 217), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 217), + (1, HUFFMAN_EMIT_SYMBOL, 227), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 227), + (1, HUFFMAN_EMIT_SYMBOL, 229), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 229), + + # Node 113 + (2, HUFFMAN_EMIT_SYMBOL, 176), + (9, HUFFMAN_EMIT_SYMBOL, 176), + (23, HUFFMAN_EMIT_SYMBOL, 176), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 176), + (2, HUFFMAN_EMIT_SYMBOL, 177), + (9, HUFFMAN_EMIT_SYMBOL, 177), + (23, HUFFMAN_EMIT_SYMBOL, 177), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 177), + (2, HUFFMAN_EMIT_SYMBOL, 179), + (9, HUFFMAN_EMIT_SYMBOL, 179), + (23, HUFFMAN_EMIT_SYMBOL, 179), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 179), + (2, HUFFMAN_EMIT_SYMBOL, 209), + (9, HUFFMAN_EMIT_SYMBOL, 209), + (23, HUFFMAN_EMIT_SYMBOL, 209), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 209), + + # Node 114 + (3, HUFFMAN_EMIT_SYMBOL, 176), + (6, HUFFMAN_EMIT_SYMBOL, 176), + (10, HUFFMAN_EMIT_SYMBOL, 176), + (15, HUFFMAN_EMIT_SYMBOL, 176), + (24, HUFFMAN_EMIT_SYMBOL, 176), + (31, HUFFMAN_EMIT_SYMBOL, 176), + (41, HUFFMAN_EMIT_SYMBOL, 176), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 176), + (3, HUFFMAN_EMIT_SYMBOL, 177), + (6, HUFFMAN_EMIT_SYMBOL, 177), + (10, HUFFMAN_EMIT_SYMBOL, 177), + (15, HUFFMAN_EMIT_SYMBOL, 177), + (24, HUFFMAN_EMIT_SYMBOL, 177), + (31, HUFFMAN_EMIT_SYMBOL, 177), + (41, HUFFMAN_EMIT_SYMBOL, 177), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 177), + + # Node 115 + (3, HUFFMAN_EMIT_SYMBOL, 179), + (6, HUFFMAN_EMIT_SYMBOL, 179), + (10, HUFFMAN_EMIT_SYMBOL, 179), + (15, HUFFMAN_EMIT_SYMBOL, 179), + (24, HUFFMAN_EMIT_SYMBOL, 179), + (31, HUFFMAN_EMIT_SYMBOL, 179), + (41, HUFFMAN_EMIT_SYMBOL, 179), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 179), + (3, HUFFMAN_EMIT_SYMBOL, 209), + (6, HUFFMAN_EMIT_SYMBOL, 209), + (10, HUFFMAN_EMIT_SYMBOL, 209), + (15, HUFFMAN_EMIT_SYMBOL, 209), + (24, HUFFMAN_EMIT_SYMBOL, 209), + (31, HUFFMAN_EMIT_SYMBOL, 209), + (41, HUFFMAN_EMIT_SYMBOL, 209), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 209), + + # Node 116 + (2, HUFFMAN_EMIT_SYMBOL, 216), + (9, HUFFMAN_EMIT_SYMBOL, 216), + (23, HUFFMAN_EMIT_SYMBOL, 216), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 216), + (2, HUFFMAN_EMIT_SYMBOL, 217), + (9, HUFFMAN_EMIT_SYMBOL, 217), + (23, HUFFMAN_EMIT_SYMBOL, 217), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 217), + (2, HUFFMAN_EMIT_SYMBOL, 227), + (9, HUFFMAN_EMIT_SYMBOL, 227), + (23, HUFFMAN_EMIT_SYMBOL, 227), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 227), + (2, HUFFMAN_EMIT_SYMBOL, 229), + (9, HUFFMAN_EMIT_SYMBOL, 229), + (23, HUFFMAN_EMIT_SYMBOL, 229), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 229), + + # Node 117 + (3, HUFFMAN_EMIT_SYMBOL, 216), + (6, HUFFMAN_EMIT_SYMBOL, 216), + (10, HUFFMAN_EMIT_SYMBOL, 216), + (15, HUFFMAN_EMIT_SYMBOL, 216), + (24, HUFFMAN_EMIT_SYMBOL, 216), + (31, HUFFMAN_EMIT_SYMBOL, 216), + (41, HUFFMAN_EMIT_SYMBOL, 216), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 216), + (3, HUFFMAN_EMIT_SYMBOL, 217), + (6, HUFFMAN_EMIT_SYMBOL, 217), + (10, HUFFMAN_EMIT_SYMBOL, 217), + (15, HUFFMAN_EMIT_SYMBOL, 217), + (24, HUFFMAN_EMIT_SYMBOL, 217), + (31, HUFFMAN_EMIT_SYMBOL, 217), + (41, HUFFMAN_EMIT_SYMBOL, 217), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 217), + + # Node 118 + (3, HUFFMAN_EMIT_SYMBOL, 227), + (6, HUFFMAN_EMIT_SYMBOL, 227), + (10, HUFFMAN_EMIT_SYMBOL, 227), + (15, HUFFMAN_EMIT_SYMBOL, 227), + (24, HUFFMAN_EMIT_SYMBOL, 227), + (31, HUFFMAN_EMIT_SYMBOL, 227), + (41, HUFFMAN_EMIT_SYMBOL, 227), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 227), + (3, HUFFMAN_EMIT_SYMBOL, 229), + (6, HUFFMAN_EMIT_SYMBOL, 229), + (10, HUFFMAN_EMIT_SYMBOL, 229), + (15, HUFFMAN_EMIT_SYMBOL, 229), + (24, HUFFMAN_EMIT_SYMBOL, 229), + (31, HUFFMAN_EMIT_SYMBOL, 229), + (41, HUFFMAN_EMIT_SYMBOL, 229), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 229), + + # Node 119 + (1, HUFFMAN_EMIT_SYMBOL, 230), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 230), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 129), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 132), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 133), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 134), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 136), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 146), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 154), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 156), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 160), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 163), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 164), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 169), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 170), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 173), + + # Node 120 + (2, HUFFMAN_EMIT_SYMBOL, 230), + (9, HUFFMAN_EMIT_SYMBOL, 230), + (23, HUFFMAN_EMIT_SYMBOL, 230), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 230), + (1, HUFFMAN_EMIT_SYMBOL, 129), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 129), + (1, HUFFMAN_EMIT_SYMBOL, 132), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 132), + (1, HUFFMAN_EMIT_SYMBOL, 133), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 133), + (1, HUFFMAN_EMIT_SYMBOL, 134), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 134), + (1, HUFFMAN_EMIT_SYMBOL, 136), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 136), + (1, HUFFMAN_EMIT_SYMBOL, 146), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 146), + + # Node 121 + (3, HUFFMAN_EMIT_SYMBOL, 230), + (6, HUFFMAN_EMIT_SYMBOL, 230), + (10, HUFFMAN_EMIT_SYMBOL, 230), + (15, HUFFMAN_EMIT_SYMBOL, 230), + (24, HUFFMAN_EMIT_SYMBOL, 230), + (31, HUFFMAN_EMIT_SYMBOL, 230), + (41, HUFFMAN_EMIT_SYMBOL, 230), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 230), + (2, HUFFMAN_EMIT_SYMBOL, 129), + (9, HUFFMAN_EMIT_SYMBOL, 129), + (23, HUFFMAN_EMIT_SYMBOL, 129), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 129), + (2, HUFFMAN_EMIT_SYMBOL, 132), + (9, HUFFMAN_EMIT_SYMBOL, 132), + (23, HUFFMAN_EMIT_SYMBOL, 132), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 132), + + # Node 122 + (3, HUFFMAN_EMIT_SYMBOL, 129), + (6, HUFFMAN_EMIT_SYMBOL, 129), + (10, HUFFMAN_EMIT_SYMBOL, 129), + (15, HUFFMAN_EMIT_SYMBOL, 129), + (24, HUFFMAN_EMIT_SYMBOL, 129), + (31, HUFFMAN_EMIT_SYMBOL, 129), + (41, HUFFMAN_EMIT_SYMBOL, 129), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 129), + (3, HUFFMAN_EMIT_SYMBOL, 132), + (6, HUFFMAN_EMIT_SYMBOL, 132), + (10, HUFFMAN_EMIT_SYMBOL, 132), + (15, HUFFMAN_EMIT_SYMBOL, 132), + (24, HUFFMAN_EMIT_SYMBOL, 132), + (31, HUFFMAN_EMIT_SYMBOL, 132), + (41, HUFFMAN_EMIT_SYMBOL, 132), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 132), + + # Node 123 + (2, HUFFMAN_EMIT_SYMBOL, 133), + (9, HUFFMAN_EMIT_SYMBOL, 133), + (23, HUFFMAN_EMIT_SYMBOL, 133), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 133), + (2, HUFFMAN_EMIT_SYMBOL, 134), + (9, HUFFMAN_EMIT_SYMBOL, 134), + (23, HUFFMAN_EMIT_SYMBOL, 134), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 134), + (2, HUFFMAN_EMIT_SYMBOL, 136), + (9, HUFFMAN_EMIT_SYMBOL, 136), + (23, HUFFMAN_EMIT_SYMBOL, 136), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 136), + (2, HUFFMAN_EMIT_SYMBOL, 146), + (9, HUFFMAN_EMIT_SYMBOL, 146), + (23, HUFFMAN_EMIT_SYMBOL, 146), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 146), + + # Node 124 + (3, HUFFMAN_EMIT_SYMBOL, 133), + (6, HUFFMAN_EMIT_SYMBOL, 133), + (10, HUFFMAN_EMIT_SYMBOL, 133), + (15, HUFFMAN_EMIT_SYMBOL, 133), + (24, HUFFMAN_EMIT_SYMBOL, 133), + (31, HUFFMAN_EMIT_SYMBOL, 133), + (41, HUFFMAN_EMIT_SYMBOL, 133), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 133), + (3, HUFFMAN_EMIT_SYMBOL, 134), + (6, HUFFMAN_EMIT_SYMBOL, 134), + (10, HUFFMAN_EMIT_SYMBOL, 134), + (15, HUFFMAN_EMIT_SYMBOL, 134), + (24, HUFFMAN_EMIT_SYMBOL, 134), + (31, HUFFMAN_EMIT_SYMBOL, 134), + (41, HUFFMAN_EMIT_SYMBOL, 134), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 134), + + # Node 125 + (3, HUFFMAN_EMIT_SYMBOL, 136), + (6, HUFFMAN_EMIT_SYMBOL, 136), + (10, HUFFMAN_EMIT_SYMBOL, 136), + (15, HUFFMAN_EMIT_SYMBOL, 136), + (24, HUFFMAN_EMIT_SYMBOL, 136), + (31, HUFFMAN_EMIT_SYMBOL, 136), + (41, HUFFMAN_EMIT_SYMBOL, 136), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 136), + (3, HUFFMAN_EMIT_SYMBOL, 146), + (6, HUFFMAN_EMIT_SYMBOL, 146), + (10, HUFFMAN_EMIT_SYMBOL, 146), + (15, HUFFMAN_EMIT_SYMBOL, 146), + (24, HUFFMAN_EMIT_SYMBOL, 146), + (31, HUFFMAN_EMIT_SYMBOL, 146), + (41, HUFFMAN_EMIT_SYMBOL, 146), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 146), + + # Node 126 + (1, HUFFMAN_EMIT_SYMBOL, 154), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 154), + (1, HUFFMAN_EMIT_SYMBOL, 156), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 156), + (1, HUFFMAN_EMIT_SYMBOL, 160), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 160), + (1, HUFFMAN_EMIT_SYMBOL, 163), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 163), + (1, HUFFMAN_EMIT_SYMBOL, 164), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 164), + (1, HUFFMAN_EMIT_SYMBOL, 169), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 169), + (1, HUFFMAN_EMIT_SYMBOL, 170), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 170), + (1, HUFFMAN_EMIT_SYMBOL, 173), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 173), + + # Node 127 + (2, HUFFMAN_EMIT_SYMBOL, 154), + (9, HUFFMAN_EMIT_SYMBOL, 154), + (23, HUFFMAN_EMIT_SYMBOL, 154), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 154), + (2, HUFFMAN_EMIT_SYMBOL, 156), + (9, HUFFMAN_EMIT_SYMBOL, 156), + (23, HUFFMAN_EMIT_SYMBOL, 156), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 156), + (2, HUFFMAN_EMIT_SYMBOL, 160), + (9, HUFFMAN_EMIT_SYMBOL, 160), + (23, HUFFMAN_EMIT_SYMBOL, 160), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 160), + (2, HUFFMAN_EMIT_SYMBOL, 163), + (9, HUFFMAN_EMIT_SYMBOL, 163), + (23, HUFFMAN_EMIT_SYMBOL, 163), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 163), + + # Node 128 + (3, HUFFMAN_EMIT_SYMBOL, 154), + (6, HUFFMAN_EMIT_SYMBOL, 154), + (10, HUFFMAN_EMIT_SYMBOL, 154), + (15, HUFFMAN_EMIT_SYMBOL, 154), + (24, HUFFMAN_EMIT_SYMBOL, 154), + (31, HUFFMAN_EMIT_SYMBOL, 154), + (41, HUFFMAN_EMIT_SYMBOL, 154), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 154), + (3, HUFFMAN_EMIT_SYMBOL, 156), + (6, HUFFMAN_EMIT_SYMBOL, 156), + (10, HUFFMAN_EMIT_SYMBOL, 156), + (15, HUFFMAN_EMIT_SYMBOL, 156), + (24, HUFFMAN_EMIT_SYMBOL, 156), + (31, HUFFMAN_EMIT_SYMBOL, 156), + (41, HUFFMAN_EMIT_SYMBOL, 156), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 156), + + # Node 129 + (3, HUFFMAN_EMIT_SYMBOL, 160), + (6, HUFFMAN_EMIT_SYMBOL, 160), + (10, HUFFMAN_EMIT_SYMBOL, 160), + (15, HUFFMAN_EMIT_SYMBOL, 160), + (24, HUFFMAN_EMIT_SYMBOL, 160), + (31, HUFFMAN_EMIT_SYMBOL, 160), + (41, HUFFMAN_EMIT_SYMBOL, 160), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 160), + (3, HUFFMAN_EMIT_SYMBOL, 163), + (6, HUFFMAN_EMIT_SYMBOL, 163), + (10, HUFFMAN_EMIT_SYMBOL, 163), + (15, HUFFMAN_EMIT_SYMBOL, 163), + (24, HUFFMAN_EMIT_SYMBOL, 163), + (31, HUFFMAN_EMIT_SYMBOL, 163), + (41, HUFFMAN_EMIT_SYMBOL, 163), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 163), + + # Node 130 + (2, HUFFMAN_EMIT_SYMBOL, 164), + (9, HUFFMAN_EMIT_SYMBOL, 164), + (23, HUFFMAN_EMIT_SYMBOL, 164), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 164), + (2, HUFFMAN_EMIT_SYMBOL, 169), + (9, HUFFMAN_EMIT_SYMBOL, 169), + (23, HUFFMAN_EMIT_SYMBOL, 169), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 169), + (2, HUFFMAN_EMIT_SYMBOL, 170), + (9, HUFFMAN_EMIT_SYMBOL, 170), + (23, HUFFMAN_EMIT_SYMBOL, 170), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 170), + (2, HUFFMAN_EMIT_SYMBOL, 173), + (9, HUFFMAN_EMIT_SYMBOL, 173), + (23, HUFFMAN_EMIT_SYMBOL, 173), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 173), + + # Node 131 + (3, HUFFMAN_EMIT_SYMBOL, 164), + (6, HUFFMAN_EMIT_SYMBOL, 164), + (10, HUFFMAN_EMIT_SYMBOL, 164), + (15, HUFFMAN_EMIT_SYMBOL, 164), + (24, HUFFMAN_EMIT_SYMBOL, 164), + (31, HUFFMAN_EMIT_SYMBOL, 164), + (41, HUFFMAN_EMIT_SYMBOL, 164), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 164), + (3, HUFFMAN_EMIT_SYMBOL, 169), + (6, HUFFMAN_EMIT_SYMBOL, 169), + (10, HUFFMAN_EMIT_SYMBOL, 169), + (15, HUFFMAN_EMIT_SYMBOL, 169), + (24, HUFFMAN_EMIT_SYMBOL, 169), + (31, HUFFMAN_EMIT_SYMBOL, 169), + (41, HUFFMAN_EMIT_SYMBOL, 169), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 169), + + # Node 132 + (3, HUFFMAN_EMIT_SYMBOL, 170), + (6, HUFFMAN_EMIT_SYMBOL, 170), + (10, HUFFMAN_EMIT_SYMBOL, 170), + (15, HUFFMAN_EMIT_SYMBOL, 170), + (24, HUFFMAN_EMIT_SYMBOL, 170), + (31, HUFFMAN_EMIT_SYMBOL, 170), + (41, HUFFMAN_EMIT_SYMBOL, 170), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 170), + (3, HUFFMAN_EMIT_SYMBOL, 173), + (6, HUFFMAN_EMIT_SYMBOL, 173), + (10, HUFFMAN_EMIT_SYMBOL, 173), + (15, HUFFMAN_EMIT_SYMBOL, 173), + (24, HUFFMAN_EMIT_SYMBOL, 173), + (31, HUFFMAN_EMIT_SYMBOL, 173), + (41, HUFFMAN_EMIT_SYMBOL, 173), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 173), + + # Node 133 + (137, 0, 0), + (138, 0, 0), + (140, 0, 0), + (141, 0, 0), + (144, 0, 0), + (145, 0, 0), + (147, 0, 0), + (150, 0, 0), + (156, 0, 0), + (159, 0, 0), + (163, 0, 0), + (166, 0, 0), + (171, 0, 0), + (174, 0, 0), + (181, 0, 0), + (190, 0, 0), + + # Node 134 + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 178), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 181), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 185), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 186), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 187), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 189), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 190), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 196), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 198), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 228), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 232), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 233), + (148, 0, 0), + (149, 0, 0), + (151, 0, 0), + (152, 0, 0), + + # Node 135 + (1, HUFFMAN_EMIT_SYMBOL, 178), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 178), + (1, HUFFMAN_EMIT_SYMBOL, 181), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 181), + (1, HUFFMAN_EMIT_SYMBOL, 185), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 185), + (1, HUFFMAN_EMIT_SYMBOL, 186), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 186), + (1, HUFFMAN_EMIT_SYMBOL, 187), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 187), + (1, HUFFMAN_EMIT_SYMBOL, 189), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 189), + (1, HUFFMAN_EMIT_SYMBOL, 190), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 190), + (1, HUFFMAN_EMIT_SYMBOL, 196), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 196), + + # Node 136 + (2, HUFFMAN_EMIT_SYMBOL, 178), + (9, HUFFMAN_EMIT_SYMBOL, 178), + (23, HUFFMAN_EMIT_SYMBOL, 178), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 178), + (2, HUFFMAN_EMIT_SYMBOL, 181), + (9, HUFFMAN_EMIT_SYMBOL, 181), + (23, HUFFMAN_EMIT_SYMBOL, 181), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 181), + (2, HUFFMAN_EMIT_SYMBOL, 185), + (9, HUFFMAN_EMIT_SYMBOL, 185), + (23, HUFFMAN_EMIT_SYMBOL, 185), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 185), + (2, HUFFMAN_EMIT_SYMBOL, 186), + (9, HUFFMAN_EMIT_SYMBOL, 186), + (23, HUFFMAN_EMIT_SYMBOL, 186), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 186), + + # Node 137 + (3, HUFFMAN_EMIT_SYMBOL, 178), + (6, HUFFMAN_EMIT_SYMBOL, 178), + (10, HUFFMAN_EMIT_SYMBOL, 178), + (15, HUFFMAN_EMIT_SYMBOL, 178), + (24, HUFFMAN_EMIT_SYMBOL, 178), + (31, HUFFMAN_EMIT_SYMBOL, 178), + (41, HUFFMAN_EMIT_SYMBOL, 178), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 178), + (3, HUFFMAN_EMIT_SYMBOL, 181), + (6, HUFFMAN_EMIT_SYMBOL, 181), + (10, HUFFMAN_EMIT_SYMBOL, 181), + (15, HUFFMAN_EMIT_SYMBOL, 181), + (24, HUFFMAN_EMIT_SYMBOL, 181), + (31, HUFFMAN_EMIT_SYMBOL, 181), + (41, HUFFMAN_EMIT_SYMBOL, 181), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 181), + + # Node 138 + (3, HUFFMAN_EMIT_SYMBOL, 185), + (6, HUFFMAN_EMIT_SYMBOL, 185), + (10, HUFFMAN_EMIT_SYMBOL, 185), + (15, HUFFMAN_EMIT_SYMBOL, 185), + (24, HUFFMAN_EMIT_SYMBOL, 185), + (31, HUFFMAN_EMIT_SYMBOL, 185), + (41, HUFFMAN_EMIT_SYMBOL, 185), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 185), + (3, HUFFMAN_EMIT_SYMBOL, 186), + (6, HUFFMAN_EMIT_SYMBOL, 186), + (10, HUFFMAN_EMIT_SYMBOL, 186), + (15, HUFFMAN_EMIT_SYMBOL, 186), + (24, HUFFMAN_EMIT_SYMBOL, 186), + (31, HUFFMAN_EMIT_SYMBOL, 186), + (41, HUFFMAN_EMIT_SYMBOL, 186), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 186), + + # Node 139 + (2, HUFFMAN_EMIT_SYMBOL, 187), + (9, HUFFMAN_EMIT_SYMBOL, 187), + (23, HUFFMAN_EMIT_SYMBOL, 187), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 187), + (2, HUFFMAN_EMIT_SYMBOL, 189), + (9, HUFFMAN_EMIT_SYMBOL, 189), + (23, HUFFMAN_EMIT_SYMBOL, 189), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 189), + (2, HUFFMAN_EMIT_SYMBOL, 190), + (9, HUFFMAN_EMIT_SYMBOL, 190), + (23, HUFFMAN_EMIT_SYMBOL, 190), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 190), + (2, HUFFMAN_EMIT_SYMBOL, 196), + (9, HUFFMAN_EMIT_SYMBOL, 196), + (23, HUFFMAN_EMIT_SYMBOL, 196), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 196), + + # Node 140 + (3, HUFFMAN_EMIT_SYMBOL, 187), + (6, HUFFMAN_EMIT_SYMBOL, 187), + (10, HUFFMAN_EMIT_SYMBOL, 187), + (15, HUFFMAN_EMIT_SYMBOL, 187), + (24, HUFFMAN_EMIT_SYMBOL, 187), + (31, HUFFMAN_EMIT_SYMBOL, 187), + (41, HUFFMAN_EMIT_SYMBOL, 187), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 187), + (3, HUFFMAN_EMIT_SYMBOL, 189), + (6, HUFFMAN_EMIT_SYMBOL, 189), + (10, HUFFMAN_EMIT_SYMBOL, 189), + (15, HUFFMAN_EMIT_SYMBOL, 189), + (24, HUFFMAN_EMIT_SYMBOL, 189), + (31, HUFFMAN_EMIT_SYMBOL, 189), + (41, HUFFMAN_EMIT_SYMBOL, 189), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 189), + + # Node 141 + (3, HUFFMAN_EMIT_SYMBOL, 190), + (6, HUFFMAN_EMIT_SYMBOL, 190), + (10, HUFFMAN_EMIT_SYMBOL, 190), + (15, HUFFMAN_EMIT_SYMBOL, 190), + (24, HUFFMAN_EMIT_SYMBOL, 190), + (31, HUFFMAN_EMIT_SYMBOL, 190), + (41, HUFFMAN_EMIT_SYMBOL, 190), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 190), + (3, HUFFMAN_EMIT_SYMBOL, 196), + (6, HUFFMAN_EMIT_SYMBOL, 196), + (10, HUFFMAN_EMIT_SYMBOL, 196), + (15, HUFFMAN_EMIT_SYMBOL, 196), + (24, HUFFMAN_EMIT_SYMBOL, 196), + (31, HUFFMAN_EMIT_SYMBOL, 196), + (41, HUFFMAN_EMIT_SYMBOL, 196), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 196), + + # Node 142 + (1, HUFFMAN_EMIT_SYMBOL, 198), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 198), + (1, HUFFMAN_EMIT_SYMBOL, 228), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 228), + (1, HUFFMAN_EMIT_SYMBOL, 232), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 232), + (1, HUFFMAN_EMIT_SYMBOL, 233), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 233), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 1), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 135), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 137), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 138), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 139), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 140), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 141), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 143), + + # Node 143 + (2, HUFFMAN_EMIT_SYMBOL, 198), + (9, HUFFMAN_EMIT_SYMBOL, 198), + (23, HUFFMAN_EMIT_SYMBOL, 198), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 198), + (2, HUFFMAN_EMIT_SYMBOL, 228), + (9, HUFFMAN_EMIT_SYMBOL, 228), + (23, HUFFMAN_EMIT_SYMBOL, 228), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 228), + (2, HUFFMAN_EMIT_SYMBOL, 232), + (9, HUFFMAN_EMIT_SYMBOL, 232), + (23, HUFFMAN_EMIT_SYMBOL, 232), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 232), + (2, HUFFMAN_EMIT_SYMBOL, 233), + (9, HUFFMAN_EMIT_SYMBOL, 233), + (23, HUFFMAN_EMIT_SYMBOL, 233), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 233), + + # Node 144 + (3, HUFFMAN_EMIT_SYMBOL, 198), + (6, HUFFMAN_EMIT_SYMBOL, 198), + (10, HUFFMAN_EMIT_SYMBOL, 198), + (15, HUFFMAN_EMIT_SYMBOL, 198), + (24, HUFFMAN_EMIT_SYMBOL, 198), + (31, HUFFMAN_EMIT_SYMBOL, 198), + (41, HUFFMAN_EMIT_SYMBOL, 198), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 198), + (3, HUFFMAN_EMIT_SYMBOL, 228), + (6, HUFFMAN_EMIT_SYMBOL, 228), + (10, HUFFMAN_EMIT_SYMBOL, 228), + (15, HUFFMAN_EMIT_SYMBOL, 228), + (24, HUFFMAN_EMIT_SYMBOL, 228), + (31, HUFFMAN_EMIT_SYMBOL, 228), + (41, HUFFMAN_EMIT_SYMBOL, 228), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 228), + + # Node 145 + (3, HUFFMAN_EMIT_SYMBOL, 232), + (6, HUFFMAN_EMIT_SYMBOL, 232), + (10, HUFFMAN_EMIT_SYMBOL, 232), + (15, HUFFMAN_EMIT_SYMBOL, 232), + (24, HUFFMAN_EMIT_SYMBOL, 232), + (31, HUFFMAN_EMIT_SYMBOL, 232), + (41, HUFFMAN_EMIT_SYMBOL, 232), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 232), + (3, HUFFMAN_EMIT_SYMBOL, 233), + (6, HUFFMAN_EMIT_SYMBOL, 233), + (10, HUFFMAN_EMIT_SYMBOL, 233), + (15, HUFFMAN_EMIT_SYMBOL, 233), + (24, HUFFMAN_EMIT_SYMBOL, 233), + (31, HUFFMAN_EMIT_SYMBOL, 233), + (41, HUFFMAN_EMIT_SYMBOL, 233), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 233), + + # Node 146 + (1, HUFFMAN_EMIT_SYMBOL, 1), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 1), + (1, HUFFMAN_EMIT_SYMBOL, 135), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 135), + (1, HUFFMAN_EMIT_SYMBOL, 137), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 137), + (1, HUFFMAN_EMIT_SYMBOL, 138), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 138), + (1, HUFFMAN_EMIT_SYMBOL, 139), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 139), + (1, HUFFMAN_EMIT_SYMBOL, 140), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 140), + (1, HUFFMAN_EMIT_SYMBOL, 141), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 141), + (1, HUFFMAN_EMIT_SYMBOL, 143), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 143), + + # Node 147 + (2, HUFFMAN_EMIT_SYMBOL, 1), + (9, HUFFMAN_EMIT_SYMBOL, 1), + (23, HUFFMAN_EMIT_SYMBOL, 1), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 1), + (2, HUFFMAN_EMIT_SYMBOL, 135), + (9, HUFFMAN_EMIT_SYMBOL, 135), + (23, HUFFMAN_EMIT_SYMBOL, 135), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 135), + (2, HUFFMAN_EMIT_SYMBOL, 137), + (9, HUFFMAN_EMIT_SYMBOL, 137), + (23, HUFFMAN_EMIT_SYMBOL, 137), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 137), + (2, HUFFMAN_EMIT_SYMBOL, 138), + (9, HUFFMAN_EMIT_SYMBOL, 138), + (23, HUFFMAN_EMIT_SYMBOL, 138), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 138), + + # Node 148 + (3, HUFFMAN_EMIT_SYMBOL, 1), + (6, HUFFMAN_EMIT_SYMBOL, 1), + (10, HUFFMAN_EMIT_SYMBOL, 1), + (15, HUFFMAN_EMIT_SYMBOL, 1), + (24, HUFFMAN_EMIT_SYMBOL, 1), + (31, HUFFMAN_EMIT_SYMBOL, 1), + (41, HUFFMAN_EMIT_SYMBOL, 1), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 1), + (3, HUFFMAN_EMIT_SYMBOL, 135), + (6, HUFFMAN_EMIT_SYMBOL, 135), + (10, HUFFMAN_EMIT_SYMBOL, 135), + (15, HUFFMAN_EMIT_SYMBOL, 135), + (24, HUFFMAN_EMIT_SYMBOL, 135), + (31, HUFFMAN_EMIT_SYMBOL, 135), + (41, HUFFMAN_EMIT_SYMBOL, 135), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 135), + + # Node 149 + (3, HUFFMAN_EMIT_SYMBOL, 137), + (6, HUFFMAN_EMIT_SYMBOL, 137), + (10, HUFFMAN_EMIT_SYMBOL, 137), + (15, HUFFMAN_EMIT_SYMBOL, 137), + (24, HUFFMAN_EMIT_SYMBOL, 137), + (31, HUFFMAN_EMIT_SYMBOL, 137), + (41, HUFFMAN_EMIT_SYMBOL, 137), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 137), + (3, HUFFMAN_EMIT_SYMBOL, 138), + (6, HUFFMAN_EMIT_SYMBOL, 138), + (10, HUFFMAN_EMIT_SYMBOL, 138), + (15, HUFFMAN_EMIT_SYMBOL, 138), + (24, HUFFMAN_EMIT_SYMBOL, 138), + (31, HUFFMAN_EMIT_SYMBOL, 138), + (41, HUFFMAN_EMIT_SYMBOL, 138), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 138), + + # Node 150 + (2, HUFFMAN_EMIT_SYMBOL, 139), + (9, HUFFMAN_EMIT_SYMBOL, 139), + (23, HUFFMAN_EMIT_SYMBOL, 139), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 139), + (2, HUFFMAN_EMIT_SYMBOL, 140), + (9, HUFFMAN_EMIT_SYMBOL, 140), + (23, HUFFMAN_EMIT_SYMBOL, 140), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 140), + (2, HUFFMAN_EMIT_SYMBOL, 141), + (9, HUFFMAN_EMIT_SYMBOL, 141), + (23, HUFFMAN_EMIT_SYMBOL, 141), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 141), + (2, HUFFMAN_EMIT_SYMBOL, 143), + (9, HUFFMAN_EMIT_SYMBOL, 143), + (23, HUFFMAN_EMIT_SYMBOL, 143), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 143), + + # Node 151 + (3, HUFFMAN_EMIT_SYMBOL, 139), + (6, HUFFMAN_EMIT_SYMBOL, 139), + (10, HUFFMAN_EMIT_SYMBOL, 139), + (15, HUFFMAN_EMIT_SYMBOL, 139), + (24, HUFFMAN_EMIT_SYMBOL, 139), + (31, HUFFMAN_EMIT_SYMBOL, 139), + (41, HUFFMAN_EMIT_SYMBOL, 139), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 139), + (3, HUFFMAN_EMIT_SYMBOL, 140), + (6, HUFFMAN_EMIT_SYMBOL, 140), + (10, HUFFMAN_EMIT_SYMBOL, 140), + (15, HUFFMAN_EMIT_SYMBOL, 140), + (24, HUFFMAN_EMIT_SYMBOL, 140), + (31, HUFFMAN_EMIT_SYMBOL, 140), + (41, HUFFMAN_EMIT_SYMBOL, 140), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 140), + + # Node 152 + (3, HUFFMAN_EMIT_SYMBOL, 141), + (6, HUFFMAN_EMIT_SYMBOL, 141), + (10, HUFFMAN_EMIT_SYMBOL, 141), + (15, HUFFMAN_EMIT_SYMBOL, 141), + (24, HUFFMAN_EMIT_SYMBOL, 141), + (31, HUFFMAN_EMIT_SYMBOL, 141), + (41, HUFFMAN_EMIT_SYMBOL, 141), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 141), + (3, HUFFMAN_EMIT_SYMBOL, 143), + (6, HUFFMAN_EMIT_SYMBOL, 143), + (10, HUFFMAN_EMIT_SYMBOL, 143), + (15, HUFFMAN_EMIT_SYMBOL, 143), + (24, HUFFMAN_EMIT_SYMBOL, 143), + (31, HUFFMAN_EMIT_SYMBOL, 143), + (41, HUFFMAN_EMIT_SYMBOL, 143), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 143), + + # Node 153 + (157, 0, 0), + (158, 0, 0), + (160, 0, 0), + (161, 0, 0), + (164, 0, 0), + (165, 0, 0), + (167, 0, 0), + (168, 0, 0), + (172, 0, 0), + (173, 0, 0), + (175, 0, 0), + (177, 0, 0), + (182, 0, 0), + (185, 0, 0), + (191, 0, 0), + (207, 0, 0), + + # Node 154 + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 147), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 149), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 150), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 151), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 152), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 155), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 157), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 158), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 165), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 166), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 168), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 174), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 175), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 180), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 182), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 183), + + # Node 155 + (1, HUFFMAN_EMIT_SYMBOL, 147), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 147), + (1, HUFFMAN_EMIT_SYMBOL, 149), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 149), + (1, HUFFMAN_EMIT_SYMBOL, 150), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 150), + (1, HUFFMAN_EMIT_SYMBOL, 151), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 151), + (1, HUFFMAN_EMIT_SYMBOL, 152), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 152), + (1, HUFFMAN_EMIT_SYMBOL, 155), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 155), + (1, HUFFMAN_EMIT_SYMBOL, 157), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 157), + (1, HUFFMAN_EMIT_SYMBOL, 158), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 158), + + # Node 156 + (2, HUFFMAN_EMIT_SYMBOL, 147), + (9, HUFFMAN_EMIT_SYMBOL, 147), + (23, HUFFMAN_EMIT_SYMBOL, 147), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 147), + (2, HUFFMAN_EMIT_SYMBOL, 149), + (9, HUFFMAN_EMIT_SYMBOL, 149), + (23, HUFFMAN_EMIT_SYMBOL, 149), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 149), + (2, HUFFMAN_EMIT_SYMBOL, 150), + (9, HUFFMAN_EMIT_SYMBOL, 150), + (23, HUFFMAN_EMIT_SYMBOL, 150), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 150), + (2, HUFFMAN_EMIT_SYMBOL, 151), + (9, HUFFMAN_EMIT_SYMBOL, 151), + (23, HUFFMAN_EMIT_SYMBOL, 151), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 151), + + # Node 157 + (3, HUFFMAN_EMIT_SYMBOL, 147), + (6, HUFFMAN_EMIT_SYMBOL, 147), + (10, HUFFMAN_EMIT_SYMBOL, 147), + (15, HUFFMAN_EMIT_SYMBOL, 147), + (24, HUFFMAN_EMIT_SYMBOL, 147), + (31, HUFFMAN_EMIT_SYMBOL, 147), + (41, HUFFMAN_EMIT_SYMBOL, 147), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 147), + (3, HUFFMAN_EMIT_SYMBOL, 149), + (6, HUFFMAN_EMIT_SYMBOL, 149), + (10, HUFFMAN_EMIT_SYMBOL, 149), + (15, HUFFMAN_EMIT_SYMBOL, 149), + (24, HUFFMAN_EMIT_SYMBOL, 149), + (31, HUFFMAN_EMIT_SYMBOL, 149), + (41, HUFFMAN_EMIT_SYMBOL, 149), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 149), + + # Node 158 + (3, HUFFMAN_EMIT_SYMBOL, 150), + (6, HUFFMAN_EMIT_SYMBOL, 150), + (10, HUFFMAN_EMIT_SYMBOL, 150), + (15, HUFFMAN_EMIT_SYMBOL, 150), + (24, HUFFMAN_EMIT_SYMBOL, 150), + (31, HUFFMAN_EMIT_SYMBOL, 150), + (41, HUFFMAN_EMIT_SYMBOL, 150), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 150), + (3, HUFFMAN_EMIT_SYMBOL, 151), + (6, HUFFMAN_EMIT_SYMBOL, 151), + (10, HUFFMAN_EMIT_SYMBOL, 151), + (15, HUFFMAN_EMIT_SYMBOL, 151), + (24, HUFFMAN_EMIT_SYMBOL, 151), + (31, HUFFMAN_EMIT_SYMBOL, 151), + (41, HUFFMAN_EMIT_SYMBOL, 151), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 151), + + # Node 159 + (2, HUFFMAN_EMIT_SYMBOL, 152), + (9, HUFFMAN_EMIT_SYMBOL, 152), + (23, HUFFMAN_EMIT_SYMBOL, 152), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 152), + (2, HUFFMAN_EMIT_SYMBOL, 155), + (9, HUFFMAN_EMIT_SYMBOL, 155), + (23, HUFFMAN_EMIT_SYMBOL, 155), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 155), + (2, HUFFMAN_EMIT_SYMBOL, 157), + (9, HUFFMAN_EMIT_SYMBOL, 157), + (23, HUFFMAN_EMIT_SYMBOL, 157), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 157), + (2, HUFFMAN_EMIT_SYMBOL, 158), + (9, HUFFMAN_EMIT_SYMBOL, 158), + (23, HUFFMAN_EMIT_SYMBOL, 158), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 158), + + # Node 160 + (3, HUFFMAN_EMIT_SYMBOL, 152), + (6, HUFFMAN_EMIT_SYMBOL, 152), + (10, HUFFMAN_EMIT_SYMBOL, 152), + (15, HUFFMAN_EMIT_SYMBOL, 152), + (24, HUFFMAN_EMIT_SYMBOL, 152), + (31, HUFFMAN_EMIT_SYMBOL, 152), + (41, HUFFMAN_EMIT_SYMBOL, 152), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 152), + (3, HUFFMAN_EMIT_SYMBOL, 155), + (6, HUFFMAN_EMIT_SYMBOL, 155), + (10, HUFFMAN_EMIT_SYMBOL, 155), + (15, HUFFMAN_EMIT_SYMBOL, 155), + (24, HUFFMAN_EMIT_SYMBOL, 155), + (31, HUFFMAN_EMIT_SYMBOL, 155), + (41, HUFFMAN_EMIT_SYMBOL, 155), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 155), + + # Node 161 + (3, HUFFMAN_EMIT_SYMBOL, 157), + (6, HUFFMAN_EMIT_SYMBOL, 157), + (10, HUFFMAN_EMIT_SYMBOL, 157), + (15, HUFFMAN_EMIT_SYMBOL, 157), + (24, HUFFMAN_EMIT_SYMBOL, 157), + (31, HUFFMAN_EMIT_SYMBOL, 157), + (41, HUFFMAN_EMIT_SYMBOL, 157), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 157), + (3, HUFFMAN_EMIT_SYMBOL, 158), + (6, HUFFMAN_EMIT_SYMBOL, 158), + (10, HUFFMAN_EMIT_SYMBOL, 158), + (15, HUFFMAN_EMIT_SYMBOL, 158), + (24, HUFFMAN_EMIT_SYMBOL, 158), + (31, HUFFMAN_EMIT_SYMBOL, 158), + (41, HUFFMAN_EMIT_SYMBOL, 158), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 158), + + # Node 162 + (1, HUFFMAN_EMIT_SYMBOL, 165), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 165), + (1, HUFFMAN_EMIT_SYMBOL, 166), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 166), + (1, HUFFMAN_EMIT_SYMBOL, 168), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 168), + (1, HUFFMAN_EMIT_SYMBOL, 174), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 174), + (1, HUFFMAN_EMIT_SYMBOL, 175), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 175), + (1, HUFFMAN_EMIT_SYMBOL, 180), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 180), + (1, HUFFMAN_EMIT_SYMBOL, 182), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 182), + (1, HUFFMAN_EMIT_SYMBOL, 183), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 183), + + # Node 163 + (2, HUFFMAN_EMIT_SYMBOL, 165), + (9, HUFFMAN_EMIT_SYMBOL, 165), + (23, HUFFMAN_EMIT_SYMBOL, 165), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 165), + (2, HUFFMAN_EMIT_SYMBOL, 166), + (9, HUFFMAN_EMIT_SYMBOL, 166), + (23, HUFFMAN_EMIT_SYMBOL, 166), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 166), + (2, HUFFMAN_EMIT_SYMBOL, 168), + (9, HUFFMAN_EMIT_SYMBOL, 168), + (23, HUFFMAN_EMIT_SYMBOL, 168), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 168), + (2, HUFFMAN_EMIT_SYMBOL, 174), + (9, HUFFMAN_EMIT_SYMBOL, 174), + (23, HUFFMAN_EMIT_SYMBOL, 174), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 174), + + # Node 164 + (3, HUFFMAN_EMIT_SYMBOL, 165), + (6, HUFFMAN_EMIT_SYMBOL, 165), + (10, HUFFMAN_EMIT_SYMBOL, 165), + (15, HUFFMAN_EMIT_SYMBOL, 165), + (24, HUFFMAN_EMIT_SYMBOL, 165), + (31, HUFFMAN_EMIT_SYMBOL, 165), + (41, HUFFMAN_EMIT_SYMBOL, 165), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 165), + (3, HUFFMAN_EMIT_SYMBOL, 166), + (6, HUFFMAN_EMIT_SYMBOL, 166), + (10, HUFFMAN_EMIT_SYMBOL, 166), + (15, HUFFMAN_EMIT_SYMBOL, 166), + (24, HUFFMAN_EMIT_SYMBOL, 166), + (31, HUFFMAN_EMIT_SYMBOL, 166), + (41, HUFFMAN_EMIT_SYMBOL, 166), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 166), + + # Node 165 + (3, HUFFMAN_EMIT_SYMBOL, 168), + (6, HUFFMAN_EMIT_SYMBOL, 168), + (10, HUFFMAN_EMIT_SYMBOL, 168), + (15, HUFFMAN_EMIT_SYMBOL, 168), + (24, HUFFMAN_EMIT_SYMBOL, 168), + (31, HUFFMAN_EMIT_SYMBOL, 168), + (41, HUFFMAN_EMIT_SYMBOL, 168), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 168), + (3, HUFFMAN_EMIT_SYMBOL, 174), + (6, HUFFMAN_EMIT_SYMBOL, 174), + (10, HUFFMAN_EMIT_SYMBOL, 174), + (15, HUFFMAN_EMIT_SYMBOL, 174), + (24, HUFFMAN_EMIT_SYMBOL, 174), + (31, HUFFMAN_EMIT_SYMBOL, 174), + (41, HUFFMAN_EMIT_SYMBOL, 174), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 174), + + # Node 166 + (2, HUFFMAN_EMIT_SYMBOL, 175), + (9, HUFFMAN_EMIT_SYMBOL, 175), + (23, HUFFMAN_EMIT_SYMBOL, 175), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 175), + (2, HUFFMAN_EMIT_SYMBOL, 180), + (9, HUFFMAN_EMIT_SYMBOL, 180), + (23, HUFFMAN_EMIT_SYMBOL, 180), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 180), + (2, HUFFMAN_EMIT_SYMBOL, 182), + (9, HUFFMAN_EMIT_SYMBOL, 182), + (23, HUFFMAN_EMIT_SYMBOL, 182), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 182), + (2, HUFFMAN_EMIT_SYMBOL, 183), + (9, HUFFMAN_EMIT_SYMBOL, 183), + (23, HUFFMAN_EMIT_SYMBOL, 183), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 183), + + # Node 167 + (3, HUFFMAN_EMIT_SYMBOL, 175), + (6, HUFFMAN_EMIT_SYMBOL, 175), + (10, HUFFMAN_EMIT_SYMBOL, 175), + (15, HUFFMAN_EMIT_SYMBOL, 175), + (24, HUFFMAN_EMIT_SYMBOL, 175), + (31, HUFFMAN_EMIT_SYMBOL, 175), + (41, HUFFMAN_EMIT_SYMBOL, 175), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 175), + (3, HUFFMAN_EMIT_SYMBOL, 180), + (6, HUFFMAN_EMIT_SYMBOL, 180), + (10, HUFFMAN_EMIT_SYMBOL, 180), + (15, HUFFMAN_EMIT_SYMBOL, 180), + (24, HUFFMAN_EMIT_SYMBOL, 180), + (31, HUFFMAN_EMIT_SYMBOL, 180), + (41, HUFFMAN_EMIT_SYMBOL, 180), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 180), + + # Node 168 + (3, HUFFMAN_EMIT_SYMBOL, 182), + (6, HUFFMAN_EMIT_SYMBOL, 182), + (10, HUFFMAN_EMIT_SYMBOL, 182), + (15, HUFFMAN_EMIT_SYMBOL, 182), + (24, HUFFMAN_EMIT_SYMBOL, 182), + (31, HUFFMAN_EMIT_SYMBOL, 182), + (41, HUFFMAN_EMIT_SYMBOL, 182), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 182), + (3, HUFFMAN_EMIT_SYMBOL, 183), + (6, HUFFMAN_EMIT_SYMBOL, 183), + (10, HUFFMAN_EMIT_SYMBOL, 183), + (15, HUFFMAN_EMIT_SYMBOL, 183), + (24, HUFFMAN_EMIT_SYMBOL, 183), + (31, HUFFMAN_EMIT_SYMBOL, 183), + (41, HUFFMAN_EMIT_SYMBOL, 183), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 183), + + # Node 169 + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 188), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 191), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 197), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 231), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 239), + (176, 0, 0), + (178, 0, 0), + (179, 0, 0), + (183, 0, 0), + (184, 0, 0), + (186, 0, 0), + (187, 0, 0), + (192, 0, 0), + (199, 0, 0), + (208, 0, 0), + (223, 0, 0), + + # Node 170 + (1, HUFFMAN_EMIT_SYMBOL, 188), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 188), + (1, HUFFMAN_EMIT_SYMBOL, 191), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 191), + (1, HUFFMAN_EMIT_SYMBOL, 197), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 197), + (1, HUFFMAN_EMIT_SYMBOL, 231), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 231), + (1, HUFFMAN_EMIT_SYMBOL, 239), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 239), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 9), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 142), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 144), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 145), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 148), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 159), + + # Node 171 + (2, HUFFMAN_EMIT_SYMBOL, 188), + (9, HUFFMAN_EMIT_SYMBOL, 188), + (23, HUFFMAN_EMIT_SYMBOL, 188), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 188), + (2, HUFFMAN_EMIT_SYMBOL, 191), + (9, HUFFMAN_EMIT_SYMBOL, 191), + (23, HUFFMAN_EMIT_SYMBOL, 191), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 191), + (2, HUFFMAN_EMIT_SYMBOL, 197), + (9, HUFFMAN_EMIT_SYMBOL, 197), + (23, HUFFMAN_EMIT_SYMBOL, 197), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 197), + (2, HUFFMAN_EMIT_SYMBOL, 231), + (9, HUFFMAN_EMIT_SYMBOL, 231), + (23, HUFFMAN_EMIT_SYMBOL, 231), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 231), + + # Node 172 + (3, HUFFMAN_EMIT_SYMBOL, 188), + (6, HUFFMAN_EMIT_SYMBOL, 188), + (10, HUFFMAN_EMIT_SYMBOL, 188), + (15, HUFFMAN_EMIT_SYMBOL, 188), + (24, HUFFMAN_EMIT_SYMBOL, 188), + (31, HUFFMAN_EMIT_SYMBOL, 188), + (41, HUFFMAN_EMIT_SYMBOL, 188), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 188), + (3, HUFFMAN_EMIT_SYMBOL, 191), + (6, HUFFMAN_EMIT_SYMBOL, 191), + (10, HUFFMAN_EMIT_SYMBOL, 191), + (15, HUFFMAN_EMIT_SYMBOL, 191), + (24, HUFFMAN_EMIT_SYMBOL, 191), + (31, HUFFMAN_EMIT_SYMBOL, 191), + (41, HUFFMAN_EMIT_SYMBOL, 191), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 191), + + # Node 173 + (3, HUFFMAN_EMIT_SYMBOL, 197), + (6, HUFFMAN_EMIT_SYMBOL, 197), + (10, HUFFMAN_EMIT_SYMBOL, 197), + (15, HUFFMAN_EMIT_SYMBOL, 197), + (24, HUFFMAN_EMIT_SYMBOL, 197), + (31, HUFFMAN_EMIT_SYMBOL, 197), + (41, HUFFMAN_EMIT_SYMBOL, 197), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 197), + (3, HUFFMAN_EMIT_SYMBOL, 231), + (6, HUFFMAN_EMIT_SYMBOL, 231), + (10, HUFFMAN_EMIT_SYMBOL, 231), + (15, HUFFMAN_EMIT_SYMBOL, 231), + (24, HUFFMAN_EMIT_SYMBOL, 231), + (31, HUFFMAN_EMIT_SYMBOL, 231), + (41, HUFFMAN_EMIT_SYMBOL, 231), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 231), + + # Node 174 + (2, HUFFMAN_EMIT_SYMBOL, 239), + (9, HUFFMAN_EMIT_SYMBOL, 239), + (23, HUFFMAN_EMIT_SYMBOL, 239), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 239), + (1, HUFFMAN_EMIT_SYMBOL, 9), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 9), + (1, HUFFMAN_EMIT_SYMBOL, 142), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 142), + (1, HUFFMAN_EMIT_SYMBOL, 144), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 144), + (1, HUFFMAN_EMIT_SYMBOL, 145), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 145), + (1, HUFFMAN_EMIT_SYMBOL, 148), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 148), + (1, HUFFMAN_EMIT_SYMBOL, 159), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 159), + + # Node 175 + (3, HUFFMAN_EMIT_SYMBOL, 239), + (6, HUFFMAN_EMIT_SYMBOL, 239), + (10, HUFFMAN_EMIT_SYMBOL, 239), + (15, HUFFMAN_EMIT_SYMBOL, 239), + (24, HUFFMAN_EMIT_SYMBOL, 239), + (31, HUFFMAN_EMIT_SYMBOL, 239), + (41, HUFFMAN_EMIT_SYMBOL, 239), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 239), + (2, HUFFMAN_EMIT_SYMBOL, 9), + (9, HUFFMAN_EMIT_SYMBOL, 9), + (23, HUFFMAN_EMIT_SYMBOL, 9), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 9), + (2, HUFFMAN_EMIT_SYMBOL, 142), + (9, HUFFMAN_EMIT_SYMBOL, 142), + (23, HUFFMAN_EMIT_SYMBOL, 142), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 142), + + # Node 176 + (3, HUFFMAN_EMIT_SYMBOL, 9), + (6, HUFFMAN_EMIT_SYMBOL, 9), + (10, HUFFMAN_EMIT_SYMBOL, 9), + (15, HUFFMAN_EMIT_SYMBOL, 9), + (24, HUFFMAN_EMIT_SYMBOL, 9), + (31, HUFFMAN_EMIT_SYMBOL, 9), + (41, HUFFMAN_EMIT_SYMBOL, 9), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 9), + (3, HUFFMAN_EMIT_SYMBOL, 142), + (6, HUFFMAN_EMIT_SYMBOL, 142), + (10, HUFFMAN_EMIT_SYMBOL, 142), + (15, HUFFMAN_EMIT_SYMBOL, 142), + (24, HUFFMAN_EMIT_SYMBOL, 142), + (31, HUFFMAN_EMIT_SYMBOL, 142), + (41, HUFFMAN_EMIT_SYMBOL, 142), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 142), + + # Node 177 + (2, HUFFMAN_EMIT_SYMBOL, 144), + (9, HUFFMAN_EMIT_SYMBOL, 144), + (23, HUFFMAN_EMIT_SYMBOL, 144), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 144), + (2, HUFFMAN_EMIT_SYMBOL, 145), + (9, HUFFMAN_EMIT_SYMBOL, 145), + (23, HUFFMAN_EMIT_SYMBOL, 145), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 145), + (2, HUFFMAN_EMIT_SYMBOL, 148), + (9, HUFFMAN_EMIT_SYMBOL, 148), + (23, HUFFMAN_EMIT_SYMBOL, 148), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 148), + (2, HUFFMAN_EMIT_SYMBOL, 159), + (9, HUFFMAN_EMIT_SYMBOL, 159), + (23, HUFFMAN_EMIT_SYMBOL, 159), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 159), + + # Node 178 + (3, HUFFMAN_EMIT_SYMBOL, 144), + (6, HUFFMAN_EMIT_SYMBOL, 144), + (10, HUFFMAN_EMIT_SYMBOL, 144), + (15, HUFFMAN_EMIT_SYMBOL, 144), + (24, HUFFMAN_EMIT_SYMBOL, 144), + (31, HUFFMAN_EMIT_SYMBOL, 144), + (41, HUFFMAN_EMIT_SYMBOL, 144), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 144), + (3, HUFFMAN_EMIT_SYMBOL, 145), + (6, HUFFMAN_EMIT_SYMBOL, 145), + (10, HUFFMAN_EMIT_SYMBOL, 145), + (15, HUFFMAN_EMIT_SYMBOL, 145), + (24, HUFFMAN_EMIT_SYMBOL, 145), + (31, HUFFMAN_EMIT_SYMBOL, 145), + (41, HUFFMAN_EMIT_SYMBOL, 145), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 145), + + # Node 179 + (3, HUFFMAN_EMIT_SYMBOL, 148), + (6, HUFFMAN_EMIT_SYMBOL, 148), + (10, HUFFMAN_EMIT_SYMBOL, 148), + (15, HUFFMAN_EMIT_SYMBOL, 148), + (24, HUFFMAN_EMIT_SYMBOL, 148), + (31, HUFFMAN_EMIT_SYMBOL, 148), + (41, HUFFMAN_EMIT_SYMBOL, 148), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 148), + (3, HUFFMAN_EMIT_SYMBOL, 159), + (6, HUFFMAN_EMIT_SYMBOL, 159), + (10, HUFFMAN_EMIT_SYMBOL, 159), + (15, HUFFMAN_EMIT_SYMBOL, 159), + (24, HUFFMAN_EMIT_SYMBOL, 159), + (31, HUFFMAN_EMIT_SYMBOL, 159), + (41, HUFFMAN_EMIT_SYMBOL, 159), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 159), + + # Node 180 + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 171), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 206), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 215), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 225), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 236), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 237), + (188, 0, 0), + (189, 0, 0), + (193, 0, 0), + (196, 0, 0), + (200, 0, 0), + (203, 0, 0), + (209, 0, 0), + (216, 0, 0), + (224, 0, 0), + (238, 0, 0), + + # Node 181 + (1, HUFFMAN_EMIT_SYMBOL, 171), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 171), + (1, HUFFMAN_EMIT_SYMBOL, 206), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 206), + (1, HUFFMAN_EMIT_SYMBOL, 215), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 215), + (1, HUFFMAN_EMIT_SYMBOL, 225), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 225), + (1, HUFFMAN_EMIT_SYMBOL, 236), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 236), + (1, HUFFMAN_EMIT_SYMBOL, 237), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 237), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 199), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 207), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 234), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 235), + + # Node 182 + (2, HUFFMAN_EMIT_SYMBOL, 171), + (9, HUFFMAN_EMIT_SYMBOL, 171), + (23, HUFFMAN_EMIT_SYMBOL, 171), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 171), + (2, HUFFMAN_EMIT_SYMBOL, 206), + (9, HUFFMAN_EMIT_SYMBOL, 206), + (23, HUFFMAN_EMIT_SYMBOL, 206), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 206), + (2, HUFFMAN_EMIT_SYMBOL, 215), + (9, HUFFMAN_EMIT_SYMBOL, 215), + (23, HUFFMAN_EMIT_SYMBOL, 215), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 215), + (2, HUFFMAN_EMIT_SYMBOL, 225), + (9, HUFFMAN_EMIT_SYMBOL, 225), + (23, HUFFMAN_EMIT_SYMBOL, 225), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 225), + + # Node 183 + (3, HUFFMAN_EMIT_SYMBOL, 171), + (6, HUFFMAN_EMIT_SYMBOL, 171), + (10, HUFFMAN_EMIT_SYMBOL, 171), + (15, HUFFMAN_EMIT_SYMBOL, 171), + (24, HUFFMAN_EMIT_SYMBOL, 171), + (31, HUFFMAN_EMIT_SYMBOL, 171), + (41, HUFFMAN_EMIT_SYMBOL, 171), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 171), + (3, HUFFMAN_EMIT_SYMBOL, 206), + (6, HUFFMAN_EMIT_SYMBOL, 206), + (10, HUFFMAN_EMIT_SYMBOL, 206), + (15, HUFFMAN_EMIT_SYMBOL, 206), + (24, HUFFMAN_EMIT_SYMBOL, 206), + (31, HUFFMAN_EMIT_SYMBOL, 206), + (41, HUFFMAN_EMIT_SYMBOL, 206), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 206), + + # Node 184 + (3, HUFFMAN_EMIT_SYMBOL, 215), + (6, HUFFMAN_EMIT_SYMBOL, 215), + (10, HUFFMAN_EMIT_SYMBOL, 215), + (15, HUFFMAN_EMIT_SYMBOL, 215), + (24, HUFFMAN_EMIT_SYMBOL, 215), + (31, HUFFMAN_EMIT_SYMBOL, 215), + (41, HUFFMAN_EMIT_SYMBOL, 215), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 215), + (3, HUFFMAN_EMIT_SYMBOL, 225), + (6, HUFFMAN_EMIT_SYMBOL, 225), + (10, HUFFMAN_EMIT_SYMBOL, 225), + (15, HUFFMAN_EMIT_SYMBOL, 225), + (24, HUFFMAN_EMIT_SYMBOL, 225), + (31, HUFFMAN_EMIT_SYMBOL, 225), + (41, HUFFMAN_EMIT_SYMBOL, 225), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 225), + + # Node 185 + (2, HUFFMAN_EMIT_SYMBOL, 236), + (9, HUFFMAN_EMIT_SYMBOL, 236), + (23, HUFFMAN_EMIT_SYMBOL, 236), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 236), + (2, HUFFMAN_EMIT_SYMBOL, 237), + (9, HUFFMAN_EMIT_SYMBOL, 237), + (23, HUFFMAN_EMIT_SYMBOL, 237), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 237), + (1, HUFFMAN_EMIT_SYMBOL, 199), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 199), + (1, HUFFMAN_EMIT_SYMBOL, 207), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 207), + (1, HUFFMAN_EMIT_SYMBOL, 234), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 234), + (1, HUFFMAN_EMIT_SYMBOL, 235), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 235), + + # Node 186 + (3, HUFFMAN_EMIT_SYMBOL, 236), + (6, HUFFMAN_EMIT_SYMBOL, 236), + (10, HUFFMAN_EMIT_SYMBOL, 236), + (15, HUFFMAN_EMIT_SYMBOL, 236), + (24, HUFFMAN_EMIT_SYMBOL, 236), + (31, HUFFMAN_EMIT_SYMBOL, 236), + (41, HUFFMAN_EMIT_SYMBOL, 236), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 236), + (3, HUFFMAN_EMIT_SYMBOL, 237), + (6, HUFFMAN_EMIT_SYMBOL, 237), + (10, HUFFMAN_EMIT_SYMBOL, 237), + (15, HUFFMAN_EMIT_SYMBOL, 237), + (24, HUFFMAN_EMIT_SYMBOL, 237), + (31, HUFFMAN_EMIT_SYMBOL, 237), + (41, HUFFMAN_EMIT_SYMBOL, 237), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 237), + + # Node 187 + (2, HUFFMAN_EMIT_SYMBOL, 199), + (9, HUFFMAN_EMIT_SYMBOL, 199), + (23, HUFFMAN_EMIT_SYMBOL, 199), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 199), + (2, HUFFMAN_EMIT_SYMBOL, 207), + (9, HUFFMAN_EMIT_SYMBOL, 207), + (23, HUFFMAN_EMIT_SYMBOL, 207), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 207), + (2, HUFFMAN_EMIT_SYMBOL, 234), + (9, HUFFMAN_EMIT_SYMBOL, 234), + (23, HUFFMAN_EMIT_SYMBOL, 234), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 234), + (2, HUFFMAN_EMIT_SYMBOL, 235), + (9, HUFFMAN_EMIT_SYMBOL, 235), + (23, HUFFMAN_EMIT_SYMBOL, 235), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 235), + + # Node 188 + (3, HUFFMAN_EMIT_SYMBOL, 199), + (6, HUFFMAN_EMIT_SYMBOL, 199), + (10, HUFFMAN_EMIT_SYMBOL, 199), + (15, HUFFMAN_EMIT_SYMBOL, 199), + (24, HUFFMAN_EMIT_SYMBOL, 199), + (31, HUFFMAN_EMIT_SYMBOL, 199), + (41, HUFFMAN_EMIT_SYMBOL, 199), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 199), + (3, HUFFMAN_EMIT_SYMBOL, 207), + (6, HUFFMAN_EMIT_SYMBOL, 207), + (10, HUFFMAN_EMIT_SYMBOL, 207), + (15, HUFFMAN_EMIT_SYMBOL, 207), + (24, HUFFMAN_EMIT_SYMBOL, 207), + (31, HUFFMAN_EMIT_SYMBOL, 207), + (41, HUFFMAN_EMIT_SYMBOL, 207), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 207), + + # Node 189 + (3, HUFFMAN_EMIT_SYMBOL, 234), + (6, HUFFMAN_EMIT_SYMBOL, 234), + (10, HUFFMAN_EMIT_SYMBOL, 234), + (15, HUFFMAN_EMIT_SYMBOL, 234), + (24, HUFFMAN_EMIT_SYMBOL, 234), + (31, HUFFMAN_EMIT_SYMBOL, 234), + (41, HUFFMAN_EMIT_SYMBOL, 234), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 234), + (3, HUFFMAN_EMIT_SYMBOL, 235), + (6, HUFFMAN_EMIT_SYMBOL, 235), + (10, HUFFMAN_EMIT_SYMBOL, 235), + (15, HUFFMAN_EMIT_SYMBOL, 235), + (24, HUFFMAN_EMIT_SYMBOL, 235), + (31, HUFFMAN_EMIT_SYMBOL, 235), + (41, HUFFMAN_EMIT_SYMBOL, 235), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 235), + + # Node 190 + (194, 0, 0), + (195, 0, 0), + (197, 0, 0), + (198, 0, 0), + (201, 0, 0), + (202, 0, 0), + (204, 0, 0), + (205, 0, 0), + (210, 0, 0), + (213, 0, 0), + (217, 0, 0), + (220, 0, 0), + (225, 0, 0), + (231, 0, 0), + (239, 0, 0), + (246, 0, 0), + + # Node 191 + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 192), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 193), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 200), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 201), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 202), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 205), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 210), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 213), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 218), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 219), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 238), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 240), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 242), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 243), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 255), + (206, 0, 0), + + # Node 192 + (1, HUFFMAN_EMIT_SYMBOL, 192), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 192), + (1, HUFFMAN_EMIT_SYMBOL, 193), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 193), + (1, HUFFMAN_EMIT_SYMBOL, 200), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 200), + (1, HUFFMAN_EMIT_SYMBOL, 201), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 201), + (1, HUFFMAN_EMIT_SYMBOL, 202), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 202), + (1, HUFFMAN_EMIT_SYMBOL, 205), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 205), + (1, HUFFMAN_EMIT_SYMBOL, 210), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 210), + (1, HUFFMAN_EMIT_SYMBOL, 213), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 213), + + # Node 193 + (2, HUFFMAN_EMIT_SYMBOL, 192), + (9, HUFFMAN_EMIT_SYMBOL, 192), + (23, HUFFMAN_EMIT_SYMBOL, 192), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 192), + (2, HUFFMAN_EMIT_SYMBOL, 193), + (9, HUFFMAN_EMIT_SYMBOL, 193), + (23, HUFFMAN_EMIT_SYMBOL, 193), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 193), + (2, HUFFMAN_EMIT_SYMBOL, 200), + (9, HUFFMAN_EMIT_SYMBOL, 200), + (23, HUFFMAN_EMIT_SYMBOL, 200), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 200), + (2, HUFFMAN_EMIT_SYMBOL, 201), + (9, HUFFMAN_EMIT_SYMBOL, 201), + (23, HUFFMAN_EMIT_SYMBOL, 201), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 201), + + # Node 194 + (3, HUFFMAN_EMIT_SYMBOL, 192), + (6, HUFFMAN_EMIT_SYMBOL, 192), + (10, HUFFMAN_EMIT_SYMBOL, 192), + (15, HUFFMAN_EMIT_SYMBOL, 192), + (24, HUFFMAN_EMIT_SYMBOL, 192), + (31, HUFFMAN_EMIT_SYMBOL, 192), + (41, HUFFMAN_EMIT_SYMBOL, 192), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 192), + (3, HUFFMAN_EMIT_SYMBOL, 193), + (6, HUFFMAN_EMIT_SYMBOL, 193), + (10, HUFFMAN_EMIT_SYMBOL, 193), + (15, HUFFMAN_EMIT_SYMBOL, 193), + (24, HUFFMAN_EMIT_SYMBOL, 193), + (31, HUFFMAN_EMIT_SYMBOL, 193), + (41, HUFFMAN_EMIT_SYMBOL, 193), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 193), + + # Node 195 + (3, HUFFMAN_EMIT_SYMBOL, 200), + (6, HUFFMAN_EMIT_SYMBOL, 200), + (10, HUFFMAN_EMIT_SYMBOL, 200), + (15, HUFFMAN_EMIT_SYMBOL, 200), + (24, HUFFMAN_EMIT_SYMBOL, 200), + (31, HUFFMAN_EMIT_SYMBOL, 200), + (41, HUFFMAN_EMIT_SYMBOL, 200), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 200), + (3, HUFFMAN_EMIT_SYMBOL, 201), + (6, HUFFMAN_EMIT_SYMBOL, 201), + (10, HUFFMAN_EMIT_SYMBOL, 201), + (15, HUFFMAN_EMIT_SYMBOL, 201), + (24, HUFFMAN_EMIT_SYMBOL, 201), + (31, HUFFMAN_EMIT_SYMBOL, 201), + (41, HUFFMAN_EMIT_SYMBOL, 201), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 201), + + # Node 196 + (2, HUFFMAN_EMIT_SYMBOL, 202), + (9, HUFFMAN_EMIT_SYMBOL, 202), + (23, HUFFMAN_EMIT_SYMBOL, 202), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 202), + (2, HUFFMAN_EMIT_SYMBOL, 205), + (9, HUFFMAN_EMIT_SYMBOL, 205), + (23, HUFFMAN_EMIT_SYMBOL, 205), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 205), + (2, HUFFMAN_EMIT_SYMBOL, 210), + (9, HUFFMAN_EMIT_SYMBOL, 210), + (23, HUFFMAN_EMIT_SYMBOL, 210), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 210), + (2, HUFFMAN_EMIT_SYMBOL, 213), + (9, HUFFMAN_EMIT_SYMBOL, 213), + (23, HUFFMAN_EMIT_SYMBOL, 213), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 213), + + # Node 197 + (3, HUFFMAN_EMIT_SYMBOL, 202), + (6, HUFFMAN_EMIT_SYMBOL, 202), + (10, HUFFMAN_EMIT_SYMBOL, 202), + (15, HUFFMAN_EMIT_SYMBOL, 202), + (24, HUFFMAN_EMIT_SYMBOL, 202), + (31, HUFFMAN_EMIT_SYMBOL, 202), + (41, HUFFMAN_EMIT_SYMBOL, 202), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 202), + (3, HUFFMAN_EMIT_SYMBOL, 205), + (6, HUFFMAN_EMIT_SYMBOL, 205), + (10, HUFFMAN_EMIT_SYMBOL, 205), + (15, HUFFMAN_EMIT_SYMBOL, 205), + (24, HUFFMAN_EMIT_SYMBOL, 205), + (31, HUFFMAN_EMIT_SYMBOL, 205), + (41, HUFFMAN_EMIT_SYMBOL, 205), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 205), + + # Node 198 + (3, HUFFMAN_EMIT_SYMBOL, 210), + (6, HUFFMAN_EMIT_SYMBOL, 210), + (10, HUFFMAN_EMIT_SYMBOL, 210), + (15, HUFFMAN_EMIT_SYMBOL, 210), + (24, HUFFMAN_EMIT_SYMBOL, 210), + (31, HUFFMAN_EMIT_SYMBOL, 210), + (41, HUFFMAN_EMIT_SYMBOL, 210), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 210), + (3, HUFFMAN_EMIT_SYMBOL, 213), + (6, HUFFMAN_EMIT_SYMBOL, 213), + (10, HUFFMAN_EMIT_SYMBOL, 213), + (15, HUFFMAN_EMIT_SYMBOL, 213), + (24, HUFFMAN_EMIT_SYMBOL, 213), + (31, HUFFMAN_EMIT_SYMBOL, 213), + (41, HUFFMAN_EMIT_SYMBOL, 213), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 213), + + # Node 199 + (1, HUFFMAN_EMIT_SYMBOL, 218), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 218), + (1, HUFFMAN_EMIT_SYMBOL, 219), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 219), + (1, HUFFMAN_EMIT_SYMBOL, 238), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 238), + (1, HUFFMAN_EMIT_SYMBOL, 240), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 240), + (1, HUFFMAN_EMIT_SYMBOL, 242), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 242), + (1, HUFFMAN_EMIT_SYMBOL, 243), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 243), + (1, HUFFMAN_EMIT_SYMBOL, 255), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 255), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 203), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 204), + + # Node 200 + (2, HUFFMAN_EMIT_SYMBOL, 218), + (9, HUFFMAN_EMIT_SYMBOL, 218), + (23, HUFFMAN_EMIT_SYMBOL, 218), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 218), + (2, HUFFMAN_EMIT_SYMBOL, 219), + (9, HUFFMAN_EMIT_SYMBOL, 219), + (23, HUFFMAN_EMIT_SYMBOL, 219), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 219), + (2, HUFFMAN_EMIT_SYMBOL, 238), + (9, HUFFMAN_EMIT_SYMBOL, 238), + (23, HUFFMAN_EMIT_SYMBOL, 238), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 238), + (2, HUFFMAN_EMIT_SYMBOL, 240), + (9, HUFFMAN_EMIT_SYMBOL, 240), + (23, HUFFMAN_EMIT_SYMBOL, 240), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 240), + + # Node 201 + (3, HUFFMAN_EMIT_SYMBOL, 218), + (6, HUFFMAN_EMIT_SYMBOL, 218), + (10, HUFFMAN_EMIT_SYMBOL, 218), + (15, HUFFMAN_EMIT_SYMBOL, 218), + (24, HUFFMAN_EMIT_SYMBOL, 218), + (31, HUFFMAN_EMIT_SYMBOL, 218), + (41, HUFFMAN_EMIT_SYMBOL, 218), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 218), + (3, HUFFMAN_EMIT_SYMBOL, 219), + (6, HUFFMAN_EMIT_SYMBOL, 219), + (10, HUFFMAN_EMIT_SYMBOL, 219), + (15, HUFFMAN_EMIT_SYMBOL, 219), + (24, HUFFMAN_EMIT_SYMBOL, 219), + (31, HUFFMAN_EMIT_SYMBOL, 219), + (41, HUFFMAN_EMIT_SYMBOL, 219), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 219), + + # Node 202 + (3, HUFFMAN_EMIT_SYMBOL, 238), + (6, HUFFMAN_EMIT_SYMBOL, 238), + (10, HUFFMAN_EMIT_SYMBOL, 238), + (15, HUFFMAN_EMIT_SYMBOL, 238), + (24, HUFFMAN_EMIT_SYMBOL, 238), + (31, HUFFMAN_EMIT_SYMBOL, 238), + (41, HUFFMAN_EMIT_SYMBOL, 238), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 238), + (3, HUFFMAN_EMIT_SYMBOL, 240), + (6, HUFFMAN_EMIT_SYMBOL, 240), + (10, HUFFMAN_EMIT_SYMBOL, 240), + (15, HUFFMAN_EMIT_SYMBOL, 240), + (24, HUFFMAN_EMIT_SYMBOL, 240), + (31, HUFFMAN_EMIT_SYMBOL, 240), + (41, HUFFMAN_EMIT_SYMBOL, 240), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 240), + + # Node 203 + (2, HUFFMAN_EMIT_SYMBOL, 242), + (9, HUFFMAN_EMIT_SYMBOL, 242), + (23, HUFFMAN_EMIT_SYMBOL, 242), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 242), + (2, HUFFMAN_EMIT_SYMBOL, 243), + (9, HUFFMAN_EMIT_SYMBOL, 243), + (23, HUFFMAN_EMIT_SYMBOL, 243), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 243), + (2, HUFFMAN_EMIT_SYMBOL, 255), + (9, HUFFMAN_EMIT_SYMBOL, 255), + (23, HUFFMAN_EMIT_SYMBOL, 255), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 255), + (1, HUFFMAN_EMIT_SYMBOL, 203), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 203), + (1, HUFFMAN_EMIT_SYMBOL, 204), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 204), + + # Node 204 + (3, HUFFMAN_EMIT_SYMBOL, 242), + (6, HUFFMAN_EMIT_SYMBOL, 242), + (10, HUFFMAN_EMIT_SYMBOL, 242), + (15, HUFFMAN_EMIT_SYMBOL, 242), + (24, HUFFMAN_EMIT_SYMBOL, 242), + (31, HUFFMAN_EMIT_SYMBOL, 242), + (41, HUFFMAN_EMIT_SYMBOL, 242), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 242), + (3, HUFFMAN_EMIT_SYMBOL, 243), + (6, HUFFMAN_EMIT_SYMBOL, 243), + (10, HUFFMAN_EMIT_SYMBOL, 243), + (15, HUFFMAN_EMIT_SYMBOL, 243), + (24, HUFFMAN_EMIT_SYMBOL, 243), + (31, HUFFMAN_EMIT_SYMBOL, 243), + (41, HUFFMAN_EMIT_SYMBOL, 243), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 243), + + # Node 205 + (3, HUFFMAN_EMIT_SYMBOL, 255), + (6, HUFFMAN_EMIT_SYMBOL, 255), + (10, HUFFMAN_EMIT_SYMBOL, 255), + (15, HUFFMAN_EMIT_SYMBOL, 255), + (24, HUFFMAN_EMIT_SYMBOL, 255), + (31, HUFFMAN_EMIT_SYMBOL, 255), + (41, HUFFMAN_EMIT_SYMBOL, 255), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 255), + (2, HUFFMAN_EMIT_SYMBOL, 203), + (9, HUFFMAN_EMIT_SYMBOL, 203), + (23, HUFFMAN_EMIT_SYMBOL, 203), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 203), + (2, HUFFMAN_EMIT_SYMBOL, 204), + (9, HUFFMAN_EMIT_SYMBOL, 204), + (23, HUFFMAN_EMIT_SYMBOL, 204), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 204), + + # Node 206 + (3, HUFFMAN_EMIT_SYMBOL, 203), + (6, HUFFMAN_EMIT_SYMBOL, 203), + (10, HUFFMAN_EMIT_SYMBOL, 203), + (15, HUFFMAN_EMIT_SYMBOL, 203), + (24, HUFFMAN_EMIT_SYMBOL, 203), + (31, HUFFMAN_EMIT_SYMBOL, 203), + (41, HUFFMAN_EMIT_SYMBOL, 203), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 203), + (3, HUFFMAN_EMIT_SYMBOL, 204), + (6, HUFFMAN_EMIT_SYMBOL, 204), + (10, HUFFMAN_EMIT_SYMBOL, 204), + (15, HUFFMAN_EMIT_SYMBOL, 204), + (24, HUFFMAN_EMIT_SYMBOL, 204), + (31, HUFFMAN_EMIT_SYMBOL, 204), + (41, HUFFMAN_EMIT_SYMBOL, 204), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 204), + + # Node 207 + (211, 0, 0), + (212, 0, 0), + (214, 0, 0), + (215, 0, 0), + (218, 0, 0), + (219, 0, 0), + (221, 0, 0), + (222, 0, 0), + (226, 0, 0), + (228, 0, 0), + (232, 0, 0), + (235, 0, 0), + (240, 0, 0), + (243, 0, 0), + (247, 0, 0), + (250, 0, 0), + + # Node 208 + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 211), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 212), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 214), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 221), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 222), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 223), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 241), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 244), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 245), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 246), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 247), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 248), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 250), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 251), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 252), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 253), + + # Node 209 + (1, HUFFMAN_EMIT_SYMBOL, 211), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 211), + (1, HUFFMAN_EMIT_SYMBOL, 212), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 212), + (1, HUFFMAN_EMIT_SYMBOL, 214), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 214), + (1, HUFFMAN_EMIT_SYMBOL, 221), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 221), + (1, HUFFMAN_EMIT_SYMBOL, 222), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 222), + (1, HUFFMAN_EMIT_SYMBOL, 223), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 223), + (1, HUFFMAN_EMIT_SYMBOL, 241), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 241), + (1, HUFFMAN_EMIT_SYMBOL, 244), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 244), + + # Node 210 + (2, HUFFMAN_EMIT_SYMBOL, 211), + (9, HUFFMAN_EMIT_SYMBOL, 211), + (23, HUFFMAN_EMIT_SYMBOL, 211), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 211), + (2, HUFFMAN_EMIT_SYMBOL, 212), + (9, HUFFMAN_EMIT_SYMBOL, 212), + (23, HUFFMAN_EMIT_SYMBOL, 212), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 212), + (2, HUFFMAN_EMIT_SYMBOL, 214), + (9, HUFFMAN_EMIT_SYMBOL, 214), + (23, HUFFMAN_EMIT_SYMBOL, 214), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 214), + (2, HUFFMAN_EMIT_SYMBOL, 221), + (9, HUFFMAN_EMIT_SYMBOL, 221), + (23, HUFFMAN_EMIT_SYMBOL, 221), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 221), + + # Node 211 + (3, HUFFMAN_EMIT_SYMBOL, 211), + (6, HUFFMAN_EMIT_SYMBOL, 211), + (10, HUFFMAN_EMIT_SYMBOL, 211), + (15, HUFFMAN_EMIT_SYMBOL, 211), + (24, HUFFMAN_EMIT_SYMBOL, 211), + (31, HUFFMAN_EMIT_SYMBOL, 211), + (41, HUFFMAN_EMIT_SYMBOL, 211), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 211), + (3, HUFFMAN_EMIT_SYMBOL, 212), + (6, HUFFMAN_EMIT_SYMBOL, 212), + (10, HUFFMAN_EMIT_SYMBOL, 212), + (15, HUFFMAN_EMIT_SYMBOL, 212), + (24, HUFFMAN_EMIT_SYMBOL, 212), + (31, HUFFMAN_EMIT_SYMBOL, 212), + (41, HUFFMAN_EMIT_SYMBOL, 212), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 212), + + # Node 212 + (3, HUFFMAN_EMIT_SYMBOL, 214), + (6, HUFFMAN_EMIT_SYMBOL, 214), + (10, HUFFMAN_EMIT_SYMBOL, 214), + (15, HUFFMAN_EMIT_SYMBOL, 214), + (24, HUFFMAN_EMIT_SYMBOL, 214), + (31, HUFFMAN_EMIT_SYMBOL, 214), + (41, HUFFMAN_EMIT_SYMBOL, 214), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 214), + (3, HUFFMAN_EMIT_SYMBOL, 221), + (6, HUFFMAN_EMIT_SYMBOL, 221), + (10, HUFFMAN_EMIT_SYMBOL, 221), + (15, HUFFMAN_EMIT_SYMBOL, 221), + (24, HUFFMAN_EMIT_SYMBOL, 221), + (31, HUFFMAN_EMIT_SYMBOL, 221), + (41, HUFFMAN_EMIT_SYMBOL, 221), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 221), + + # Node 213 + (2, HUFFMAN_EMIT_SYMBOL, 222), + (9, HUFFMAN_EMIT_SYMBOL, 222), + (23, HUFFMAN_EMIT_SYMBOL, 222), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 222), + (2, HUFFMAN_EMIT_SYMBOL, 223), + (9, HUFFMAN_EMIT_SYMBOL, 223), + (23, HUFFMAN_EMIT_SYMBOL, 223), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 223), + (2, HUFFMAN_EMIT_SYMBOL, 241), + (9, HUFFMAN_EMIT_SYMBOL, 241), + (23, HUFFMAN_EMIT_SYMBOL, 241), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 241), + (2, HUFFMAN_EMIT_SYMBOL, 244), + (9, HUFFMAN_EMIT_SYMBOL, 244), + (23, HUFFMAN_EMIT_SYMBOL, 244), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 244), + + # Node 214 + (3, HUFFMAN_EMIT_SYMBOL, 222), + (6, HUFFMAN_EMIT_SYMBOL, 222), + (10, HUFFMAN_EMIT_SYMBOL, 222), + (15, HUFFMAN_EMIT_SYMBOL, 222), + (24, HUFFMAN_EMIT_SYMBOL, 222), + (31, HUFFMAN_EMIT_SYMBOL, 222), + (41, HUFFMAN_EMIT_SYMBOL, 222), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 222), + (3, HUFFMAN_EMIT_SYMBOL, 223), + (6, HUFFMAN_EMIT_SYMBOL, 223), + (10, HUFFMAN_EMIT_SYMBOL, 223), + (15, HUFFMAN_EMIT_SYMBOL, 223), + (24, HUFFMAN_EMIT_SYMBOL, 223), + (31, HUFFMAN_EMIT_SYMBOL, 223), + (41, HUFFMAN_EMIT_SYMBOL, 223), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 223), + + # Node 215 + (3, HUFFMAN_EMIT_SYMBOL, 241), + (6, HUFFMAN_EMIT_SYMBOL, 241), + (10, HUFFMAN_EMIT_SYMBOL, 241), + (15, HUFFMAN_EMIT_SYMBOL, 241), + (24, HUFFMAN_EMIT_SYMBOL, 241), + (31, HUFFMAN_EMIT_SYMBOL, 241), + (41, HUFFMAN_EMIT_SYMBOL, 241), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 241), + (3, HUFFMAN_EMIT_SYMBOL, 244), + (6, HUFFMAN_EMIT_SYMBOL, 244), + (10, HUFFMAN_EMIT_SYMBOL, 244), + (15, HUFFMAN_EMIT_SYMBOL, 244), + (24, HUFFMAN_EMIT_SYMBOL, 244), + (31, HUFFMAN_EMIT_SYMBOL, 244), + (41, HUFFMAN_EMIT_SYMBOL, 244), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 244), + + # Node 216 + (1, HUFFMAN_EMIT_SYMBOL, 245), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 245), + (1, HUFFMAN_EMIT_SYMBOL, 246), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 246), + (1, HUFFMAN_EMIT_SYMBOL, 247), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 247), + (1, HUFFMAN_EMIT_SYMBOL, 248), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 248), + (1, HUFFMAN_EMIT_SYMBOL, 250), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 250), + (1, HUFFMAN_EMIT_SYMBOL, 251), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 251), + (1, HUFFMAN_EMIT_SYMBOL, 252), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 252), + (1, HUFFMAN_EMIT_SYMBOL, 253), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 253), + + # Node 217 + (2, HUFFMAN_EMIT_SYMBOL, 245), + (9, HUFFMAN_EMIT_SYMBOL, 245), + (23, HUFFMAN_EMIT_SYMBOL, 245), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 245), + (2, HUFFMAN_EMIT_SYMBOL, 246), + (9, HUFFMAN_EMIT_SYMBOL, 246), + (23, HUFFMAN_EMIT_SYMBOL, 246), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 246), + (2, HUFFMAN_EMIT_SYMBOL, 247), + (9, HUFFMAN_EMIT_SYMBOL, 247), + (23, HUFFMAN_EMIT_SYMBOL, 247), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 247), + (2, HUFFMAN_EMIT_SYMBOL, 248), + (9, HUFFMAN_EMIT_SYMBOL, 248), + (23, HUFFMAN_EMIT_SYMBOL, 248), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 248), + + # Node 218 + (3, HUFFMAN_EMIT_SYMBOL, 245), + (6, HUFFMAN_EMIT_SYMBOL, 245), + (10, HUFFMAN_EMIT_SYMBOL, 245), + (15, HUFFMAN_EMIT_SYMBOL, 245), + (24, HUFFMAN_EMIT_SYMBOL, 245), + (31, HUFFMAN_EMIT_SYMBOL, 245), + (41, HUFFMAN_EMIT_SYMBOL, 245), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 245), + (3, HUFFMAN_EMIT_SYMBOL, 246), + (6, HUFFMAN_EMIT_SYMBOL, 246), + (10, HUFFMAN_EMIT_SYMBOL, 246), + (15, HUFFMAN_EMIT_SYMBOL, 246), + (24, HUFFMAN_EMIT_SYMBOL, 246), + (31, HUFFMAN_EMIT_SYMBOL, 246), + (41, HUFFMAN_EMIT_SYMBOL, 246), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 246), + + # Node 219 + (3, HUFFMAN_EMIT_SYMBOL, 247), + (6, HUFFMAN_EMIT_SYMBOL, 247), + (10, HUFFMAN_EMIT_SYMBOL, 247), + (15, HUFFMAN_EMIT_SYMBOL, 247), + (24, HUFFMAN_EMIT_SYMBOL, 247), + (31, HUFFMAN_EMIT_SYMBOL, 247), + (41, HUFFMAN_EMIT_SYMBOL, 247), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 247), + (3, HUFFMAN_EMIT_SYMBOL, 248), + (6, HUFFMAN_EMIT_SYMBOL, 248), + (10, HUFFMAN_EMIT_SYMBOL, 248), + (15, HUFFMAN_EMIT_SYMBOL, 248), + (24, HUFFMAN_EMIT_SYMBOL, 248), + (31, HUFFMAN_EMIT_SYMBOL, 248), + (41, HUFFMAN_EMIT_SYMBOL, 248), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 248), + + # Node 220 + (2, HUFFMAN_EMIT_SYMBOL, 250), + (9, HUFFMAN_EMIT_SYMBOL, 250), + (23, HUFFMAN_EMIT_SYMBOL, 250), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 250), + (2, HUFFMAN_EMIT_SYMBOL, 251), + (9, HUFFMAN_EMIT_SYMBOL, 251), + (23, HUFFMAN_EMIT_SYMBOL, 251), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 251), + (2, HUFFMAN_EMIT_SYMBOL, 252), + (9, HUFFMAN_EMIT_SYMBOL, 252), + (23, HUFFMAN_EMIT_SYMBOL, 252), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 252), + (2, HUFFMAN_EMIT_SYMBOL, 253), + (9, HUFFMAN_EMIT_SYMBOL, 253), + (23, HUFFMAN_EMIT_SYMBOL, 253), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 253), + + # Node 221 + (3, HUFFMAN_EMIT_SYMBOL, 250), + (6, HUFFMAN_EMIT_SYMBOL, 250), + (10, HUFFMAN_EMIT_SYMBOL, 250), + (15, HUFFMAN_EMIT_SYMBOL, 250), + (24, HUFFMAN_EMIT_SYMBOL, 250), + (31, HUFFMAN_EMIT_SYMBOL, 250), + (41, HUFFMAN_EMIT_SYMBOL, 250), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 250), + (3, HUFFMAN_EMIT_SYMBOL, 251), + (6, HUFFMAN_EMIT_SYMBOL, 251), + (10, HUFFMAN_EMIT_SYMBOL, 251), + (15, HUFFMAN_EMIT_SYMBOL, 251), + (24, HUFFMAN_EMIT_SYMBOL, 251), + (31, HUFFMAN_EMIT_SYMBOL, 251), + (41, HUFFMAN_EMIT_SYMBOL, 251), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 251), + + # Node 222 + (3, HUFFMAN_EMIT_SYMBOL, 252), + (6, HUFFMAN_EMIT_SYMBOL, 252), + (10, HUFFMAN_EMIT_SYMBOL, 252), + (15, HUFFMAN_EMIT_SYMBOL, 252), + (24, HUFFMAN_EMIT_SYMBOL, 252), + (31, HUFFMAN_EMIT_SYMBOL, 252), + (41, HUFFMAN_EMIT_SYMBOL, 252), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 252), + (3, HUFFMAN_EMIT_SYMBOL, 253), + (6, HUFFMAN_EMIT_SYMBOL, 253), + (10, HUFFMAN_EMIT_SYMBOL, 253), + (15, HUFFMAN_EMIT_SYMBOL, 253), + (24, HUFFMAN_EMIT_SYMBOL, 253), + (31, HUFFMAN_EMIT_SYMBOL, 253), + (41, HUFFMAN_EMIT_SYMBOL, 253), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 253), + + # Node 223 + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 254), + (227, 0, 0), + (229, 0, 0), + (230, 0, 0), + (233, 0, 0), + (234, 0, 0), + (236, 0, 0), + (237, 0, 0), + (241, 0, 0), + (242, 0, 0), + (244, 0, 0), + (245, 0, 0), + (248, 0, 0), + (249, 0, 0), + (251, 0, 0), + (252, 0, 0), + + # Node 224 + (1, HUFFMAN_EMIT_SYMBOL, 254), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 254), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 2), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 3), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 4), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 5), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 6), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 7), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 8), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 11), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 12), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 14), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 15), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 16), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 17), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 18), + + # Node 225 + (2, HUFFMAN_EMIT_SYMBOL, 254), + (9, HUFFMAN_EMIT_SYMBOL, 254), + (23, HUFFMAN_EMIT_SYMBOL, 254), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 254), + (1, HUFFMAN_EMIT_SYMBOL, 2), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 2), + (1, HUFFMAN_EMIT_SYMBOL, 3), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 3), + (1, HUFFMAN_EMIT_SYMBOL, 4), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 4), + (1, HUFFMAN_EMIT_SYMBOL, 5), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 5), + (1, HUFFMAN_EMIT_SYMBOL, 6), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 6), + (1, HUFFMAN_EMIT_SYMBOL, 7), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 7), + + # Node 226 + (3, HUFFMAN_EMIT_SYMBOL, 254), + (6, HUFFMAN_EMIT_SYMBOL, 254), + (10, HUFFMAN_EMIT_SYMBOL, 254), + (15, HUFFMAN_EMIT_SYMBOL, 254), + (24, HUFFMAN_EMIT_SYMBOL, 254), + (31, HUFFMAN_EMIT_SYMBOL, 254), + (41, HUFFMAN_EMIT_SYMBOL, 254), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 254), + (2, HUFFMAN_EMIT_SYMBOL, 2), + (9, HUFFMAN_EMIT_SYMBOL, 2), + (23, HUFFMAN_EMIT_SYMBOL, 2), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 2), + (2, HUFFMAN_EMIT_SYMBOL, 3), + (9, HUFFMAN_EMIT_SYMBOL, 3), + (23, HUFFMAN_EMIT_SYMBOL, 3), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 3), + + # Node 227 + (3, HUFFMAN_EMIT_SYMBOL, 2), + (6, HUFFMAN_EMIT_SYMBOL, 2), + (10, HUFFMAN_EMIT_SYMBOL, 2), + (15, HUFFMAN_EMIT_SYMBOL, 2), + (24, HUFFMAN_EMIT_SYMBOL, 2), + (31, HUFFMAN_EMIT_SYMBOL, 2), + (41, HUFFMAN_EMIT_SYMBOL, 2), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 2), + (3, HUFFMAN_EMIT_SYMBOL, 3), + (6, HUFFMAN_EMIT_SYMBOL, 3), + (10, HUFFMAN_EMIT_SYMBOL, 3), + (15, HUFFMAN_EMIT_SYMBOL, 3), + (24, HUFFMAN_EMIT_SYMBOL, 3), + (31, HUFFMAN_EMIT_SYMBOL, 3), + (41, HUFFMAN_EMIT_SYMBOL, 3), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 3), + + # Node 228 + (2, HUFFMAN_EMIT_SYMBOL, 4), + (9, HUFFMAN_EMIT_SYMBOL, 4), + (23, HUFFMAN_EMIT_SYMBOL, 4), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 4), + (2, HUFFMAN_EMIT_SYMBOL, 5), + (9, HUFFMAN_EMIT_SYMBOL, 5), + (23, HUFFMAN_EMIT_SYMBOL, 5), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 5), + (2, HUFFMAN_EMIT_SYMBOL, 6), + (9, HUFFMAN_EMIT_SYMBOL, 6), + (23, HUFFMAN_EMIT_SYMBOL, 6), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 6), + (2, HUFFMAN_EMIT_SYMBOL, 7), + (9, HUFFMAN_EMIT_SYMBOL, 7), + (23, HUFFMAN_EMIT_SYMBOL, 7), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 7), + + # Node 229 + (3, HUFFMAN_EMIT_SYMBOL, 4), + (6, HUFFMAN_EMIT_SYMBOL, 4), + (10, HUFFMAN_EMIT_SYMBOL, 4), + (15, HUFFMAN_EMIT_SYMBOL, 4), + (24, HUFFMAN_EMIT_SYMBOL, 4), + (31, HUFFMAN_EMIT_SYMBOL, 4), + (41, HUFFMAN_EMIT_SYMBOL, 4), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 4), + (3, HUFFMAN_EMIT_SYMBOL, 5), + (6, HUFFMAN_EMIT_SYMBOL, 5), + (10, HUFFMAN_EMIT_SYMBOL, 5), + (15, HUFFMAN_EMIT_SYMBOL, 5), + (24, HUFFMAN_EMIT_SYMBOL, 5), + (31, HUFFMAN_EMIT_SYMBOL, 5), + (41, HUFFMAN_EMIT_SYMBOL, 5), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 5), + + # Node 230 + (3, HUFFMAN_EMIT_SYMBOL, 6), + (6, HUFFMAN_EMIT_SYMBOL, 6), + (10, HUFFMAN_EMIT_SYMBOL, 6), + (15, HUFFMAN_EMIT_SYMBOL, 6), + (24, HUFFMAN_EMIT_SYMBOL, 6), + (31, HUFFMAN_EMIT_SYMBOL, 6), + (41, HUFFMAN_EMIT_SYMBOL, 6), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 6), + (3, HUFFMAN_EMIT_SYMBOL, 7), + (6, HUFFMAN_EMIT_SYMBOL, 7), + (10, HUFFMAN_EMIT_SYMBOL, 7), + (15, HUFFMAN_EMIT_SYMBOL, 7), + (24, HUFFMAN_EMIT_SYMBOL, 7), + (31, HUFFMAN_EMIT_SYMBOL, 7), + (41, HUFFMAN_EMIT_SYMBOL, 7), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 7), + + # Node 231 + (1, HUFFMAN_EMIT_SYMBOL, 8), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 8), + (1, HUFFMAN_EMIT_SYMBOL, 11), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 11), + (1, HUFFMAN_EMIT_SYMBOL, 12), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 12), + (1, HUFFMAN_EMIT_SYMBOL, 14), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 14), + (1, HUFFMAN_EMIT_SYMBOL, 15), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 15), + (1, HUFFMAN_EMIT_SYMBOL, 16), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 16), + (1, HUFFMAN_EMIT_SYMBOL, 17), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 17), + (1, HUFFMAN_EMIT_SYMBOL, 18), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 18), + + # Node 232 + (2, HUFFMAN_EMIT_SYMBOL, 8), + (9, HUFFMAN_EMIT_SYMBOL, 8), + (23, HUFFMAN_EMIT_SYMBOL, 8), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 8), + (2, HUFFMAN_EMIT_SYMBOL, 11), + (9, HUFFMAN_EMIT_SYMBOL, 11), + (23, HUFFMAN_EMIT_SYMBOL, 11), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 11), + (2, HUFFMAN_EMIT_SYMBOL, 12), + (9, HUFFMAN_EMIT_SYMBOL, 12), + (23, HUFFMAN_EMIT_SYMBOL, 12), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 12), + (2, HUFFMAN_EMIT_SYMBOL, 14), + (9, HUFFMAN_EMIT_SYMBOL, 14), + (23, HUFFMAN_EMIT_SYMBOL, 14), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 14), + + # Node 233 + (3, HUFFMAN_EMIT_SYMBOL, 8), + (6, HUFFMAN_EMIT_SYMBOL, 8), + (10, HUFFMAN_EMIT_SYMBOL, 8), + (15, HUFFMAN_EMIT_SYMBOL, 8), + (24, HUFFMAN_EMIT_SYMBOL, 8), + (31, HUFFMAN_EMIT_SYMBOL, 8), + (41, HUFFMAN_EMIT_SYMBOL, 8), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 8), + (3, HUFFMAN_EMIT_SYMBOL, 11), + (6, HUFFMAN_EMIT_SYMBOL, 11), + (10, HUFFMAN_EMIT_SYMBOL, 11), + (15, HUFFMAN_EMIT_SYMBOL, 11), + (24, HUFFMAN_EMIT_SYMBOL, 11), + (31, HUFFMAN_EMIT_SYMBOL, 11), + (41, HUFFMAN_EMIT_SYMBOL, 11), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 11), + + # Node 234 + (3, HUFFMAN_EMIT_SYMBOL, 12), + (6, HUFFMAN_EMIT_SYMBOL, 12), + (10, HUFFMAN_EMIT_SYMBOL, 12), + (15, HUFFMAN_EMIT_SYMBOL, 12), + (24, HUFFMAN_EMIT_SYMBOL, 12), + (31, HUFFMAN_EMIT_SYMBOL, 12), + (41, HUFFMAN_EMIT_SYMBOL, 12), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 12), + (3, HUFFMAN_EMIT_SYMBOL, 14), + (6, HUFFMAN_EMIT_SYMBOL, 14), + (10, HUFFMAN_EMIT_SYMBOL, 14), + (15, HUFFMAN_EMIT_SYMBOL, 14), + (24, HUFFMAN_EMIT_SYMBOL, 14), + (31, HUFFMAN_EMIT_SYMBOL, 14), + (41, HUFFMAN_EMIT_SYMBOL, 14), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 14), + + # Node 235 + (2, HUFFMAN_EMIT_SYMBOL, 15), + (9, HUFFMAN_EMIT_SYMBOL, 15), + (23, HUFFMAN_EMIT_SYMBOL, 15), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 15), + (2, HUFFMAN_EMIT_SYMBOL, 16), + (9, HUFFMAN_EMIT_SYMBOL, 16), + (23, HUFFMAN_EMIT_SYMBOL, 16), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 16), + (2, HUFFMAN_EMIT_SYMBOL, 17), + (9, HUFFMAN_EMIT_SYMBOL, 17), + (23, HUFFMAN_EMIT_SYMBOL, 17), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 17), + (2, HUFFMAN_EMIT_SYMBOL, 18), + (9, HUFFMAN_EMIT_SYMBOL, 18), + (23, HUFFMAN_EMIT_SYMBOL, 18), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 18), + + # Node 236 + (3, HUFFMAN_EMIT_SYMBOL, 15), + (6, HUFFMAN_EMIT_SYMBOL, 15), + (10, HUFFMAN_EMIT_SYMBOL, 15), + (15, HUFFMAN_EMIT_SYMBOL, 15), + (24, HUFFMAN_EMIT_SYMBOL, 15), + (31, HUFFMAN_EMIT_SYMBOL, 15), + (41, HUFFMAN_EMIT_SYMBOL, 15), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 15), + (3, HUFFMAN_EMIT_SYMBOL, 16), + (6, HUFFMAN_EMIT_SYMBOL, 16), + (10, HUFFMAN_EMIT_SYMBOL, 16), + (15, HUFFMAN_EMIT_SYMBOL, 16), + (24, HUFFMAN_EMIT_SYMBOL, 16), + (31, HUFFMAN_EMIT_SYMBOL, 16), + (41, HUFFMAN_EMIT_SYMBOL, 16), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 16), + + # Node 237 + (3, HUFFMAN_EMIT_SYMBOL, 17), + (6, HUFFMAN_EMIT_SYMBOL, 17), + (10, HUFFMAN_EMIT_SYMBOL, 17), + (15, HUFFMAN_EMIT_SYMBOL, 17), + (24, HUFFMAN_EMIT_SYMBOL, 17), + (31, HUFFMAN_EMIT_SYMBOL, 17), + (41, HUFFMAN_EMIT_SYMBOL, 17), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 17), + (3, HUFFMAN_EMIT_SYMBOL, 18), + (6, HUFFMAN_EMIT_SYMBOL, 18), + (10, HUFFMAN_EMIT_SYMBOL, 18), + (15, HUFFMAN_EMIT_SYMBOL, 18), + (24, HUFFMAN_EMIT_SYMBOL, 18), + (31, HUFFMAN_EMIT_SYMBOL, 18), + (41, HUFFMAN_EMIT_SYMBOL, 18), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 18), + + # Node 238 + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 19), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 20), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 21), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 23), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 24), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 25), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 26), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 27), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 28), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 29), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 30), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 31), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 127), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 220), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 249), + (253, 0, 0), + + # Node 239 + (1, HUFFMAN_EMIT_SYMBOL, 19), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 19), + (1, HUFFMAN_EMIT_SYMBOL, 20), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 20), + (1, HUFFMAN_EMIT_SYMBOL, 21), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 21), + (1, HUFFMAN_EMIT_SYMBOL, 23), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 23), + (1, HUFFMAN_EMIT_SYMBOL, 24), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 24), + (1, HUFFMAN_EMIT_SYMBOL, 25), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 25), + (1, HUFFMAN_EMIT_SYMBOL, 26), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 26), + (1, HUFFMAN_EMIT_SYMBOL, 27), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 27), + + # Node 240 + (2, HUFFMAN_EMIT_SYMBOL, 19), + (9, HUFFMAN_EMIT_SYMBOL, 19), + (23, HUFFMAN_EMIT_SYMBOL, 19), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 19), + (2, HUFFMAN_EMIT_SYMBOL, 20), + (9, HUFFMAN_EMIT_SYMBOL, 20), + (23, HUFFMAN_EMIT_SYMBOL, 20), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 20), + (2, HUFFMAN_EMIT_SYMBOL, 21), + (9, HUFFMAN_EMIT_SYMBOL, 21), + (23, HUFFMAN_EMIT_SYMBOL, 21), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 21), + (2, HUFFMAN_EMIT_SYMBOL, 23), + (9, HUFFMAN_EMIT_SYMBOL, 23), + (23, HUFFMAN_EMIT_SYMBOL, 23), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 23), + + # Node 241 + (3, HUFFMAN_EMIT_SYMBOL, 19), + (6, HUFFMAN_EMIT_SYMBOL, 19), + (10, HUFFMAN_EMIT_SYMBOL, 19), + (15, HUFFMAN_EMIT_SYMBOL, 19), + (24, HUFFMAN_EMIT_SYMBOL, 19), + (31, HUFFMAN_EMIT_SYMBOL, 19), + (41, HUFFMAN_EMIT_SYMBOL, 19), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 19), + (3, HUFFMAN_EMIT_SYMBOL, 20), + (6, HUFFMAN_EMIT_SYMBOL, 20), + (10, HUFFMAN_EMIT_SYMBOL, 20), + (15, HUFFMAN_EMIT_SYMBOL, 20), + (24, HUFFMAN_EMIT_SYMBOL, 20), + (31, HUFFMAN_EMIT_SYMBOL, 20), + (41, HUFFMAN_EMIT_SYMBOL, 20), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 20), + + # Node 242 + (3, HUFFMAN_EMIT_SYMBOL, 21), + (6, HUFFMAN_EMIT_SYMBOL, 21), + (10, HUFFMAN_EMIT_SYMBOL, 21), + (15, HUFFMAN_EMIT_SYMBOL, 21), + (24, HUFFMAN_EMIT_SYMBOL, 21), + (31, HUFFMAN_EMIT_SYMBOL, 21), + (41, HUFFMAN_EMIT_SYMBOL, 21), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 21), + (3, HUFFMAN_EMIT_SYMBOL, 23), + (6, HUFFMAN_EMIT_SYMBOL, 23), + (10, HUFFMAN_EMIT_SYMBOL, 23), + (15, HUFFMAN_EMIT_SYMBOL, 23), + (24, HUFFMAN_EMIT_SYMBOL, 23), + (31, HUFFMAN_EMIT_SYMBOL, 23), + (41, HUFFMAN_EMIT_SYMBOL, 23), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 23), + + # Node 243 + (2, HUFFMAN_EMIT_SYMBOL, 24), + (9, HUFFMAN_EMIT_SYMBOL, 24), + (23, HUFFMAN_EMIT_SYMBOL, 24), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 24), + (2, HUFFMAN_EMIT_SYMBOL, 25), + (9, HUFFMAN_EMIT_SYMBOL, 25), + (23, HUFFMAN_EMIT_SYMBOL, 25), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 25), + (2, HUFFMAN_EMIT_SYMBOL, 26), + (9, HUFFMAN_EMIT_SYMBOL, 26), + (23, HUFFMAN_EMIT_SYMBOL, 26), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 26), + (2, HUFFMAN_EMIT_SYMBOL, 27), + (9, HUFFMAN_EMIT_SYMBOL, 27), + (23, HUFFMAN_EMIT_SYMBOL, 27), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 27), + + # Node 244 + (3, HUFFMAN_EMIT_SYMBOL, 24), + (6, HUFFMAN_EMIT_SYMBOL, 24), + (10, HUFFMAN_EMIT_SYMBOL, 24), + (15, HUFFMAN_EMIT_SYMBOL, 24), + (24, HUFFMAN_EMIT_SYMBOL, 24), + (31, HUFFMAN_EMIT_SYMBOL, 24), + (41, HUFFMAN_EMIT_SYMBOL, 24), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 24), + (3, HUFFMAN_EMIT_SYMBOL, 25), + (6, HUFFMAN_EMIT_SYMBOL, 25), + (10, HUFFMAN_EMIT_SYMBOL, 25), + (15, HUFFMAN_EMIT_SYMBOL, 25), + (24, HUFFMAN_EMIT_SYMBOL, 25), + (31, HUFFMAN_EMIT_SYMBOL, 25), + (41, HUFFMAN_EMIT_SYMBOL, 25), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 25), + + # Node 245 + (3, HUFFMAN_EMIT_SYMBOL, 26), + (6, HUFFMAN_EMIT_SYMBOL, 26), + (10, HUFFMAN_EMIT_SYMBOL, 26), + (15, HUFFMAN_EMIT_SYMBOL, 26), + (24, HUFFMAN_EMIT_SYMBOL, 26), + (31, HUFFMAN_EMIT_SYMBOL, 26), + (41, HUFFMAN_EMIT_SYMBOL, 26), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 26), + (3, HUFFMAN_EMIT_SYMBOL, 27), + (6, HUFFMAN_EMIT_SYMBOL, 27), + (10, HUFFMAN_EMIT_SYMBOL, 27), + (15, HUFFMAN_EMIT_SYMBOL, 27), + (24, HUFFMAN_EMIT_SYMBOL, 27), + (31, HUFFMAN_EMIT_SYMBOL, 27), + (41, HUFFMAN_EMIT_SYMBOL, 27), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 27), + + # Node 246 + (1, HUFFMAN_EMIT_SYMBOL, 28), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 28), + (1, HUFFMAN_EMIT_SYMBOL, 29), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 29), + (1, HUFFMAN_EMIT_SYMBOL, 30), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 30), + (1, HUFFMAN_EMIT_SYMBOL, 31), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 31), + (1, HUFFMAN_EMIT_SYMBOL, 127), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 127), + (1, HUFFMAN_EMIT_SYMBOL, 220), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 220), + (1, HUFFMAN_EMIT_SYMBOL, 249), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 249), + (254, 0, 0), + (255, 0, 0), + + # Node 247 + (2, HUFFMAN_EMIT_SYMBOL, 28), + (9, HUFFMAN_EMIT_SYMBOL, 28), + (23, HUFFMAN_EMIT_SYMBOL, 28), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 28), + (2, HUFFMAN_EMIT_SYMBOL, 29), + (9, HUFFMAN_EMIT_SYMBOL, 29), + (23, HUFFMAN_EMIT_SYMBOL, 29), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 29), + (2, HUFFMAN_EMIT_SYMBOL, 30), + (9, HUFFMAN_EMIT_SYMBOL, 30), + (23, HUFFMAN_EMIT_SYMBOL, 30), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 30), + (2, HUFFMAN_EMIT_SYMBOL, 31), + (9, HUFFMAN_EMIT_SYMBOL, 31), + (23, HUFFMAN_EMIT_SYMBOL, 31), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 31), + + # Node 248 + (3, HUFFMAN_EMIT_SYMBOL, 28), + (6, HUFFMAN_EMIT_SYMBOL, 28), + (10, HUFFMAN_EMIT_SYMBOL, 28), + (15, HUFFMAN_EMIT_SYMBOL, 28), + (24, HUFFMAN_EMIT_SYMBOL, 28), + (31, HUFFMAN_EMIT_SYMBOL, 28), + (41, HUFFMAN_EMIT_SYMBOL, 28), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 28), + (3, HUFFMAN_EMIT_SYMBOL, 29), + (6, HUFFMAN_EMIT_SYMBOL, 29), + (10, HUFFMAN_EMIT_SYMBOL, 29), + (15, HUFFMAN_EMIT_SYMBOL, 29), + (24, HUFFMAN_EMIT_SYMBOL, 29), + (31, HUFFMAN_EMIT_SYMBOL, 29), + (41, HUFFMAN_EMIT_SYMBOL, 29), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 29), + + # Node 249 + (3, HUFFMAN_EMIT_SYMBOL, 30), + (6, HUFFMAN_EMIT_SYMBOL, 30), + (10, HUFFMAN_EMIT_SYMBOL, 30), + (15, HUFFMAN_EMIT_SYMBOL, 30), + (24, HUFFMAN_EMIT_SYMBOL, 30), + (31, HUFFMAN_EMIT_SYMBOL, 30), + (41, HUFFMAN_EMIT_SYMBOL, 30), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 30), + (3, HUFFMAN_EMIT_SYMBOL, 31), + (6, HUFFMAN_EMIT_SYMBOL, 31), + (10, HUFFMAN_EMIT_SYMBOL, 31), + (15, HUFFMAN_EMIT_SYMBOL, 31), + (24, HUFFMAN_EMIT_SYMBOL, 31), + (31, HUFFMAN_EMIT_SYMBOL, 31), + (41, HUFFMAN_EMIT_SYMBOL, 31), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 31), + + # Node 250 + (2, HUFFMAN_EMIT_SYMBOL, 127), + (9, HUFFMAN_EMIT_SYMBOL, 127), + (23, HUFFMAN_EMIT_SYMBOL, 127), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 127), + (2, HUFFMAN_EMIT_SYMBOL, 220), + (9, HUFFMAN_EMIT_SYMBOL, 220), + (23, HUFFMAN_EMIT_SYMBOL, 220), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 220), + (2, HUFFMAN_EMIT_SYMBOL, 249), + (9, HUFFMAN_EMIT_SYMBOL, 249), + (23, HUFFMAN_EMIT_SYMBOL, 249), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 249), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 10), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 13), + (0, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 22), + (0, HUFFMAN_FAIL, 0), + + # Node 251 + (3, HUFFMAN_EMIT_SYMBOL, 127), + (6, HUFFMAN_EMIT_SYMBOL, 127), + (10, HUFFMAN_EMIT_SYMBOL, 127), + (15, HUFFMAN_EMIT_SYMBOL, 127), + (24, HUFFMAN_EMIT_SYMBOL, 127), + (31, HUFFMAN_EMIT_SYMBOL, 127), + (41, HUFFMAN_EMIT_SYMBOL, 127), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 127), + (3, HUFFMAN_EMIT_SYMBOL, 220), + (6, HUFFMAN_EMIT_SYMBOL, 220), + (10, HUFFMAN_EMIT_SYMBOL, 220), + (15, HUFFMAN_EMIT_SYMBOL, 220), + (24, HUFFMAN_EMIT_SYMBOL, 220), + (31, HUFFMAN_EMIT_SYMBOL, 220), + (41, HUFFMAN_EMIT_SYMBOL, 220), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 220), + + # Node 252 + (3, HUFFMAN_EMIT_SYMBOL, 249), + (6, HUFFMAN_EMIT_SYMBOL, 249), + (10, HUFFMAN_EMIT_SYMBOL, 249), + (15, HUFFMAN_EMIT_SYMBOL, 249), + (24, HUFFMAN_EMIT_SYMBOL, 249), + (31, HUFFMAN_EMIT_SYMBOL, 249), + (41, HUFFMAN_EMIT_SYMBOL, 249), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 249), + (1, HUFFMAN_EMIT_SYMBOL, 10), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 10), + (1, HUFFMAN_EMIT_SYMBOL, 13), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 13), + (1, HUFFMAN_EMIT_SYMBOL, 22), + (22, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 22), + (0, HUFFMAN_FAIL, 0), + (0, HUFFMAN_FAIL, 0), + + # Node 253 + (2, HUFFMAN_EMIT_SYMBOL, 10), + (9, HUFFMAN_EMIT_SYMBOL, 10), + (23, HUFFMAN_EMIT_SYMBOL, 10), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 10), + (2, HUFFMAN_EMIT_SYMBOL, 13), + (9, HUFFMAN_EMIT_SYMBOL, 13), + (23, HUFFMAN_EMIT_SYMBOL, 13), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 13), + (2, HUFFMAN_EMIT_SYMBOL, 22), + (9, HUFFMAN_EMIT_SYMBOL, 22), + (23, HUFFMAN_EMIT_SYMBOL, 22), + (40, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 22), + (0, HUFFMAN_FAIL, 0), + (0, HUFFMAN_FAIL, 0), + (0, HUFFMAN_FAIL, 0), + (0, HUFFMAN_FAIL, 0), + + # Node 254 + (3, HUFFMAN_EMIT_SYMBOL, 10), + (6, HUFFMAN_EMIT_SYMBOL, 10), + (10, HUFFMAN_EMIT_SYMBOL, 10), + (15, HUFFMAN_EMIT_SYMBOL, 10), + (24, HUFFMAN_EMIT_SYMBOL, 10), + (31, HUFFMAN_EMIT_SYMBOL, 10), + (41, HUFFMAN_EMIT_SYMBOL, 10), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 10), + (3, HUFFMAN_EMIT_SYMBOL, 13), + (6, HUFFMAN_EMIT_SYMBOL, 13), + (10, HUFFMAN_EMIT_SYMBOL, 13), + (15, HUFFMAN_EMIT_SYMBOL, 13), + (24, HUFFMAN_EMIT_SYMBOL, 13), + (31, HUFFMAN_EMIT_SYMBOL, 13), + (41, HUFFMAN_EMIT_SYMBOL, 13), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 13), + + # Node 255 + (3, HUFFMAN_EMIT_SYMBOL, 22), + (6, HUFFMAN_EMIT_SYMBOL, 22), + (10, HUFFMAN_EMIT_SYMBOL, 22), + (15, HUFFMAN_EMIT_SYMBOL, 22), + (24, HUFFMAN_EMIT_SYMBOL, 22), + (31, HUFFMAN_EMIT_SYMBOL, 22), + (41, HUFFMAN_EMIT_SYMBOL, 22), + (56, HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL, 22), + (0, HUFFMAN_FAIL, 0), + (0, HUFFMAN_FAIL, 0), + (0, HUFFMAN_FAIL, 0), + (0, HUFFMAN_FAIL, 0), + (0, HUFFMAN_FAIL, 0), + (0, HUFFMAN_FAIL, 0), + (0, HUFFMAN_FAIL, 0), + (0, HUFFMAN_FAIL, 0), +] diff --git a/vinetrimmer/vendor/hpack/struct.py b/vinetrimmer/vendor/hpack/struct.py new file mode 100644 index 0000000..e860cd7 --- /dev/null +++ b/vinetrimmer/vendor/hpack/struct.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +""" +hpack/struct +~~~~~~~~~~~~ + +Contains structures for representing header fields with associated metadata. +""" + + +class HeaderTuple(tuple): + """ + A data structure that stores a single header field. + + HTTP headers can be thought of as tuples of ``(field name, field value)``. + A single header block is a sequence of such tuples. + + In HTTP/2, however, certain bits of additional information are required for + compressing these headers: in particular, whether the header field can be + safely added to the HPACK compression context. + + This class stores a header that can be added to the compression context. In + all other ways it behaves exactly like a tuple. + """ + __slots__ = () + + indexable = True + + def __new__(_cls, *args): + return tuple.__new__(_cls, args) + + +class NeverIndexedHeaderTuple(HeaderTuple): + """ + A data structure that stores a single header field that cannot be added to + a HTTP/2 header compression context. + """ + __slots__ = () + + indexable = False diff --git a/vinetrimmer/vendor/hpack/table.py b/vinetrimmer/vendor/hpack/table.py new file mode 100644 index 0000000..9a89c72 --- /dev/null +++ b/vinetrimmer/vendor/hpack/table.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +# flake8: noqa +from collections import deque +import logging + +from .exceptions import InvalidTableIndex + +log = logging.getLogger(__name__) + + +def table_entry_size(name, value): + """ + Calculates the size of a single entry + + This size is mostly irrelevant to us and defined + specifically to accommodate memory management for + lower level implementations. The 32 extra bytes are + considered the "maximum" overhead that would be + required to represent each entry in the table. + + See RFC7541 Section 4.1 + """ + return 32 + len(name) + len(value) + + +class HeaderTable(object): + """ + Implements the combined static and dynamic header table + + The name and value arguments for all the functions + should ONLY be byte strings (b'') however this is not + strictly enforced in the interface. + + See RFC7541 Section 2.3 + """ + #: Default maximum size of the dynamic table. See + #: RFC7540 Section 6.5.2. + DEFAULT_SIZE = 4096 + + #: Constant list of static headers. See RFC7541 Section + #: 2.3.1 and Appendix A + STATIC_TABLE = ( + (b':authority' , b'' ), # noqa + (b':method' , b'GET' ), # noqa + (b':method' , b'POST' ), # noqa + (b':path' , b'/' ), # noqa + (b':path' , b'/index.html' ), # noqa + (b':scheme' , b'http' ), # noqa + (b':scheme' , b'https' ), # noqa + (b':status' , b'200' ), # noqa + (b':status' , b'204' ), # noqa + (b':status' , b'206' ), # noqa + (b':status' , b'304' ), # noqa + (b':status' , b'400' ), # noqa + (b':status' , b'404' ), # noqa + (b':status' , b'500' ), # noqa + (b'accept-charset' , b'' ), # noqa + (b'accept-encoding' , b'gzip, deflate'), # noqa + (b'accept-language' , b'' ), # noqa + (b'accept-ranges' , b'' ), # noqa + (b'accept' , b'' ), # noqa + (b'access-control-allow-origin' , b'' ), # noqa + (b'age' , b'' ), # noqa + (b'allow' , b'' ), # noqa + (b'authorization' , b'' ), # noqa + (b'cache-control' , b'' ), # noqa + (b'content-disposition' , b'' ), # noqa + (b'content-encoding' , b'' ), # noqa + (b'content-language' , b'' ), # noqa + (b'content-length' , b'' ), # noqa + (b'content-location' , b'' ), # noqa + (b'content-range' , b'' ), # noqa + (b'content-type' , b'' ), # noqa + (b'cookie' , b'' ), # noqa + (b'date' , b'' ), # noqa + (b'etag' , b'' ), # noqa + (b'expect' , b'' ), # noqa + (b'expires' , b'' ), # noqa + (b'from' , b'' ), # noqa + (b'host' , b'' ), # noqa + (b'if-match' , b'' ), # noqa + (b'if-modified-since' , b'' ), # noqa + (b'if-none-match' , b'' ), # noqa + (b'if-range' , b'' ), # noqa + (b'if-unmodified-since' , b'' ), # noqa + (b'last-modified' , b'' ), # noqa + (b'link' , b'' ), # noqa + (b'location' , b'' ), # noqa + (b'max-forwards' , b'' ), # noqa + (b'proxy-authenticate' , b'' ), # noqa + (b'proxy-authorization' , b'' ), # noqa + (b'range' , b'' ), # noqa + (b'referer' , b'' ), # noqa + (b'refresh' , b'' ), # noqa + (b'retry-after' , b'' ), # noqa + (b'server' , b'' ), # noqa + (b'set-cookie' , b'' ), # noqa + (b'strict-transport-security' , b'' ), # noqa + (b'transfer-encoding' , b'' ), # noqa + (b'user-agent' , b'' ), # noqa + (b'vary' , b'' ), # noqa + (b'via' , b'' ), # noqa + (b'www-authenticate' , b'' ), # noqa + ) # noqa + + STATIC_TABLE_LENGTH = len(STATIC_TABLE) + + def __init__(self): + self._maxsize = HeaderTable.DEFAULT_SIZE + self._current_size = 0 + self.resized = False + self.dynamic_entries = deque() + + def get_by_index(self, index): + """ + Returns the entry specified by index + + Note that the table is 1-based ie an index of 0 is + invalid. This is due to the fact that a zero value + index signals that a completely unindexed header + follows. + + The entry will either be from the static table or + the dynamic table depending on the value of index. + """ + original_index = index + index -= 1 + if 0 <= index: + if index < HeaderTable.STATIC_TABLE_LENGTH: + return HeaderTable.STATIC_TABLE[index] + + index -= HeaderTable.STATIC_TABLE_LENGTH + if index < len(self.dynamic_entries): + return self.dynamic_entries[index] + + raise InvalidTableIndex("Invalid table index %d" % original_index) + + def __repr__(self): + return "HeaderTable(%d, %s, %r)" % ( + self._maxsize, + self.resized, + self.dynamic_entries + ) + + def add(self, name, value): + """ + Adds a new entry to the table + + We reduce the table size if the entry will make the + table size greater than maxsize. + """ + # We just clear the table if the entry is too big + size = table_entry_size(name, value) + if size > self._maxsize: + self.dynamic_entries.clear() + self._current_size = 0 + else: + # Add new entry + self.dynamic_entries.appendleft((name, value)) + self._current_size += size + self._shrink() + + def search(self, name, value): + """ + Searches the table for the entry specified by name + and value + + Returns one of the following: + - ``None``, no match at all + - ``(index, name, None)`` for partial matches on name only. + - ``(index, name, value)`` for perfect matches. + """ + offset = HeaderTable.STATIC_TABLE_LENGTH + 1 + partial = None + for (i, (n, v)) in enumerate(HeaderTable.STATIC_TABLE): + if n == name: + if v == value: + return i + 1, n, v + elif partial is None: + partial = (i + 1, n, None) + for (i, (n, v)) in enumerate(self.dynamic_entries): + if n == name: + if v == value: + return i + offset, n, v + elif partial is None: + partial = (i + offset, n, None) + return partial + + @property + def maxsize(self): + return self._maxsize + + @maxsize.setter + def maxsize(self, newmax): + newmax = int(newmax) + log.debug("Resizing header table to %d from %d", newmax, self._maxsize) + oldmax = self._maxsize + self._maxsize = newmax + self.resized = (newmax != oldmax) + if newmax <= 0: + self.dynamic_entries.clear() + self._current_size = 0 + elif oldmax > newmax: + self._shrink() + + def _shrink(self): + """ + Shrinks the dynamic table to be at or below maxsize + """ + cursize = self._current_size + while cursize > self._maxsize: + name, value = self.dynamic_entries.pop() + cursize -= table_entry_size(name, value) + log.debug("Evicting %s: %s from the header table", name, value) + self._current_size = cursize diff --git a/vinetrimmer/vendor/hyper/__init__.py b/vinetrimmer/vendor/hyper/__init__.py new file mode 100644 index 0000000..afa8803 --- /dev/null +++ b/vinetrimmer/vendor/hyper/__init__.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +""" +hyper +~~~~~~ + +A module for providing an abstraction layer over the differences between +HTTP/1.1 and HTTP/2. +""" +import logging + +from .common.connection import HTTPConnection +from .http20.connection import HTTP20Connection +from .http20.response import HTTP20Response, HTTP20Push +from .http11.connection import HTTP11Connection +from .http11.response import HTTP11Response + +# Throw import errors on Python <2.7 and 3.0-3.2. +import sys as _sys +if _sys.version_info < (2, 7) or (3, 0) <= _sys.version_info < (3, 3): + raise ImportError( + "hyper only supports Python 2.7 and Python 3.3 or higher." + ) + +__all__ = [ + HTTPConnection, + HTTP20Response, + HTTP20Push, + HTTP20Connection, + HTTP11Connection, + HTTP11Response, +] + +# Set default logging handler. +logging.getLogger(__name__).addHandler(logging.NullHandler()) + +__version__ = '0.7.0' diff --git a/vinetrimmer/vendor/hyper/certs.pem b/vinetrimmer/vendor/hyper/certs.pem new file mode 100644 index 0000000..72a750f --- /dev/null +++ b/vinetrimmer/vendor/hyper/certs.pem @@ -0,0 +1,5116 @@ + +# Issuer: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA +# Subject: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA +# Label: "GlobalSign Root CA" +# Serial: 4835703278459707669005204 +# MD5 Fingerprint: 3e:45:52:15:09:51:92:e1:b7:5d:37:9f:b1:87:29:8a +# SHA1 Fingerprint: b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c +# SHA256 Fingerprint: eb:d4:10:40:e4:bb:3e:c7:42:c9:e3:81:d3:1e:f2:a4:1a:48:b6:68:5c:96:e7:ce:f3:c1:df:6c:d4:33:1c:99 +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2 +# Label: "GlobalSign Root CA - R2" +# Serial: 4835703278459682885658125 +# MD5 Fingerprint: 94:14:77:7e:3e:5e:fd:8f:30:bd:41:b0:cf:e7:d0:30 +# SHA1 Fingerprint: 75:e0:ab:b6:13:85:12:27:1c:04:f8:5f:dd:de:38:e4:b7:24:2e:fe +# SHA256 Fingerprint: ca:42:dd:41:74:5f:d0:b8:1e:b9:02:36:2c:f9:d8:bf:71:9d:a1:bd:1b:1e:fc:94:6f:5b:4c:99:f4:2c:1b:9e +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 +MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL +v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 +eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq +tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd +C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa +zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB +mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH +V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n +bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG +3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs +J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO +291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS +ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd +AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only +# Label: "Verisign Class 3 Public Primary Certification Authority - G3" +# Serial: 206684696279472310254277870180966723415 +# MD5 Fingerprint: cd:68:b6:a7:c7:c4:ce:75:e0:1d:4f:57:44:61:92:09 +# SHA1 Fingerprint: 13:2d:0d:45:53:4b:69:97:cd:b2:d5:c3:39:e2:55:76:60:9b:5c:c6 +# SHA256 Fingerprint: eb:04:cf:5e:b1:f3:9a:fa:76:2f:2b:b1:20:f2:96:cb:a5:20:c1:b9:7d:b1:58:95:65:b8:1c:b9:a1:7b:72:44 +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b +N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t +KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu +kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm +CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ +Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu +imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te +2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe +DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC +/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p +F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt +TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== +-----END CERTIFICATE----- + +# Issuer: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited +# Subject: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited +# Label: "Entrust.net Premium 2048 Secure Server CA" +# Serial: 946069240 +# MD5 Fingerprint: ee:29:31:bc:32:7e:9a:e6:e8:b5:f7:51:b4:34:71:90 +# SHA1 Fingerprint: 50:30:06:09:1d:97:d4:f5:ae:39:f7:cb:e7:92:7d:7d:65:2d:34:31 +# SHA256 Fingerprint: 6d:c4:71:72:e0:1c:bc:b0:bf:62:58:0d:89:5f:e2:b8:ac:9a:d4:f8:73:80:1e:0c:10:b9:c8:37:d2:1e:b1:77 +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML +RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp +bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5 +IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3 +MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3 +LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp +YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG +A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq +K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe +sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX +MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT +XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/ +HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH +4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub +j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo +U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf +zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b +u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+ +bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er +fF6adulZkMV8gzURZVE= +-----END CERTIFICATE----- + +# Issuer: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust +# Subject: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust +# Label: "Baltimore CyberTrust Root" +# Serial: 33554617 +# MD5 Fingerprint: ac:b6:94:a5:9c:17:e0:d7:91:52:9b:b1:97:06:a6:e4 +# SHA1 Fingerprint: d4:de:20:d0:5e:66:fc:53:fe:1a:50:88:2c:78:db:28:52:ca:e4:74 +# SHA256 Fingerprint: 16:af:57:a9:f6:76:b0:ab:12:60:95:aa:5e:ba:de:f2:2a:b3:11:19:d6:44:ac:95:cd:4b:93:db:f3:f2:6a:eb +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ +RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD +VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX +DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y +ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy +VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr +mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr +IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK +mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu +XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy +dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye +jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 +BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 +DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 +9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx +jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 +Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz +ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS +R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust Class 1 CA Root O=AddTrust AB OU=AddTrust TTP Network +# Subject: CN=AddTrust Class 1 CA Root O=AddTrust AB OU=AddTrust TTP Network +# Label: "AddTrust Low-Value Services Root" +# Serial: 1 +# MD5 Fingerprint: 1e:42:95:02:33:92:6b:b9:5f:c0:7f:da:d6:b2:4b:fc +# SHA1 Fingerprint: cc:ab:0e:a0:4c:23:01:d6:69:7b:dd:37:9f:cd:12:eb:24:e3:94:9d +# SHA256 Fingerprint: 8c:72:09:27:9a:c0:4e:27:5e:16:d0:7f:d3:b7:75:e8:01:54:b5:96:80:46:e3:1f:52:dd:25:76:63:24:e9:a7 +-----BEGIN CERTIFICATE----- +MIIEGDCCAwCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwHhcNMDAwNTMw +MTAzODMxWhcNMjAwNTMwMTAzODMxWjBlMQswCQYDVQQGEwJTRTEUMBIGA1UEChML +QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYD +VQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCWltQhSWDia+hBBwzexODcEyPNwTXH+9ZOEQpnXvUGW2ul +CDtbKRY654eyNAbFvAWlA3yCyykQruGIgb3WntP+LVbBFc7jJp0VLhD7Bo8wBN6n +tGO0/7Gcrjyvd7ZWxbWroulpOj0OM3kyP3CCkplhbY0wCI9xP6ZIVxn4JdxLZlyl +dI+Yrsj5wAYi56xz36Uu+1LcsRVlIPo1Zmne3yzxbrww2ywkEtvrNTVokMsAsJch +PXQhI2U0K7t4WaPW4XY5mqRJjox0r26kmqPZm9I4XJuiGMx1I4S+6+JNM3GOGvDC ++Mcdoq0Dlyz4zyXG9rgkMbFjXZJ/Y/AlyVMuH79NAgMBAAGjgdIwgc8wHQYDVR0O +BBYEFJWxtPCUtr3H2tERCSG+wa9J/RB7MAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MIGPBgNVHSMEgYcwgYSAFJWxtPCUtr3H2tERCSG+wa9J/RB7oWmkZzBl +MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFk +ZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENB +IFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBACxtZBsfzQ3duQH6lmM0MkhHma6X +7f1yFqZzR1r0693p9db7RcwpiURdv0Y5PejuvE1Uhh4dbOMXJ0PhiVYrqW9yTkkz +43J8KiOavD7/KCrto/8cI7pDVwlnTUtiBi34/2ydYB7YHEt9tTEv2dB8Xfjea4MY +eDdXL+gzB2ffHsdrKpV2ro9Xo/D0UrSpUwjP4E/TelOL/bscVjby/rK25Xa71SJl +pz/+0WatC7xrmYbvP33zGDLKe8bjq2RGlfgmadlVg3sslgf/WSxEo8bl6ancoWOA +WiFeIc9TVPC6b4nbqKqVz4vjccweGyBECMB6tkD9xOQ14R0WHNC8K47Wcdk= +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network +# Subject: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network +# Label: "AddTrust External Root" +# Serial: 1 +# MD5 Fingerprint: 1d:35:54:04:85:78:b0:3f:42:42:4d:bf:20:73:0a:3f +# SHA1 Fingerprint: 02:fa:f3:e2:91:43:54:68:60:78:57:69:4d:f5:e4:5b:68:85:18:68 +# SHA256 Fingerprint: 68:7f:a4:51:38:22:78:ff:f0:c8:b1:1f:8d:43:d5:76:67:1c:6e:b2:bc:ea:b4:13:fb:83:d9:65:d0:6d:2f:f2 +-----BEGIN CERTIFICATE----- +MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs +IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 +MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h +bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v +dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt +H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 +uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX +mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX +a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN +E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 +WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD +VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 +Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU +cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx +IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN +AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH +YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 +6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC +Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX +c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a +mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust Public CA Root O=AddTrust AB OU=AddTrust TTP Network +# Subject: CN=AddTrust Public CA Root O=AddTrust AB OU=AddTrust TTP Network +# Label: "AddTrust Public Services Root" +# Serial: 1 +# MD5 Fingerprint: c1:62:3e:23:c5:82:73:9c:03:59:4b:2b:e9:77:49:7f +# SHA1 Fingerprint: 2a:b6:28:48:5e:78:fb:f3:ad:9e:79:10:dd:6b:df:99:72:2c:96:e5 +# SHA256 Fingerprint: 07:91:ca:07:49:b2:07:82:aa:d3:c7:d7:bd:0c:df:c9:48:58:35:84:3e:b2:d7:99:60:09:ce:43:ab:6c:69:27 +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSAwHgYDVQQDExdBZGRUcnVzdCBQdWJsaWMgQ0EgUm9vdDAeFw0wMDA1MzAx +MDQxNTBaFw0yMDA1MzAxMDQxNTBaMGQxCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtB +ZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIDAeBgNV +BAMTF0FkZFRydXN0IFB1YmxpYyBDQSBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA6Rowj4OIFMEg2Dybjxt+A3S72mnTRqX4jsIMEZBRpS9mVEBV +6tsfSlbunyNu9DnLoblv8n75XYcmYZ4c+OLspoH4IcUkzBEMP9smcnrHAZcHF/nX +GCwwfQ56HmIexkvA/X1id9NEHif2P0tEs7c42TkfYNVRknMDtABp4/MUTu7R3AnP +dzRGULD4EfL+OHn3Bzn+UZKXC1sIXzSGAa2Il+tmzV7R/9x98oTaunet3IAIx6eH +1lWfl2royBFkuucZKT8Rs3iQhCBSWxHveNCD9tVIkNAwHM+A+WD+eeSI8t0A65RF +62WUaUC6wNW0uLp9BBGo6zEFlpROWCGOn9Bg/QIDAQABo4HRMIHOMB0GA1UdDgQW +BBSBPjfYkrAfd59ctKtzquf2NGAv+jALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zCBjgYDVR0jBIGGMIGDgBSBPjfYkrAfd59ctKtzquf2NGAv+qFopGYwZDEL +MAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRU +cnVzdCBUVFAgTmV0d29yazEgMB4GA1UEAxMXQWRkVHJ1c3QgUHVibGljIENBIFJv +b3SCAQEwDQYJKoZIhvcNAQEFBQADggEBAAP3FUr4JNojVhaTdt02KLmuG7jD8WS6 +IBh4lSknVwW8fCr0uVFV2ocC3g8WFzH4qnkuCRO7r7IgGRLlk/lL+YPoRNWyQSW/ +iHVv/xD8SlTQX/D67zZzfRs2RcYhbbQVuE7PnFylPVoAjgbjPGsye/Kf8Lb93/Ao +GEjwxrzQvzSAlsJKsW2Ox5BF3i9nrEUEo3rcVZLJR2bYGozH7ZxOmuASu7VqTITh +4SINhwBk/ox9Yjllpu9CtoAlEmEBqCQTcAARJl/6NVDFSMwGR+gn2HCNX2TmoUQm +XiLsks3/QppEIW1cxeMiHV9HEufOX1362KqxMy3ZdvJOOjMMK7MtkAY= +-----END CERTIFICATE----- + +# Issuer: CN=AddTrust Qualified CA Root O=AddTrust AB OU=AddTrust TTP Network +# Subject: CN=AddTrust Qualified CA Root O=AddTrust AB OU=AddTrust TTP Network +# Label: "AddTrust Qualified Certificates Root" +# Serial: 1 +# MD5 Fingerprint: 27:ec:39:47:cd:da:5a:af:e2:9a:01:65:21:a9:4c:bb +# SHA1 Fingerprint: 4d:23:78:ec:91:95:39:b5:00:7f:75:8f:03:3b:21:1e:c5:4d:8b:cf +# SHA256 Fingerprint: 80:95:21:08:05:db:4b:bc:35:5e:44:28:d8:fd:6e:c2:cd:e3:ab:5f:b9:7a:99:42:98:8e:b8:f4:dc:d0:60:16 +-----BEGIN CERTIFICATE----- +MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 +b3JrMSMwIQYDVQQDExpBZGRUcnVzdCBRdWFsaWZpZWQgQ0EgUm9vdDAeFw0wMDA1 +MzAxMDQ0NTBaFw0yMDA1MzAxMDQ0NTBaMGcxCzAJBgNVBAYTAlNFMRQwEgYDVQQK +EwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIzAh +BgNVBAMTGkFkZFRydXN0IFF1YWxpZmllZCBDQSBSb290MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA5B6a/twJWoekn0e+EV+vhDTbYjx5eLfpMLXsDBwq +xBb/4Oxx64r1EW7tTw2R0hIYLUkVAcKkIhPHEWT/IhKauY5cLwjPcWqzZwFZ8V1G +87B4pfYOQnrjfxvM0PC3KP0q6p6zsLkEqv32x7SxuCqg+1jxGaBvcCV+PmlKfw8i +2O+tCBGaKZnhqkRFmhJePp1tUvznoD1oL/BLcHwTOK28FSXx1s6rosAx1i+f4P8U +WfyEk9mHfExUE+uf0S0R+Bg6Ot4l2ffTQO2kBhLEO+GRwVY18BTcZTYJbqukB8c1 +0cIDMzZbdSZtQvESa0NvS3GU+jQd7RNuyoB/mC9suWXY6QIDAQABo4HUMIHRMB0G +A1UdDgQWBBQ5lYtii1zJ1IC6WA+XPxUIQ8yYpzALBgNVHQ8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zCBkQYDVR0jBIGJMIGGgBQ5lYtii1zJ1IC6WA+XPxUIQ8yYp6Fr +pGkwZzELMAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQL +ExRBZGRUcnVzdCBUVFAgTmV0d29yazEjMCEGA1UEAxMaQWRkVHJ1c3QgUXVhbGlm +aWVkIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBABmrder4i2VhlRO6aQTv +hsoToMeqT2QbPxj2qC0sVY8FtzDqQmodwCVRLae/DLPt7wh/bDxGGuoYQ992zPlm +hpwsaPXpF/gxsxjE1kh9I0xowX67ARRvxdlu3rsEQmr49lx95dr6h+sNNVJn0J6X +dgWTP5XHAeZpVTh/EGGZyeNfpso+gmNIquIISD6q8rKFYqa0p9m9N5xotS1WfbC3 +P6CxB9bpT9zeRXEwMn8bLgn5v1Kh7sKAPgZcLlVAwRv1cEWw3F369nJad9Jjzc9Y +iQBCYz95OdBEsIJuQRno3eDBiFrRHnGTHyQwdOUeqN48Jzd/g66ed8/wMLH/S5no +xqE= +-----END CERTIFICATE----- + +# Issuer: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. +# Subject: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. +# Label: "Entrust Root Certification Authority" +# Serial: 1164660820 +# MD5 Fingerprint: d6:a5:c3:ed:5d:dd:3e:00:c1:3d:87:92:1f:1d:3f:e4 +# SHA1 Fingerprint: b3:1e:b1:b7:40:e3:6c:84:02:da:dc:37:d4:4d:f5:d4:67:49:52:f9 +# SHA256 Fingerprint: 73:c1:76:43:4f:1b:c6:d5:ad:f4:5b:0e:76:e7:27:28:7c:8d:e5:76:16:c1:e6:e6:14:1a:2b:2c:bc:7d:8e:4c +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +# Issuer: O=RSA Security Inc OU=RSA Security 2048 V3 +# Subject: O=RSA Security Inc OU=RSA Security 2048 V3 +# Label: "RSA Security 2048 v3" +# Serial: 13297492616345471454730593562152402946 +# MD5 Fingerprint: 77:0d:19:b1:21:fd:00:42:9c:3e:0c:a5:dd:0b:02:8e +# SHA1 Fingerprint: 25:01:90:19:cf:fb:d9:99:1c:b7:68:25:74:8d:94:5f:30:93:95:42 +# SHA256 Fingerprint: af:8b:67:62:a1:e5:28:22:81:61:a9:5d:5c:55:9e:e2:66:27:8f:75:d7:9e:83:01:89:a5:03:50:6a:bd:6b:4c +-----BEGIN CERTIFICATE----- +MIIDYTCCAkmgAwIBAgIQCgEBAQAAAnwAAAAKAAAAAjANBgkqhkiG9w0BAQUFADA6 +MRkwFwYDVQQKExBSU0EgU2VjdXJpdHkgSW5jMR0wGwYDVQQLExRSU0EgU2VjdXJp +dHkgMjA0OCBWMzAeFw0wMTAyMjIyMDM5MjNaFw0yNjAyMjIyMDM5MjNaMDoxGTAX +BgNVBAoTEFJTQSBTZWN1cml0eSBJbmMxHTAbBgNVBAsTFFJTQSBTZWN1cml0eSAy +MDQ4IFYzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt49VcdKA3Xtp +eafwGFAyPGJn9gqVB93mG/Oe2dJBVGutn3y+Gc37RqtBaB4Y6lXIL5F4iSj7Jylg +/9+PjDvJSZu1pJTOAeo+tWN7fyb9Gd3AIb2E0S1PRsNO3Ng3OTsor8udGuorryGl +wSMiuLgbWhOHV4PR8CDn6E8jQrAApX2J6elhc5SYcSa8LWrg903w8bYqODGBDSnh +AMFRD0xS+ARaqn1y07iHKrtjEAMqs6FPDVpeRrc9DvV07Jmf+T0kgYim3WBU6JU2 +PcYJk5qjEoAAVZkZR73QpXzDuvsf9/UP+Ky5tfQ3mBMY3oVbtwyCO4dvlTlYMNpu +AWgXIszACwIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAfBgNVHSMEGDAWgBQHw1EwpKrpRa41JPr/JCwz0LGdjDAdBgNVHQ4EFgQUB8NR +MKSq6UWuNST6/yQsM9CxnYwwDQYJKoZIhvcNAQEFBQADggEBAF8+hnZuuDU8TjYc +HnmYv/3VEhF5Ug7uMYm83X/50cYVIeiKAVQNOvtUudZj1LGqlk2iQk3UUx+LEN5/ +Zb5gEydxiKRz44Rj0aRV4VCT5hsOedBnvEbIvz8XDZXmxpBp3ue0L96VfdASPz0+ +f00/FGj1EVDVwfSQpQgdMWD/YIwjVAqv/qFuxdF6Kmh4zx6CCiC0H63lhbJqaHVO +rSU3lIW+vaHU6rcMSzyd6BIA8F+sDeGscGNz9395nzIlQnQFgCi/vcEkllgVsRch +6YlL2weIZ/QVrXA+L02FO8K32/6YaCOJ4XQP3vTFhGMpG8zLB8kApKnXwiJPZ9d3 +7CAFYd4= +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Global CA O=GeoTrust Inc. +# Subject: CN=GeoTrust Global CA O=GeoTrust Inc. +# Label: "GeoTrust Global CA" +# Serial: 144470 +# MD5 Fingerprint: f7:75:ab:29:fb:51:4e:b7:77:5e:ff:05:3c:99:8e:f5 +# SHA1 Fingerprint: de:28:f4:a4:ff:e5:b9:2f:a3:c5:03:d1:a3:49:a7:f9:96:2a:82:12 +# SHA256 Fingerprint: ff:85:6a:2d:25:1d:cd:88:d3:66:56:f4:50:12:67:98:cf:ab:aa:de:40:79:9c:72:2d:e4:d2:b5:db:36:a7:3a +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i +YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg +R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 +9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq +fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv +iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU +1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ +bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW +MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA +ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l +uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn +Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS +tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF +PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un +hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV +5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Global CA 2 O=GeoTrust Inc. +# Subject: CN=GeoTrust Global CA 2 O=GeoTrust Inc. +# Label: "GeoTrust Global CA 2" +# Serial: 1 +# MD5 Fingerprint: 0e:40:a7:6c:de:03:5d:8f:d1:0f:e4:d1:8d:f9:6c:a9 +# SHA1 Fingerprint: a9:e9:78:08:14:37:58:88:f2:05:19:b0:6d:2b:0d:2b:60:16:90:7d +# SHA256 Fingerprint: ca:2d:82:a0:86:77:07:2f:8a:b6:76:4f:f0:35:67:6c:fe:3e:5e:32:5e:01:21:72:df:3f:92:09:6d:b7:9b:85 +-----BEGIN CERTIFICATE----- +MIIDZjCCAk6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFs +IENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMTkwMzA0MDUwMDAwWjBEMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3Qg +R2xvYmFsIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDvPE1A +PRDfO1MA4Wf+lGAVPoWI8YkNkMgoI5kF6CsgncbzYEbYwbLVjDHZ3CB5JIG/NTL8 +Y2nbsSpr7iFY8gjpeMtvy/wWUsiRxP89c96xPqfCfWbB9X5SJBri1WeR0IIQ13hL +TytCOb1kLUCgsBDTOEhGiKEMuzozKmKY+wCdE1l/bztyqu6mD4b5BWHqZ38MN5aL +5mkWRxHCJ1kDs6ZgwiFAVvqgx306E+PsV8ez1q6diYD3Aecs9pYrEw15LNnA5IZ7 +S4wMcoKK+xfNAGw6EzywhIdLFnopsk/bHdQL82Y3vdj2V7teJHq4PIu5+pIaGoSe +2HSPqht/XvT+RSIhAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FHE4NvICMVNHK266ZUapEBVYIAUJMB8GA1UdIwQYMBaAFHE4NvICMVNHK266ZUap +EBVYIAUJMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAQEAA/e1K6td +EPx7srJerJsOflN4WT5CBP51o62sgU7XAotexC3IUnbHLB/8gTKY0UvGkpMzNTEv +/NgdRN3ggX+d6YvhZJFiCzkIjKx0nVnZellSlxG5FntvRdOW2TF9AjYPnDtuzywN +A0ZF66D0f0hExghAzN4bcLUprbqLOzRldRtxIR0sFAqwlpW41uryZfspuk/qkZN0 +abby/+Ea0AzRdoXLiiW9l14sbxWZJue2Kf8i7MkCx1YAzUm5s2x7UwQa4qjJqhIF +I8LO57sEAszAR6LkxCkvW0VXiVHuPOtSCP8HNR6fNWpHSlaY0VqFH4z1Ir+rzoPz +4iIprn2DQKi6bA== +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Universal CA O=GeoTrust Inc. +# Subject: CN=GeoTrust Universal CA O=GeoTrust Inc. +# Label: "GeoTrust Universal CA" +# Serial: 1 +# MD5 Fingerprint: 92:65:58:8b:a2:1a:31:72:73:68:5c:b4:a5:7a:07:48 +# SHA1 Fingerprint: e6:21:f3:35:43:79:05:9a:4b:68:30:9d:8a:2f:74:22:15:87:ec:79 +# SHA256 Fingerprint: a0:45:9b:9f:63:b2:25:59:f5:fa:5d:4c:6d:b3:f9:f7:2f:f1:93:42:03:35:78:f0:73:bf:1d:1b:46:cb:b9:12 +-----BEGIN CERTIFICATE----- +MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy +c2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE +BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0 +IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV +VaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8 +cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT +QjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh +F7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v +c7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w +mZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd +VHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX +teGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ +f9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe +Bi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+ +nhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY +MBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG +9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc +aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX +IwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn +ANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z +uzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN +Pnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja +QI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW +koRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9 +ER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt +DF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm +bJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw= +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Universal CA 2 O=GeoTrust Inc. +# Subject: CN=GeoTrust Universal CA 2 O=GeoTrust Inc. +# Label: "GeoTrust Universal CA 2" +# Serial: 1 +# MD5 Fingerprint: 34:fc:b8:d0:36:db:9e:14:b3:c2:f2:db:8f:e4:94:c7 +# SHA1 Fingerprint: 37:9a:19:7b:41:85:45:35:0c:a6:03:69:f3:3c:2e:af:47:4f:20:79 +# SHA256 Fingerprint: a0:23:4f:3b:c8:52:7c:a5:62:8e:ec:81:ad:5d:69:89:5d:a5:68:0d:c9:1d:1c:b8:47:7f:33:f8:78:b9:5b:0b +-----BEGIN CERTIFICATE----- +MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy +c2FsIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYD +VQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1 +c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0DE81 +WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUG +FF+3Qs17j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdq +XbboW0W63MOhBW9Wjo8QJqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL +se4YuU6W3Nx2/zu+z18DwPw76L5GG//aQMJS9/7jOvdqdzXQ2o3rXhhqMcceujwb +KNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2WP0+GfPtDCapkzj4T8Fd +IgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP20gaXT73 +y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRt +hAAnZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgoc +QIgfksILAAX/8sgCSqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4 +Lt1ZrtmhN79UNdxzMk+MBB4zsslG8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAfBgNV +HSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8EBAMCAYYwDQYJ +KoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z +dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQ +L1EuxBRa3ugZ4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgr +Fg5fNuH8KrUwJM/gYwx7WBr+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSo +ag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpqA1Ihn0CoZ1Dy81of398j9tx4TuaY +T1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpgY+RdM4kX2TGq2tbz +GDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiPpm8m +1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJV +OCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH +6aLcr34YEoP9VhdBLtUpgn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwX +QMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS +-----END CERTIFICATE----- + +# Issuer: CN=Visa eCommerce Root O=VISA OU=Visa International Service Association +# Subject: CN=Visa eCommerce Root O=VISA OU=Visa International Service Association +# Label: "Visa eCommerce Root" +# Serial: 25952180776285836048024890241505565794 +# MD5 Fingerprint: fc:11:b8:d8:08:93:30:00:6d:23:f9:7e:eb:52:1e:02 +# SHA1 Fingerprint: 70:17:9b:86:8c:00:a4:fa:60:91:52:22:3f:9f:3e:32:bd:e0:05:62 +# SHA256 Fingerprint: 69:fa:c9:bd:55:fb:0a:c7:8d:53:bb:ee:5c:f1:d5:97:98:9f:d0:aa:ab:20:a2:51:51:bd:f1:73:3e:e7:d1:22 +-----BEGIN CERTIFICATE----- +MIIDojCCAoqgAwIBAgIQE4Y1TR0/BvLB+WUF1ZAcYjANBgkqhkiG9w0BAQUFADBr +MQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRl +cm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNv +bW1lcmNlIFJvb3QwHhcNMDIwNjI2MDIxODM2WhcNMjIwNjI0MDAxNjEyWjBrMQsw +CQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRlcm5h +dGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNvbW1l +cmNlIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvV95WHm6h +2mCxlCfLF9sHP4CFT8icttD0b0/Pmdjh28JIXDqsOTPHH2qLJj0rNfVIsZHBAk4E +lpF7sDPwsRROEW+1QK8bRaVK7362rPKgH1g/EkZgPI2h4H3PVz4zHvtH8aoVlwdV +ZqW1LS7YgFmypw23RuwhY/81q6UCzyr0TP579ZRdhE2o8mCP2w4lPJ9zcc+U30rq +299yOIzzlr3xF7zSujtFWsan9sYXiwGd/BmoKoMWuDpI/k4+oKsGGelT84ATB+0t +vz8KPFUgOSwsAGl0lUq8ILKpeeUYiZGo3BxN77t+Nwtd/jmliFKMAGzsGHxBvfaL +dXe6YJ2E5/4tAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQVOIMPPyw/cDMezUb+B4wg4NfDtzANBgkqhkiG9w0BAQUF +AAOCAQEAX/FBfXxcCLkr4NWSR/pnXKUTwwMhmytMiUbPWU3J/qVAtmPN3XEolWcR +zCSs00Rsca4BIGsDoo8Ytyk6feUWYFN4PMCvFYP3j1IzJL1kk5fui/fbGKhtcbP3 +LBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pzzkWKsKZJ/0x9nXGIxHYdkFsd +7v3M9+79YKWxehZx0RbQfBI8bGmX265fOZpwLwU8GUYEmSA20GBuYQa7FkKMcPcw +++DbZqMAAb3mLNqRX6BGi01qnD093QVG/na/oAo85ADmJ7f/hC3euiInlhBx6yLt +398znM/jra6O1I7mT1GvFpLgXPYHDw== +-----END CERTIFICATE----- + +# Issuer: CN=Certum CA O=Unizeto Sp. z o.o. +# Subject: CN=Certum CA O=Unizeto Sp. z o.o. +# Label: "Certum Root CA" +# Serial: 65568 +# MD5 Fingerprint: 2c:8f:9f:66:1d:18:90:b1:47:26:9d:8e:86:82:8c:a9 +# SHA1 Fingerprint: 62:52:dc:40:f7:11:43:a2:2f:de:9e:f7:34:8e:06:42:51:b1:81:18 +# SHA256 Fingerprint: d8:e0:fe:bc:1d:b2:e3:8d:00:94:0f:37:d2:7d:41:34:4d:99:3e:73:4b:99:d5:65:6d:97:78:d4:d8:14:36:24 +-----BEGIN CERTIFICATE----- +MIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBM +MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD +QTAeFw0wMjA2MTExMDQ2MzlaFw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBM +MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD +QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6xwS7TT3zNJc4YPk/E +jG+AanPIW1H4m9LcuwBcsaD8dQPugfCI7iNS6eYVM42sLQnFdvkrOYCJ5JdLkKWo +ePhzQ3ukYbDYWMzhbGZ+nPMJXlVjhNWo7/OxLjBos8Q82KxujZlakE403Daaj4GI +ULdtlkIJ89eVgw1BS7Bqa/j8D35in2fE7SZfECYPCE/wpFcozo+47UX2bu4lXapu +Ob7kky/ZR6By6/qmW6/KUz/iDsaWVhFu9+lmqSbYf5VT7QqFiLpPKaVCjF62/IUg +AKpoC6EahQGcxEZjgoi2IrHu/qpGWX7PNSzVttpd90gzFFS269lvzs2I1qsb2pY7 +HVkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA +uI3O7+cUus/usESSbLQ5PqKEbq24IXfS1HeCh+YgQYHu4vgRt2PRFze+GXYkHAQa +TOs9qmdvLdTN/mUxcMUbpgIKumB7bVjCmkn+YzILa+M6wKyrO7Do0wlRjBCDxjTg +xSvgGrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1q +CjqTE5s7FCMTY5w/0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5x +O/fIR/RpbxXyEV6DHpx8Uq79AtoSqFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs +6GAqm4VKQPNriiTsBhYscw== +-----END CERTIFICATE----- + +# Issuer: CN=AAA Certificate Services O=Comodo CA Limited +# Subject: CN=AAA Certificate Services O=Comodo CA Limited +# Label: "Comodo AAA Services root" +# Serial: 1 +# MD5 Fingerprint: 49:79:04:b0:eb:87:19:ac:47:b0:bc:11:51:9b:74:d0 +# SHA1 Fingerprint: d1:eb:23:a4:6d:17:d6:8f:d9:25:64:c2:f1:f1:60:17:64:d8:e3:49 +# SHA256 Fingerprint: d7:a7:a0:fb:5d:7e:27:31:d7:71:e9:48:4e:bc:de:f7:1d:5f:0c:3e:0a:29:48:78:2b:c8:3e:e0:ea:69:9e:f4 +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- + +# Issuer: CN=Secure Certificate Services O=Comodo CA Limited +# Subject: CN=Secure Certificate Services O=Comodo CA Limited +# Label: "Comodo Secure Services root" +# Serial: 1 +# MD5 Fingerprint: d3:d9:bd:ae:9f:ac:67:24:b3:c8:1b:52:e1:b9:a9:bd +# SHA1 Fingerprint: 4a:65:d5:f4:1d:ef:39:b8:b8:90:4a:4a:d3:64:81:33:cf:c7:a1:d1 +# SHA256 Fingerprint: bd:81:ce:3b:4f:65:91:d1:1a:67:b5:fc:7a:47:fd:ef:25:52:1b:f9:aa:4e:18:b9:e3:df:2e:34:a7:80:3b:e8 +-----BEGIN CERTIFICATE----- +MIIEPzCCAyegAwIBAgIBATANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEkMCIGA1UEAwwbU2VjdXJlIENlcnRp +ZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVow +fjELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxJDAiBgNV +BAMMG1NlY3VyZSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAMBxM4KK0HDrc4eCQNUd5MvJDkKQ+d40uaG6EfQlhfPM +cm3ye5drswfxdySRXyWP9nQ95IDC+DwN879A6vfIUtFyb+/Iq0G4bi4XKpVpDM3S +HpR7LZQdqnXXs5jLrLxkU0C8j6ysNstcrbvd4JQX7NFc0L/vpZXJkMWwrPsbQ996 +CF23uPJAGysnnlDOXmWCiIxe004MeuoIkbY2qitC++rCoznl2yY4rYsK7hljxxwk +3wN42ubqwUcaCwtGCd0C/N7Lh1/XMGNooa7cMqG6vv5Eq2i2pRcV/b3Vp6ea5EQz +6YiO/O1R65NxTq0B50SOqy3LqP4BSUjwwN3HaNiS/j0CAwEAAaOBxzCBxDAdBgNV +HQ4EFgQUPNiTiMLAggnMAZkGkyDpnnAJY08wDgYDVR0PAQH/BAQDAgEGMA8GA1Ud +EwEB/wQFMAMBAf8wgYEGA1UdHwR6MHgwO6A5oDeGNWh0dHA6Ly9jcmwuY29tb2Rv +Y2EuY29tL1NlY3VyZUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDmgN6A1hjNodHRw +Oi8vY3JsLmNvbW9kby5uZXQvU2VjdXJlQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmww +DQYJKoZIhvcNAQEFBQADggEBAIcBbSMdflsXfcFhMs+P5/OKlFlm4J4oqF7Tt/Q0 +5qo5spcWxYJvMqTpjOev/e/C6LlLqqP05tqNZSH7uoDrJiiFGv45jN5bBAS0VPmj +Z55B+glSzAVIqMk/IQQezkhr/IXownuvf7fM+F86/TXGDe+X3EyrEeFryzHRbPtI +gKvcnDe4IRRLDXE97IMzbtFuMhbsmMcWi1mmNKsFVy2T96oTy9IT4rcuO81rUBcJ +aD61JlfutuC23bkpgHl9j6PwpCikFcSF9CfUa7/lXORlAnZUtOM3ZiTTGWHIUhDl +izeauan5Hb/qmZJhlv8BzaFfDbxxvA6sCx1HRR3B7Hzs/Sk= +-----END CERTIFICATE----- + +# Issuer: CN=Trusted Certificate Services O=Comodo CA Limited +# Subject: CN=Trusted Certificate Services O=Comodo CA Limited +# Label: "Comodo Trusted Services root" +# Serial: 1 +# MD5 Fingerprint: 91:1b:3f:6e:cd:9e:ab:ee:07:fe:1f:71:d2:b3:61:27 +# SHA1 Fingerprint: e1:9f:e3:0e:8b:84:60:9e:80:9b:17:0d:72:a8:c5:ba:6e:14:09:bd +# SHA256 Fingerprint: 3f:06:e5:56:81:d4:96:f5:be:16:9e:b5:38:9f:9f:2b:8f:f6:1e:17:08:df:68:81:72:48:49:cd:5d:27:cb:69 +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDElMCMGA1UEAwwcVHJ1c3RlZCBDZXJ0 +aWZpY2F0ZSBTZXJ2aWNlczAeFw0wNDAxMDEwMDAwMDBaFw0yODEyMzEyMzU5NTla +MH8xCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO +BgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSUwIwYD +VQQDDBxUcnVzdGVkIENlcnRpZmljYXRlIFNlcnZpY2VzMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA33FvNlhTWvI2VFeAxHQIIO0Yfyod5jWaHiWsnOWW +fnJSoBVC21ndZHoa0Lh73TkVvFVIxO06AOoxEbrycXQaZ7jPM8yoMa+j49d/vzMt +TGo87IvDktJTdyR0nAducPy9C1t2ul/y/9c3S0pgePfw+spwtOpZqqPOSC+pw7IL +fhdyFgymBwwbOM/JYrc/oJOlh0Hyt3BAd9i+FHzjqMB6juljatEPmsbS9Is6FARW +1O24zG71++IsWL1/T2sr92AkWCTOJu80kTrV44HQsvAEAtdbtz6SrGsSivnkBbA7 +kUlcsutT6vifR4buv5XAwAaf0lteERv0xwQ1KdJVXOTt6wIDAQABo4HJMIHGMB0G +A1UdDgQWBBTFe1i97doladL3WRaoszLAeydb9DAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zCBgwYDVR0fBHwwejA8oDqgOIY2aHR0cDovL2NybC5jb21v +ZG9jYS5jb20vVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMuY3JsMDqgOKA2hjRo +dHRwOi8vY3JsLmNvbW9kby5uZXQvVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMu +Y3JsMA0GCSqGSIb3DQEBBQUAA4IBAQDIk4E7ibSvuIQSTI3S8NtwuleGFTQQuS9/ +HrCoiWChisJ3DFBKmwCL2Iv0QeLQg4pKHBQGsKNoBXAxMKdTmw7pSqBYaWcOrp32 +pSxBvzwGa+RZzG0Q8ZZvH9/0BAKkn0U+yNj6NkZEUD+Cl5EfKNsYEYwq5GWDVxIS +jBc/lDb+XbDABHcTuPQV1T84zJQ6VdCsmPW6AF/ghhmBeC8owH7TzEIK9a5QoNE+ +xqFx7D+gIIxmOom0jtTYsU0lR+4viMi14QVFwL4Ucd56/Y57fU0IlqUSc/Atyjcn +dBInTMu2l+nZrghtWjlA3QVHdWpaIbOjGM9O9y5Xt5hwXsjEeLBi +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root Certification Authority O=QuoVadis Limited OU=Root Certification Authority +# Subject: CN=QuoVadis Root Certification Authority O=QuoVadis Limited OU=Root Certification Authority +# Label: "QuoVadis Root CA" +# Serial: 985026699 +# MD5 Fingerprint: 27:de:36:fe:72:b7:00:03:00:9d:f4:f0:1e:6c:04:24 +# SHA1 Fingerprint: de:3f:40:bd:50:93:d3:9b:6c:60:f6:da:bc:07:62:01:00:89:76:c9 +# SHA256 Fingerprint: a4:5e:de:3b:bb:f0:9c:8a:e1:5c:72:ef:c0:72:68:d6:93:a2:1c:99:6f:d5:1e:67:ca:07:94:60:fd:6d:88:73 +-----BEGIN CERTIFICATE----- +MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAzMTkxODMzMzNaFw0yMTAzMTcxODMz +MzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUw +IwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQDEyVR +dW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Yp +li4kVEAkOPcahdxYTMukJ0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2D +rOpm2RgbaIr1VxqYuvXtdj182d6UajtLF8HVj71lODqV0D1VNk7feVcxKh7YWWVJ +WCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeLYzcS19Dsw3sgQUSj7cug +F+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWenAScOospU +xbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCC +Ak4wPQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVv +dmFkaXNvZmZzaG9yZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREw +ggENMIIBCQYJKwYBBAG+WAABMIH7MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNl +IG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBh +c3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFy +ZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh +Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYI +KwYBBQUHAgEWFmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3T +KbkGGew5Oanwl4Rqy+/fMIGuBgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rq +y+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1p +dGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYD +VQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6tlCL +MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSk +fnIYj9lofFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf8 +7C9TqnN7Az10buYWnuulLsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1R +cHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2xgI4JVrmcGmD+XcHXetwReNDWXcG31a0y +mQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi5upZIof4l/UO/erMkqQW +xFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi5nrQNiOK +SnQ2+Q== +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 2 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 2 O=QuoVadis Limited +# Label: "QuoVadis Root CA 2" +# Serial: 1289 +# MD5 Fingerprint: 5e:39:7b:dd:f8:ba:ec:82:e9:ac:62:ba:0c:54:00:2b +# SHA1 Fingerprint: ca:3a:fb:cf:12:40:36:4b:44:b2:16:20:88:80:48:39:19:93:7c:f7 +# SHA256 Fingerprint: 85:a0:dd:7d:d7:20:ad:b7:ff:05:f8:3d:54:2b:20:9d:c7:ff:45:28:f7:d6:77:b1:83:89:fe:a5:e5:c4:9e:86 +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa +GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg +Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J +WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB +rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp ++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 +ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i +Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz +PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og +/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH +oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI +yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud +EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 +A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL +MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f +BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn +g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl +fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K +WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha +B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc +hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR +TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD +mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z +ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y +4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza +8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 3" +# Serial: 1478 +# MD5 Fingerprint: 31:85:3c:62:94:97:63:b9:aa:fd:89:4e:af:6f:e0:cf +# SHA1 Fingerprint: 1f:49:14:f7:d8:74:95:1d:dd:ae:02:c0:be:fd:3a:2d:82:75:51:85 +# SHA256 Fingerprint: 18:f1:fc:7f:20:5d:f8:ad:dd:eb:7f:e0:07:dd:57:e3:af:37:5a:9c:4d:8d:73:54:6b:f4:f1:fe:d1:e1:8d:35 +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM +V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB +4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr +H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd +8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv +vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT +mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe +btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc +T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt +WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ +c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A +4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD +VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG +CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0 +aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu +dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw +czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G +A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg +Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0 +7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem +d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd ++LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B +4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN +t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x +DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57 +k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s +zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j +Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT +mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK +4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- + +# Issuer: O=SECOM Trust.net OU=Security Communication RootCA1 +# Subject: O=SECOM Trust.net OU=Security Communication RootCA1 +# Label: "Security Communication Root CA" +# Serial: 0 +# MD5 Fingerprint: f1:bc:63:6a:54:e0:b5:27:f5:cd:e7:1a:e3:4d:6e:4a +# SHA1 Fingerprint: 36:b1:2b:49:f9:81:9e:d7:4c:9e:bc:38:0f:c6:56:8f:5d:ac:b2:f7 +# SHA256 Fingerprint: e7:5e:72:ed:9f:56:0e:ec:6e:b4:80:00:73:a4:3f:c3:ad:19:19:5a:39:22:82:01:78:95:97:4a:99:02:6b:6c +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY +MBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t +dW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5 +WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD +VQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8 +9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ +DKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9 +Ms+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N +QV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ +xrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G +A1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG +kl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr +Uj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5 +Bw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU +JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot +RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw== +-----END CERTIFICATE----- + +# Issuer: CN=Sonera Class2 CA O=Sonera +# Subject: CN=Sonera Class2 CA O=Sonera +# Label: "Sonera Class 2 Root CA" +# Serial: 29 +# MD5 Fingerprint: a3:ec:75:0f:2e:88:df:fa:48:01:4e:0b:5c:48:6f:fb +# SHA1 Fingerprint: 37:f7:6d:e6:07:7c:90:c5:b1:3e:93:1a:b7:41:10:b4:f2:e4:9a:27 +# SHA256 Fingerprint: 79:08:b4:03:14:c1:38:10:0b:51:8d:07:35:80:7f:fb:fc:f8:51:8a:00:95:33:71:05:ba:38:6b:15:3d:d9:27 +-----BEGIN CERTIFICATE----- +MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP +MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAx +MDQwNjA3Mjk0MFoXDTIxMDQwNjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNV +BAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMiBDQTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3/Ei9vX+ALTU74W+o +Z6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybTdXnt +5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s +3TmVToMGf+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2Ej +vOr7nQKV0ba5cTppCD8PtOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu +8nYybieDwnPz3BjotJPqdURrBGAgcVeHnfO+oJAjPYok4doh28MCAwEAAaMzMDEw +DwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITTXjwwCwYDVR0PBAQDAgEG +MA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt0jSv9zil +zqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/ +3DEIcbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvD +FNr450kkkdAdavphOe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6 +Tk6ezAyNlNzZRZxe7EJQY670XcSxEtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2 +ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLHllpwrN9M +-----END CERTIFICATE----- + +# Issuer: CN=UTN-USERFirst-Hardware O=The USERTRUST Network OU=http://www.usertrust.com +# Subject: CN=UTN-USERFirst-Hardware O=The USERTRUST Network OU=http://www.usertrust.com +# Label: "UTN USERFirst Hardware Root CA" +# Serial: 91374294542884704022267039221184531197 +# MD5 Fingerprint: 4c:56:41:e5:0d:bb:2b:e8:ca:a3:ed:18:08:ad:43:39 +# SHA1 Fingerprint: 04:83:ed:33:99:ac:36:08:05:87:22:ed:bc:5e:46:00:e3:be:f9:d7 +# SHA256 Fingerprint: 6e:a5:47:41:d0:04:66:7e:ed:1b:48:16:63:4a:a3:a7:9e:6e:4b:96:95:0f:82:79:da:fc:8d:9b:d8:81:21:37 +-----BEGIN CERTIFICATE----- +MIIEdDCCA1ygAwIBAgIQRL4Mi1AAJLQR0zYq/mUK/TANBgkqhkiG9w0BAQUFADCB +lzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3Qt +SGFyZHdhcmUwHhcNOTkwNzA5MTgxMDQyWhcNMTkwNzA5MTgxOTIyWjCBlzELMAkG +A1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEe +MBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8v +d3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdh +cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCx98M4P7Sof885glFn +0G2f0v9Y8+efK+wNiVSZuTiZFvfgIXlIwrthdBKWHTxqctU8EGc6Oe0rE81m65UJ +M6Rsl7HoxuzBdXmcRl6Nq9Bq/bkqVRcQVLMZ8Jr28bFdtqdt++BxF2uiiPsA3/4a +MXcMmgF6sTLjKwEHOG7DpV4jvEWbe1DByTCP2+UretNb+zNAHqDVmBe8i4fDidNd +oI6yqqr2jmmIBsX6iSHzCJ1pLgkzmykNRg+MzEk0sGlRvfkGzWitZky8PqxhvQqI +DsjfPe58BEydCl5rkdbux+0ojatNh4lz0G6k0B4WixThdkQDf2Os5M1JnMWS9Ksy +oUhbAgMBAAGjgbkwgbYwCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFKFyXyYbKJhDlV0HN9WFlp1L0sNFMEQGA1UdHwQ9MDswOaA3oDWGM2h0 +dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNFUkZpcnN0LUhhcmR3YXJlLmNy +bDAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwUGCCsGAQUFBwMGBggrBgEF +BQcDBzANBgkqhkiG9w0BAQUFAAOCAQEARxkP3nTGmZev/K0oXnWO6y1n7k57K9cM +//bey1WiCuFMVGWTYGufEpytXoMs61quwOQt9ABjHbjAbPLPSbtNk28Gpgoiskli +CE7/yMgUsogWXecB5BKV5UU0s4tpvc+0hY91UZ59Ojg6FEgSxvunOxqNDYJAB+gE +CJChicsZUN/KHAG8HQQZexB2lzvukJDKxA4fFm517zP4029bHpbj4HR3dHuKom4t +3XbWOTCC8KucUvIqx69JXn7HaOWCgchqJ/kniCrVWFCVH/A7HFe7fRQ5YiuayZSS +KqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67nfhmqA== +-----END CERTIFICATE----- + +# Issuer: CN=Chambers of Commerce Root O=AC Camerfirma SA CIF A82743287 OU=http://www.chambersign.org +# Subject: CN=Chambers of Commerce Root O=AC Camerfirma SA CIF A82743287 OU=http://www.chambersign.org +# Label: "Camerfirma Chambers of Commerce Root" +# Serial: 0 +# MD5 Fingerprint: b0:01:ee:14:d9:af:29:18:94:76:8e:f1:69:33:2a:84 +# SHA1 Fingerprint: 6e:3a:55:a4:19:0c:19:5c:93:84:3c:c0:db:72:2e:31:30:61:f0:b1 +# SHA256 Fingerprint: 0c:25:8a:12:a5:67:4a:ef:25:f2:8b:a7:dc:fa:ec:ee:a3:48:e5:41:e6:f5:cc:4e:e6:3b:71:b3:61:60:6a:c3 +-----BEGIN CERTIFICATE----- +MIIEvTCCA6WgAwIBAgIBADANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJFVTEn +MCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL +ExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEiMCAGA1UEAxMZQ2hhbWJlcnMg +b2YgQ29tbWVyY2UgUm9vdDAeFw0wMzA5MzAxNjEzNDNaFw0zNzA5MzAxNjEzNDRa +MH8xCzAJBgNVBAYTAkVVMScwJQYDVQQKEx5BQyBDYW1lcmZpcm1hIFNBIENJRiBB +ODI3NDMyODcxIzAhBgNVBAsTGmh0dHA6Ly93d3cuY2hhbWJlcnNpZ24ub3JnMSIw +IAYDVQQDExlDaGFtYmVycyBvZiBDb21tZXJjZSBSb290MIIBIDANBgkqhkiG9w0B +AQEFAAOCAQ0AMIIBCAKCAQEAtzZV5aVdGDDg2olUkfzIx1L4L1DZ77F1c2VHfRtb +unXF/KGIJPov7coISjlUxFF6tdpg6jg8gbLL8bvZkSM/SAFwdakFKq0fcfPJVD0d +BmpAPrMMhe5cG3nCYsS4No41XQEMIwRHNaqbYE6gZj3LJgqcQKH0XZi/caulAGgq +7YN6D6IUtdQis4CwPAxaUWktWBiP7Zme8a7ileb2R6jWDA+wWFjbw2Y3npuRVDM3 +0pQcakjJyfKl2qUMI/cjDpwyVV5xnIQFUZot/eZOKjRa3spAN2cMVCFVd9oKDMyX +roDclDZK9D7ONhMeU+SsTjoF7Nuucpw4i9A5O4kKPnf+dQIBA6OCAUQwggFAMBIG +A1UdEwEB/wQIMAYBAf8CAQwwPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL2NybC5j +aGFtYmVyc2lnbi5vcmcvY2hhbWJlcnNyb290LmNybDAdBgNVHQ4EFgQU45T1sU3p +26EpW1eLTXYGduHRooowDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIA +BzAnBgNVHREEIDAegRxjaGFtYmVyc3Jvb3RAY2hhbWJlcnNpZ24ub3JnMCcGA1Ud +EgQgMB6BHGNoYW1iZXJzcm9vdEBjaGFtYmVyc2lnbi5vcmcwWAYDVR0gBFEwTzBN +BgsrBgEEAYGHLgoDATA+MDwGCCsGAQUFBwIBFjBodHRwOi8vY3BzLmNoYW1iZXJz +aWduLm9yZy9jcHMvY2hhbWJlcnNyb290Lmh0bWwwDQYJKoZIhvcNAQEFBQADggEB +AAxBl8IahsAifJ/7kPMa0QOx7xP5IV8EnNrJpY0nbJaHkb5BkAFyk+cefV/2icZd +p0AJPaxJRUXcLo0waLIJuvvDL8y6C98/d3tGfToSJI6WjzwFCm/SlCgdbQzALogi +1djPHRPH8EjX1wWnz8dHnjs8NMiAT9QUu/wNUPf6s+xCX6ndbcj0dc97wXImsQEc +XCz9ek60AcUFV7nnPKoF2YjpB0ZBzu9Bga5Y34OirsrXdx/nADydb47kMgkdTXg0 +eDQ8lJsm7U9xxhl6vSAiSFr+S30Dt+dYvsYyTnQeaN2oaFuzPu5ifdmA6Ap1erfu +tGWaIZDgqtCYvDi1czyL+Nw= +-----END CERTIFICATE----- + +# Issuer: CN=Global Chambersign Root O=AC Camerfirma SA CIF A82743287 OU=http://www.chambersign.org +# Subject: CN=Global Chambersign Root O=AC Camerfirma SA CIF A82743287 OU=http://www.chambersign.org +# Label: "Camerfirma Global Chambersign Root" +# Serial: 0 +# MD5 Fingerprint: c5:e6:7b:bf:06:d0:4f:43:ed:c4:7a:65:8a:fb:6b:19 +# SHA1 Fingerprint: 33:9b:6b:14:50:24:9b:55:7a:01:87:72:84:d9:e0:2f:c3:d2:d8:e9 +# SHA256 Fingerprint: ef:3c:b4:17:fc:8e:bf:6f:97:87:6c:9e:4e:ce:39:de:1e:a5:fe:64:91:41:d1:02:8b:7d:11:c0:b2:29:8c:ed +-----BEGIN CERTIFICATE----- +MIIExTCCA62gAwIBAgIBADANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJFVTEn +MCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL +ExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4GA1UEAxMXR2xvYmFsIENo +YW1iZXJzaWduIFJvb3QwHhcNMDMwOTMwMTYxNDE4WhcNMzcwOTMwMTYxNDE4WjB9 +MQswCQYDVQQGEwJFVTEnMCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgy +NzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4G +A1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwggEgMA0GCSqGSIb3DQEBAQUA +A4IBDQAwggEIAoIBAQCicKLQn0KuWxfH2H3PFIP8T8mhtxOviteePgQKkotgVvq0 +Mi+ITaFgCPS3CU6gSS9J1tPfnZdan5QEcOw/Wdm3zGaLmFIoCQLfxS+EjXqXd7/s +QJ0lcqu1PzKY+7e3/HKE5TWH+VX6ox8Oby4o3Wmg2UIQxvi1RMLQQ3/bvOSiPGpV +eAp3qdjqGTK3L/5cPxvusZjsyq16aUXjlg9V9ubtdepl6DJWk0aJqCWKZQbua795 +B9Dxt6/tLE2Su8CoX6dnfQTyFQhwrJLWfQTSM/tMtgsL+xrJxI0DqX5c8lCrEqWh +z0hQpe/SyBoT+rB/sYIcd2oPX9wLlY/vQ37mRQklAgEDo4IBUDCCAUwwEgYDVR0T +AQH/BAgwBgEB/wIBDDA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY3JsLmNoYW1i +ZXJzaWduLm9yZy9jaGFtYmVyc2lnbnJvb3QuY3JsMB0GA1UdDgQWBBRDnDafsJ4w +TcbOX60Qq+UDpfqpFDAOBgNVHQ8BAf8EBAMCAQYwEQYJYIZIAYb4QgEBBAQDAgAH +MCoGA1UdEQQjMCGBH2NoYW1iZXJzaWducm9vdEBjaGFtYmVyc2lnbi5vcmcwKgYD +VR0SBCMwIYEfY2hhbWJlcnNpZ25yb290QGNoYW1iZXJzaWduLm9yZzBbBgNVHSAE +VDBSMFAGCysGAQQBgYcuCgEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly9jcHMuY2hh +bWJlcnNpZ24ub3JnL2Nwcy9jaGFtYmVyc2lnbnJvb3QuaHRtbDANBgkqhkiG9w0B +AQUFAAOCAQEAPDtwkfkEVCeR4e3t/mh/YV3lQWVPMvEYBZRqHN4fcNs+ezICNLUM +bKGKfKX0j//U2K0X1S0E0T9YgOKBWYi+wONGkyT+kL0mojAt6JcmVzWJdJYY9hXi +ryQZVgICsroPFOrGimbBhkVVi76SvpykBMdJPJ7oKXqJ1/6v/2j1pReQvayZzKWG +VwlnRtvWFsJG8eSpUPWP0ZIV018+xgBJOm5YstHRJw0lyDL4IBHNfTIzSJRUTN3c +ecQwn+uOuFW114hcxWokPbLTBQNRxgfvzBRydD1ucs4YKIxKoHflCStFREest2d/ +AYoFWpO+ocH/+OcOZ6RHSXZddZAa9SaP8A== +-----END CERTIFICATE----- + +# Issuer: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com +# Subject: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com +# Label: "XRamp Global CA Root" +# Serial: 107108908803651509692980124233745014957 +# MD5 Fingerprint: a1:0b:44:b3:ca:10:d8:00:6e:9d:0f:d8:0f:92:0a:d1 +# SHA1 Fingerprint: b8:01:86:d1:eb:9c:86:a5:41:04:cf:30:54:f3:4c:52:b7:e5:58:c6 +# SHA256 Fingerprint: ce:cd:dc:90:50:99:d8:da:df:c5:b1:d2:09:b7:37:cb:e2:c1:8c:fb:2c:10:c0:ff:0b:cf:0d:32:86:fc:1a:a2 +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB +gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk +MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY +UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx +NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 +dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy +dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 +38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP +KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q +DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 +qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa +JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi +PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P +BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs +jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 +eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD +ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR +vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa +IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy +i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ +O+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- + +# Issuer: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority +# Subject: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority +# Label: "Go Daddy Class 2 CA" +# Serial: 0 +# MD5 Fingerprint: 91:de:06:25:ab:da:fd:32:17:0c:bb:25:17:2a:84:67 +# SHA1 Fingerprint: 27:96:ba:e6:3f:18:01:e2:77:26:1b:a0:d7:77:70:02:8f:20:ee:e4 +# SHA256 Fingerprint: c3:84:6b:f2:4b:9e:93:ca:64:27:4c:0e:c6:7c:1e:cc:5e:02:4f:fc:ac:d2:d7:40:19:35:0e:81:fe:54:6a:e4 +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh +MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE +YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 +MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo +ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg +MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN +ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA +PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w +wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi +EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY +avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ +YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE +sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h +/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 +IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy +OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P +TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER +dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf +ReYNnyicsbkqWletNw+vHX/bvZ8= +-----END CERTIFICATE----- + +# Issuer: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority +# Subject: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority +# Label: "Starfield Class 2 CA" +# Serial: 0 +# MD5 Fingerprint: 32:4a:4b:bb:c8:63:69:9b:be:74:9a:c6:dd:1d:46:24 +# SHA1 Fingerprint: ad:7e:1c:28:b0:64:ef:8f:60:03:40:20:14:c3:d0:e3:37:0e:b5:8a +# SHA256 Fingerprint: 14:65:fa:20:53:97:b8:76:fa:a6:f0:a9:95:8e:55:90:e4:0f:cc:7f:aa:4f:b7:c2:c8:67:75:21:fb:5f:b6:58 +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl +MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp +U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw +NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp +ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 +DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf +8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN ++lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 +X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa +K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA +1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G +A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR +zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 +YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD +bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 +L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D +eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp +VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY +WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- + +# Issuer: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Subject: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Label: "StartCom Certification Authority" +# Serial: 1 +# MD5 Fingerprint: 22:4d:8f:8a:fc:f7:35:c2:bb:57:34:90:7b:8b:22:16 +# SHA1 Fingerprint: 3e:2b:f7:f2:03:1b:96:f3:8c:e6:c4:d8:a8:5d:3e:2d:58:47:6a:0f +# SHA256 Fingerprint: c7:66:a9:be:f2:d4:07:1c:86:3a:31:aa:49:20:e8:13:b2:d1:98:60:8c:b7:b7:cf:e2:11:43:b8:36:df:09:ea +-----BEGIN CERTIFICATE----- +MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg +Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM2WhcNMzYwOTE3MTk0NjM2WjB9 +MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi +U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh +cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk +pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf +OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C +Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT +Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi +HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM +Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w ++2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+ +Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3 +Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B +26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID +AQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE +FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9j +ZXJ0LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3Js +LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFM +BgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUHAgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0 +Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRwOi8vY2VydC5zdGFy +dGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYgU3Rh +cnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlh +YmlsaXR5LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2Yg +dGhlIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFp +bGFibGUgYXQgaHR0cDovL2NlcnQuc3RhcnRjb20ub3JnL3BvbGljeS5wZGYwEQYJ +YIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNT +TCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOCAgEAFmyZ +9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8 +jhvh3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUW +FjgKXlf2Ysd6AgXmvB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJz +ewT4F+irsfMuXGRuczE6Eri8sxHkfY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1 +ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3fsNrarnDy0RLrHiQi+fHLB5L +EUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZEoalHmdkrQYu +L6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq +yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuC +O3NJo2pXh5Tl1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6V +um0ABj6y6koQOdjQK/W/7HW/lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkySh +NOsF/5oirpt9P/FlUQqmMGqz9IgcgA38corog14= +-----END CERTIFICATE----- + +# Issuer: O=Government Root Certification Authority +# Subject: O=Government Root Certification Authority +# Label: "Taiwan GRCA" +# Serial: 42023070807708724159991140556527066870 +# MD5 Fingerprint: 37:85:44:53:32:45:1f:20:f0:f3:95:e1:25:c4:43:4e +# SHA1 Fingerprint: f4:8b:11:bf:de:ab:be:94:54:20:71:e6:41:de:6b:be:88:2b:40:b9 +# SHA256 Fingerprint: 76:00:29:5e:ef:e8:5b:9e:1f:d6:24:db:76:06:2a:aa:ae:59:81:8a:54:d2:77:4c:d4:c0:b2:c0:11:31:e1:b3 +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/ +MQswCQYDVQQGEwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5MB4XDTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1ow +PzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dvdmVybm1lbnQgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +AJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qNw8XR +IePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1q +gQdW8or5BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKy +yhwOeYHWtXBiCAEuTk8O1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAts +F/tnyMKtsc2AtJfcdgEWFelq16TheEfOhtX7MfP6Mb40qij7cEwdScevLJ1tZqa2 +jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wovJ5pGfaENda1UhhXcSTvx +ls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7Q3hub/FC +VGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHK +YS1tB6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoH +EgKXTiCQ8P8NHuJBO9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThN +Xo+EHWbNxWCWtFJaBYmOlXqYwZE8lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1Ud +DgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNVHRMEBTADAQH/MDkGBGcqBwAE +MTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg209yewDL7MTqK +UWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ +TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyf +qzvS/3WXy6TjZwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaK +ZEk9GhiHkASfQlK3T8v+R0F2Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFE +JPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlUD7gsL0u8qV1bYH+Mh6XgUmMqvtg7 +hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6QzDxARvBMB1uUO07+1 +EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+HbkZ6Mm +nD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WX +udpVBrkk7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44Vbnz +ssQwmSNOXfJIoRIM3BKQCZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDe +LMDDav7v3Aun+kbfYNucpllQdSNpc5Oy+fwC00fmcc4QAu4njIT/rEUNE1yDMuAl +pYYsfPQS +-----END CERTIFICATE----- + +# Issuer: CN=Swisscom Root CA 1 O=Swisscom OU=Digital Certificate Services +# Subject: CN=Swisscom Root CA 1 O=Swisscom OU=Digital Certificate Services +# Label: "Swisscom Root CA 1" +# Serial: 122348795730808398873664200247279986742 +# MD5 Fingerprint: f8:38:7c:77:88:df:2c:16:68:2e:c2:e2:52:4b:b8:f9 +# SHA1 Fingerprint: 5f:3a:fc:0a:8b:64:f6:86:67:34:74:df:7e:a9:a2:fe:f9:fa:7a:51 +# SHA256 Fingerprint: 21:db:20:12:36:60:bb:2e:d4:18:20:5d:a1:1e:e7:a8:5a:65:e2:bc:6e:55:b5:af:7e:78:99:c8:a2:66:d9:2e +-----BEGIN CERTIFICATE----- +MIIF2TCCA8GgAwIBAgIQXAuFXAvnWUHfV8w/f52oNjANBgkqhkiG9w0BAQUFADBk +MQswCQYDVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0 +YWwgQ2VydGlmaWNhdGUgU2VydmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3Qg +Q0EgMTAeFw0wNTA4MTgxMjA2MjBaFw0yNTA4MTgyMjA2MjBaMGQxCzAJBgNVBAYT +AmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGlnaXRhbCBDZXJ0aWZp +Y2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAxMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0LmwqAzZuz8h+BvVM5OAFmUgdbI9 +m2BtRsiMMW8Xw/qabFbtPMWRV8PNq5ZJkCoZSx6jbVfd8StiKHVFXqrWW/oLJdih +FvkcxC7mlSpnzNApbjyFNDhhSbEAn9Y6cV9Nbc5fuankiX9qUvrKm/LcqfmdmUc/ +TilftKaNXXsLmREDA/7n29uj/x2lzZAeAR81sH8A25Bvxn570e56eqeqDFdvpG3F +EzuwpdntMhy0XmeLVNxzh+XTF3xmUHJd1BpYwdnP2IkCb6dJtDZd0KTeByy2dbco +kdaXvij1mB7qWybJvbCXc9qukSbraMH5ORXWZ0sKbU/Lz7DkQnGMU3nn7uHbHaBu +HYwadzVcFh4rUx80i9Fs/PJnB3r1re3WmquhsUvhzDdf/X/NTa64H5xD+SpYVUNF +vJbNcA78yeNmuk6NO4HLFWR7uZToXTNShXEuT46iBhFRyePLoW4xCGQMwtI89Tbo +19AOeCMgkckkKmUpWyL3Ic6DXqTz3kvTaI9GdVyDCW4pa8RwjPWd1yAv/0bSKzjC +L3UcPX7ape8eYIVpQtPM+GP+HkM5haa2Y0EQs3MevNP6yn0WR+Kn1dCjigoIlmJW +bjTb2QK5MHXjBNLnj8KwEUAKrNVxAmKLMb7dxiNYMUJDLXT5xp6mig/p/r+D5kNX +JLrvRjSq1xIBOO0CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0hBBYw +FDASBgdghXQBUwABBgdghXQBUwABMBIGA1UdEwEB/wQIMAYBAf8CAQcwHwYDVR0j +BBgwFoAUAyUv3m+CATpcLNwroWm1Z9SM0/0wHQYDVR0OBBYEFAMlL95vggE6XCzc +K6FptWfUjNP9MA0GCSqGSIb3DQEBBQUAA4ICAQA1EMvspgQNDQ/NwNurqPKIlwzf +ky9NfEBWMXrrpA9gzXrzvsMnjgM+pN0S734edAY8PzHyHHuRMSG08NBsl9Tpl7Ik +Vh5WwzW9iAUPWxAaZOHHgjD5Mq2eUCzneAXQMbFamIp1TpBcahQq4FJHgmDmHtqB +sfsUC1rxn9KVuj7QG9YVHaO+htXbD8BJZLsuUBlL0iT43R4HVtA4oJVwIHaM190e +3p9xxCPvgxNcoyQVTSlAPGrEqdi3pkSlDfTgnXceQHAm/NrZNuR55LU/vJtlvrsR +ls/bxig5OgjOR1tTWsWZ/l2p3e9M1MalrQLmjAcSHm8D0W+go/MpvRLHUKKwf4ip +mXeascClOS5cfGniLLDqN2qk4Vrh9VDlg++luyqI54zb/W1elxmofmZ1a3Hqv7HH +b6D0jqTsNFFbjCYDcKF31QESVwA12yPeDooomf2xEG9L/zgtYE4snOtnta1J7ksf +rK/7DZBaZmBwXarNeNQk7shBoJMBkpxqnvy5JMWzFYJ+vq6VK+uxwNrjAWALXmms +hFZhvnEX/h0TD/7Gh0Xp/jKgGg0TpJRVcaUWi7rKibCyx/yP2FS1k2Kdzs9Z+z0Y +zirLNRWCXf9UIltxUvu3yf5gmwBBZPCqKuy2QkPOiWaByIufOVQDJdMWNY6E0F/6 +MBr1mmz0DlP5OlvRHA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root CA" +# Serial: 17154717934120587862167794914071425081 +# MD5 Fingerprint: 87:ce:0b:7b:2a:0e:49:00:e1:58:71:9b:37:a8:93:72 +# SHA1 Fingerprint: 05:63:b8:63:0d:62:d7:5a:bb:c8:ab:1e:4b:df:b5:a8:99:b2:4d:43 +# SHA256 Fingerprint: 3e:90:99:b5:01:5e:8f:48:6c:00:bc:ea:9d:11:1e:e7:21:fa:ba:35:5a:89:bc:f1:df:69:56:1e:3d:c6:32:5c +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root CA" +# Serial: 10944719598952040374951832963794454346 +# MD5 Fingerprint: 79:e4:a9:84:0d:7d:3a:96:d7:c0:4f:e2:43:4c:89:2e +# SHA1 Fingerprint: a8:98:5d:3a:65:e5:e5:c4:b2:d7:d6:6d:40:c6:dd:2f:b1:9c:54:36 +# SHA256 Fingerprint: 43:48:a0:e9:44:4c:78:cb:26:5e:05:8d:5e:89:44:b4:d8:4f:96:62:bd:26:db:25:7f:89:34:a4:43:c7:01:61 +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert High Assurance EV Root CA" +# Serial: 3553400076410547919724730734378100087 +# MD5 Fingerprint: d4:74:de:57:5c:39:b2:d3:9c:85:83:c5:c0:65:49:8a +# SHA1 Fingerprint: 5f:b7:ee:06:33:e2:59:db:ad:0c:4c:9a:e6:d3:8f:1a:61:c7:dc:25 +# SHA256 Fingerprint: 74:31:e5:f4:c3:c1:ce:46:90:77:4f:0b:61:e0:54:40:88:3b:a9:a0:1e:d0:0b:a6:ab:d7:80:6e:d3:b1:18:cf +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- + +# Issuer: CN=Class 2 Primary CA O=Certplus +# Subject: CN=Class 2 Primary CA O=Certplus +# Label: "Certplus Class 2 Primary CA" +# Serial: 177770208045934040241468760488327595043 +# MD5 Fingerprint: 88:2c:8c:52:b8:a2:3c:f3:f7:bb:03:ea:ae:ac:42:0b +# SHA1 Fingerprint: 74:20:74:41:72:9c:dd:92:ec:79:31:d8:23:10:8d:c2:81:92:e2:bb +# SHA256 Fingerprint: 0f:99:3c:8a:ef:97:ba:af:56:87:14:0e:d5:9a:d1:82:1b:b4:af:ac:f0:aa:9a:58:b5:d5:7a:33:8a:3a:fb:cb +-----BEGIN CERTIFICATE----- +MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAw +PTELMAkGA1UEBhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFz +cyAyIFByaW1hcnkgQ0EwHhcNOTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9 +MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2VydHBsdXMxGzAZBgNVBAMTEkNsYXNz +IDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxQ +ltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR5aiR +VhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyL +kcAbmXuZVg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCd +EgETjdyAYveVqUSISnFOYFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yas +H7WLO7dDWWuwJKZtkIvEcupdM5i3y95ee++U8Rs+yskhwcWYAqqi9lt3m/V+llU0 +HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRMECDAGAQH/AgEKMAsGA1Ud +DwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJYIZIAYb4 +QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMu +Y29tL0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/ +AN9WM2K191EBkOvDP9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8 +yfFC82x/xXp8HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR +FcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+7UCmnYR0ObncHoUW2ikbhiMA +ybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW//1IMwrh3KWB +kJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7 +l7+ijrRU +-----END CERTIFICATE----- + +# Issuer: CN=DST Root CA X3 O=Digital Signature Trust Co. +# Subject: CN=DST Root CA X3 O=Digital Signature Trust Co. +# Label: "DST Root CA X3" +# Serial: 91299735575339953335919266965803778155 +# MD5 Fingerprint: 41:03:52:dc:0f:f7:50:1b:16:f0:02:8e:ba:6f:45:c5 +# SHA1 Fingerprint: da:c9:02:4f:54:d8:f6:df:94:93:5f:b1:73:26:38:ca:6a:d7:7c:13 +# SHA256 Fingerprint: 06:87:26:03:31:a7:24:03:d9:09:f1:05:e6:9b:cf:0d:32:e1:bd:24:93:ff:c6:d9:20:6d:11:bc:d6:77:07:39 +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow +PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD +Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O +rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq +OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b +xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw +7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD +aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG +SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 +ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr +AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz +R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 +JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo +Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- + +# Issuer: CN=DST ACES CA X6 O=Digital Signature Trust OU=DST ACES +# Subject: CN=DST ACES CA X6 O=Digital Signature Trust OU=DST ACES +# Label: "DST ACES CA X6" +# Serial: 17771143917277623872238992636097467865 +# MD5 Fingerprint: 21:d8:4c:82:2b:99:09:33:a2:eb:14:24:8d:8e:5f:e8 +# SHA1 Fingerprint: 40:54:da:6f:1c:3f:40:74:ac:ed:0f:ec:cd:db:79:d1:53:fb:90:1d +# SHA256 Fingerprint: 76:7c:95:5a:76:41:2c:89:af:68:8e:90:a1:c7:0f:55:6c:fd:6b:60:25:db:ea:10:41:6d:7e:b6:83:1f:8c:40 +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIQDV6ZCtadt3js2AdWO4YV2TANBgkqhkiG9w0BAQUFADBb +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3Qx +ETAPBgNVBAsTCERTVCBBQ0VTMRcwFQYDVQQDEw5EU1QgQUNFUyBDQSBYNjAeFw0w +MzExMjAyMTE5NThaFw0xNzExMjAyMTE5NThaMFsxCzAJBgNVBAYTAlVTMSAwHgYD +VQQKExdEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdDERMA8GA1UECxMIRFNUIEFDRVMx +FzAVBgNVBAMTDkRTVCBBQ0VTIENBIFg2MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAuT31LMmU3HWKlV1j6IR3dma5WZFcRt2SPp/5DgO0PWGSvSMmtWPu +ktKe1jzIDZBfZIGxqAgNTNj50wUoUrQBJcWVHAx+PhCEdc/BGZFjz+iokYi5Q1K7 +gLFViYsx+tC3dr5BPTCapCIlF3PoHuLTrCq9Wzgh1SpL11V94zpVvddtawJXa+ZH +fAjIgrrep4c9oW24MFbCswKBXy314powGCi4ZtPLAZZv6opFVdbgnf9nKxcCpk4a +ahELfrd755jWjHZvwTvbUJN+5dCOHze4vbrGn2zpfDPyMjwmR/onJALJfh1biEIT +ajV8fTXpLmaRcpPVMibEdPVTo7NdmvYJywIDAQABo4HIMIHFMA8GA1UdEwEB/wQF +MAMBAf8wDgYDVR0PAQH/BAQDAgHGMB8GA1UdEQQYMBaBFHBraS1vcHNAdHJ1c3Rk +c3QuY29tMGIGA1UdIARbMFkwVwYKYIZIAWUDAgEBATBJMEcGCCsGAQUFBwIBFjto +dHRwOi8vd3d3LnRydXN0ZHN0LmNvbS9jZXJ0aWZpY2F0ZXMvcG9saWN5L0FDRVMt +aW5kZXguaHRtbDAdBgNVHQ4EFgQUCXIGThhDD+XWzMNqizF7eI+og7gwDQYJKoZI +hvcNAQEFBQADggEBAKPYjtay284F5zLNAdMEA+V25FYrnJmQ6AgwbN99Pe7lv7Uk +QIRJ4dEorsTCOlMwiPH1d25Ryvr/ma8kXxug/fKshMrfqfBfBC6tFr8hlxCBPeP/ +h40y3JTlR4peahPJlJU90u7INJXQgNStMgiAVDzgvVJT11J8smk/f3rPanTK+gQq +nExaBqXpIK1FZg9p8d2/6eMyi/rgwYZNcjwu2JN4Cir42NInPRmJX1p7ijvMDNpR +rscL9yuwNwXsvFcj4jjSm2jzVhKIT0J8uDHEtdvkyCE06UgRNe76x5JXxZ805Mf2 +9w4LTJxoeHtxMcfrHuBnQfO3oKfN5XozNmr6mis= +-----END CERTIFICATE----- + +# Issuer: CN=SwissSign Gold CA - G2 O=SwissSign AG +# Subject: CN=SwissSign Gold CA - G2 O=SwissSign AG +# Label: "SwissSign Gold CA - G2" +# Serial: 13492815561806991280 +# MD5 Fingerprint: 24:77:d9:a8:91:d1:3b:fa:88:2d:c2:ff:f8:cd:33:93 +# SHA1 Fingerprint: d8:c5:38:8a:b7:30:1b:1b:6e:d4:7a:e6:45:25:3a:6f:9f:1a:27:61 +# SHA256 Fingerprint: 62:dd:0b:e9:b9:f5:0a:16:3e:a0:f8:e7:5c:05:3b:1e:ca:57:ea:55:c8:68:8f:64:7c:68:81:f2:c8:35:7b:95 +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln +biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF +MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT +d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 +76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ +bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c +6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE +emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd +MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt +MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y +MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y +FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi +aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM +gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB +qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 +lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn +8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 +45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO +UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 +O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC +bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv +GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a +77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC +hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 +92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp +Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w +ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt +Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- + +# Issuer: CN=SwissSign Silver CA - G2 O=SwissSign AG +# Subject: CN=SwissSign Silver CA - G2 O=SwissSign AG +# Label: "SwissSign Silver CA - G2" +# Serial: 5700383053117599563 +# MD5 Fingerprint: e0:06:a1:c9:7d:cf:c9:fc:0d:c0:56:75:96:d8:62:13 +# SHA1 Fingerprint: 9b:aa:e5:9f:56:ee:21:cb:43:5a:be:25:93:df:a7:f0:40:d1:1d:cb +# SHA256 Fingerprint: be:6c:4d:a2:bb:b9:ba:59:b6:f3:93:97:68:37:42:46:c3:c0:05:99:3f:a9:8f:02:0d:1d:ed:be:d4:8a:81:d5 +-----BEGIN CERTIFICATE----- +MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UE +BhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWdu +IFNpbHZlciBDQSAtIEcyMB4XDTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0Nlow +RzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMY +U3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644N0Mv +Fz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7br +YT7QbNHm+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieF +nbAVlDLaYQ1HTWBCrpJH6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH +6ATK72oxh9TAtvmUcXtnZLi2kUpCe2UuMGoM9ZDulebyzYLs2aFK7PayS+VFheZt +eJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5hqAaEuSh6XzjZG6k4sIN/ +c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5FZGkECwJ +MoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRH +HTBsROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTf +jNFusB3hB48IHpmccelM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb6 +5i/4z3GcRm25xBWNOHkDRUjvxF3XCO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOB +rDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +F6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRBtjpbO8tFnb0c +wpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 +cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIB +AHPGgeAn0i0P4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShp +WJHckRE1qTodvBqlYJ7YH39FkWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9 +xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L3XWgwF15kIwb4FDm3jH+mHtwX6WQ +2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx/uNncqCxv1yL5PqZ +IseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFaDGi8 +aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2X +em1ZqSqPe97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQR +dAtq/gsD/KNVV4n+SsuuWxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/ +OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJDIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+ +hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy +tGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Primary Certification Authority O=GeoTrust Inc. +# Subject: CN=GeoTrust Primary Certification Authority O=GeoTrust Inc. +# Label: "GeoTrust Primary Certification Authority" +# Serial: 32798226551256963324313806436981982369 +# MD5 Fingerprint: 02:26:c3:01:5e:08:30:37:43:a9:d0:7d:cf:37:e6:bf +# SHA1 Fingerprint: 32:3c:11:8e:1b:f7:b8:b6:52:54:e2:e2:10:0d:d6:02:90:37:f0:96 +# SHA256 Fingerprint: 37:d5:10:06:c5:12:ea:ab:62:64:21:f1:ec:8c:92:01:3f:c5:f8:2a:e9:8e:e5:33:eb:46:19:b8:de:b4:d0:6c +-----BEGIN CERTIFICATE----- +MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY +MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo +R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx +MjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK +Ew1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9 +AWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA +ZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0 +7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W +kBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI +mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ +KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1 +6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl +4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K +oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj +UjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU +AT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= +-----END CERTIFICATE----- + +# Issuer: CN=thawte Primary Root CA O=thawte, Inc. OU=Certification Services Division/(c) 2006 thawte, Inc. - For authorized use only +# Subject: CN=thawte Primary Root CA O=thawte, Inc. OU=Certification Services Division/(c) 2006 thawte, Inc. - For authorized use only +# Label: "thawte Primary Root CA" +# Serial: 69529181992039203566298953787712940909 +# MD5 Fingerprint: 8c:ca:dc:0b:22:ce:f5:be:72:ac:41:1a:11:a8:d8:12 +# SHA1 Fingerprint: 91:c6:d6:ee:3e:8a:c8:63:84:e5:48:c2:99:29:5c:75:6c:81:7b:81 +# SHA256 Fingerprint: 8d:72:2f:81:a9:c1:13:c0:79:1d:f1:36:a2:96:6d:b2:6c:95:0a:97:1d:b4:6b:41:99:f4:ea:54:b7:8b:fb:9f +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB +qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV +BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw +NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j +LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG +A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs +W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta +3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk +6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6 +Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J +NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP +r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU +DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz +YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX +xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2 +/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/ +LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7 +jVaMaA== +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G5 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2006 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G5 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2006 VeriSign, Inc. - For authorized use only +# Label: "VeriSign Class 3 Public Primary Certification Authority - G5" +# Serial: 33037644167568058970164719475676101450 +# MD5 Fingerprint: cb:17:e4:31:67:3e:e2:09:fe:45:57:93:f3:0a:fa:1c +# SHA1 Fingerprint: 4e:b6:d5:78:49:9b:1c:cf:5f:58:1e:ad:56:be:3d:9b:67:44:a5:e5 +# SHA256 Fingerprint: 9a:cf:ab:7e:43:c8:d8:80:d0:6b:26:2a:94:de:ee:e4:b4:65:99:89:c3:d0:ca:f1:9b:af:64:05:e4:1a:b7:df +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 +nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex +t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz +SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG +BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ +rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ +NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E +BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH +BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv +MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE +p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y +5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK +WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ +4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N +hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- + +# Issuer: CN=SecureTrust CA O=SecureTrust Corporation +# Subject: CN=SecureTrust CA O=SecureTrust Corporation +# Label: "SecureTrust CA" +# Serial: 17199774589125277788362757014266862032 +# MD5 Fingerprint: dc:32:c3:a7:6d:25:57:c7:68:09:9d:ea:2d:a9:a2:d1 +# SHA1 Fingerprint: 87:82:c6:c3:04:35:3b:cf:d2:96:92:d2:59:3e:7d:44:d9:34:ff:11 +# SHA256 Fingerprint: f1:c1:b5:0a:e5:a2:0d:d8:03:0e:c9:f6:bc:24:82:3d:d3:67:b5:25:57:59:b4:e7:1b:61:fc:e9:f7:37:5d:73 +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz +MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv +cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz +Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO +0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao +wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj +7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS +8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT +BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg +JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 +6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ +3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm +D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS +CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- + +# Issuer: CN=Secure Global CA O=SecureTrust Corporation +# Subject: CN=Secure Global CA O=SecureTrust Corporation +# Label: "Secure Global CA" +# Serial: 9751836167731051554232119481456978597 +# MD5 Fingerprint: cf:f4:27:0d:d4:ed:dc:65:16:49:6d:3d:da:bf:6e:de +# SHA1 Fingerprint: 3a:44:73:5a:e5:81:90:1f:24:86:61:46:1e:3b:9c:c4:5f:f5:3a:1b +# SHA256 Fingerprint: 42:00:f5:04:3a:c8:59:0e:bb:52:7d:20:9e:d1:50:30:29:fb:cb:d4:1c:a1:b5:06:ec:27:f1:5a:de:7d:ac:69 +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx +MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg +Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ +iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa +/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ +jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI +HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 +sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w +gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw +KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG +AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L +URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO +H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm +I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY +iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- + +# Issuer: CN=COMODO Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO Certification Authority O=COMODO CA Limited +# Label: "COMODO Certification Authority" +# Serial: 104350513648249232941998508985834464573 +# MD5 Fingerprint: 5c:48:dc:f7:42:72:ec:56:94:6d:1c:cc:71:35:80:75 +# SHA1 Fingerprint: 66:31:bf:9e:f7:4f:9e:b6:c9:d5:a6:0c:ba:6a:be:d1:f7:bd:ef:7b +# SHA256 Fingerprint: 0c:2c:d6:3d:f7:80:6f:a3:99:ed:e8:09:11:6b:57:5b:f8:79:89:f0:65:18:f9:80:8c:86:05:03:17:8b:af:66 +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw +MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW +/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g +PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u +QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY +SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv +IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 +zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd +BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB +ZQ== +-----END CERTIFICATE----- + +# Issuer: CN=Network Solutions Certificate Authority O=Network Solutions L.L.C. +# Subject: CN=Network Solutions Certificate Authority O=Network Solutions L.L.C. +# Label: "Network Solutions Certificate Authority" +# Serial: 116697915152937497490437556386812487904 +# MD5 Fingerprint: d3:f3:a6:16:c0:fa:6b:1d:59:b1:2d:96:4d:0e:11:2e +# SHA1 Fingerprint: 74:f8:a3:c3:ef:e7:b3:90:06:4b:83:90:3c:21:64:60:20:e5:df:ce +# SHA256 Fingerprint: 15:f0:ba:00:a3:ac:7a:f3:ac:88:4c:07:2b:10:11:a0:77:bd:77:c0:97:f4:01:64:b2:f8:59:8a:bd:83:86:0c +-----BEGIN CERTIFICATE----- +MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi +MQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu +MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp +dHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV +UzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO +ZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz +c7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP +OCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl +mGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF +BgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4 +qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw +gZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwu +bmV0c29sc3NsLmNvbS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3Jp +dHkuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc8 +6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/ +h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH +/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv +wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN +pGxlaKFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey +-----END CERTIFICATE----- + +# Issuer: CN=WellsSecure Public Root Certificate Authority O=Wells Fargo WellsSecure OU=Wells Fargo Bank NA +# Subject: CN=WellsSecure Public Root Certificate Authority O=Wells Fargo WellsSecure OU=Wells Fargo Bank NA +# Label: "WellsSecure Public Root Certificate Authority" +# Serial: 1 +# MD5 Fingerprint: 15:ac:a5:c2:92:2d:79:bc:e8:7f:cb:67:ed:02:cf:36 +# SHA1 Fingerprint: e7:b4:f6:9d:61:ec:90:69:db:7e:90:a7:40:1a:3c:f4:7d:4f:e8:ee +# SHA256 Fingerprint: a7:12:72:ae:aa:a3:cf:e8:72:7f:7f:b3:9f:0f:b3:d1:e5:42:6e:90:60:b0:6e:e6:f1:3e:9a:3c:58:33:cd:43 +-----BEGIN CERTIFICATE----- +MIIEvTCCA6WgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBhTELMAkGA1UEBhMCVVMx +IDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxs +cyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9v +dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDcxMjEzMTcwNzU0WhcNMjIxMjE0 +MDAwNzU0WjCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdl +bGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQD +DC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDub7S9eeKPCCGeOARBJe+r +WxxTkqxtnt3CxC5FlAM1iGd0V+PfjLindo8796jE2yljDpFoNoqXjopxaAkH5OjU +Dk/41itMpBb570OYj7OeUt9tkTmPOL13i0Nj67eT/DBMHAGTthP796EfvyXhdDcs +HqRePGj4S78NuR4uNuip5Kf4D8uCdXw1LSLWwr8L87T8bJVhHlfXBIEyg1J55oNj +z7fLY4sR4r1e6/aN7ZVyKLSsEmLpSjPmgzKuBXWVvYSV2ypcm44uDLiBK0HmOFaf +SZtsdvqKXfcBeYF8wYNABf5x/Qw/zE5gCQ5lRxAvAcAFP4/4s0HvWkJ+We/Slwxl +AgMBAAGjggE0MIIBMDAPBgNVHRMBAf8EBTADAQH/MDkGA1UdHwQyMDAwLqAsoCqG +KGh0dHA6Ly9jcmwucGtpLndlbGxzZmFyZ28uY29tL3dzcHJjYS5jcmwwDgYDVR0P +AQH/BAQDAgHGMB0GA1UdDgQWBBQmlRkQ2eihl5H/3BnZtQQ+0nMKajCBsgYDVR0j +BIGqMIGngBQmlRkQ2eihl5H/3BnZtQQ+0nMKaqGBi6SBiDCBhTELMAkGA1UEBhMC +VVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNX +ZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMg +Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHmCAQEwDQYJKoZIhvcNAQEFBQADggEB +ALkVsUSRzCPIK0134/iaeycNzXK7mQDKfGYZUMbVmO2rvwNa5U3lHshPcZeG1eMd +/ZDJPHV3V3p9+N701NX3leZ0bh08rnyd2wIDBSxxSyU+B+NemvVmFymIGjifz6pB +A4SXa5M4esowRBskRDPQ5NHcKDj0E0M1NSljqHyita04pO2t/caaH/+Xc/77szWn +k4bGdpEA5qxRFsQnMlzbc9qlk1eOPm01JghZ1edE13YgY+esE2fDbbFwRnzVlhE9 +iW9dqKHrjQrawx0zbKPqZxmamX9LPYNRKh3KL4YMon4QLSvUFpULB6ouFJJJtylv +2G0xffX8oRAHh84vWdw+WNs= +-----END CERTIFICATE----- + +# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Label: "COMODO ECC Certification Authority" +# Serial: 41578283867086692638256921589707938090 +# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23 +# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11 +# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7 +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +# Issuer: CN=IGC/A O=PM/SGDN OU=DCSSI +# Subject: CN=IGC/A O=PM/SGDN OU=DCSSI +# Label: "IGC/A" +# Serial: 245102874772 +# MD5 Fingerprint: 0c:7f:dd:6a:f4:2a:b9:c8:9b:bd:20:7e:a9:db:5c:37 +# SHA1 Fingerprint: 60:d6:89:74:b5:c2:65:9e:8a:0f:c1:88:7c:88:d2:46:69:1b:18:2c +# SHA256 Fingerprint: b9:be:a7:86:0a:96:2e:a3:61:1d:ab:97:ab:6d:a3:e2:1c:10:68:b9:7d:55:57:5e:d0:e1:12:79:c1:1c:89:32 +-----BEGIN CERTIFICATE----- +MIIEAjCCAuqgAwIBAgIFORFFEJQwDQYJKoZIhvcNAQEFBQAwgYUxCzAJBgNVBAYT +AkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQ +TS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG +9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZyMB4XDTAyMTIxMzE0MjkyM1oXDTIw +MTAxNzE0MjkyMlowgYUxCzAJBgNVBAYTAkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAM +BgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEO +MAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2 +LmZyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsh/R0GLFMzvABIaI +s9z4iPf930Pfeo2aSVz2TqrMHLmh6yeJ8kbpO0px1R2OLc/mratjUMdUC24SyZA2 +xtgv2pGqaMVy/hcKshd+ebUyiHDKcMCWSo7kVc0dJ5S/znIq7Fz5cyD+vfcuiWe4 +u0dzEvfRNWk68gq5rv9GQkaiv6GFGvm/5P9JhfejcIYyHF2fYPepraX/z9E0+X1b +F8bc1g4oa8Ld8fUzaJ1O/Id8NhLWo4DoQw1VYZTqZDdH6nfK0LJYBcNdfrGoRpAx +Vs5wKpayMLh35nnAvSk7/ZR3TL0gzUEl4C7HG7vupARB0l2tEmqKm0f7yd1GQOGd +PDPQtQIDAQABo3cwdTAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBRjAVBgNV +HSAEDjAMMAoGCCqBegF5AQEBMB0GA1UdDgQWBBSjBS8YYFDCiQrdKyFP/45OqDAx +NjAfBgNVHSMEGDAWgBSjBS8YYFDCiQrdKyFP/45OqDAxNjANBgkqhkiG9w0BAQUF +AAOCAQEABdwm2Pp3FURo/C9mOnTgXeQp/wYHE4RKq89toB9RlPhJy3Q2FLwV3duJ +L92PoF189RLrn544pEfMs5bZvpwlqwN+Mw+VgQ39FuCIvjfwbF3QMZsyK10XZZOY +YLxuj7GoPB7ZHPOpJkL5ZB3C55L29B5aqhlSXa/oovdgoPaN8In1buAKBQGVyYsg +Crpa/JosPL3Dt8ldeCUFP1YUmwza+zpI/pdpXsoQhvdOlgQITeywvl3cO45Pwf2a +NjSaTFR+FwNIlQgRHAdvhQh+XU3Endv7rs6y0bO4g2wdsrN58dhwmX7wEwLOXt1R +0982gaEbeC9xs/FZTEYYKKuF0mBWWg== +-----END CERTIFICATE----- + +# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication EV RootCA1 +# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication EV RootCA1 +# Label: "Security Communication EV RootCA1" +# Serial: 0 +# MD5 Fingerprint: 22:2d:a6:01:ea:7c:0a:f7:f0:6c:56:43:3f:77:76:d3 +# SHA1 Fingerprint: fe:b8:c4:32:dc:f9:76:9a:ce:ae:3d:d8:90:8f:fd:28:86:65:64:7d +# SHA256 Fingerprint: a2:2d:ba:68:1e:97:37:6e:2d:39:7d:72:8a:ae:3a:9b:62:96:b9:fd:ba:60:bc:2e:11:f6:47:f2:c6:75:fb:37 +-----BEGIN CERTIFICATE----- +MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEqMCgGA1UECxMh +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBFViBSb290Q0ExMB4XDTA3MDYwNjAyMTIz +MloXDTM3MDYwNjAyMTIzMlowYDELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09N +IFRydXN0IFN5c3RlbXMgQ08uLExURC4xKjAoBgNVBAsTIVNlY3VyaXR5IENvbW11 +bmljYXRpb24gRVYgUm9vdENBMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBALx/7FebJOD+nLpCeamIivqA4PUHKUPqjgo0No0c+qe1OXj/l3X3L+SqawSE +RMqm4miO/VVQYg+kcQ7OBzgtQoVQrTyWb4vVog7P3kmJPdZkLjjlHmy1V4qe70gO +zXppFodEtZDkBp2uoQSXWHnvIEqCa4wiv+wfD+mEce3xDuS4GBPMVjZd0ZoeUWs5 +bmB2iDQL87PRsJ3KYeJkHcFGB7hj3R4zZbOOCVVSPbW9/wfrrWFVGCypaZhKqkDF +MxRldAD5kd6vA0jFQFTcD4SQaCDFkpbcLuUCRarAX1T4bepJz11sS6/vmsJWXMY1 +VkJqMF/Cq/biPT+zyRGPMUzXn0kCAwEAAaNCMEAwHQYDVR0OBBYEFDVK9U2vP9eC +OKyrcWUXdYydVZPmMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBBQUAA4IBAQCoh+ns+EBnXcPBZsdAS5f8hxOQWsTvoMpfi7ent/HW +tWS3irO4G8za+6xmiEHO6Pzk2x6Ipu0nUBsCMCRGef4Eh3CXQHPRwMFXGZpppSeZ +q51ihPZRwSzJIxXYKLerJRO1RuGGAv8mjMSIkh1W/hln8lXkgKNrnKt34VFxDSDb +EJrbvXZ5B3eZKK2aXtqxT0QsNY6llsf9g/BYxnnWmHyojf6GPgcWkuF75x3sM3Z+ +Qi5KhfmRiWiEA4Glm5q+4zfFVKtWOxgtQaQM+ELbmaDgcm+7XeEWT1MKZPlO9L9O +VL14bIjqv5wTJMJwaaJ/D8g8rQjJsJhAoyrniIPtd490 +-----END CERTIFICATE----- + +# Issuer: CN=OISTE WISeKey Global Root GA CA O=WISeKey OU=Copyright (c) 2005/OISTE Foundation Endorsed +# Subject: CN=OISTE WISeKey Global Root GA CA O=WISeKey OU=Copyright (c) 2005/OISTE Foundation Endorsed +# Label: "OISTE WISeKey Global Root GA CA" +# Serial: 86718877871133159090080555911823548314 +# MD5 Fingerprint: bc:6c:51:33:a7:e9:d3:66:63:54:15:72:1b:21:92:93 +# SHA1 Fingerprint: 59:22:a1:e1:5a:ea:16:35:21:f8:98:39:6a:46:46:b0:44:1b:0f:a9 +# SHA256 Fingerprint: 41:c9:23:86:6a:b4:ca:d6:b7:ad:57:80:81:58:2e:02:07:97:a6:cb:df:4f:ff:78:ce:83:96:b3:89:37:d7:f5 +-----BEGIN CERTIFICATE----- +MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCB +ijELMAkGA1UEBhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHly +aWdodCAoYykgMjAwNTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl +ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQSBDQTAeFw0w +NTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYDVQQGEwJDSDEQMA4G +A1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIwIAYD +VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBX +SVNlS2V5IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAy0+zAJs9Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxR +VVuuk+g3/ytr6dTqvirdqFEr12bDYVxgAsj1znJ7O7jyTmUIms2kahnBAbtzptf2 +w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbDd50kc3vkDIzh2TbhmYsF +mQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ/yxViJGg +4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t9 +4B3RLoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQw +EAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOx +SPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vImMMkQyh2I+3QZH4VFvbBsUfk2 +ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4+vg1YFkCExh8 +vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa +hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZi +Fj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ +/L7fCg0= +-----END CERTIFICATE----- + +# Issuer: CN=Microsec e-Szigno Root CA O=Microsec Ltd. OU=e-Szigno CA +# Subject: CN=Microsec e-Szigno Root CA O=Microsec Ltd. OU=e-Szigno CA +# Label: "Microsec e-Szigno Root CA" +# Serial: 272122594155480254301341951808045322001 +# MD5 Fingerprint: f0:96:b6:2f:c5:10:d5:67:8e:83:25:32:e8:5e:2e:e5 +# SHA1 Fingerprint: 23:88:c9:d3:71:cc:9e:96:3d:ff:7d:3c:a7:ce:fc:d6:25:ec:19:0d +# SHA256 Fingerprint: 32:7a:3d:76:1a:ba:de:a0:34:eb:99:84:06:27:5c:b1:a4:77:6e:fd:ae:2f:df:6d:01:68:ea:1c:4f:55:67:d0 +-----BEGIN CERTIFICATE----- +MIIHqDCCBpCgAwIBAgIRAMy4579OKRr9otxmpRwsDxEwDQYJKoZIhvcNAQEFBQAw +cjELMAkGA1UEBhMCSFUxETAPBgNVBAcTCEJ1ZGFwZXN0MRYwFAYDVQQKEw1NaWNy +b3NlYyBMdGQuMRQwEgYDVQQLEwtlLVN6aWdubyBDQTEiMCAGA1UEAxMZTWljcm9z +ZWMgZS1Temlnbm8gUm9vdCBDQTAeFw0wNTA0MDYxMjI4NDRaFw0xNzA0MDYxMjI4 +NDRaMHIxCzAJBgNVBAYTAkhVMREwDwYDVQQHEwhCdWRhcGVzdDEWMBQGA1UEChMN +TWljcm9zZWMgTHRkLjEUMBIGA1UECxMLZS1Temlnbm8gQ0ExIjAgBgNVBAMTGU1p +Y3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDtyADVgXvNOABHzNuEwSFpLHSQDCHZU4ftPkNEU6+r+ICbPHiN1I2u +uO/TEdyB5s87lozWbxXGd36hL+BfkrYn13aaHUM86tnsL+4582pnS4uCzyL4ZVX+ +LMsvfUh6PXX5qqAnu3jCBspRwn5mS6/NoqdNAoI/gqyFxuEPkEeZlApxcpMqyabA +vjxWTHOSJ/FrtfX9/DAFYJLG65Z+AZHCabEeHXtTRbjcQR/Ji3HWVBTji1R4P770 +Yjtb9aPs1ZJ04nQw7wHb4dSrmZsqa/i9phyGI0Jf7Enemotb9HI6QMVJPqW+jqpx +62z69Rrkav17fVVA71hu5tnVvCSrwe+3AgMBAAGjggQ3MIIEMzBnBggrBgEFBQcB +AQRbMFkwKAYIKwYBBQUHMAGGHGh0dHBzOi8vcmNhLmUtc3ppZ25vLmh1L29jc3Aw +LQYIKwYBBQUHMAKGIWh0dHA6Ly93d3cuZS1zemlnbm8uaHUvUm9vdENBLmNydDAP +BgNVHRMBAf8EBTADAQH/MIIBcwYDVR0gBIIBajCCAWYwggFiBgwrBgEEAYGoGAIB +AQEwggFQMCgGCCsGAQUFBwIBFhxodHRwOi8vd3d3LmUtc3ppZ25vLmh1L1NaU1ov +MIIBIgYIKwYBBQUHAgIwggEUHoIBEABBACAAdABhAG4A+gBzAO0AdAB2AOEAbgB5 +ACAA6QByAHQAZQBsAG0AZQB6AOkAcwDpAGgAZQB6ACAA6QBzACAAZQBsAGYAbwBn +AGEAZADhAHMA4QBoAG8AegAgAGEAIABTAHoAbwBsAGcA4QBsAHQAYQB0APMAIABT +AHoAbwBsAGcA4QBsAHQAYQB0AOEAcwBpACAAUwB6AGEAYgDhAGwAeQB6AGEAdABh +ACAAcwB6AGUAcgBpAG4AdAAgAGsAZQBsAGwAIABlAGwAagDhAHIAbgBpADoAIABo +AHQAdABwADoALwAvAHcAdwB3AC4AZQAtAHMAegBpAGcAbgBvAC4AaAB1AC8AUwBa +AFMAWgAvMIHIBgNVHR8EgcAwgb0wgbqggbeggbSGIWh0dHA6Ly93d3cuZS1zemln +bm8uaHUvUm9vdENBLmNybIaBjmxkYXA6Ly9sZGFwLmUtc3ppZ25vLmh1L0NOPU1p +Y3Jvc2VjJTIwZS1Temlnbm8lMjBSb290JTIwQ0EsT1U9ZS1Temlnbm8lMjBDQSxP +PU1pY3Jvc2VjJTIwTHRkLixMPUJ1ZGFwZXN0LEM9SFU/Y2VydGlmaWNhdGVSZXZv +Y2F0aW9uTGlzdDtiaW5hcnkwDgYDVR0PAQH/BAQDAgEGMIGWBgNVHREEgY4wgYuB +EGluZm9AZS1zemlnbm8uaHWkdzB1MSMwIQYDVQQDDBpNaWNyb3NlYyBlLVN6aWdu +w7MgUm9vdCBDQTEWMBQGA1UECwwNZS1TemlnbsOzIEhTWjEWMBQGA1UEChMNTWlj +cm9zZWMgS2Z0LjERMA8GA1UEBxMIQnVkYXBlc3QxCzAJBgNVBAYTAkhVMIGsBgNV +HSMEgaQwgaGAFMegSXUWYYTbMUuE0vE3QJDvTtz3oXakdDByMQswCQYDVQQGEwJI +VTERMA8GA1UEBxMIQnVkYXBlc3QxFjAUBgNVBAoTDU1pY3Jvc2VjIEx0ZC4xFDAS +BgNVBAsTC2UtU3ppZ25vIENBMSIwIAYDVQQDExlNaWNyb3NlYyBlLVN6aWdubyBS +b290IENBghEAzLjnv04pGv2i3GalHCwPETAdBgNVHQ4EFgQUx6BJdRZhhNsxS4TS +8TdAkO9O3PcwDQYJKoZIhvcNAQEFBQADggEBANMTnGZjWS7KXHAM/IO8VbH0jgds +ZifOwTsgqRy7RlRw7lrMoHfqaEQn6/Ip3Xep1fvj1KcExJW4C+FEaGAHQzAxQmHl +7tnlJNUb3+FKG6qfx1/4ehHqE5MAyopYse7tDk2016g2JnzgOsHVV4Lxdbb9iV/a +86g4nzUGCM4ilb7N1fy+W955a9x6qWVmvrElWl/tftOsRm1M9DKHtCAE4Gx4sHfR +hUZLphK3dehKyVZs15KrnfVJONJPU+NVkBHbmJbGSfI+9J8b4PeI3CVimUTYc78/ +MPMMNz7UwiiAc7EBt51alhQBS6kRnSlqLtBdgcDPsiBDxwPgN05dCtxZICU= +-----END CERTIFICATE----- + +# Issuer: CN=Certigna O=Dhimyotis +# Subject: CN=Certigna O=Dhimyotis +# Label: "Certigna" +# Serial: 18364802974209362175 +# MD5 Fingerprint: ab:57:a6:5b:7d:42:82:19:b5:d8:58:26:28:5e:fd:ff +# SHA1 Fingerprint: b1:2e:13:63:45:86:a4:6f:1a:b2:60:68:37:58:2d:c4:ac:fd:94:97 +# SHA256 Fingerprint: e3:b6:a2:db:2e:d7:ce:48:84:2f:7a:c5:32:41:c7:b7:1d:54:14:4b:fb:40:c1:1f:3f:1d:0b:42:f5:ee:a1:2d +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV +BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X +DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ +BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 +QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny +gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw +zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q +130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 +JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw +ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT +AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj +AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG +9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h +bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc +fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu +HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w +t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- + +# Issuer: CN=Deutsche Telekom Root CA 2 O=Deutsche Telekom AG OU=T-TeleSec Trust Center +# Subject: CN=Deutsche Telekom Root CA 2 O=Deutsche Telekom AG OU=T-TeleSec Trust Center +# Label: "Deutsche Telekom Root CA 2" +# Serial: 38 +# MD5 Fingerprint: 74:01:4a:91:b1:08:c4:58:ce:47:cd:f0:dd:11:53:08 +# SHA1 Fingerprint: 85:a4:08:c0:9c:19:3e:5d:51:58:7d:cd:d6:13:30:fd:8c:de:37:bf +# SHA256 Fingerprint: b6:19:1a:50:d0:c3:97:7f:7d:a9:9b:cd:aa:c8:6a:22:7d:ae:b9:67:9e:c7:0b:a3:b0:c9:d9:22:71:c1:70:d3 +-----BEGIN CERTIFICATE----- +MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEc +MBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2Vj +IFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENB +IDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5MjM1OTAwWjBxMQswCQYDVQQGEwJE +RTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxl +U2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290 +IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEU +ha88EOQ5bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhC +QN/Po7qCWWqSG6wcmtoIKyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1Mjwr +rFDa1sPeg5TKqAyZMg4ISFZbavva4VhYAUlfckE8FQYBjl2tqriTtM2e66foai1S +NNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aKSe5TBY8ZTNXeWHmb0moc +QqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTVjlsB9WoH +txa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAP +BgNVHRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC +AQEAlGRZrTlk5ynrE/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756Abrsp +tJh6sTtU6zkXR34ajgv8HzFZMQSyzhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpa +IzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8rZ7/gFnkm0W09juwzTkZmDLl +6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4Gdyd1Lx+4ivn+ +xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU +Cm26OWMohpLzGITY+9HPBVZkVw== +-----END CERTIFICATE----- + +# Issuer: CN=Cybertrust Global Root O=Cybertrust, Inc +# Subject: CN=Cybertrust Global Root O=Cybertrust, Inc +# Label: "Cybertrust Global Root" +# Serial: 4835703278459682877484360 +# MD5 Fingerprint: 72:e4:4a:87:e3:69:40:80:77:ea:bc:e3:f4:ff:f0:e1 +# SHA1 Fingerprint: 5f:43:e5:b1:bf:f8:78:8c:ac:1c:c7:ca:4a:9a:c6:22:2b:cc:34:c6 +# SHA256 Fingerprint: 96:0a:df:00:63:e9:63:56:75:0c:29:65:dd:0a:08:67:da:0b:9c:bd:6e:77:71:4a:ea:fb:23:49:ab:39:3d:a3 +-----BEGIN CERTIFICATE----- +MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG +A1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh +bCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE +ChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBS +b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+Mi8vRRQZhP/8NN5 +7CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW0ozS +J8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2y +HLtgwEZLAfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iP +t3sMpTjr3kfb1V05/Iin89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNz +FtApD0mpSPCzqrdsxacwOUBdrsTiXSZT8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAY +XSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2MDSgMqAw +hi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3Js +MB8GA1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUA +A4IBAQBW7wojoFROlZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMj +Wqd8BfP9IjsO0QbE2zZMcwSO5bAi5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUx +XOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2hO0j9n0Hq0V+09+zv+mKts2o +omcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+TX3EJIrduPuoc +A06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW +WL1WMRJOEcgh4LMRkWXbtKaIOM5V +-----END CERTIFICATE----- + +# Issuer: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority +# Subject: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority +# Label: "ePKI Root Certification Authority" +# Serial: 28956088682735189655030529057352760477 +# MD5 Fingerprint: 1b:2e:00:ca:26:06:90:3d:ad:fe:6f:15:68:d3:6b:b3 +# SHA1 Fingerprint: 67:65:0d:f1:7e:8e:7e:5b:82:40:a4:f4:56:4b:cf:e2:3d:69:c6:f0 +# SHA256 Fingerprint: c0:a6:f4:dc:63:a2:4b:fd:cf:54:ef:2a:6a:08:2a:0a:72:de:35:80:3e:2f:f5:ff:52:7a:e5:d8:72:06:df:d5 +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw +IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL +SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH +SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh +ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X +DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 +TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ +fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA +sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU +WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS +nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH +dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip +NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC +AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF +MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB +uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl +PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP +JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ +gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 +j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 +5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB +o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS +/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z +Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE +W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D +hNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- + +# Issuer: CN=TÜBİTAK UEKAE Kök Sertifika Hizmet Sağlayıcısı - Sürüm 3 O=Türkiye Bilimsel ve Teknolojik Araştırma Kurumu - TÜBİTAK OU=Ulusal Elektronik ve Kriptoloji Araştırma Enstitüsü - UEKAE/Kamu Sertifikasyon Merkezi +# Subject: CN=TÜBİTAK UEKAE Kök Sertifika Hizmet Sağlayıcısı - Sürüm 3 O=Türkiye Bilimsel ve Teknolojik Araştırma Kurumu - TÜBİTAK OU=Ulusal Elektronik ve Kriptoloji Araştırma Enstitüsü - UEKAE/Kamu Sertifikasyon Merkezi +# Label: "T\xc3\x9c\x42\xC4\xB0TAK UEKAE K\xC3\xB6k Sertifika Hizmet Sa\xC4\x9Flay\xc4\xb1\x63\xc4\xb1s\xc4\xb1 - S\xC3\xBCr\xC3\xBCm 3" +# Serial: 17 +# MD5 Fingerprint: ed:41:f5:8c:50:c5:2b:9c:73:e6:ee:6c:eb:c2:a8:26 +# SHA1 Fingerprint: 1b:4b:39:61:26:27:6b:64:91:a2:68:6d:d7:02:43:21:2d:1f:1d:96 +# SHA256 Fingerprint: e4:c7:34:30:d7:a5:b5:09:25:df:43:37:0a:0d:21:6e:9a:79:b9:d6:db:83:73:a0:c6:9e:b1:cc:31:c7:c5:2a +-----BEGIN CERTIFICATE----- +MIIFFzCCA/+gAwIBAgIBETANBgkqhkiG9w0BAQUFADCCASsxCzAJBgNVBAYTAlRS +MRgwFgYDVQQHDA9HZWJ6ZSAtIEtvY2FlbGkxRzBFBgNVBAoMPlTDvHJraXllIEJp +bGltc2VsIHZlIFRla25vbG9qaWsgQXJhxZ90xLFybWEgS3VydW11IC0gVMOcQsSw +VEFLMUgwRgYDVQQLDD9VbHVzYWwgRWxla3Ryb25payB2ZSBLcmlwdG9sb2ppIEFy +YcWfdMSxcm1hIEVuc3RpdMO8c8O8IC0gVUVLQUUxIzAhBgNVBAsMGkthbXUgU2Vy +dGlmaWthc3lvbiBNZXJrZXppMUowSAYDVQQDDEFUw5xCxLBUQUsgVUVLQUUgS8O2 +ayBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsSAtIFPDvHLDvG0gMzAe +Fw0wNzA4MjQxMTM3MDdaFw0xNzA4MjExMTM3MDdaMIIBKzELMAkGA1UEBhMCVFIx +GDAWBgNVBAcMD0dlYnplIC0gS29jYWVsaTFHMEUGA1UECgw+VMO8cmtpeWUgQmls +aW1zZWwgdmUgVGVrbm9sb2ppayBBcmHFn3TEsXJtYSBLdXJ1bXUgLSBUw5xCxLBU +QUsxSDBGBgNVBAsMP1VsdXNhbCBFbGVrdHJvbmlrIHZlIEtyaXB0b2xvamkgQXJh +xZ90xLFybWEgRW5zdGl0w7xzw7wgLSBVRUtBRTEjMCEGA1UECwwaS2FtdSBTZXJ0 +aWZpa2FzeW9uIE1lcmtlemkxSjBIBgNVBAMMQVTDnELEsFRBSyBVRUtBRSBLw7Zr +IFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxIC0gU8O8csO8bSAzMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAim1L/xCIOsP2fpTo6iBkcK4h +gb46ezzb8R1Sf1n68yJMlaCQvEhOEav7t7WNeoMojCZG2E6VQIdhn8WebYGHV2yK +O7Rm6sxA/OOqbLLLAdsyv9Lrhc+hDVXDWzhXcLh1xnnRFDDtG1hba+818qEhTsXO +fJlfbLm4IpNQp81McGq+agV/E5wrHur+R84EpW+sky58K5+eeROR6Oqeyjh1jmKw +lZMq5d/pXpduIF9fhHpEORlAHLpVK/swsoHvhOPc7Jg4OQOFCKlUAwUp8MmPi+oL +hmUZEdPpCSPeaJMDyTYcIW7OjGbxmTDY17PDHfiBLqi9ggtm/oLL4eAagsNAgQID +AQABo0IwQDAdBgNVHQ4EFgQUvYiHyY/2pAoLquvF/pEjnatKijIwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAB18+kmP +NOm3JpIWmgV050vQbTlswyb2zrgxvMTfvCr4N5EY3ATIZJkrGG2AA1nJrvhY0D7t +wyOfaTyGOBye79oneNGEN3GKPEs5z35FBtYt2IpNeBLWrcLTy9LQQfMmNkqblWwM +7uXRQydmwYj3erMgbOqwaSvHIOgMA8RBBZniP+Rr+KCGgceExh/VS4ESshYhLBOh +gLJeDEoTniDYYkCrkOpkSi+sDQESeUWoL4cZaMjihccwsnX5OD+ywJO0a+IDRM5n +oN+J1q2MdqMTw5RhK2vZbMEHCiIHhWyFJEapvj+LeISCfiQMnf2BN+MlqO02TpUs +yZyQ2uypQjyttgI= +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 2 CA 1 O=Buypass AS-983163327 +# Subject: CN=Buypass Class 2 CA 1 O=Buypass AS-983163327 +# Label: "Buypass Class 2 CA 1" +# Serial: 1 +# MD5 Fingerprint: b8:08:9a:f0:03:cc:1b:0d:c8:6c:0b:76:a1:75:64:23 +# SHA1 Fingerprint: a0:a1:ab:90:c9:fc:84:7b:3b:12:61:e8:97:7d:5f:d3:22:61:d3:cc +# SHA256 Fingerprint: 0f:4e:9c:dd:26:4b:02:55:50:d1:70:80:63:40:21:4f:e9:44:34:c9:b0:2f:69:7e:c7:10:fc:5f:ea:fb:5e:38 +-----BEGIN CERTIFICATE----- +MIIDUzCCAjugAwIBAgIBATANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3Mg +Q2xhc3MgMiBDQSAxMB4XDTA2MTAxMzEwMjUwOVoXDTE2MTAxMzEwMjUwOVowSzEL +MAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MR0wGwYD +VQQDDBRCdXlwYXNzIENsYXNzIDIgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAIs8B0XY9t/mx8q6jUPFR42wWsE425KEHK8T1A9vNkYgxC7McXA0 +ojTTNy7Y3Tp3L8DrKehc0rWpkTSHIln+zNvnma+WwajHQN2lFYxuyHyXA8vmIPLX +l18xoS830r7uvqmtqEyeIWZDO6i88wmjONVZJMHCR3axiFyCO7srpgTXjAePzdVB +HfCuuCkslFJgNJQ72uA40Z0zPhX0kzLFANq1KWYOOngPIVJfAuWSeyXTkh4vFZ2B +5J2O6O+JzhRMVB0cgRJNcKi+EAUXfh/RuFdV7c27UsKwHnjCTTZoy1YmwVLBvXb3 +WNVyfh9EdrsAiR0WnVE1703CVu9r4Iw7DekCAwEAAaNCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUP42aWYv8e3uco684sDntkHGA1sgwDgYDVR0PAQH/BAQD +AgEGMA0GCSqGSIb3DQEBBQUAA4IBAQAVGn4TirnoB6NLJzKyQJHyIdFkhb5jatLP +gcIV1Xp+DCmsNx4cfHZSldq1fyOhKXdlyTKdqC5Wq2B2zha0jX94wNWZUYN/Xtm+ +DKhQ7SLHrQVMdvvt7h5HZPb3J31cKA9FxVxiXqaakZG3Uxcu3K1gnZZkOb1naLKu +BctN518fV4bVIJwo+28TOPX2EZL2fZleHwzoq0QkKXJAPTZSr4xYkHPB7GEseaHs +h7U/2k3ZIQAw3pDaDtMaSKk+hQsUi4y8QZ5q9w5wwDX3OaJdZtB7WZ+oRxKaJyOk +LY4ng5IgodcVf/EuGO70SH8vf/GhGLWhC5SgYiAynB321O+/TIho +-----END CERTIFICATE----- + +# Issuer: CN=EBG Elektronik Sertifika Hizmet Sağlayıcısı O=EBG Bilişim Teknolojileri ve Hizmetleri A.Ş. +# Subject: CN=EBG Elektronik Sertifika Hizmet Sağlayıcısı O=EBG Bilişim Teknolojileri ve Hizmetleri A.Ş. +# Label: "EBG Elektronik Sertifika Hizmet Sa\xC4\x9Flay\xc4\xb1\x63\xc4\xb1s\xc4\xb1" +# Serial: 5525761995591021570 +# MD5 Fingerprint: 2c:20:26:9d:cb:1a:4a:00:85:b5:b7:5a:ae:c2:01:37 +# SHA1 Fingerprint: 8c:96:ba:eb:dd:2b:07:07:48:ee:30:32:66:a0:f3:98:6e:7c:ae:58 +# SHA256 Fingerprint: 35:ae:5b:dd:d8:f7:ae:63:5c:ff:ba:56:82:a8:f0:0b:95:f4:84:62:c7:10:8e:e9:a0:e5:29:2b:07:4a:af:b2 +-----BEGIN CERTIFICATE----- +MIIF5zCCA8+gAwIBAgIITK9zQhyOdAIwDQYJKoZIhvcNAQEFBQAwgYAxODA2BgNV +BAMML0VCRyBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx +c8SxMTcwNQYDVQQKDC5FQkcgQmlsacWfaW0gVGVrbm9sb2ppbGVyaSB2ZSBIaXpt +ZXRsZXJpIEEuxZ4uMQswCQYDVQQGEwJUUjAeFw0wNjA4MTcwMDIxMDlaFw0xNjA4 +MTQwMDMxMDlaMIGAMTgwNgYDVQQDDC9FQkcgRWxla3Ryb25payBTZXJ0aWZpa2Eg +SGl6bWV0IFNhxJ9sYXnEsWPEsXPEsTE3MDUGA1UECgwuRUJHIEJpbGnFn2ltIFRl +a25vbG9qaWxlcmkgdmUgSGl6bWV0bGVyaSBBLsWeLjELMAkGA1UEBhMCVFIwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDuoIRh0DpqZhAy2DE4f6en5f2h +4fuXd7hxlugTlkaDT7byX3JWbhNgpQGR4lvFzVcfd2NR/y8927k/qqk153nQ9dAk +tiHq6yOU/im/+4mRDGSaBUorzAzu8T2bgmmkTPiab+ci2hC6X5L8GCcKqKpE+i4s +tPtGmggDg3KriORqcsnlZR9uKg+ds+g75AxuetpX/dfreYteIAbTdgtsApWjluTL +dlHRKJ2hGvxEok3MenaoDT2/F08iiFD9rrbskFBKW5+VQarKD7JK/oCZTqNGFav4 +c0JqwmZ2sQomFd2TkuzbqV9UIlKRcF0T6kjsbgNs2d1s/OsNA/+mgxKb8amTD8Um +TDGyY5lhcucqZJnSuOl14nypqZoaqsNW2xCaPINStnuWt6yHd6i58mcLlEOzrz5z ++kI2sSXFCjEmN1ZnuqMLfdb3ic1nobc6HmZP9qBVFCVMLDMNpkGMvQQxahByCp0O +Lna9XvNRiYuoP1Vzv9s6xiQFlpJIqkuNKgPlV5EQ9GooFW5Hd4RcUXSfGenmHmMW +OeMRFeNYGkS9y8RsZteEBt8w9DeiQyJ50hBs37vmExH8nYQKE3vwO9D8owrXieqW +fo1IhR5kX9tUoqzVegJ5a9KK8GfaZXINFHDk6Y54jzJ0fFfy1tb0Nokb+Clsi7n2 +l9GkLqq+CxnCRelwXQIDAJ3Zo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB +/wQEAwIBBjAdBgNVHQ4EFgQU587GT/wWZ5b6SqMHwQSny2re2kcwHwYDVR0jBBgw +FoAU587GT/wWZ5b6SqMHwQSny2re2kcwDQYJKoZIhvcNAQEFBQADggIBAJuYml2+ +8ygjdsZs93/mQJ7ANtyVDR2tFcU22NU57/IeIl6zgrRdu0waypIN30ckHrMk2pGI +6YNw3ZPX6bqz3xZaPt7gyPvT/Wwp+BVGoGgmzJNSroIBk5DKd8pNSe/iWtkqvTDO +TLKBtjDOWU/aWR1qeqRFsIImgYZ29fUQALjuswnoT4cCB64kXPBfrAowzIpAoHME +wfuJJPaaHFy3PApnNgUIMbOv2AFoKuB4j3TeuFGkjGwgPaL7s9QJ/XvCgKqTbCmY +Iai7FvOpEl90tYeY8pUm3zTvilORiF0alKM/fCL414i6poyWqD1SNGKfAB5UVUJn +xk1Gj7sURT0KlhaOEKGXmdXTMIXM3rRyt7yKPBgpaP3ccQfuJDlq+u2lrDgv+R4Q +DgZxGhBM/nV+/x5XOULK1+EVoVZVWRvRo68R2E7DpSvvkL/A7IITW43WciyTTo9q +Kd+FPNMN4KIYEsxVL0e3p5sC/kH2iExt2qkBR4NkJ2IQgtYSe14DHzSpyZH+r11t +hie3I6p1GMog57AP14kOpmciY/SDQSsGS7tY1dHXt7kQY9iJSrSq3RZj9W6+YKH4 +7ejWkE8axsWgKdOnIaj1Wjz3x0miIZpKlVIglnKaZsv30oZDfCK+lvm9AahH3eU7 +QPl1K5srRmSGjR70j/sHd9DqSaIcjVIUpgqT +-----END CERTIFICATE----- + +# Issuer: O=certSIGN OU=certSIGN ROOT CA +# Subject: O=certSIGN OU=certSIGN ROOT CA +# Label: "certSIGN ROOT CA" +# Serial: 35210227249154 +# MD5 Fingerprint: 18:98:c0:d6:e9:3a:fc:f9:b0:f5:0c:f7:4b:01:44:17 +# SHA1 Fingerprint: fa:b7:ee:36:97:26:62:fb:2d:b0:2a:f6:bf:03:fd:e8:7c:4b:2f:9b +# SHA256 Fingerprint: ea:a9:62:c4:fa:4a:6b:af:eb:e4:15:19:6d:35:1c:cd:88:8d:4f:53:f3:fa:8a:e6:d7:c4:66:a9:4e:60:42:bb +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT +AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD +QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP +MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do +0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ +UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d +RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ +OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv +JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C +AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O +BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ +LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY +MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ +44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I +Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw +i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN +9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- + +# Issuer: CN=CNNIC ROOT O=CNNIC +# Subject: CN=CNNIC ROOT O=CNNIC +# Label: "CNNIC ROOT" +# Serial: 1228079105 +# MD5 Fingerprint: 21:bc:82:ab:49:c4:13:3b:4b:b2:2b:5c:6b:90:9c:19 +# SHA1 Fingerprint: 8b:af:4c:9b:1d:f0:2a:92:f7:da:12:8e:b9:1b:ac:f4:98:60:4b:6f +# SHA256 Fingerprint: e2:83:93:77:3d:a8:45:a6:79:f2:08:0c:c7:fb:44:a3:b7:a1:c3:79:2c:b7:eb:77:29:fd:cb:6a:8d:99:ae:a7 +-----BEGIN CERTIFICATE----- +MIIDVTCCAj2gAwIBAgIESTMAATANBgkqhkiG9w0BAQUFADAyMQswCQYDVQQGEwJD +TjEOMAwGA1UEChMFQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwHhcNMDcwNDE2 +MDcwOTE0WhcNMjcwNDE2MDcwOTE0WjAyMQswCQYDVQQGEwJDTjEOMAwGA1UEChMF +Q05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDTNfc/c3et6FtzF8LRb+1VvG7q6KR5smzDo+/hn7E7SIX1mlwh +IhAsxYLO2uOabjfhhyzcuQxauohV3/2q2x8x6gHx3zkBwRP9SFIhxFXf2tizVHa6 +dLG3fdfA6PZZxU3Iva0fFNrfWEQlMhkqx35+jq44sDB7R3IJMfAw28Mbdim7aXZO +V/kbZKKTVrdvmW7bCgScEeOAH8tjlBAKqeFkgjH5jCftppkA9nCTGPihNIaj3XrC +GHn2emU1z5DrvTOTn1OrczvmmzQgLx3vqR1jGqCA2wMv+SYahtKNu6m+UjqHZ0gN +v7Sg2Ca+I19zN38m5pIEo3/PIKe38zrKy5nLAgMBAAGjczBxMBEGCWCGSAGG+EIB +AQQEAwIABzAfBgNVHSMEGDAWgBRl8jGtKvf33VKWCscCwQ7vptU7ETAPBgNVHRMB +Af8EBTADAQH/MAsGA1UdDwQEAwIB/jAdBgNVHQ4EFgQUZfIxrSr3991SlgrHAsEO +76bVOxEwDQYJKoZIhvcNAQEFBQADggEBAEs17szkrr/Dbq2flTtLP1se31cpolnK +OOK5Gv+e5m4y3R6u6jW39ZORTtpC4cMXYFDy0VwmuYK36m3knITnA3kXr5g9lNvH +ugDnuL8BV8F3RTIMO/G0HAiw/VGgod2aHRM2mm23xzy54cXZF/qD1T0VoDy7Hgvi +yJA/qIYM/PmLXoXLT1tLYhFHxUV8BS9BsZ4QaRuZluBVeftOhpm4lNqGOGqTo+fL +buXf6iFViZx9fX+Y9QCJ7uOEwFyWtcVG6kbghVW2G8kS1sHNzYDzAgE8yGnLRUhj +2JTQ7IUOO04RZfSCjKY9ri4ilAnIXOo8gV0WKgOXFlUJ24pBgp5mmxE= +-----END CERTIFICATE----- + +# Issuer: O=Japanese Government OU=ApplicationCA +# Subject: O=Japanese Government OU=ApplicationCA +# Label: "ApplicationCA - Japanese Government" +# Serial: 49 +# MD5 Fingerprint: 7e:23:4e:5b:a7:a5:b4:25:e9:00:07:74:11:62:ae:d6 +# SHA1 Fingerprint: 7f:8a:b0:cf:d0:51:87:6a:66:f3:36:0f:47:c8:8d:8c:d3:35:fc:74 +# SHA256 Fingerprint: 2d:47:43:7d:e1:79:51:21:5a:12:f3:c5:8e:51:c7:29:a5:80:26:ef:1f:cc:0a:5f:b3:d9:dc:01:2f:60:0d:19 +-----BEGIN CERTIFICATE----- +MIIDoDCCAoigAwIBAgIBMTANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJKUDEc +MBoGA1UEChMTSmFwYW5lc2UgR292ZXJubWVudDEWMBQGA1UECxMNQXBwbGljYXRp +b25DQTAeFw0wNzEyMTIxNTAwMDBaFw0xNzEyMTIxNTAwMDBaMEMxCzAJBgNVBAYT +AkpQMRwwGgYDVQQKExNKYXBhbmVzZSBHb3Zlcm5tZW50MRYwFAYDVQQLEw1BcHBs +aWNhdGlvbkNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp23gdE6H +j6UG3mii24aZS2QNcfAKBZuOquHMLtJqO8F6tJdhjYq+xpqcBrSGUeQ3DnR4fl+K +f5Sk10cI/VBaVuRorChzoHvpfxiSQE8tnfWuREhzNgaeZCw7NCPbXCbkcXmP1G55 +IrmTwcrNwVbtiGrXoDkhBFcsovW8R0FPXjQilbUfKW1eSvNNcr5BViCH/OlQR9cw +FO5cjFW6WY2H/CPek9AEjP3vbb3QesmlOmpyM8ZKDQUXKi17safY1vC+9D/qDiht +QWEjdnjDuGWk81quzMKq2edY3rZ+nYVunyoKb58DKTCXKB28t89UKU5RMfkntigm +/qJj5kEW8DOYRwIDAQABo4GeMIGbMB0GA1UdDgQWBBRUWssmP3HMlEYNllPqa0jQ +k/5CdTAOBgNVHQ8BAf8EBAMCAQYwWQYDVR0RBFIwUKROMEwxCzAJBgNVBAYTAkpQ +MRgwFgYDVQQKDA/ml6XmnKzlm73mlL/lupwxIzAhBgNVBAsMGuOCouODl+ODquOC +seODvOOCt+ODp+ODs0NBMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADlqRHZ3ODrso2dGD/mLBqj7apAxzn7s2tGJfHrrLgy9mTLnsCTWw//1sogJ +hyzjVOGjprIIC8CFqMjSnHH2HZ9g/DgzE+Ge3Atf2hZQKXsvcJEPmbo0NI2VdMV+ +eKlmXb3KIXdCEKxmJj3ekav9FfBv7WxfEPjzFvYDio+nEhEMy/0/ecGc/WLuo89U +DNErXxc+4z6/wCs+CZv+iKZ+tJIX/COUgb1up8WMwusRRdv4QcmWdupwX3kSa+Sj +B1oF7ydJzyGfikwJcGapJsErEU4z0g781mzSDjJkaP+tBXhfAx2o45CsJOAPQKdL +rosot4LKGAfmt1t06SAZf7IbiVQ= +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Primary Certification Authority - G3 O=GeoTrust Inc. OU=(c) 2008 GeoTrust Inc. - For authorized use only +# Subject: CN=GeoTrust Primary Certification Authority - G3 O=GeoTrust Inc. OU=(c) 2008 GeoTrust Inc. - For authorized use only +# Label: "GeoTrust Primary Certification Authority - G3" +# Serial: 28809105769928564313984085209975885599 +# MD5 Fingerprint: b5:e8:34:36:c9:10:44:58:48:70:6d:2e:83:d4:b8:05 +# SHA1 Fingerprint: 03:9e:ed:b8:0b:e7:a0:3c:69:53:89:3b:20:d2:d9:32:3a:4c:2a:fd +# SHA256 Fingerprint: b4:78:b8:12:25:0d:f8:78:63:5c:2a:a7:ec:7d:15:5e:aa:62:5e:e8:29:16:e2:cd:29:43:61:88:6c:d1:fb:d4 +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsT +MChjKSAyMDA4IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhv +cml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIzNTk1OVowgZgxCzAJ +BgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg +MjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0 +BgNVBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz ++uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5jK/BGvESyiaHAKAxJcCGVn2TAppMSAmUm +hsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdEc5IiaacDiGydY8hS2pgn +5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3CIShwiP/W +JmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exAL +DmKudlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZC +huOl1UcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +HQYDVR0OBBYEFMR5yo6hTgMdHNxr2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IB +AQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9cr5HqQ6XErhK8WTTOd8lNNTB +zU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbEAp7aDHdlDkQN +kv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD +AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUH +SJsMC8tJP33st/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2G +spki4cErx5z481+oghLrGREt +-----END CERTIFICATE----- + +# Issuer: CN=thawte Primary Root CA - G2 O=thawte, Inc. OU=(c) 2007 thawte, Inc. - For authorized use only +# Subject: CN=thawte Primary Root CA - G2 O=thawte, Inc. OU=(c) 2007 thawte, Inc. - For authorized use only +# Label: "thawte Primary Root CA - G2" +# Serial: 71758320672825410020661621085256472406 +# MD5 Fingerprint: 74:9d:ea:60:24:c4:fd:22:53:3e:cc:3a:72:d9:29:4f +# SHA1 Fingerprint: aa:db:bc:22:23:8f:c4:01:a1:27:bb:38:dd:f4:1d:db:08:9e:f0:12 +# SHA256 Fingerprint: a4:31:0d:50:af:18:a6:44:71:90:37:2a:86:af:af:8b:95:1f:fb:43:1d:83:7f:1e:56:88:b4:59:71:ed:15:57 +-----BEGIN CERTIFICATE----- +MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMp +IDIwMDcgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAi +BgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMjAeFw0wNzExMDUwMDAw +MDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh +d3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBGb3Ig +YXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9v +dCBDQSAtIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/ +BebfowJPDQfGAFG6DAJSLSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6 +papu+7qzcMBniKI11KOasf2twu8x+qi58/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUmtgAMADna3+FGO6Lts6K +DPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUNG4k8VIZ3 +KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41ox +XZ3Krr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg== +-----END CERTIFICATE----- + +# Issuer: CN=thawte Primary Root CA - G3 O=thawte, Inc. OU=Certification Services Division/(c) 2008 thawte, Inc. - For authorized use only +# Subject: CN=thawte Primary Root CA - G3 O=thawte, Inc. OU=Certification Services Division/(c) 2008 thawte, Inc. - For authorized use only +# Label: "thawte Primary Root CA - G3" +# Serial: 127614157056681299805556476275995414779 +# MD5 Fingerprint: fb:1b:5d:43:8a:94:cd:44:c6:76:f2:43:4b:47:e7:31 +# SHA1 Fingerprint: f1:8b:53:8d:1b:e9:03:b6:a6:f0:56:43:5b:17:15:89:ca:f3:6b:f2 +# SHA256 Fingerprint: 4b:03:f4:58:07:ad:70:f2:1b:fc:2c:ae:71:c9:fd:e4:60:4c:06:4c:f5:ff:b6:86:ba:e5:db:aa:d7:fd:d3:4c +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCB +rjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf +Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw +MDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNV +BAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0wODA0MDIwMDAwMDBa +Fw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3Rl +LCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9u +MTgwNgYDVQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXpl +ZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEcz +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr8nLPvb2FvdeHsbnndm +gcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2AtP0LMqmsywCPLLEHd5N/8 +YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC+BsUa0Lf +b1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS9 +9irY7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2S +zhkGcuYMXDhpxwTWvGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUk +OQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV +HQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJKoZIhvcNAQELBQADggEBABpA +2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweKA3rD6z8KLFIW +oCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu +t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c +KUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM +m7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu +MdRAGmI0Nj81Aa6sY6A= +-----END CERTIFICATE----- + +# Issuer: CN=GeoTrust Primary Certification Authority - G2 O=GeoTrust Inc. OU=(c) 2007 GeoTrust Inc. - For authorized use only +# Subject: CN=GeoTrust Primary Certification Authority - G2 O=GeoTrust Inc. OU=(c) 2007 GeoTrust Inc. - For authorized use only +# Label: "GeoTrust Primary Certification Authority - G2" +# Serial: 80682863203381065782177908751794619243 +# MD5 Fingerprint: 01:5e:d8:6b:bd:6f:3d:8e:a1:31:f8:12:e0:98:73:6a +# SHA1 Fingerprint: 8d:17:84:d5:37:f3:03:7d:ec:70:fe:57:8b:51:9a:99:e6:10:d7:b0 +# SHA256 Fingerprint: 5e:db:7a:c4:3b:82:a0:6a:87:61:e8:d7:be:49:79:eb:f2:61:1f:7d:d7:9b:f9:1c:1c:6b:56:6a:21:9e:d7:66 +-----BEGIN CERTIFICATE----- +MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDEL +MAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChj +KSAyMDA3IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2 +MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1OVowgZgxCzAJBgNV +BAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykgMjAw +NyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNV +BAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH +MjB2MBAGByqGSM49AgEGBSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcL +So17VDs6bl8VAsBQps8lL33KSLjHUGMcKiEIfJo22Av+0SbFWDEwKCXzXV2juLal +tJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+EVXVMAoG +CCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGT +qQ7mndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBucz +rD6ogRLQy7rQkgu2npaqBA+K +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Universal Root Certification Authority O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2008 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Universal Root Certification Authority O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2008 VeriSign, Inc. - For authorized use only +# Label: "VeriSign Universal Root Certification Authority" +# Serial: 85209574734084581917763752644031726877 +# MD5 Fingerprint: 8e:ad:b5:01:aa:4d:81:e4:8c:1d:d1:e1:14:00:95:19 +# SHA1 Fingerprint: 36:79:ca:35:66:87:72:30:4d:30:a5:fb:87:3b:0f:a7:7b:b7:0d:54 +# SHA256 Fingerprint: 23:99:56:11:27:a5:71:25:de:8c:ef:ea:61:0d:df:2f:a0:78:b5:c8:06:7f:4e:82:82:90:bf:b8:60:e8:4b:3c +-----BEGIN CERTIFICATE----- +MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCB +vTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MTgwNgYDVQQDEy9W +ZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJVUzEX +MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0 +IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9y +IGF1dGhvcml6ZWQgdXNlIG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNh +bCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj1mCOkdeQmIN65lgZOIzF +9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGPMiJhgsWH +H26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+H +LL729fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN +/BMReYTtXlT2NJ8IAfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPT +rJ9VAMf2CGqUuV/c4DPxhGD5WycRtPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0GCCsGAQUFBwEMBGEwX6FdoFsw +WTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2Oa8PPgGrUSBgs +exkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud +DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4 +sAPmLGd75JR3Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+ +seQxIcaBlVZaDrHC1LGmWazxY8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz +4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTxP/jgdFcrGJ2BtMQo2pSXpXDrrB2+ +BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+PwGZsY6rp2aQW9IHR +lRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4mJO3 +7M2CYfE45k+XmCpajQ== +-----END CERTIFICATE----- + +# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G4 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2007 VeriSign, Inc. - For authorized use only +# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G4 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2007 VeriSign, Inc. - For authorized use only +# Label: "VeriSign Class 3 Public Primary Certification Authority - G4" +# Serial: 63143484348153506665311985501458640051 +# MD5 Fingerprint: 3a:52:e1:e7:fd:6f:3a:e3:6f:f3:6f:99:1b:f9:22:41 +# SHA1 Fingerprint: 22:d5:d8:df:8f:02:31:d1:8d:f7:9d:b7:cf:8a:2d:64:c9:3f:6c:3a +# SHA256 Fingerprint: 69:dd:d7:ea:90:bb:57:c9:3e:13:5d:c8:5e:a6:fc:d5:48:0b:60:32:39:bd:c4:54:fc:75:8b:2a:26:cf:7f:79 +-----BEGIN CERTIFICATE----- +MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjELMAkG +A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJp +U2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwg +SW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2ln +biBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8Utpkmw4tXNherJI9/gHm +GUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGzrl0Bp3ve +fLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJ +aW1hZ2UvZ2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYj +aHR0cDovL2xvZ28udmVyaXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMW +kf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMDA2gAMGUCMGYhDBgmYFo4e1ZC +4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIxAJw9SDkjOVga +FRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== +-----END CERTIFICATE----- + +# Issuer: CN=NetLock Arany (Class Gold) Főtanúsítvány O=NetLock Kft. OU=Tanúsítványkiadók (Certification Services) +# Subject: CN=NetLock Arany (Class Gold) Főtanúsítvány O=NetLock Kft. OU=Tanúsítványkiadók (Certification Services) +# Label: "NetLock Arany (Class Gold) Főtanúsítvány" +# Serial: 80544274841616 +# MD5 Fingerprint: c5:a1:b7:ff:73:dd:d6:d7:34:32:18:df:fc:3c:ad:88 +# SHA1 Fingerprint: 06:08:3f:59:3f:15:a1:04:a0:69:a4:6b:a9:03:d0:06:b7:97:09:91 +# SHA256 Fingerprint: 6c:61:da:c3:a2:de:f0:31:50:6b:e0:36:d2:a6:fe:40:19:94:fb:d1:3d:f9:c8:d4:66:59:92:74:c4:46:ec:98 +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +# Issuer: CN=Staat der Nederlanden Root CA - G2 O=Staat der Nederlanden +# Subject: CN=Staat der Nederlanden Root CA - G2 O=Staat der Nederlanden +# Label: "Staat der Nederlanden Root CA - G2" +# Serial: 10000012 +# MD5 Fingerprint: 7c:a5:0f:f8:5b:9a:7d:6d:30:ae:54:5a:e3:42:a2:8a +# SHA1 Fingerprint: 59:af:82:79:91:86:c7:b4:75:07:cb:cf:03:57:46:eb:04:dd:b7:16 +# SHA256 Fingerprint: 66:8c:83:94:7d:a6:3b:72:4b:ec:e1:74:3c:31:a0:e6:ae:d0:db:8e:c5:b3:1b:e3:77:bb:78:4f:91:b6:71:6f +-----BEGIN CERTIFICATE----- +MIIFyjCCA7KgAwIBAgIEAJiWjDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEcyMB4XDTA4MDMyNjExMTgxN1oX +DTIwMDMyNTExMDMxMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl +ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv +b3QgQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMVZ5291 +qj5LnLW4rJ4L5PnZyqtdj7U5EILXr1HgO+EASGrP2uEGQxGZqhQlEq0i6ABtQ8Sp +uOUfiUtnvWFI7/3S4GCI5bkYYCjDdyutsDeqN95kWSpGV+RLufg3fNU254DBtvPU +Z5uW6M7XxgpT0GtJlvOjCwV3SPcl5XCsMBQgJeN/dVrlSPhOewMHBPqCYYdu8DvE +pMfQ9XQ+pV0aCPKbJdL2rAQmPlU6Yiile7Iwr/g3wtG61jj99O9JMDeZJiFIhQGp +5Rbn3JBV3w/oOM2ZNyFPXfUib2rFEhZgF1XyZWampzCROME4HYYEhLoaJXhena/M +UGDWE4dS7WMfbWV9whUYdMrhfmQpjHLYFhN9C0lK8SgbIHRrxT3dsKpICT0ugpTN +GmXZK4iambwYfp/ufWZ8Pr2UuIHOzZgweMFvZ9C+X+Bo7d7iscksWXiSqt8rYGPy +5V6548r6f1CGPqI0GAwJaCgRHOThuVw+R7oyPxjMW4T182t0xHJ04eOLoEq9jWYv +6q012iDTiIJh8BIitrzQ1aTsr1SIJSQ8p22xcik/Plemf1WvbibG/ufMQFxRRIEK +eN5KzlW/HdXZt1bv8Hb/C3m1r737qWmRRpdogBQ2HbN/uymYNqUg+oJgYjOk7Na6 +B6duxc8UpufWkjTYgfX8HV2qXB72o007uPc5AgMBAAGjgZcwgZQwDwYDVR0TAQH/ +BAUwAwEB/zBSBgNVHSAESzBJMEcGBFUdIAAwPzA9BggrBgEFBQcCARYxaHR0cDov +L3d3dy5wa2lvdmVyaGVpZC5ubC9wb2xpY2llcy9yb290LXBvbGljeS1HMjAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJFoMocVHYnitfGsNig0jQt8YojrMA0GCSqG +SIb3DQEBCwUAA4ICAQCoQUpnKpKBglBu4dfYszk78wIVCVBR7y29JHuIhjv5tLyS +CZa59sCrI2AGeYwRTlHSeYAz+51IvuxBQ4EffkdAHOV6CMqqi3WtFMTC6GY8ggen +5ieCWxjmD27ZUD6KQhgpxrRW/FYQoAUXvQwjf/ST7ZwaUb7dRUG/kSS0H4zpX897 +IZmflZ85OkYcbPnNe5yQzSipx6lVu6xiNGI1E0sUOlWDuYaNkqbG9AclVMwWVxJK +gnjIFNkXgiYtXSAfea7+1HAWFpWD2DU5/1JddRwWxRNVz0fMdWVSSt7wsKfkCpYL ++63C4iWEst3kvX5ZbJvw8NjnyvLplzh+ib7M+zkXYT9y2zqR2GUBGR2tUKRXCnxL +vJxxcypFURmFzI79R6d0lR2o0a9OF7FpJsKqeFdbxU2n5Z4FF5TKsl+gSRiNNOkm +bEgeqmiSBeGCc1qb3AdbCG19ndeNIdn8FCCqwkXfP+cAslHkwvgFuXkajDTznlvk +N1trSt8sV4pAWja63XVECDdCcAz+3F4hoKOKwJCcaNpQ5kUQR3i2TtJlycM33+FC +Y7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoVIPVVYpbtbZNQvOSqeK3Z +ywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm66+KAQ== +-----END CERTIFICATE----- + +# Issuer: CN=Juur-SK O=AS Sertifitseerimiskeskus +# Subject: CN=Juur-SK O=AS Sertifitseerimiskeskus +# Label: "Juur-SK" +# Serial: 999181308 +# MD5 Fingerprint: aa:8e:5d:d9:f8:db:0a:58:b7:8d:26:87:6c:82:35:55 +# SHA1 Fingerprint: 40:9d:4b:d9:17:b5:5c:27:b6:9b:64:cb:98:22:44:0d:cd:09:b8:89 +# SHA256 Fingerprint: ec:c3:e9:c3:40:75:03:be:e0:91:aa:95:2f:41:34:8f:f8:8b:aa:86:3b:22:64:be:fa:c8:07:90:15:74:e9:39 +-----BEGIN CERTIFICATE----- +MIIE5jCCA86gAwIBAgIEO45L/DANBgkqhkiG9w0BAQUFADBdMRgwFgYJKoZIhvcN +AQkBFglwa2lAc2suZWUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKExlBUyBTZXJ0aWZp +dHNlZXJpbWlza2Vza3VzMRAwDgYDVQQDEwdKdXVyLVNLMB4XDTAxMDgzMDE0MjMw +MVoXDTE2MDgyNjE0MjMwMVowXTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMQsw +CQYDVQQGEwJFRTEiMCAGA1UEChMZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEQ +MA4GA1UEAxMHSnV1ci1TSzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AIFxNj4zB9bjMI0TfncyRsvPGbJgMUaXhvSYRqTCZUXP00B841oiqBB4M8yIsdOB +SvZiF3tfTQou0M+LI+5PAk676w7KvRhj6IAcjeEcjT3g/1tf6mTll+g/mX8MCgkz +ABpTpyHhOEvWgxutr2TC+Rx6jGZITWYfGAriPrsfB2WThbkasLnE+w0R9vXW+RvH +LCu3GFH+4Hv2qEivbDtPL+/40UceJlfwUR0zlv/vWT3aTdEVNMfqPxZIe5EcgEMP +PbgFPtGzlc3Yyg/CQ2fbt5PgIoIuvvVoKIO5wTtpeyDaTpxt4brNj3pssAki14sL +2xzVWiZbDcDq5WDQn/413z8CAwEAAaOCAawwggGoMA8GA1UdEwEB/wQFMAMBAf8w +ggEWBgNVHSAEggENMIIBCTCCAQUGCisGAQQBzh8BAQEwgfYwgdAGCCsGAQUFBwIC +MIHDHoHAAFMAZQBlACAAcwBlAHIAdABpAGYAaQBrAGEAYQB0ACAAbwBuACAAdgDk +AGwAagBhAHMAdABhAHQAdQBkACAAQQBTAC0AaQBzACAAUwBlAHIAdABpAGYAaQB0 +AHMAZQBlAHIAaQBtAGkAcwBrAGUAcwBrAHUAcwAgAGEAbABhAG0ALQBTAEsAIABz +AGUAcgB0AGkAZgBpAGsAYQBhAHQAaQBkAGUAIABrAGkAbgBuAGkAdABhAG0AaQBz +AGUAawBzMCEGCCsGAQUFBwIBFhVodHRwOi8vd3d3LnNrLmVlL2Nwcy8wKwYDVR0f +BCQwIjAgoB6gHIYaaHR0cDovL3d3dy5zay5lZS9qdXVyL2NybC8wHQYDVR0OBBYE +FASqekej5ImvGs8KQKcYP2/v6X2+MB8GA1UdIwQYMBaAFASqekej5ImvGs8KQKcY +P2/v6X2+MA4GA1UdDwEB/wQEAwIB5jANBgkqhkiG9w0BAQUFAAOCAQEAe8EYlFOi +CfP+JmeaUOTDBS8rNXiRTHyoERF5TElZrMj3hWVcRrs7EKACr81Ptcw2Kuxd/u+g +kcm2k298gFTsxwhwDY77guwqYHhpNjbRxZyLabVAyJRld/JXIWY7zoVAtjNjGr95 +HvxcHdMdkxuLDF2FvZkwMhgJkVLpfKG6/2SSmuz+Ne6ML678IIbsSt4beDI3poHS +na9aEhbKmVv8b20OxaAehsmR0FyYgl9jDIpaq9iVpszLita/ZEuOyoqysOkhMp6q +qIWYNIE5ITuoOlIyPfZrN4YGWhWY3PARZv40ILcD9EEQfTmEeZZyY7aWAuVrua0Z +TbvGRNs2yyqcjg== +-----END CERTIFICATE----- + +# Issuer: CN=Hongkong Post Root CA 1 O=Hongkong Post +# Subject: CN=Hongkong Post Root CA 1 O=Hongkong Post +# Label: "Hongkong Post Root CA 1" +# Serial: 1000 +# MD5 Fingerprint: a8:0d:6f:39:78:b9:43:6d:77:42:6d:98:5a:cc:23:ca +# SHA1 Fingerprint: d6:da:a8:20:8d:09:d2:15:4d:24:b5:2f:cb:34:6e:b2:58:b2:8a:58 +# SHA256 Fingerprint: f9:e6:7d:33:6c:51:00:2a:c0:54:c6:32:02:2d:66:dd:a2:e7:e3:ff:f1:0a:d0:61:ed:31:d8:bb:b4:10:cf:b2 +-----BEGIN CERTIFICATE----- +MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsx +FjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3Qg +Um9vdCBDQSAxMB4XDTAzMDUxNTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkG +A1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdr +b25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1ApzQ +jVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEn +PzlTCeqrauh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjh +ZY4bXSNmO7ilMlHIhqqhqZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9 +nnV0ttgCXjqQesBCNnLsak3c78QA3xMYV18meMjWCnl3v/evt3a5pQuEF10Q6m/h +q5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNVHRMBAf8ECDAGAQH/AgED +MA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7ih9legYsC +mEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI3 +7piol7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clB +oiMBdDhViw+5LmeiIAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJs +EhTkYY2sEJCehFC78JZvRZ+K88psT/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpO +fMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilTc4afU9hDDl3WY4JxHYB0yvbi +AmvZWg== +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign RootCA11 O=Japan Certification Services, Inc. +# Subject: CN=SecureSign RootCA11 O=Japan Certification Services, Inc. +# Label: "SecureSign RootCA11" +# Serial: 1 +# MD5 Fingerprint: b7:52:74:e2:92:b4:80:93:f2:75:e4:cc:d7:f2:ea:26 +# SHA1 Fingerprint: 3b:c4:9f:48:f8:f3:73:a0:9c:1e:bd:f8:5b:b1:c3:65:c7:d8:11:b3 +# SHA256 Fingerprint: bf:0f:ee:fb:9e:3a:58:1a:d5:f9:e9:db:75:89:98:57:43:d2:61:08:5c:4d:31:4f:6f:5d:72:59:aa:42:16:12 +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDEr +MCkGA1UEChMiSmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoG +A1UEAxMTU2VjdXJlU2lnbiBSb290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0 +MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSswKQYDVQQKEyJKYXBhbiBDZXJ0aWZp +Y2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1cmVTaWduIFJvb3RD +QTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvLTJsz +i1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8 +h9uuywGOwvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOV +MdrAG/LuYpmGYz+/3ZMqg6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9 +UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rPO7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni +8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitAbpSACW22s293bzUIUPsC +h8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZXt94wDgYD +VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB +AKChOBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xm +KbabfSVSSUOrTC4rbnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQ +X5Ucv+2rIrVls4W6ng+4reV6G4pQOh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWr +QbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01y8hSyn+B/tlr0/cR7SXf+Of5 +pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061lgeLKBObjBmN +QSdJQO7e5iNEOdyhIta6A/I= +-----END CERTIFICATE----- + +# Issuer: CN=ACEDICOM Root O=EDICOM OU=PKI +# Subject: CN=ACEDICOM Root O=EDICOM OU=PKI +# Label: "ACEDICOM Root" +# Serial: 7029493972724711941 +# MD5 Fingerprint: 42:81:a0:e2:1c:e3:55:10:de:55:89:42:65:96:22:e6 +# SHA1 Fingerprint: e0:b4:32:2e:b2:f6:a5:68:b6:54:53:84:48:18:4a:50:36:87:43:84 +# SHA256 Fingerprint: 03:95:0f:b4:9a:53:1f:3e:19:91:94:23:98:df:a9:e0:ea:32:d7:ba:1c:dd:9b:c8:5d:b5:7e:d9:40:0b:43:4a +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIIYY3HhjsBggUwDQYJKoZIhvcNAQEFBQAwRDEWMBQGA1UE +AwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZFRElDT00x +CzAJBgNVBAYTAkVTMB4XDTA4MDQxODE2MjQyMloXDTI4MDQxMzE2MjQyMlowRDEW +MBQGA1UEAwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZF +RElDT00xCzAJBgNVBAYTAkVTMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC +AgEA/5KV4WgGdrQsyFhIyv2AVClVYyT/kGWbEHV7w2rbYgIB8hiGtXxaOLHkWLn7 +09gtn70yN78sFW2+tfQh0hOR2QetAQXW8713zl9CgQr5auODAKgrLlUTY4HKRxx7 +XBZXehuDYAQ6PmXDzQHe3qTWDLqO3tkE7hdWIpuPY/1NFgu3e3eM+SW10W2ZEi5P +Grjm6gSSrj0RuVFCPYewMYWveVqc/udOXpJPQ/yrOq2lEiZmueIM15jO1FillUAK +t0SdE3QrwqXrIhWYENiLxQSfHY9g5QYbm8+5eaA9oiM/Qj9r+hwDezCNzmzAv+Yb +X79nuIQZ1RXve8uQNjFiybwCq0Zfm/4aaJQ0PZCOrfbkHQl/Sog4P75n/TSW9R28 +MHTLOO7VbKvU/PQAtwBbhTIWdjPp2KOZnQUAqhbm84F9b32qhm2tFXTTxKJxqvQU +fecyuB+81fFOvW8XAjnXDpVCOscAPukmYxHqC9FK/xidstd7LzrZlvvoHpKuE1XI +2Sf23EgbsCTBheN3nZqk8wwRHQ3ItBTutYJXCb8gWH8vIiPYcMt5bMlL8qkqyPyH +K9caUPgn6C9D4zq92Fdx/c6mUlv53U3t5fZvie27k5x2IXXwkkwp9y+cAS7+UEae +ZAwUswdbxcJzbPEHXEUkFDWug/FqTYl6+rPYLWbwNof1K1MCAwEAAaOBqjCBpzAP +BgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKaz4SsrSbbXc6GqlPUB53NlTKxQ +MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUprPhKytJttdzoaqU9QHnc2VMrFAw +RAYDVR0gBD0wOzA5BgRVHSAAMDEwLwYIKwYBBQUHAgEWI2h0dHA6Ly9hY2VkaWNv +bS5lZGljb21ncm91cC5jb20vZG9jMA0GCSqGSIb3DQEBBQUAA4ICAQDOLAtSUWIm +fQwng4/F9tqgaHtPkl7qpHMyEVNEskTLnewPeUKzEKbHDZ3Ltvo/Onzqv4hTGzz3 +gvoFNTPhNahXwOf9jU8/kzJPeGYDdwdY6ZXIfj7QeQCM8htRM5u8lOk6e25SLTKe +I6RF+7YuE7CLGLHdztUdp0J/Vb77W7tH1PwkzQSulgUV1qzOMPPKC8W64iLgpq0i +5ALudBF/TP94HTXa5gI06xgSYXcGCRZj6hitoocf8seACQl1ThCojz2GuHURwCRi +ipZ7SkXp7FnFvmuD5uHorLUwHv4FB4D54SMNUI8FmP8sX+g7tq3PgbUhh8oIKiMn +MCArz+2UW6yyetLHKKGKC5tNSixthT8Jcjxn4tncB7rrZXtaAWPWkFtPF2Y9fwsZ +o5NjEFIqnxQWWOLcpfShFosOkYuByptZ+thrkQdlVV9SH686+5DdaaVbnG0OLLb6 +zqylfDJKZ0DcMDQj3dcEI2bw/FWAp/tmGYI1Z2JwOV5vx+qQQEQIHriy1tvuWacN +GHk0vFQYXlPKNFHtRQrmjseCNj6nOGOpMCwXEGCSn1WHElkQwg9naRHMTh5+Spqt +r0CodaxWkHS4oJyleW/c6RrIaQXpuvoDs3zk4E7Czp3otkYNbn5XOmeUwssfnHdK +Z05phkOTOPu220+DkdRgfks+KzgHVZhepA== +-----END CERTIFICATE----- + +# Issuer: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Subject: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Label: "Microsec e-Szigno Root CA 2009" +# Serial: 14014712776195784473 +# MD5 Fingerprint: f8:49:f4:03:bc:44:2d:83:be:48:69:7d:29:64:fc:b1 +# SHA1 Fingerprint: 89:df:74:fe:5c:f4:0f:4a:80:f9:e3:37:7d:54:da:91:e1:01:31:8e +# SHA256 Fingerprint: 3c:5f:81:fe:a5:fa:b8:2c:64:bf:a2:ea:ec:af:cd:e8:e0:77:fc:86:20:a7:ca:e5:37:16:3d:f3:6e:db:f3:78 +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Label: "GlobalSign Root CA - R3" +# Serial: 4835703278459759426209954 +# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28 +# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad +# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- + +# Issuer: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Subject: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Label: "Autoridad de Certificacion Firmaprofesional CIF A62634068" +# Serial: 6047274297262753887 +# MD5 Fingerprint: 73:3a:74:7a:ec:bb:a3:96:a6:c2:e4:e2:c8:9b:c0:c3 +# SHA1 Fingerprint: ae:c5:fb:3f:c8:e1:bf:c4:e5:4f:03:07:5a:9a:e8:00:b7:f7:b6:fa +# SHA256 Fingerprint: 04:04:80:28:bf:1f:28:64:d4:8f:9a:d4:d8:32:94:36:6a:82:88:56:55:3f:3b:14:30:3f:90:14:7f:5d:40:ef +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEy +MzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNHDhpkLzCBpgYD +VR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp +cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBv +ACAAZABlACAAbABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBl +AGwAbwBuAGEAIAAwADgAMAAxADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF +661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx51tkljYyGOylMnfX40S2wBEqgLk9 +am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qkR71kMrv2JYSiJ0L1 +ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaPT481 +PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS +3a/DTg4fJl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5k +SeTy36LssUzAKh3ntLFlosS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF +3dvd6qJ2gHN99ZwExEWN57kci57q13XRcrHedUTnQn3iV2t93Jm8PYMo6oCTjcVM +ZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoRsaS8I8nkvof/uZS2+F0g +StRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTDKCOM/icz +Q0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQB +jLMi6Et8Vcad+qMUu2WFbm5PEn4KPJ2V +-----END CERTIFICATE----- + +# Issuer: CN=Izenpe.com O=IZENPE S.A. +# Subject: CN=Izenpe.com O=IZENPE S.A. +# Label: "Izenpe.com" +# Serial: 917563065490389241595536686991402621 +# MD5 Fingerprint: a6:b0:cd:85:80:da:5c:50:34:a3:39:90:2f:55:67:73 +# SHA1 Fingerprint: 2f:78:3d:25:52:18:a7:4a:65:39:71:b5:2c:a2:9c:45:15:6f:e9:19 +# SHA256 Fingerprint: 25:30:cc:8e:98:32:15:02:ba:d9:6f:9b:1f:ba:1b:09:9e:2d:29:9e:0f:45:48:bb:91:4f:36:3b:c0:d4:53:1f +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +# Issuer: CN=Chambers of Commerce Root - 2008 O=AC Camerfirma S.A. +# Subject: CN=Chambers of Commerce Root - 2008 O=AC Camerfirma S.A. +# Label: "Chambers of Commerce Root - 2008" +# Serial: 11806822484801597146 +# MD5 Fingerprint: 5e:80:9e:84:5a:0e:65:0b:17:02:f3:55:18:2a:3e:d7 +# SHA1 Fingerprint: 78:6a:74:ac:76:ab:14:7f:9c:6a:30:50:ba:9e:a8:7e:fe:9a:ce:3c +# SHA256 Fingerprint: 06:3e:4a:fa:c4:91:df:d3:32:f3:08:9b:85:42:e9:46:17:d8:93:d7:fe:94:4e:10:a7:93:7e:e2:9d:96:93:c0 +-----BEGIN CERTIFICATE----- +MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYD +VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 +IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 +MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xKTAnBgNVBAMTIENoYW1iZXJz +IG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEyMjk1MFoXDTM4MDcz +MTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBj +dXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIw +EAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEp +MCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW9 +28sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKAXuFixrYp4YFs8r/lfTJq +VKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorjh40G072Q +DuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR +5gN/ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfL +ZEFHcpOrUMPrCXZkNNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05a +Sd+pZgvMPMZ4fKecHePOjlO+Bd5gD2vlGts/4+EhySnB8esHnFIbAURRPHsl18Tl +UlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331lubKgdaX8ZSD6e2wsWsSaR6s ++12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ0wlf2eOKNcx5 +Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj +ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAx +hduub+84Mxh2EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNV +HQ4EFgQU+SSsD7K1+HnA+mCIG8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1 ++HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpN +YWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29t +L2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVy +ZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAt +IDIwMDiCCQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRV +HSAAMCowKAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20w +DQYJKoZIhvcNAQEFBQADggIBAJASryI1wqM58C7e6bXpeHxIvj99RZJe6dqxGfwW +PJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH3qLPaYRgM+gQDROpI9CF +5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbURWpGqOt1 +glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaH +FoI6M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2 +pSB7+R5KBWIBpih1YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MD +xvbxrN8y8NmBGuScvfaAFPDRLLmF9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QG +tjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcKzBIKinmwPQN/aUv0NCB9szTq +jktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvGnrDQWzilm1De +fhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg +OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZ +d0jQ +-----END CERTIFICATE----- + +# Issuer: CN=Global Chambersign Root - 2008 O=AC Camerfirma S.A. +# Subject: CN=Global Chambersign Root - 2008 O=AC Camerfirma S.A. +# Label: "Global Chambersign Root - 2008" +# Serial: 14541511773111788494 +# MD5 Fingerprint: 9e:80:ff:78:01:0c:2e:c1:36:bd:fe:96:90:6e:08:f3 +# SHA1 Fingerprint: 4a:bd:ee:ec:95:0d:35:9c:89:ae:c7:52:a1:2c:5b:29:f6:d6:aa:0c +# SHA256 Fingerprint: 13:63:35:43:93:34:a7:69:80:16:a0:d3:24:de:72:28:4e:07:9d:7b:52:20:bb:8f:bd:74:78:16:ee:be:ba:ca +-----BEGIN CERTIFICATE----- +MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYD +VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 +IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 +MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD +aGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMxNDBaFw0zODA3MzEx +MjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3Vy +cmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAG +A1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAl +BgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZI +hvcNAQEBBQADggIPADCCAgoCggIBAMDfVtPkOpt2RbQT2//BthmLN0EYlVJH6xed +KYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXfXjaOcNFccUMd2drvXNL7 +G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0ZJJ0YPP2 +zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4 +ddPB/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyG +HoiMvvKRhI9lNNgATH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2 +Id3UwD2ln58fQ1DJu7xsepeY7s2MH/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3V +yJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfeOx2YItaswTXbo6Al/3K1dh3e +beksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSFHTynyQbehP9r +6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh +wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsog +zCtLkykPAgMBAAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQW +BBS5CcqcHtvTbDprru1U8VuTBjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDpr +ru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UEBhMCRVUxQzBBBgNVBAcTOk1hZHJp +ZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJmaXJtYS5jb20vYWRk +cmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJmaXJt +YSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiC +CQDJzdPp1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCow +KAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZI +hvcNAQEFBQADggIBAICIf3DekijZBZRG/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZ +UohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6ReAJ3spED8IXDneRRXoz +X1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/sdZ7LoR/x +fxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVz +a2Mg9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yyd +Yhz2rXzdpjEetrHHfoUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMd +SqlapskD7+3056huirRXhOukP9DuqqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9O +AP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETrP3iZ8ntxPjzxmKfFGBI/5rso +M0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVqc5iJWzouE4ge +v8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z +09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B +-----END CERTIFICATE----- + +# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Label: "Go Daddy Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01 +# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b +# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96 +# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e +# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Services Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 17:35:74:af:7b:61:1c:eb:f4:f9:3c:e2:ee:40:f9:a2 +# SHA1 Fingerprint: 92:5a:8f:8d:2c:6d:04:e0:66:5f:59:6a:ff:22:d8:63:e8:25:6f:3f +# SHA256 Fingerprint: 56:8d:69:05:a2:c8:87:08:a4:b3:02:51:90:ed:cf:ed:b1:97:4a:60:6a:13:c6:e5:29:0f:cb:2a:e6:3e:da:b5 +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Commercial O=AffirmTrust +# Subject: CN=AffirmTrust Commercial O=AffirmTrust +# Label: "AffirmTrust Commercial" +# Serial: 8608355977964138876 +# MD5 Fingerprint: 82:92:ba:5b:ef:cd:8a:6f:a6:3d:55:f9:84:f6:d6:b7 +# SHA1 Fingerprint: f9:b5:b6:32:45:5f:9c:be:ec:57:5f:80:dc:e9:6e:2c:c7:b2:78:b7 +# SHA256 Fingerprint: 03:76:ab:1d:54:c5:f9:80:3c:e4:b2:e2:01:a0:ee:7e:ef:7b:57:b6:36:e8:a9:3c:9b:8d:48:60:c9:6f:5f:a7 +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP +Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr +ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL +MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 +yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr +VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ +nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG +XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj +vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt +Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g +N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC +nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Networking O=AffirmTrust +# Subject: CN=AffirmTrust Networking O=AffirmTrust +# Label: "AffirmTrust Networking" +# Serial: 8957382827206547757 +# MD5 Fingerprint: 42:65:ca:be:01:9a:9a:4c:a9:8c:41:49:cd:c0:d5:7f +# SHA1 Fingerprint: 29:36:21:02:8b:20:ed:02:f5:66:c5:32:d1:d6:ed:90:9f:45:00:2f +# SHA256 Fingerprint: 0a:81:ec:5a:92:97:77:f1:45:90:4a:f3:8d:5d:50:9f:66:b5:e2:c5:8f:cd:b5:31:05:8b:0e:17:f3:f0:b4:1b +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y +YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua +kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL +QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp +6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG +yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i +QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO +tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu +QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ +Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u +olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 +x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Premium O=AffirmTrust +# Subject: CN=AffirmTrust Premium O=AffirmTrust +# Label: "AffirmTrust Premium" +# Serial: 7893706540734352110 +# MD5 Fingerprint: c4:5d:0e:48:b6:ac:28:30:4e:0a:bc:f9:38:16:87:57 +# SHA1 Fingerprint: d8:a6:33:2c:e0:03:6f:b1:85:f6:63:4f:7d:6a:06:65:26:32:28:27 +# SHA256 Fingerprint: 70:a7:3f:7f:37:6b:60:07:42:48:90:45:34:b1:14:82:d5:bf:0e:69:8e:cc:49:8d:f5:25:77:eb:f2:e9:3b:9a +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz +dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG +A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U +cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf +qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ +JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ ++jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS +s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 +HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 +70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG +V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S +qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S +5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia +C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX +OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE +FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 +KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B +8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ +MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc +0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ +u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF +u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH +YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 +GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO +RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e +KeC2uAloGRwYQw== +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Premium ECC O=AffirmTrust +# Subject: CN=AffirmTrust Premium ECC O=AffirmTrust +# Label: "AffirmTrust Premium ECC" +# Serial: 8401224907861490260 +# MD5 Fingerprint: 64:b0:09:55:cf:b1:d5:99:e2:be:13:ab:a6:5d:ea:4d +# SHA1 Fingerprint: b8:23:6b:00:2f:1d:16:86:53:01:55:6c:11:a4:37:ca:eb:ff:c3:bb +# SHA256 Fingerprint: bd:71:fd:f6:da:97:e4:cf:62:d1:64:7a:dd:25:81:b0:7d:79:ad:f8:39:7e:b4:ec:ba:9c:5e:84:88:82:14:23 +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ +cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ +BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt +VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D +0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 +ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G +A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs +aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I +flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Network CA" +# Serial: 279744 +# MD5 Fingerprint: d5:e9:81:40:c5:18:69:fc:46:2c:89:75:62:0f:aa:78 +# SHA1 Fingerprint: 07:e0:32:e0:20:b7:2c:3f:19:2f:06:28:a2:59:3a:19:a7:0f:06:9e +# SHA256 Fingerprint: 5c:58:46:8d:55:f5:8e:49:7e:74:39:82:d2:b5:00:10:b6:d1:65:37:4a:cf:83:a7:d4:a3:2d:b7:68:c4:40:8e +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +# Issuer: CN=Certinomis - Autorité Racine O=Certinomis OU=0002 433998903 +# Subject: CN=Certinomis - Autorité Racine O=Certinomis OU=0002 433998903 +# Label: "Certinomis - Autorité Racine" +# Serial: 1 +# MD5 Fingerprint: 7f:30:78:8c:03:e3:ca:c9:0a:e2:c9:ea:1e:aa:55:1a +# SHA1 Fingerprint: 2e:14:da:ec:28:f0:fa:1e:8e:38:9a:4e:ab:eb:26:c0:0a:d3:83:c3 +# SHA256 Fingerprint: fc:bf:e2:88:62:06:f7:2b:27:59:3c:8b:07:02:97:e1:2d:76:9e:d1:0e:d7:93:07:05:a8:09:8e:ff:c1:4d:17 +-----BEGIN CERTIFICATE----- +MIIFnDCCA4SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJGUjET +MBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxJjAk +BgNVBAMMHUNlcnRpbm9taXMgLSBBdXRvcml0w6kgUmFjaW5lMB4XDTA4MDkxNzA4 +Mjg1OVoXDTI4MDkxNzA4Mjg1OVowYzELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNl +cnRpbm9taXMxFzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMSYwJAYDVQQDDB1DZXJ0 +aW5vbWlzIC0gQXV0b3JpdMOpIFJhY2luZTCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAJ2Fn4bT46/HsmtuM+Cet0I0VZ35gb5j2CN2DpdUzZlMGvE5x4jY +F1AMnmHawE5V3udauHpOd4cN5bjr+p5eex7Ezyh0x5P1FMYiKAT5kcOrJ3NqDi5N +8y4oH3DfVS9O7cdxbwlyLu3VMpfQ8Vh30WC8Tl7bmoT2R2FFK/ZQpn9qcSdIhDWe +rP5pqZ56XjUl+rSnSTV3lqc2W+HN3yNw2F1MpQiD8aYkOBOo7C+ooWfHpi2GR+6K +/OybDnT0K0kCe5B1jPyZOQE51kqJ5Z52qz6WKDgmi92NjMD2AR5vpTESOH2VwnHu +7XSu5DaiQ3XV8QCb4uTXzEIDS3h65X27uK4uIJPT5GHfceF2Z5c/tt9qc1pkIuVC +28+BA5PY9OMQ4HL2AHCs8MF6DwV/zzRpRbWT5BnbUhYjBYkOjUjkJW+zeL9i9Qf6 +lSTClrLooyPCXQP8w9PlfMl1I9f09bze5N/NgL+RiH2nE7Q5uiy6vdFrzPOlKO1E +nn1So2+WLhl+HPNbxxaOu2B9d2ZHVIIAEWBsMsGoOBvrbpgT1u449fCfDu/+MYHB +0iSVL1N6aaLwD4ZFjliCK0wi1F6g530mJ0jfJUaNSih8hp75mxpZuWW/Bd22Ql09 +5gBIgl4g9xGC3srYn+Y3RyYe63j3YcNBZFgCQfna4NH4+ej9Uji29YnfAgMBAAGj +WzBZMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBQN +jLZh2kS40RR9w759XkjwzspqsDAXBgNVHSAEEDAOMAwGCiqBegFWAgIAAQEwDQYJ +KoZIhvcNAQEFBQADggIBACQ+YAZ+He86PtvqrxyaLAEL9MW12Ukx9F1BjYkMTv9s +ov3/4gbIOZ/xWqndIlgVqIrTseYyCYIDbNc/CMf4uboAbbnW/FIyXaR/pDGUu7ZM +OH8oMDX/nyNTt7buFHAAQCvaR6s0fl6nVjBhK4tDrP22iCj1a7Y+YEq6QpA0Z43q +619FVDsXrIvkxmUP7tCMXWY5zjKn2BCXwH40nJ+U8/aGH88bc62UeYdocMMzpXDn +2NU4lG9jeeu/Cg4I58UvD0KgKxRA/yHgBcUn4YQRE7rWhh1BCxMjidPJC+iKunqj +o3M3NYB9Ergzd0A4wPpeMNLytqOx1qKVl4GbUu1pTP+A5FPbVFsDbVRfsbjvJL1v +nxHDx2TCDyhihWZeGnuyt++uNckZM6i4J9szVb9o4XVIRFb7zdNIu0eJOqxp9YDG +5ERQL1TEqkPFMTFYvZbF6nVsmnWxTfj3l/+WFvKXTej28xH5On2KOG4Ey+HTRRWq +pdEdnV1j6CTmNhTih60bWfVEm/vXd3wfAXBioSAaosUaKPQhA+4u2cGA6rnZgtZb +dsLLO7XSAPCjDuGtbkD326C00EauFddEwk01+dIL8hf2rGbVJLJP0RyZwG71fet0 +BLj5TXcJ17TPBzAJ8bgAVtkXFhYKK4bfjwEZGuW7gmP/vgt2Fl43N+bYdJeimUV5 +-----END CERTIFICATE----- + +# Issuer: CN=Root CA Generalitat Valenciana O=Generalitat Valenciana OU=PKIGVA +# Subject: CN=Root CA Generalitat Valenciana O=Generalitat Valenciana OU=PKIGVA +# Label: "Root CA Generalitat Valenciana" +# Serial: 994436456 +# MD5 Fingerprint: 2c:8c:17:5e:b1:54:ab:93:17:b5:36:5a:db:d1:c6:f2 +# SHA1 Fingerprint: a0:73:e5:c5:bd:43:61:0d:86:4c:21:13:0a:85:58:57:cc:9c:ea:46 +# SHA256 Fingerprint: 8c:4e:df:d0:43:48:f3:22:96:9e:7e:29:a4:cd:4d:ca:00:46:55:06:1c:16:e1:b0:76:42:2e:f3:42:ad:63:0e +-----BEGIN CERTIFICATE----- +MIIGizCCBXOgAwIBAgIEO0XlaDANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJF +UzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJ +R1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwHhcN +MDEwNzA2MTYyMjQ3WhcNMjEwNzAxMTUyMjQ3WjBoMQswCQYDVQQGEwJFUzEfMB0G +A1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScw +JQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGKqtXETcvIorKA3Qdyu0togu8M1JAJke+ +WmmmO3I2F0zo37i7L3bhQEZ0ZQKQUgi0/6iMweDHiVYQOTPvaLRfX9ptI6GJXiKj +SgbwJ/BXufjpTjJ3Cj9BZPPrZe52/lSqfR0grvPXdMIKX/UIKFIIzFVd0g/bmoGl +u6GzwZTNVOAydTGRGmKy3nXiz0+J2ZGQD0EbtFpKd71ng+CT516nDOeB0/RSrFOy +A8dEJvt55cs0YFAQexvba9dHq198aMpunUEDEO5rmXteJajCq+TA81yc477OMUxk +Hl6AovWDfgzWyoxVjr7gvkkHD6MkQXpYHYTqWBLI4bft75PelAgxAgMBAAGjggM7 +MIIDNzAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9vY3NwLnBr +aS5ndmEuZXMwEgYDVR0TAQH/BAgwBgEB/wIBAjCCAjQGA1UdIASCAiswggInMIIC +IwYKKwYBBAG/VQIBADCCAhMwggHoBggrBgEFBQcCAjCCAdoeggHWAEEAdQB0AG8A +cgBpAGQAYQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAFIA +YQDtAHoAIABkAGUAIABsAGEAIABHAGUAbgBlAHIAYQBsAGkAdABhAHQAIABWAGEA +bABlAG4AYwBpAGEAbgBhAC4ADQAKAEwAYQAgAEQAZQBjAGwAYQByAGEAYwBpAPMA +bgAgAGQAZQAgAFAAcgDhAGMAdABpAGMAYQBzACAAZABlACAAQwBlAHIAdABpAGYA +aQBjAGEAYwBpAPMAbgAgAHEAdQBlACAAcgBpAGcAZQAgAGUAbAAgAGYAdQBuAGMA +aQBvAG4AYQBtAGkAZQBuAHQAbwAgAGQAZQAgAGwAYQAgAHAAcgBlAHMAZQBuAHQA +ZQAgAEEAdQB0AG8AcgBpAGQAYQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEA +YwBpAPMAbgAgAHMAZQAgAGUAbgBjAHUAZQBuAHQAcgBhACAAZQBuACAAbABhACAA +ZABpAHIAZQBjAGMAaQDzAG4AIAB3AGUAYgAgAGgAdAB0AHAAOgAvAC8AdwB3AHcA +LgBwAGsAaQAuAGcAdgBhAC4AZQBzAC8AYwBwAHMwJQYIKwYBBQUHAgEWGWh0dHA6 +Ly93d3cucGtpLmd2YS5lcy9jcHMwHQYDVR0OBBYEFHs100DSHHgZZu90ECjcPk+y +eAT8MIGVBgNVHSMEgY0wgYqAFHs100DSHHgZZu90ECjcPk+yeAT8oWykajBoMQsw +CQYDVQQGEwJFUzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0G +A1UECxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVu +Y2lhbmGCBDtF5WgwDQYJKoZIhvcNAQEFBQADggEBACRhTvW1yEICKrNcda3Fbcrn +lD+laJWIwVTAEGmiEi8YPyVQqHxK6sYJ2fR1xkDar1CdPaUWu20xxsdzCkj+IHLt +b8zog2EWRpABlUt9jppSCS/2bxzkoXHPjCpaF3ODR00PNvsETUlR4hTJZGH71BTg +9J63NI8KJr2XXPR5OkowGcytT6CYirQxlyric21+eLj4iIlPsSKRZEv1UN4D2+XF +ducTZnV+ZfsBn5OHiJ35Rld8TWCvmHMTI6QgkYH60GFmuH3Rr9ZvHmw96RH9qfmC +IoaZM3Fa6hlXPZHNqcCjbgcTpsnt+GijnsNacgmHKNHEc8RzGF9QdRYxn7fofMM= +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Label: "TWCA Root Certification Authority" +# Serial: 1 +# MD5 Fingerprint: aa:08:8f:f6:f9:7b:b7:f2:b1:a7:1e:9b:ea:ea:bd:79 +# SHA1 Fingerprint: cf:9e:87:6d:d3:eb:fc:42:26:97:a3:b5:a3:7a:a0:76:a9:06:23:48 +# SHA256 Fingerprint: bf:d8:8f:e1:10:1c:41:ae:3e:80:1b:f8:be:56:35:0e:e9:ba:d1:a6:b9:bd:51:5e:dc:5c:6d:5b:87:11:ac:44 +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES +MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU +V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz +WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO +LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE +AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH +K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX +RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z +rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx +3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq +hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC +MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls +XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D +lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn +aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ +YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Label: "Security Communication RootCA2" +# Serial: 0 +# MD5 Fingerprint: 6c:39:7d:a4:0e:55:59:b2:3f:d6:41:b1:12:50:de:43 +# SHA1 Fingerprint: 5f:3b:8c:f2:f8:10:b3:7d:78:b4:ce:ec:19:19:c3:73:34:b9:c7:74 +# SHA256 Fingerprint: 51:3b:2c:ec:b8:10:d4:cd:e5:dd:85:39:1a:df:c6:c2:dd:60:d8:7b:b7:36:d2:b5:21:48:4a:a4:7a:0e:be:f6 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority +# Subject: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority +# Label: "Hellenic Academic and Research Institutions RootCA 2011" +# Serial: 0 +# MD5 Fingerprint: 73:9f:4c:4b:73:5b:79:e9:fa:ba:1c:ef:6e:cb:d5:c9 +# SHA1 Fingerprint: fe:45:65:9b:79:03:5b:98:a1:61:b5:51:2e:ac:da:58:09:48:22:4d +# SHA256 Fingerprint: bc:10:4f:15:a4:8b:e7:09:dc:a5:42:a7:e1:d4:b9:df:6f:05:45:27:e8:02:ea:a9:2d:59:54:44:25:8a:fe:71 +-----BEGIN CERTIFICATE----- +MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix +RDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p +YyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw +NjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK +EztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl +cnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz +dYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ +fel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns +bgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD +75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP +FEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV +HRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp +5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu +b3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA +A4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p +6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 +TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7 +dIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys +Nnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI +l7WdmplNsDz4SgCbZN2fOUvRJ9e4 +-----END CERTIFICATE----- + +# Issuer: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Subject: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Label: "Actalis Authentication Root CA" +# Serial: 6271844772424770508 +# MD5 Fingerprint: 69:c1:0d:4f:07:a3:1b:c3:fe:56:3d:04:bc:11:f6:a6 +# SHA1 Fingerprint: f3:73:b3:87:06:5a:28:84:8a:f2:f3:4a:ce:19:2b:dd:c7:8e:9c:ac +# SHA256 Fingerprint: 55:92:60:84:ec:96:3a:64:b9:6e:2a:be:01:ce:0b:a8:6a:64:fb:fe:bc:c7:aa:b5:af:c1:55:b3:7f:d7:60:66 +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +# Issuer: O=Trustis Limited OU=Trustis FPS Root CA +# Subject: O=Trustis Limited OU=Trustis FPS Root CA +# Label: "Trustis FPS Root CA" +# Serial: 36053640375399034304724988975563710553 +# MD5 Fingerprint: 30:c9:e7:1e:6b:e6:14:eb:65:b2:16:69:20:31:67:4d +# SHA1 Fingerprint: 3b:c0:38:0b:33:c3:f6:a6:0c:86:15:22:93:d9:df:f5:4b:81:c0:04 +# SHA256 Fingerprint: c1:b4:82:99:ab:a5:20:8f:e9:63:0a:ce:55:ca:68:a0:3e:da:5a:51:9c:88:02:a0:d3:a6:73:be:8f:8e:55:7d +-----BEGIN CERTIFICATE----- +MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBF +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQL +ExNUcnVzdGlzIEZQUyBSb290IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTEx +MzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1RydXN0aXMgTGltaXRlZDEc +MBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQRUN+ +AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihH +iTHcDnlkH5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjj +vSkCqPoc4Vu5g6hBSLwacY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA +0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zto3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlB +OrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEAAaNTMFEwDwYDVR0TAQH/ +BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAdBgNVHQ4E +FgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01 +GX2cGE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmW +zaD+vkAMXBJV+JOCyinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP4 +1BIy+Q7DsdwyhEQsb8tGD+pmQQ9P8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZE +f1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHVl/9D7S3B2l0pKoU/rGXuhg8F +jZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYliB6XzCGcKQEN +ZetX2fNXlrtIzYE= +-----END CERTIFICATE----- + +# Issuer: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Subject: CN=StartCom Certification Authority O=StartCom Ltd. OU=Secure Digital Certificate Signing +# Label: "StartCom Certification Authority" +# Serial: 45 +# MD5 Fingerprint: c9:3b:0d:84:41:fc:a4:76:79:23:08:57:de:10:19:16 +# SHA1 Fingerprint: a3:f1:33:3f:e2:42:bf:cf:c5:d1:4e:8f:39:42:98:40:68:10:d1:a0 +# SHA256 Fingerprint: e1:78:90:ee:09:a3:fb:f4:f4:8b:9c:41:4a:17:d6:37:b7:a5:06:47:e9:bc:75:23:22:72:7f:cc:17:42:a9:11 +-----BEGIN CERTIFICATE----- +MIIHhzCCBW+gAwIBAgIBLTANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg +Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM3WhcNMzYwOTE3MTk0NjM2WjB9 +MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi +U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh +cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk +pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf +OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C +Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT +Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi +HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM +Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w ++2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+ +Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3 +Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B +26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID +AQABo4ICEDCCAgwwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFE4L7xqkQFulF2mHMMo0aEPQQa7yMB8GA1UdIwQYMBaAFE4L7xqkQFul +F2mHMMo0aEPQQa7yMIIBWgYDVR0gBIIBUTCCAU0wggFJBgsrBgEEAYG1NwEBATCC +ATgwLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5w +ZGYwNAYIKwYBBQUHAgEWKGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL2ludGVybWVk +aWF0ZS5wZGYwgc8GCCsGAQUFBwICMIHCMCcWIFN0YXJ0IENvbW1lcmNpYWwgKFN0 +YXJ0Q29tKSBMdGQuMAMCAQEagZZMaW1pdGVkIExpYWJpbGl0eSwgcmVhZCB0aGUg +c2VjdGlvbiAqTGVnYWwgTGltaXRhdGlvbnMqIG9mIHRoZSBTdGFydENvbSBDZXJ0 +aWZpY2F0aW9uIEF1dGhvcml0eSBQb2xpY3kgYXZhaWxhYmxlIGF0IGh0dHA6Ly93 +d3cuc3RhcnRzc2wuY29tL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgG +CWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eTANBgkqhkiG9w0BAQsFAAOCAgEAjo/n3JR5fPGFf59Jb2vKXfuM/gTF +wWLRfUKKvFO3lANmMD+x5wqnUCBVJX92ehQN6wQOQOY+2IirByeDqXWmN3PH/UvS +Ta0XQMhGvjt/UfzDtgUx3M2FIk5xt/JxXrAaxrqTi3iSSoX4eA+D/i+tLPfkpLst +0OcNOrg+zvZ49q5HJMqjNTbOx8aHmNrs++myziebiMMEofYLWWivydsQD032ZGNc +pRJvkrKTlMeIFw6Ttn5ii5B/q06f/ON1FE8qMt9bDeD1e5MNq6HPh+GlBEXoPBKl +CcWw0bdT82AUuoVpaiF8H3VhFyAXe2w7QSlc4axa0c2Mm+tgHRns9+Ww2vl5GKVF +P0lDV9LdJNUso/2RjSe15esUBppMeyG7Oq0wBhjA2MFrLH9ZXF2RsXAiV+uKa0hK +1Q8p7MZAwC+ITGgBF3f0JBlPvfrhsiAhS90a2Cl9qrjeVOwhVYBsHvUwyKMQ5bLm +KhQxw4UtjJixhlpPiVktucf3HMiKf8CdBUrmQk9io20ppB+Fq9vlgcitKj1MXVuE +JnHEhV5xJMqlG2zYYdMa4FTbzrqpMrUi9nNBCV24F10OD5mQ1kfabwo6YigUZ4LZ +8dCAWZvLMdibD4x3TrVoivJs9iQOLWxwxXPR3hTQcY+203sC9uO41Alua551hDnm +fyWl8kgAwKQB2j8= +-----END CERTIFICATE----- + +# Issuer: CN=StartCom Certification Authority G2 O=StartCom Ltd. +# Subject: CN=StartCom Certification Authority G2 O=StartCom Ltd. +# Label: "StartCom Certification Authority G2" +# Serial: 59 +# MD5 Fingerprint: 78:4b:fb:9e:64:82:0a:d3:b8:4c:62:f3:64:f2:90:64 +# SHA1 Fingerprint: 31:f1:fd:68:22:63:20:ee:c6:3b:3f:9d:ea:4a:3e:53:7c:7c:39:17 +# SHA256 Fingerprint: c7:ba:65:67:de:93:a7:98:ae:1f:aa:79:1e:71:2d:37:8f:ae:1f:93:c4:39:7f:ea:44:1b:b7:cb:e6:fd:59:95 +-----BEGIN CERTIFICATE----- +MIIFYzCCA0ugAwIBAgIBOzANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoGA1UEAxMjU3RhcnRDb20gQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkgRzIwHhcNMTAwMTAxMDEwMDAxWhcNMzkxMjMxMjM1 +OTAxWjBTMQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoG +A1UEAxMjU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgRzIwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2iTZbB7cgNr2Cu+EWIAOVeq8Oo1XJ +JZlKxdBWQYeQTSFgpBSHO839sj60ZwNq7eEPS8CRhXBF4EKe3ikj1AENoBB5uNsD +vfOpL9HG4A/LnooUCri99lZi8cVytjIl2bLzvWXFDSxu1ZJvGIsAQRSCb0AgJnoo +D/Uefyf3lLE3PbfHkffiAez9lInhzG7TNtYKGXmu1zSCZf98Qru23QumNK9LYP5/ +Q0kGi4xDuFby2X8hQxfqp0iVAXV16iulQ5XqFYSdCI0mblWbq9zSOdIxHWDirMxW +RST1HFSr7obdljKF+ExP6JV2tgXdNiNnvP8V4so75qbsO+wmETRIjfaAKxojAuuK +HDp2KntWFhxyKrOq42ClAJ8Em+JvHhRYW6Vsi1g8w7pOOlz34ZYrPu8HvKTlXcxN +nw3h3Kq74W4a7I/htkxNeXJdFzULHdfBR9qWJODQcqhaX2YtENwvKhOuJv4KHBnM +0D4LnMgJLvlblnpHnOl68wVQdJVznjAJ85eCXuaPOQgeWeU1FEIT/wCc976qUM/i +UUjXuG+v+E5+M5iSFGI6dWPPe/regjupuznixL0sAA7IF6wT700ljtizkC+p2il9 +Ha90OrInwMEePnWjFqmveiJdnxMaz6eg6+OGCtP95paV1yPIN93EfKo2rJgaErHg +TuixO/XWb/Ew1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE +AwIBBjAdBgNVHQ4EFgQUS8W0QGutHLOlHGVuRjaJhwUMDrYwDQYJKoZIhvcNAQEL +BQADggIBAHNXPyzVlTJ+N9uWkusZXn5T50HsEbZH77Xe7XRcxfGOSeD8bpkTzZ+K +2s06Ctg6Wgk/XzTQLwPSZh0avZyQN8gMjgdalEVGKua+etqhqaRpEpKwfTbURIfX +UfEpY9Z1zRbkJ4kd+MIySP3bmdCPX1R0zKxnNBFi2QwKN4fRoxdIjtIXHfbX/dtl +6/2o1PXWT6RbdejF0mCy2wl+JYt7ulKSnj7oxXehPOBKc2thz4bcQ///If4jXSRK +9dNtD2IEBVeC2m6kMyV5Sy5UGYvMLD0w6dEG/+gyRr61M3Z3qAFdlsHB1b6uJcDJ +HgoJIIihDsnzb02CVAAgp9KP5DlUFy6NHrgbuxu9mk47EDTcnIhT76IxW1hPkWLI +wpqazRVdOKnWvvgTtZ8SafJQYqz7Fzf07rh1Z2AQ+4NQ+US1dZxAF7L+/XldblhY +XzD8AK6vM8EOTmy6p6ahfzLbOOCxchcKK5HsamMm7YnUeMx0HgX4a/6ManY5Ka5l +IxKVCCIcl85bBu4M4ru8H0ST9tg4RQUh7eStqxK2A6RCLi3ECToDZ2mEmuFZkIoo +hdVddLHRDiBYmxOlsGOm7XtH/UVVMKTumtTm4ofvmMkyghEpIrwACjFeLQ/Ajulr +so8uBtjRkcfGEvRM/TAXw8HaOFvjqermobp573PYtlNXLfbQ4ddI +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 2 Root CA" +# Serial: 2 +# MD5 Fingerprint: 46:a7:d2:fe:45:fb:64:5a:a8:59:90:9b:78:44:9b:29 +# SHA1 Fingerprint: 49:0a:75:74:de:87:0a:47:fe:58:ee:f6:c7:6b:eb:c6:0b:12:40:99 +# SHA256 Fingerprint: 9a:11:40:25:19:7c:5b:b9:5d:94:e6:3d:55:cd:43:79:08:47:b6:46:b2:3c:df:11:ad:a4:a0:0e:ff:15:fb:48 +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 3 Root CA" +# Serial: 2 +# MD5 Fingerprint: 3d:3b:18:9e:2c:64:5a:e8:d5:88:ce:0e:f9:37:c2:ec +# SHA1 Fingerprint: da:fa:f7:fa:66:84:ec:06:8f:14:50:bd:c7:c2:81:a5:bc:a9:64:57 +# SHA256 Fingerprint: ed:f7:eb:bc:a2:7a:2a:38:4d:38:7b:7d:40:10:c6:66:e2:ed:b4:84:3e:4c:29:b4:ae:1d:5b:93:32:e6:b2:4d +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 3" +# Serial: 1 +# MD5 Fingerprint: ca:fb:40:a8:4e:39:92:8a:1d:fe:8e:2f:c4:27:ea:ef +# SHA1 Fingerprint: 55:a6:72:3e:cb:f2:ec:cd:c3:23:74:70:19:9d:2a:be:11:e3:81:d1 +# SHA256 Fingerprint: fd:73:da:d3:1c:64:4f:f1:b4:3b:ef:0c:cd:da:96:71:0b:9c:d9:87:5e:ca:7e:31:70:7a:f3:e9:6d:52:2b:bd +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- + +# Issuer: CN=EE Certification Centre Root CA O=AS Sertifitseerimiskeskus +# Subject: CN=EE Certification Centre Root CA O=AS Sertifitseerimiskeskus +# Label: "EE Certification Centre Root CA" +# Serial: 112324828676200291871926431888494945866 +# MD5 Fingerprint: 43:5e:88:d4:7d:1a:4a:7e:fd:84:2e:52:eb:01:d4:6f +# SHA1 Fingerprint: c9:a8:b9:e7:55:80:5e:58:e3:53:77:a7:25:eb:af:c3:7b:27:cc:d7 +# SHA256 Fingerprint: 3e:84:ba:43:42:90:85:16:e7:75:73:c0:99:2f:09:79:ca:08:4e:46:85:68:1f:f1:95:cc:ba:8a:22:9b:8a:76 +-----BEGIN CERTIFICATE----- +MIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1 +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG +CSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIwMTAxMDMwMTAxMDMwWhgPMjAzMDEy +MTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNl +ZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBS +b290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUy +euuOF0+W2Ap7kaJjbMeMTC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvO +bntl8jixwKIy72KyaOBhU8E2lf/slLo2rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIw +WFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw93X2PaRka9ZP585ArQ/d +MtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtNP2MbRMNE +1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/ +zQas8fElyalL1BSZMEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYB +BQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEF +BQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+RjxY6hUFaTlrg4wCQiZrxTFGGV +v9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqMlIpPnTX/dqQG +E5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u +uSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIW +iAYLtqZLICjU3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/v +GVCJYMzpJJUPwssd8m92kMfMdcGWxZ0= +-----END CERTIFICATE----- + +# Issuer: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı O=TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. (c) Aralık 2007 +# Subject: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı O=TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. (c) Aralık 2007 +# Label: "TURKTRUST Certificate Services Provider Root 2007" +# Serial: 1 +# MD5 Fingerprint: 2b:70:20:56:86:82:a0:18:c8:07:53:12:28:70:21:72 +# SHA1 Fingerprint: f1:7f:6f:b6:31:dc:99:e3:a3:c8:7f:fe:1c:f1:81:10:88:d9:60:33 +# SHA256 Fingerprint: 97:8c:d9:66:f2:fa:a0:7b:a7:aa:95:00:d9:c0:2e:9d:77:f2:cd:ad:a6:ad:6b:a7:4a:f4:b9:1c:66:59:3c:50 +-----BEGIN CERTIFICATE----- +MIIEPTCCAyWgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvzE/MD0GA1UEAww2VMOc +UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx +c8SxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMV4wXAYDVQQKDFVUw5xS +S1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kg +SGl6bWV0bGVyaSBBLsWeLiAoYykgQXJhbMSxayAyMDA3MB4XDTA3MTIyNTE4Mzcx +OVoXDTE3MTIyMjE4MzcxOVowgb8xPzA9BgNVBAMMNlTDnFJLVFJVU1QgRWxla3Ry +b25payBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsTELMAkGA1UEBhMC +VFIxDzANBgNVBAcMBkFua2FyYTFeMFwGA1UECgxVVMOcUktUUlVTVCBCaWxnaSDE +sGxldGnFn2ltIHZlIEJpbGnFn2ltIEfDvHZlbmxpxJ9pIEhpem1ldGxlcmkgQS7F +ni4gKGMpIEFyYWzEsWsgMjAwNzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAKu3PgqMyKVYFeaK7yc9SrToJdPNM8Ig3BnuiD9NYvDdE3ePYakqtdTyuTFY +KTsvP2qcb3N2Je40IIDu6rfwxArNK4aUyeNgsURSsloptJGXg9i3phQvKUmi8wUG ++7RP2qFsmmaf8EMJyupyj+sA1zU511YXRxcw9L6/P8JorzZAwan0qafoEGsIiveG +HtyaKhUG9qPw9ODHFNRRf8+0222vR5YXm3dx2KdxnSQM9pQ/hTEST7ruToK4uT6P +IzdezKKqdfcYbwnTrqdUKDT74eA7YH2gvnmJhsifLfkKS8RQouf9eRbHegsYz85M +733WB2+Y8a+xwXrXgTW4qhe04MsCAwEAAaNCMEAwHQYDVR0OBBYEFCnFkKslrxHk +Yb+j/4hhkeYO/pyBMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBBQUAA4IBAQAQDdr4Ouwo0RSVgrESLFF6QSU2TJ/sPx+EnWVUXKgW +AkD6bho3hO9ynYYKVZ1WKKxmLNA6VpM0ByWtCLCPyA8JWcqdmBzlVPi5RX9ql2+I +aE1KBiY3iAIOtsbWcpnOa3faYjGkVh+uX4132l32iPwa2Z61gfAyuOOI0JzzaqC5 +mxRZNTZPz/OOXl0XrRWV2N2y1RVuAE6zS89mlOTgzbUF2mNXi+WzqtvALhyQRNsa +XRik7r4EW5nVcV9VZWRi1aKbBFmGyGJ353yCRWo9F7/snXUMrqNvWtMvmDb08PUZ +qxFdyKbjKlhqQgnDvZImZjINXQhVdP+MmNAKpoRq0Tl9 +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 2009" +# Serial: 623603 +# MD5 Fingerprint: cd:e0:25:69:8d:47:ac:9c:89:35:90:f7:fd:51:3d:2f +# SHA1 Fingerprint: 58:e8:ab:b0:36:15:33:fb:80:f7:9b:1b:6d:29:d3:ff:8d:5f:00:f0 +# SHA256 Fingerprint: 49:e7:a4:42:ac:f0:ea:62:87:05:00:54:b5:25:64:b6:50:e4:f4:9e:42:e3:48:d6:aa:38:e0:39:e9:57:b1:c1 +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 EV 2009" +# Serial: 623604 +# MD5 Fingerprint: aa:c6:43:2c:5e:2d:cd:c4:34:c0:50:4f:11:02:4f:b6 +# SHA1 Fingerprint: 96:c9:1b:0b:95:b4:10:98:42:fa:d0:d8:22:79:fe:60:fa:b9:16:83 +# SHA256 Fingerprint: ee:c5:49:6b:98:8c:e9:86:25:b9:34:09:2e:ec:29:08:be:d0:b0:f3:16:c2:d4:73:0c:84:ea:f1:f3:d3:48:81 +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +# Issuer: CN=Autoridad de Certificacion Raiz del Estado Venezolano O=Sistema Nacional de Certificacion Electronica OU=Superintendencia de Servicios de Certificacion Electronica +# Subject: CN=PSCProcert O=Sistema Nacional de Certificacion Electronica OU=Proveedor de Certificados PROCERT +# Label: "PSCProcert" +# Serial: 11 +# MD5 Fingerprint: e6:24:e9:12:01:ae:0c:de:8e:85:c4:ce:a3:12:dd:ec +# SHA1 Fingerprint: 70:c1:8d:74:b4:28:81:0a:e4:fd:a5:75:d7:01:9f:99:b0:3d:50:74 +# SHA256 Fingerprint: 3c:fc:3c:14:d1:f6:84:ff:17:e3:8c:43:ca:44:0c:00:b9:67:ec:93:3e:8b:fe:06:4c:a1:d7:2c:90:f2:ad:b0 +-----BEGIN CERTIFICATE----- +MIIJhjCCB26gAwIBAgIBCzANBgkqhkiG9w0BAQsFADCCAR4xPjA8BgNVBAMTNUF1 +dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIFJhaXogZGVsIEVzdGFkbyBWZW5lem9s +YW5vMQswCQYDVQQGEwJWRTEQMA4GA1UEBxMHQ2FyYWNhczEZMBcGA1UECBMQRGlz +dHJpdG8gQ2FwaXRhbDE2MDQGA1UEChMtU2lzdGVtYSBOYWNpb25hbCBkZSBDZXJ0 +aWZpY2FjaW9uIEVsZWN0cm9uaWNhMUMwQQYDVQQLEzpTdXBlcmludGVuZGVuY2lh +IGRlIFNlcnZpY2lvcyBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9uaWNhMSUwIwYJ +KoZIhvcNAQkBFhZhY3JhaXpAc3VzY2VydGUuZ29iLnZlMB4XDTEwMTIyODE2NTEw +MFoXDTIwMTIyNTIzNTk1OVowgdExJjAkBgkqhkiG9w0BCQEWF2NvbnRhY3RvQHBy +b2NlcnQubmV0LnZlMQ8wDQYDVQQHEwZDaGFjYW8xEDAOBgNVBAgTB01pcmFuZGEx +KjAoBgNVBAsTIVByb3ZlZWRvciBkZSBDZXJ0aWZpY2Fkb3MgUFJPQ0VSVDE2MDQG +A1UEChMtU2lzdGVtYSBOYWNpb25hbCBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9u +aWNhMQswCQYDVQQGEwJWRTETMBEGA1UEAxMKUFNDUHJvY2VydDCCAiIwDQYJKoZI +hvcNAQEBBQADggIPADCCAgoCggIBANW39KOUM6FGqVVhSQ2oh3NekS1wwQYalNo9 +7BVCwfWMrmoX8Yqt/ICV6oNEolt6Vc5Pp6XVurgfoCfAUFM+jbnADrgV3NZs+J74 +BCXfgI8Qhd19L3uA3VcAZCP4bsm+lU/hdezgfl6VzbHvvnpC2Mks0+saGiKLt38G +ieU89RLAu9MLmV+QfI4tL3czkkohRqipCKzx9hEC2ZUWno0vluYC3XXCFCpa1sl9 +JcLB/KpnheLsvtF8PPqv1W7/U0HU9TI4seJfxPmOEO8GqQKJ/+MMbpfg353bIdD0 +PghpbNjU5Db4g7ayNo+c7zo3Fn2/omnXO1ty0K+qP1xmk6wKImG20qCZyFSTXai2 +0b1dCl53lKItwIKOvMoDKjSuc/HUtQy9vmebVOvh+qBa7Dh+PsHMosdEMXXqP+UH +0quhJZb25uSgXTcYOWEAM11G1ADEtMo88aKjPvM6/2kwLkDd9p+cJsmWN63nOaK/ +6mnbVSKVUyqUtd+tFjiBdWbjxywbk5yqjKPK2Ww8F22c3HxT4CAnQzb5EuE8XL1m +v6JpIzi4mWCZDlZTOpx+FIywBm/xhnaQr/2v/pDGj59/i5IjnOcVdo/Vi5QTcmn7 +K2FjiO/mpF7moxdqWEfLcU8UC17IAggmosvpr2uKGcfLFFb14dq12fy/czja+eev +bqQ34gcnAgMBAAGjggMXMIIDEzASBgNVHRMBAf8ECDAGAQH/AgEBMDcGA1UdEgQw +MC6CD3N1c2NlcnRlLmdvYi52ZaAbBgVghl4CAqASDBBSSUYtRy0yMDAwNDAzNi0w +MB0GA1UdDgQWBBRBDxk4qpl/Qguk1yeYVKIXTC1RVDCCAVAGA1UdIwSCAUcwggFD +gBStuyIdxuDSAaj9dlBSk+2YwU2u06GCASakggEiMIIBHjE+MDwGA1UEAxM1QXV0 +b3JpZGFkIGRlIENlcnRpZmljYWNpb24gUmFpeiBkZWwgRXN0YWRvIFZlbmV6b2xh +bm8xCzAJBgNVBAYTAlZFMRAwDgYDVQQHEwdDYXJhY2FzMRkwFwYDVQQIExBEaXN0 +cml0byBDYXBpdGFsMTYwNAYDVQQKEy1TaXN0ZW1hIE5hY2lvbmFsIGRlIENlcnRp +ZmljYWNpb24gRWxlY3Ryb25pY2ExQzBBBgNVBAsTOlN1cGVyaW50ZW5kZW5jaWEg +ZGUgU2VydmljaW9zIGRlIENlcnRpZmljYWNpb24gRWxlY3Ryb25pY2ExJTAjBgkq +hkiG9w0BCQEWFmFjcmFpekBzdXNjZXJ0ZS5nb2IudmWCAQowDgYDVR0PAQH/BAQD +AgEGME0GA1UdEQRGMESCDnByb2NlcnQubmV0LnZloBUGBWCGXgIBoAwMClBTQy0w +MDAwMDKgGwYFYIZeAgKgEgwQUklGLUotMzE2MzUzNzMtNzB2BgNVHR8EbzBtMEag +RKBChkBodHRwOi8vd3d3LnN1c2NlcnRlLmdvYi52ZS9sY3IvQ0VSVElGSUNBRE8t +UkFJWi1TSEEzODRDUkxERVIuY3JsMCOgIaAfhh1sZGFwOi8vYWNyYWl6LnN1c2Nl +cnRlLmdvYi52ZTA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUHMAGGG2h0dHA6Ly9v +Y3NwLnN1c2NlcnRlLmdvYi52ZTBBBgNVHSAEOjA4MDYGBmCGXgMBAjAsMCoGCCsG +AQUFBwIBFh5odHRwOi8vd3d3LnN1c2NlcnRlLmdvYi52ZS9kcGMwDQYJKoZIhvcN +AQELBQADggIBACtZ6yKZu4SqT96QxtGGcSOeSwORR3C7wJJg7ODU523G0+1ng3dS +1fLld6c2suNUvtm7CpsR72H0xpkzmfWvADmNg7+mvTV+LFwxNG9s2/NkAZiqlCxB +3RWGymspThbASfzXg0gTB1GEMVKIu4YXx2sviiCtxQuPcD4quxtxj7mkoP3Yldmv +Wb8lK5jpY5MvYB7Eqvh39YtsL+1+LrVPQA3uvFd359m21D+VJzog1eWuq2w1n8Gh +HVnchIHuTQfiSLaeS5UtQbHh6N5+LwUeaO6/u5BlOsju6rEYNxxik6SgMexxbJHm +pHmJWhSnFFAFTKQAVzAswbVhltw+HoSvOULP5dAssSS830DD7X9jSr3hTxJkhpXz +sOfIt+FTvZLm8wyWuevo5pLtp4EJFAv8lXrPj9Y0TzYS3F7RNHXGRoAvlQSMx4bE +qCaJqD8Zm4G7UaRKhqsLEQ+xrmNTbSjq3TNWOByyrYDT13K9mmyZY+gAu0F2Bbdb +mRiKw7gSXFbPVgx96OLP7bx0R/vu0xdOIk9W/1DzLuY5poLWccret9W6aAjtmcz9 +opLLabid+Qqkpj5PkygqYWwHJgD/ll9ohri4zspV4KuxPX+Y1zMOWj3YeMLEYC/H +YvBhkdI4sPaeVdtAgAUSM84dkpvRabP/v/GSCmE1P93+hvS84Bpxs2Km +-----END CERTIFICATE----- + +# Issuer: CN=China Internet Network Information Center EV Certificates Root O=China Internet Network Information Center +# Subject: CN=China Internet Network Information Center EV Certificates Root O=China Internet Network Information Center +# Label: "China Internet Network Information Center EV Certificates Root" +# Serial: 1218379777 +# MD5 Fingerprint: 55:5d:63:00:97:bd:6a:97:f5:67:ab:4b:fb:6e:63:15 +# SHA1 Fingerprint: 4f:99:aa:93:fb:2b:d1:37:26:a1:99:4a:ce:7f:f0:05:f2:93:5d:1e +# SHA256 Fingerprint: 1c:01:c6:f4:db:b2:fe:fc:22:55:8b:2b:ca:32:56:3f:49:84:4a:cf:c3:2b:7b:e4:b0:ff:59:9f:9e:8c:7a:f7 +-----BEGIN CERTIFICATE----- +MIID9zCCAt+gAwIBAgIESJ8AATANBgkqhkiG9w0BAQUFADCBijELMAkGA1UEBhMC +Q04xMjAwBgNVBAoMKUNoaW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24g +Q2VudGVyMUcwRQYDVQQDDD5DaGluYSBJbnRlcm5ldCBOZXR3b3JrIEluZm9ybWF0 +aW9uIENlbnRlciBFViBDZXJ0aWZpY2F0ZXMgUm9vdDAeFw0xMDA4MzEwNzExMjVa +Fw0zMDA4MzEwNzExMjVaMIGKMQswCQYDVQQGEwJDTjEyMDAGA1UECgwpQ2hpbmEg +SW50ZXJuZXQgTmV0d29yayBJbmZvcm1hdGlvbiBDZW50ZXIxRzBFBgNVBAMMPkNo +aW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24gQ2VudGVyIEVWIENlcnRp +ZmljYXRlcyBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm35z +7r07eKpkQ0H1UN+U8i6yjUqORlTSIRLIOTJCBumD1Z9S7eVnAztUwYyZmczpwA// +DdmEEbK40ctb3B75aDFk4Zv6dOtouSCV98YPjUesWgbdYavi7NifFy2cyjw1l1Vx +zUOFsUcW9SxTgHbP0wBkvUCZ3czY28Sf1hNfQYOL+Q2HklY0bBoQCxfVWhyXWIQ8 +hBouXJE0bhlffxdpxWXvayHG1VA6v2G5BY3vbzQ6sm8UY78WO5upKv23KzhmBsUs +4qpnHkWnjQRmQvaPK++IIGmPMowUc9orhpFjIpryp9vOiYurXccUwVswah+xt54u +gQEC7c+WXmPbqOY4twIDAQABo2MwYTAfBgNVHSMEGDAWgBR8cks5x8DbYqVPm6oY +NJKiyoOCWTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4E +FgQUfHJLOcfA22KlT5uqGDSSosqDglkwDQYJKoZIhvcNAQEFBQADggEBACrDx0M3 +j92tpLIM7twUbY8opJhJywyA6vPtI2Z1fcXTIWd50XPFtQO3WKwMVC/GVhMPMdoG +52U7HW8228gd+f2ABsqjPWYWqJ1MFn3AlUa1UeTiH9fqBk1jjZaM7+czV0I664zB +echNdn3e9rG3geCg+aF4RhcaVpjwTj2rHO3sOdwHSPdj/gauwqRcalsyiMXHM4Ws +ZkJHwlgkmeHlPuV1LI5D1l08eB6olYIpUNHRFrrvwb562bTYzB5MRuF3sTGrvSrI +zo9uoV1/A3U05K2JRVRevq4opbs/eHnrc7MKDf2+yfdWrPa37S+bISnHOLaVxATy +wy39FCqQmbkHzJ8= +-----END CERTIFICATE----- + +# Issuer: CN=Swisscom Root CA 2 O=Swisscom OU=Digital Certificate Services +# Subject: CN=Swisscom Root CA 2 O=Swisscom OU=Digital Certificate Services +# Label: "Swisscom Root CA 2" +# Serial: 40698052477090394928831521023204026294 +# MD5 Fingerprint: 5b:04:69:ec:a5:83:94:63:18:a7:86:d0:e4:f2:6e:19 +# SHA1 Fingerprint: 77:47:4f:c6:30:e4:0f:4c:47:64:3f:84:ba:b8:c6:95:4a:8a:41:ec +# SHA256 Fingerprint: f0:9b:12:2c:71:14:f4:a0:9b:d4:ea:4f:4a:99:d5:58:b4:6e:4c:25:cd:81:14:0d:29:c0:56:13:91:4c:38:41 +-----BEGIN CERTIFICATE----- +MIIF2TCCA8GgAwIBAgIQHp4o6Ejy5e/DfEoeWhhntjANBgkqhkiG9w0BAQsFADBk +MQswCQYDVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0 +YWwgQ2VydGlmaWNhdGUgU2VydmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3Qg +Q0EgMjAeFw0xMTA2MjQwODM4MTRaFw0zMTA2MjUwNzM4MTRaMGQxCzAJBgNVBAYT +AmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGlnaXRhbCBDZXJ0aWZp +Y2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAyMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAlUJOhJ1R5tMJ6HJaI2nbeHCOFvEr +jw0DzpPMLgAIe6szjPTpQOYXTKueuEcUMncy3SgM3hhLX3af+Dk7/E6J2HzFZ++r +0rk0X2s682Q2zsKwzxNoysjL67XiPS4h3+os1OD5cJZM/2pYmLcX5BtS5X4HAB1f +2uY+lQS3aYg5oUFgJWFLlTloYhyxCwWJwDaCFCE/rtuh/bxvHGCGtlOUSbkrRsVP +ACu/obvLP+DHVxxX6NZp+MEkUp2IVd3Chy50I9AU/SpHWrumnf2U5NGKpV+GY3aF +y6//SSj8gO1MedK75MDvAe5QQQg1I3ArqRa0jG6F6bYRzzHdUyYb3y1aSgJA/MTA +tukxGggo5WDDH8SQjhBiYEQN7Aq+VRhxLKX0srwVYv8c474d2h5Xszx+zYIdkeNL +6yxSNLCK/RJOlrDrcH+eOfdmQrGrrFLadkBXeyq96G4DsguAhYidDMfCd7Camlf0 +uPoTXGiTOmekl9AbmbeGMktg2M7v0Ax/lZ9vh0+Hio5fCHyqW/xavqGRn1V9TrAL +acywlKinh/LTSlDcX3KwFnUey7QYYpqwpzmqm59m2I2mbJYV4+by+PGDYmy7Velh +k6M99bFXi08jsJvllGov34zflVEpYKELKeRcVVi3qPyZ7iVNTA6z00yPhOgpD/0Q +VAKFyPnlw4vP5w8CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0hBBYw +FDASBgdghXQBUwIBBgdghXQBUwIBMBIGA1UdEwEB/wQIMAYBAf8CAQcwHQYDVR0O +BBYEFE0mICKJS9PVpAqhb97iEoHF8TwuMB8GA1UdIwQYMBaAFE0mICKJS9PVpAqh +b97iEoHF8TwuMA0GCSqGSIb3DQEBCwUAA4ICAQAyCrKkG8t9voJXiblqf/P0wS4R +fbgZPnm3qKhyN2abGu2sEzsOv2LwnN+ee6FTSA5BesogpxcbtnjsQJHzQq0Qw1zv +/2BZf82Fo4s9SBwlAjxnffUy6S8w5X2lejjQ82YqZh6NM4OKb3xuqFp1mrjX2lhI +REeoTPpMSQpKwhI3qEAMw8jh0FcNlzKVxzqfl9NX+Ave5XLzo9v/tdhZsnPdTSpx +srpJ9csc1fV5yJmz/MFMdOO0vSk3FQQoHt5FRnDsr7p4DooqzgB53MBfGWcsa0vv +aGgLQ+OswWIJ76bdZWGgr4RVSJFSHMYlkSrQwSIjYVmvRRGFHQEkNI/Ps/8XciAT +woCqISxxOQ7Qj1zB09GOInJGTB2Wrk9xseEFKZZZ9LuedT3PDTcNYtsmjGOpI99n +Bjx8Oto0QuFmtEYE3saWmA9LSHokMnWRn6z3aOkquVVlzl1h0ydw2Df+n7mvoC5W +t6NlUe07qxS/TFED6F+KBZvuim6c779o+sjaC+NCydAXFJy3SuCvkychVSa1ZC+N +8f+mQAWFBVzKBxlcCxMoTFh/wqXvRdpg065lYZ1Tg3TCrvJcwhbtkj6EPnNgiLx2 +9CzP0H1907he0ZESEOnN3col49XtmS++dYFLJPlFRpTJKSFTnCZFqhMX5OfNeOI5 +wSsSnqaeG8XmDtkx2Q== +-----END CERTIFICATE----- + +# Issuer: CN=Swisscom Root EV CA 2 O=Swisscom OU=Digital Certificate Services +# Subject: CN=Swisscom Root EV CA 2 O=Swisscom OU=Digital Certificate Services +# Label: "Swisscom Root EV CA 2" +# Serial: 322973295377129385374608406479535262296 +# MD5 Fingerprint: 7b:30:34:9f:dd:0a:4b:6b:35:ca:31:51:28:5d:ae:ec +# SHA1 Fingerprint: e7:a1:90:29:d3:d5:52:dc:0d:0f:c6:92:d3:ea:88:0d:15:2e:1a:6b +# SHA256 Fingerprint: d9:5f:ea:3c:a4:ee:dc:e7:4c:d7:6e:75:fc:6d:1f:f6:2c:44:1f:0f:a8:bc:77:f0:34:b1:9e:5d:b2:58:01:5d +-----BEGIN CERTIFICATE----- +MIIF4DCCA8igAwIBAgIRAPL6ZOJ0Y9ON/RAdBB92ylgwDQYJKoZIhvcNAQELBQAw +ZzELMAkGA1UEBhMCY2gxETAPBgNVBAoTCFN3aXNzY29tMSUwIwYDVQQLExxEaWdp +dGFsIENlcnRpZmljYXRlIFNlcnZpY2VzMR4wHAYDVQQDExVTd2lzc2NvbSBSb290 +IEVWIENBIDIwHhcNMTEwNjI0MDk0NTA4WhcNMzEwNjI1MDg0NTA4WjBnMQswCQYD +VQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0YWwgQ2Vy +dGlmaWNhdGUgU2VydmljZXMxHjAcBgNVBAMTFVN3aXNzY29tIFJvb3QgRVYgQ0Eg +MjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMT3HS9X6lds93BdY7Bx +UglgRCgzo3pOCvrY6myLURYaVa5UJsTMRQdBTxB5f3HSek4/OE6zAMaVylvNwSqD +1ycfMQ4jFrclyxy0uYAyXhqdk/HoPGAsp15XGVhRXrwsVgu42O+LgrQ8uMIkqBPH +oCE2G3pXKSinLr9xJZDzRINpUKTk4RtiGZQJo/PDvO/0vezbE53PnUgJUmfANykR +HvvSEaeFGHR55E+FFOtSN+KxRdjMDUN/rhPSays/p8LiqG12W0OfvrSdsyaGOx9/ +5fLoZigWJdBLlzin5M8J0TbDC77aO0RYjb7xnglrPvMyxyuHxuxenPaHZa0zKcQv +idm5y8kDnftslFGXEBuGCxobP/YCfnvUxVFkKJ3106yDgYjTdLRZncHrYTNaRdHL +OdAGalNgHa/2+2m8atwBz735j9m9W8E6X47aD0upm50qKGsaCnw8qyIL5XctcfaC +NYGu+HuB5ur+rPQam3Rc6I8k9l2dRsQs0h4rIWqDJ2dVSqTjyDKXZpBy2uPUZC5f +46Fq9mDU5zXNysRojddxyNMkM3OxbPlq4SjbX8Y96L5V5jcb7STZDxmPX2MYWFCB +UWVv8p9+agTnNCRxunZLWB4ZvRVgRaoMEkABnRDixzgHcgplwLa7JSnaFp6LNYth +7eVxV4O1PHGf40+/fh6Bn0GXAgMBAAGjgYYwgYMwDgYDVR0PAQH/BAQDAgGGMB0G +A1UdIQQWMBQwEgYHYIV0AVMCAgYHYIV0AVMCAjASBgNVHRMBAf8ECDAGAQH/AgED +MB0GA1UdDgQWBBRF2aWBbj2ITY1x0kbBbkUe88SAnTAfBgNVHSMEGDAWgBRF2aWB +bj2ITY1x0kbBbkUe88SAnTANBgkqhkiG9w0BAQsFAAOCAgEAlDpzBp9SSzBc1P6x +XCX5145v9Ydkn+0UjrgEjihLj6p7jjm02Vj2e6E1CqGdivdj5eu9OYLU43otb98T +PLr+flaYC/NUn81ETm484T4VvwYmneTwkLbUwp4wLh/vx3rEUMfqe9pQy3omywC0 +Wqu1kx+AiYQElY2NfwmTv9SoqORjbdlk5LgpWgi/UOGED1V7XwgiG/W9mR4U9s70 +WBCCswo9GcG/W6uqmdjyMb3lOGbcWAXH7WMaLgqXfIeTK7KK4/HsGOV1timH59yL +Gn602MnTihdsfSlEvoqq9X46Lmgxk7lq2prg2+kupYTNHAq4Sgj5nPFhJpiTt3tm +7JFe3VE/23MPrQRYCd0EApUKPtN236YQHoA96M2kZNEzx5LH4k5E4wnJTsJdhw4S +nr8PyQUQ3nqjsTzyP6WqJ3mtMX0f/fwZacXduT98zca0wjAefm6S139hdlqP65VN +vBFuIXxZN5nQBrz5Bm0yFqXZaajh3DyAHmBR3NdUIR7KYndP+tiPsys6DXhyyWhB +WkdKwqPrGtcKqzwyVcgKEZzfdNbwQBUdyLmPtTbFr/giuMod89a2GQ+fYWVq6nTI +fI/DT11lgh/ZDYnadXL77/FHZxOzyNEZiCcmmpl5fx7kLD977vHeTYuWl8PVP3wb +I+2ksx0WckNLIOFZfsLorSa/ovc= +-----END CERTIFICATE----- + +# Issuer: CN=CA Disig Root R1 O=Disig a.s. +# Subject: CN=CA Disig Root R1 O=Disig a.s. +# Label: "CA Disig Root R1" +# Serial: 14052245610670616104 +# MD5 Fingerprint: be:ec:11:93:9a:f5:69:21:bc:d7:c1:c0:67:89:cc:2a +# SHA1 Fingerprint: 8e:1c:74:f8:a6:20:b9:e5:8a:f4:61:fa:ec:2b:47:56:51:1a:52:c6 +# SHA256 Fingerprint: f9:6f:23:f4:c3:e7:9c:07:7a:46:98:8d:5a:f5:90:06:76:a0:f0:39:cb:64:5d:d1:75:49:b2:16:c8:24:40:ce +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAMMDmu5QkG4oMA0GCSqGSIb3DQEBBQUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIxMB4XDTEyMDcxOTA5MDY1NloXDTQy +MDcxOTA5MDY1NlowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjEw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCqw3j33Jijp1pedxiy3QRk +D2P9m5YJgNXoqqXinCaUOuiZc4yd39ffg/N4T0Dhf9Kn0uXKE5Pn7cZ3Xza1lK/o +OI7bm+V8u8yN63Vz4STN5qctGS7Y1oprFOsIYgrY3LMATcMjfF9DCCMyEtztDK3A +fQ+lekLZWnDZv6fXARz2m6uOt0qGeKAeVjGu74IKgEH3G8muqzIm1Cxr7X1r5OJe +IgpFy4QxTaz+29FHuvlglzmxZcfe+5nkCiKxLU3lSCZpq+Kq8/v8kiky6bM+TR8n +oc2OuRf7JT7JbvN32g0S9l3HuzYQ1VTW8+DiR0jm3hTaYVKvJrT1cU/J19IG32PK +/yHoWQbgCNWEFVP3Q+V8xaCJmGtzxmjOZd69fwX3se72V6FglcXM6pM6vpmumwKj +rckWtc7dXpl4fho5frLABaTAgqWjR56M6ly2vGfb5ipN0gTco65F97yLnByn1tUD +3AjLLhbKXEAz6GfDLuemROoRRRw1ZS0eRWEkG4IupZ0zXWX4Qfkuy5Q/H6MMMSRE +7cderVC6xkGbrPAXZcD4XW9boAo0PO7X6oifmPmvTiT6l7Jkdtqr9O3jw2Dv1fkC +yC2fg69naQanMVXVz0tv/wQFx1isXxYb5dKj6zHbHzMVTdDypVP1y+E9Tmgt2BLd +qvLmTZtJ5cUoobqwWsagtQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUiQq0OJMa5qvum5EY+fU8PjXQ04IwDQYJKoZI +hvcNAQEFBQADggIBADKL9p1Kyb4U5YysOMo6CdQbzoaz3evUuii+Eq5FLAR0rBNR +xVgYZk2C2tXck8An4b58n1KeElb21Zyp9HWc+jcSjxyT7Ff+Bw+r1RL3D65hXlaA +SfX8MPWbTx9BLxyE04nH4toCdu0Jz2zBuByDHBb6lM19oMgY0sidbvW9adRtPTXo +HqJPYNcHKfyyo6SdbhWSVhlMCrDpfNIZTUJG7L399ldb3Zh+pE3McgODWF3vkzpB +emOqfDqo9ayk0d2iLbYq/J8BjuIQscTK5GfbVSUZP/3oNn6z4eGBrxEWi1CXYBmC +AMBrTXO40RMHPuq2MU/wQppt4hF05ZSsjYSVPCGvxdpHyN85YmLLW1AL14FABZyb +7bq2ix4Eb5YgOe2kfSnbSM6C3NQCjR0EMVrHS/BsYVLXtFHCgWzN4funodKSds+x +DzdYpPJScWc/DIh4gInByLUfkmO+p3qKViwaqKactV2zY9ATIKHrkWzQjX2v3wvk +F7mGnjixlAxYjOBVqjtjbZqJYLhkKpLGN/R+Q0O3c+gB53+XD9fyexn9GtePyfqF +a3qdnom2piiZk4hA9z7NUaPK6u95RyG1/jLix8NRb76AdPCkwzryT+lf3xkK8jsT +Q6wxpLPn6/wY1gGp8yqPNg7rtLG8t0zJa7+h89n07eLw4+1knj0vllJPgFOL +-----END CERTIFICATE----- + +# Issuer: CN=CA Disig Root R2 O=Disig a.s. +# Subject: CN=CA Disig Root R2 O=Disig a.s. +# Label: "CA Disig Root R2" +# Serial: 10572350602393338211 +# MD5 Fingerprint: 26:01:fb:d8:27:a7:17:9a:45:54:38:1a:43:01:3b:03 +# SHA1 Fingerprint: b5:61:eb:ea:a4:de:e4:25:4b:69:1a:98:a5:57:47:c2:34:c7:d9:71 +# SHA256 Fingerprint: e2:3d:4a:03:6d:7b:70:e9:f5:95:b1:42:20:79:d2:b9:1e:df:bb:1f:b6:51:a0:63:3e:aa:8a:9d:c5:f8:07:03 +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +# Issuer: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Subject: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Label: "ACCVRAIZ1" +# Serial: 6828503384748696800 +# MD5 Fingerprint: d0:a0:5a:ee:05:b6:09:94:21:a1:7d:f1:b2:29:82:02 +# SHA1 Fingerprint: 93:05:7a:88:15:c6:4f:ce:88:2f:fa:91:16:52:28:78:bc:53:64:17 +# SHA256 Fingerprint: 9a:6e:c0:12:e1:a7:da:9d:be:34:19:4d:47:8a:d7:c0:db:18:22:fb:07:1d:f1:29:81:49:6e:d1:04:38:41:13 +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Label: "TWCA Global Root CA" +# Serial: 3262 +# MD5 Fingerprint: f9:03:7e:cf:e6:9e:3c:73:7a:2a:90:07:69:ff:2b:96 +# SHA1 Fingerprint: 9c:bb:48:53:f6:a4:f6:d3:52:a4:e8:32:52:55:60:13:f5:ad:af:65 +# SHA256 Fingerprint: 59:76:90:07:f7:68:5d:0f:cd:50:87:2f:9f:95:d5:75:5a:5b:2b:45:7d:81:f3:69:2b:61:0a:98:67:2f:0e:1b +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- + +# Issuer: CN=TeliaSonera Root CA v1 O=TeliaSonera +# Subject: CN=TeliaSonera Root CA v1 O=TeliaSonera +# Label: "TeliaSonera Root CA v1" +# Serial: 199041966741090107964904287217786801558 +# MD5 Fingerprint: 37:41:49:1b:18:56:9a:26:f5:ad:c2:66:fb:40:a5:4c +# SHA1 Fingerprint: 43:13:bb:96:f1:d5:86:9b:c1:4e:6a:92:f6:cf:f6:34:69:87:82:37 +# SHA256 Fingerprint: dd:69:36:fe:21:f8:f0:77:c1:23:a1:a5:21:c1:22:24:f7:22:55:b7:3e:03:a7:26:06:93:e8:a2:4b:0f:a3:89 +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw +NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv +b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD +VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F +VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 +7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X +Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ +/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs +81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm +dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe +Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu +sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 +pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs +slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ +arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD +VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG +9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl +dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj +TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed +Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 +Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI +OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 +vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW +t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn +HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx +SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- + +# Issuer: CN=E-Tugra Certification Authority O=E-Tuğra EBG Bilişim Teknolojileri ve Hizmetleri A.Ş. OU=E-Tugra Sertifikasyon Merkezi +# Subject: CN=E-Tugra Certification Authority O=E-Tuğra EBG Bilişim Teknolojileri ve Hizmetleri A.Ş. OU=E-Tugra Sertifikasyon Merkezi +# Label: "E-Tugra Certification Authority" +# Serial: 7667447206703254355 +# MD5 Fingerprint: b8:a1:03:63:b0:bd:21:71:70:8a:6f:13:3a:bb:79:49 +# SHA1 Fingerprint: 51:c6:e7:08:49:06:6e:f3:92:d4:5c:a0:0d:6d:a3:62:8f:c3:52:39 +# SHA256 Fingerprint: b0:bf:d5:2b:b0:d7:d9:bd:92:bf:5d:4d:c1:3d:a2:55:c0:2c:54:2f:37:83:65:ea:89:39:11:f5:5e:55:f2:3c +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNV +BAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBC +aWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNV +BAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQDDB9FLVR1 +Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMwNTEyMDk0OFoXDTIz +MDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+ +BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhp +em1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN +ZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vU/kwVRHoViVF56C/UY +B4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vdhQd2h8y/L5VMzH2nPbxH +D5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5KCKpbknSF +Q9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEo +q1+gElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3D +k14opz8n8Y4e0ypQBaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcH +fC425lAcP9tDJMW/hkd5s3kc91r0E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsut +dEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gzrt48Ue7LE3wBf4QOXVGUnhMM +ti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAqjqFGOjGY5RH8 +zLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn +rFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUX +U8u3Zg5mTPj5dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6 +Jyr+zE7S6E5UMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5 +XPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAF +Nzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAKkEh47U6YA5n+KGCR +HTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jOXKqY +GwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c +77NCR807VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3 ++GbHeJAAFS6LrVE1Uweoa2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WK +vJUawSg5TB9D0pH0clmKuVb8P7Sd2nCcdlqMQ1DujjByTd//SffGqWfZbawCEeI6 +FiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEVKV0jq9BgoRJP3vQXzTLl +yb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gTDx4JnW2P +AJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpD +y4Q08ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8d +NL/+I5c30jn6PQ0GC7TbO6Orb1wdtn7os4I07QZcJA== +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 2" +# Serial: 1 +# MD5 Fingerprint: 2b:9b:9e:e4:7b:6c:1f:00:72:1a:cc:c1:77:79:df:6a +# SHA1 Fingerprint: 59:0d:2d:7d:88:4f:40:2e:61:7e:a5:62:32:17:65:cf:17:d8:94:e9 +# SHA256 Fingerprint: 91:e2:f5:78:8d:58:10:eb:a7:ba:58:73:7d:e1:54:8a:8e:ca:cd:01:45:98:bc:0b:14:3e:04:1b:17:05:25:52 +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot 2011 O=Atos +# Subject: CN=Atos TrustedRoot 2011 O=Atos +# Label: "Atos TrustedRoot 2011" +# Serial: 6643877497813316402 +# MD5 Fingerprint: ae:b9:c4:32:4b:ac:7f:5d:66:cc:77:94:bb:2a:77:56 +# SHA1 Fingerprint: 2b:b1:f5:3e:55:0c:1d:c5:f1:d4:e6:b7:6a:46:4b:55:06:02:ac:21 +# SHA256 Fingerprint: f3:56:be:a2:44:b7:a9:1e:b3:5d:53:ca:9a:d7:86:4a:ce:01:8e:2d:35:d5:f8:f9:6d:df:68:a6:f4:1a:a4:74 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 1 G3" +# Serial: 687049649626669250736271037606554624078720034195 +# MD5 Fingerprint: a4:bc:5b:3f:fe:37:9a:fa:64:f0:e2:fa:05:3d:0b:ab +# SHA1 Fingerprint: 1b:8e:ea:57:96:29:1a:c9:39:ea:b8:0a:81:1a:73:73:c0:93:79:67 +# SHA256 Fingerprint: 8a:86:6f:d1:b2:76:b5:7e:57:8e:92:1c:65:82:8a:2b:ed:58:e9:f2:f2:88:05:41:34:b7:f1:f4:bf:c9:cc:74 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 2 G3" +# Serial: 390156079458959257446133169266079962026824725800 +# MD5 Fingerprint: af:0c:86:6e:bf:40:2d:7f:0b:3e:12:50:ba:12:3d:06 +# SHA1 Fingerprint: 09:3c:61:f3:8b:8b:dc:7d:55:df:75:38:02:05:00:e1:25:f5:c8:36 +# SHA256 Fingerprint: 8f:e4:fb:0a:f9:3a:4d:0d:67:db:0b:eb:b2:3e:37:c7:1b:f3:25:dc:bc:dd:24:0e:a0:4d:af:58:b4:7e:18:40 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 3 G3" +# Serial: 268090761170461462463995952157327242137089239581 +# MD5 Fingerprint: df:7d:b9:ad:54:6f:68:a1:df:89:57:03:97:43:b0:d7 +# SHA1 Fingerprint: 48:12:bd:92:3c:a8:c4:39:06:e7:30:6d:27:96:e6:a4:cf:22:2e:7d +# SHA256 Fingerprint: 88:ef:81:de:20:2e:b0:18:45:2e:43:f8:64:72:5c:ea:5f:bd:1f:c2:d9:d2:05:73:07:09:c5:d8:b8:69:0f:46 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G2" +# Serial: 15385348160840213938643033620894905419 +# MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d +# SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f +# SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85 +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G3" +# Serial: 15459312981008553731928384953135426796 +# MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb +# SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89 +# SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2 +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G2" +# Serial: 4293743540046975378534879503202253541 +# MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44 +# SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4 +# SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G3" +# Serial: 7089244469030293291760083333884364146 +# MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca +# SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e +# SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0 +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Trusted Root G4" +# Serial: 7451500558977370777930084869016614236 +# MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49 +# SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4 +# SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88 +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- + +# Issuer: CN=Certification Authority of WoSign O=WoSign CA Limited +# Subject: CN=Certification Authority of WoSign O=WoSign CA Limited +# Label: "WoSign" +# Serial: 125491772294754854453622855443212256657 +# MD5 Fingerprint: a1:f2:f9:b5:d2:c8:7a:74:b8:f3:05:f1:d7:e1:84:8d +# SHA1 Fingerprint: b9:42:94:bf:91:ea:8f:b6:4b:e6:10:97:c7:fb:00:13:59:b6:76:cb +# SHA256 Fingerprint: 4b:22:d5:a6:ae:c9:9f:3c:db:79:aa:5e:c0:68:38:47:9c:d5:ec:ba:71:64:f7:f2:2d:c1:d6:5f:63:d8:57:08 +-----BEGIN CERTIFICATE----- +MIIFdjCCA16gAwIBAgIQXmjWEXGUY1BWAGjzPsnFkTANBgkqhkiG9w0BAQUFADBV +MQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxKjAoBgNV +BAMTIUNlcnRpZmljYXRpb24gQXV0aG9yaXR5IG9mIFdvU2lnbjAeFw0wOTA4MDgw +MTAwMDFaFw0zOTA4MDgwMTAwMDFaMFUxCzAJBgNVBAYTAkNOMRowGAYDVQQKExFX +b1NpZ24gQ0EgTGltaXRlZDEqMCgGA1UEAxMhQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgb2YgV29TaWduMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvcqN +rLiRFVaXe2tcesLea9mhsMMQI/qnobLMMfo+2aYpbxY94Gv4uEBf2zmoAHqLoE1U +fcIiePyOCbiohdfMlZdLdNiefvAA5A6JrkkoRBoQmTIPJYhTpA2zDxIIFgsDcScc +f+Hb0v1naMQFXQoOXXDX2JegvFNBmpGN9J42Znp+VsGQX+axaCA2pIwkLCxHC1l2 +ZjC1vt7tj/id07sBMOby8w7gLJKA84X5KIq0VC6a7fd2/BVoFutKbOsuEo/Uz/4M +x1wdC34FMr5esAkqQtXJTpCzWQ27en7N1QhatH/YHGkR+ScPewavVIMYe+HdVHpR +aG53/Ma/UkpmRqGyZxq7o093oL5d//xWC0Nyd5DKnvnyOfUNqfTq1+ezEC8wQjch +zDBwyYaYD8xYTYO7feUapTeNtqwylwA6Y3EkHp43xP901DfA4v6IRmAR3Qg/UDar +uHqklWJqbrDKaiFaafPz+x1wOZXzp26mgYmhiMU7ccqjUu6Du/2gd/Tkb+dC221K +mYo0SLwX3OSACCK28jHAPwQ+658geda4BmRkAjHXqc1S+4RFaQkAKtxVi8QGRkvA +Sh0JWzko/amrzgD5LkhLJuYwTKVYyrREgk/nkR4zw7CT/xH8gdLKH3Ep3XZPkiWv +HYG3Dy+MwwbMLyejSuQOmbp8HkUff6oZRZb9/D0CAwEAAaNCMEAwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOFmzw7R8bNLtwYgFP6H +EtX2/vs+MA0GCSqGSIb3DQEBBQUAA4ICAQCoy3JAsnbBfnv8rWTjMnvMPLZdRtP1 +LOJwXcgu2AZ9mNELIaCJWSQBnfmvCX0KI4I01fx8cpm5o9dU9OpScA7F9dY74ToJ +MuYhOZO9sxXqT2r09Ys/L3yNWC7F4TmgPsc9SnOeQHrAK2GpZ8nzJLmzbVUsWh2e +JXLOC62qx1ViC777Y7NhRCOjy+EaDveaBk3e1CNOIZZbOVtXHS9dCF4Jef98l7VN +g64N1uajeeAz0JmWAjCnPv/So0M/BVoG6kQC2nz4SNAzqfkHx5Xh9T71XXG68pWp +dIhhWeO/yloTunK0jF02h+mmxTwTv97QRCbut+wucPrXnbes5cVAWubXbHssw1ab +R80LzvobtCHXt2a49CUwi1wNuepnsvRtrtWhnk/Yn+knArAdBtaP4/tIEp9/EaEQ +PkxROpaw0RPxx9gmrjrKkcRpnd8BKWRRb2jaFOwIQZeQjdCygPLPwj2/kWjFgGce +xGATVdVhmVd8upUPYUk6ynW8yQqTP2cOEvIo4jEbwFcW3wh8GcF+Dx+FHgo2fFt+ +J7x6v+Db9NpSvd4MVHAxkUOVyLzwPt0JfjBkUO1/AaQzZ01oT74V77D2AhGiGxMl +OtzCWfHjXEa7ZywCRuoeSKbmW9m1vFGikpbbqsY3Iqb+zCB0oy2pLmvLwIIRIbWT +ee5Ehr7XHuQe+w== +-----END CERTIFICATE----- + +# Issuer: CN=CA 沃通根证书 O=WoSign CA Limited +# Subject: CN=CA 沃通根证书 O=WoSign CA Limited +# Label: "WoSign China" +# Serial: 106921963437422998931660691310149453965 +# MD5 Fingerprint: 78:83:5b:52:16:76:c4:24:3b:83:78:e8:ac:da:9a:93 +# SHA1 Fingerprint: 16:32:47:8d:89:f9:21:3a:92:00:85:63:f5:a4:a7:d3:12:40:8a:d6 +# SHA256 Fingerprint: d6:f0:34:bd:94:aa:23:3f:02:97:ec:a4:24:5b:28:39:73:e4:47:aa:59:0f:31:0c:77:f4:8f:df:83:11:22:54 +-----BEGIN CERTIFICATE----- +MIIFWDCCA0CgAwIBAgIQUHBrzdgT/BtOOzNy0hFIjTANBgkqhkiG9w0BAQsFADBG +MQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxGzAZBgNV +BAMMEkNBIOayg+mAmuagueivgeS5pjAeFw0wOTA4MDgwMTAwMDFaFw0zOTA4MDgw +MTAwMDFaMEYxCzAJBgNVBAYTAkNOMRowGAYDVQQKExFXb1NpZ24gQ0EgTGltaXRl +ZDEbMBkGA1UEAwwSQ0Eg5rKD6YCa5qC56K+B5LmmMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA0EkhHiX8h8EqwqzbdoYGTufQdDTc7WU1/FDWiD+k8H/r +D195L4mx/bxjWDeTmzj4t1up+thxx7S8gJeNbEvxUNUqKaqoGXqW5pWOdO2XCld1 +9AXbbQs5uQF/qvbW2mzmBeCkTVL829B0txGMe41P/4eDrv8FAxNXUDf+jJZSEExf +v5RxadmWPgxDT74wwJ85dE8GRV2j1lY5aAfMh09Qd5Nx2UQIsYo06Yms25tO4dnk +UkWMLhQfkWsZHWgpLFbE4h4TV2TwYeO5Ed+w4VegG63XX9Gv2ystP9Bojg/qnw+L +NVgbExz03jWhCl3W6t8Sb8D7aQdGctyB9gQjF+BNdeFyb7Ao65vh4YOhn0pdr8yb ++gIgthhid5E7o9Vlrdx8kHccREGkSovrlXLp9glk3Kgtn3R46MGiCWOc76DbT52V +qyBPt7D3h1ymoOQ3OMdc4zUPLK2jgKLsLl3Az+2LBcLmc272idX10kaO6m1jGx6K +yX2m+Jzr5dVjhU1zZmkR/sgO9MHHZklTfuQZa/HpelmjbX7FF+Ynxu8b22/8DU0G +AbQOXDBGVWCvOGU6yke6rCzMRh+yRpY/8+0mBe53oWprfi1tWFxK1I5nuPHa1UaK +J/kR8slC/k7e3x9cxKSGhxYzoacXGKUN5AXlK8IrC6KVkLn9YDxOiT7nnO4fuwEC +AwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFOBNv9ybQV0T6GTwp+kVpOGBwboxMA0GCSqGSIb3DQEBCwUAA4ICAQBqinA4 +WbbaixjIvirTthnVZil6Xc1bL3McJk6jfW+rtylNpumlEYOnOXOvEESS5iVdT2H6 +yAa+Tkvv/vMx/sZ8cApBWNromUuWyXi8mHwCKe0JgOYKOoICKuLJL8hWGSbueBwj +/feTZU7n85iYr83d2Z5AiDEoOqsuC7CsDCT6eiaY8xJhEPRdF/d+4niXVOKM6Cm6 +jBAyvd0zaziGfjk9DgNyp115j0WKWa5bIW4xRtVZjc8VX90xJc/bYNaBRHIpAlf2 +ltTW/+op2znFuCyKGo3Oy+dCMYYFaA6eFN0AkLppRQjbbpCBhqcqBT/mhDn4t/lX +X0ykeVoQDF7Va/81XwVRHmyjdanPUIPTfPRm94KNPQx96N97qA4bLJyuQHCH2u2n +FoJavjVsIE4iYdm8UXrNemHcSxH5/mc0zy4EZmFcV5cjjPOGG0jfKq+nwf/Yjj4D +u9gqsPoUJbJRa4ZDhS4HIxaAjUz7tGM7zMN07RujHv41D198HRaG9Q7DlfEvr10l +O1Hm13ZBONFLAzkopR6RctR9q5czxNM+4Gm2KHmgCY0c0f9BckgG/Jou5yD5m6Le +ie2uPAmvylezkolwQOQvT8Jwg0DXJCxr5wkf09XHwQj02w47HAcLQxGEIYbpgNR1 +2KvxAmLBsX5VYc8T1yaw15zLKYs4SgsOkI26oQ== +-----END CERTIFICATE----- + +# Issuer: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Label: "COMODO RSA Certification Authority" +# Serial: 101909084537582093308941363524873193117 +# MD5 Fingerprint: 1b:31:b0:71:40:36:cc:14:36:91:ad:c4:3e:fd:ec:18 +# SHA1 Fingerprint: af:e5:d2:44:a8:d1:19:42:30:ff:47:9f:e2:f8:97:bb:cd:7a:8c:b4 +# SHA256 Fingerprint: 52:f0:e1:c4:e5:8e:c6:29:29:1b:60:31:7f:07:46:71:b8:5d:7e:a8:0d:5b:07:27:34:63:53:4b:32:b4:02:34 +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- + +# Issuer: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Label: "USERTrust RSA Certification Authority" +# Serial: 2645093764781058787591871645665788717 +# MD5 Fingerprint: 1b:fe:69:d1:91:b7:19:33:a3:72:a8:0f:e1:55:e5:b5 +# SHA1 Fingerprint: 2b:8f:1b:57:33:0d:bb:a2:d0:7a:6c:51:f7:0e:e9:0d:da:b9:ad:8e +# SHA256 Fingerprint: e7:93:c9:b0:2f:d8:aa:13:e2:1c:31:22:8a:cc:b0:81:19:64:3b:74:9c:89:89:64:b1:74:6d:46:c3:d4:cb:d2 +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +# Issuer: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Label: "USERTrust ECC Certification Authority" +# Serial: 123013823720199481456569720443997572134 +# MD5 Fingerprint: fa:68:bc:d9:b5:7f:ad:fd:c9:1d:06:83:28:cc:24:c1 +# SHA1 Fingerprint: d1:cb:ca:5d:b2:d5:2a:7f:69:3b:67:4d:e5:f0:5a:1d:0c:95:7d:f0 +# SHA256 Fingerprint: 4f:f4:60:d5:4b:9c:86:da:bf:bc:fc:57:12:e0:40:0d:2b:ed:3f:bc:4d:4f:bd:aa:86:e0:6a:dc:d2:a9:ad:7a +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Label: "GlobalSign ECC Root CA - R4" +# Serial: 14367148294922964480859022125800977897474 +# MD5 Fingerprint: 20:f0:27:68:d1:7e:a0:9d:0e:e6:2a:ca:df:5c:89:8e +# SHA1 Fingerprint: 69:69:56:2e:40:80:f4:24:a1:e7:19:9f:14:ba:f3:ee:58:ab:6a:bb +# SHA256 Fingerprint: be:c9:49:11:c2:95:56:76:db:6c:0a:55:09:86:d7:6e:3b:a0:05:66:7c:44:2c:97:62:b4:fb:b7:73:de:22:8c +-----BEGIN CERTIFICATE----- +MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprlOQcJ +FspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61F +uOJAf/sKbvu+M8k8o4TVMAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGX +kPoUVy0D7O48027KqGx2vKLeuwIgJ6iFJzWbVsaj8kfSt24bAgAXqmemFZHe+pTs +ewv4n4Q= +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Label: "GlobalSign ECC Root CA - R5" +# Serial: 32785792099990507226680698011560947931244 +# MD5 Fingerprint: 9f:ad:3b:1c:02:1e:8a:ba:17:74:38:81:0c:a2:bc:08 +# SHA1 Fingerprint: 1f:24:c6:30:cd:a4:18:ef:20:69:ff:ad:4f:dd:5f:46:3a:1b:69:aa +# SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24 +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +# Issuer: CN=Staat der Nederlanden Root CA - G3 O=Staat der Nederlanden +# Subject: CN=Staat der Nederlanden Root CA - G3 O=Staat der Nederlanden +# Label: "Staat der Nederlanden Root CA - G3" +# Serial: 10003001 +# MD5 Fingerprint: 0b:46:67:07:db:10:2f:19:8c:35:50:60:d1:0b:f4:37 +# SHA1 Fingerprint: d8:eb:6b:41:51:92:59:e0:f3:e7:85:00:c0:3d:b6:88:97:c9:ee:fc +# SHA256 Fingerprint: 3c:4f:b0:b9:5a:b8:b3:00:32:f4:32:b8:6f:53:5f:e1:72:c1:85:d0:fd:39:86:58:37:cf:36:18:7f:a6:f4:28 +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloX +DTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl +ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv +b3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4yolQP +cPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WW +IkYFsO2tx1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqX +xz8ecAgwoNzFs21v0IJyEavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFy +KJLZWyNtZrVtB0LrpjPOktvA9mxjeM3KTj215VKb8b475lRgsGYeCasH/lSJEULR +9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUurmkVLoR9BvUhTFXFkC4az +5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU51nus6+N8 +6U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7 +Ngzp07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHP +bMk7ccHViLVlvMDoFxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXt +BznaqB16nzaeErAMZRKQFWDZJkBE41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTt +XUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleuyjWcLhL75Lpd +INyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD +U5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwp +LiniyMMB8jPqKqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8 +Ipf3YF3qKS9Ysr1YvY2WTxB1v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixp +gZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA8KCWAg8zxXHzniN9lLf9OtMJgwYh +/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b8KKaa8MFSu1BYBQw +0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0rmj1A +fsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq +4BZ+Extq1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR +1VmiiXTTn74eS9fGbbeIJG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/ +QFH1T/U67cjF68IeHRaVesd+QnGTbksVtzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM +94B7IWcnMFk= +-----END CERTIFICATE----- + +# Issuer: CN=Staat der Nederlanden EV Root CA O=Staat der Nederlanden +# Subject: CN=Staat der Nederlanden EV Root CA O=Staat der Nederlanden +# Label: "Staat der Nederlanden EV Root CA" +# Serial: 10000013 +# MD5 Fingerprint: fc:06:af:7b:e8:1a:f1:9a:b4:e8:d2:70:1f:c0:f5:ba +# SHA1 Fingerprint: 76:e2:7e:c1:4f:db:82:c1:c0:a6:75:b5:05:be:3d:29:b4:ed:db:bb +# SHA256 Fingerprint: 4d:24:91:41:4c:fe:95:67:46:ec:4c:ef:a6:cf:6f:72:e2:8a:13:29:43:2f:9d:8a:90:7a:c4:cb:5d:ad:c1:5a +-----BEGIN CERTIFICATE----- +MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gRVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0y +MjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5MMR4wHAYDVQQKDBVTdGFhdCBkZXIg +TmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRlcmxhbmRlbiBFViBS +b290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkkSzrS +M4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nC +UiY4iKTWO0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3d +Z//BYY1jTw+bbRcwJu+r0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46p +rfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13l +pJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gVXJrm0w912fxBmJc+qiXb +j5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr08C+eKxC +KFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS +/ZbV0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0X +cgOPvZuM5l5Tnrmd74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH +1vI4gnPah1vlPNOePqc7nvQDs/nxfRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrP +px9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwaivsnuL8wbqg7 +MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI +eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u +2dfOWBfoqSmuc0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHS +v4ilf0X8rLiltTMMgsT7B/Zq5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTC +wPTxGfARKbalGAKb12NMcIxHowNDXLldRqANb/9Zjr7dn3LDWyvfjFvO5QxGbJKy +CqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tNf1zuacpzEPuKqf2e +vTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi5Dp6 +Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIa +Gl6I6lD4WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeL +eG9QgkRQP2YGiqtDhFZKDyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8 +FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGyeUN51q1veieQA6TqJIc/2b3Z6fJfUEkc +7uzXLg== +-----END CERTIFICATE----- + +# Issuer: CN=IdenTrust Commercial Root CA 1 O=IdenTrust +# Subject: CN=IdenTrust Commercial Root CA 1 O=IdenTrust +# Label: "IdenTrust Commercial Root CA 1" +# Serial: 13298821034946342390520003877796839426 +# MD5 Fingerprint: b3:3e:77:73:75:ee:a0:d3:e3:7e:49:63:49:59:bb:c7 +# SHA1 Fingerprint: df:71:7e:aa:4a:d9:4e:c9:55:84:99:60:2d:48:de:5f:bc:f0:3a:25 +# SHA256 Fingerprint: 5d:56:49:9b:e4:d2:e0:8b:cf:ca:d0:8a:3e:38:72:3d:50:50:3b:de:70:69:48:e4:2f:55:60:30:19:e5:28:ae +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +# Issuer: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust +# Subject: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust +# Label: "IdenTrust Public Sector Root CA 1" +# Serial: 13298821034946342390521976156843933698 +# MD5 Fingerprint: 37:06:a5:b0:fc:89:9d:ba:f4:6b:8c:1a:64:cd:d5:ba +# SHA1 Fingerprint: ba:29:41:60:77:98:3f:f4:f3:ef:f2:31:05:3b:2e:ea:6d:4d:45:fd +# SHA256 Fingerprint: 30:d0:89:5a:9a:44:8a:26:20:91:63:55:22:d1:f5:20:10:b5:86:7a:ca:e1:2c:78:ef:95:8f:d4:f4:38:9f:2f +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu +VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN +MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 +MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 +ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy +RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS +bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF +/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R +3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw +EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy +9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V +GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ +2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV +WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD +W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN +AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV +DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 +TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G +lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW +mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df +WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 ++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ +tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA +GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv +8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +# Issuer: CN=Entrust Root Certification Authority - G2 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2009 Entrust, Inc. - for authorized use only +# Subject: CN=Entrust Root Certification Authority - G2 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2009 Entrust, Inc. - for authorized use only +# Label: "Entrust Root Certification Authority - G2" +# Serial: 1246989352 +# MD5 Fingerprint: 4b:e2:c9:91:96:65:0c:f4:0e:5a:93:92:a0:0a:fe:b2 +# SHA1 Fingerprint: 8c:f4:27:fd:79:0c:3a:d1:66:06:8d:e8:1e:57:ef:bb:93:22:72:d4 +# SHA256 Fingerprint: 43:df:57:74:b0:3e:7f:ef:5f:e4:0d:93:1a:7b:ed:f1:bb:2e:6b:42:73:8c:4e:6d:38:41:10:3d:3a:a7:f3:39 +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 +cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs +IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz +dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy +NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu +dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt +dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 +aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T +RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN +cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW +wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 +U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 +jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN +BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ +jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v +1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R +nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH +VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== +-----END CERTIFICATE----- + +# Issuer: CN=Entrust Root Certification Authority - EC1 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2012 Entrust, Inc. - for authorized use only +# Subject: CN=Entrust Root Certification Authority - EC1 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2012 Entrust, Inc. - for authorized use only +# Label: "Entrust Root Certification Authority - EC1" +# Serial: 51543124481930649114116133369 +# MD5 Fingerprint: b6:7e:1d:f0:58:c5:49:6c:24:3b:3d:ed:98:18:ed:bc +# SHA1 Fingerprint: 20:d8:06:40:df:9b:25:f5:12:25:3a:11:ea:f7:59:8a:eb:14:b5:47 +# SHA256 Fingerprint: 02:ed:0e:b2:8c:14:da:45:16:5c:56:67:91:70:0d:64:51:d7:fb:56:f0:b2:ab:1d:3b:8e:b0:70:e5:6e:df:f5 +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG +A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 +d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu +dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq +RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy +MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD +VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g +Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi +A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt +ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH +Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC +R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX +hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- + +# Issuer: CN=CFCA EV ROOT O=China Financial Certification Authority +# Subject: CN=CFCA EV ROOT O=China Financial Certification Authority +# Label: "CFCA EV ROOT" +# Serial: 407555286 +# MD5 Fingerprint: 74:e1:b6:ed:26:7a:7a:44:30:33:94:ab:7b:27:81:30 +# SHA1 Fingerprint: e2:b8:29:4b:55:84:ab:6b:58:c2:90:46:6c:ac:3f:b8:39:8f:84:83 +# SHA256 Fingerprint: 5c:c3:d7:8e:4e:1d:5e:45:54:7a:04:e6:87:3e:64:f9:0c:f9:53:6d:1c:cc:2e:f8:00:f3:55:c4:c5:fd:70:fd +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx +MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP +T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 +sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL +TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 +/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp +7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz +EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt +hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP +a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot +aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg +TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV +PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv +cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL +tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT +ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL +jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS +ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy +P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 +xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d +Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN +5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe +/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z +AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ +5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +# Issuer: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı H5 O=TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. +# Subject: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı H5 O=TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. +# Label: "TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı H5" +# Serial: 156233699172481 +# MD5 Fingerprint: da:70:8e:f0:22:df:93:26:f6:5f:9f:d3:15:06:52:4e +# SHA1 Fingerprint: c4:18:f6:4d:46:d1:df:00:3d:27:30:13:72:43:a9:12:11:c6:75:fb +# SHA256 Fingerprint: 49:35:1b:90:34:44:c1:85:cc:dc:5c:69:3d:24:d8:55:5c:b2:08:d6:a8:14:13:07:69:9f:4a:f0:63:19:9d:78 +-----BEGIN CERTIFICATE----- +MIIEJzCCAw+gAwIBAgIHAI4X/iQggTANBgkqhkiG9w0BAQsFADCBsTELMAkGA1UE +BhMCVFIxDzANBgNVBAcMBkFua2FyYTFNMEsGA1UECgxEVMOcUktUUlVTVCBCaWxn +aSDEsGxldGnFn2ltIHZlIEJpbGnFn2ltIEfDvHZlbmxpxJ9pIEhpem1ldGxlcmkg +QS7Fni4xQjBABgNVBAMMOVTDnFJLVFJVU1QgRWxla3Ryb25payBTZXJ0aWZpa2Eg +SGl6bWV0IFNhxJ9sYXnEsWPEsXPEsSBINTAeFw0xMzA0MzAwODA3MDFaFw0yMzA0 +MjgwODA3MDFaMIGxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMU0wSwYD +VQQKDERUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8 +dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLjFCMEAGA1UEAww5VMOcUktUUlVTVCBF +bGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxIEg1MIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApCUZ4WWe60ghUEoI5RHwWrom +/4NZzkQqL/7hzmAD/I0Dpe3/a6i6zDQGn1k19uwsu537jVJp45wnEFPzpALFp/kR +Gml1bsMdi9GYjZOHp3GXDSHHmflS0yxjXVW86B8BSLlg/kJK9siArs1mep5Fimh3 +4khon6La8eHBEJ/rPCmBp+EyCNSgBbGM+42WAA4+Jd9ThiI7/PS98wl+d+yG6w8z +5UNP9FR1bSmZLmZaQ9/LXMrI5Tjxfjs1nQ/0xVqhzPMggCTTV+wVunUlm+hkS7M0 +hO8EuPbJbKoCPrZV4jI3X/xml1/N1p7HIL9Nxqw/dV8c7TKcfGkAaZHjIxhT6QID +AQABo0IwQDAdBgNVHQ4EFgQUVpkHHtOsDGlktAxQR95DLL4gwPswDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJ5FdnsX +SDLyOIspve6WSk6BGLFRRyDN0GSxDsnZAdkJzsiZ3GglE9Rc8qPoBP5yCccLqh0l +VX6Wmle3usURehnmp349hQ71+S4pL+f5bFgWV1Al9j4uPqrtd3GqqpmWRgqujuwq +URawXs3qZwQcWDD1YIq9pr1N5Za0/EKJAWv2cMhQOQwt1WbZyNKzMrcbGW3LM/nf +peYVhDfwwvJllpKQd/Ct9JDpEXjXk4nAPQu6KfTomZ1yju2dL+6SfaHx/126M2CF +Yv4HAqGEVka+lgqaE9chTLd8B59OTj+RdPsnnRHM3eaxynFNExc5JsUpISuTKWqW ++qtB4Uu2NQvAmxU= +-----END CERTIFICATE----- + +# Issuer: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı H6 O=TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. +# Subject: CN=TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı H6 O=TÜRKTRUST Bilgi İletişim ve Bilişim Güvenliği Hizmetleri A.Ş. +# Label: "TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı H6" +# Serial: 138134509972618 +# MD5 Fingerprint: f8:c5:ee:2a:6b:be:95:8d:08:f7:25:4a:ea:71:3e:46 +# SHA1 Fingerprint: 8a:5c:8c:ee:a5:03:e6:05:56:ba:d8:1b:d4:f6:c9:b0:ed:e5:2f:e0 +# SHA256 Fingerprint: 8d:e7:86:55:e1:be:7f:78:47:80:0b:93:f6:94:d2:1d:36:8c:c0:6e:03:3e:7f:ab:04:bb:5e:b9:9d:a6:b7:00 +-----BEGIN CERTIFICATE----- +MIIEJjCCAw6gAwIBAgIGfaHyZeyKMA0GCSqGSIb3DQEBCwUAMIGxMQswCQYDVQQG +EwJUUjEPMA0GA1UEBwwGQW5rYXJhMU0wSwYDVQQKDERUw5xSS1RSVVNUIEJpbGdp +IMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBB +LsWeLjFCMEAGA1UEAww5VMOcUktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBI +aXptZXQgU2HEn2xhecSxY8Sxc8SxIEg2MB4XDTEzMTIxODA5MDQxMFoXDTIzMTIx +NjA5MDQxMFowgbExCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExTTBLBgNV +BAoMRFTDnFJLVFJVU1QgQmlsZ2kgxLBsZXRpxZ9pbSB2ZSBCaWxpxZ9pbSBHw7x2 +ZW5sacSfaSBIaXptZXRsZXJpIEEuxZ4uMUIwQAYDVQQDDDlUw5xSS1RSVVNUIEVs +ZWt0cm9uaWsgU2VydGlmaWthIEhpem1ldCBTYcSfbGF5xLFjxLFzxLEgSDYwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdsGjW6L0UlqMACprx9MfMkU1x +eHe59yEmFXNRFpQJRwXiM/VomjX/3EsvMsew7eKC5W/a2uqsxgbPJQ1BgfbBOCK9 ++bGlprMBvD9QFyv26WZV1DOzXPhDIHiTVRZwGTLmiddk671IUP320EEDwnS3/faA +z1vFq6TWlRKb55cTMgPp1KtDWxbtMyJkKbbSk60vbNg9tvYdDjTu0n2pVQ8g9P0p +u5FbHH3GQjhtQiht1AH7zYiXSX6484P4tZgvsycLSF5W506jM7NE1qXyGJTtHB6p +lVxiSvgNZ1GpryHV+DKdeboaX+UEVU0TRv/yz3THGmNtwx8XEsMeED5gCLMxAgMB +AAGjQjBAMB0GA1UdDgQWBBTdVRcT9qzoSCHK77Wv0QAy7Z6MtTAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAb1gNl0Oq +FlQ+v6nfkkU/hQu7VtMMUszIv3ZnXuaqs6fvuay0EBQNdH49ba3RfdCaqaXKGDsC +QC4qnFAUi/5XfldcEQlLNkVS9z2sFP1E34uXI9TDwe7UU5X+LEr+DXCqu4svLcsy +o4LyVN/Y8t3XSHLuSqMplsNEzm61kod2pLv0kmzOLBQJZo6NrRa1xxsJYTvjIKID +gI6tflEATseWhvtDmHd9KMeP2Cpu54Rvl0EpABZeTeIT6lnAY2c6RPuY/ATTMHKm +9ocJV612ph1jmv3XZch4gyt1O6VbuA1df74jrlZVlFjvH4GMKrLN5ptjnhi85WsG +tAuYSyher4hYyw== +-----END CERTIFICATE----- + +# Issuer: CN=Certinomis - Root CA O=Certinomis OU=0002 433998903 +# Subject: CN=Certinomis - Root CA O=Certinomis OU=0002 433998903 +# Label: "Certinomis - Root CA" +# Serial: 1 +# MD5 Fingerprint: 14:0a:fd:8d:a8:28:b5:38:69:db:56:7e:61:22:03:3f +# SHA1 Fingerprint: 9d:70:bb:01:a5:a4:a0:18:11:2e:f7:1c:01:b9:32:c5:34:e7:88:a8 +# SHA256 Fingerprint: 2a:99:f5:bc:11:74:b7:3c:bb:1d:62:08:84:e0:1c:34:e5:1c:cb:39:78:da:12:5f:0e:33:26:88:83:bf:41:58 +-----BEGIN CERTIFICATE----- +MIIFkjCCA3qgAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJGUjET +MBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxHTAb +BgNVBAMTFENlcnRpbm9taXMgLSBSb290IENBMB4XDTEzMTAyMTA5MTcxOFoXDTMz +MTAyMTA5MTcxOFowWjELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNlcnRpbm9taXMx +FzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMR0wGwYDVQQDExRDZXJ0aW5vbWlzIC0g +Um9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANTMCQosP5L2 +fxSeC5yaah1AMGT9qt8OHgZbn1CF6s2Nq0Nn3rD6foCWnoR4kkjW4znuzuRZWJfl +LieY6pOod5tK8O90gC3rMB+12ceAnGInkYjwSond3IjmFPnVAy//ldu9n+ws+hQV +WZUKxkd8aRi5pwP5ynapz8dvtF4F/u7BUrJ1Mofs7SlmO/NKFoL21prbcpjp3vDF +TKWrteoB4owuZH9kb/2jJZOLyKIOSY008B/sWEUuNKqEUL3nskoTuLAPrjhdsKkb +5nPJWqHZZkCqqU2mNAKthH6yI8H7KsZn9DS2sJVqM09xRLWtwHkziOC/7aOgFLSc +CbAK42C++PhmiM1b8XcF4LVzbsF9Ri6OSyemzTUK/eVNfaoqoynHWmgE6OXWk6Ri +wsXm9E/G+Z8ajYJJGYrKWUM66A0ywfRMEwNvbqY/kXPLynNvEiCL7sCCeN5LLsJJ +wx3tFvYk9CcbXFcx3FXuqB5vbKziRcxXV4p1VxngtViZSTYxPDMBbRZKzbgqg4SG +m/lg0h9tkQPTYKbVPZrdd5A9NaSfD171UkRpucC63M9933zZxKyGIjK8e2uR73r4 +F2iw4lNVYC2vPsKD2NkJK/DAZNuHi5HMkesE/Xa0lZrmFAYb1TQdvtj/dBxThZng +WVJKYe2InmtJiUZ+IFrZ50rlau7SZRFDAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTvkUz1pcMw6C8I6tNxIqSSaHh0 +2TAfBgNVHSMEGDAWgBTvkUz1pcMw6C8I6tNxIqSSaHh02TANBgkqhkiG9w0BAQsF +AAOCAgEAfj1U2iJdGlg+O1QnurrMyOMaauo++RLrVl89UM7g6kgmJs95Vn6RHJk/ +0KGRHCwPT5iVWVO90CLYiF2cN/z7ZMF4jIuaYAnq1fohX9B0ZedQxb8uuQsLrbWw +F6YSjNRieOpWauwK0kDDPAUwPk2Ut59KA9N9J0u2/kTO+hkzGm2kQtHdzMjI1xZS +g081lLMSVX3l4kLr5JyTCcBMWwerx20RoFAXlCOotQqSD7J6wWAsOMwaplv/8gzj +qh8c3LigkyfeY+N/IZ865Z764BNqdeuWXGKRlI5nU7aJ+BIJy29SWwNyhlCVCNSN +h4YVH5Uk2KRvms6knZtt0rJ2BobGVgjF6wnaNsIbW0G+YSrjcOa4pvi2WsS9Iff/ +ql+hbHY5ZtbqTFXhADObE5hjyW/QASAJN1LnDE8+zbz1X5YnpyACleAu6AdBBR8V +btaw5BngDwKTACdyxYvRVB9dSsNAl35VpnzBMwQUAR1JIGkLGZOdblgi90AMRgwj +Y/M50n92Uaf0yKHxDHYiI0ZSKS3io0EHVmmY0gUJvGnHWmHNj4FgFU2A3ZDifcRQ +8ow7bkrHxuaAKzyBvBGAFhAn1/DNP3nMcyrDflOR1m749fPH0FFNjkulW+YZFzvW +gQncItzujrnEj1PhZ7szuIgVRs/taTX/dQ1G885x4cVrhkIGuUE= +-----END CERTIFICATE----- + +# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed +# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed +# Label: "OISTE WISeKey Global Root GB CA" +# Serial: 157768595616588414422159278966750757568 +# MD5 Fingerprint: a4:eb:b9:61:28:2e:b7:2f:98:b0:35:26:90:99:51:1d +# SHA1 Fingerprint: 0f:f9:40:76:18:d3:d7:6a:4b:98:f0:a8:35:9e:0c:fd:27:ac:cc:ed +# SHA256 Fingerprint: 6b:9c:08:e8:6e:b0:f7:67:cf:ad:65:cd:98:b6:21:49:e5:49:4a:67:f5:84:5e:7b:d1:ed:01:9f:27:b8:6b:d6 +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt +MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg +Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i +YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x +CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG +b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 +HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx +WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX +1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk +u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P +99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r +M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh +cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 +gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO +ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf +aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- + +# Issuer: CN=Certification Authority of WoSign G2 O=WoSign CA Limited +# Subject: CN=Certification Authority of WoSign G2 O=WoSign CA Limited +# Label: "Certification Authority of WoSign G2" +# Serial: 142423943073812161787490648904721057092 +# MD5 Fingerprint: c8:1c:7d:19:aa:cb:71:93:f2:50:f8:52:a8:1e:ba:60 +# SHA1 Fingerprint: fb:ed:dc:90:65:b7:27:20:37:bc:55:0c:9c:56:de:bb:f2:78:94:e1 +# SHA256 Fingerprint: d4:87:a5:6f:83:b0:74:82:e8:5e:96:33:94:c1:ec:c2:c9:e5:1d:09:03:ee:94:6b:02:c3:01:58:1e:d9:9e:16 +-----BEGIN CERTIFICATE----- +MIIDfDCCAmSgAwIBAgIQayXaioidfLwPBbOxemFFRDANBgkqhkiG9w0BAQsFADBY +MQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxLTArBgNV +BAMTJENlcnRpZmljYXRpb24gQXV0aG9yaXR5IG9mIFdvU2lnbiBHMjAeFw0xNDEx +MDgwMDU4NThaFw00NDExMDgwMDU4NThaMFgxCzAJBgNVBAYTAkNOMRowGAYDVQQK +ExFXb1NpZ24gQ0EgTGltaXRlZDEtMCsGA1UEAxMkQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkgb2YgV29TaWduIEcyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAvsXEoCKASU+/2YcRxlPhuw+9YH+v9oIOH9ywjj2X4FA8jzrvZjtFB5sg+OPX +JYY1kBaiXW8wGQiHC38Gsp1ij96vkqVg1CuAmlI/9ZqD6TRay9nVYlzmDuDfBpgO +gHzKtB0TiGsOqCR3A9DuW/PKaZE1OVbFbeP3PU9ekzgkyhjpJMuSA93MHD0JcOQg +5PGurLtzaaNjOg9FD6FKmsLRY6zLEPg95k4ot+vElbGs/V6r+kHLXZ1L3PR8du9n +fwB6jdKgGlxNIuG12t12s9R23164i5jIFFTMaxeSt+BKv0mUYQs4kI9dJGwlezt5 +2eJ+na2fmKEG/HgUYFf47oB3sQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU+mCp62XF3RYUCE4MD42b4Pdkr2cwDQYJ +KoZIhvcNAQELBQADggEBAFfDejaCnI2Y4qtAqkePx6db7XznPWZaOzG73/MWM5H8 +fHulwqZm46qwtyeYP0nXYGdnPzZPSsvxFPpahygc7Y9BMsaV+X3avXtbwrAh449G +3CE4Q3RM+zD4F3LBMvzIkRfEzFg3TgvMWvchNSiDbGAtROtSjFA9tWwS1/oJu2yy +SrHFieT801LYYRf+epSEj3m2M1m6D8QL4nCgS3gu+sif/a+RZQp4OBXllxcU3fng +LDT4ONCEIgDAFFEYKwLcMFrw6AF8NTojrwjkr6qOKEJJLvD1mTS+7Q9LGOHSJDy7 +XUe3IfKN0QqZjuNuPq1w4I+5ysxugTH2e5x6eeRncRg= +-----END CERTIFICATE----- + +# Issuer: CN=CA WoSign ECC Root O=WoSign CA Limited +# Subject: CN=CA WoSign ECC Root O=WoSign CA Limited +# Label: "CA WoSign ECC Root" +# Serial: 138625735294506723296996289575837012112 +# MD5 Fingerprint: 80:c6:53:ee:61:82:28:72:f0:ff:21:b9:17:ca:b2:20 +# SHA1 Fingerprint: d2:7a:d2:be:ed:94:c0:a1:3c:c7:25:21:ea:5d:71:be:81:19:f3:2b +# SHA256 Fingerprint: 8b:45:da:1c:06:f7:91:eb:0c:ab:f2:6b:e5:88:f5:fb:23:16:5c:2e:61:4b:f8:85:56:2d:0d:ce:50:b2:9b:02 +-----BEGIN CERTIFICATE----- +MIICCTCCAY+gAwIBAgIQaEpYcIBr8I8C+vbe6LCQkDAKBggqhkjOPQQDAzBGMQsw +CQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxGzAZBgNVBAMT +EkNBIFdvU2lnbiBFQ0MgUm9vdDAeFw0xNDExMDgwMDU4NThaFw00NDExMDgwMDU4 +NThaMEYxCzAJBgNVBAYTAkNOMRowGAYDVQQKExFXb1NpZ24gQ0EgTGltaXRlZDEb +MBkGA1UEAxMSQ0EgV29TaWduIEVDQyBSb290MHYwEAYHKoZIzj0CAQYFK4EEACID +YgAE4f2OuEMkq5Z7hcK6C62N4DrjJLnSsb6IOsq/Srj57ywvr1FQPEd1bPiUt5v8 +KB7FVMxjnRZLU8HnIKvNrCXSf4/CwVqCXjCLelTOA7WRf6qU0NGKSMyCBSah1VES +1ns2o0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUqv3VWqP2h4syhf3RMluARZPzA7gwCgYIKoZIzj0EAwMDaAAwZQIxAOSkhLCB +1T2wdKyUpOgOPQB0TKGXa/kNUTyh2Tv0Daupn75OcsqF1NnstTJFGG+rrQIwfcf3 +aWMvoeGY7xMQ0Xk/0f7qO3/eVvSQsRUR2LIiFdAvwyYua/GRspBl9JrmkO5K +-----END CERTIFICATE----- + +# Issuer: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. +# Subject: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. +# Label: "SZAFIR ROOT CA2" +# Serial: 357043034767186914217277344587386743377558296292 +# MD5 Fingerprint: 11:64:c1:89:b0:24:b1:8c:b1:07:7e:89:9e:51:9e:99 +# SHA1 Fingerprint: e2:52:fa:95:3f:ed:db:24:60:bd:6e:28:f3:9c:cc:cf:5e:b3:3f:de +# SHA256 Fingerprint: a1:33:9d:33:28:1a:0b:56:e5:57:d3:d3:2b:1c:e7:f9:36:7e:b0:94:bd:5f:a7:2a:7e:50:04:c8:de:d7:ca:fe +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 +ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw +NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L +cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg +Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN +QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT +3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw +3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 +3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 +BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN +XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF +AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw +8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG +nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP +oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy +d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg +LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Network CA 2" +# Serial: 44979900017204383099463764357512596969 +# MD5 Fingerprint: 6d:46:9e:d9:25:6d:08:23:5b:5e:74:7d:1e:27:db:f2 +# SHA1 Fingerprint: d3:dd:48:3e:2b:bf:4c:05:e8:af:10:f5:fa:76:26:cf:d3:dc:30:92 +# SHA256 Fingerprint: b6:76:f2:ed:da:e8:77:5c:d3:6c:b0:f6:3c:d1:d4:60:39:61:f4:9e:62:65:ba:01:3a:2f:03:07:b6:d0:b8:04 +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB +gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu +QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG +A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz +OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ +VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 +b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA +DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn +0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB +OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE +fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E +Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m +o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i +sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW +OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez +Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS +adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n +3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ +F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf +CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 +XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm +djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ +WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb +AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq +P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko +b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj +XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P +5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi +DrW5viSP +-----END CERTIFICATE----- diff --git a/vinetrimmer/vendor/hyper/cli.py b/vinetrimmer/vendor/hyper/cli.py new file mode 100644 index 0000000..1d5384c --- /dev/null +++ b/vinetrimmer/vendor/hyper/cli.py @@ -0,0 +1,264 @@ +# -*- coding: utf-8 -*- +""" +hyper/cli +~~~~~~~~~ + +Command line interface for Hyper inspired by Httpie. +""" +import json +import locale +import logging +import sys +from argparse import ArgumentParser, RawTextHelpFormatter +from argparse import OPTIONAL, ZERO_OR_MORE +from pprint import pformat +from textwrap import dedent + +from hyper import HTTPConnection, HTTP20Connection +from hyper import __version__ +from hyper.compat import is_py2, urlencode, urlsplit, write_to_stdout +from hyper.common.util import to_host_port_tuple + + +log = logging.getLogger('hyper') + +PREFERRED_ENCODING = locale.getpreferredencoding() + +# Various separators used in args +SEP_HEADERS = ':' +SEP_QUERY = '==' +SEP_DATA = '=' + +SEP_GROUP_ITEMS = [ + SEP_HEADERS, + SEP_QUERY, + SEP_DATA, +] + + +class KeyValue(object): + """Base key-value pair parsed from CLI.""" + + def __init__(self, key, value, sep, orig): + self.key = key + self.value = value + self.sep = sep + self.orig = orig + + +class KeyValueArgType(object): + """A key-value pair argument type used with `argparse`. + + Parses a key-value arg and constructs a `KeyValue` instance. + Used for headers, form data, and other key-value pair types. + This class is inspired by httpie and implements simple tokenizer only. + """ + def __init__(self, *separators): + self.separators = separators + + def __call__(self, string): + for sep in self.separators: + splitted = string.split(sep, 1) + if len(splitted) == 2: + key, value = splitted + return KeyValue(key, value, sep, string) + + +def make_positional_argument(parser): + parser.add_argument( + 'method', metavar='METHOD', nargs=OPTIONAL, default='GET', + help=dedent(""" + The HTTP method to be used for the request + (GET, POST, PUT, DELETE, ...). + """)) + parser.add_argument( + '_url', metavar='URL', + help=dedent(""" + The scheme defaults to 'https://' if the URL does not include one. + """)) + parser.add_argument( + 'items', + metavar='REQUEST_ITEM', + nargs=ZERO_OR_MORE, + type=KeyValueArgType(*SEP_GROUP_ITEMS), + help=dedent(""" + Optional key-value pairs to be included in the request. + The separator used determines the type: + + ':' HTTP headers: + + Referer:http://httpie.org Cookie:foo=bar User-Agent:bacon/1.0 + + '==' URL parameters to be appended to the request URI: + + search==hyper + + '=' Data fields to be serialized into a JSON object: + + name=Hyper language=Python description='CLI HTTP client' + """)) + + +def make_troubleshooting_argument(parser): + parser.add_argument( + '--version', action='version', version=__version__, + help='Show version and exit.') + parser.add_argument( + '--debug', action='store_true', default=False, + help='Show debugging information (loglevel=DEBUG)') + parser.add_argument( + '--h2', action='store_true', default=False, + help="Do HTTP/2 directly, skipping plaintext upgrade and ignoring " + "NPN/ALPN." + ) + + +def split_host_and_port(hostname): + if ':' in hostname: + return to_host_port_tuple(hostname, default_port=443) + return hostname, None + + +class UrlInfo(object): + def __init__(self): + self.fragment = None + self.host = 'localhost' + self.netloc = None + self.path = '/' + self.port = 443 + self.query = None + self.scheme = 'https' + self.secure = False + + +def set_url_info(args): + info = UrlInfo() + _result = urlsplit(args._url) + for attr in vars(info).keys(): + value = getattr(_result, attr, None) + if value: + setattr(info, attr, value) + + if info.scheme == 'http' and not _result.port: + info.port = 80 + + # Set the secure arg is the scheme is HTTPS, otherwise do unsecured. + info.secure = info.scheme == 'https' + + if info.netloc: + hostname, _ = split_host_and_port(info.netloc) + info.host = hostname # ensure stripping port number + else: + if _result.path: + _path = _result.path.split('/', 1) + hostname, port = split_host_and_port(_path[0]) + info.host = hostname + if info.path == _path[0]: + info.path = '/' + elif len(_path) == 2 and _path[1]: + info.path = '/' + _path[1] + if port is not None: + info.port = port + + log.debug('Url Info: %s', vars(info)) + args.url = info + + +def set_request_data(args): + body, headers, params = {}, {}, {} + for i in args.items: + if i.sep == SEP_HEADERS: + if i.key: + headers[i.key] = i.value + else: + # when overriding a HTTP/2 special header there will be a + # leading colon, which tricks the command line parser into + # thinking the header is empty + k, v = i.value.split(':', 1) + headers[':' + k] = v + elif i.sep == SEP_QUERY: + params[i.key] = i.value + elif i.sep == SEP_DATA: + value = i.value + if is_py2: # pragma: no cover + value = value.decode(PREFERRED_ENCODING) + body[i.key] = value + + if params: + args.url.path += '?' + urlencode(params) + + if body: + content_type = 'application/json' + headers.setdefault('content-type', content_type) + args.body = json.dumps(body) + + if args.method is None: + args.method = 'POST' if args.body else 'GET' + + args.method = args.method.upper() + args.headers = headers + + +def parse_argument(argv=None): + parser = ArgumentParser(formatter_class=RawTextHelpFormatter) + parser.set_defaults(body=None, headers={}) + make_positional_argument(parser) + make_troubleshooting_argument(parser) + args = parser.parse_args(sys.argv[1:] if argv is None else argv) + + if args.debug: + handler = logging.StreamHandler() + handler.setLevel(logging.DEBUG) + log.addHandler(handler) + log.setLevel(logging.DEBUG) + + set_url_info(args) + set_request_data(args) + return args + + +def get_content_type_and_charset(response): + charset = 'utf-8' + content_type = response.headers.get('content-type') + if content_type is None: + return 'unknown', charset + + content_type = content_type[0].decode('utf-8').lower() + type_and_charset = content_type.split(';', 1) + ctype = type_and_charset[0].strip() + if len(type_and_charset) == 2: + charset = type_and_charset[1].strip().split('=')[1] + + return ctype, charset + + +def request(args): + if not args.h2: + conn = HTTPConnection( + args.url.host, args.url.port, secure=args.url.secure + ) + else: # pragma: no cover + conn = HTTP20Connection( + args.url.host, + args.url.port, + secure=args.url.secure, + force_proto='h2' + ) + + conn.request(args.method, args.url.path, args.body, args.headers) + response = conn.get_response() + log.debug('Response Headers:\n%s', pformat(response.headers)) + ctype, charset = get_content_type_and_charset(response) + data = response.read() + return data + + +def main(argv=None): + args = parse_argument(argv) + log.debug('Commandline Argument: %s', args) + data = request(args) + write_to_stdout(data) + + +if __name__ == '__main__': # pragma: no cover + main() diff --git a/vinetrimmer/vendor/hyper/common/__init__.py b/vinetrimmer/vendor/hyper/common/__init__.py new file mode 100644 index 0000000..cf84291 --- /dev/null +++ b/vinetrimmer/vendor/hyper/common/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +""" +hyper/common +~~~~~~~~~~~~ + +Common code in hyper. +""" diff --git a/vinetrimmer/vendor/hyper/common/bufsocket.py b/vinetrimmer/vendor/hyper/common/bufsocket.py new file mode 100644 index 0000000..b35393a --- /dev/null +++ b/vinetrimmer/vendor/hyper/common/bufsocket.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- +""" +hyper/http20/bufsocket.py +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This file implements a buffered socket wrapper. + +The purpose of this is to avoid the overhead of unnecessary syscalls while +allowing small reads from the network. This represents a potentially massive +performance optimisation at the cost of burning some memory in the userspace +process. +""" +import select +from .exceptions import ConnectionResetError, LineTooLongError + + +class BufferedSocket(object): + """ + A buffered socket wrapper. + + The purpose of this is to avoid the overhead of unnecessary syscalls while + allowing small reads from the network. This represents a potentially + massive performance optimisation at the cost of burning some memory in the + userspace process. + """ + def __init__(self, sck, buffer_size=1000): + """ + Create the buffered socket. + + :param sck: The socket to wrap. + :param buffer_size: The size of the backing buffer in bytes. This + parameter should be set to an appropriate value for your use case. + Small values of ``buffer_size`` increase the overhead of buffer + management: large values cause more memory to be used. + """ + # The wrapped socket. + self._sck = sck + + # The buffer we're using. + self._backing_buffer = bytearray(buffer_size) + self._buffer_view = memoryview(self._backing_buffer) + + # The size of the buffer. + self._buffer_size = buffer_size + + # The start index in the memory view. + self._index = 0 + + # The number of bytes in the buffer. + self._bytes_in_buffer = 0 + + @property + def _remaining_capacity(self): + """ + The maximum number of bytes the buffer could still contain. + """ + return self._buffer_size - self._index + + @property + def _buffer_end(self): + """ + The index of the first free byte in the buffer. + """ + return self._index + self._bytes_in_buffer + + @property + def can_read(self): + """ + Whether or not there is more data to read from the socket. + """ + read = select.select([self._sck], [], [], 0)[0] + if read: + return True + + return False + + @property + def buffer(self): + """ + Get access to the buffer itself. + """ + return self._buffer_view[self._index:self._buffer_end] + + def advance_buffer(self, count): + """ + Advances the buffer by the amount of data consumed outside the socket. + """ + self._index += count + self._bytes_in_buffer -= count + + def new_buffer(self): + """ + This method moves all the data in the backing buffer to the start of + a new, fresh buffer. This gives the ability to read much more data. + """ + def read_all_from_buffer(): + end = self._index + self._bytes_in_buffer + return self._buffer_view[self._index:end] + + new_buffer = bytearray(self._buffer_size) + new_buffer_view = memoryview(new_buffer) + new_buffer_view[0:self._bytes_in_buffer] = read_all_from_buffer() + + self._index = 0 + self._backing_buffer = new_buffer + self._buffer_view = new_buffer_view + + return + + def recv(self, amt): + """ + Read some data from the socket. + + :param amt: The amount of data to read. + :returns: A ``memoryview`` object containing the appropriate number of + bytes. The data *must* be copied out by the caller before the next + call to this function. + """ + # In this implementation you can never read more than the number of + # bytes in the buffer. + if amt > self._buffer_size: + amt = self._buffer_size + + # If the amount of data we've been asked to read is less than the + # remaining space in the buffer, we need to clear out the buffer and + # start over. + if amt > self._remaining_capacity: + self.new_buffer() + + # If there's still some room in the buffer, opportunistically attempt + # to read into it. + # If we don't actually _need_ the data (i.e. there's enough in the + # buffer to satisfy the request), use select to work out if the read + # attempt will block. If it will, don't bother reading. If we need the + # data, always do the read. + if self._bytes_in_buffer >= amt: + should_read = select.select([self._sck], [], [], 0)[0] + else: + should_read = True + + if (self._remaining_capacity > self._bytes_in_buffer and should_read): + count = self._sck.recv_into(self._buffer_view[self._buffer_end:]) + + # The socket just got closed. We should throw an exception if we + # were asked for more data than we can return. + if not count and amt > self._bytes_in_buffer: + raise ConnectionResetError() + self._bytes_in_buffer += count + + # Read out the bytes and update the index. + amt = min(amt, self._bytes_in_buffer) + data = self._buffer_view[self._index:self._index+amt] + + self._index += amt + self._bytes_in_buffer -= amt + + return data + + def fill(self): + """ + Attempts to fill the buffer as much as possible. It will block for at + most the time required to have *one* ``recv_into`` call return. + """ + if not self._remaining_capacity: + self.new_buffer() + + count = self._sck.recv_into(self._buffer_view[self._buffer_end:]) + if not count: + raise ConnectionResetError() + + self._bytes_in_buffer += count + + return + + def readline(self): + """ + Read up to a newline from the network and returns it. The implicit + maximum line length is the buffer size of the buffered socket. + + Note that, unlike recv, this method absolutely *does* block until it + can read the line. + + :returns: A ``memoryview`` object containing the appropriate number of + bytes. The data *must* be copied out by the caller before the next + call to this function. + """ + # First, check if there's anything in the buffer. This is one of those + # rare circumstances where this will work correctly on all platforms. + index = self._backing_buffer.find( + b'\n', + self._index, + self._index + self._bytes_in_buffer + ) + + if index != -1: + length = index + 1 - self._index + data = self._buffer_view[self._index:self._index+length] + self._index += length + self._bytes_in_buffer -= length + return data + + # In this case, we didn't find a newline in the buffer. To fix that, + # read some data into the buffer. To do our best to satisfy the read, + # we should shunt the data down in the buffer so that it's right at + # the start. We don't bother if we're already at the start of the + # buffer. + if self._index != 0: + self.new_buffer() + + while self._bytes_in_buffer < self._buffer_size: + count = self._sck.recv_into(self._buffer_view[self._buffer_end:]) + if not count: + raise ConnectionResetError() + + # We have some more data. Again, look for a newline in that gap. + first_new_byte = self._buffer_end + self._bytes_in_buffer += count + index = self._backing_buffer.find( + b'\n', + first_new_byte, + first_new_byte + count, + ) + + if index != -1: + # The length of the buffer is the index into the + # buffer at which we found the newline plus 1, minus the start + # index of the buffer, which really should be zero. + assert not self._index + length = index + 1 + data = self._buffer_view[:length] + self._index += length + self._bytes_in_buffer -= length + return data + + # If we got here, it means we filled the buffer without ever getting + # a newline. Time to throw an exception. + raise LineTooLongError() + + def __getattr__(self, name): + return getattr(self._sck, name) diff --git a/vinetrimmer/vendor/hyper/common/connection.py b/vinetrimmer/vendor/hyper/common/connection.py new file mode 100644 index 0000000..dee18d6 --- /dev/null +++ b/vinetrimmer/vendor/hyper/common/connection.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +""" +hyper/common/connection +~~~~~~~~~~~~~~~~~~~~~~~ + +Hyper's HTTP/1.1 and HTTP/2 abstraction layer. +""" +from .exceptions import TLSUpgrade, HTTPUpgrade +from ..http11.connection import HTTP11Connection +from ..http20.connection import HTTP20Connection +from ..tls import H2_NPN_PROTOCOLS, H2C_PROTOCOL + + +class HTTPConnection(object): + """ + An object representing a single HTTP connection to a server. + + This object behaves similarly to the Python standard library's + ``HTTPConnection`` object, with a few critical differences. + + Most of the standard library's arguments to the constructor are not + supported by hyper. Most optional parameters apply to *either* HTTP/1.1 or + HTTP/2. + + :param host: The host to connect to. This may be an IP address or a + hostname, and optionally may include a port: for example, + ``'http2bin.org'``, ``'http2bin.org:443'`` or ``'127.0.0.1'``. + :param port: (optional) The port to connect to. If not provided and one + also isn't provided in the ``host`` parameter, defaults to 80. + :param secure: (optional) Whether the request should use TLS. + Defaults to ``False`` for most requests, but to ``True`` for any + request issued to port 443. + :param window_manager: (optional) The class to use to manage flow control + windows. This needs to be a subclass of the + :class:`BaseFlowControlManager + <hyper.http20.window.BaseFlowControlManager>`. If not provided, + :class:`FlowControlManager <hyper.http20.window.FlowControlManager>` + will be used. + :param enable_push: (optional) Whether the server is allowed to push + resources to the client (see + :meth:`get_pushes() <hyper.HTTP20Connection.get_pushes>`). + :param ssl_context: (optional) A class with custom certificate settings. + If not provided then hyper's default ``SSLContext`` is used instead. + :param proxy_host: (optional) The proxy to connect to. This can be an IP + address or a host name and may include a port. + :param proxy_port: (optional) The proxy port to connect to. If not provided + and one also isn't provided in the ``proxy`` parameter, defaults to + 8080. + """ + def __init__(self, + host, + port=None, + secure=None, + window_manager=None, + enable_push=False, + ssl_context=None, + proxy_host=None, + proxy_port=None, + **kwargs): + + self._host = host + self._port = port + self._h1_kwargs = { + 'secure': secure, 'ssl_context': ssl_context, + 'proxy_host': proxy_host, 'proxy_port': proxy_port + } + self._h2_kwargs = { + 'window_manager': window_manager, 'enable_push': enable_push, + 'secure': secure, 'ssl_context': ssl_context, + 'proxy_host': proxy_host, 'proxy_port': proxy_port + } + + # Add any unexpected kwargs to both dictionaries. + self._h1_kwargs.update(kwargs) + self._h2_kwargs.update(kwargs) + + self._conn = HTTP11Connection( + self._host, self._port, **self._h1_kwargs + ) + + def request(self, method, url, body=None, headers=None): + """ + This will send a request to the server using the HTTP request method + ``method`` and the selector ``url``. If the ``body`` argument is + present, it should be string or bytes object of data to send after the + headers are finished. Strings are encoded as UTF-8. To use other + encodings, pass a bytes object. The Content-Length header is set to the + length of the body field. + + :param method: The request method, e.g. ``'GET'``. + :param url: The URL to contact, e.g. ``'/path/segment'``. + :param body: (optional) The request body to send. Must be a bytestring + or a file-like object. + :param headers: (optional) The headers to send on the request. + :returns: A stream ID for the request, or ``None`` if the request is + made over HTTP/1.1. + """ + + headers = headers or {} + + try: + return self._conn.request( + method=method, url=url, body=body, headers=headers + ) + except TLSUpgrade as e: + # We upgraded in the NPN/ALPN handshake. We can just go straight to + # the world of HTTP/2. Replace the backing object and insert the + # socket into it. + assert e.negotiated in H2_NPN_PROTOCOLS + + self._conn = HTTP20Connection( + self._host, self._port, **self._h2_kwargs + ) + self._conn._sock = e.sock + + # Because we skipped the connecting logic, we need to send the + # HTTP/2 preamble. + self._conn._send_preamble() + + return self._conn.request( + method=method, url=url, body=body, headers=headers + ) + + def get_response(self, *args, **kwargs): + """ + Returns a response object. + """ + try: + return self._conn.get_response(*args, **kwargs) + except HTTPUpgrade as e: + # We upgraded via the HTTP Upgrade mechanism. We can just + # go straight to the world of HTTP/2. Replace the backing object + # and insert the socket into it. + assert e.negotiated == H2C_PROTOCOL + + self._conn = HTTP20Connection( + self._host, self._port, **self._h2_kwargs + ) + + self._conn._connect_upgrade(e.sock) + # stream id 1 is used by the upgrade request and response + # and is half-closed by the client + + return self._conn.get_response(1) + + # The following two methods are the implementation of the context manager + # protocol. + def __enter__(self): # pragma: no cover + return self + + def __exit__(self, type, value, tb): # pragma: no cover + self._conn.close() + return False # Never swallow exceptions. + + # Can anyone say 'proxy object pattern'? + def __getattr__(self, name): + return getattr(self._conn, name) diff --git a/vinetrimmer/vendor/hyper/common/decoder.py b/vinetrimmer/vendor/hyper/common/decoder.py new file mode 100644 index 0000000..e86f1f4 --- /dev/null +++ b/vinetrimmer/vendor/hyper/common/decoder.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +""" +hyper/common/decoder +~~~~~~~~~~~~~~~~~~~~ + +Contains hyper's code for handling compressed bodies. +""" +import zlib + + +class DeflateDecoder(object): + """ + This is a decoding object that wraps ``zlib`` and is used for decoding + deflated content. + + This rationale for the existence of this object is pretty unpleasant. + The HTTP RFC specifies that 'deflate' is a valid content encoding. However, + the spec _meant_ the zlib encoding form. Unfortunately, people who didn't + read the RFC very carefully actually implemented a different form of + 'deflate'. Insanely, ``zlib`` handles them using two wbits values. This is + such a mess it's hard to adequately articulate. + + This class was lovingly borrowed from the excellent urllib3 library under + license: see NOTICES. If you ever see @shazow, you should probably buy him + a drink or something. + """ + def __init__(self): + self._first_try = True + self._data = b'' + self._obj = zlib.decompressobj(zlib.MAX_WBITS) + + def __getattr__(self, name): + return getattr(self._obj, name) + + def decompress(self, data): + if not self._first_try: + return self._obj.decompress(data) + + self._data += data + try: + return self._obj.decompress(data) + except zlib.error: + self._first_try = False + self._obj = zlib.decompressobj(-zlib.MAX_WBITS) + try: + return self.decompress(self._data) + finally: + self._data = None diff --git a/vinetrimmer/vendor/hyper/common/exceptions.py b/vinetrimmer/vendor/hyper/common/exceptions.py new file mode 100644 index 0000000..268431a --- /dev/null +++ b/vinetrimmer/vendor/hyper/common/exceptions.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +""" +hyper/common/exceptions +~~~~~~~~~~~~~~~~~~~~~~~ + +Contains hyper's exceptions. +""" + + +class ChunkedDecodeError(Exception): + """ + An error was encountered while decoding a chunked response. + """ + pass + + +class InvalidResponseError(Exception): + """ + A problem was found with the response that makes it invalid. + """ + pass + + +class SocketError(Exception): + """ + An error occurred during socket operation. + """ + pass + + +class LineTooLongError(Exception): + """ + An attempt to read a line from a socket failed because no newline was + found. + """ + pass + + +# Create our own ConnectionResetError. +try: # pragma: no cover + ConnectionResetError = ConnectionResetError +except NameError: # pragma: no cover + class ConnectionResetError(Exception): + """ + A HTTP connection was unexpectedly reset. + """ + + +class TLSUpgrade(Exception): + """ + We upgraded to a new protocol in the NPN/ALPN handshake. + """ + def __init__(self, negotiated, sock): + super(TLSUpgrade, self).__init__() + self.negotiated = negotiated + self.sock = sock + + +class HTTPUpgrade(Exception): + """ + We upgraded to a new protocol via the HTTP Upgrade response. + """ + def __init__(self, negotiated, sock): + super(HTTPUpgrade, self).__init__() + self.negotiated = negotiated + self.sock = sock + + +class MissingCertFile(Exception): + """ + The certificate file could not be found. + """ + pass diff --git a/vinetrimmer/vendor/hyper/common/headers.py b/vinetrimmer/vendor/hyper/common/headers.py new file mode 100644 index 0000000..e474ce9 --- /dev/null +++ b/vinetrimmer/vendor/hyper/common/headers.py @@ -0,0 +1,260 @@ +# -*- coding: utf-8 -*- +""" +hyper/common/headers +~~~~~~~~~~~~~~~~~~~~~ + +Contains hyper's structures for storing and working with HTTP headers. +""" +import collections + +from vinetrimmer.vendor.hyper.common.util import to_bytestring, to_bytestring_tuple + + +class HTTPHeaderMap(collections.abc.MutableMapping): + """ + A structure that contains HTTP headers. + + HTTP headers are a curious beast. At the surface level they look roughly + like a name-value set, but in practice they have many variations that + make them tricky: + + - duplicate keys are allowed + - keys are compared case-insensitively + - duplicate keys are isomorphic to comma-separated values, *except when + they aren't*! + - they logically contain a form of ordering + + This data structure is an attempt to preserve all of that information + while being as user-friendly as possible. It retains all of the mapping + convenience methods (allowing by-name indexing), while avoiding using a + dictionary for storage. + + When iterated over, this structure returns headers in 'canonical form'. + This form is a tuple, where the first entry is the header name (in + lower-case), and the second entry is a list of header values (in original + case). + + The mapping always emits both names and values in the form of bytestrings: + never unicode strings. It can accept names and values in unicode form, and + will automatically be encoded to bytestrings using UTF-8. The reason for + what appears to be a user-unfriendly decision here is primarily to allow + the broadest-possible compatibility (to make it possible to send headers in + unusual encodings) while ensuring that users are never confused about what + type of data they will receive. + + .. warning:: Note that this data structure makes none of the performance + guarantees of a dictionary. Lookup and deletion is not an O(1) + operation. Inserting a new value *is* O(1), all other + operations are O(n), including *replacing* a header entirely. + """ + def __init__(self, *args, **kwargs): + # The meat of the structure. In practice, headers are an ordered list + # of tuples. This early version of the data structure simply uses this + # directly under the covers. + # + # An important curiosity here is that the headers are not stored in + # 'canonical form', but are instead stored in the form they were + # provided in. This is to ensure that it is always possible to + # reproduce the original header structure if necessary. This leads to + # some unfortunate performance costs on structure access where it is + # often necessary to transform the data into canonical form on access. + # This cost is judged acceptable in low-level code like `hyper`, but + # higher-level abstractions should consider if they really require this + # logic. + self._items = [] + + for arg in args: + self._items.extend(map(lambda x: to_bytestring_tuple(*x), arg)) + + for k, v in kwargs.items(): + self._items.append(to_bytestring_tuple(k, v)) + + def __getitem__(self, key): + """ + Unlike the dict __getitem__, this returns a list of items in the order + they were added. These items are returned in 'canonical form', meaning + that comma-separated values are split into multiple values. + """ + key = to_bytestring(key) + values = [] + + for k, v in self._items: + if _keys_equal(k, key): + values.extend(x[1] for x in canonical_form(k, v)) + + if not values: + raise KeyError("Nonexistent header key: {}".format(key)) + + return values + + def __setitem__(self, key, value): + """ + Unlike the dict __setitem__, this appends to the list of items. + """ + self._items.append(to_bytestring_tuple(key, value)) + + def __delitem__(self, key): + """ + Sadly, __delitem__ is kind of stupid here, but the best we can do is + delete all headers with a given key. To correctly achieve the 'KeyError + on missing key' logic from dictionaries, we need to do this slowly. + """ + key = to_bytestring(key) + indices = [] + for (i, (k, v)) in enumerate(self._items): + if _keys_equal(k, key): + indices.append(i) + + if not indices: + raise KeyError("Nonexistent header key: {}".format(key)) + + for i in indices[::-1]: + self._items.pop(i) + + def __iter__(self): + """ + This mapping iterates like the list of tuples it is. The headers are + returned in canonical form. + """ + for pair in self._items: + for value in canonical_form(*pair): + yield value + + def __len__(self): + """ + The length of this mapping is the number of individual headers in + canonical form. Sadly, this is a somewhat expensive operation. + """ + size = 0 + for _ in self: + size += 1 + + return size + + def __contains__(self, key): + """ + If any header is present with this key, returns True. + """ + key = to_bytestring(key) + return any(_keys_equal(key, k) for k, _ in self._items) + + def keys(self): + """ + Returns an iterable of the header keys in the mapping. This explicitly + does not filter duplicates, ensuring that it's the same length as + len(). + """ + for n, _ in self: + yield n + + def items(self): + """ + This mapping iterates like the list of tuples it is. + """ + return self.__iter__() + + def values(self): + """ + This is an almost nonsensical query on a header dictionary, but we + satisfy it in the exact same way we satisfy 'keys'. + """ + for _, v in self: + yield v + + def get(self, name, default=None): + """ + Unlike the dict get, this returns a list of items in the order + they were added. + """ + try: + return self[name] + except KeyError: + return default + + def iter_raw(self): + """ + Allows iterating over the headers in 'raw' form: that is, the form in + which they were added to the structure. This iteration is in order, + and can be used to rebuild the original headers (e.g. to determine + exactly what a server sent). + """ + for item in self._items: + yield item + + def replace(self, key, value): + """ + Replace existing header with new value. If header doesn't exist this + method work like ``__setitem__``. Replacing leads to deletion of all + existing headers with the same name. + """ + key, value = to_bytestring_tuple(key, value) + indices = [] + for (i, (k, v)) in enumerate(self._items): + if _keys_equal(k, key): + indices.append(i) + + # If the key isn't present, this is easy: just append and abort early. + if not indices: + self._items.append((key, value)) + return + + # Delete all but the first. I swear, this is the correct slicing + # syntax! + base_index = indices[0] + for i in indices[:0:-1]: + self._items.pop(i) + + del self._items[base_index] + self._items.insert(base_index, (key, value)) + + def merge(self, other): + """ + Merge another header set or any other dict-like into this one. + """ + # Short circuit to avoid infinite loops in case we try to merge into + # ourselves. + if other is self: + return + + if isinstance(other, HTTPHeaderMap): + self._items.extend(other.iter_raw()) + return + + for k, v in other.items(): + self._items.append(to_bytestring_tuple(k, v)) + + def __eq__(self, other): + return self._items == other._items + + def __ne__(self, other): + return self._items != other._items + + def __str__(self): # pragma: no cover + return 'HTTPHeaderMap(%s)' % self._items + + def __repr__(self): # pragma: no cover + return str(self) + + +def canonical_form(k, v): + """ + Returns an iterable of key-value-pairs corresponding to the header in + canonical form. This means that the header is split on commas unless for + any reason it's a super-special snowflake (I'm looking at you Set-Cookie). + """ + SPECIAL_SNOWFLAKES = set([b'set-cookie', b'set-cookie2']) + + k = k.lower() + + if k in SPECIAL_SNOWFLAKES: + yield k, v + else: + for sub_val in v.split(b','): + yield k, sub_val.strip() + + +def _keys_equal(x, y): + """ + Returns 'True' if the two keys are equal by the laws of HTTP headers. + """ + return x.lower() == y.lower() diff --git a/vinetrimmer/vendor/hyper/common/util.py b/vinetrimmer/vendor/hyper/common/util.py new file mode 100644 index 0000000..231c935 --- /dev/null +++ b/vinetrimmer/vendor/hyper/common/util.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +""" +hyper/common/util +~~~~~~~~~~~~~~~~~ + +General utility functions for use with hyper. +""" +from vinetrimmer.vendor.hyper.compat import unicode, bytes, imap +from ..packages.rfc3986.uri import URIReference +from ..compat import is_py3 + + +def to_bytestring(element): + """ + Converts a single string to a bytestring, encoding via UTF-8 if needed. + """ + if isinstance(element, unicode): + return element.encode('utf-8') + elif isinstance(element, bytes): + return element + else: + raise ValueError("Non string type.") + + +def to_bytestring_tuple(*x): + """ + Converts the given strings to a bytestring if necessary, returning a + tuple. Uses ``to_bytestring``. + """ + return tuple(imap(to_bytestring, x)) + + +def to_host_port_tuple(host_port_str, default_port=80): + """ + Converts the given string containing a host and possibly a port + to a tuple. + """ + uri = URIReference( + scheme=None, + authority=host_port_str, + path=None, + query=None, + fragment=None + ) + + host = uri.host.strip('[]') + if not uri.port: + port = default_port + else: + port = int(uri.port) + + return (host, port) + + +def to_native_string(string, encoding='utf-8'): + if isinstance(string, str): + return string + + return string.decode(encoding) if is_py3 else string.encode(encoding) diff --git a/vinetrimmer/vendor/hyper/compat.py b/vinetrimmer/vendor/hyper/compat.py new file mode 100644 index 0000000..368b5fe --- /dev/null +++ b/vinetrimmer/vendor/hyper/compat.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# flake8: noqa +""" +hyper/compat +~~~~~~~~~~~~ + +Normalizes the Python 2/3 API for internal use. +""" +from contextlib import contextmanager +import sys +import zlib + +try: + from . import ssl_compat +except ImportError: + # TODO log? + ssl_compat = None + +_ver = sys.version_info +is_py2 = _ver[0] == 2 +is_py2_7_9_or_later = _ver[0] >= 2 and _ver[1] >= 7 and _ver[2] >= 9 +is_py3 = _ver[0] == 3 +is_py3_3 = is_py3 and _ver[1] == 3 + + +@contextmanager +def ignore_missing(): + try: + yield + except (AttributeError, NotImplementedError): # pragma: no cover + pass + +if is_py2: + if is_py2_7_9_or_later: + import ssl + else: + ssl = ssl_compat + + from urllib import urlencode + from urlparse import urlparse, urlsplit + from itertools import imap + + def to_byte(char): + return ord(char) + + def decode_hex(b): + return b.decode('hex') + + def write_to_stdout(data): + sys.stdout.write(data + '\n') + sys.stdout.flush() + + # The standard zlib.compressobj() accepts only positional arguments. + def zlib_compressobj(level=6, method=zlib.DEFLATED, wbits=15, memlevel=8, + strategy=zlib.Z_DEFAULT_STRATEGY): + return zlib.compressobj(level, method, wbits, memlevel, strategy) + + unicode = unicode + bytes = str + +elif is_py3: + from urllib.parse import urlencode, urlparse, urlsplit + + imap = map + + def to_byte(char): + return char + + def decode_hex(b): + return bytes.fromhex(b) + + def write_to_stdout(data): + sys.stdout.buffer.write(data + b'\n') + sys.stdout.buffer.flush() + + zlib_compressobj = zlib.compressobj + + if is_py3_3: + ssl = ssl_compat + else: + import ssl + + unicode = str + bytes = bytes diff --git a/vinetrimmer/vendor/hyper/contrib.py b/vinetrimmer/vendor/hyper/contrib.py new file mode 100644 index 0000000..35fa993 --- /dev/null +++ b/vinetrimmer/vendor/hyper/contrib.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +""" +hyper/contrib +~~~~~~~~~~~~~ + +Contains a few utilities for use with other HTTP libraries. +""" +try: + from requests.adapters import HTTPAdapter + from requests.models import Response + from requests.structures import CaseInsensitiveDict + from requests.utils import get_encoding_from_headers + from requests.cookies import extract_cookies_to_jar +except ImportError: # pragma: no cover + HTTPAdapter = object + +from vinetrimmer.vendor.hyper.common.connection import HTTPConnection +from vinetrimmer.vendor.hyper.compat import urlparse +from vinetrimmer.vendor.hyper.tls import init_context + + +class HTTP20Adapter(HTTPAdapter): + """ + A Requests Transport Adapter that uses hyper to send requests over + HTTP/2. This implements some degree of connection pooling to maximise the + HTTP/2 gain. + """ + def __init__(self, *args, **kwargs): + #: A mapping between HTTP netlocs and ``HTTP20Connection`` objects. + self.connections = {} + + def get_connection(self, host, port, scheme, cert=None): + """ + Gets an appropriate HTTP/2 connection object based on + host/port/scheme/cert tuples. + """ + secure = (scheme == 'https') + + if port is None: # pragma: no cover + port = 80 if not secure else 443 + + ssl_context = None + if cert is not None: + ssl_context = init_context(cert=cert) + + try: + conn = self.connections[(host, port, scheme, cert)] + except KeyError: + conn = HTTPConnection( + host, + port, + secure=secure, + ssl_context=ssl_context) + self.connections[(host, port, scheme, cert)] = conn + + return conn + + def send(self, request, stream=False, cert=None, **kwargs): + """ + Sends a HTTP message to the server. + """ + parsed = urlparse(request.url) + conn = self.get_connection( + parsed.hostname, + parsed.port, + parsed.scheme, + cert=cert) + + # Build the selector. + selector = parsed.path + selector += '?' + parsed.query if parsed.query else '' + selector += '#' + parsed.fragment if parsed.fragment else '' + + conn.request( + request.method, + selector, + request.body, + request.headers + ) + resp = conn.get_response() + + r = self.build_response(request, resp) + + if not stream: + r.content + + return r + + def build_response(self, request, resp): + """ + Builds a Requests' response object. This emulates most of the logic of + the standard fuction but deals with the lack of the ``.headers`` + property on the HTTP20Response object. + + Additionally, this function builds in a number of features that are + purely for HTTPie. This is to allow maximum compatibility with what + urllib3 does, so that HTTPie doesn't fall over when it uses us. + """ + response = Response() + + response.status_code = resp.status + response.headers = CaseInsensitiveDict(resp.headers.iter_raw()) + response.raw = resp + response.reason = resp.reason + response.encoding = get_encoding_from_headers(response.headers) + + extract_cookies_to_jar(response.cookies, request, response) + response.url = request.url + + response.request = request + response.connection = self + + # First horrible patch: Requests expects its raw responses to have a + # release_conn method, which I don't. We should monkeypatch a no-op on. + resp.release_conn = lambda: None + + # Next, add the things HTTPie needs. It needs the following things: + # + # - The `raw` object has a property called `_original_response` that is + # a `httplib` response object. + # - `raw._original_response` has three simple properties: `version`, + # `status`, `reason`. + # - `raw._original_response.version` has one of three values: `9`, + # `10`, `11`. + # - `raw._original_response.msg` exists. + # - `raw._original_response.msg._headers` exists and is an iterable of + # two-tuples. + # + # We fake this out. Most of this exists on our response object already, + # and the rest can be faked. + # + # All of this exists for httpie, which I don't have any tests for, + # so I'm not going to bother adding test coverage for it. + class FakeOriginalResponse(object): # pragma: no cover + def __init__(self, headers): + self._headers = headers + + def get_all(self, name, default=None): + values = [] + + for n, v in self._headers: + if n == name.lower(): + values.append(v) + + if not values: + return default + + return values + + def getheaders(self, name): + return self.get_all(name, []) + + response.raw._original_response = orig = FakeOriginalResponse(None) + orig.version = 20 + orig.status = resp.status + orig.reason = resp.reason + orig.msg = FakeOriginalResponse(resp.headers.iter_raw()) + + return response diff --git a/vinetrimmer/vendor/hyper/http11/__init__.py b/vinetrimmer/vendor/hyper/http11/__init__.py new file mode 100644 index 0000000..2faef66 --- /dev/null +++ b/vinetrimmer/vendor/hyper/http11/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +""" +hyper/http11 +~~~~~~~~~~~~ + +The HTTP/1.1 submodule that powers hyper. +""" diff --git a/vinetrimmer/vendor/hyper/http11/connection.py b/vinetrimmer/vendor/hyper/http11/connection.py new file mode 100644 index 0000000..baa8d77 --- /dev/null +++ b/vinetrimmer/vendor/hyper/http11/connection.py @@ -0,0 +1,384 @@ +# -*- coding: utf-8 -*- +""" +hyper/http11/connection +~~~~~~~~~~~~~~~~~~~~~~~ + +Objects that build hyper's connection-level HTTP/1.1 abstraction. +""" +import logging +import os +import socket +import base64 + +from collections.abc import Iterable, Mapping + +import collections +from vinetrimmer.vendor.hyperframe.frame import SettingsFrame + +from .response import HTTP11Response +from ..tls import wrap_socket, H2C_PROTOCOL +from ..common.bufsocket import BufferedSocket +from ..common.exceptions import TLSUpgrade, HTTPUpgrade +from ..common.headers import HTTPHeaderMap +from ..common.util import to_bytestring, to_host_port_tuple +from ..compat import bytes + +# We prefer pycohttpparser to the pure-Python interpretation +try: # pragma: no cover + from pycohttpparser.api import Parser +except ImportError: # pragma: no cover + from .parser import Parser + + +log = logging.getLogger(__name__) + +BODY_CHUNKED = 1 +BODY_FLAT = 2 + + +class HTTP11Connection(object): + """ + An object representing a single HTTP/1.1 connection to a server. + + :param host: The host to connect to. This may be an IP address or a + hostname, and optionally may include a port: for example, + ``'twitter.com'``, ``'twitter.com:443'`` or ``'127.0.0.1'``. + :param port: (optional) The port to connect to. If not provided and one + also isn't provided in the ``host`` parameter, defaults to 80. + :param secure: (optional) Whether the request should use TLS. Defaults to + ``False`` for most requests, but to ``True`` for any request issued to + port 443. + :param ssl_context: (optional) A class with custom certificate settings. + If not provided then hyper's default ``SSLContext`` is used instead. + :param proxy_host: (optional) The proxy to connect to. This can be an IP + address or a host name and may include a port. + :param proxy_port: (optional) The proxy port to connect to. If not provided + and one also isn't provided in the ``proxy`` parameter, + defaults to 8080. + """ + def __init__(self, host, port=None, secure=None, ssl_context=None, + proxy_host=None, proxy_port=None, **kwargs): + if port is None: + self.host, self.port = to_host_port_tuple(host, default_port=80) + else: + self.host, self.port = host, port + + # Record whether we plan to secure the request. In future this should + # be extended to a security profile, but a bool will do for now. + # TODO: Actually do something with this! + if secure is not None: + self.secure = secure + elif self.port == 443: + self.secure = True + else: + self.secure = False + + # only send http upgrade headers for non-secure connection + self._send_http_upgrade = not self.secure + + self.ssl_context = ssl_context + self._sock = None + + # Setup proxy details if applicable. + if proxy_host: + if proxy_port is None: + self.proxy_host, self.proxy_port = to_host_port_tuple( + proxy_host, default_port=8080 + ) + else: + self.proxy_host, self.proxy_port = proxy_host, proxy_port + else: + self.proxy_host = None + self.proxy_port = None + + #: The size of the in-memory buffer used to store data from the + #: network. This is used as a performance optimisation. Increase buffer + #: size to improve performance: decrease it to conserve memory. + #: Defaults to 64kB. + self.network_buffer_size = 65536 + + #: The object used to perform HTTP/1.1 parsing. Needs to conform to + #: the standard hyper parsing interface. + self.parser = Parser() + + def connect(self): + """ + Connect to the server specified when the object was created. This is a + no-op if we're already connected. + + :returns: Nothing. + """ + if self._sock is None: + if not self.proxy_host: + host = self.host + port = self.port + else: + host = self.proxy_host + port = self.proxy_port + + sock = socket.create_connection((host, port), 5) + proto = None + + if self.secure: + assert not self.proxy_host, "Proxy with HTTPS not supported." + sock, proto = wrap_socket(sock, host, self.ssl_context) + + log.debug("Selected protocol: %s", proto) + sock = BufferedSocket(sock, self.network_buffer_size) + + if proto not in ('http/1.1', None): + raise TLSUpgrade(proto, sock) + + self._sock = sock + + return + + def request(self, method, url, body=None, headers=None): + """ + This will send a request to the server using the HTTP request method + ``method`` and the selector ``url``. If the ``body`` argument is + present, it should be string or bytes object of data to send after the + headers are finished. Strings are encoded as UTF-8. To use other + encodings, pass a bytes object. The Content-Length header is set to the + length of the body field. + + :param method: The request method, e.g. ``'GET'``. + :param url: The URL to contact, e.g. ``'/path/segment'``. + :param body: (optional) The request body to send. Must be a bytestring, + an iterable of bytestring, or a file-like object. + :param headers: (optional) The headers to send on the request. + :returns: Nothing. + """ + + headers = headers or {} + + method = to_bytestring(method) + url = to_bytestring(url) + + if not isinstance(headers, HTTPHeaderMap): + if isinstance(headers, Mapping): + headers = HTTPHeaderMap(headers.items()) + elif isinstance(headers, Iterable): + headers = HTTPHeaderMap(headers) + else: + raise ValueError( + 'Header argument must be a dictionary or an iterable' + ) + + if self._sock is None: + self.connect() + + if self._send_http_upgrade: + self._add_upgrade_headers(headers) + self._send_http_upgrade = False + + # We may need extra headers. + if body: + body_type = self._add_body_headers(headers, body) + + if b'host' not in headers: + headers[b'host'] = self.host + + # Begin by emitting the header block. + self._send_headers(method, url, headers) + + # Next, send the request body. + if body: + self._send_body(body, body_type) + + return + + def get_response(self): + """ + Returns a response object. + + This is an early beta, so the response object is pretty stupid. That's + ok, we'll fix it later. + """ + headers = HTTPHeaderMap() + + response = None + while response is None: + # 'encourage' the socket to receive data. + self._sock.fill() + response = self.parser.parse_response(self._sock.buffer) + + for n, v in response.headers: + headers[n.tobytes()] = v.tobytes() + + self._sock.advance_buffer(response.consumed) + + if (response.status == 101 and + b'upgrade' in headers['connection'] and + H2C_PROTOCOL.encode('utf-8') in headers['upgrade']): + raise HTTPUpgrade(H2C_PROTOCOL, self._sock) + + return HTTP11Response( + response.status, + response.msg.tobytes(), + headers, + self._sock, + self + ) + + def _send_headers(self, method, url, headers): + """ + Handles the logic of sending the header block. + """ + self._sock.send(b' '.join([method, url, b'HTTP/1.1\r\n'])) + + for name, value in headers.iter_raw(): + name, value = to_bytestring(name), to_bytestring(value) + header = b''.join([name, b': ', value, b'\r\n']) + self._sock.send(header) + + self._sock.send(b'\r\n') + + def _add_body_headers(self, headers, body): + """ + Adds any headers needed for sending the request body. This will always + defer to the user-supplied header content. + + :returns: One of (BODY_CHUNKED, BODY_FLAT), indicating what type of + request body should be used. + """ + if b'content-length' in headers: + return BODY_FLAT + + if b'chunked' in headers.get(b'transfer-encoding', []): + return BODY_CHUNKED + + # For bytestring bodies we upload the content with a fixed length. + # For file objects, we use the length of the file object. + if isinstance(body, bytes): + length = str(len(body)).encode('utf-8') + elif hasattr(body, 'fileno'): + length = str(os.fstat(body.fileno()).st_size).encode('utf-8') + else: + length = None + + if length: + headers[b'content-length'] = length + return BODY_FLAT + + headers[b'transfer-encoding'] = b'chunked' + return BODY_CHUNKED + + def _add_upgrade_headers(self, headers): + # Add HTTP Upgrade headers. + headers[b'connection'] = b'Upgrade, HTTP2-Settings' + headers[b'upgrade'] = H2C_PROTOCOL + + # Encode SETTINGS frame payload in Base64 and put into the HTTP-2 + # Settings header. + http2_settings = SettingsFrame(0) + http2_settings.settings[SettingsFrame.INITIAL_WINDOW_SIZE] = 65535 + encoded_settings = base64.urlsafe_b64encode( + http2_settings.serialize_body() + ) + headers[b'HTTP2-Settings'] = encoded_settings.rstrip(b'=') + + def _send_body(self, body, body_type): + """ + Handles the HTTP/1.1 logic for sending HTTP bodies. This does magical + different things in different cases. + """ + if body_type == BODY_FLAT: + # Special case for files and other 'readable' objects. + if hasattr(body, 'read'): + return self._send_file_like_obj(body) + + # Case for bytestrings. + elif isinstance(body, bytes): + self._sock.send(body) + + return + + # Iterables that set a specific content length. + elif isinstance(body, collections.Iterable): + for item in body: + try: + self._sock.send(item) + except TypeError: + raise ValueError( + "Elements in iterable body must be bytestrings. " + "Illegal element: {}".format(item) + ) + return + + else: + raise ValueError( + 'Request body must be a bytestring, a file-like object ' + 'returning bytestrings or an iterable of bytestrings. ' + 'Got: {}'.format(type(body)) + ) + + # Chunked! + return self._send_chunked(body) + + def _send_chunked(self, body): + """ + Handles the HTTP/1.1 logic for sending a chunk-encoded body. + """ + # Chunked! For chunked bodies we don't special-case, we just iterate + # over what we have and send stuff out. + for chunk in body: + length = '{0:x}'.format(len(chunk)).encode('ascii') + + # For now write this as four 'send' calls. That's probably + # inefficient, let's come back to it. + try: + self._sock.send(length) + self._sock.send(b'\r\n') + self._sock.send(chunk) + self._sock.send(b'\r\n') + except TypeError: + raise ValueError( + "Iterable bodies must always iterate in bytestrings" + ) + + self._sock.send(b'0\r\n\r\n') + return + + def _send_file_like_obj(self, fobj): + """ + Handles streaming a file-like object to the network. + """ + while True: + block = fobj.read(16*1024) + if not block: + break + + try: + self._sock.send(block) + except TypeError: + raise ValueError( + "File-like bodies must return bytestrings. Got: " + "{}".format(type(block)) + ) + + return + + def close(self): + """ + Closes the connection. This closes the socket and then abandons the + reference to it. After calling this method, any outstanding + :class:`Response <hyper.http11.response.Response>` objects will throw + exceptions if attempts are made to read their bodies. + + In some cases this method will automatically be called. + + .. warning:: This method should absolutely only be called when you are + certain the connection object is no longer needed. + """ + self._sock.close() + self._sock = None + + # The following two methods are the implementation of the context manager + # protocol. + def __enter__(self): + return self + + def __exit__(self, type, value, tb): + self.close() + return False # Never swallow exceptions. diff --git a/vinetrimmer/vendor/hyper/http11/parser.py b/vinetrimmer/vendor/hyper/http11/parser.py new file mode 100644 index 0000000..ee39121 --- /dev/null +++ b/vinetrimmer/vendor/hyper/http11/parser.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +""" +hyper/http11/parser +~~~~~~~~~~~~~~~~~~~ + +This module contains hyper's pure-Python HTTP/1.1 parser. This module defines +an abstraction layer for HTTP/1.1 parsing that allows for dropping in other +modules if needed, in order to obtain speedups on your chosen platform. +""" +from collections import namedtuple + + +Response = namedtuple( + 'Response', ['status', 'msg', 'minor_version', 'headers', 'consumed'] +) + + +class ParseError(Exception): + """ + An invalid HTTP message was passed to the parser. + """ + pass + + +class Parser(object): + """ + A single HTTP parser object. + This object is not thread-safe, and it does maintain state that is shared + across parsing requests. For this reason, make sure that access to this + object is synchronized if you use it across multiple threads. + """ + def __init__(self): + pass + + def parse_response(self, buffer): + """ + Parses a single HTTP response from a buffer. + :param buffer: A ``memoryview`` object wrapping a buffer containing a + HTTP response. + :returns: A :class:`Response <hyper.http11.parser.Response>` object, or + ``None`` if there is not enough data in the buffer. + """ + # Begin by copying the data out of the buffer. This is necessary + # because as much as possible we want to use the built-in bytestring + # methods, rather than looping over the data in Python. + temp_buffer = buffer.tobytes() + + index = temp_buffer.find(b'\n') + if index == -1: + return None + + version, status, reason = temp_buffer[0:index].split(None, 2) + if not version.startswith(b'HTTP/1.'): + raise ParseError("Not HTTP/1.X!") + + minor_version = int(version[7:]) + status = int(status) + reason = memoryview(reason.strip()) + + # Chomp the newline. + index += 1 + + # Now, parse the headers out. + end_index = index + headers = [] + + while True: + end_index = temp_buffer.find(b'\n', index) + if end_index == -1: + return None + elif (end_index - index) <= 1: + # Chomp the newline + end_index += 1 + break + + name, value = temp_buffer[index:end_index].split(b':', 1) + value = value.strip() + headers.append((memoryview(name), memoryview(value))) + index = end_index + 1 + + resp = Response(status, reason, minor_version, headers, end_index) + return resp diff --git a/vinetrimmer/vendor/hyper/http11/response.py b/vinetrimmer/vendor/hyper/http11/response.py new file mode 100644 index 0000000..ee23be0 --- /dev/null +++ b/vinetrimmer/vendor/hyper/http11/response.py @@ -0,0 +1,322 @@ +# -*- coding: utf-8 -*- +""" +hyper/http11/response +~~~~~~~~~~~~~~~~~~~~~ + +Contains the HTTP/1.1 equivalent of the HTTPResponse object defined in +httplib/http.client. +""" +import logging +import weakref +import zlib + +from ..common.decoder import DeflateDecoder +from ..common.exceptions import ChunkedDecodeError, InvalidResponseError +from ..common.exceptions import ConnectionResetError + +log = logging.getLogger(__name__) + + +class HTTP11Response(object): + """ + An ``HTTP11Response`` wraps the HTTP/1.1 response from the server. It + provides access to the response headers and the entity body. The response + is an iterable object and can be used in a with statement. + """ + def __init__(self, code, reason, headers, sock, connection=None): + #: The reason phrase returned by the server. + self.reason = reason + + #: The status code returned by the server. + self.status = code + + #: The response headers. These are determined upon creation, assigned + #: once, and never assigned again. + self.headers = headers + + #: The response trailers. These are always intially ``None``. + self.trailers = None + + # The socket this response is being sent over. + self._sock = sock + + # Whether we expect the connection to be closed. If we do, we don't + # bother checking for content-length, we just keep reading until + # we no longer can. + self._expect_close = False + if b'close' in self.headers.get(b'connection', []): + self._expect_close = True + + # The expected length of the body. + try: + self._length = int(self.headers[b'content-length'][0]) + except KeyError: + self._length = None + + # Whether we expect a chunked response. + self._chunked = ( + b'chunked' in self.headers.get(b'transfer-encoding', []) + ) + + # One of the following must be true: we must expect that the connection + # will be closed following the body, or that a content-length was sent, + # or that we're getting a chunked response. + # FIXME: Remove naked assert, replace with something better. + assert self._expect_close or self._length is not None or self._chunked + + # This object is used for decompressing gzipped request bodies. Right + # now we only support gzip because that's all the RFC mandates of us. + # Later we'll add support for more encodings. + # This 16 + MAX_WBITS nonsense is to force gzip. See this + # Stack Overflow answer for more: + # http://stackoverflow.com/a/2695466/1401686 + if b'gzip' in self.headers.get(b'content-encoding', []): + self._decompressobj = zlib.decompressobj(16 + zlib.MAX_WBITS) + elif b'deflate' in self.headers.get(b'content-encoding', []): + self._decompressobj = DeflateDecoder() + else: + self._decompressobj = None + + # This is a reference that allows for the Response class to tell the + # parent connection object to throw away its socket object. This is to + # be used when the connection is genuinely closed, so that the user + # can keep using the Connection object. + # Strictly, we take a weakreference to this so that we don't set up a + # reference cycle. + if connection is not None: + self._parent = weakref.ref(connection) + else: + self._parent = None + + self._buffered_data = b'' + self._chunker = None + + def read(self, amt=None, decode_content=True): + """ + Reads the response body, or up to the next ``amt`` bytes. + + :param amt: (optional) The amount of data to read. If not provided, all + the data will be read from the response. + :param decode_content: (optional) If ``True``, will transparently + decode the response data. + :returns: The read data. Note that if ``decode_content`` is set to + ``True``, the actual amount of data returned may be different to + the amount requested. + """ + # Return early if we've lost our connection. + if self._sock is None: + return b'' + + if self._chunked: + return self._normal_read_chunked(amt, decode_content) + + # If we're asked to do a read without a length, we need to read + # everything. That means either the entire content length, or until the + # socket is closed, depending. + if amt is None: + if self._length is not None: + amt = self._length + elif self._expect_close: + return self._read_expect_closed(decode_content) + else: # pragma: no cover + raise InvalidResponseError( + "Response must either have length or Connection: close" + ) + + # Otherwise, we've been asked to do a bounded read. We should read no + # more than the remaining length, obviously. + # FIXME: Handle cases without _length + if self._length is not None: + amt = min(amt, self._length) + + # Now, issue reads until we read that length. This is to account for + # the fact that it's possible that we'll be asked to read more than + # 65kB in one shot. + to_read = amt + chunks = [] + + # Ideally I'd like this to read 'while to_read', but I want to be + # defensive against the admittedly unlikely case that the socket + # returns *more* data than I want. + while to_read > 0: + chunk = self._sock.recv(amt).tobytes() + + # If we got an empty read, but were expecting more, the remote end + # has hung up. Raise an exception if we were expecting more data, + # but if we were expecting the remote end to close then it's ok. + if not chunk: + if self._length is not None or not self._expect_close: + self.close(socket_close=True) + raise ConnectionResetError("Remote end hung up!") + + break + + to_read -= len(chunk) + chunks.append(chunk) + + data = b''.join(chunks) + if self._length is not None: + self._length -= len(data) + + # If we're at the end of the request, we have some cleaning up to do. + # Close the stream, and if necessary flush the buffer. Checking that + # we're at the end is actually obscenely complex: either we've read the + # full content-length or, if we were expecting a closed connection, + # we've had a read shorter than the requested amount. We also have to + # do this before we try to decompress the body. + end_of_request = (self._length == 0 or + (self._expect_close and len(data) < amt)) + + # We may need to decode the body. + if decode_content and self._decompressobj and data: + data = self._decompressobj.decompress(data) + + if decode_content and self._decompressobj and end_of_request: + data += self._decompressobj.flush() + + # We're at the end. Close the connection. Explicit check for zero here + # because self._length might be None. + if end_of_request: + self.close(socket_close=self._expect_close) + + return data + + def read_chunked(self, decode_content=True): + """ + Reads chunked transfer encoded bodies. This method returns a generator: + each iteration of which yields one chunk *unless* the chunks are + compressed, in which case it yields whatever the decompressor provides + for each chunk. + + .. warning:: This may yield the empty string, without that being the + end of the body! + """ + if not self._chunked: + raise ChunkedDecodeError( + "Attempted chunked read of non-chunked body." + ) + + # Return early if possible. + if self._sock is None: + return + + while True: + # Read to the newline to get the chunk length. This is a + # hexadecimal integer. + chunk_length = int(self._sock.readline().tobytes().strip(), 16) + data = b'' + + # If the chunk length is zero, consume the newline and then we're + # done. If we were decompressing data, return the remaining data. + if not chunk_length: + self._sock.readline() + + if decode_content and self._decompressobj: + yield self._decompressobj.flush() + + self.close(socket_close=self._expect_close) + break + + # Then read that many bytes. + while chunk_length > 0: + chunk = self._sock.recv(chunk_length).tobytes() + data += chunk + chunk_length -= len(chunk) + + assert chunk_length == 0 + + # Now, consume the newline. + self._sock.readline() + + # We may need to decode the body. + if decode_content and self._decompressobj and data: + data = self._decompressobj.decompress(data) + + yield data + + return + + def close(self, socket_close=False): + """ + Close the response. This causes the Response to lose access to the + backing socket. In some cases, it can also cause the backing connection + to be torn down. + + :param socket_close: Whether to close the backing socket. + :returns: Nothing. + """ + if socket_close and self._parent is not None: + # The double call is necessary because we need to dereference the + # weakref. If the weakref is no longer valid, that's fine, there's + # no connection object to tell. + parent = self._parent() + if parent is not None: + parent.close() + + self._sock = None + + def _read_expect_closed(self, decode_content): + """ + Implements the logic for an unbounded read on a socket that we expect + to be closed by the remote end. + """ + # In this case, just read until we cannot read anymore. Then, close the + # socket, becuase we know we have to. + chunks = [] + while True: + try: + chunk = self._sock.recv(65535).tobytes() + if not chunk: + break + except ConnectionResetError: + break + else: + chunks.append(chunk) + + self.close(socket_close=True) + + # We may need to decompress the data. + data = b''.join(chunks) + if decode_content and self._decompressobj: + data = self._decompressobj.decompress(data) + data += self._decompressobj.flush() + + return data + + def _normal_read_chunked(self, amt, decode_content): + """ + Implements the logic for calling ``read()`` on a chunked response. + """ + # If we're doing a full read, read it as chunked and then just join + # the chunks together! + if amt is None: + return self._buffered_data + b''.join(self.read_chunked()) + + if self._chunker is None: + self._chunker = self.read_chunked() + + # Otherwise, we have a certain amount of data we want to read. + current_amount = len(self._buffered_data) + + extra_data = [self._buffered_data] + while current_amount < amt: + try: + chunk = next(self._chunker) + except StopIteration: + self.close(socket_close=self._expect_close) + break + + current_amount += len(chunk) + extra_data.append(chunk) + + data = b''.join(extra_data) + self._buffered_data = data[amt:] + return data[:amt] + + # The following methods implement the context manager protocol. + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + return False # Never swallow exceptions. diff --git a/vinetrimmer/vendor/hyper/http20/__init__.py b/vinetrimmer/vendor/hyper/http20/__init__.py new file mode 100644 index 0000000..4a756d2 --- /dev/null +++ b/vinetrimmer/vendor/hyper/http20/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +""" +hyper/http20 +~~~~~~~~~~~~ + +The HTTP/2 submodule that powers hyper. +""" diff --git a/vinetrimmer/vendor/hyper/http20/connection.py b/vinetrimmer/vendor/hyper/http20/connection.py new file mode 100644 index 0000000..849180c --- /dev/null +++ b/vinetrimmer/vendor/hyper/http20/connection.py @@ -0,0 +1,850 @@ +# -*- coding: utf-8 -*- +""" +hyper/http20/connection +~~~~~~~~~~~~~~~~~~~~~~~ + +Objects that build hyper's connection-level HTTP/2 abstraction. +""" +import vinetrimmer.vendor.h2.connection +import vinetrimmer.vendor.h2.events +import vinetrimmer.vendor.h2.settings + +from ..compat import ssl +from ..tls import wrap_socket, H2_NPN_PROTOCOLS, H2C_PROTOCOL +from ..common.exceptions import ConnectionResetError +from ..common.bufsocket import BufferedSocket +from ..common.headers import HTTPHeaderMap +from ..common.util import to_host_port_tuple, to_native_string, to_bytestring +from ..compat import unicode, bytes +from .stream import Stream +from .response import HTTP20Response, HTTP20Push +from .window import FlowControlManager +from .exceptions import ConnectionError, StreamResetError +from . import errors + +import errno +import logging +import socket +import time +import threading + +log = logging.getLogger(__name__) + +DEFAULT_WINDOW_SIZE = 65535 + +TRANSIENT_SSL_ERRORS = (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE) + + +class _LockedObject(object): + """ + A wrapper class that hides a specific object behind a lock. + + The goal here is to provide a simple way to protect access to an object + that cannot safely be simultaneously accessed from multiple threads. The + intended use of this class is simple: take hold of it with a context + manager, which returns the protected object. + """ + def __init__(self, obj): + self.lock = threading.RLock() + self._obj = obj + + def __enter__(self): + self.lock.acquire() + return self._obj + + def __exit__(self, _exc_type, _exc_val, _exc_tb): + self.lock.release() + + +class HTTP20Connection(object): + """ + An object representing a single HTTP/2 connection to a server. + + This object behaves similarly to the Python standard library's + ``HTTPConnection`` object, with a few critical differences. + + Most of the standard library's arguments to the constructor are irrelevant + for HTTP/2 or not supported by hyper. + + :param host: The host to connect to. This may be an IP address or a + hostname, and optionally may include a port: for example, + ``'http2bin.org'``, ``'http2bin.org:443'`` or ``'127.0.0.1'``. + :param port: (optional) The port to connect to. If not provided and one + also isn't provided in the ``host`` parameter, defaults to 443. + :param secure: (optional) Whether the request should use TLS. Defaults to + ``False`` for most requests, but to ``True`` for any request issued to + port 443. + :param window_manager: (optional) The class to use to manage flow control + windows. This needs to be a subclass of the + :class:`BaseFlowControlManager + <hyper.http20.window.BaseFlowControlManager>`. If not provided, + :class:`FlowControlManager <hyper.http20.window.FlowControlManager>` + will be used. + :param enable_push: (optional) Whether the server is allowed to push + resources to the client (see + :meth:`get_pushes() <hyper.HTTP20Connection.get_pushes>`). + :param ssl_context: (optional) A class with custom certificate settings. + If not provided then hyper's default ``SSLContext`` is used instead. + :param proxy_host: (optional) The proxy to connect to. This can be an IP + address or a host name and may include a port. + :param proxy_port: (optional) The proxy port to connect to. If not provided + and one also isn't provided in the ``proxy`` parameter, defaults to + 8080. + """ + def __init__(self, host, port=None, secure=None, window_manager=None, + enable_push=False, ssl_context=None, proxy_host=None, + proxy_port=None, force_proto=None, **kwargs): + """ + Creates an HTTP/2 connection to a specific server. + """ + if port is None: + self.host, self.port = to_host_port_tuple(host, default_port=443) + else: + self.host, self.port = host, port + + if secure is not None: + self.secure = secure + elif self.port == 443: + self.secure = True + else: + self.secure = False + + self._enable_push = enable_push + self.ssl_context = ssl_context + + # Setup proxy details if applicable. + if proxy_host: + if proxy_port is None: + self.proxy_host, self.proxy_port = to_host_port_tuple( + proxy_host, default_port=8080 + ) + else: + self.proxy_host, self.proxy_port = proxy_host, proxy_port + else: + self.proxy_host = None + self.proxy_port = None + + #: The size of the in-memory buffer used to store data from the + #: network. This is used as a performance optimisation. Increase buffer + #: size to improve performance: decrease it to conserve memory. + #: Defaults to 64kB. + self.network_buffer_size = 65536 + + self.force_proto = force_proto + + # Concurrency + # + # Use one lock (_lock) to synchronize any interaction with global + # connection state, e.g. stream creation/deletion. + # + # It's ok to use the same in lock all these cases as they occur at + # different/linked points in the connection's lifecycle. + # + # Use another 2 locks (_write_lock, _read_lock) to synchronize + # - _send_cb + # - _recv_cb + # respectively. + # + # I.e, send/recieve on the connection and its streams are serialized + # separately across the threads accessing the connection. This is a + # simple way of providing thread-safety. + # + # _write_lock and _read_lock synchronize all interactions between + # streams and the connnection. There is a third I/O callback, + # _close_stream, passed to a stream's constructor. It does not need to + # be synchronized, it uses _send_cb internally (which is serialized); + # its other activity (safe deletion of the stream from self.streams) + # does not require synchronization. + # + # _read_lock may be acquired when already holding the _write_lock, + # when they both held it is always by acquiring _write_lock first. + # + # Either _read_lock or _write_lock may be acquired whilst holding _lock + # which should always be acquired before either of the other two. + self._lock = threading.RLock() + self._write_lock = threading.RLock() + self._read_lock = threading.RLock() + + # Create the mutable state. + self.__wm_class = window_manager or FlowControlManager + self.__init_state() + + return + + def __init_state(self): + """ + Initializes the 'mutable state' portions of the HTTP/2 connection + object. + + This method exists to enable HTTP20Connection objects to be reused if + they're closed, by resetting the connection object to its basic state + whenever it ends up closed. Any situation that needs to recreate the + connection can call this method and it will be done. + + This is one of the only methods in hyper that is truly private, as + users should be strongly discouraged from messing about with connection + objects themselves. + """ + self._conn = _LockedObject(vinetrimmer.vendor.h2.connection.H2Connection()) + + # Streams are stored in a dictionary keyed off their stream IDs. We + # also save the most recent one for easy access without having to walk + # the dictionary. + # + # We add a set of all streams that we or the remote party forcefully + # closed with RST_STREAM, to avoid encountering issues where frames + # were already in flight before the RST was processed. + # + # Finally, we add a set of streams that recently received data. When + # using multiple threads, this avoids reading on threads that have just + # acquired the I/O lock whose streams have already had their data read + # for them by prior threads. + self.streams = {} + self.recent_stream = None + self.next_stream_id = 1 + self.reset_streams = set() + self.recent_recv_streams = set() + + # The socket used to send data. + self._sock = None + + # Instantiate a window manager. + self.window_manager = self.__wm_class(65535) + + return + + def ping(self, opaque_data): + """ + Send a PING frame. + + Concurrency + ----------- + + This method is thread-safe. + + :param opaque_data: A bytestring of length 8 that will be sent in the + PING frame. + :returns: Nothing + """ + self.connect() + with self._write_lock: + with self._conn as conn: + conn.ping(to_bytestring(opaque_data)) + self._send_outstanding_data() + + def request(self, method, url, body=None, headers=None): + """ + This will send a request to the server using the HTTP request method + ``method`` and the selector ``url``. If the ``body`` argument is + present, it should be string or bytes object of data to send after the + headers are finished. Strings are encoded as UTF-8. To use other + encodings, pass a bytes object. The Content-Length header is set to the + length of the body field. + + Concurrency + ----------- + + This method is thread-safe. + + :param method: The request method, e.g. ``'GET'``. + :param url: The URL to contact, e.g. ``'/path/segment'``. + :param body: (optional) The request body to send. Must be a bytestring + or a file-like object. + :param headers: (optional) The headers to send on the request. + :returns: A stream ID for the request. + """ + headers = headers or {} + + # Concurrency + # + # It's necessary to hold a lock while this method runs to satisfy H2 + # protocol requirements. + # + # - putrequest obtains the next valid new stream_id + # - endheaders sends a http2 message using the new stream_id + # + # If threads interleave these operations, it could result in messages + # being sent in the wrong order, which can lead to the out-of-order + # messages with lower stream IDs being closed prematurely. + with self._write_lock: + stream_id = self.putrequest(method, url) + + default_headers = (':method', ':scheme', ':authority', ':path') + for name, value in headers.items(): + is_default = to_native_string(name) in default_headers + self.putheader(name, value, stream_id, replace=is_default) + + # Convert the body to bytes if needed. + if body and isinstance(body, (unicode, bytes)): + body = to_bytestring(body) + + self.endheaders(message_body=body, final=True, stream_id=stream_id) + + return stream_id + + def _get_stream(self, stream_id): + if stream_id is None: + return self.recent_stream + elif stream_id in self.reset_streams or stream_id not in self.streams: + raise StreamResetError("Stream forcefully closed") + else: + return self.streams[stream_id] + + def get_response(self, stream_id=None): + """ + Should be called after a request is sent to get a response from the + server. If sending multiple parallel requests, pass the stream ID of + the request whose response you want. Returns a + :class:`HTTP20Response <hyper.HTTP20Response>` instance. + If you pass no ``stream_id``, you will receive the oldest + :class:`HTTPResponse <hyper.HTTP20Response>` still outstanding. + + Concurrency + ----------- + + This method is thread-safe. + + :param stream_id: (optional) The stream ID of the request for which to + get a response. + :returns: A :class:`HTTP20Response <hyper.HTTP20Response>` object. + """ + stream = self._get_stream(stream_id) + return HTTP20Response(stream.getheaders(), stream) + + def get_pushes(self, stream_id=None, capture_all=False): + """ + Returns a generator that yields push promises from the server. **Note + that this method is not idempotent**: promises returned in one call + will not be returned in subsequent calls. Iterating through generators + returned by multiple calls to this method simultaneously results in + undefined behavior. + + :param stream_id: (optional) The stream ID of the request for which to + get push promises. + :param capture_all: (optional) If ``False``, the generator will yield + all buffered push promises without blocking. If ``True``, the + generator will first yield all buffered push promises, then yield + additional ones as they arrive, and terminate when the original + stream closes. + :returns: A generator of :class:`HTTP20Push <hyper.HTTP20Push>` objects + corresponding to the streams pushed by the server. + """ + stream = self._get_stream(stream_id) + for promised_stream_id, headers in stream.get_pushes(capture_all): + yield HTTP20Push( + HTTPHeaderMap(headers), self.streams[promised_stream_id] + ) + + def connect(self): + """ + Connect to the server specified when the object was created. This is a + no-op if we're already connected. + + Concurrency + ----------- + + This method is thread-safe. It may be called from multiple threads, and + is a noop for all threads apart from the first. + + :returns: Nothing. + + """ + with self._lock: + if self._sock is not None: + return + + if not self.proxy_host: + host = self.host + port = self.port + else: + host = self.proxy_host + port = self.proxy_port + + sock = socket.create_connection((host, port)) + + if self.secure: + assert not self.proxy_host, "Proxy with HTTPS not supported." + sock, proto = wrap_socket(sock, host, self.ssl_context, + force_proto=self.force_proto) + else: + proto = H2C_PROTOCOL + + log.debug("Selected NPN protocol: %s", proto) + assert proto in H2_NPN_PROTOCOLS or proto == H2C_PROTOCOL + + self._sock = BufferedSocket(sock, self.network_buffer_size) + + self._send_preamble() + + def _connect_upgrade(self, sock): + """ + Called by the generic HTTP connection when we're being upgraded. Locks + in a new socket and places the backing state machine into an upgrade + state, then sends the preamble. + """ + self._sock = sock + + with self._conn as conn: + conn.initiate_upgrade_connection() + conn.update_settings( + {h2.settings.ENABLE_PUSH: int(self._enable_push)} + ) + self._send_outstanding_data() + + # The server will also send an initial settings frame, so get it. + # However, we need to make sure our stream state is set up properly + # first, or any extra data we receive might cause us problems. + s = self._new_stream(local_closed=True) + self.recent_stream = s + + self._recv_cb() + + def _send_preamble(self): + """ + Sends the necessary HTTP/2 preamble. + """ + # We need to send the connection header immediately on this + # connection, followed by an initial settings frame. + with self._conn as conn: + conn.initiate_connection() + conn.update_settings( + {vinetrimmer.vendor.h2.settings.ENABLE_PUSH: int(self._enable_push)} + ) + self._send_outstanding_data() + + # The server will also send an initial settings frame, so get it. + self._recv_cb() + + def close(self, error_code=None): + """ + Close the connection to the server. + + Concurrency + ----------- + + This method is thread-safe. + + :param error_code: (optional) The error code to reset all streams with. + :returns: Nothing. + """ + # Concurrency + # + # It's necessary to hold the lock here to ensure that threads closing + # the connection see consistent state, and to prevent creation of + # of new streams while the connection is being closed. + # + # I/O occurs while the lock is held; waiting threads will see a delay. + with self._lock: + # Close all streams + for stream in list(self.streams.values()): + log.debug("Close stream %d" % stream.stream_id) + stream.close(error_code) + + # Send GoAway frame to the server + try: + with self._conn as conn: + conn.close_connection(error_code or 0) + self._send_outstanding_data(tolerate_peer_gone=True) + except Exception as e: # pragma: no cover + log.warn("GoAway frame could not be sent: %s" % e) + + if self._sock is not None: + self._sock.close() + self.__init_state() + + def _send_outstanding_data(self, tolerate_peer_gone=False, + send_empty=True): + # Concurrency + # + # Hold _write_lock; getting and writing data from _conn is synchronized + # + # I/O occurs while the lock is held; waiting threads will see a delay. + with self._write_lock: + with self._conn as conn: + data = conn.data_to_send() + if data or send_empty: + self._send_cb(data, tolerate_peer_gone=tolerate_peer_gone) + + def putrequest(self, method, selector, **kwargs): + """ + This should be the first call for sending a given HTTP request to a + server. It returns a stream ID for the given connection that should be + passed to all subsequent request building calls. + + Concurrency + ----------- + + This method is thread-safe. It can be called from multiple threads, + and each thread should receive a unique stream ID. + + :param method: The request method, e.g. ``'GET'``. + :param selector: The path selector. + :returns: A stream ID for the request. + """ + # Create a new stream. + s = self._new_stream() + + # To this stream we need to immediately add a few headers that are + # HTTP/2 specific. These are: ":method", ":scheme", ":authority" and + # ":path". We can set all of these now. + s.add_header(":method", method) + s.add_header(":scheme", "https" if self.secure else "http") + s.add_header(":authority", self.host) + s.add_header(":path", selector) + + # Save the stream. + self.recent_stream = s + + return s.stream_id + + def putheader(self, header, argument, stream_id=None, replace=False): + """ + Sends an HTTP header to the server, with name ``header`` and value + ``argument``. + + Unlike the ``httplib`` version of this function, this version does not + actually send anything when called. Instead, it queues the headers up + to be sent when you call + :meth:`endheaders() <hyper.HTTP20Connection.endheaders>`. + + This method ensures that headers conform to the HTTP/2 specification. + In particular, it strips out the ``Connection`` header, as that header + is no longer valid in HTTP/2. This is to make it easy to write code + that runs correctly in both HTTP/1.1 and HTTP/2. + + :param header: The name of the header. + :param argument: The value of the header. + :param stream_id: (optional) The stream ID of the request to add the + header to. + :returns: Nothing. + """ + stream = self._get_stream(stream_id) + stream.add_header(header, argument, replace) + + return + + def endheaders(self, message_body=None, final=False, stream_id=None): + """ + Sends the prepared headers to the server. If the ``message_body`` + argument is provided it will also be sent to the server as the body of + the request, and the stream will immediately be closed. If the + ``final`` argument is set to True, the stream will also immediately + be closed: otherwise, the stream will be left open and subsequent calls + to ``send()`` will be required. + + :param message_body: (optional) The body to send. May not be provided + assuming that ``send()`` will be called. + :param final: (optional) If the ``message_body`` parameter is provided, + should be set to ``True`` if no further data will be provided via + calls to :meth:`send() <hyper.HTTP20Connection.send>`. + :param stream_id: (optional) The stream ID of the request to finish + sending the headers on. + :returns: Nothing. + """ + self.connect() + + stream = self._get_stream(stream_id) + + headers_only = (message_body is None and final) + + # Concurrency: + # + # Hold _write_lock: synchronize access to the connection's HPACK + # encoder and decoder and the subsquent write to the connection + with self._write_lock: + stream.send_headers(headers_only) + + # Send whatever data we have. + if message_body is not None: + stream.send_data(message_body, final) + + self._send_outstanding_data() + + return + + def send(self, data, final=False, stream_id=None): + """ + Sends some data to the server. This data will be sent immediately + (excluding the normal HTTP/2 flow control rules). If this is the last + data that will be sent as part of this request, the ``final`` argument + should be set to ``True``. This will cause the stream to be closed. + + :param data: The data to send. + :param final: (optional) Whether this is the last bit of data to be + sent on this request. + :param stream_id: (optional) The stream ID of the request to send the + data on. + :returns: Nothing. + """ + stream = self._get_stream(stream_id) + stream.send_data(data, final) + + return + + def _new_stream(self, stream_id=None, local_closed=False): + """ + Returns a new stream object for this connection. + """ + # Concurrency + # + # Hold _lock: ensure that threads accessing the connection see + # self.next_stream_id in a consistent state + # + # No I/O occurs, the delay in waiting threads depends on their number. + with self._lock: + s = Stream( + stream_id or self.next_stream_id, + self.__wm_class(DEFAULT_WINDOW_SIZE), + self._conn, + self._send_outstanding_data, + self._recv_cb, + self._stream_close_cb, + ) + s.local_closed = local_closed + self.streams[s.stream_id] = s + self.next_stream_id += 2 + + return s + + def _send_cb(self, data, tolerate_peer_gone=False): + """ + This is the callback used by streams to send data on the connection. + + This acts as a dumb wrapper around the socket send method. + """ + # Concurrency + # + # Hold _write_lock: ensures only writer at a time + # + # I/O occurs while the lock is held; waiting threads will see a delay. + with self._write_lock: + try: + self._sock.sendall(data) + except socket.error as e: + if (not tolerate_peer_gone or + e.errno not in (errno.EPIPE, errno.ECONNRESET)): + raise + + def _adjust_receive_window(self, frame_len): + """ + Adjusts the window size in response to receiving a DATA frame of length + ``frame_len``. May send a WINDOWUPDATE frame if necessary. + """ + # Concurrency + # + # Hold _write_lock; synchronize the window manager update and the + # subsequent potential write to the connection + # + # I/O may occur while the lock is held; waiting threads may see a + # delay. + with self._write_lock: + increment = self.window_manager._handle_frame(frame_len) + + if increment: + with self._conn as conn: + conn.increment_flow_control_window(increment) + self._send_outstanding_data(tolerate_peer_gone=True) + + return + + def _single_read(self): + """ + Performs a single read from the socket and hands the data off to the + h2 connection object. + """ + # Begin by reading what we can from the socket. + # + # Concurrency + # + # Synchronizes reading the data + # + # I/O occurs while the lock is held; waiting threads will see a delay. + with self._read_lock: + if self._sock is None: + raise ConnectionError('tried to read after connection close') + self._sock.fill() + data = self._sock.buffer.tobytes() + self._sock.advance_buffer(len(data)) + with self._conn as conn: + events = conn.receive_data(data) + stream_ids = set(getattr(e, 'stream_id', -1) for e in events) + stream_ids.discard(-1) # sentinel + stream_ids.discard(0) # connection events + self.recent_recv_streams |= stream_ids + + for event in events: + if isinstance(event, vinetrimmer.vendor.h2.events.DataReceived): + self._adjust_receive_window(event.flow_controlled_length) + self.streams[event.stream_id].receive_data(event) + elif isinstance(event, vinetrimmer.vendor.h2.events.PushedStreamReceived): + if self._enable_push: + self._new_stream(event.pushed_stream_id, local_closed=True) + self.streams[event.parent_stream_id].receive_push(event) + else: + # Servers are forbidden from sending push promises when + # the ENABLE_PUSH setting is 0, but the spec leaves the + # client action undefined when they do it anyway. So we + # just refuse the stream and go about our business. + self._send_rst_frame(event.pushed_stream_id, 7) + elif isinstance(event, vinetrimmer.vendor.h2.events.ResponseReceived): + self.streams[event.stream_id].receive_response(event) + elif isinstance(event, vinetrimmer.vendor.h2.events.TrailersReceived): + self.streams[event.stream_id].receive_trailers(event) + elif isinstance(event, vinetrimmer.vendor.h2.events.StreamEnded): + self.streams[event.stream_id].receive_end_stream(event) + elif isinstance(event, vinetrimmer.vendor.h2.events.StreamReset): + if event.stream_id not in self.reset_streams: + self.reset_streams.add(event.stream_id) + self.streams[event.stream_id].receive_reset(event) + elif isinstance(event, vinetrimmer.vendor.h2.events.ConnectionTerminated): + # If we get GoAway with error code zero, we are doing a + # graceful shutdown and all is well. Otherwise, throw an + # exception. + self.close() + + # If an error occured, try to read the error description from + # code registry otherwise use the frame's additional data. + if event.error_code != 0: + try: + name, number, description = errors.get_data( + event.error_code + ) + except ValueError: + error_string = ( + "Encountered error code %d" % event.error_code + ) + else: + error_string = ( + "Encountered error %s %s: %s" % + (name, number, description) + ) + + raise ConnectionError(error_string) + else: + log.info("Received unhandled event %s", event) + + self._send_outstanding_data(tolerate_peer_gone=True, send_empty=False) + + def _recv_cb(self, stream_id=0): + """ + This is the callback used by streams to read data from the connection. + + This stream reads what data it can, and throws it into the underlying + connection, before farming out any events that fire to the relevant + streams. If the socket remains readable, it will then optimistically + continue to attempt to read. + + This is generally called by a stream, not by the connection itself, and + it's likely that streams will read a frame that doesn't belong to them. + + :param stream_id: (optional) The stream ID of the stream reading data + from the connection. + + """ + # Begin by reading what we can from the socket. + # + # Concurrency + # + # Ignore this read if some other thread has recently read data from + # from the requested stream. + # + # The lock here looks broad, but is needed to ensure correct behavior + # when there are multiple readers of the same stream. It is + # re-acquired in the calls to self._single_read. + # + # I/O occurs while the lock is held; waiting threads will see a delay. + with self._read_lock: + log.debug('recv for stream %d with %s already present', + stream_id, + self.recent_recv_streams) + if stream_id in self.recent_recv_streams: + self.recent_recv_streams.discard(stream_id) + return + + # make sure to validate the stream is readable. + # if the connection was reset, this stream id won't appear in + # self.streams and will cause this call to raise an exception. + if stream_id: + self._get_stream(stream_id) + + # TODO: Re-evaluate this. + self._single_read() + count = 9 + retry_wait = 0.05 # can improve responsiveness to delay the retry + + while count and self._sock is not None and self._sock.can_read: + # If the connection has been closed, bail out, but retry + # on transient errors. + try: + self._single_read() + except ConnectionResetError: + break + except ssl.SSLError as e: # pragma: no cover + # these are transient errors that can occur while reading + # from ssl connections. + if e.args[0] in TRANSIENT_SSL_ERRORS: + continue + else: + raise + except socket.error as e: # pragma: no cover + if e.errno in (errno.EINTR, errno.EAGAIN): + # if 'interrupted' or 'try again', continue + time.sleep(retry_wait) + continue + elif e.errno == errno.ECONNRESET: + break + else: + raise + + count -= 1 + + def _send_rst_frame(self, stream_id, error_code): + """ + Send reset stream frame with error code and remove stream from map. + """ + # Concurrency + # + # Hold _write_lock; synchronize generating the reset frame and writing + # it + # + # I/O occurs while the lock is held; waiting threads will see a delay. + with self._write_lock: + with self._conn as conn: + conn.reset_stream(stream_id, error_code=error_code) + self._send_outstanding_data() + + # Concurrency + # + # Hold _lock; the stream storage is being updated. No I/O occurs, any + # delay is proportional to the number of waiting threads. + with self._lock: + try: + del self.streams[stream_id] + self.recent_recv_streams.discard(stream_id) + except KeyError as e: # pragma: no cover + log.warn( + "Stream with id %d does not exist: %s", + stream_id, e) + + # Keep track of the fact that we reset this stream in case there + # are other frames in flight. + self.reset_streams.add(stream_id) + + def _stream_close_cb(self, stream_id): + """ + Called by a stream when it is closing, so that state can be cleared. + """ + try: + del self.streams[stream_id] + self.recent_recv_streams.discard(stream_id) + except KeyError: + pass + + # The following two methods are the implementation of the context manager + # protocol. + def __enter__(self): + return self + + def __exit__(self, type, value, tb): + self.close() + return False # Never swallow exceptions. diff --git a/vinetrimmer/vendor/hyper/http20/errors.py b/vinetrimmer/vendor/hyper/http20/errors.py new file mode 100644 index 0000000..bcad885 --- /dev/null +++ b/vinetrimmer/vendor/hyper/http20/errors.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# flake8: noqa +""" +hyper/http20/errors +~~~~~~~~~~~~~~~~~~~ + +Global error code registry containing the established HTTP/2 error codes. +The registry is based on a 32-bit space so we use the error code to index into +the array. + +The current registry is available at: +https://tools.ietf.org/html/rfc7540#section-11.4 +""" + +NO_ERROR = {'Name': 'NO_ERROR', + 'Code': '0x0', + 'Description': 'Graceful shutdown'} +PROTOCOL_ERROR = {'Name': 'PROTOCOL_ERROR', + 'Code': '0x1', + 'Description': 'Protocol error detected'} +INTERNAL_ERROR = {'Name': 'INTERNAL_ERROR', + 'Code': '0x2', + 'Description': 'Implementation fault'} +FLOW_CONTROL_ERROR = {'Name': 'FLOW_CONTROL_ERROR', + 'Code': '0x3', + 'Description': 'Flow control limits exceeded'} +SETTINGS_TIMEOUT = {'Name': 'SETTINGS_TIMEOUT', + 'Code': '0x4', + 'Description': 'Settings not acknowledged'} +STREAM_CLOSED = {'Name': 'STREAM_CLOSED', + 'Code': '0x5', + 'Description': 'Frame received for closed stream'} +FRAME_SIZE_ERROR = {'Name': 'FRAME_SIZE_ERROR', + 'Code': '0x6', + 'Description': 'Frame size incorrect'} +REFUSED_STREAM = {'Name': 'REFUSED_STREAM ', + 'Code': '0x7', + 'Description': 'Stream not processed'} +CANCEL = {'Name': 'CANCEL', + 'Code': '0x8', + 'Description': 'Stream cancelled'} +COMPRESSION_ERROR = {'Name': 'COMPRESSION_ERROR', + 'Code': '0x9', + 'Description': 'Compression state not updated'} +CONNECT_ERROR = {'Name': 'CONNECT_ERROR', + 'Code': '0xa', + 'Description': + 'TCP connection error for CONNECT method'} +ENHANCE_YOUR_CALM = {'Name': 'ENHANCE_YOUR_CALM', + 'Code': '0xb', + 'Description': 'Processing capacity exceeded'} +INADEQUATE_SECURITY = {'Name': 'INADEQUATE_SECURITY', + 'Code': '0xc', + 'Description': + 'Negotiated TLS parameters not acceptable'} +HTTP_1_1_REQUIRED = {'Name': 'HTTP_1_1_REQUIRED', + 'Code': '0xd', + 'Description': 'Use HTTP/1.1 for the request'} + +H2_ERRORS = [NO_ERROR, PROTOCOL_ERROR, INTERNAL_ERROR, FLOW_CONTROL_ERROR, + SETTINGS_TIMEOUT, STREAM_CLOSED, FRAME_SIZE_ERROR, REFUSED_STREAM, + CANCEL, COMPRESSION_ERROR, CONNECT_ERROR, ENHANCE_YOUR_CALM, + INADEQUATE_SECURITY, HTTP_1_1_REQUIRED] + + +def get_data(error_code): + """ + Lookup the error code description, if not available throw a value error + """ + if error_code < 0 or error_code >= len(H2_ERRORS): + raise ValueError("Error code is invalid") + + name = H2_ERRORS[error_code]['Name'] + number = H2_ERRORS[error_code]['Code'] + description = H2_ERRORS[error_code]['Description'] + + return name, number, description diff --git a/vinetrimmer/vendor/hyper/http20/exceptions.py b/vinetrimmer/vendor/hyper/http20/exceptions.py new file mode 100644 index 0000000..69e2581 --- /dev/null +++ b/vinetrimmer/vendor/hyper/http20/exceptions.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +""" +hyper/http20/exceptions +~~~~~~~~~~~~~~~~~~~~~~~ + +This defines exceptions used in the HTTP/2 portion of hyper. +""" + + +class HTTP20Error(Exception): + """ + The base class for all of ``hyper``'s HTTP/2-related exceptions. + """ + pass + + +class HPACKEncodingError(HTTP20Error): + """ + An error has been encountered while performing HPACK encoding. + """ + pass + + +class HPACKDecodingError(HTTP20Error): + """ + An error has been encountered while performing HPACK decoding. + """ + pass + + +class ConnectionError(HTTP20Error): + """ + The remote party signalled an error affecting the entire HTTP/2 + connection, and the connection has been closed. + """ + pass + + +class ProtocolError(HTTP20Error): + """ + The remote party violated the HTTP/2 protocol. + """ + pass + + +class StreamResetError(HTTP20Error): + """ + A stream was forcefully reset by the remote party. + """ + pass diff --git a/vinetrimmer/vendor/hyper/http20/response.py b/vinetrimmer/vendor/hyper/http20/response.py new file mode 100644 index 0000000..bb339b2 --- /dev/null +++ b/vinetrimmer/vendor/hyper/http20/response.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +""" +hyper/http20/response +~~~~~~~~~~~~~~~~~~~~~ + +Contains the HTTP/2 equivalent of the HTTPResponse object defined in +httplib/http.client. +""" +import logging +import zlib + +from ..common.decoder import DeflateDecoder +from ..common.headers import HTTPHeaderMap + +log = logging.getLogger(__name__) + + +def strip_headers(headers): + """ + Strips the headers attached to the instance of any header beginning + with a colon that ``hyper`` doesn't understand. This method logs at + warning level about the deleted headers, for discoverability. + """ + # Convert to list to ensure that we don't mutate the headers while + # we iterate over them. + for name in list(headers.keys()): + if name.startswith(b':'): + del headers[name] + + +class HTTP20Response(object): + """ + An ``HTTP20Response`` wraps the HTTP/2 response from the server. It + provides access to the response headers and the entity body. The response + is an iterable object and can be used in a with statement (though due to + the persistent connections used in HTTP/2 this has no effect, and is done + soley for compatibility). + """ + def __init__(self, headers, stream): + #: The reason phrase returned by the server. This is not used in + #: HTTP/2, and so is always the empty string. + self.reason = '' + + status = headers[b':status'][0] + strip_headers(headers) + + #: The status code returned by the server. + self.status = int(status) + + #: The response headers. These are determined upon creation, assigned + #: once, and never assigned again. + self.headers = headers + + # The response trailers. These are always intially ``None``. + self._trailers = None + + # The stream this response is being sent over. + self._stream = stream + + # We always read in one-data-frame increments from the stream, so we + # may need to buffer some for incomplete reads. + self._data_buffer = b'' + + # This object is used for decompressing gzipped request bodies. Right + # now we only support gzip because that's all the RFC mandates of us. + # Later we'll add support for more encodings. + # This 16 + MAX_WBITS nonsense is to force gzip. See this + # Stack Overflow answer for more: + # http://stackoverflow.com/a/2695466/1401686 + if b'gzip' in self.headers.get(b'content-encoding', []): + self._decompressobj = zlib.decompressobj(16 + zlib.MAX_WBITS) + elif b'deflate' in self.headers.get(b'content-encoding', []): + self._decompressobj = DeflateDecoder() + else: + self._decompressobj = None + + @property + def trailers(self): + """ + Trailers on the HTTP message, if any. + + .. warning:: Note that this property requires that the stream is + totally exhausted. This means that, if you have not + completely read from the stream, all stream data will be + read into memory. + """ + if self._trailers is None: + self._trailers = self._stream.gettrailers() or HTTPHeaderMap() + strip_headers(self._trailers) + + return self._trailers + + def read(self, amt=None, decode_content=True): + """ + Reads the response body, or up to the next ``amt`` bytes. + + :param amt: (optional) The amount of data to read. If not provided, all + the data will be read from the response. + :param decode_content: (optional) If ``True``, will transparently + decode the response data. + :returns: The read data. Note that if ``decode_content`` is set to + ``True``, the actual amount of data returned may be different to + the amount requested. + """ + if amt is not None and amt <= len(self._data_buffer): + data = self._data_buffer[:amt] + self._data_buffer = self._data_buffer[amt:] + response_complete = False + elif amt is not None: + read_amt = amt - len(self._data_buffer) + self._data_buffer += self._stream._read(read_amt) + data = self._data_buffer[:amt] + self._data_buffer = self._data_buffer[amt:] + response_complete = len(data) < amt + else: + data = b''.join([self._data_buffer, self._stream._read()]) + response_complete = True + + # We may need to decode the body. + if decode_content and self._decompressobj and data: + data = self._decompressobj.decompress(data) + + # If we're at the end of the request, we have some cleaning up to do. + # Close the stream, and if necessary flush the buffer. + if response_complete: + if decode_content and self._decompressobj: + data += self._decompressobj.flush() + + if self._stream.response_headers: + self.headers.merge(self._stream.response_headers) + + # We're at the end, close the connection. + if response_complete: + self.close() + + return data + + def read_chunked(self, decode_content=True): + """ + Reads chunked transfer encoded bodies. This method returns a generator: + each iteration of which yields one data frame *unless* the frames + contain compressed data and ``decode_content`` is ``True``, in which + case it yields whatever the decompressor provides for each chunk. + + .. warning:: This may yield the empty string, without that being the + end of the body! + """ + while True: + data = self._stream._read_one_frame() + + if data is None: + break + + if decode_content and self._decompressobj: + data = self._decompressobj.decompress(data) + + yield data + + if decode_content and self._decompressobj: + yield self._decompressobj.flush() + + self.close() + + return + + def fileno(self): + """ + Return the ``fileno`` of the underlying socket. This function is + currently not implemented. + """ + raise NotImplementedError("Not currently implemented.") + + def close(self): + """ + Close the response. In effect this closes the backing HTTP/2 stream. + + :returns: Nothing. + """ + self._stream.close() + + # The following methods implement the context manager protocol. + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + return False # Never swallow exceptions. + + +class HTTP20Push(object): + """ + Represents a request-response pair sent by the server through the server + push mechanism. + """ + def __init__(self, request_headers, stream): + #: The scheme of the simulated request + self.scheme = request_headers[b':scheme'][0] + #: The method of the simulated request (must be safe and cacheable, + #: e.g. GET) + self.method = request_headers[b':method'][0] + #: The authority of the simulated request (usually host:port) + self.authority = request_headers[b':authority'][0] + #: The path of the simulated request + self.path = request_headers[b':path'][0] + + strip_headers(request_headers) + + #: The headers the server attached to the simulated request. + self.request_headers = request_headers + + self._stream = stream + + def get_response(self): + """ + Get the pushed response provided by the server. + + :returns: A :class:`HTTP20Response <hyper.HTTP20Response>` object + representing the pushed response. + """ + return HTTP20Response(self._stream.getheaders(), self._stream) + + def cancel(self): + """ + Cancel the pushed response and close the stream. + + :returns: Nothing. + """ + self._stream.close(8) # CANCEL diff --git a/vinetrimmer/vendor/hyper/http20/stream.py b/vinetrimmer/vendor/hyper/http20/stream.py new file mode 100644 index 0000000..c7df1de --- /dev/null +++ b/vinetrimmer/vendor/hyper/http20/stream.py @@ -0,0 +1,341 @@ +# -*- coding: utf-8 -*- +""" +hyper/http20/stream +~~~~~~~~~~~~~~~~~~~ + +Objects that make up the stream-level abstraction of hyper's HTTP/2 support. + +These objects are not expected to be part of the public HTTP/2 API: they're +intended purely for use inside hyper's HTTP/2 abstraction. + +Conceptually, a single HTTP/2 connection is made up of many streams: each +stream is an independent, bi-directional sequence of HTTP headers and data. +Each stream is identified by a monotonically increasing integer, assigned to +the stream by the endpoint that initiated the stream. +""" +import vinetrimmer.vendor.h2.exceptions + +from ..common.headers import HTTPHeaderMap +from .util import h2_safe_headers +import logging + +log = logging.getLogger(__name__) + +# Define the largest chunk of data we'll send in one go. Realistically, we +# should take the MSS into account but that's pretty dull, so let's just say +# 1kB and call it a day. +MAX_CHUNK = 1024 + + +class Stream(object): + """ + A single HTTP/2 stream. + + A stream is an independent, bi-directional sequence of HTTP headers and + data. Each stream is identified by a single integer. From a HTTP + perspective, a stream _approximately_ matches a single request-response + pair. + """ + def __init__(self, + stream_id, + window_manager, + connection, + send_outstanding_data, + recv_cb, + close_cb): + self.stream_id = stream_id + self.headers = HTTPHeaderMap() + + # Set to a key-value set of the response headers once their + # HEADERS..CONTINUATION frame sequence finishes. + self.response_headers = None + + # Set to a key-value set of the response trailers once their + # HEADERS..CONTINUATION frame sequence finishes. + self.response_trailers = None + + # A dict mapping the promised stream ID of a pushed resource to a + # key-value set of its request headers. Entries are added once their + # PUSH_PROMISE..CONTINUATION frame sequence finishes. + self.promised_headers = {} + + # Unconsumed response data chunks. Empties after every call to _read(). + self.data = [] + + # Whether the remote side has completed the stream. + self.remote_closed = False + + # Whether we have closed the stream. + self.local_closed = False + + # There are two flow control windows: one for data we're sending, + # one for data being sent to us. + self._in_window_manager = window_manager + + # Save off a reference to the state machine wrapped with lock. + self._conn = connection + + # Save off a data callback. + self._send_outstanding_data = send_outstanding_data + self._recv_cb = recv_cb + self._close_cb = close_cb + + def add_header(self, name, value, replace=False): + """ + Adds a single HTTP header to the headers to be sent on the request. + """ + if not replace: + self.headers[name] = value + else: + self.headers.replace(name, value) + + def send_headers(self, end_stream=False): + """ + Sends the complete saved header block on the stream. + """ + headers = self.get_headers() + with self._conn as conn: + conn.send_headers(self.stream_id, headers, end_stream) + self._send_outstanding_data() + + if end_stream: + self.local_closed = True + + def send_data(self, data, final): + """ + Send some data on the stream. If this is the end of the data to be + sent, the ``final`` flag _must_ be set to True. If no data is to be + sent, set ``data`` to ``None``. + """ + # Define a utility iterator for file objects. + def file_iterator(fobj): + while True: + data = fobj.read(MAX_CHUNK) + yield data + if len(data) < MAX_CHUNK: + break + + # Build the appropriate iterator for the data, in chunks of CHUNK_SIZE. + if hasattr(data, 'read'): + chunks = file_iterator(data) + else: + chunks = (data[i:i+MAX_CHUNK] + for i in range(0, len(data), MAX_CHUNK)) + + for chunk in chunks: + self._send_chunk(chunk, final) + + def _read(self, amt=None): + """ + Read data from the stream. Unlike a normal read behaviour, this + function returns _at least_ ``amt`` data, but may return more. + """ + def listlen(list): + return sum(map(len, list)) + + # Keep reading until the stream is closed or we get enough data. + while (not self.remote_closed and + (amt is None or listlen(self.data) < amt)): + self._recv_cb(stream_id=self.stream_id) + + result = b''.join(self.data) + self.data = [] + return result + + def _read_one_frame(self): + """ + Reads a single data frame from the stream and returns it. + """ + # Keep reading until the stream is closed or we have a data frame. + while not self.remote_closed and not self.data: + self._recv_cb(stream_id=self.stream_id) + + try: + return self.data.pop(0) + except IndexError: + return None + + def receive_response(self, event): + """ + Receive response headers. + """ + # TODO: If this is called while we're still sending data, we may want + # to stop sending that data and check the response. Early responses to + # big uploads are almost always a problem. + self.response_headers = HTTPHeaderMap(event.headers) + + def receive_trailers(self, event): + """ + Receive response trailers. + """ + self.response_trailers = HTTPHeaderMap(event.headers) + + def receive_push(self, event): + """ + Receive the request headers for a pushed stream. + """ + self.promised_headers[event.pushed_stream_id] = event.headers + + def receive_data(self, event): + """ + Receive a chunk of data. + """ + size = event.flow_controlled_length + increment = self._in_window_manager._handle_frame(size) + + # Append the data to the buffer. + self.data.append(event.data) + + if increment: + try: + with self._conn as conn: + conn.increment_flow_control_window( + increment, stream_id=self.stream_id + ) + except vinetrimmer.vendor.h2.exceptions.StreamClosedError: + # We haven't got to it yet, but the stream is already + # closed. We don't need to increment the window in this + # case! + pass + else: + self._send_outstanding_data() + + def receive_end_stream(self, event): + """ + All of the data is returned now. + """ + self.remote_closed = True + + def receive_reset(self, event): + """ + Stream forcefully reset. + """ + self.remote_closed = True + self._close_cb(self.stream_id) + + def get_headers(self): + """ + Provides the headers to the connection object. + """ + # Strip any headers invalid in H2. + return h2_safe_headers(self.headers) + + def getheaders(self): + """ + Once all data has been sent on this connection, returns a key-value set + of the headers of the response to the original request. + """ + # Keep reading until all headers are received. + while self.response_headers is None: + self._recv_cb(stream_id=self.stream_id) + + # Find the Content-Length header if present. + self._in_window_manager.document_size = ( + int(self.response_headers.get(b'content-length', [0])[0]) + ) + + return self.response_headers + + def gettrailers(self): + """ + Once all data has been sent on this connection, returns a key-value set + of the trailers of the response to the original request. + + .. warning:: Note that this method requires that the stream is + totally exhausted. This means that, if you have not + completely read from the stream, all stream data will be + read into memory. + + :returns: The key-value set of the trailers, or ``None`` if no trailers + were sent. + """ + # Keep reading until the stream is done. + while not self.remote_closed: + self._recv_cb(stream_id=self.stream_id) + + return self.response_trailers + + def get_pushes(self, capture_all=False): + """ + Returns a generator that yields push promises from the server. Note + that this method is not idempotent; promises returned in one call will + not be returned in subsequent calls. Iterating through generators + returned by multiple calls to this method simultaneously results in + undefined behavior. + + :param capture_all: If ``False``, the generator will yield all buffered + push promises without blocking. If ``True``, the generator will + first yield all buffered push promises, then yield additional ones + as they arrive, and terminate when the original stream closes. + """ + while True: + for pair in self.promised_headers.items(): + yield pair + self.promised_headers = {} + if not capture_all or self.remote_closed: + break + self._recv_cb(stream_id=self.stream_id) + + def close(self, error_code=None): + """ + Closes the stream. If the stream is currently open, attempts to close + it as gracefully as possible. + + :param error_code: (optional) The error code to reset the stream with. + :returns: Nothing. + """ + # FIXME: I think this is overbroad, but for now it's probably ok. + if not (self.remote_closed and self.local_closed): + try: + with self._conn as conn: + conn.reset_stream(self.stream_id, error_code or 0) + except h2.exceptions.ProtocolError: + # If for any reason we can't reset the stream, just + # tolerate it. + pass + else: + self._send_outstanding_data(tolerate_peer_gone=True) + self.remote_closed = True + self.local_closed = True + + self._close_cb(self.stream_id) + + @property + def _out_flow_control_window(self): + """ + The size of our outbound flow control window. + """ + + with self._conn as conn: + return conn.local_flow_control_window(self.stream_id) + + def _send_chunk(self, data, final): + """ + Implements most of the sending logic. + + Takes a single chunk of size at most MAX_CHUNK, wraps it in a frame and + sends it. Optionally sets the END_STREAM flag if this is the last chunk + (determined by being of size less than MAX_CHUNK) and no more data is + to be sent. + """ + # If we don't fit in the connection window, try popping frames off the + # connection in hope that one might be a window update frame. + while len(data) > self._out_flow_control_window: + self._recv_cb() + + # If the length of the data is less than MAX_CHUNK, we're probably + # at the end of the file. If this is the end of the data, mark it + # as END_STREAM. + end_stream = False + if len(data) < MAX_CHUNK and final: + end_stream = True + + # Send the frame and decrement the flow control window. + with self._conn as conn: + conn.send_data( + stream_id=self.stream_id, data=data, end_stream=end_stream + ) + self._send_outstanding_data() + + if end_stream: + self.local_closed = True diff --git a/vinetrimmer/vendor/hyper/http20/util.py b/vinetrimmer/vendor/hyper/http20/util.py new file mode 100644 index 0000000..b116d09 --- /dev/null +++ b/vinetrimmer/vendor/hyper/http20/util.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +""" +hyper/http20/util +~~~~~~~~~~~~~~~~~ + +Utility functions for use with hyper. +""" +from collections import defaultdict + + +def combine_repeated_headers(kvset): + """ + Given a list of key-value pairs (like for HTTP headers!), combines pairs + with the same key together, separating the values with NULL bytes. This + function maintains the order of input keys, because it's awesome. + """ + def set_pop(set, item): + set.remove(item) + return item + + headers = defaultdict(list) + keys = set() + + for key, value in kvset: + headers[key].append(value) + keys.add(key) + + return [(set_pop(keys, k), b'\x00'.join(headers[k])) for k, v in kvset + if k in keys] + + +def split_repeated_headers(kvset): + """ + Given a set of key-value pairs (like for HTTP headers!), finds values that + have NULL bytes in them and splits them into a dictionary whose values are + lists. + """ + headers = defaultdict(list) + + for key, value in kvset: + headers[key] = value.split(b'\x00') + + return dict(headers) + + +def h2_safe_headers(headers): + """ + This method takes a set of headers that are provided by the user and + transforms them into a form that is safe for emitting over HTTP/2. + + Currently, this strips the Connection header and any header it refers to. + """ + stripped = { + i.lower().strip() + for k, v in headers if k == 'connection' + for i in v.split(',') + } + stripped.add('connection') + + return [header for header in headers if header[0] not in stripped] diff --git a/vinetrimmer/vendor/hyper/http20/window.py b/vinetrimmer/vendor/hyper/http20/window.py new file mode 100644 index 0000000..a4e13c3 --- /dev/null +++ b/vinetrimmer/vendor/hyper/http20/window.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +""" +hyper/http20/window +~~~~~~~~~~~~~~~~~~~ + +Objects that understand flow control in hyper. + +HTTP/2 implements connection- and stream-level flow control. This flow +control is mandatory. Unfortunately, it's difficult for hyper to be +all that intelligent about how it manages flow control in a general case. + +This module defines an interface for pluggable flow-control managers. These +managers will define a flow-control policy. This policy will determine when to +send WINDOWUPDATE frames. +""" + + +class BaseFlowControlManager(object): + """ + The abstract base class for flow control managers. + + This class defines the interface for pluggable flow-control managers. A + flow-control manager defines a flow-control policy, which basically boils + down to deciding when to increase the flow control window. + + This decision can be based on a number of factors: + + - the initial window size, + - the size of the document being retrieved, + - the size of the received data frames, + - any other information the manager can obtain + + A flow-control manager may be defined at the connection level or at the + stream level. If no stream-level flow-control manager is defined, an + instance of the connection-level flow control manager is used. + + A class that inherits from this one must not adjust the member variables + defined in this class. They are updated and set by methods on this class. + """ + def __init__(self, initial_window_size, document_size=None): + #: The initial size of the connection window in bytes. This is set at + #: creation time. + self.initial_window_size = initial_window_size + + #: The current size of the connection window. Any methods overridden + #: by the user must not adjust this value. + self.window_size = initial_window_size + + #: The size of the document being retrieved, in bytes. This is + #: retrieved from the Content-Length header, if provided. Note that + #: the total number of bytes that will be received may be larger than + #: this value due to HTTP/2 padding. It should not be assumed that + #: simply because the the document size is smaller than the initial + #: window size that there will never be a need to increase the window + #: size. + self.document_size = document_size + + def increase_window_size(self, frame_size): + """ + Determine whether or not to emit a WINDOWUPDATE frame. + + This method should be overridden to determine, based on the state of + the system and the size of the received frame, whether or not a + WindowUpdate frame should be sent for the stream. + + This method should *not* adjust any of the member variables of this + class. + + Note that this method is called before the window size is decremented + as a result of the frame being handled. + + :param frame_size: The size of the received frame. Note that this *may* + be zero. When this parameter is zero, it's possible that a + WINDOWUPDATE frame may want to be emitted anyway. A zero-length frame + size is usually associated with a change in the size of the receive + window due to a SETTINGS frame. + :returns: The amount to increase the receive window by. Return zero if + the window should not be increased. + """ + raise NotImplementedError( + "FlowControlManager is an abstract base class" + ) + + def blocked(self): + """ + Called whenever the remote endpoint reports that it is blocked behind + the flow control window. + + When this method is called the remote endpoint is signaling that it + has more data to send and that the transport layer is capable of + transmitting it, but that the HTTP/2 flow control window prevents it + being sent. + + This method should return the size by which the window should be + incremented, which may be zero. This method should *not* adjust any + of the member variables of this class. + + :returns: The amount to increase the receive window by. Return zero if + the window should not be increased. + """ + # TODO: Is this method necessary? + raise NotImplementedError( + "FlowControlManager is an abstract base class" + ) + + def _handle_frame(self, frame_size): + """ + This internal method is called by the connection or stream that owns + the flow control manager. It handles the generic behaviour of flow + control managers: namely, keeping track of the window size. + """ + rc = self.increase_window_size(frame_size) + self.window_size -= frame_size + self.window_size += rc + return rc + + def _blocked(self): + """ + This internal method is called by the connection or stream that owns + the flow control manager. It handles the generic behaviour of receiving + BLOCKED frames. + """ + rc = self.blocked() + self.window_size += rc + return rc + + +class FlowControlManager(BaseFlowControlManager): + """ + ``hyper``'s default flow control manager. + + This implements hyper's flow control algorithms. This algorithm attempts to + reduce the number of WINDOWUPDATE frames we send without blocking the + remote endpoint behind the flow control window. + + This algorithm will become more complicated over time. In the current form, + the algorithm is very simple: + + - When the flow control window gets less than 1/4 of the maximum size, + increment back to the maximum. + - Otherwise, if the flow control window gets to less than 1kB, increment + back to the maximum. + """ + def increase_window_size(self, frame_size): + future_window_size = self.window_size - frame_size + + if ((future_window_size < (self.initial_window_size / 4)) or + (future_window_size < 1000)): + return self.initial_window_size - future_window_size + + return 0 + + def blocked(self): + return self.initial_window_size - self.window_size diff --git a/vinetrimmer/vendor/hyper/httplib_compat.py b/vinetrimmer/vendor/hyper/httplib_compat.py new file mode 100644 index 0000000..deb07cf --- /dev/null +++ b/vinetrimmer/vendor/hyper/httplib_compat.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +""" +hyper/httplib_compat +~~~~~~~~~~~~~~~~~~~~ + +This file defines the publicly-accessible API for hyper. This API also +constitutes the abstraction layer between HTTP/1.1 and HTTP/2. + +This API doesn't currently work, and is a lower priority than the HTTP/2 +stack at this time. +""" +import socket +try: + import http.client as httplib +except ImportError: + import httplib + +from .compat import ssl +from .http20.tls import wrap_socket + +# If there's no NPN support, we're going to drop all support for HTTP/2. +try: + support_20 = ssl.HAS_NPN +except AttributeError: + support_20 = False + +# The HTTPConnection object is currently always the underlying one. +HTTPConnection = httplib.HTTPConnection +HTTPSConnection = httplib.HTTPSConnection + +# If we have NPN support, define our custom one, otherwise just use the +# default. +if support_20: + class HTTPSConnection(object): + """ + An object representing a single HTTPS connection, whether HTTP/1.1 or + HTTP/2. + + More specifically, this object represents an abstraction over the + distinction. This object encapsulates a connection object for one of + the specific types of connection, and delegates most of the work to + that object. + """ + def __init__(self, *args, **kwargs): + # Whatever arguments and keyword arguments are passed to this + # object need to be saved off for when we initialise one of our + # subsidiary objects. + self._original_args = args + self._original_kwargs = kwargs + + # Set up some variables we're going to use later. + self._sock = None + self._conn = None + + # Prepare our backlog of method calls. + self._call_queue = [] + + def __getattr__(self, name): + # Anything that can't be found on this instance is presumably a + # property of underlying connection object. + # We need to be a little bit careful here. There are a few methods + # that can act on a HTTPSConnection before it actually connects to + # the remote server. We don't want to change the semantics of the, + # HTTPSConnection so we need to spot these and queue them up. When + # we actually create the backing Connection, we'll apply them + # immediately. These methods can't throw exceptions, so we should + # be fine. + delay_methods = ["set_tunnel", "set_debuglevel"] + + if self._conn is None and name in delay_methods: + # Return a little closure that saves off the method call to + # apply later. + def capture(obj, *args, **kwargs): + self._call_queue.append((name, args, kwargs)) + return capture + elif self._conn is None: + # We're being told to do something! We can now connect to the + # remote server and build the connection object. + self._delayed_connect() + + # Call through to the underlying object. + return getattr(self._conn, name) + + def _delayed_connect(self): + """ + Called when we need to work out what kind of HTTPS connection we're + actually going to use. + """ + # Because we're ghetto, we're going to quickly create a + # HTTPConnection object to parse the args and kwargs for us, and + # grab the values out. + tempconn = httplib.HTTPConnection(*self._original_args, + **self._original_kwargs) + host = tempconn.host + port = tempconn.port + timeout = tempconn.timeout + source_address = tempconn.source_address + + # Connect to the remote server. + sock = socket.create_connection( + (host, port), + timeout, + source_address + ) + + # Wrap it in TLS. This needs to be looked at in future when I pull + # in the TLS verification logic from urllib3, but right now we + # accept insecurity because no-one's using this anyway. + sock = wrap_socket(sock, host) + + # At this early stage the library can't do HTTP/2, so who cares? + tempconn.sock = sock + self._sock = sock + self._conn = tempconn + + return diff --git a/vinetrimmer/vendor/hyper/packages/__init__.py b/vinetrimmer/vendor/hyper/packages/__init__.py new file mode 100644 index 0000000..4cf1e65 --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +""" +hyper/packages +~~~~~~~~~~~~~~ + +This module contains external packages that are vendored into hyper. +""" diff --git a/vinetrimmer/vendor/hyper/packages/hpack/__init__.py b/vinetrimmer/vendor/hyper/packages/hpack/__init__.py new file mode 100644 index 0000000..98b2e0f --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/hpack/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +""" +hpack +~~~~~ + +HTTP/2 header encoding for Python. +""" +__version__ = '1.0.1' diff --git a/vinetrimmer/vendor/hyper/packages/hpack/compat.py b/vinetrimmer/vendor/hyper/packages/hpack/compat.py new file mode 100644 index 0000000..03a0768 --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/hpack/compat.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" +hpack/compat +~~~~~~~~~~~~ + +Normalizes the Python 2/3 API for internal use. +""" +import sys + + +_ver = sys.version_info +is_py2 = _ver[0] == 2 +is_py3 = _ver[0] == 3 + +if is_py2: + def to_byte(char): + return ord(char) + + def decode_hex(b): + return b.decode('hex') + + unicode = unicode + bytes = str + +elif is_py3: + def to_byte(char): + return char + + def decode_hex(b): + return bytes.fromhex(b) + + unicode = str + bytes = bytes diff --git a/vinetrimmer/vendor/hyper/packages/hpack/exceptions.py b/vinetrimmer/vendor/hyper/packages/hpack/exceptions.py new file mode 100644 index 0000000..0dbc4d9 --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/hpack/exceptions.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +""" +hyper/http20/exceptions +~~~~~~~~~~~~~~~~~~~~~~~ + +This defines exceptions used in the HTTP/2 portion of hyper. +""" + +class HTTP20Error(Exception): + """ + The base class for all of ``hyper``'s HTTP/2-related exceptions. + """ + pass + + +class HPACKEncodingError(HTTP20Error): + """ + An error has been encountered while performing HPACK encoding. + """ + pass + + +class HPACKDecodingError(HTTP20Error): + """ + An error has been encountered while performing HPACK decoding. + """ + pass + + +class ConnectionError(HTTP20Error): + """ + The remote party signalled an error affecting the entire HTTP/2 + connection, and the connection has been closed. + """ + pass + + +class ProtocolError(HTTP20Error): + """ + The remote party violated the HTTP/2 protocol. + """ + pass diff --git a/vinetrimmer/vendor/hyper/packages/hpack/hpack.py b/vinetrimmer/vendor/hyper/packages/hpack/hpack.py new file mode 100644 index 0000000..c3c5663 --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/hpack/hpack.py @@ -0,0 +1,662 @@ +# -*- coding: utf-8 -*- +""" +hpack/hpack +~~~~~~~~~~~ + +Implements the HPACK header compression algorithm as detailed by the IETF. +""" +import collections +import logging + +from .compat import to_byte +from .huffman import HuffmanDecoder, HuffmanEncoder +from .huffman_constants import ( + REQUEST_CODES, REQUEST_CODES_LENGTH +) + +log = logging.getLogger(__name__) + + +def encode_integer(integer, prefix_bits): + """ + This encodes an integer according to the wacky integer encoding rules + defined in the HPACK spec. + """ + log.debug("Encoding %d with %d bits", integer, prefix_bits) + + max_number = (2 ** prefix_bits) - 1 + + if (integer < max_number): + return bytearray([integer]) # Seriously? + else: + elements = [max_number] + integer = integer - max_number + + while integer >= 128: + elements.append((integer % 128) + 128) + integer = integer // 128 # We need integer division + + elements.append(integer) + + return bytearray(elements) + + +def decode_integer(data, prefix_bits): + """ + This decodes an integer according to the wacky integer encoding rules + defined in the HPACK spec. Returns a tuple of the decoded integer and the + number of bytes that were consumed from ``data`` in order to get that + integer. + """ + multiple = lambda index: 128 ** (index - 1) + max_number = (2 ** prefix_bits) - 1 + mask = 0xFF >> (8 - prefix_bits) + index = 0 + + number = to_byte(data[index]) & mask + + if (number == max_number): + + while True: + index += 1 + next_byte = to_byte(data[index]) + + if next_byte >= 128: + number += (next_byte - 128) * multiple(index) + else: + number += next_byte * multiple(index) + break + + log.debug("Decoded %d, consumed %d bytes", number, index + 1) + + return (number, index + 1) + + +def _to_bytes(string): + """ + Convert string to bytes. + """ + if not isinstance(string, (str, bytes)): # pragma: no cover + string = str(string) + + return string if isinstance(string, bytes) else string.encode('utf-8') + + +def header_table_size(table): + """ + Calculates the 'size' of the header table as defined by the HTTP/2 + specification. + """ + # It's phenomenally frustrating that the specification feels it is able to + # tell me how large the header table is, considering that its calculations + # assume a very particular layout that most implementations will not have. + # I appreciate it's an attempt to prevent DoS attacks by sending lots of + # large headers in the header table, but it seems like a better approach + # would be to limit the size of headers. Ah well. + return sum(32 + len(name) + len(value) for name, value in table) + + +class Encoder(object): + """ + An HPACK encoder object. This object takes HTTP headers and emits encoded + HTTP/2 header blocks. + """ + # This is the static table of header fields. + static_table = [ + (b':authority', b''), + (b':method', b'GET'), + (b':method', b'POST'), + (b':path', b'/'), + (b':path', b'/index.html'), + (b':scheme', b'http'), + (b':scheme', b'https'), + (b':status', b'200'), + (b':status', b'204'), + (b':status', b'206'), + (b':status', b'304'), + (b':status', b'400'), + (b':status', b'404'), + (b':status', b'500'), + (b'accept-charset', b''), + (b'accept-encoding', b'gzip, deflate'), + (b'accept-language', b''), + (b'accept-ranges', b''), + (b'accept', b''), + (b'access-control-allow-origin', b''), + (b'age', b''), + (b'allow', b''), + (b'authorization', b''), + (b'cache-control', b''), + (b'content-disposition', b''), + (b'content-encoding', b''), + (b'content-language', b''), + (b'content-length', b''), + (b'content-location', b''), + (b'content-range', b''), + (b'content-type', b''), + (b'cookie', b''), + (b'date', b''), + (b'etag', b''), + (b'expect', b''), + (b'expires', b''), + (b'from', b''), + (b'host', b''), + (b'if-match', b''), + (b'if-modified-since', b''), + (b'if-none-match', b''), + (b'if-range', b''), + (b'if-unmodified-since', b''), + (b'last-modified', b''), + (b'link', b''), + (b'location', b''), + (b'max-forwards', b''), + (b'proxy-authenticate', b''), + (b'proxy-authorization', b''), + (b'range', b''), + (b'referer', b''), + (b'refresh', b''), + (b'retry-after', b''), + (b'server', b''), + (b'set-cookie', b''), + (b'strict-transport-security', b''), + (b'transfer-encoding', b''), + (b'user-agent', b''), + (b'vary', b''), + (b'via', b''), + (b'www-authenticate', b''), + ] + + def __init__(self): + self.header_table = collections.deque() + self._header_table_size = 4096 # This value set by the standard. + self.huffman_coder = HuffmanEncoder( + REQUEST_CODES, REQUEST_CODES_LENGTH + ) + + # We need to keep track of whether the header table size has been + # changed since we last encoded anything. If it has, we need to signal + # that change in the HPACK block. + self._table_size_changed = False + + @property + def header_table_size(self): + return self._header_table_size + + @header_table_size.setter + def header_table_size(self, value): + log.debug( + "Setting header table size to %d from %d", + value, + self._header_table_size + ) + + # If the new value is larger than the current one, no worries! + # Otherwise, we may need to shrink the header table. + if value < self._header_table_size: + current_size = header_table_size(self.header_table) + + while value < current_size: + header = self.header_table.pop() + n, v = header + current_size -= ( + 32 + len(n) + len(v) + ) + + log.debug( + "Removed %s: %s from the encoder header table", n, v + ) + + if value != self._header_table_size: + self._table_size_changed = True + + self._header_table_size = value + + def encode(self, headers, huffman=True): + """ + Takes a set of headers and encodes them into a HPACK-encoded header + block. + + Transforming the headers into a header block is a procedure that can + be modeled as a chain or pipe. First, the headers are encoded. This + encoding can be done a number of ways. If the header name-value pair + are already in the header table we can represent them using the indexed + representation: the same is true if they are in the static table. + Otherwise, a literal representation will be used. + """ + log.debug("HPACK encoding %s", headers) + header_block = [] + + # Turn the headers into a list of tuples if possible. This is the + # natural way to interact with them in HPACK. + if isinstance(headers, dict): + headers = headers.items() + + # Next, walk across the headers and turn them all into bytestrings. + headers = [(_to_bytes(n), _to_bytes(v)) for n, v in headers] + + # Before we begin, if the header table size has been changed we need + # to signal that appropriately. + if self._table_size_changed: + header_block.append(self._encode_table_size_change()) + self._table_size_changed = False + + # We can now encode each header in the block. + header_block.extend( + (self.add(header, huffman) for header in headers) + ) + + header_block = b''.join(header_block) + + log.debug("Encoded header block to %s", header_block) + + return header_block + + def add(self, to_add, huffman=False): + """ + This function takes a header key-value tuple and serializes it. + """ + log.debug("Adding %s to the header table", to_add) + + name, value = to_add + + # Search for a matching header in the header table. + match = self.matching_header(name, value) + + if match is None: + # Not in the header table. Encode using the literal syntax, + # and add it to the header table. + encoded = self._encode_literal(name, value, True, huffman) + self._add_to_header_table(to_add) + return encoded + + # The header is in the table, break out the values. If we matched + # perfectly, we can use the indexed representation: otherwise we + # can use the indexed literal. + index, perfect = match + + if perfect: + # Indexed representation. + encoded = self._encode_indexed(index) + else: + # Indexed literal. We are going to add header to the + # header table unconditionally. It is a future todo to + # filter out headers which are known to be ineffective for + # indexing since they just take space in the table and + # pushed out other valuable headers. + encoded = self._encode_indexed_literal(index, value, huffman) + self._add_to_header_table(to_add) + + return encoded + + def matching_header(self, name, value): + """ + Scans the header table and the static table. Returns a tuple, where the + first value is the index of the match, and the second is whether there + was a full match or not. Prefers full matches to partial ones. + + Upsettingly, the header table is one-indexed, not zero-indexed. + """ + partial_match = None + static_table_len = len(Encoder.static_table) + + for (i, (n, v)) in enumerate(Encoder.static_table): + if n == name: + if v == value: + return (i + 1, Encoder.static_table[i]) + elif partial_match is None: + partial_match = (i + 1, None) + + for (i, (n, v)) in enumerate(self.header_table): + if n == name: + if v == value: + return (i + static_table_len + 1, self.header_table[i]) + elif partial_match is None: + partial_match = (i + static_table_len + 1, None) + + return partial_match + + def _add_to_header_table(self, header): + """ + Adds a header to the header table, evicting old ones if necessary. + """ + # Be optimistic: add the header straight away. + self.header_table.appendleft(header) + + # Now, work out how big the header table is. + actual_size = header_table_size(self.header_table) + + # Loop and remove whatever we need to. + while actual_size > self.header_table_size: + header = self.header_table.pop() + n, v = header + actual_size -= ( + 32 + len(n) + len(v) + ) + + log.debug("Evicted %s: %s from the header table", n, v) + + def _encode_indexed(self, index): + """ + Encodes a header using the indexed representation. + """ + field = encode_integer(index, 7) + field[0] = field[0] | 0x80 # we set the top bit + return bytes(field) + + def _encode_literal(self, name, value, indexing, huffman=False): + """ + Encodes a header with a literal name and literal value. If ``indexing`` + is True, the header will be added to the header table: otherwise it + will not. + """ + prefix = b'\x40' if indexing else b'\x00' + + if huffman: + name = self.huffman_coder.encode(name) + value = self.huffman_coder.encode(value) + + name_len = encode_integer(len(name), 7) + value_len = encode_integer(len(value), 7) + + if huffman: + name_len[0] |= 0x80 + value_len[0] |= 0x80 + + return b''.join([prefix, bytes(name_len), name, bytes(value_len), value]) + + def _encode_indexed_literal(self, index, value, huffman=False): + """ + Encodes a header with an indexed name and a literal value and performs + incremental indexing. + """ + prefix = encode_integer(index, 6) + prefix[0] |= 0x40 + + if huffman: + value = self.huffman_coder.encode(value) + + value_len = encode_integer(len(value), 7) + + if huffman: + value_len[0] |= 0x80 + + return b''.join([bytes(prefix), bytes(value_len), value]) + + def _encode_table_size_change(self): + """ + Produces the encoded form of a header table size change context update. + """ + size_bytes = encode_integer(self.header_table_size, 5) + size_bytes[0] |= 0x20 + return bytes(size_bytes) + + +class Decoder(object): + """ + An HPACK decoder object. + """ + static_table = [ + (b':authority', b''), + (b':method', b'GET'), + (b':method', b'POST'), + (b':path', b'/'), + (b':path', b'/index.html'), + (b':scheme', b'http'), + (b':scheme', b'https'), + (b':status', b'200'), + (b':status', b'204'), + (b':status', b'206'), + (b':status', b'304'), + (b':status', b'400'), + (b':status', b'404'), + (b':status', b'500'), + (b'accept-charset', b''), + (b'accept-encoding', b'gzip, deflate'), + (b'accept-language', b''), + (b'accept-ranges', b''), + (b'accept', b''), + (b'access-control-allow-origin', b''), + (b'age', b''), + (b'allow', b''), + (b'authorization', b''), + (b'cache-control', b''), + (b'content-disposition', b''), + (b'content-encoding', b''), + (b'content-language', b''), + (b'content-length', b''), + (b'content-location', b''), + (b'content-range', b''), + (b'content-type', b''), + (b'cookie', b''), + (b'date', b''), + (b'etag', b''), + (b'expect', b''), + (b'expires', b''), + (b'from', b''), + (b'host', b''), + (b'if-match', b''), + (b'if-modified-since', b''), + (b'if-none-match', b''), + (b'if-range', b''), + (b'if-unmodified-since', b''), + (b'last-modified', b''), + (b'link', b''), + (b'location', b''), + (b'max-forwards', b''), + (b'proxy-authenticate', b''), + (b'proxy-authorization', b''), + (b'range', b''), + (b'referer', b''), + (b'refresh', b''), + (b'retry-after', b''), + (b'server', b''), + (b'set-cookie', b''), + (b'strict-transport-security', b''), + (b'transfer-encoding', b''), + (b'user-agent', b''), + (b'vary', b''), + (b'via', b''), + (b'www-authenticate', b''), + ] + + def __init__(self): + self.header_table = collections.deque() + self._header_table_size = 4096 # This value set by the standard. + self.huffman_coder = HuffmanDecoder( + REQUEST_CODES, REQUEST_CODES_LENGTH + ) + + @property + def header_table_size(self): + return self._header_table_size + + @header_table_size.setter + def header_table_size(self, value): + log.debug( + "Resizing decoder header table to %d from %d", + value, + self._header_table_size + ) + + # If the new value is larger than the current one, no worries! + # Otherwise, we may need to shrink the header table. + if value < self._header_table_size: + current_size = header_table_size(self.header_table) + + while value < current_size: + header = self.header_table.pop() + n, v = header + current_size -= ( + 32 + len(n) + len(v) + ) + + log.debug("Evicting %s: %s from the header table", n, v) + + self._header_table_size = value + + def decode(self, data): + """ + Takes an HPACK-encoded header block and decodes it into a header set. + """ + log.debug("Decoding %s", data) + + headers = [] + data_len = len(data) + current_index = 0 + + while current_index < data_len: + # Work out what kind of header we're decoding. + # If the high bit is 1, it's an indexed field. + current = to_byte(data[current_index]) + indexed = bool(current & 0x80) + + # Otherwise, if the second-highest bit is 1 it's a field that does + # alter the header table. + literal_index = bool(current & 0x40) + + # Otherwise, if the third-highest bit is 1 it's an encoding context + # update. + encoding_update = bool(current & 0x20) + + if indexed: + header, consumed = self._decode_indexed(data[current_index:]) + elif literal_index: + # It's a literal header that does affect the header table. + header, consumed = self._decode_literal_index( + data[current_index:] + ) + elif encoding_update: + # It's an update to the encoding context. + consumed = self._update_encoding_context(data) + header = None + else: + # It's a literal header that does not affect the header table. + header, consumed = self._decode_literal_no_index( + data[current_index:] + ) + + if header: + headers.append(header) + + current_index += consumed + + return [(n.decode('utf-8'), v.decode('utf-8')) for n, v in headers] + + def _add_to_header_table(self, new_header): + """ + Adds a header to the header table, evicting old ones if necessary. + """ + # Be optimistic: add the header straight away. + self.header_table.appendleft(new_header) + + # Now, work out how big the header table is. + actual_size = header_table_size(self.header_table) + + # Loop and remove whatever we need to. + while actual_size > self.header_table_size: + header = self.header_table.pop() + n, v = header + actual_size -= ( + 32 + len(n) + len(v) + ) + + log.debug("Evicting %s: %s from the header table", n, v) + + def _update_encoding_context(self, data): + """ + Handles a byte that updates the encoding context. + """ + # We've been asked to resize the header table. + new_size, consumed = decode_integer(data, 5) + self.header_table_size = new_size + return consumed + + def _decode_indexed(self, data): + """ + Decodes a header represented using the indexed representation. + """ + index, consumed = decode_integer(data, 7) + index -= 1 # Because this idiot table is 1-indexed. Ugh. + + if index >= len(Decoder.static_table): + index -= len(Decoder.static_table) + header = self.header_table[index] + else: + header = Decoder.static_table[index] + + log.debug("Decoded %s, consumed %d", header, consumed) + return header, consumed + + def _decode_literal_no_index(self, data): + return self._decode_literal(data, False) + + def _decode_literal_index(self, data): + return self._decode_literal(data, True) + + def _decode_literal(self, data, should_index): + """ + Decodes a header represented with a literal. + """ + total_consumed = 0 + + # When should_index is true, if the low six bits of the first byte are + # nonzero, the header name is indexed. + # When should_index is false, if the low four bits of the first byte + # are nonzero the header name is indexed. + if should_index: + indexed_name = to_byte(data[0]) & 0x3F + name_len = 6 + else: + indexed_name = to_byte(data[0]) & 0x0F + name_len = 4 + + if indexed_name: + # Indexed header name. + index, consumed = decode_integer(data, name_len) + index -= 1 + + if index >= len(Decoder.static_table): + index -= len(Decoder.static_table) + name = self.header_table[index][0] + else: + name = Decoder.static_table[index][0] + + total_consumed = consumed + length = 0 + else: + # Literal header name. The first byte was consumed, so we need to + # move forward. + data = data[1:] + + length, consumed = decode_integer(data, 7) + name = data[consumed:consumed + length] + + if to_byte(data[0]) & 0x80: + name = self.huffman_coder.decode(name) + total_consumed = consumed + length + 1 # Since we moved forward 1. + + data = data[consumed + length:] + + # The header value is definitely length-based. + length, consumed = decode_integer(data, 7) + value = data[consumed:consumed + length] + + if to_byte(data[0]) & 0x80: + value = self.huffman_coder.decode(value) + + # Updated the total consumed length. + total_consumed += length + consumed + + # If we've been asked to index this, add it to the header table. + header = (name, value) + if should_index: + self._add_to_header_table(header) + + log.debug( + "Decoded %s, total consumed %d bytes, indexed %s", + header, + total_consumed, + should_index + ) + + return header, total_consumed diff --git a/vinetrimmer/vendor/hyper/packages/hpack/hpack_compat.py b/vinetrimmer/vendor/hyper/packages/hpack/hpack_compat.py new file mode 100644 index 0000000..87918d7 --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/hpack/hpack_compat.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +""" +hpack/hpack_compat +~~~~~~~~~~~~~~~~~~ + +Provides an abstraction layer over two HPACK implementations. + +This module has a pure-Python greenfield HPACK implementation that can be used +on all Python platforms. However, this implementation is both slower and more +memory-hungry than could be achieved with a C-language version. Additionally, +nghttp2's HPACK implementation currently achieves better compression ratios +than hyper's in almost all benchmarks. + +For those who care about efficiency and speed in HPACK, this module allows you +to use nghttp2's HPACK implementation instead of ours. This module detects +whether the nghttp2 bindings are installed, and if they are it wraps them in +a hpack-compatible API and uses them instead of its own. If not, it falls back +to the built-in Python bindings. +""" +import logging +from .hpack import _to_bytes + +log = logging.getLogger(__name__) + +# Attempt to import nghttp2. +try: + import nghttp2 + USE_NGHTTP2 = True + log.debug("Using nghttp2's HPACK implementation.") +except ImportError: + USE_NGHTTP2 = False + log.debug("Using our pure-Python HPACK implementation.") + +if USE_NGHTTP2: + class Encoder(object): + """ + An HPACK encoder object. This object takes HTTP headers and emits + encoded HTTP/2 header blocks. + """ + def __init__(self): + self._e = nghttp2.HDDeflater() + + @property + def header_table_size(self): + """ + Returns the header table size. For the moment this isn't + useful, so we don't use it. + """ + raise NotImplementedError() + + @header_table_size.setter + def header_table_size(self, value): + log.debug("Setting header table size to %d", value) + self._e.change_table_size(value) + + def encode(self, headers, huffman=True): + """ + Encode the headers. The huffman parameter has no effect, it is + simply present for compatibility. + """ + log.debug("HPACK encoding %s", headers) + + # Turn the headers into a list of tuples if possible. This is the + # natural way to interact with them in HPACK. + if isinstance(headers, dict): + headers = headers.items() + + # Next, walk across the headers and turn them all into bytestrings. + headers = [(_to_bytes(n), _to_bytes(v)) for n, v in headers] + + # Now, let nghttp2 do its thing. + header_block = self._e.deflate(headers) + + return header_block + + class Decoder(object): + """ + An HPACK decoder object. + """ + def __init__(self): + self._d = nghttp2.HDInflater() + + @property + def header_table_size(self): + """ + Returns the header table size. For the moment this isn't + useful, so we don't use it. + """ + raise NotImplementedError() + + @header_table_size.setter + def header_table_size(self, value): + log.debug("Setting header table size to %d", value) + self._d.change_table_size(value) + + def decode(self, data): + """ + Takes an HPACK-encoded header block and decodes it into a header + set. + """ + log.debug("Decoding %s", data) + + headers = self._d.inflate(data) + return [(n.decode('utf-8'), v.decode('utf-8')) for n, v in headers] +else: + # Grab the built-in encoder and decoder. + from .hpack import Encoder, Decoder diff --git a/vinetrimmer/vendor/hyper/packages/hpack/huffman.py b/vinetrimmer/vendor/hyper/packages/hpack/huffman.py new file mode 100644 index 0000000..c8aa276 --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/hpack/huffman.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +""" +hpack/huffman_decoder +~~~~~~~~~~~~~~~~~~~~~ + +An implementation of a bitwise prefix tree specially built for decoding +Huffman-coded content where we already know the Huffman table. +""" +from .compat import to_byte, decode_hex +from .exceptions import HPACKDecodingError + +def _pad_binary(bin_str, req_len=8): + """ + Given a binary string (returned by bin()), pad it to a full byte length. + """ + bin_str = bin_str[2:] # Strip the 0b prefix + return max(0, req_len - len(bin_str)) * '0' + bin_str + +def _hex_to_bin_str(hex_string): + """ + Given a Python bytestring, returns a string representing those bytes in + unicode form. + """ + unpadded_bin_string_list = (bin(to_byte(c)) for c in hex_string) + padded_bin_string_list = map(_pad_binary, unpadded_bin_string_list) + bitwise_message = "".join(padded_bin_string_list) + return bitwise_message + + +class HuffmanDecoder(object): + """ + Decodes a Huffman-coded bytestream according to the Huffman table laid out + in the HPACK specification. + """ + class _Node(object): + def __init__(self, data): + self.data = data + self.mapping = {} + + def __init__(self, huffman_code_list, huffman_code_list_lengths): + self.root = self._Node(None) + for index, (huffman_code, code_length) in enumerate(zip(huffman_code_list, huffman_code_list_lengths)): + self._insert(huffman_code, code_length, index) + + def _insert(self, hex_number, hex_length, letter): + """ + Inserts a Huffman code point into the tree. + """ + hex_number = _pad_binary(bin(hex_number), hex_length) + cur_node = self.root + for digit in hex_number: + if digit not in cur_node.mapping: + cur_node.mapping[digit] = self._Node(None) + cur_node = cur_node.mapping[digit] + cur_node.data = letter + + def decode(self, encoded_string): + """ + Decode the given Huffman coded string. + """ + number = _hex_to_bin_str(encoded_string) + cur_node = self.root + decoded_message = bytearray() + + try: + for digit in number: + cur_node = cur_node.mapping[digit] + if cur_node.data is not None: + # If we get EOS, everything else is padding. + if cur_node.data == 256: + break + + decoded_message.append(cur_node.data) + cur_node = self.root + except KeyError: + # We have a Huffman-coded string that doesn't match our trie. This + # is pretty bad: raise a useful exception. + raise HPACKDecodingError("Invalid Huffman-coded string received.") + return bytes(decoded_message) + + +class HuffmanEncoder(object): + """ + Encodes a string according to the Huffman encoding table defined in the + HPACK specification. + """ + def __init__(self, huffman_code_list, huffman_code_list_lengths): + self.huffman_code_list = huffman_code_list + self.huffman_code_list_lengths = huffman_code_list_lengths + + def encode(self, bytes_to_encode): + """ + Given a string of bytes, encodes them according to the HPACK Huffman + specification. + """ + # If handed the empty string, just immediately return. + if not bytes_to_encode: + return b'' + + final_num = 0 + final_int_len = 0 + + # Turn each byte into its huffman code. These codes aren't necessarily + # octet aligned, so keep track of how far through an octet we are. To + # handle this cleanly, just use a single giant integer. + for char in bytes_to_encode: + byte = to_byte(char) + bin_int_len = self.huffman_code_list_lengths[byte] + bin_int = self.huffman_code_list[byte] & (2 ** (bin_int_len + 1) - 1) + final_num <<= bin_int_len + final_num |= bin_int + final_int_len += bin_int_len + + # Pad out to an octet with ones. + bits_to_be_padded = (8 - (final_int_len % 8)) % 8 + final_num <<= bits_to_be_padded + final_num |= (1 << (bits_to_be_padded)) - 1 + + # Convert the number to hex and strip off the leading '0x' and the + # trailing 'L', if present. + final_num = hex(final_num)[2:].rstrip('L') + + # If this is odd, prepend a zero. + final_num = '0' + final_num if len(final_num) % 2 != 0 else final_num + + # This number should have twice as many digits as bytes. If not, we're + # missing some leading zeroes. Work out how many bytes we want and how + # many digits we have, then add the missing zero digits to the front. + total_bytes = (final_int_len + bits_to_be_padded) // 8 + expected_digits = total_bytes * 2 + + if len(final_num) != expected_digits: + missing_digits = expected_digits - len(final_num) + final_num = ('0' * missing_digits) + final_num + + return decode_hex(final_num) diff --git a/vinetrimmer/vendor/hyper/packages/hpack/huffman_constants.py b/vinetrimmer/vendor/hyper/packages/hpack/huffman_constants.py new file mode 100644 index 0000000..c2b3bb2 --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/hpack/huffman_constants.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- +""" +hpack/huffman_constants +~~~~~~~~~~~~~~~~~~~~~~~ + +Defines the constant Huffman table. This takes up an upsetting amount of space, +but c'est la vie. +""" + +REQUEST_CODES = [ + 0x1ff8, + 0x7fffd8, + 0xfffffe2, + 0xfffffe3, + 0xfffffe4, + 0xfffffe5, + 0xfffffe6, + 0xfffffe7, + 0xfffffe8, + 0xffffea, + 0x3ffffffc, + 0xfffffe9, + 0xfffffea, + 0x3ffffffd, + 0xfffffeb, + 0xfffffec, + 0xfffffed, + 0xfffffee, + 0xfffffef, + 0xffffff0, + 0xffffff1, + 0xffffff2, + 0x3ffffffe, + 0xffffff3, + 0xffffff4, + 0xffffff5, + 0xffffff6, + 0xffffff7, + 0xffffff8, + 0xffffff9, + 0xffffffa, + 0xffffffb, + 0x14, + 0x3f8, + 0x3f9, + 0xffa, + 0x1ff9, + 0x15, + 0xf8, + 0x7fa, + 0x3fa, + 0x3fb, + 0xf9, + 0x7fb, + 0xfa, + 0x16, + 0x17, + 0x18, + 0x0, + 0x1, + 0x2, + 0x19, + 0x1a, + 0x1b, + 0x1c, + 0x1d, + 0x1e, + 0x1f, + 0x5c, + 0xfb, + 0x7ffc, + 0x20, + 0xffb, + 0x3fc, + 0x1ffa, + 0x21, + 0x5d, + 0x5e, + 0x5f, + 0x60, + 0x61, + 0x62, + 0x63, + 0x64, + 0x65, + 0x66, + 0x67, + 0x68, + 0x69, + 0x6a, + 0x6b, + 0x6c, + 0x6d, + 0x6e, + 0x6f, + 0x70, + 0x71, + 0x72, + 0xfc, + 0x73, + 0xfd, + 0x1ffb, + 0x7fff0, + 0x1ffc, + 0x3ffc, + 0x22, + 0x7ffd, + 0x3, + 0x23, + 0x4, + 0x24, + 0x5, + 0x25, + 0x26, + 0x27, + 0x6, + 0x74, + 0x75, + 0x28, + 0x29, + 0x2a, + 0x7, + 0x2b, + 0x76, + 0x2c, + 0x8, + 0x9, + 0x2d, + 0x77, + 0x78, + 0x79, + 0x7a, + 0x7b, + 0x7ffe, + 0x7fc, + 0x3ffd, + 0x1ffd, + 0xffffffc, + 0xfffe6, + 0x3fffd2, + 0xfffe7, + 0xfffe8, + 0x3fffd3, + 0x3fffd4, + 0x3fffd5, + 0x7fffd9, + 0x3fffd6, + 0x7fffda, + 0x7fffdb, + 0x7fffdc, + 0x7fffdd, + 0x7fffde, + 0xffffeb, + 0x7fffdf, + 0xffffec, + 0xffffed, + 0x3fffd7, + 0x7fffe0, + 0xffffee, + 0x7fffe1, + 0x7fffe2, + 0x7fffe3, + 0x7fffe4, + 0x1fffdc, + 0x3fffd8, + 0x7fffe5, + 0x3fffd9, + 0x7fffe6, + 0x7fffe7, + 0xffffef, + 0x3fffda, + 0x1fffdd, + 0xfffe9, + 0x3fffdb, + 0x3fffdc, + 0x7fffe8, + 0x7fffe9, + 0x1fffde, + 0x7fffea, + 0x3fffdd, + 0x3fffde, + 0xfffff0, + 0x1fffdf, + 0x3fffdf, + 0x7fffeb, + 0x7fffec, + 0x1fffe0, + 0x1fffe1, + 0x3fffe0, + 0x1fffe2, + 0x7fffed, + 0x3fffe1, + 0x7fffee, + 0x7fffef, + 0xfffea, + 0x3fffe2, + 0x3fffe3, + 0x3fffe4, + 0x7ffff0, + 0x3fffe5, + 0x3fffe6, + 0x7ffff1, + 0x3ffffe0, + 0x3ffffe1, + 0xfffeb, + 0x7fff1, + 0x3fffe7, + 0x7ffff2, + 0x3fffe8, + 0x1ffffec, + 0x3ffffe2, + 0x3ffffe3, + 0x3ffffe4, + 0x7ffffde, + 0x7ffffdf, + 0x3ffffe5, + 0xfffff1, + 0x1ffffed, + 0x7fff2, + 0x1fffe3, + 0x3ffffe6, + 0x7ffffe0, + 0x7ffffe1, + 0x3ffffe7, + 0x7ffffe2, + 0xfffff2, + 0x1fffe4, + 0x1fffe5, + 0x3ffffe8, + 0x3ffffe9, + 0xffffffd, + 0x7ffffe3, + 0x7ffffe4, + 0x7ffffe5, + 0xfffec, + 0xfffff3, + 0xfffed, + 0x1fffe6, + 0x3fffe9, + 0x1fffe7, + 0x1fffe8, + 0x7ffff3, + 0x3fffea, + 0x3fffeb, + 0x1ffffee, + 0x1ffffef, + 0xfffff4, + 0xfffff5, + 0x3ffffea, + 0x7ffff4, + 0x3ffffeb, + 0x7ffffe6, + 0x3ffffec, + 0x3ffffed, + 0x7ffffe7, + 0x7ffffe8, + 0x7ffffe9, + 0x7ffffea, + 0x7ffffeb, + 0xffffffe, + 0x7ffffec, + 0x7ffffed, + 0x7ffffee, + 0x7ffffef, + 0x7fffff0, + 0x3ffffee, + 0x3fffffff, +] + +REQUEST_CODES_LENGTH = [ + 13, 23, 28, 28, 28, 28, 28, 28, 28, 24, 30, 28, 28, 30, 28, 28, + 28, 28, 28, 28, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 6, 10, 10, 12, 13, 6, 8, 11, 10, 10, 8, 11, 8, 6, 6, 6, + 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 15, 6, 12, 10, + 13, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 8, 13, 19, 13, 14, 6, + 15, 5, 6, 5, 6, 5, 6, 6, 6, 5, 7, 7, 6, 6, 6, 5, + 6, 7, 6, 5, 5, 6, 7, 7, 7, 7, 7, 15, 11, 14, 13, 28, + 20, 22, 20, 20, 22, 22, 22, 23, 22, 23, 23, 23, 23, 23, 24, 23, + 24, 24, 22, 23, 24, 23, 23, 23, 23, 21, 22, 23, 22, 23, 23, 24, + 22, 21, 20, 22, 22, 23, 23, 21, 23, 22, 22, 24, 21, 22, 23, 23, + 21, 21, 22, 21, 23, 22, 23, 23, 20, 22, 22, 22, 23, 22, 22, 23, + 26, 26, 20, 19, 22, 23, 22, 25, 26, 26, 26, 27, 27, 26, 24, 25, + 19, 21, 26, 27, 27, 26, 27, 24, 21, 21, 26, 26, 28, 27, 27, 27, + 20, 24, 20, 21, 22, 21, 21, 23, 22, 22, 25, 25, 24, 24, 26, 23, + 26, 27, 26, 26, 27, 27, 27, 27, 27, 28, 27, 27, 27, 27, 27, 26, + 30, +] diff --git a/vinetrimmer/vendor/hyper/packages/hyperframe/__init__.py b/vinetrimmer/vendor/hyper/packages/hyperframe/__init__.py new file mode 100644 index 0000000..9b6a84e --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/hyperframe/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +""" +hyperframe +~~~~~~~~~~ + +A module for providing a pure-Python HTTP/2 framing layer. +""" +__version__ = '2.1.0' diff --git a/vinetrimmer/vendor/hyper/packages/hyperframe/flags.py b/vinetrimmer/vendor/hyper/packages/hyperframe/flags.py new file mode 100644 index 0000000..e8f6300 --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/hyperframe/flags.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +""" +hyperframe/flags +~~~~~~~~~~~~~~~~ + +Defines basic Flag and Flags data structures. +""" +import collections + + +Flag = collections.namedtuple("Flag", ["name", "bit"]) + + +class Flags(collections.MutableSet): + """ + A simple MutableSet implementation that will only accept known flags as elements. + + Will behave like a regular set(), except that a ValueError will be thrown when .add()ing + unexpected flags. + """ + def __init__(self, defined_flags): + self._valid_flags = set(flag.name for flag in defined_flags) + self._flags = set() + + def __contains__(self, x): + return self._flags.__contains__(x) + + def __iter__(self): + return self._flags.__iter__() + + def __len__(self): + return self._flags.__len__() + + def discard(self, value): + return self._flags.discard(value) + + def add(self, value): + if value not in self._valid_flags: + raise ValueError("Unexpected flag: {}".format(value)) + return self._flags.add(value) diff --git a/vinetrimmer/vendor/hyper/packages/hyperframe/frame.py b/vinetrimmer/vendor/hyper/packages/hyperframe/frame.py new file mode 100644 index 0000000..22e2003 --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/hyperframe/frame.py @@ -0,0 +1,625 @@ +# -*- coding: utf-8 -*- +""" +hyperframe/frame +~~~~~~~~~~~~~~~~ + +Defines framing logic for HTTP/2. Provides both classes to represent framed +data and logic for aiding the connection when it comes to reading from the +socket. +""" +import collections +import struct + +from .flags import Flag, Flags + +# The maximum initial length of a frame. Some frames have shorter maximum lengths. +FRAME_MAX_LEN = (2 ** 14) + +# The maximum allowed length of a frame. +FRAME_MAX_ALLOWED_LEN = (2 ** 24) - 1 + + +class Frame(object): + """ + The base class for all HTTP/2 frames. + """ + # The flags defined on this type of frame. + defined_flags = [] + + # The type of the frame. + type = None + + # If 'has-stream', the frame's stream_id must be non-zero. If 'no-stream', + # it must be zero. If 'either', it's not checked. + stream_association = None + + def __init__(self, stream_id, flags=()): + self.stream_id = stream_id + self.flags = Flags(self.defined_flags) + self.body_len = 0 + + for flag in flags: + self.flags.add(flag) + + if self.stream_association == 'has-stream' and not self.stream_id: + raise ValueError('Stream ID must be non-zero') + if self.stream_association == 'no-stream' and self.stream_id: + raise ValueError('Stream ID must be zero') + + def __repr__(self): + flags = ", ".join(self.flags) or "None" + body = self.serialize_body() + if len(body) > 100: + body = str(body[:100]) + "..." + return ( + "{type}(Stream: {stream}; Flags: {flags}): {body}" + ).format(type=type(self).__name__, stream=self.stream_id, flags=flags, body=body) + + @staticmethod + def parse_frame_header(header): + """ + Takes a 9-byte frame header and returns a tuple of the appropriate + Frame object and the length that needs to be read from the socket. + """ + fields = struct.unpack("!HBBBL", header) + # First 24 bits are frame length. + length = (fields[0] << 8) + fields[1] + type = fields[2] + flags = fields[3] + stream_id = fields[4] + + if type not in FRAMES: + raise ValueError("Unknown frame type %d" % type) + + frame = FRAMES[type](stream_id) + frame.parse_flags(flags) + return (frame, length) + + def parse_flags(self, flag_byte): + for flag, flag_bit in self.defined_flags: + if flag_byte & flag_bit: + self.flags.add(flag) + + return self.flags + + def serialize(self): + body = self.serialize_body() + self.body_len = len(body) + + # Build the common frame header. + # First, get the flags. + flags = 0 + + for flag, flag_bit in self.defined_flags: + if flag in self.flags: + flags |= flag_bit + + header = struct.pack( + "!HBBBL", + (self.body_len & 0xFFFF00) >> 8, # Length is spread over top 24 bits + self.body_len & 0x0000FF, + self.type, + flags, + self.stream_id & 0x7FFFFFFF # Stream ID is 32 bits. + ) + + return header + body + + def serialize_body(self): + raise NotImplementedError() + + def parse_body(self, data): + raise NotImplementedError() + + +class Padding(object): + """ + Mixin for frames that contain padding. + """ + def __init__(self, stream_id, pad_length=0, **kwargs): + super(Padding, self).__init__(stream_id, **kwargs) + + self.pad_length = pad_length + + + def serialize_padding_data(self): + if 'PADDED' in self.flags: + return struct.pack('!B', self.pad_length) + return b'' + + def parse_padding_data(self, data): + if 'PADDED' in self.flags: + self.pad_length = struct.unpack('!B', data[:1])[0] + return 1 + return 0 + + @property + def total_padding(self): + """Return the total length of the padding, if any.""" + return self.pad_length + + +class Priority(object): + """ + Mixin for frames that contain priority data. + """ + def __init__(self, stream_id, depends_on=None, stream_weight=None, exclusive=None, **kwargs): + super(Priority, self).__init__(stream_id, **kwargs) + + # The stream ID of the stream on which this stream depends. + self.depends_on = depends_on + + # The weight of the stream. This is an integer between 0 and 256. + self.stream_weight = stream_weight + + # Whether the exclusive bit was set. + self.exclusive = exclusive + + def serialize_priority_data(self): + return struct.pack( + "!LB", + self.depends_on | (int(self.exclusive) << 31), + self.stream_weight + ) + + def parse_priority_data(self, data): + MASK = 0x80000000 + self.depends_on, self.stream_weight = struct.unpack( + "!LB", data[:5] + ) + self.exclusive = bool(self.depends_on & MASK) + self.depends_on &= ~MASK + return 5 + + +class DataFrame(Padding, Frame): + """ + DATA frames convey arbitrary, variable-length sequences of octets + associated with a stream. One or more DATA frames are used, for instance, + to carry HTTP request or response payloads. + """ + defined_flags = [ + Flag('END_STREAM', 0x01), + Flag('PADDED', 0x08), + ] + + type = 0x0 + + stream_association = 'has-stream' + + def __init__(self, stream_id, data=b'', **kwargs): + super(DataFrame, self).__init__(stream_id, **kwargs) + + self.data = data + + def serialize_body(self): + padding_data = self.serialize_padding_data() + padding = b'\0' * self.total_padding + return b''.join([padding_data, self.data, padding]) + + def parse_body(self, data): + padding_data_length = self.parse_padding_data(data) + self.data = data[padding_data_length:len(data)-self.total_padding].tobytes() + self.body_len = len(data) + + @property + def flow_controlled_length(self): + """ + If the frame is padded we need to include the padding length byte in + the flow control used. + """ + padding_len = self.total_padding + 1 if self.total_padding else 0 + return len(self.data) + padding_len + + +class PriorityFrame(Priority, Frame): + """ + The PRIORITY frame specifies the sender-advised priority of a stream. It + can be sent at any time for an existing stream. This enables + reprioritisation of existing streams. + """ + defined_flags = [] + + type = 0x02 + + stream_association = 'has-stream' + + def serialize_body(self): + return self.serialize_priority_data() + + def parse_body(self, data): + self.parse_priority_data(data) + self.body_len = len(data) + + +class RstStreamFrame(Frame): + """ + The RST_STREAM frame allows for abnormal termination of a stream. When sent + by the initiator of a stream, it indicates that they wish to cancel the + stream or that an error condition has occurred. When sent by the receiver + of a stream, it indicates that either the receiver is rejecting the stream, + requesting that the stream be cancelled or that an error condition has + occurred. + """ + defined_flags = [] + + type = 0x03 + + stream_association = 'has-stream' + + def __init__(self, stream_id, error_code=0, **kwargs): + super(RstStreamFrame, self).__init__(stream_id, **kwargs) + + self.error_code = error_code + + def serialize_body(self): + return struct.pack("!L", self.error_code) + + def parse_body(self, data): + if len(data) != 4: + raise ValueError() + + self.error_code = struct.unpack("!L", data)[0] + self.body_len = len(data) + + +class SettingsFrame(Frame): + """ + The SETTINGS frame conveys configuration parameters that affect how + endpoints communicate. The parameters are either constraints on peer + behavior or preferences. + + Settings are not negotiated. Settings describe characteristics of the + sending peer, which are used by the receiving peer. Different values for + the same setting can be advertised by each peer. For example, a client + might set a high initial flow control window, whereas a server might set a + lower value to conserve resources. + """ + defined_flags = [Flag('ACK', 0x01)] + + type = 0x04 + + stream_association = 'no-stream' + + # We need to define the known settings, they may as well be class + # attributes. + HEADER_TABLE_SIZE = 0x01 + ENABLE_PUSH = 0x02 + MAX_CONCURRENT_STREAMS = 0x03 + INITIAL_WINDOW_SIZE = 0x04 + SETTINGS_MAX_FRAME_SIZE = 0x05 + SETTINGS_MAX_HEADER_LIST_SIZE = 0x06 + + def __init__(self, stream_id=0, settings=None, **kwargs): + super(SettingsFrame, self).__init__(stream_id, **kwargs) + + if settings and "ACK" in kwargs.get("flags", ()): + raise ValueError("Settings must be empty if ACK flag is set.") + + # A dictionary of the setting type byte to the value. + self.settings = settings or {} + + def serialize_body(self): + settings = [struct.pack("!HL", setting & 0xFF, value) + for setting, value in self.settings.items()] + return b''.join(settings) + + def parse_body(self, data): + for i in range(0, len(data), 6): + name, value = struct.unpack("!HL", data[i:i+6]) + self.settings[name] = value + + self.body_len = len(data) + + +class PushPromiseFrame(Padding, Frame): + """ + The PUSH_PROMISE frame is used to notify the peer endpoint in advance of + streams the sender intends to initiate. + """ + defined_flags = [ + Flag('END_HEADERS', 0x04), + Flag('PADDED', 0x08) + ] + + type = 0x05 + + stream_association = 'has-stream' + + def __init__(self, stream_id, promised_stream_id=0, data=b'', **kwargs): + super(PushPromiseFrame, self).__init__(stream_id, **kwargs) + + self.promised_stream_id = promised_stream_id + self.data = data + + def serialize_body(self): + padding_data = self.serialize_padding_data() + padding = b'\0' * self.total_padding + data = struct.pack("!L", self.promised_stream_id) + return b''.join([padding_data, data, self.data, padding]) + + def parse_body(self, data): + padding_data_length = self.parse_padding_data(data) + self.promised_stream_id = struct.unpack("!L", data[padding_data_length:padding_data_length + 4])[0] + self.data = data[padding_data_length + 4:].tobytes() + self.body_len = len(data) + + +class PingFrame(Frame): + """ + The PING frame is a mechanism for measuring a minimal round-trip time from + the sender, as well as determining whether an idle connection is still + functional. PING frames can be sent from any endpoint. + """ + defined_flags = [Flag('ACK', 0x01)] + + type = 0x06 + + stream_association = 'no-stream' + + def __init__(self, stream_id=0, opaque_data=b'', **kwargs): + super(PingFrame, self).__init__(stream_id, **kwargs) + + self.opaque_data = opaque_data + + def serialize_body(self): + if len(self.opaque_data) > 8: + raise ValueError() + + data = self.opaque_data + data += b'\x00' * (8 - len(self.opaque_data)) + return data + + def parse_body(self, data): + if len(data) > 8: + raise ValueError() + + self.opaque_data = data.tobytes() + self.body_len = len(data) + + +class GoAwayFrame(Frame): + """ + The GOAWAY frame informs the remote peer to stop creating streams on this + connection. It can be sent from the client or the server. Once sent, the + sender will ignore frames sent on new streams for the remainder of the + connection. + """ + type = 0x07 + + stream_association = 'no-stream' + + def __init__(self, stream_id=0, last_stream_id=0, error_code=0, additional_data=b'', **kwargs): + super(GoAwayFrame, self).__init__(stream_id, **kwargs) + + self.last_stream_id = last_stream_id + self.error_code = error_code + self.additional_data = additional_data + + def serialize_body(self): + data = struct.pack( + "!LL", + self.last_stream_id & 0x7FFFFFFF, + self.error_code + ) + data += self.additional_data + + return data + + def parse_body(self, data): + self.last_stream_id, self.error_code = struct.unpack("!LL", data[:8]) + self.body_len = len(data) + + if len(data) > 8: + self.additional_data = data[8:].tobytes() + + +class WindowUpdateFrame(Frame): + """ + The WINDOW_UPDATE frame is used to implement flow control. + + Flow control operates at two levels: on each individual stream and on the + entire connection. + + Both types of flow control are hop by hop; that is, only between the two + endpoints. Intermediaries do not forward WINDOW_UPDATE frames between + dependent connections. However, throttling of data transfer by any receiver + can indirectly cause the propagation of flow control information toward the + original sender. + """ + type = 0x08 + + stream_association = 'either' + + def __init__(self, stream_id, window_increment=0, **kwargs): + super(WindowUpdateFrame, self).__init__(stream_id, **kwargs) + + self.window_increment = window_increment + + def serialize_body(self): + return struct.pack("!L", self.window_increment & 0x7FFFFFFF) + + def parse_body(self, data): + self.window_increment = struct.unpack("!L", data)[0] + self.body_len = len(data) + + +class HeadersFrame(Padding, Priority, Frame): + """ + The HEADERS frame carries name-value pairs. It is used to open a stream. + HEADERS frames can be sent on a stream in the "open" or "half closed + (remote)" states. + + The HeadersFrame class is actually basically a data frame in this + implementation, because of the requirement to control the sizes of frames. + A header block fragment that doesn't fit in an entire HEADERS frame needs + to be followed with CONTINUATION frames. From the perspective of the frame + building code the header block is an opaque data segment. + """ + type = 0x01 + + stream_association = 'has-stream' + + defined_flags = [ + Flag('END_STREAM', 0x01), + Flag('END_HEADERS', 0x04), + Flag('PADDED', 0x08), + Flag('PRIORITY', 0x20), + ] + + def __init__(self, stream_id, data=b'', **kwargs): + super(HeadersFrame, self).__init__(stream_id, **kwargs) + + self.data = data + + def serialize_body(self): + padding_data = self.serialize_padding_data() + padding = b'\0' * self.total_padding + + if 'PRIORITY' in self.flags: + priority_data = self.serialize_priority_data() + else: + priority_data = b'' + + return b''.join([padding_data, priority_data, self.data, padding]) + + def parse_body(self, data): + padding_data_length = self.parse_padding_data(data) + data = data[padding_data_length:] + + if 'PRIORITY' in self.flags: + priority_data_length = self.parse_priority_data(data) + else: + priority_data_length = 0 + + self.body_len = len(data) + self.data = data[priority_data_length:len(data)-self.total_padding].tobytes() + + +class ContinuationFrame(Frame): + """ + The CONTINUATION frame is used to continue a sequence of header block + fragments. Any number of CONTINUATION frames can be sent on an existing + stream, as long as the preceding frame on the same stream is one of + HEADERS, PUSH_PROMISE or CONTINUATION without the END_HEADERS flag set. + + Much like the HEADERS frame, hyper treats this as an opaque data frame with + different flags and a different type. + """ + type = 0x09 + + stream_association = 'has-stream' + + defined_flags = [Flag('END_HEADERS', 0x04),] + + def __init__(self, stream_id, data=b'', **kwargs): + super(ContinuationFrame, self).__init__(stream_id, **kwargs) + + self.data = data + + def serialize_body(self): + return self.data + + def parse_body(self, data): + self.data = data.tobytes() + self.body_len = len(data) + + +Origin = collections.namedtuple('Origin', ['scheme', 'host', 'port']) + + +class AltSvcFrame(Frame): + """ + The ALTSVC frame is used to advertise alternate services that the current + host, or a different one, can understand. + """ + type = 0xA + + stream_association = 'no-stream' + + def __init__(self, stream_id=0, host=b'', port=0, protocol_id=b'', max_age=0, origin=None, **kwargs): + super(AltSvcFrame, self).__init__(stream_id, **kwargs) + + self.host = host + self.port = port + self.protocol_id = protocol_id + self.max_age = max_age + self.origin = origin + + def serialize_origin(self): + if self.origin is not None: + if self.origin.port is None: + hostport = self.origin.host + else: + hostport = self.origin.host + b':' + str(self.origin.port).encode('ascii') + return self.origin.scheme + b'://' + hostport + return b'' + + def parse_origin(self, data): + if len(data) > 0: + data = data.tobytes() + scheme, hostport = data.split(b'://') + host, _, port = hostport.partition(b':') + self.origin = Origin(scheme=scheme, host=host, + port=int(port) if len(port) > 0 else None) + + def serialize_body(self): + first = struct.pack("!LHxB", self.max_age, self.port, len(self.protocol_id)) + host_length = struct.pack("!B", len(self.host)) + return b''.join([first, self.protocol_id, host_length, self.host, + self.serialize_origin()]) + + def parse_body(self, data): + self.body_len = len(data) + self.max_age, self.port, protocol_id_length = struct.unpack("!LHxB", data[:8]) + pos = 8 + self.protocol_id = data[pos:pos+protocol_id_length].tobytes() + pos += protocol_id_length + host_length = struct.unpack("!B", data[pos:pos+1])[0] + pos += 1 + self.host = data[pos:pos+host_length].tobytes() + pos += host_length + self.parse_origin(data[pos:]) + + +class BlockedFrame(Frame): + """ + The BLOCKED frame indicates that the sender is unable to send data due to a + closed flow control window. + + The BLOCKED frame is used to provide feedback about the performance of flow + control for the purposes of performance tuning and debugging. The BLOCKED + frame can be sent by a peer when flow controlled data cannot be sent due to + the connection- or stream-level flow control. This frame MUST NOT be sent + if there are other reasons preventing data from being sent, either a lack + of available data, or the underlying transport being blocked. + """ + type = 0x0B + + stream_association = 'both' + + defined_flags = [] + + def serialize_body(self): + return b'' + + def parse_body(self, data): + pass + + +# A map of type byte to frame class. +_FRAME_CLASSES = [ + DataFrame, + HeadersFrame, + PriorityFrame, + RstStreamFrame, + SettingsFrame, + PushPromiseFrame, + PingFrame, + GoAwayFrame, + WindowUpdateFrame, + ContinuationFrame, + AltSvcFrame, + BlockedFrame +] +FRAMES = {cls.type: cls for cls in _FRAME_CLASSES} diff --git a/vinetrimmer/vendor/hyper/packages/rfc3986/LICENSE b/vinetrimmer/vendor/hyper/packages/rfc3986/LICENSE new file mode 100644 index 0000000..72ce24c --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/rfc3986/LICENSE @@ -0,0 +1,13 @@ +Copyright 2014 Ian Cordasco, Rackspace + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/vinetrimmer/vendor/hyper/packages/rfc3986/__init__.py b/vinetrimmer/vendor/hyper/packages/rfc3986/__init__.py new file mode 100644 index 0000000..a3aea4c --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/rfc3986/__init__.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2014 Rackspace +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +rfc3986 +======= + +An implementation of semantics and validations described in RFC 3986. See +http://rfc3986.rtfd.org/ for documentation. + +:copyright: (c) 2014 Rackspace +:license: Apache v2.0, see LICENSE for details +""" + +__title__ = 'rfc3986' +__author__ = 'Ian Cordasco' +__author_email__ = 'ian.cordasco@rackspace.com' +__license__ = 'Apache v2.0' +__copyright__ = 'Copyright 2014 Rackspace' +__version__ = '0.3.0' + +from .api import (URIReference, uri_reference, is_valid_uri, normalize_uri, + urlparse) +from .parseresult import ParseResult + +__all__ = ( + 'ParseResult', + 'URIReference', + 'is_valid_uri', + 'normalize_uri', + 'uri_reference', + 'urlparse', +) diff --git a/vinetrimmer/vendor/hyper/packages/rfc3986/api.py b/vinetrimmer/vendor/hyper/packages/rfc3986/api.py new file mode 100644 index 0000000..3e9e401 --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/rfc3986/api.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2014 Rackspace +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +rfc3986.api +~~~~~~~~~~~ + +This defines the simple API to rfc3986. This module defines 3 functions and +provides access to the class ``URIReference``. +""" + +from .uri import URIReference +from .parseresult import ParseResult + + +def uri_reference(uri, encoding='utf-8'): + """Parse a URI string into a URIReference. + + This is a convenience function. You could achieve the same end by using + ``URIReference.from_string(uri)``. + + :param str uri: The URI which needs to be parsed into a reference. + :param str encoding: The encoding of the string provided + :returns: A parsed URI + :rtype: :class:`URIReference` + """ + return URIReference.from_string(uri, encoding) + + +def is_valid_uri(uri, encoding='utf-8', **kwargs): + """Determine if the URI given is valid. + + This is a convenience function. You could use either + ``uri_reference(uri).is_valid()`` or + ``URIReference.from_string(uri).is_valid()`` to achieve the same result. + + :param str uri: The URI to be validated. + :param str encoding: The encoding of the string provided + :param bool require_scheme: Set to ``True`` if you wish to require the + presence of the scheme component. + :param bool require_authority: Set to ``True`` if you wish to require the + presence of the authority component. + :param bool require_path: Set to ``True`` if you wish to require the + presence of the path component. + :param bool require_query: Set to ``True`` if you wish to require the + presence of the query component. + :param bool require_fragment: Set to ``True`` if you wish to require the + presence of the fragment component. + :returns: ``True`` if the URI is valid, ``False`` otherwise. + :rtype: bool + """ + return URIReference.from_string(uri, encoding).is_valid(**kwargs) + + +def normalize_uri(uri, encoding='utf-8'): + """Normalize the given URI. + + This is a convenience function. You could use either + ``uri_reference(uri).normalize().unsplit()`` or + ``URIReference.from_string(uri).normalize().unsplit()`` instead. + + :param str uri: The URI to be normalized. + :param str encoding: The encoding of the string provided + :returns: The normalized URI. + :rtype: str + """ + normalized_reference = URIReference.from_string(uri, encoding).normalize() + return normalized_reference.unsplit() + + +def urlparse(uri, encoding='utf-8'): + """Parse a given URI and return a ParseResult. + + This is a partial replacement of the standard library's urlparse function. + + :param str uri: The URI to be parsed. + :param str encoding: The encoding of the string provided. + :returns: A parsed URI + :rtype: :class:`~rfc3986.parseresult.ParseResult` + """ + return ParseResult.from_string(uri, encoding, strict=False) diff --git a/vinetrimmer/vendor/hyper/packages/rfc3986/compat.py b/vinetrimmer/vendor/hyper/packages/rfc3986/compat.py new file mode 100644 index 0000000..6fc7f6d --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/rfc3986/compat.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2014 Rackspace +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys + + +if sys.version_info >= (3, 0): + unicode = str # Python 3.x + + +def to_str(b, encoding): + if hasattr(b, 'decode') and not isinstance(b, unicode): + b = b.decode('utf-8') + return b + + +def to_bytes(s, encoding): + if hasattr(s, 'encode') and not isinstance(s, bytes): + s = s.encode('utf-8') + return s diff --git a/vinetrimmer/vendor/hyper/packages/rfc3986/exceptions.py b/vinetrimmer/vendor/hyper/packages/rfc3986/exceptions.py new file mode 100644 index 0000000..f9adbde --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/rfc3986/exceptions.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +class RFC3986Exception(Exception): + pass + + +class InvalidAuthority(RFC3986Exception): + def __init__(self, authority): + super(InvalidAuthority, self).__init__( + "The authority ({0}) is not valid.".format(authority)) + + +class InvalidPort(RFC3986Exception): + def __init__(self, port): + super(InvalidPort, self).__init__( + 'The port ("{0}") is not valid.'.format(port)) + + +class ResolutionError(RFC3986Exception): + def __init__(self, uri): + super(ResolutionError, self).__init__( + "{0} is not an absolute URI.".format(uri.unsplit())) diff --git a/vinetrimmer/vendor/hyper/packages/rfc3986/misc.py b/vinetrimmer/vendor/hyper/packages/rfc3986/misc.py new file mode 100644 index 0000000..c599434 --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/rfc3986/misc.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2014 Rackspace +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +rfc3986.misc +~~~~~~~~~~~~ + +This module contains important constants, patterns, and compiled regular +expressions for parsing and validating URIs and their components. +""" + +import re + +# These are enumerated for the named tuple used as a superclass of +# URIReference +URI_COMPONENTS = ['scheme', 'authority', 'path', 'query', 'fragment'] + +important_characters = { + 'generic_delimiters': ":/?#[]@", + 'sub_delimiters': "!$&'()*+,;=", + # We need to escape the '*' in this case + 're_sub_delimiters': "!$&'()\*+,;=", + 'unreserved_chars': ('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + '0123456789._~-'), + # We need to escape the '-' in this case: + 're_unreserved': 'A-Za-z0-9._~\-', + } +# For details about delimiters and reserved characters, see: +# http://tools.ietf.org/html/rfc3986#section-2.2 +GENERIC_DELIMITERS = set(important_characters['generic_delimiters']) +SUB_DELIMITERS = set(important_characters['sub_delimiters']) +RESERVED_CHARS = GENERIC_DELIMITERS.union(SUB_DELIMITERS) +# For details about unreserved characters, see: +# http://tools.ietf.org/html/rfc3986#section-2.3 +UNRESERVED_CHARS = set(important_characters['unreserved_chars']) +NON_PCT_ENCODED = RESERVED_CHARS.union(UNRESERVED_CHARS).union('%') + +# Extracted from http://tools.ietf.org/html/rfc3986#appendix-B +component_pattern_dict = { + 'scheme': '[^:/?#]+', + 'authority': '[^/?#]*', + 'path': '[^?#]*', + 'query': '[^#]*', + 'fragment': '.*', + } + +# See http://tools.ietf.org/html/rfc3986#appendix-B +# In this case, we name each of the important matches so we can use +# SRE_Match#groupdict to parse the values out if we so choose. This is also +# modified to ignore other matches that are not important to the parsing of +# the reference so we can also simply use SRE_Match#groups. +expression = ('(?:(?P<scheme>{scheme}):)?(?://(?P<authority>{authority}))?' + '(?P<path>{path})(?:\?(?P<query>{query}))?' + '(?:#(?P<fragment>{fragment}))?' + ).format(**component_pattern_dict) + +URI_MATCHER = re.compile(expression) + +# ######################### +# Authority Matcher Section +# ######################### + +# Host patterns, see: http://tools.ietf.org/html/rfc3986#section-3.2.2 +# The pattern for a regular name, e.g., www.google.com, api.github.com +reg_name = '(({0})*|[{1}]*)'.format( + '%[0-9A-Fa-f]{2}', + important_characters['re_sub_delimiters'] + + important_characters['re_unreserved'] + ) +# The pattern for an IPv4 address, e.g., 192.168.255.255, 127.0.0.1, +ipv4 = '(\d{1,3}.){3}\d{1,3}' +# Hexadecimal characters used in each piece of an IPv6 address +hexdig = '[0-9A-Fa-f]{1,4}' +# Least-significant 32 bits of an IPv6 address +ls32 = '({hex}:{hex}|{ipv4})'.format(hex=hexdig, ipv4=ipv4) +# Substitutions into the following patterns for IPv6 patterns defined +# http://tools.ietf.org/html/rfc3986#page-20 +subs = {'hex': hexdig, 'ls32': ls32} + +# Below: h16 = hexdig, see: https://tools.ietf.org/html/rfc5234 for details +# about ABNF (Augmented Backus-Naur Form) use in the comments +variations = [ + # 6( h16 ":" ) ls32 + '(%(hex)s:){6}%(ls32)s' % subs, + # "::" 5( h16 ":" ) ls32 + '::(%(hex)s:){5}%(ls32)s' % subs, + # [ h16 ] "::" 4( h16 ":" ) ls32 + '(%(hex)s)?::(%(hex)s:){4}%(ls32)s' % subs, + # [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 + '((%(hex)s:)?%(hex)s)?::(%(hex)s:){3}%(ls32)s' % subs, + # [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 + '((%(hex)s:){0,2}%(hex)s)?::(%(hex)s:){2}%(ls32)s' % subs, + # [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 + '((%(hex)s:){0,3}%(hex)s)?::%(hex)s:%(ls32)s' % subs, + # [ *4( h16 ":" ) h16 ] "::" ls32 + '((%(hex)s:){0,4}%(hex)s)?::%(ls32)s' % subs, + # [ *5( h16 ":" ) h16 ] "::" h16 + '((%(hex)s:){0,5}%(hex)s)?::%(hex)s' % subs, + # [ *6( h16 ":" ) h16 ] "::" + '((%(hex)s:){0,6}%(hex)s)?::' % subs, + ] + +ipv6 = '(({0})|({1})|({2})|({3})|({4})|({5})|({6})|({7}))'.format(*variations) + +ipv_future = 'v[0-9A-Fa-f]+.[%s]+' % ( + important_characters['re_unreserved'] + + important_characters['re_sub_delimiters'] + + ':') + +ip_literal = '\[({0}|{1})\]'.format(ipv6, ipv_future) + +# Pattern for matching the host piece of the authority +HOST_PATTERN = '({0}|{1}|{2})'.format(reg_name, ipv4, ip_literal) + +SUBAUTHORITY_MATCHER = re.compile(( + '^(?:(?P<userinfo>[A-Za-z0-9_.~\-%:]+)@)?' # userinfo + '(?P<host>{0}?)' # host + ':?(?P<port>\d+)?$' # port + ).format(HOST_PATTERN)) + +IPv4_MATCHER = re.compile('^' + ipv4 + '$') + + +# #################### +# Path Matcher Section +# #################### + +# See http://tools.ietf.org/html/rfc3986#section-3.3 for more information +# about the path patterns defined below. + +# Percent encoded character values +pct_encoded = '%[A-Fa-f0-9]{2}' +pchar = ('([' + important_characters['re_unreserved'] + + important_characters['re_sub_delimiters'] + + ':@]|%s)' % pct_encoded) +segments = { + 'segment': pchar + '*', + # Non-zero length segment + 'segment-nz': pchar + '+', + # Non-zero length segment without ":" + 'segment-nz-nc': pchar.replace(':', '') + '+' + } + +# Path types taken from Section 3.3 (linked above) +path_empty = '^$' +path_rootless = '%(segment-nz)s(/%(segment)s)*' % segments +path_noscheme = '%(segment-nz-nc)s(/%(segment)s)*' % segments +path_absolute = '/(%s)?' % path_rootless +path_abempty = '(/%(segment)s)*' % segments + +# Matcher used to validate path components +PATH_MATCHER = re.compile('^(%s|%s|%s|%s|%s)$' % ( + path_abempty, path_absolute, path_noscheme, path_rootless, path_empty + )) + + +# ################################## +# Query and Fragment Matcher Section +# ################################## + +QUERY_MATCHER = re.compile( + '^([/?:@' + important_characters['re_unreserved'] + + important_characters['re_sub_delimiters'] + + ']|%s)*$' % pct_encoded) + +FRAGMENT_MATCHER = QUERY_MATCHER + +# Scheme validation, see: http://tools.ietf.org/html/rfc3986#section-3.1 +SCHEME_MATCHER = re.compile('^[A-Za-z][A-Za-z0-9+.\-]*$') + +# Relative reference matcher + +# See http://tools.ietf.org/html/rfc3986#section-4.2 for details +relative_part = '(//%s%s|%s|%s|%s)' % ( + component_pattern_dict['authority'], path_abempty, path_absolute, + path_noscheme, path_empty + ) + +RELATIVE_REF_MATCHER = re.compile('^%s(\?%s)?(#%s)?$' % ( + relative_part, QUERY_MATCHER.pattern, FRAGMENT_MATCHER.pattern + )) + +# See http://tools.ietf.org/html/rfc3986#section-3 for definition +hier_part = '(//%s%s|%s|%s|%s)' % ( + component_pattern_dict['authority'], path_abempty, path_absolute, + path_rootless, path_empty + ) + +# See http://tools.ietf.org/html/rfc3986#section-4.3 +ABSOLUTE_URI_MATCHER = re.compile('^%s:%s(\?%s)?$' % ( + component_pattern_dict['scheme'], hier_part, QUERY_MATCHER.pattern[1:-1] + )) + + +# Path merger as defined in http://tools.ietf.org/html/rfc3986#section-5.2.3 +def merge_paths(base_uri, relative_path): + """Merge a base URI's path with a relative URI's path.""" + if base_uri.path is None and base_uri.authority is not None: + return '/' + relative_path + else: + path = base_uri.path or '' + index = path.rfind('/') + return path[:index] + '/' + relative_path diff --git a/vinetrimmer/vendor/hyper/packages/rfc3986/normalizers.py b/vinetrimmer/vendor/hyper/packages/rfc3986/normalizers.py new file mode 100644 index 0000000..bb0630c --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/rfc3986/normalizers.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2014 Rackspace +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import re + +from .compat import to_bytes +from .misc import NON_PCT_ENCODED + + +def normalize_scheme(scheme): + return scheme.lower() + + +def normalize_authority(authority): + userinfo, host, port = authority + result = '' + if userinfo: + result += normalize_percent_characters(userinfo) + '@' + if host: + result += host.lower() + if port: + result += ':' + port + return result + + +def normalize_path(path): + if not path: + return path + + path = normalize_percent_characters(path) + return remove_dot_segments(path) + + +def normalize_query(query): + return normalize_percent_characters(query) + + +def normalize_fragment(fragment): + return normalize_percent_characters(fragment) + + +PERCENT_MATCHER = re.compile('%[A-Fa-f0-9]{2}') + + +def normalize_percent_characters(s): + """All percent characters should be upper-cased. + + For example, ``"%3afoo%DF%ab"`` should be turned into ``"%3Afoo%DF%AB"``. + """ + matches = set(PERCENT_MATCHER.findall(s)) + for m in matches: + if not m.isupper(): + s = s.replace(m, m.upper()) + return s + + +def remove_dot_segments(s): + # See http://tools.ietf.org/html/rfc3986#section-5.2.4 for pseudo-code + segments = s.split('/') # Turn the path into a list of segments + output = [] # Initialize the variable to use to store output + + for segment in segments: + # '.' is the current directory, so ignore it, it is superfluous + if segment == '.': + continue + # Anything other than '..', should be appended to the output + elif segment != '..': + output.append(segment) + # In this case segment == '..', if we can, we should pop the last + # element + elif output: + output.pop() + + # If the path starts with '/' and the output is empty or the first string + # is non-empty + if s.startswith('/') and (not output or output[0]): + output.insert(0, '') + + # If the path starts with '/.' or '/..' ensure we add one more empty + # string to add a trailing '/' + if s.endswith(('/.', '/..')): + output.append('') + + return '/'.join(output) + + +def encode_component(uri_component, encoding): + if uri_component is None: + return uri_component + + uri_bytes = to_bytes(uri_component, encoding) + + encoded_uri = bytearray() + + for i in range(0, len(uri_bytes)): + # Will return a single character bytestring on both Python 2 & 3 + byte = uri_bytes[i:i+1] + byte_ord = ord(byte) + if byte_ord < 128 and byte.decode() in NON_PCT_ENCODED: + encoded_uri.extend(byte) + continue + encoded_uri.extend('%{0:02x}'.format(byte_ord).encode()) + + return encoded_uri.decode(encoding) diff --git a/vinetrimmer/vendor/hyper/packages/rfc3986/parseresult.py b/vinetrimmer/vendor/hyper/packages/rfc3986/parseresult.py new file mode 100644 index 0000000..2def55b --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/rfc3986/parseresult.py @@ -0,0 +1,303 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015 Ian Cordasco +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from collections import namedtuple + +from . import compat +from . import exceptions +from . import normalizers +from . import uri + +__all__ = ('ParseResult', 'ParseResultBytes') + +PARSED_COMPONENTS = ('scheme', 'userinfo', 'host', 'port', 'path', 'query', + 'fragment') + + +class ParseResultMixin(object): + def _generate_authority(self, attributes): + # I swear I did not align the comparisons below. That's just how they + # happened to align based on pep8 and attribute lengths. + userinfo, host, port = (attributes[p] + for p in ('userinfo', 'host', 'port')) + if (self.userinfo != userinfo or + self.host != host or + self.port != port): + if port: + port = '{0}'.format(port) + return normalizers.normalize_authority( + (compat.to_str(userinfo, self.encoding), + compat.to_str(host, self.encoding), + port) + ) + return self.authority + + def geturl(self): + """Standard library shim to the unsplit method.""" + return self.unsplit() + + @property + def hostname(self): + """Standard library shim for the host portion of the URI.""" + return self.host + + @property + def netloc(self): + """Standard library shim for the authority portion of the URI.""" + return self.authority + + @property + def params(self): + """Standard library shim for the query portion of the URI.""" + return self.query + + +class ParseResult(namedtuple('ParseResult', PARSED_COMPONENTS), + ParseResultMixin): + slots = () + + def __new__(cls, scheme, userinfo, host, port, path, query, fragment, + uri_ref, encoding='utf-8'): + parse_result = super(ParseResult, cls).__new__( + cls, + scheme or None, + userinfo or None, + host, + port or None, + path or None, + query or None, + fragment or None) + parse_result.encoding = encoding + parse_result.reference = uri_ref + return parse_result + + @classmethod + def from_string(cls, uri_string, encoding='utf-8', strict=True): + """Parse a URI from the given unicode URI string. + + :param str uri_string: Unicode URI to be parsed into a reference. + :param str encoding: The encoding of the string provided + :param bool strict: Parse strictly according to :rfc:`3986` if True. + If False, parse similarly to the standard library's urlparse + function. + :returns: :class:`ParseResult` or subclass thereof + """ + reference = uri.URIReference.from_string(uri_string, encoding) + try: + subauthority = reference.authority_info() + except exceptions.InvalidAuthority: + if strict: + raise + userinfo, host, port = split_authority(reference.authority) + else: + # Thanks to Richard Barrell for this idea: + # https://twitter.com/0x2ba22e11/status/617338811975139328 + userinfo, host, port = (subauthority.get(p) + for p in ('userinfo', 'host', 'port')) + + if port: + try: + port = int(port) + except ValueError: + raise exceptions.InvalidPort(port) + + return cls(scheme=reference.scheme, + userinfo=userinfo, + host=host, + port=port, + path=reference.path, + query=reference.query, + fragment=reference.fragment, + uri_ref=reference, + encoding=encoding) + + @property + def authority(self): + """Normalized authority generated from the subauthority parts.""" + return self.reference.authority + + def copy_with(self, scheme=None, userinfo=None, host=None, port=None, + path=None, query=None, fragment=None): + attributes = zip(PARSED_COMPONENTS, + (scheme, userinfo, host, port, path, query, fragment)) + attrs_dict = {} + for name, value in attributes: + if value is None: + value = getattr(self, name) + attrs_dict[name] = value + authority = self._generate_authority(attrs_dict) + ref = self.reference.copy_with(scheme=attrs_dict['scheme'], + authority=authority, + path=attrs_dict['path'], + query=attrs_dict['query'], + fragment=attrs_dict['fragment']) + return ParseResult(uri_ref=ref, encoding=self.encoding, **attrs_dict) + + def encode(self, encoding=None): + encoding = encoding or self.encoding + attrs = dict( + zip(PARSED_COMPONENTS, + (attr.encode(encoding) if hasattr(attr, 'encode') else attr + for attr in self))) + return ParseResultBytes( + uri_ref=self.reference, + encoding=encoding, + **attrs + ) + + def unsplit(self, use_idna=False): + """Create a URI string from the components. + + :returns: The parsed URI reconstituted as a string. + :rtype: str + """ + parse_result = self + if use_idna and self.host: + hostbytes = self.host.encode('idna') + host = hostbytes.decode(self.encoding) + parse_result = self.copy_with(host=host) + return parse_result.reference.unsplit() + + +class ParseResultBytes(namedtuple('ParseResultBytes', PARSED_COMPONENTS), + ParseResultMixin): + def __new__(cls, scheme, userinfo, host, port, path, query, fragment, + uri_ref, encoding='utf-8'): + parse_result = super(ParseResultBytes, cls).__new__( + cls, + scheme or None, + userinfo or None, + host, + port or None, + path or None, + query or None, + fragment or None) + parse_result.encoding = encoding + parse_result.reference = uri_ref + return parse_result + + @classmethod + def from_string(cls, uri_string, encoding='utf-8', strict=True): + """Parse a URI from the given unicode URI string. + + :param str uri_string: Unicode URI to be parsed into a reference. + :param str encoding: The encoding of the string provided + :param bool strict: Parse strictly according to :rfc:`3986` if True. + If False, parse similarly to the standard library's urlparse + function. + :returns: :class:`ParseResultBytes` or subclass thereof + """ + reference = uri.URIReference.from_string(uri_string, encoding) + try: + subauthority = reference.authority_info() + except exceptions.InvalidAuthority: + if strict: + raise + userinfo, host, port = split_authority(reference.authority) + else: + # Thanks to Richard Barrell for this idea: + # https://twitter.com/0x2ba22e11/status/617338811975139328 + userinfo, host, port = (subauthority.get(p) + for p in ('userinfo', 'host', 'port')) + + if port: + try: + port = int(port) + except ValueError: + raise exceptions.InvalidPort(port) + + to_bytes = compat.to_bytes + return cls(scheme=to_bytes(reference.scheme, encoding), + userinfo=to_bytes(userinfo, encoding), + host=to_bytes(host, encoding), + port=port, + path=to_bytes(reference.path, encoding), + query=to_bytes(reference.query, encoding), + fragment=to_bytes(reference.fragment, encoding), + uri_ref=reference, + encoding=encoding) + + @property + def authority(self): + """Normalized authority generated from the subauthority parts.""" + return self.reference.authority.encode(self.encoding) + + def copy_with(self, scheme=None, userinfo=None, host=None, port=None, + path=None, query=None, fragment=None): + attributes = zip(PARSED_COMPONENTS, + (scheme, userinfo, host, port, path, query, fragment)) + attrs_dict = {} + for name, value in attributes: + if value is None: + value = getattr(self, name) + if not isinstance(value, bytes) and hasattr(value, 'encode'): + value = value.encode(self.encoding) + attrs_dict[name] = value + authority = self._generate_authority(attrs_dict) + to_str = compat.to_str + ref = self.reference.copy_with( + scheme=to_str(attrs_dict['scheme'], self.encoding), + authority=authority, + path=to_str(attrs_dict['path'], self.encoding), + query=to_str(attrs_dict['query'], self.encoding), + fragment=to_str(attrs_dict['fragment'], self.encoding) + ) + return ParseResultBytes( + uri_ref=ref, + encoding=self.encoding, + **attrs_dict + ) + + def unsplit(self, use_idna=False): + """Create a URI bytes object from the components. + + :returns: The parsed URI reconstituted as a string. + :rtype: bytes + """ + parse_result = self + if use_idna and self.host: + # self.host is bytes, to encode to idna, we need to decode it + # first + host = self.host.decode(self.encoding) + hostbytes = host.encode('idna') + parse_result = self.copy_with(host=hostbytes) + uri = parse_result.reference.unsplit() + return uri.encode(self.encoding) + + +def split_authority(authority): + # Initialize our expected return values + userinfo = host = port = None + # Initialize an extra var we may need to use + extra_host = None + # Set-up rest in case there is no userinfo portion + rest = authority + + if '@' in authority: + userinfo, rest = authority.rsplit('@', 1) + + # Handle IPv6 host addresses + if rest.startswith('['): + host, rest = rest.split(']', 1) + host += ']' + + if ':' in rest: + extra_host, port = rest.split(':', 1) + elif not host and rest: + host = rest + + if extra_host and not host: + host = extra_host + + return userinfo, host, port diff --git a/vinetrimmer/vendor/hyper/packages/rfc3986/uri.py b/vinetrimmer/vendor/hyper/packages/rfc3986/uri.py new file mode 100644 index 0000000..b7f5ccb --- /dev/null +++ b/vinetrimmer/vendor/hyper/packages/rfc3986/uri.py @@ -0,0 +1,385 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2014 Rackspace +# Copyright (c) 2015 Ian Cordasco +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from collections import namedtuple + +from .compat import to_str +from .exceptions import InvalidAuthority, ResolutionError +from .misc import ( + ABSOLUTE_URI_MATCHER, FRAGMENT_MATCHER, IPv4_MATCHER, PATH_MATCHER, + QUERY_MATCHER, SCHEME_MATCHER, SUBAUTHORITY_MATCHER, URI_MATCHER, + URI_COMPONENTS, merge_paths + ) +from .normalizers import ( + encode_component, normalize_scheme, normalize_authority, normalize_path, + normalize_query, normalize_fragment + ) + + +class URIReference(namedtuple('URIReference', URI_COMPONENTS)): + slots = () + + def __new__(cls, scheme, authority, path, query, fragment, + encoding='utf-8'): + ref = super(URIReference, cls).__new__( + cls, + scheme or None, + authority or None, + path or None, + query or None, + fragment or None) + ref.encoding = encoding + return ref + + def __eq__(self, other): + other_ref = other + if isinstance(other, tuple): + other_ref = URIReference(*other) + elif not isinstance(other, URIReference): + try: + other_ref = URIReference.from_string(other) + except TypeError: + raise TypeError( + 'Unable to compare URIReference() to {0}()'.format( + type(other).__name__)) + + # See http://tools.ietf.org/html/rfc3986#section-6.2 + naive_equality = tuple(self) == tuple(other_ref) + return naive_equality or self.normalized_equality(other_ref) + + @classmethod + def from_string(cls, uri_string, encoding='utf-8'): + """Parse a URI reference from the given unicode URI string. + + :param str uri_string: Unicode URI to be parsed into a reference. + :param str encoding: The encoding of the string provided + :returns: :class:`URIReference` or subclass thereof + """ + uri_string = to_str(uri_string, encoding) + + split_uri = URI_MATCHER.match(uri_string).groupdict() + return cls(split_uri['scheme'], split_uri['authority'], + encode_component(split_uri['path'], encoding), + encode_component(split_uri['query'], encoding), + encode_component(split_uri['fragment'], encoding), encoding) + + def authority_info(self): + """Returns a dictionary with the ``userinfo``, ``host``, and ``port``. + + If the authority is not valid, it will raise a ``InvalidAuthority`` + Exception. + + :returns: + ``{'userinfo': 'username:password', 'host': 'www.example.com', + 'port': '80'}`` + :rtype: dict + :raises InvalidAuthority: If the authority is not ``None`` and can not + be parsed. + """ + if not self.authority: + return {'userinfo': None, 'host': None, 'port': None} + + match = SUBAUTHORITY_MATCHER.match(self.authority) + + if match is None: + # In this case, we have an authority that was parsed from the URI + # Reference, but it cannot be further parsed by our + # SUBAUTHORITY_MATCHER. In this case it must not be a valid + # authority. + raise InvalidAuthority(self.authority.encode(self.encoding)) + + # We had a match, now let's ensure that it is actually a valid host + # address if it is IPv4 + matches = match.groupdict() + host = matches.get('host') + + if (host and IPv4_MATCHER.match(host) and not + valid_ipv4_host_address(host)): + # If we have a host, it appears to be IPv4 and it does not have + # valid bytes, it is an InvalidAuthority. + raise InvalidAuthority(self.authority.encode(self.encoding)) + + return matches + + @property + def host(self): + """If present, a string representing the host.""" + try: + authority = self.authority_info() + except InvalidAuthority: + return None + return authority['host'] + + @property + def port(self): + """If present, the port (as a string) extracted from the authority.""" + try: + authority = self.authority_info() + except InvalidAuthority: + return None + return authority['port'] + + @property + def userinfo(self): + """If present, the userinfo extracted from the authority.""" + try: + authority = self.authority_info() + except InvalidAuthority: + return None + return authority['userinfo'] + + def is_absolute(self): + """Determine if this URI Reference is an absolute URI. + + See http://tools.ietf.org/html/rfc3986#section-4.3 for explanation. + + :returns: ``True`` if it is an absolute URI, ``False`` otherwise. + :rtype: bool + """ + return bool(ABSOLUTE_URI_MATCHER.match(self.unsplit())) + + def is_valid(self, **kwargs): + """Determines if the URI is valid. + + :param bool require_scheme: Set to ``True`` if you wish to require the + presence of the scheme component. + :param bool require_authority: Set to ``True`` if you wish to require + the presence of the authority component. + :param bool require_path: Set to ``True`` if you wish to require the + presence of the path component. + :param bool require_query: Set to ``True`` if you wish to require the + presence of the query component. + :param bool require_fragment: Set to ``True`` if you wish to require + the presence of the fragment component. + :returns: ``True`` if the URI is valid. ``False`` otherwise. + :rtype: bool + """ + validators = [ + (self.scheme_is_valid, kwargs.get('require_scheme', False)), + (self.authority_is_valid, kwargs.get('require_authority', False)), + (self.path_is_valid, kwargs.get('require_path', False)), + (self.query_is_valid, kwargs.get('require_query', False)), + (self.fragment_is_valid, kwargs.get('require_fragment', False)), + ] + return all(v(r) for v, r in validators) + + def _is_valid(self, value, matcher, require): + if require: + return (value is not None + and matcher.match(value)) + + # require is False and value is not None + return value is None or matcher.match(value) + + def authority_is_valid(self, require=False): + """Determines if the authority component is valid. + + :param str require: Set to ``True`` to require the presence of this + component. + :returns: ``True`` if the authority is valid. ``False`` otherwise. + :rtype: bool + """ + try: + self.authority_info() + except InvalidAuthority: + return False + + is_valid = self._is_valid(self.authority, + SUBAUTHORITY_MATCHER, + require) + + # Ensure that IPv4 addresses have valid bytes + if is_valid and self.host and IPv4_MATCHER.match(self.host): + return valid_ipv4_host_address(self.host) + + # Perhaps the host didn't exist or if it did, it wasn't an IPv4-like + # address. In either case, we want to rely on the `_is_valid` check, + # so let's return that. + return is_valid + + def scheme_is_valid(self, require=False): + """Determines if the scheme component is valid. + + :param str require: Set to ``True`` to require the presence of this + component. + :returns: ``True`` if the scheme is valid. ``False`` otherwise. + :rtype: bool + """ + return self._is_valid(self.scheme, SCHEME_MATCHER, require) + + def path_is_valid(self, require=False): + """Determines if the path component is valid. + + :param str require: Set to ``True`` to require the presence of this + component. + :returns: ``True`` if the path is valid. ``False`` otherwise. + :rtype: bool + """ + return self._is_valid(self.path, PATH_MATCHER, require) + + def query_is_valid(self, require=False): + """Determines if the query component is valid. + + :param str require: Set to ``True`` to require the presence of this + component. + :returns: ``True`` if the query is valid. ``False`` otherwise. + :rtype: bool + """ + return self._is_valid(self.query, QUERY_MATCHER, require) + + def fragment_is_valid(self, require=False): + """Determines if the fragment component is valid. + + :param str require: Set to ``True`` to require the presence of this + component. + :returns: ``True`` if the fragment is valid. ``False`` otherwise. + :rtype: bool + """ + return self._is_valid(self.fragment, FRAGMENT_MATCHER, require) + + def normalize(self): + """Normalize this reference as described in Section 6.2.2 + + This is not an in-place normalization. Instead this creates a new + URIReference. + + :returns: A new reference object with normalized components. + :rtype: URIReference + """ + # See http://tools.ietf.org/html/rfc3986#section-6.2.2 for logic in + # this method. + return URIReference(normalize_scheme(self.scheme or ''), + normalize_authority( + (self.userinfo, self.host, self.port)), + normalize_path(self.path or ''), + normalize_query(self.query or ''), + normalize_fragment(self.fragment or '')) + + def normalized_equality(self, other_ref): + """Compare this URIReference to another URIReference. + + :param URIReference other_ref: (required), The reference with which + we're comparing. + :returns: ``True`` if the references are equal, ``False`` otherwise. + :rtype: bool + """ + return tuple(self.normalize()) == tuple(other_ref.normalize()) + + def resolve_with(self, base_uri, strict=False): + """Use an absolute URI Reference to resolve this relative reference. + + Assuming this is a relative reference that you would like to resolve, + use the provided base URI to resolve it. + + See http://tools.ietf.org/html/rfc3986#section-5 for more information. + + :param base_uri: Either a string or URIReference. It must be an + absolute URI or it will raise an exception. + :returns: A new URIReference which is the result of resolving this + reference using ``base_uri``. + :rtype: :class:`URIReference` + :raises ResolutionError: If the ``base_uri`` is not an absolute URI. + """ + if not isinstance(base_uri, URIReference): + base_uri = URIReference.from_string(base_uri) + + if not base_uri.is_absolute(): + raise ResolutionError(base_uri) + + # This is optional per + # http://tools.ietf.org/html/rfc3986#section-5.2.1 + base_uri = base_uri.normalize() + + # The reference we're resolving + resolving = self + + if not strict and resolving.scheme == base_uri.scheme: + resolving = resolving.copy_with(scheme=None) + + # http://tools.ietf.org/html/rfc3986#page-32 + if resolving.scheme is not None: + target = resolving.copy_with(path=normalize_path(resolving.path)) + else: + if resolving.authority is not None: + target = resolving.copy_with( + scheme=base_uri.scheme, + path=normalize_path(resolving.path) + ) + else: + if resolving.path is None: + if resolving.query is not None: + query = resolving.query + else: + query = base_uri.query + target = resolving.copy_with( + scheme=base_uri.scheme, + authority=base_uri.authority, + path=base_uri.path, + query=query + ) + else: + if resolving.path.startswith('/'): + path = normalize_path(resolving.path) + else: + path = normalize_path( + merge_paths(base_uri, resolving.path) + ) + target = resolving.copy_with( + scheme=base_uri.scheme, + authority=base_uri.authority, + path=path, + query=resolving.query + ) + return target + + def unsplit(self): + """Create a URI string from the components. + + :returns: The URI Reference reconstituted as a string. + :rtype: str + """ + # See http://tools.ietf.org/html/rfc3986#section-5.3 + result_list = [] + if self.scheme: + result_list.extend([self.scheme, ':']) + if self.authority: + result_list.extend(['//', self.authority]) + if self.path: + result_list.append(self.path) + if self.query: + result_list.extend(['?', self.query]) + if self.fragment: + result_list.extend(['#', self.fragment]) + return ''.join(result_list) + + def copy_with(self, scheme=None, authority=None, path=None, query=None, + fragment=None): + attributes = { + 'scheme': scheme, + 'authority': authority, + 'path': path, + 'query': query, + 'fragment': fragment, + } + for key, value in list(attributes.items()): + if value is None: + del attributes[key] + return self._replace(**attributes) + + +def valid_ipv4_host_address(host): + # If the host exists, and it might be IPv4, check each byte in the + # address. + return all([0 <= int(byte, base=10) <= 255 for byte in host.split('.')]) diff --git a/vinetrimmer/vendor/hyper/ssl_compat.py b/vinetrimmer/vendor/hyper/ssl_compat.py new file mode 100644 index 0000000..976b623 --- /dev/null +++ b/vinetrimmer/vendor/hyper/ssl_compat.py @@ -0,0 +1,307 @@ +# -*- coding: utf-8 -*- +""" +hyper/ssl_compat +~~~~~~~~~ + +Shoves pyOpenSSL into an API that looks like the standard Python 3.x ssl +module. + +Currently exposes exactly those attributes, classes, and methods that we +actually use in hyper (all method signatures are complete, however). May be +expanded to something more general-purpose in the future. +""" +try: + import StringIO as BytesIO +except ImportError: + from io import BytesIO +import errno +import socket +import time + +from OpenSSL import SSL as ossl +from service_identity.pyopenssl import verify_hostname as _verify + +CERT_NONE = ossl.VERIFY_NONE +CERT_REQUIRED = ossl.VERIFY_PEER | ossl.VERIFY_FAIL_IF_NO_PEER_CERT + +_OPENSSL_ATTRS = dict( + OP_NO_COMPRESSION='OP_NO_COMPRESSION', + PROTOCOL_TLSv1_2='TLSv1_2_METHOD', + PROTOCOL_SSLv23='SSLv23_METHOD', +) + +for external, internal in _OPENSSL_ATTRS.items(): + value = getattr(ossl, internal, None) + if value: + locals()[external] = value + +OP_ALL = 0 +# TODO: Find out the names of these other flags. +for bit in [31] + list(range(10)): + OP_ALL |= 1 << bit + +HAS_NPN = True + + +def _proxy(method): + def inner(self, *args, **kwargs): + return getattr(self._conn, method)(*args, **kwargs) + return inner + +# Referenced in hyper/http20/connection.py. These values come +# from the python ssl package, and must be defined in this file +# for hyper to work in python versions <2.7.9 +SSL_ERROR_WANT_READ = 2 +SSL_ERROR_WANT_WRITE = 3 + + +# TODO missing some attributes +class SSLError(OSError): + pass + + +class CertificateError(SSLError): + pass + + +def verify_hostname(ssl_sock, server_hostname): + """ + A method nearly compatible with the stdlib's match_hostname. + """ + if isinstance(server_hostname, bytes): + server_hostname = server_hostname.decode('ascii') + return _verify(ssl_sock._conn, server_hostname) + + +class SSLSocket(object): + SSL_TIMEOUT = 3 + SSL_RETRY = .01 + + def __init__(self, conn, server_side, do_handshake_on_connect, + suppress_ragged_eofs, server_hostname, check_hostname): + self._conn = conn + self._do_handshake_on_connect = do_handshake_on_connect + self._suppress_ragged_eofs = suppress_ragged_eofs + self._check_hostname = check_hostname + + if server_side: + self._conn.set_accept_state() + else: + if server_hostname: + self._conn.set_tlsext_host_name( + server_hostname.encode('utf-8') + ) + self._server_hostname = server_hostname + # FIXME does this override do_handshake_on_connect=False? + self._conn.set_connect_state() + + if self.connected and self._do_handshake_on_connect: + self.do_handshake() + + @property + def connected(self): + try: + self._conn.getpeername() + except socket.error as e: + if e.errno != errno.ENOTCONN: + # It's an exception other than the one we expected if we're not + # connected. + raise + return False + return True + + # Lovingly stolen from CherryPy + # (http://svn.cherrypy.org/tags/cherrypy-3.2.1/cherrypy/wsgiserver/ssl_pyopenssl.py). + def _safe_ssl_call(self, suppress_ragged_eofs, call, *args, **kwargs): + """Wrap the given call with SSL error-trapping.""" + start = time.time() + while True: + try: + return call(*args, **kwargs) + except (ossl.WantReadError, ossl.WantWriteError): + # Sleep and try again. This is dangerous, because it means + # the rest of the stack has no way of differentiating + # between a "new handshake" error and "client dropped". + # Note this isn't an endless loop: there's a timeout below. + time.sleep(self.SSL_RETRY) + except ossl.Error as e: + if suppress_ragged_eofs and e.args == (-1, 'Unexpected EOF'): + return b'' + raise socket.error(e.args[0]) + + if time.time() - start > self.SSL_TIMEOUT: + raise socket.timeout('timed out') + + def connect(self, address): + self._conn.connect(address) + if self._do_handshake_on_connect: + self.do_handshake() + + def do_handshake(self): + self._safe_ssl_call(False, self._conn.do_handshake) + if self._check_hostname: + verify_hostname(self, self._server_hostname) + + def recv(self, bufsize, flags=None): + return self._safe_ssl_call( + self._suppress_ragged_eofs, + self._conn.recv, + bufsize, + flags + ) + + def recv_into(self, buffer, bufsize=None, flags=None): + # A temporary recv_into implementation. Should be replaced when + # PyOpenSSL has merged pyca/pyopenssl#121. + if bufsize is None: + bufsize = len(buffer) + + data = self.recv(bufsize, flags) + data_len = len(data) + buffer[0:data_len] = data + return data_len + + def send(self, data, flags=None): + return self._safe_ssl_call(False, self._conn.send, data, flags) + + def sendall(self, data, flags=None): + return self._safe_ssl_call(False, self._conn.sendall, data, flags) + + def selected_npn_protocol(self): + proto = self._conn.get_next_proto_negotiated() + if isinstance(proto, bytes): + proto = proto.decode('ascii') + + return proto if proto else None + + def selected_alpn_protocol(self): + proto = self._conn.get_alpn_proto_negotiated() + if isinstance(proto, bytes): + proto = proto.decode('ascii') + + return proto if proto else None + + def getpeercert(self): + def resolve_alias(alias): + return dict( + C='countryName', + ST='stateOrProvinceName', + L='localityName', + O='organizationName', + OU='organizationalUnitName', + CN='commonName', + ).get(alias, alias) + + def to_components(name): + # TODO Verify that these are actually *supposed* to all be + # single-element tuples, and that's not just a quirk of the + # examples I've seen. + return tuple( + [ + (resolve_alias(k.decode('utf-8'), v.decode('utf-8')),) + for k, v in name.get_components() + ] + ) + + # The standard getpeercert() takes the nice X509 object tree returned + # by OpenSSL and turns it into a dict according to some format it seems + # to have made up on the spot. Here, we do our best to emulate that. + cert = self._conn.get_peer_certificate() + result = dict( + issuer=to_components(cert.get_issuer()), + subject=to_components(cert.get_subject()), + version=cert.get_subject(), + serialNumber=cert.get_serial_number(), + notBefore=cert.get_notBefore(), + notAfter=cert.get_notAfter(), + ) + # TODO extensions, including subjectAltName + # (see _decode_certificate in _ssl.c) + return result + + # a dash of magic to reduce boilerplate + methods = ['accept', 'bind', 'close', 'getsockname', 'listen', 'fileno'] + for method in methods: + locals()[method] = _proxy(method) + + +class SSLContext(object): + def __init__(self, protocol): + self.protocol = protocol + self._ctx = ossl.Context(protocol) + self.options = OP_ALL + self.check_hostname = False + self.npn_protos = [] + + @property + def options(self): + return self._options + + @options.setter + def options(self, value): + self._options = value + self._ctx.set_options(value) + + @property + def verify_mode(self): + return self._ctx.get_verify_mode() + + @verify_mode.setter + def verify_mode(self, value): + # TODO verify exception is raised on failure + self._ctx.set_verify( + value, lambda conn, cert, errnum, errdepth, ok: ok + ) + + def set_default_verify_paths(self): + self._ctx.set_default_verify_paths() + + def load_verify_locations(self, cafile=None, capath=None, cadata=None): + # TODO factor out common code + if cafile is not None: + cafile = cafile.encode('utf-8') + if capath is not None: + capath = capath.encode('utf-8') + self._ctx.load_verify_locations(cafile, capath) + if cadata is not None: + self._ctx.load_verify_locations(BytesIO(cadata)) + + def load_cert_chain(self, certfile, keyfile=None, password=None): + self._ctx.use_certificate_file(certfile) + if password is not None: + self._ctx.set_passwd_cb( + lambda max_length, prompt_twice, userdata: password + ) + self._ctx.use_privatekey_file(keyfile or certfile) + + def set_npn_protocols(self, protocols): + self.protocols = list(map(lambda x: x.encode('ascii'), protocols)) + + def cb(conn, protos): + # Detect the overlapping set of protocols. + overlap = set(protos) & set(self.protocols) + + # Select the option that comes last in the list in the overlap. + for p in self.protocols: + if p in overlap: + return p + else: + return b'' + + self._ctx.set_npn_select_callback(cb) + + def set_alpn_protocols(self, protocols): + protocols = list(map(lambda x: x.encode('ascii'), protocols)) + self._ctx.set_alpn_protos(protocols) + + def wrap_socket(self, + sock, + server_side=False, + do_handshake_on_connect=True, + suppress_ragged_eofs=True, + server_hostname=None): + conn = ossl.Connection(self._ctx, sock) + return SSLSocket(conn, server_side, do_handshake_on_connect, + suppress_ragged_eofs, server_hostname, + # TODO what if this is changed after the fact? + self.check_hostname) diff --git a/vinetrimmer/vendor/hyper/tls.py b/vinetrimmer/vendor/hyper/tls.py new file mode 100644 index 0000000..422b001 --- /dev/null +++ b/vinetrimmer/vendor/hyper/tls.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +""" +hyper/tls +~~~~~~~~~ + +Contains the TLS/SSL logic for use in hyper. +""" +import os.path as path +from .common.exceptions import MissingCertFile +from .compat import ignore_missing, ssl + + +NPN_PROTOCOL = 'h2' +H2_NPN_PROTOCOLS = [NPN_PROTOCOL, 'h2-16', 'h2-15', 'h2-14'] +SUPPORTED_NPN_PROTOCOLS = H2_NPN_PROTOCOLS + ['http/1.1'] + +H2C_PROTOCOL = 'h2c' + +# We have a singleton SSLContext object. There's no reason to be creating one +# per connection. +_context = None + +# Work out where our certificates are. +cert_loc = path.join(path.dirname(__file__), 'certs.pem') + + +def wrap_socket(sock, server_hostname, ssl_context=None, force_proto=None): + """ + A vastly simplified SSL wrapping function. We'll probably extend this to + do more things later. + """ + + global _context + + if ssl_context: + # if an SSLContext is provided then use it instead of default context + _ssl_context = ssl_context + else: + # create the singleton SSLContext we use + if _context is None: # pragma: no cover + _context = init_context() + _ssl_context = _context + + # the spec requires SNI support + ssl_sock = _ssl_context.wrap_socket(sock, server_hostname=server_hostname) + # Setting SSLContext.check_hostname to True only verifies that the + # post-handshake servername matches that of the certificate. We also need + # to check that it matches the requested one. + if _ssl_context.check_hostname: # pragma: no cover + try: + ssl.match_hostname(ssl_sock.getpeercert(), server_hostname) + except AttributeError: + ssl.verify_hostname(ssl_sock, server_hostname) # pyopenssl + + # Allow for the protocol to be forced externally. + proto = force_proto + + # ALPN is newer, so we prefer it over NPN. The odds of us getting + # different answers is pretty low, but let's be sure. + with ignore_missing(): + if proto is None: + proto = ssl_sock.selected_alpn_protocol() + + with ignore_missing(): + if proto is None: + proto = ssl_sock.selected_npn_protocol() + + return (ssl_sock, proto) + + +def init_context(cert_path=None, cert=None, cert_password=None): + """ + Create a new ``SSLContext`` that is correctly set up for an HTTP/2 + connection. This SSL context object can be customized and passed as a + parameter to the :class:`HTTPConnection <hyper.HTTPConnection>` class. + Provide your own certificate file in case you don’t want to use hyper’s + default certificate. The path to the certificate can be absolute or + relative to your working directory. + + :param cert_path: (optional) The path to the certificate file of + “certification authority” (CA) certificates + :param cert: (optional) if string, path to ssl client cert file (.pem). + If tuple, ('cert', 'key') pair. + The certfile string must be the path to a single file in PEM format + containing the certificate as well as any number of CA certificates + needed to establish the certificate’s authenticity. The keyfile string, + if present, must point to a file containing the private key in. + Otherwise the private key will be taken from certfile as well. + :param cert_password: (optional) The password argument may be a function to + call to get the password for decrypting the private key. It will only + be called if the private key is encrypted and a password is necessary. + It will be called with no arguments, and it should return a string, + bytes, or bytearray. If the return value is a string it will be + encoded as UTF-8 before using it to decrypt the key. Alternatively a + string, bytes, or bytearray value may be supplied directly as the + password argument. It will be ignored if the private key is not + encrypted and no password is needed. + :returns: An ``SSLContext`` correctly set up for HTTP/2. + """ + cafile = cert_path or cert_loc + if not cafile or not path.exists(cafile): + err_msg = ("No certificate found at " + str(cafile) + ". Either " + + "ensure the default cert.pem file is included in the " + + "distribution or provide a custom certificate when " + + "creating the connection.") + raise MissingCertFile(err_msg) + + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.set_default_verify_paths() + context.load_verify_locations(cafile=cafile) + context.verify_mode = ssl.CERT_REQUIRED + context.check_hostname = True + + with ignore_missing(): + context.set_npn_protocols(SUPPORTED_NPN_PROTOCOLS) + + with ignore_missing(): + context.set_alpn_protocols(SUPPORTED_NPN_PROTOCOLS) + + # required by the spec + context.options |= ssl.OP_NO_COMPRESSION + + if cert is not None: + try: + basestring + except NameError: + basestring = (str, bytes) + if not isinstance(cert, basestring): + context.load_cert_chain(cert[0], cert[1], cert_password) + else: + context.load_cert_chain(cert, password=cert_password) + + return context diff --git a/vinetrimmer/vendor/hyperframe/__init__.py b/vinetrimmer/vendor/hyperframe/__init__.py new file mode 100644 index 0000000..86190c3 --- /dev/null +++ b/vinetrimmer/vendor/hyperframe/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +""" +hyperframe +~~~~~~~~~~ + +A module for providing a pure-Python HTTP/2 framing layer. +""" +__version__ = '3.2.0' diff --git a/vinetrimmer/vendor/hyperframe/exceptions.py b/vinetrimmer/vendor/hyperframe/exceptions.py new file mode 100644 index 0000000..dd30369 --- /dev/null +++ b/vinetrimmer/vendor/hyperframe/exceptions.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" +hyperframe/exceptions +~~~~~~~~~~~~~~~~~~~~~ + +Defines the exceptions that can be thrown by hyperframe. +""" + + +class UnknownFrameError(ValueError): + """ + An frame of unknown type was received. + """ + def __init__(self, frame_type, length): + #: The type byte of the unknown frame that was received. + self.frame_type = frame_type + + #: The length of the data portion of the unknown frame. + self.length = length + + def __str__(self): + return ( + "UnknownFrameError: Unknown frame type 0x%X received, " + "length %d bytes" % (self.frame_type, self.length) + ) + + +class InvalidPaddingError(ValueError): + """ + A frame with invalid padding was received. + """ + pass + + +class InvalidFrameError(ValueError): + """ + Parsing a frame failed because the data was not laid out appropriately. + + .. versionadded:: 3.0.2 + """ + pass diff --git a/vinetrimmer/vendor/hyperframe/flags.py b/vinetrimmer/vendor/hyperframe/flags.py new file mode 100644 index 0000000..0809d34 --- /dev/null +++ b/vinetrimmer/vendor/hyperframe/flags.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +""" +hyperframe/flags +~~~~~~~~~~~~~~~~ + +Defines basic Flag and Flags data structures. +""" +import collections + + +Flag = collections.namedtuple("Flag", ["name", "bit"]) + + +class Flags(collections.abc.MutableSet): + """ + A simple MutableSet implementation that will only accept known flags as elements. + + Will behave like a regular set(), except that a ValueError will be thrown when .add()ing + unexpected flags. + """ + def __init__(self, defined_flags): + self._valid_flags = set(flag.name for flag in defined_flags) + self._flags = set() + + def __contains__(self, x): + return self._flags.__contains__(x) + + def __iter__(self): + return self._flags.__iter__() + + def __len__(self): + return self._flags.__len__() + + def discard(self, value): + return self._flags.discard(value) + + def add(self, value): + if value not in self._valid_flags: + raise ValueError("Unexpected flag: {}. Valid flags are: {}".format(value, self._valid_flags)) + return self._flags.add(value) diff --git a/vinetrimmer/vendor/hyperframe/frame.py b/vinetrimmer/vendor/hyperframe/frame.py new file mode 100644 index 0000000..52225aa --- /dev/null +++ b/vinetrimmer/vendor/hyperframe/frame.py @@ -0,0 +1,769 @@ +# -*- coding: utf-8 -*- +""" +hyperframe/frame +~~~~~~~~~~~~~~~~ + +Defines framing logic for HTTP/2. Provides both classes to represent framed +data and logic for aiding the connection when it comes to reading from the +socket. +""" +import collections +import struct +import binascii + +from .exceptions import ( + UnknownFrameError, InvalidPaddingError, InvalidFrameError +) +from .flags import Flag, Flags + +# The maximum initial length of a frame. Some frames have shorter maximum lengths. +FRAME_MAX_LEN = (2 ** 14) + +# The maximum allowed length of a frame. +FRAME_MAX_ALLOWED_LEN = (2 ** 24) - 1 + + +class Frame(object): + """ + The base class for all HTTP/2 frames. + """ + #: The flags defined on this type of frame. + defined_flags = [] + + #: The byte used to define the type of the frame. + type = None + + # If 'has-stream', the frame's stream_id must be non-zero. If 'no-stream', + # it must be zero. If 'either', it's not checked. + stream_association = None + + def __init__(self, stream_id, flags=()): + #: The stream identifier for the stream this frame was received on. + #: Set to 0 for frames sent on the connection (stream-id 0). + self.stream_id = stream_id + + #: The flags set for this frame. + self.flags = Flags(self.defined_flags) + + #: The frame length, excluding the nine-byte header. + self.body_len = 0 + + for flag in flags: + self.flags.add(flag) + + if self.stream_association == 'has-stream' and not self.stream_id: + raise ValueError('Stream ID must be non-zero') + if self.stream_association == 'no-stream' and self.stream_id: + raise ValueError('Stream ID must be zero') + + def __repr__(self): + flags = ", ".join(self.flags) or "None" + body = binascii.hexlify(self.serialize_body()).decode('ascii') + if len(body) > 20: + body = body[:20] + "..." + return ( + "{type}(Stream: {stream}; Flags: {flags}): {body}" + ).format(type=type(self).__name__, stream=self.stream_id, flags=flags, body=body) + + @staticmethod + def parse_frame_header(header): + """ + Takes a 9-byte frame header and returns a tuple of the appropriate + Frame object and the length that needs to be read from the socket. + + This populates the flags field, and determines how long the body is. + + :raises hyperframe.exceptions.UnknownFrameError: If a frame of unknown + type is received. + """ + try: + fields = struct.unpack("!HBBBL", header) + except struct.error: + raise InvalidFrameError("Invalid frame header") + + # First 24 bits are frame length. + length = (fields[0] << 8) + fields[1] + type = fields[2] + flags = fields[3] + stream_id = fields[4] + + if type not in FRAMES: + raise UnknownFrameError(type, length) + + frame = FRAMES[type](stream_id) + frame.parse_flags(flags) + return (frame, length) + + def parse_flags(self, flag_byte): + for flag, flag_bit in self.defined_flags: + if flag_byte & flag_bit: + self.flags.add(flag) + + return self.flags + + def serialize(self): + """ + Convert a frame into a bytestring, representing the serialized form of + the frame. + """ + body = self.serialize_body() + self.body_len = len(body) + + # Build the common frame header. + # First, get the flags. + flags = 0 + + for flag, flag_bit in self.defined_flags: + if flag in self.flags: + flags |= flag_bit + + header = struct.pack( + "!HBBBL", + (self.body_len & 0xFFFF00) >> 8, # Length is spread over top 24 bits + self.body_len & 0x0000FF, + self.type, + flags, + self.stream_id & 0x7FFFFFFF # Stream ID is 32 bits. + ) + + return header + body + + def serialize_body(self): + raise NotImplementedError() + + def parse_body(self, data): + """ + Given the body of a frame, parses it into frame data. This populates + the non-header parts of the frame: that is, it does not populate the + stream ID or flags. + + :param data: A memoryview object containing the body data of the frame. + Must not contain *more* data than the length returned by + :meth:`parse_frame_header + <hyperframe.frame.Frame.parse_frame_header>`. + """ + raise NotImplementedError() + + +class Padding(object): + """ + Mixin for frames that contain padding. Defines extra fields that can be + used and set by frames that can be padded. + """ + def __init__(self, stream_id, pad_length=0, **kwargs): + super(Padding, self).__init__(stream_id, **kwargs) + + #: The length of the padding to use. + self.pad_length = pad_length + + def serialize_padding_data(self): + if 'PADDED' in self.flags: + return struct.pack('!B', self.pad_length) + return b'' + + def parse_padding_data(self, data): + if 'PADDED' in self.flags: + try: + self.pad_length = struct.unpack('!B', data[:1])[0] + except struct.error: + raise InvalidFrameError("Invalid Padding data") + return 1 + return 0 + + @property + def total_padding(self): + return self.pad_length + + +class Priority(object): + """ + Mixin for frames that contain priority data. Defines extra fields that can + be used and set by frames that contain priority data. + """ + def __init__(self, stream_id, depends_on=0x0, stream_weight=0x0, exclusive=False, **kwargs): + super(Priority, self).__init__(stream_id, **kwargs) + + #: The stream ID of the stream on which this stream depends. + self.depends_on = depends_on + + #: The weight of the stream. This is an integer between 0 and 256. + self.stream_weight = stream_weight + + #: Whether the exclusive bit was set. + self.exclusive = exclusive + + def serialize_priority_data(self): + return struct.pack( + "!LB", + self.depends_on | (int(self.exclusive) << 31), + self.stream_weight + ) + + def parse_priority_data(self, data): + MASK = 0x80000000 + + try: + self.depends_on, self.stream_weight = struct.unpack( + "!LB", data[:5] + ) + except struct.error: + raise InvalidFrameError("Invalid Priority data") + + self.exclusive = bool(self.depends_on & MASK) + self.depends_on &= ~MASK + return 5 + + +class DataFrame(Padding, Frame): + """ + DATA frames convey arbitrary, variable-length sequences of octets + associated with a stream. One or more DATA frames are used, for instance, + to carry HTTP request or response payloads. + """ + #: The flags defined for DATA frames. + defined_flags = [ + Flag('END_STREAM', 0x01), + Flag('PADDED', 0x08), + ] + + #: The type byte for data frames. + type = 0x0 + + stream_association = 'has-stream' + + def __init__(self, stream_id, data=b'', **kwargs): + super(DataFrame, self).__init__(stream_id, **kwargs) + + #: The data contained on this frame. + self.data = data + + def serialize_body(self): + padding_data = self.serialize_padding_data() + padding = b'\0' * self.total_padding + return b''.join([padding_data, self.data, padding]) + + def parse_body(self, data): + padding_data_length = self.parse_padding_data(data) + self.data = data[padding_data_length:len(data)-self.total_padding].tobytes() + self.body_len = len(data) + + if self.total_padding and self.total_padding >= self.body_len: + raise InvalidPaddingError("Padding is too long.") + + @property + def flow_controlled_length(self): + """ + The length of the frame that needs to be accounted for when considering + flow control. + """ + padding_len = self.total_padding + 1 if self.total_padding else 0 + return len(self.data) + padding_len + + +class PriorityFrame(Priority, Frame): + """ + The PRIORITY frame specifies the sender-advised priority of a stream. It + can be sent at any time for an existing stream. This enables + reprioritisation of existing streams. + """ + #: The flags defined for PRIORITY frames. + defined_flags = [] + + #: The type byte defined for PRIORITY frames. + type = 0x02 + + stream_association = 'has-stream' + + def serialize_body(self): + return self.serialize_priority_data() + + def parse_body(self, data): + self.parse_priority_data(data) + self.body_len = len(data) + + +class RstStreamFrame(Frame): + """ + The RST_STREAM frame allows for abnormal termination of a stream. When sent + by the initiator of a stream, it indicates that they wish to cancel the + stream or that an error condition has occurred. When sent by the receiver + of a stream, it indicates that either the receiver is rejecting the stream, + requesting that the stream be cancelled or that an error condition has + occurred. + """ + #: The flags defined for RST_STREAM frames. + defined_flags = [] + + #: The type byte defined for RST_STREAM frames. + type = 0x03 + + stream_association = 'has-stream' + + def __init__(self, stream_id, error_code=0, **kwargs): + super(RstStreamFrame, self).__init__(stream_id, **kwargs) + + #: The error code used when resetting the stream. + self.error_code = error_code + + def serialize_body(self): + return struct.pack("!L", self.error_code) + + def parse_body(self, data): + if len(data) != 4: + raise InvalidFrameError( + "RST_STREAM must have 4 byte body: actual length %s." % + len(data) + ) + + try: + self.error_code = struct.unpack("!L", data)[0] + except struct.error: # pragma: no cover + raise InvalidFrameError("Invalid RST_STREAM body") + + self.body_len = len(data) + + +class SettingsFrame(Frame): + """ + The SETTINGS frame conveys configuration parameters that affect how + endpoints communicate. The parameters are either constraints on peer + behavior or preferences. + + Settings are not negotiated. Settings describe characteristics of the + sending peer, which are used by the receiving peer. Different values for + the same setting can be advertised by each peer. For example, a client + might set a high initial flow control window, whereas a server might set a + lower value to conserve resources. + """ + #: The flags defined for SETTINGS frames. + defined_flags = [Flag('ACK', 0x01)] + + #: The type byte defined for SETTINGS frames. + type = 0x04 + + stream_association = 'no-stream' + + # We need to define the known settings, they may as well be class + # attributes. + #: The byte that signals the SETTINGS_HEADER_TABLE_SIZE setting. + HEADER_TABLE_SIZE = 0x01 + #: The byte that signals the SETTINGS_ENABLE_PUSH setting. + ENABLE_PUSH = 0x02 + #: The byte that signals the SETTINGS_MAX_CONCURRENT_STREAMS setting. + MAX_CONCURRENT_STREAMS = 0x03 + #: The byte that signals the SETTINGS_INITIAL_WINDOW_SIZE setting. + INITIAL_WINDOW_SIZE = 0x04 + #: The byte that signals the SETTINGS_MAX_FRAME_SIZE setting. + MAX_FRAME_SIZE = 0x05 + #: The byte that signals the SETTINGS_MAX_HEADER_LIST_SIZE setting. + MAX_HEADER_LIST_SIZE = 0x06 + + #: The byte that signals the SETTINGS_MAX_FRAME_SIZE setting. + #: .. deprecated:: 3.2.0 + #: Use :data:`MAX_FRAME_SIZE <SettingsFrame.MAX_FRAME_SIZE>` instead. + SETTINGS_MAX_FRAME_SIZE = MAX_FRAME_SIZE + #: The byte that signals the SETTINGS_MAX_HEADER_LIST_SIZE setting. + #: .. deprecated:: 3.2.0 + # Use :data:`MAX_HEADER_LIST_SIZE <SettingsFrame.MAX_HEADER_LIST_SIZE>` instead. + SETTINGS_MAX_HEADER_LIST_SIZE = MAX_HEADER_LIST_SIZE + + def __init__(self, stream_id=0, settings=None, **kwargs): + super(SettingsFrame, self).__init__(stream_id, **kwargs) + + if settings and "ACK" in kwargs.get("flags", ()): + raise ValueError("Settings must be empty if ACK flag is set.") + + #: A dictionary of the setting type byte to the value of the setting. + self.settings = settings or {} + + def serialize_body(self): + settings = [struct.pack("!HL", setting & 0xFF, value) + for setting, value in self.settings.items()] + return b''.join(settings) + + def parse_body(self, data): + for i in range(0, len(data), 6): + try: + name, value = struct.unpack("!HL", data[i:i+6]) + except struct.error: + raise InvalidFrameError("Invalid SETTINGS body") + + self.settings[name] = value + + self.body_len = len(data) + + +class PushPromiseFrame(Padding, Frame): + """ + The PUSH_PROMISE frame is used to notify the peer endpoint in advance of + streams the sender intends to initiate. + """ + #: The flags defined for PUSH_PROMISE frames. + defined_flags = [ + Flag('END_HEADERS', 0x04), + Flag('PADDED', 0x08) + ] + + #: The type byte defined for PUSH_PROMISE frames. + type = 0x05 + + stream_association = 'has-stream' + + def __init__(self, stream_id, promised_stream_id=0, data=b'', **kwargs): + super(PushPromiseFrame, self).__init__(stream_id, **kwargs) + + #: The stream ID that is promised by this frame. + self.promised_stream_id = promised_stream_id + + #: The HPACK-encoded header block for the simulated request on the new + #: stream. + self.data = data + + def serialize_body(self): + padding_data = self.serialize_padding_data() + padding = b'\0' * self.total_padding + data = struct.pack("!L", self.promised_stream_id) + return b''.join([padding_data, data, self.data, padding]) + + def parse_body(self, data): + padding_data_length = self.parse_padding_data(data) + + try: + self.promised_stream_id = struct.unpack( + "!L", data[padding_data_length:padding_data_length + 4] + )[0] + except struct.error: + raise InvalidFrameError("Invalid PUSH_PROMISE body") + + self.data = data[padding_data_length + 4:].tobytes() + self.body_len = len(data) + + if self.total_padding and self.total_padding >= self.body_len: + raise InvalidPaddingError("Padding is too long.") + + +class PingFrame(Frame): + """ + The PING frame is a mechanism for measuring a minimal round-trip time from + the sender, as well as determining whether an idle connection is still + functional. PING frames can be sent from any endpoint. + """ + #: The flags defined for PING frames. + defined_flags = [Flag('ACK', 0x01)] + + #: The type byte defined for PING frames. + type = 0x06 + + stream_association = 'no-stream' + + def __init__(self, stream_id=0, opaque_data=b'', **kwargs): + super(PingFrame, self).__init__(stream_id, **kwargs) + + #: The opaque data sent in this PING frame, as a bytestring. + self.opaque_data = opaque_data + + def serialize_body(self): + if len(self.opaque_data) > 8: + raise InvalidFrameError( + "PING frame may not have more than 8 bytes of data, got %s" % + self.opaque_data + ) + + data = self.opaque_data + data += b'\x00' * (8 - len(self.opaque_data)) + return data + + def parse_body(self, data): + if len(data) != 8: + raise InvalidFrameError( + "PING frame must have 8 byte length: got %s" % len(data) + ) + + self.opaque_data = data.tobytes() + self.body_len = len(data) + + +class GoAwayFrame(Frame): + """ + The GOAWAY frame informs the remote peer to stop creating streams on this + connection. It can be sent from the client or the server. Once sent, the + sender will ignore frames sent on new streams for the remainder of the + connection. + """ + #: The flags defined for GOAWAY frames. + defined_flags = [] + + #: The type byte defined for GOAWAY frames. + type = 0x07 + + stream_association = 'no-stream' + + def __init__(self, stream_id=0, last_stream_id=0, error_code=0, additional_data=b'', **kwargs): + super(GoAwayFrame, self).__init__(stream_id, **kwargs) + + #: The last stream ID definitely seen by the remote peer. + self.last_stream_id = last_stream_id + + #: The error code for connection teardown. + self.error_code = error_code + + #: Any additional data sent in the GOAWAY. + self.additional_data = additional_data + + def serialize_body(self): + data = struct.pack( + "!LL", + self.last_stream_id & 0x7FFFFFFF, + self.error_code + ) + data += self.additional_data + + return data + + def parse_body(self, data): + try: + self.last_stream_id, self.error_code = struct.unpack( + "!LL", data[:8] + ) + except struct.error: + raise InvalidFrameError("Invalid GOAWAY body.") + + self.body_len = len(data) + + if len(data) > 8: + self.additional_data = data[8:].tobytes() + + +class WindowUpdateFrame(Frame): + """ + The WINDOW_UPDATE frame is used to implement flow control. + + Flow control operates at two levels: on each individual stream and on the + entire connection. + + Both types of flow control are hop by hop; that is, only between the two + endpoints. Intermediaries do not forward WINDOW_UPDATE frames between + dependent connections. However, throttling of data transfer by any receiver + can indirectly cause the propagation of flow control information toward the + original sender. + """ + #: The flags defined for WINDOW_UPDATE frames. + defined_flags = [] + + #: The type byte defined for WINDOW_UPDATE frames. + type = 0x08 + + stream_association = 'either' + + def __init__(self, stream_id, window_increment=0, **kwargs): + super(WindowUpdateFrame, self).__init__(stream_id, **kwargs) + + #: The amount the flow control window is to be incremented. + self.window_increment = window_increment + + def serialize_body(self): + return struct.pack("!L", self.window_increment & 0x7FFFFFFF) + + def parse_body(self, data): + try: + self.window_increment = struct.unpack("!L", data)[0] + except struct.error: + raise InvalidFrameError("Invalid WINDOW_UPDATE body") + + self.body_len = len(data) + + +class HeadersFrame(Padding, Priority, Frame): + """ + The HEADERS frame carries name-value pairs. It is used to open a stream. + HEADERS frames can be sent on a stream in the "open" or "half closed + (remote)" states. + + The HeadersFrame class is actually basically a data frame in this + implementation, because of the requirement to control the sizes of frames. + A header block fragment that doesn't fit in an entire HEADERS frame needs + to be followed with CONTINUATION frames. From the perspective of the frame + building code the header block is an opaque data segment. + """ + #: The flags defined for HEADERS frames. + defined_flags = [ + Flag('END_STREAM', 0x01), + Flag('END_HEADERS', 0x04), + Flag('PADDED', 0x08), + Flag('PRIORITY', 0x20), + ] + + #: The type byte defined for HEADERS frames. + type = 0x01 + + stream_association = 'has-stream' + + def __init__(self, stream_id, data=b'', **kwargs): + super(HeadersFrame, self).__init__(stream_id, **kwargs) + + #: The HPACK-encoded header block. + self.data = data + + def serialize_body(self): + padding_data = self.serialize_padding_data() + padding = b'\0' * self.total_padding + + if 'PRIORITY' in self.flags: + priority_data = self.serialize_priority_data() + else: + priority_data = b'' + + return b''.join([padding_data, priority_data, self.data, padding]) + + def parse_body(self, data): + padding_data_length = self.parse_padding_data(data) + data = data[padding_data_length:] + + if 'PRIORITY' in self.flags: + priority_data_length = self.parse_priority_data(data) + else: + priority_data_length = 0 + + self.body_len = len(data) + self.data = data[priority_data_length:len(data)-self.total_padding].tobytes() + + if self.total_padding and self.total_padding >= self.body_len: + raise InvalidPaddingError("Padding is too long.") + + +class ContinuationFrame(Frame): + """ + The CONTINUATION frame is used to continue a sequence of header block + fragments. Any number of CONTINUATION frames can be sent on an existing + stream, as long as the preceding frame on the same stream is one of + HEADERS, PUSH_PROMISE or CONTINUATION without the END_HEADERS flag set. + + Much like the HEADERS frame, hyper treats this as an opaque data frame with + different flags and a different type. + """ + #: The flags defined for CONTINUATION frames. + defined_flags = [Flag('END_HEADERS', 0x04),] + + #: The type byte defined for CONTINUATION frames. + type = 0x09 + + stream_association = 'has-stream' + + def __init__(self, stream_id, data=b'', **kwargs): + super(ContinuationFrame, self).__init__(stream_id, **kwargs) + + #: The HPACK-encoded header block. + self.data = data + + def serialize_body(self): + return self.data + + def parse_body(self, data): + self.data = data.tobytes() + self.body_len = len(data) + + +Origin = collections.namedtuple('Origin', ['scheme', 'host', 'port']) + + +class AltSvcFrame(Frame): + """ + The ALTSVC frame is used to advertise alternate services that the current + host, or a different one, can understand. + """ + type = 0xA + + stream_association = 'no-stream' + + def __init__(self, stream_id=0, host=b'', port=0, protocol_id=b'', max_age=0, origin=None, **kwargs): + super(AltSvcFrame, self).__init__(stream_id, **kwargs) + + self.host = host + self.port = port + self.protocol_id = protocol_id + self.max_age = max_age + self.origin = origin + + def serialize_origin(self): + if self.origin is not None: + if self.origin.port is None: + hostport = self.origin.host + else: + hostport = self.origin.host + b':' + str(self.origin.port).encode('ascii') + return self.origin.scheme + b'://' + hostport + return b'' + + def parse_origin(self, data): + if len(data) > 0: + data = data.tobytes() + scheme, hostport = data.split(b'://') + host, _, port = hostport.partition(b':') + self.origin = Origin(scheme=scheme, host=host, + port=int(port) if len(port) > 0 else None) + + def serialize_body(self): + first = struct.pack("!LHxB", self.max_age, self.port, len(self.protocol_id)) + host_length = struct.pack("!B", len(self.host)) + return b''.join([first, self.protocol_id, host_length, self.host, + self.serialize_origin()]) + + def parse_body(self, data): + try: + self.body_len = len(data) + self.max_age, self.port, protocol_id_length = struct.unpack( + "!LHxB", data[:8] + ) + pos = 8 + self.protocol_id = data[pos:pos+protocol_id_length].tobytes() + pos += protocol_id_length + host_length = struct.unpack("!B", data[pos:pos+1])[0] + pos += 1 + self.host = data[pos:pos+host_length].tobytes() + pos += host_length + self.parse_origin(data[pos:]) + except (struct.error, ValueError): + raise InvalidFrameError("Invalid ALTSVC frame body.") + + +class BlockedFrame(Frame): + """ + The BLOCKED frame indicates that the sender is unable to send data due to a + closed flow control window. + + The BLOCKED frame is used to provide feedback about the performance of flow + control for the purposes of performance tuning and debugging. The BLOCKED + frame can be sent by a peer when flow controlled data cannot be sent due to + the connection- or stream-level flow control. This frame MUST NOT be sent + if there are other reasons preventing data from being sent, either a lack + of available data, or the underlying transport being blocked. + """ + type = 0x0B + + stream_association = 'both' + + defined_flags = [] + + def serialize_body(self): + return b'' + + def parse_body(self, data): + pass + + +_FRAME_CLASSES = [ + DataFrame, + HeadersFrame, + PriorityFrame, + RstStreamFrame, + SettingsFrame, + PushPromiseFrame, + PingFrame, + GoAwayFrame, + WindowUpdateFrame, + ContinuationFrame, + AltSvcFrame, + BlockedFrame +] +#: FRAMES maps the type byte for each frame to the class used to represent that +#: frame. +FRAMES = {cls.type: cls for cls in _FRAME_CLASSES} diff --git a/vinetrimmer/vendor/pymp4/__init__.py b/vinetrimmer/vendor/pymp4/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/vinetrimmer/vendor/pymp4/__init__.py @@ -0,0 +1 @@ + diff --git a/vinetrimmer/vendor/pymp4/cli.py b/vinetrimmer/vendor/pymp4/cli.py new file mode 100644 index 0000000..426b8f6 --- /dev/null +++ b/vinetrimmer/vendor/pymp4/cli.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +from __future__ import print_function +import io +import logging +import argparse + +from pymp4.parser import Box +from construct import setglobalfullprinting + +log = logging.getLogger(__name__) +setglobalfullprinting(True) + + +def dump(): + parser = argparse.ArgumentParser(description='Dump all the boxes from an MP4 file') + parser.add_argument("input_file", type=argparse.FileType("rb"), metavar="FILE", help="Path to the MP4 file to open") + + args = parser.parse_args() + + fd = args.input_file + fd.seek(0, io.SEEK_END) + eof = fd.tell() + fd.seek(0) + + while fd.tell() < eof: + box = Box.parse_stream(fd) + print(box) diff --git a/vinetrimmer/vendor/pymp4/exceptions.py b/vinetrimmer/vendor/pymp4/exceptions.py new file mode 100644 index 0000000..7d3015e --- /dev/null +++ b/vinetrimmer/vendor/pymp4/exceptions.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +""" + Copyright 2016 beardypig + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + + +class BoxNotFound(Exception): + pass diff --git a/vinetrimmer/vendor/pymp4/parser.py b/vinetrimmer/vendor/pymp4/parser.py new file mode 100644 index 0000000..e3ab8b2 --- /dev/null +++ b/vinetrimmer/vendor/pymp4/parser.py @@ -0,0 +1,889 @@ +#!/usr/bin/env python +""" + Copyright 2016 beardypig + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +import logging +from uuid import UUID + +from construct import * +import construct.core +from construct.lib import * + +log = logging.getLogger(__name__) + +UNITY_MATRIX = [0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000] + + +class PrefixedIncludingSize(Subconstruct): + __slots__ = ["name", "lengthfield", "subcon"] + + def __init__(self, lengthfield, subcon): + super(PrefixedIncludingSize, self).__init__(subcon) + self.lengthfield = lengthfield + + def _parse(self, stream, context, path): + try: + lengthfield_size = self.lengthfield.sizeof() + length = self.lengthfield._parse(stream, context, path) + except SizeofError: + offset_start = stream.tell() + length = self.lengthfield._parse(stream, context, path) + lengthfield_size = stream.tell() - offset_start + + stream2 = BoundBytesIO(stream, length - lengthfield_size) + obj = self.subcon._parse(stream2, context, path) + return obj + + def _build(self, obj, stream, context, path): + try: + # needs to be both fixed size, seekable and tellable (third not checked) + self.lengthfield.sizeof() + if not stream.seekable: + raise SizeofError + offset_start = stream.tell() + self.lengthfield._build(0, stream, context, path) + self.subcon._build(obj, stream, context, path) + offset_end = stream.tell() + stream.seek(offset_start) + self.lengthfield._build(offset_end - offset_start, stream, context, path) + stream.seek(offset_end) + except SizeofError: + data = self.subcon.build(obj, context) + sl, p_sl = 0, 0 + dlen = len(data) + # do..while + i = 0 + while True: + i += 1 + p_sl = sl + sl = len(self.lengthfield.build(dlen + sl)) + if p_sl == sl: break + + self.lengthfield._build(dlen + sl, stream, context, path) + else: + self.lengthfield._build(len(data), stream, context, path) + construct.core._write_stream(stream, len(data), data) + + def _sizeof(self, context, path): + return self.lengthfield._sizeof(context, path) + self.subcon._sizeof(context, path) + + +# Header box + +FileTypeBox = Struct( + "type" / Const(b"ftyp"), + "major_brand" / String(4), + "minor_version" / Int32ub, + "compatible_brands" / GreedyRange(String(4)), +) + +SegmentTypeBox = Struct( + "type" / Const(b"styp"), + "major_brand" / String(4), + "minor_version" / Int32ub, + "compatible_brands" / GreedyRange(String(4)), +) + +# Catch find boxes + +RawBox = Struct( + "type" / String(4, padchar=b" ", paddir="right"), + "data" / Default(GreedyBytes, b"") +) + +FreeBox = Struct( + "type" / Const(b"free"), + "data" / GreedyBytes +) + +SkipBox = Struct( + "type" / Const(b"skip"), + "data" / GreedyBytes +) + +# Movie boxes, contained in a moov Box + +MovieHeaderBox = Struct( + "type" / Const(b"mvhd"), + "version" / Default(Int8ub, 0), + "flags" / Default(Int24ub, 0), + Embedded(Switch(this.version, { + 1: Struct( + "creation_time" / Default(Int64ub, 0), + "modification_time" / Default(Int64ub, 0), + "timescale" / Default(Int32ub, 10000000), + "duration" / Int64ub + ), + 0: Struct( + "creation_time" / Default(Int32ub, 0), + "modification_time" / Default(Int32ub, 0), + "timescale" / Default(Int32ub, 10000000), + "duration" / Int32ub, + ), + })), + "rate" / Default(Int32sb, 65536), + "volume" / Default(Int16sb, 256), + # below could be just Padding(10) but why not + Const(Int16ub, 0), + Const(Int32ub, 0), + Const(Int32ub, 0), + "matrix" / Default(Int32sb[9], UNITY_MATRIX), + "pre_defined" / Default(Int32ub[6], [0] * 6), + "next_track_ID" / Default(Int32ub, 0xffffffff) +) + +# Track boxes, contained in trak box + +TrackHeaderBox = Struct( + "type" / Const(b"tkhd"), + "version" / Default(Int8ub, 0), + "flags" / Default(Int24ub, 1), + Embedded(Switch(this.version, { + 1: Struct( + "creation_time" / Default(Int64ub, 0), + "modification_time" / Default(Int64ub, 0), + "track_ID" / Default(Int32ub, 1), + Padding(4), + "duration" / Default(Int64ub, 0), + ), + 0: Struct( + "creation_time" / Default(Int32ub, 0), + "modification_time" / Default(Int32ub, 0), + "track_ID" / Default(Int32ub, 1), + Padding(4), + "duration" / Default(Int32ub, 0), + ), + })), + Padding(8), + "layer" / Default(Int16sb, 0), + "alternate_group" / Default(Int16sb, 0), + "volume" / Default(Int16sb, 0), + Padding(2), + "matrix" / Default(Array(9, Int32sb), UNITY_MATRIX), + "width" / Default(Int32ub, 0), + "height" / Default(Int32ub, 0), +) + +HDSSegmentBox = Struct( + "type" / Const(b"abst"), + "version" / Default(Int8ub, 0), + "flags" / Default(Int24ub, 0), + "info_version" / Int32ub, + EmbeddedBitStruct( + Padding(1), + "profile" / Flag, + "live" / Flag, + "update" / Flag, + Padding(4) + ), + "time_scale" / Int32ub, + "current_media_time" / Int64ub, + "smpte_time_code_offset" / Int64ub, + "movie_identifier" / CString(), + "server_entry_table" / PrefixedArray(Int8ub, CString()), + "quality_entry_table" / PrefixedArray(Int8ub, CString()), + "drm_data" / CString(), + "metadata" / CString(), + "segment_run_table" / PrefixedArray(Int8ub, LazyBound(lambda x: Box)), + "fragment_run_table" / PrefixedArray(Int8ub, LazyBound(lambda x: Box)) +) + +HDSSegmentRunBox = Struct( + "type" / Const(b"asrt"), + "version" / Default(Int8ub, 0), + "flags" / Default(Int24ub, 0), + "quality_entry_table" / PrefixedArray(Int8ub, CString()), + "segment_run_enteries" / PrefixedArray(Int32ub, Struct( + "first_segment" / Int32ub, + "fragments_per_segment" / Int32ub + )) +) + +HDSFragmentRunBox = Struct( + "type" / Const(b"afrt"), + "version" / Default(Int8ub, 0), + "flags" / BitStruct( + Padding(23), + "update" / Flag + ), + "time_scale" / Int32ub, + "quality_entry_table" / PrefixedArray(Int8ub, CString()), + "fragment_run_enteries" / PrefixedArray(Int32ub, Struct( + "first_fragment" / Int32ub, + "first_fragment_timestamp" / Int64ub, + "fragment_duration" / Int32ub, + "discontinuity" / If(this.fragment_duration == 0, Int8ub) + )) +) + + +# Boxes contained by Media Box + +class ISO6392TLanguageCode(Adapter): + def _decode(self, obj, context): + """ + Get the python representation of the obj + """ + return b''.join(map(int2byte, [c + 0x60 for c in bytearray(obj)])).decode("utf8") + + def _encode(self, obj, context): + """ + Get the bytes representation of the obj + """ + return [c - 0x60 for c in bytearray(obj.encode("utf8"))] + + +MediaHeaderBox = Struct( + "type" / Const(b"mdhd"), + "version" / Default(Int8ub, 0), + "flags" / Const(Int24ub, 0), + "creation_time" / IfThenElse(this.version == 1, Int64ub, Int32ub), + "modification_time" / IfThenElse(this.version == 1, Int64ub, Int32ub), + "timescale" / Int32ub, + "duration" / IfThenElse(this.version == 1, Int64ub, Int32ub), + Embedded(BitStruct( + Padding(1), + "language" / ISO6392TLanguageCode(BitsInteger(5)[3]), + )), + Padding(2, pattern=b"\x00"), +) + +HandlerReferenceBox = Struct( + "type" / Const(b"hdlr"), + "version" / Const(Int8ub, 0), + "flags" / Const(Int24ub, 0), + Padding(4, pattern=b"\x00"), + "handler_type" / String(4), + Padding(12, pattern=b"\x00"), # Int32ub[3] + "name" / CString(encoding="utf8") +) + +# Boxes contained by Media Info Box + +VideoMediaHeaderBox = Struct( + "type" / Const(b"vmhd"), + "version" / Default(Int8ub, 0), + "flags" / Const(Int24ub, 1), + "graphics_mode" / Default(Int16ub, 0), + "opcolor" / Struct( + "red" / Default(Int16ub, 0), + "green" / Default(Int16ub, 0), + "blue" / Default(Int16ub, 0), + ), +) + +DataEntryUrlBox = PrefixedIncludingSize(Int32ub, Struct( + "type" / Const(b"url "), + "version" / Const(Int8ub, 0), + "flags" / BitStruct( + Padding(23), "self_contained" / Rebuild(Flag, ~this._.location) + ), + "location" / If(~this.flags.self_contained, CString(encoding="utf8")), +)) + +DataEntryUrnBox = PrefixedIncludingSize(Int32ub, Struct( + "type" / Const(b"urn "), + "version" / Const(Int8ub, 0), + "flags" / BitStruct( + Padding(23), "self_contained" / Rebuild(Flag, ~(this._.name & this._.location)) + ), + "name" / If(this.flags == 0, CString(encoding="utf8")), + "location" / If(this.flags == 0, CString(encoding="utf8")), +)) + +DataReferenceBox = Struct( + "type" / Const(b"dref"), + "version" / Const(Int8ub, 0), + "flags" / Default(Int24ub, 0), + "data_entries" / PrefixedArray(Int32ub, Select(DataEntryUrnBox, DataEntryUrlBox)), +) + +# Sample Table boxes (stbl) + +MP4ASampleEntryBox = Struct( + "version" / Default(Int16ub, 0), + "revision" / Const(Int16ub, 0), + "vendor" / Const(Int32ub, 0), + "channels" / Default(Int16ub, 2), + "bits_per_sample" / Default(Int16ub, 16), + "compression_id" / Default(Int16sb, 0), + "packet_size" / Const(Int16ub, 0), + "sampling_rate" / Int16ub, + Padding(2) +) + + +class MaskedInteger(Adapter): + def _decode(self, obj, context): + return obj & 0x1F + + def _encode(self, obj, context): + return obj & 0x1F + + +AAVC = Struct( + "version" / Const(Int8ub, 1), + "profile" / Int8ub, + "compatibility" / Int8ub, + "level" / Int8ub, + EmbeddedBitStruct( + Padding(6, pattern=b'\x01'), + "nal_unit_length_field" / Default(BitsInteger(2), 3), + ), + "sps" / Default(PrefixedArray(MaskedInteger(Int8ub), PascalString(Int16ub)), []), + "pps" / Default(PrefixedArray(Int8ub, PascalString(Int16ub)), []) +) + +HVCC = Struct( + EmbeddedBitStruct( + "version" / Const(BitsInteger(8), 1), + "profile_space" / BitsInteger(2), + "general_tier_flag" / BitsInteger(1), + "general_profile" / BitsInteger(5), + "general_profile_compatibility_flags" / BitsInteger(32), + "general_constraint_indicator_flags" / BitsInteger(48), + "general_level" / BitsInteger(8), + Padding(4, pattern=b'\xff'), + "min_spatial_segmentation" / BitsInteger(12), + Padding(6, pattern=b'\xff'), + "parallelism_type" / BitsInteger(2), + Padding(6, pattern=b'\xff'), + "chroma_format" / BitsInteger(2), + Padding(5, pattern=b'\xff'), + "luma_bit_depth" / BitsInteger(3), + Padding(5, pattern=b'\xff'), + "chroma_bit_depth" / BitsInteger(3), + "average_frame_rate" / BitsInteger(16), + "constant_frame_rate" / BitsInteger(2), + "num_temporal_layers" / BitsInteger(3), + "temporal_id_nested" / BitsInteger(1), + "nalu_length_size" / BitsInteger(2), + ), + # TODO: parse NALUs + "raw_bytes" / GreedyBytes +) + +AVC1SampleEntryBox = Struct( + "version" / Default(Int16ub, 0), + "revision" / Const(Int16ub, 0), + "vendor" / Default(String(4, padchar=b" "), b"brdy"), + "temporal_quality" / Default(Int32ub, 0), + "spatial_quality" / Default(Int32ub, 0), + "width" / Int16ub, + "height" / Int16ub, + "horizontal_resolution" / Default(Int16ub, 72), # TODO: actually a fixed point decimal + Padding(2), + "vertical_resolution" / Default(Int16ub, 72), # TODO: actually a fixed point decimal + Padding(2), + "data_size" / Const(Int32ub, 0), + "frame_count" / Default(Int16ub, 1), + "compressor_name" / Default(String(32, padchar=b" "), ""), + "depth" / Default(Int16ub, 24), + "color_table_id" / Default(Int16sb, -1), + "avc_data" / PrefixedIncludingSize(Int32ub, Struct( + "type" / String(4, padchar=b" ", paddir="right"), + Embedded(Switch(this.type, { + b"avcC": AAVC, + b"hvcC": HVCC, + }, Struct("data" / GreedyBytes))) + )), + "sample_info" / LazyBound(lambda _: GreedyRange(Box)) +) + +SampleEntryBox = PrefixedIncludingSize(Int32ub, Struct( + "format" / String(4, padchar=b" ", paddir="right"), + Padding(6, pattern=b"\x00"), + "data_reference_index" / Default(Int16ub, 1), + Embedded(Switch(this.format, { + b"ec-3": MP4ASampleEntryBox, + b"mp4a": MP4ASampleEntryBox, + b"enca": MP4ASampleEntryBox, + b"avc1": AVC1SampleEntryBox, + b"encv": AVC1SampleEntryBox, + b"wvtt": Struct("children" / LazyBound(lambda ctx: GreedyRange(Box))) + }, Struct("data" / GreedyBytes))) +)) + +BitRateBox = Struct( + "type" / Const(b"btrt"), + "bufferSizeDB" / Int32ub, + "maxBitrate" / Int32ub, + "avgBirate" / Int32ub, +) + +SampleDescriptionBox = Struct( + "type" / Const(b"stsd"), + "version" / Default(Int8ub, 0), + "flags" / Const(Int24ub, 0), + "entries" / PrefixedArray(Int32ub, SampleEntryBox) +) + +SampleSizeBox = Struct( + "type" / Const(b"stsz"), + "version" / Int8ub, + "flags" / Const(Int24ub, 0), + "sample_size" / Int32ub, + "sample_count" / Int32ub, + "entry_sizes" / If(this.sample_size == 0, Array(this.sample_count, Int32ub)) +) + +SampleSizeBox2 = Struct( + "type" / Const(b"stz2"), + "version" / Int8ub, + "flags" / Const(Int24ub, 0), + Padding(3, pattern=b"\x00"), + "field_size" / Int8ub, + "sample_count" / Int24ub, + "entries" / Array(this.sample_count, Struct( + "entry_size" / LazyBound(lambda ctx: globals()["Int%dub" % ctx.field_size]) + )) +) + +SampleDegradationPriorityBox = Struct( + "type" / Const(b"stdp"), + "version" / Const(Int8ub, 0), + "flags" / Const(Int24ub, 0), +) + +TimeToSampleBox = Struct( + "type" / Const(b"stts"), + "version" / Const(Int8ub, 0), + "flags" / Const(Int24ub, 0), + "entries" / Default(PrefixedArray(Int32ub, Struct( + "sample_count" / Int32ub, + "sample_delta" / Int32ub, + )), []) +) + +SyncSampleBox = Struct( + "type" / Const(b"stss"), + "version" / Const(Int8ub, 0), + "flags" / Const(Int24ub, 0), + "entries" / Default(PrefixedArray(Int32ub, Struct( + "sample_number" / Int32ub, + )), []) +) + +SampleToChunkBox = Struct( + "type" / Const(b"stsc"), + "version" / Const(Int8ub, 0), + "flags" / Const(Int24ub, 0), + "entries" / Default(PrefixedArray(Int32ub, Struct( + "first_chunk" / Int32ub, + "samples_per_chunk" / Int32ub, + "sample_description_index" / Int32ub, + )), []) +) + +ChunkOffsetBox = Struct( + "type" / Const(b"stco"), + "version" / Const(Int8ub, 0), + "flags" / Const(Int24ub, 0), + "entries" / Default(PrefixedArray(Int32ub, Struct( + "chunk_offset" / Int32ub, + )), []) +) + +ChunkLargeOffsetBox = Struct( + "type" / Const(b"co64"), + "version" / Const(Int8ub, 0), + "flags" / Const(Int24ub, 0), + "entries" / PrefixedArray(Int32ub, Struct( + "chunk_offset" / Int64ub, + )) +) + +# Movie Fragment boxes, contained in moof box + +MovieFragmentHeaderBox = Struct( + "type" / Const(b"mfhd"), + "version" / Const(Int8ub, 0), + "flags" / Const(Int24ub, 0), + "sequence_number" / Int32ub +) + +TrackFragmentBaseMediaDecodeTimeBox = Struct( + "type" / Const(b"tfdt"), + "version" / Int8ub, + "flags" / Const(Int24ub, 0), + "baseMediaDecodeTime" / Switch(this.version, {1: Int64ub, 0: Int32ub}) +) + +TrackSampleFlags = BitStruct( + Padding(4), + "is_leading" / Default(Enum(BitsInteger(2), UNKNOWN=0, LEADINGDEP=1, NOTLEADING=2, LEADINGNODEP=3, default=0), 0), + "sample_depends_on" / Default(Enum(BitsInteger(2), UNKNOWN=0, DEPENDS=1, NOTDEPENDS=2, RESERVED=3, default=0), 0), + "sample_is_depended_on" / Default(Enum(BitsInteger(2), UNKNOWN=0, NOTDISPOSABLE=1, DISPOSABLE=2, RESERVED=3, default=0), 0), + "sample_has_redundancy" / Default(Enum(BitsInteger(2), UNKNOWN=0, REDUNDANT=1, NOTREDUNDANT=2, RESERVED=3, default=0), 0), + "sample_padding_value" / Default(BitsInteger(3), 0), + "sample_is_non_sync_sample" / Default(Flag, False), + "sample_degradation_priority" / Default(BitsInteger(16), 0), +) + +TrackRunBox = Struct( + "type" / Const(b"trun"), + "version" / Int8ub, + "flags" / BitStruct( + Padding(12), + "sample_composition_time_offsets_present" / Flag, + "sample_flags_present" / Flag, + "sample_size_present" / Flag, + "sample_duration_present" / Flag, + Padding(5), + "first_sample_flags_present" / Flag, + Padding(1), + "data_offset_present" / Flag, + ), + "sample_count" / Int32ub, + "data_offset" / Default(If(this.flags.data_offset_present, Int32sb), None), + "first_sample_flags" / Default(If(this.flags.first_sample_flags_present, Int32ub), None), + "sample_info" / Array(this.sample_count, Struct( + "sample_duration" / If(this._.flags.sample_duration_present, Int32ub), + "sample_size" / If(this._.flags.sample_size_present, Int32ub), + "sample_flags" / If(this._.flags.sample_flags_present, TrackSampleFlags), + "sample_composition_time_offsets" / If( + this._.flags.sample_composition_time_offsets_present, + IfThenElse(this._.version == 0, Int32ub, Int32sb) + ), + )) +) + +TrackFragmentHeaderBox = Struct( + "type" / Const(b"tfhd"), + "version" / Int8ub, + "flags" / BitStruct( + Padding(6), + "default_base_is_moof" / Flag, + "duration_is_empty" / Flag, + Padding(10), + "default_sample_flags_present" / Flag, + "default_sample_size_present" / Flag, + "default_sample_duration_present" / Flag, + Padding(1), + "sample_description_index_present" / Flag, + "base_data_offset_present" / Flag, + ), + "track_ID" / Int32ub, + "base_data_offset" / Default(If(this.flags.base_data_offset_present, Int64ub), None), + "sample_description_index" / Default(If(this.flags.sample_description_index_present, Int32ub), None), + "default_sample_duration" / Default(If(this.flags.default_sample_duration_present, Int32ub), None), + "default_sample_size" / Default(If(this.flags.default_sample_size_present, Int32ub), None), + "default_sample_flags" / Default(If(this.flags.default_sample_flags_present, TrackSampleFlags), None), +) + +MovieExtendsHeaderBox = Struct( + "type" / Const(b"mehd"), + "version" / Default(Int8ub, 0), + "flags" / Const(Int24ub, 0), + "fragment_duration" / IfThenElse(this.version == 1, + Default(Int64ub, 0), + Default(Int32ub, 0)) +) + +TrackExtendsBox = Struct( + "type" / Const(b"trex"), + "version" / Const(Int8ub, 0), + "flags" / Const(Int24ub, 0), + "track_ID" / Int32ub, + "default_sample_description_index" / Default(Int32ub, 1), + "default_sample_duration" / Default(Int32ub, 0), + "default_sample_size" / Default(Int32ub, 0), + "default_sample_flags" / Default(TrackSampleFlags, Container()), +) + +SegmentIndexBox = Struct( + "type" / Const(b"sidx"), + "version" / Int8ub, + "flags" / Const(Int24ub, 0), + "reference_ID" / Int32ub, + "timescale" / Int32ub, + "earliest_presentation_time" / IfThenElse(this.version == 0, Int32ub, Int64ub), + "first_offset" / IfThenElse(this.version == 0, Int32ub, Int64ub), + Padding(2), + "reference_count" / Int16ub, + "references" / Array(this.reference_count, BitStruct( + "reference_type" / Enum(BitsInteger(1), INDEX=1, MEDIA=0), + "referenced_size" / BitsInteger(31), + "segment_duration" / BitsInteger(32), + "starts_with_SAP" / Flag, + "SAP_type" / BitsInteger(3), + "SAP_delta_time" / BitsInteger(28), + )) +) + +SampleAuxiliaryInformationSizesBox = Struct( + "type" / Const(b"saiz"), + "version" / Const(Int8ub, 0), + "flags" / BitStruct( + Padding(23), + "has_aux_info_type" / Flag, + ), + # Optional fields + "aux_info_type" / Default(If(this.flags.has_aux_info_type, Int32ub), None), + "aux_info_type_parameter" / Default(If(this.flags.has_aux_info_type, Int32ub), None), + "default_sample_info_size" / Int8ub, + "sample_count" / Int32ub, + # only if sample default_sample_info_size is 0 + "sample_info_sizes" / If(this.default_sample_info_size == 0, + Array(this.sample_count, Int8ub)) +) + +SampleAuxiliaryInformationOffsetsBox = Struct( + "type" / Const(b"saio"), + "version" / Int8ub, + "flags" / BitStruct( + Padding(23), + "has_aux_info_type" / Flag, + ), + # Optional fields + "aux_info_type" / Default(If(this.flags.has_aux_info_type, Int32ub), None), + "aux_info_type_parameter" / Default(If(this.flags.has_aux_info_type, Int32ub), None), + # Short offsets in version 0, long in version 1 + "offsets" / PrefixedArray(Int32ub, Switch(this.version, {0: Int32ub, 1: Int64ub})) +) + +# Movie data box + +MovieDataBox = Struct( + "type" / Const(b"mdat"), + "data" / GreedyBytes +) + +# Media Info Box + +SoundMediaHeaderBox = Struct( + "type" / Const(b"smhd"), + "version" / Const(Int8ub, 0), + "flags" / Const(Int24ub, 0), + "balance" / Default(Int16sb, 0), + "reserved" / Const(Int16ub, 0) +) + + +# DASH Boxes + +class UUIDBytes(Adapter): + def _decode(self, obj, context): + return UUID(bytes=obj) + + def _encode(self, obj, context): + return obj.bytes + + +ProtectionSystemHeaderBox = Struct( + "type" / If(this._.type != b"uuid", Const(b"pssh")), + "version" / Rebuild(Int8ub, lambda ctx: 1 if (hasattr(ctx, "key_IDs") and ctx.key_IDs) else 0), + "flags" / Const(Int24ub, 0), + "system_ID" / UUIDBytes(Bytes(16)), + "key_IDs" / Default(If(this.version == 1, + PrefixedArray(Int32ub, UUIDBytes(Bytes(16)))), + None), + "init_data" / Prefixed(Int32ub, GreedyBytes) +) + +TrackEncryptionBox = Struct( + "type" / If(this._.type != b"uuid", Const(b"tenc")), + "version" / Default(OneOf(Int8ub, (0, 1)), 0), + "flags" / Default(Int24ub, 0), + "_reserved" / Const(Int8ub, 0), + "default_byte_blocks" / Default(IfThenElse( + this.version > 0, + BitStruct( + # count of encrypted blocks in the protection pattern, where each block is 16-bytes + "crypt" / Nibble, + # count of unencrypted blocks in the protection pattern + "skip" / Nibble + ), + Const(Int8ub, 0) + ), 0), + "is_encrypted" / OneOf(Int8ub, (0, 1)), + "iv_size" / OneOf(Int8ub, (0, 8, 16)), + "key_ID" / UUIDBytes(Bytes(16)), + "constant_iv" / Default(If( + this.is_encrypted and this.iv_size == 0, + PrefixedArray(Int8ub, Byte) + ), None) +) + +SampleEncryptionBox = Struct( + "type" / If(this._.type != b"uuid", Const(b"senc")), + "version" / Const(Int8ub, 0), + "flags" / BitStruct( + Padding(22), + "has_subsample_encryption_info" / Flag, + Padding(1) + ), + "sample_encryption_info" / PrefixedArray(Int32ub, Struct( + "iv" / Bytes(8), + # include the sub sample encryption information + "subsample_encryption_info" / Default(If(this.flags.has_subsample_encryption_info, PrefixedArray(Int16ub, Struct( + "clear_bytes" / Int16ub, + "cipher_bytes" / Int32ub + ))), None) + )) +) + +OriginalFormatBox = Struct( + "type" / Const(b"frma"), + "original_format" / Default(String(4), b"avc1") +) + +SchemeTypeBox = Struct( + "type" / Const(b"schm"), + "version" / Default(Int8ub, 0), + "flags" / Default(Int24ub, 0), + "scheme_type" / Default(String(4), b"cenc"), + "scheme_version" / Default(Int32ub, 0x00010000), + "schema_uri" / Default(If(this.flags & 1 == 1, CString()), None) +) + +ProtectionSchemeInformationBox = Struct( + "type" / Const(b"sinf"), + # TODO: define which children are required 'schm', 'schi' and 'tenc' + "children" / LazyBound(lambda _: GreedyRange(Box)) +) + +# PIFF boxes + +UUIDBox = Struct( + "type" / Const(b"uuid"), + "extended_type" / UUIDBytes(Bytes(16)), + "data" / Switch(this.extended_type, { + UUID("A2394F52-5A9B-4F14-A244-6C427C648DF4"): SampleEncryptionBox, + UUID("D08A4F18-10F3-4A82-B6C8-32D8ABA183D3"): ProtectionSystemHeaderBox, + UUID("8974DBCE-7BE7-4C51-84F9-7148F9882554"): TrackEncryptionBox + }, GreedyBytes) +) + +# WebVTT boxes + +CueIDBox = Struct( + "type" / Const(b"iden"), + "cue_id" / GreedyString("utf8") +) + +CueSettingsBox = Struct( + "type" / Const(b"sttg"), + "settings" / GreedyString("utf8") +) + +CuePayloadBox = Struct( + "type" / Const(b"payl"), + "cue_text" / GreedyString("utf8") +) + +WebVTTConfigurationBox = Struct( + "type" / Const(b"vttC"), + "config" / GreedyString("utf8") +) + +WebVTTSourceLabelBox = Struct( + "type" / Const(b"vlab"), + "label" / GreedyString("utf8") +) + +ContainerBoxLazy = LazyBound(lambda ctx: ContainerBox) + + +class TellMinusSizeOf(Subconstruct): + def __init__(self, subcon): + super(TellMinusSizeOf, self).__init__(subcon) + self.flagbuildnone = True + + def _parse(self, stream, context, path): + return stream.tell() - self.subcon.sizeof(context) + + def _build(self, obj, stream, context, path): + return b"" + + def sizeof(self, context=None, **kw): + return 0 + + +Box = PrefixedIncludingSize(Int32ub, Struct( + "offset" / TellMinusSizeOf(Int32ub), + "type" / Peek(String(4, padchar=b" ", paddir="right")), + Embedded(Switch(this.type, { + b"ftyp": FileTypeBox, + b"styp": SegmentTypeBox, + b"mvhd": MovieHeaderBox, + b"moov": ContainerBoxLazy, + b"moof": ContainerBoxLazy, + b"mfhd": MovieFragmentHeaderBox, + b"tfdt": TrackFragmentBaseMediaDecodeTimeBox, + b"trun": TrackRunBox, + b"tfhd": TrackFragmentHeaderBox, + b"traf": ContainerBoxLazy, + b"mvex": ContainerBoxLazy, + b"mehd": MovieExtendsHeaderBox, + b"trex": TrackExtendsBox, + b"trak": ContainerBoxLazy, + b"mdia": ContainerBoxLazy, + b"tkhd": TrackHeaderBox, + b"mdat": MovieDataBox, + b"free": FreeBox, + b"skip": SkipBox, + b"mdhd": MediaHeaderBox, + b"hdlr": HandlerReferenceBox, + b"minf": ContainerBoxLazy, + b"vmhd": VideoMediaHeaderBox, + b"dinf": ContainerBoxLazy, + b"dref": DataReferenceBox, + b"stbl": ContainerBoxLazy, + b"stsd": SampleDescriptionBox, + b"stsz": SampleSizeBox, + b"stz2": SampleSizeBox2, + b"stts": TimeToSampleBox, + b"stss": SyncSampleBox, + b"stsc": SampleToChunkBox, + b"stco": ChunkOffsetBox, + b"co64": ChunkLargeOffsetBox, + b"smhd": SoundMediaHeaderBox, + b"sidx": SegmentIndexBox, + b"saiz": SampleAuxiliaryInformationSizesBox, + b"saio": SampleAuxiliaryInformationOffsetsBox, + b"btrt": BitRateBox, + # dash + b"tenc": TrackEncryptionBox, + b"pssh": ProtectionSystemHeaderBox, + b"senc": SampleEncryptionBox, + b"sinf": ProtectionSchemeInformationBox, + b"frma": OriginalFormatBox, + b"schm": SchemeTypeBox, + b"schi": ContainerBoxLazy, + # piff + b"uuid": UUIDBox, + # HDS boxes + b'abst': HDSSegmentBox, + b'asrt': HDSSegmentRunBox, + b'afrt': HDSFragmentRunBox, + # WebVTT + b"vttC": WebVTTConfigurationBox, + b"vlab": WebVTTSourceLabelBox, + b"vttc": ContainerBoxLazy, + b"vttx": ContainerBoxLazy, + b"iden": CueIDBox, + b"sttg": CueSettingsBox, + b"payl": CuePayloadBox + }, default=RawBox)), + "end" / Tell +)) + +ContainerBox = Struct( + "type" / String(4, padchar=b" ", paddir="right"), + "children" / GreedyRange(Box) +) + +MP4 = GreedyRange(Box) diff --git a/vinetrimmer/vendor/pymp4/util.py b/vinetrimmer/vendor/pymp4/util.py new file mode 100644 index 0000000..73a3eb8 --- /dev/null +++ b/vinetrimmer/vendor/pymp4/util.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +""" + Copyright 2016-2019 beardypig + Copyright 2017-2019 truedread + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +import logging + +from pymp4.exceptions import BoxNotFound + +log = logging.getLogger(__name__) + + +class BoxUtil(object): + @classmethod + def first(cls, box, type_): + if box.type == type_: + return box + if hasattr(box, "children"): + for sbox in box.children: + try: + return cls.first(sbox, type_) + except BoxNotFound: + # ignore the except when the box is not found in sub-boxes + pass + + raise BoxNotFound("could not find box of type: {}".format(type_)) + + @classmethod + def index(cls, box, type_): + if hasattr(box, "children"): + for i, box in enumerate(box.children): + if box.type == type_: + return i + + @classmethod + def find(cls, box, type_): + if box.type == type_: + yield box + elif hasattr(box, "children"): + for sbox in box.children: + for fbox in cls.find(sbox, type_): + yield fbox + + @classmethod + def find_extended(cls, box, extended_type_): + if hasattr(box, "extended_type"): + if box.extended_type == extended_type_: + yield box + elif hasattr(box, "children"): + for sbox in box.children: + for fbox in cls.find_extended(sbox, extended_type_): + yield fbox + elif hasattr(box, "children"): + for sbox in box.children: + for fbox in cls.find_extended(sbox, extended_type_): + yield fbox diff --git a/vinetrimmer/vinetrimmer.py b/vinetrimmer/vinetrimmer.py new file mode 100644 index 0000000..d31f166 --- /dev/null +++ b/vinetrimmer/vinetrimmer.py @@ -0,0 +1,78 @@ +import logging +import os +import sys +from datetime import datetime + +import click +import coloredlogs + +from vinetrimmer.config import directories, filenames # isort: split +from vinetrimmer.commands import dl + + +@click.command(context_settings=dict( + allow_extra_args=True, + ignore_unknown_options=True, + max_content_width=116, # max PEP8 line-width, -4 to adjust for initial indent +)) +@click.option("--debug", is_flag=True, default=False, + help="Enable DEBUG level logs on the console. This is always enabled for log files.") +def main(debug): + """ + vinetrimmer is the most convenient command-line program to + download videos from Widevine DRM-protected video platforms. + """ + LOG_FORMAT = "{asctime} [{levelname[0]}] {name} : {message}" + LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" + LOG_STYLE = "{" + + def log_exit(self, msg, *args, **kwargs): + self.critical(msg, *args, **kwargs) + sys.exit(1) + + logging.Logger.exit = log_exit + + os.makedirs(directories.logs, exist_ok=True) + logging.basicConfig( + level=logging.DEBUG, + format=LOG_FORMAT, + datefmt=LOG_DATE_FORMAT, + style=LOG_STYLE, + handlers=[logging.FileHandler( + os.path.join(directories.logs, filenames.log.format(time=datetime.now().strftime("%Y%m%d-%H%M%S"))), + encoding='utf-8' + )] + ) + + coloredlogs.install( + level=logging.DEBUG if debug else logging.INFO, + fmt=LOG_FORMAT, + datefmt=LOG_DATE_FORMAT, + style=LOG_STYLE, + handlers=[logging.StreamHandler()], + ) + + log = logging.getLogger("vt") + + log.info("vinetrimmer - Widevine DRM downloader and decrypter") + log.info(f"[Root Config] : {filenames.user_root_config}") + log.info(f"[Service Configs] : {directories.service_configs}") + log.info(f"[Cookies] : {directories.cookies}") + log.info(f"[CDM Devices] : {directories.devices}") + log.info(f"[Cache] : {directories.cache}") + log.info(f"[Logs] : {directories.logs}") + log.info(f"[Temp Files] : {directories.temp}") + log.info(f"[Downloads] : {directories.downloads}") + + os.environ['PATH'] = os.path.abspath('./binaries') + + if len(sys.argv) > 1 and sys.argv[1].lower() == "dl": + sys.argv.pop(1) + + dl() + +# D:\PlayReady-Amazon-Tool-main\.venv\Scripts\python.exe -X pycache_prefix=C:\Users\Aswin\AppData\Local\JetBrains\PyCharm2024.3\cpython-cache "C:/Program Files (x86)/JetBrains/PyCharm 2024.2.4/plugins/python-ce/helpers/pydev/pydevd.py" --port 42000 --module --multiprocess --save-signatures --qt-support=auto --file poetry run vt dl --no-cache --keys AMZN 0H7LY5ZKKBM1MIW0244WE9O2C4 +# Above seems to work +if __name__ == "__main__": + #sys.argv = ["vinetrimmer", "dl", "--no-cache", "--keys", "AMZN", "0H7LY5ZKKBM1MIW0244WE9O2C4"] + main() diff --git a/vinetrimmer/vinetrimmer.yml b/vinetrimmer/vinetrimmer.yml new file mode 100644 index 0000000..38918e5 --- /dev/null +++ b/vinetrimmer/vinetrimmer.yml @@ -0,0 +1,42 @@ +decrypter: 'packager' +tag: 'JEFF' +tag_sd: 'JEFF' + +aria2c: + file_allocation: 'prealloc' + + +cdm: + default: 'hisense_smarttv_he55a7000euwts_sl3000' + Amazon: 'hisense_smarttv_he55a7000euwts_sl3000' + +cdm_api: + - name: 'playready' + host: 'http://api.drmlab.io/prvinetrimmer' + key: 'MKBsQ04VbRkq0sMzPKoT5hnOazeH7rOA' + device: 'pr3k' + type: 'PLAYREADY' + system_id: 3000 + security_level: 1 + +credentials: + iTunes: 'Playreadydrm@proton.me' + +directories: + temp: '' + +headers: + User-Agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0' + + +output_template: + movies: '{title}.{year}.{quality}.{source}.WEB-DL.{audio}.{video}-{tag}' + series: '{title}.{season_episode}.{episode_name}.{quality}.{source}.WEB-DL.{audio}.{video}-{tag}' + use_last_audio: false + +profiles: + default: 'default' + Viki: false + + +nordvpn: {} diff --git a/vinetrimmer1.py b/vinetrimmer1.py new file mode 100644 index 0000000..d31f166 --- /dev/null +++ b/vinetrimmer1.py @@ -0,0 +1,78 @@ +import logging +import os +import sys +from datetime import datetime + +import click +import coloredlogs + +from vinetrimmer.config import directories, filenames # isort: split +from vinetrimmer.commands import dl + + +@click.command(context_settings=dict( + allow_extra_args=True, + ignore_unknown_options=True, + max_content_width=116, # max PEP8 line-width, -4 to adjust for initial indent +)) +@click.option("--debug", is_flag=True, default=False, + help="Enable DEBUG level logs on the console. This is always enabled for log files.") +def main(debug): + """ + vinetrimmer is the most convenient command-line program to + download videos from Widevine DRM-protected video platforms. + """ + LOG_FORMAT = "{asctime} [{levelname[0]}] {name} : {message}" + LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" + LOG_STYLE = "{" + + def log_exit(self, msg, *args, **kwargs): + self.critical(msg, *args, **kwargs) + sys.exit(1) + + logging.Logger.exit = log_exit + + os.makedirs(directories.logs, exist_ok=True) + logging.basicConfig( + level=logging.DEBUG, + format=LOG_FORMAT, + datefmt=LOG_DATE_FORMAT, + style=LOG_STYLE, + handlers=[logging.FileHandler( + os.path.join(directories.logs, filenames.log.format(time=datetime.now().strftime("%Y%m%d-%H%M%S"))), + encoding='utf-8' + )] + ) + + coloredlogs.install( + level=logging.DEBUG if debug else logging.INFO, + fmt=LOG_FORMAT, + datefmt=LOG_DATE_FORMAT, + style=LOG_STYLE, + handlers=[logging.StreamHandler()], + ) + + log = logging.getLogger("vt") + + log.info("vinetrimmer - Widevine DRM downloader and decrypter") + log.info(f"[Root Config] : {filenames.user_root_config}") + log.info(f"[Service Configs] : {directories.service_configs}") + log.info(f"[Cookies] : {directories.cookies}") + log.info(f"[CDM Devices] : {directories.devices}") + log.info(f"[Cache] : {directories.cache}") + log.info(f"[Logs] : {directories.logs}") + log.info(f"[Temp Files] : {directories.temp}") + log.info(f"[Downloads] : {directories.downloads}") + + os.environ['PATH'] = os.path.abspath('./binaries') + + if len(sys.argv) > 1 and sys.argv[1].lower() == "dl": + sys.argv.pop(1) + + dl() + +# D:\PlayReady-Amazon-Tool-main\.venv\Scripts\python.exe -X pycache_prefix=C:\Users\Aswin\AppData\Local\JetBrains\PyCharm2024.3\cpython-cache "C:/Program Files (x86)/JetBrains/PyCharm 2024.2.4/plugins/python-ce/helpers/pydev/pydevd.py" --port 42000 --module --multiprocess --save-signatures --qt-support=auto --file poetry run vt dl --no-cache --keys AMZN 0H7LY5ZKKBM1MIW0244WE9O2C4 +# Above seems to work +if __name__ == "__main__": + #sys.argv = ["vinetrimmer", "dl", "--no-cache", "--keys", "AMZN", "0H7LY5ZKKBM1MIW0244WE9O2C4"] + main() diff --git a/vt.py b/vt.py new file mode 100644 index 0000000..d31f166 --- /dev/null +++ b/vt.py @@ -0,0 +1,78 @@ +import logging +import os +import sys +from datetime import datetime + +import click +import coloredlogs + +from vinetrimmer.config import directories, filenames # isort: split +from vinetrimmer.commands import dl + + +@click.command(context_settings=dict( + allow_extra_args=True, + ignore_unknown_options=True, + max_content_width=116, # max PEP8 line-width, -4 to adjust for initial indent +)) +@click.option("--debug", is_flag=True, default=False, + help="Enable DEBUG level logs on the console. This is always enabled for log files.") +def main(debug): + """ + vinetrimmer is the most convenient command-line program to + download videos from Widevine DRM-protected video platforms. + """ + LOG_FORMAT = "{asctime} [{levelname[0]}] {name} : {message}" + LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" + LOG_STYLE = "{" + + def log_exit(self, msg, *args, **kwargs): + self.critical(msg, *args, **kwargs) + sys.exit(1) + + logging.Logger.exit = log_exit + + os.makedirs(directories.logs, exist_ok=True) + logging.basicConfig( + level=logging.DEBUG, + format=LOG_FORMAT, + datefmt=LOG_DATE_FORMAT, + style=LOG_STYLE, + handlers=[logging.FileHandler( + os.path.join(directories.logs, filenames.log.format(time=datetime.now().strftime("%Y%m%d-%H%M%S"))), + encoding='utf-8' + )] + ) + + coloredlogs.install( + level=logging.DEBUG if debug else logging.INFO, + fmt=LOG_FORMAT, + datefmt=LOG_DATE_FORMAT, + style=LOG_STYLE, + handlers=[logging.StreamHandler()], + ) + + log = logging.getLogger("vt") + + log.info("vinetrimmer - Widevine DRM downloader and decrypter") + log.info(f"[Root Config] : {filenames.user_root_config}") + log.info(f"[Service Configs] : {directories.service_configs}") + log.info(f"[Cookies] : {directories.cookies}") + log.info(f"[CDM Devices] : {directories.devices}") + log.info(f"[Cache] : {directories.cache}") + log.info(f"[Logs] : {directories.logs}") + log.info(f"[Temp Files] : {directories.temp}") + log.info(f"[Downloads] : {directories.downloads}") + + os.environ['PATH'] = os.path.abspath('./binaries') + + if len(sys.argv) > 1 and sys.argv[1].lower() == "dl": + sys.argv.pop(1) + + dl() + +# D:\PlayReady-Amazon-Tool-main\.venv\Scripts\python.exe -X pycache_prefix=C:\Users\Aswin\AppData\Local\JetBrains\PyCharm2024.3\cpython-cache "C:/Program Files (x86)/JetBrains/PyCharm 2024.2.4/plugins/python-ce/helpers/pydev/pydevd.py" --port 42000 --module --multiprocess --save-signatures --qt-support=auto --file poetry run vt dl --no-cache --keys AMZN 0H7LY5ZKKBM1MIW0244WE9O2C4 +# Above seems to work +if __name__ == "__main__": + #sys.argv = ["vinetrimmer", "dl", "--no-cache", "--keys", "AMZN", "0H7LY5ZKKBM1MIW0244WE9O2C4"] + main()